ultisuite-client/components/drive/richtext/docs-page-setup-dialog.tsx
R3D347HR4Y 8e420509a8
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
imports docx 1
2026-06-10 00:27:44 +02:00

304 lines
10 KiB
TypeScript

"use client"
import { useEffect, useMemo, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
DRIVE_BTN_GHOST,
DRIVE_BTN_PRIMARY,
DRIVE_DIALOG_CONTENT,
DRIVE_DIALOG_FOOTER,
DRIVE_DIALOG_OVERLAY,
DRIVE_TEXT_SECONDARY,
DRIVE_TEXT_TITLE,
} from "@/lib/drive/drive-dialog-styles"
import {
buildPageSetupFromDraft,
draftFromPageSetup,
formatPaperSizeLabel,
type DocPageSetup,
type PageSetupDraft,
} from "@/lib/drive/doc-page-setup"
import {
pageSetupDraftsEqual,
readUserPageSetupDefaults,
saveUserPageSetupDefaults,
} from "@/lib/drive/docs-page-defaults"
import { PAGE_FORMATS, type PageFormatId } from "@/lib/drive/page-formats"
import { cn } from "@/lib/utils"
type MarginSide = keyof PageSetupDraft["marginsCm"]
const MARGIN_FIELDS: { key: MarginSide; label: string }[] = [
{ key: "top", label: "Haut" },
{ key: "bottom", label: "Bas" },
{ key: "left", label: "Gauche" },
{ key: "right", label: "Droite" },
]
const FIELD_LABEL = "text-xs font-medium text-muted-foreground"
const FIELD_CONTROL = "h-9"
function parseMarginInput(raw: string): number {
const normalized = raw.replace(",", ".").trim()
if (!normalized) return 0
const value = Number.parseFloat(normalized)
return Number.isFinite(value) ? value : 0
}
export function DocsPageSetupDialog({
open,
onOpenChange,
pageSetup,
fallbackFormatId,
onApply,
}: {
open: boolean
onOpenChange: (open: boolean) => void
pageSetup: DocPageSetup | null
fallbackFormatId: PageFormatId
onApply: (setup: DocPageSetup) => void
}) {
const [draft, setDraft] = useState<PageSetupDraft>(() =>
draftFromPageSetup(pageSetup, fallbackFormatId)
)
const [savedDefaults, setSavedDefaults] = useState<PageSetupDraft>(() =>
readUserPageSetupDefaults(fallbackFormatId)
)
useEffect(() => {
if (open) {
setDraft(draftFromPageSetup(pageSetup, fallbackFormatId))
setSavedDefaults(readUserPageSetupDefaults(fallbackFormatId))
}
}, [open, pageSetup, fallbackFormatId])
const matchesSavedDefaults = useMemo(
() => pageSetupDraftsEqual(draft, savedDefaults),
[draft, savedDefaults]
)
const handleApply = () => {
onApply(buildPageSetupFromDraft(draft, pageSetup))
onOpenChange(false)
}
const handleSaveDefaults = () => {
saveUserPageSetupDefaults(draft)
setSavedDefaults({ ...draft })
}
const updateMargin = (key: MarginSide, raw: string) => {
setDraft((prev) => ({
...prev,
marginsCm: { ...prev.marginsCm, [key]: parseMarginInput(raw) },
}))
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
overlayClassName={DRIVE_DIALOG_OVERLAY}
className={cn(
DRIVE_DIALOG_CONTENT,
"h-auto max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] gap-0 overflow-y-auto p-0 sm:max-w-[480px]"
)}
>
<DialogHeader className="space-y-0 px-5 py-3 text-left">
<DialogTitle className={cn("text-lg font-normal", DRIVE_TEXT_TITLE)}>
Configuration de la page
</DialogTitle>
<DialogDescription className="sr-only">
Format, orientation, couleur et marges du document.
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="pages" className="gap-0">
<div className="px-5">
<TabsList className="h-9 w-full gap-0 rounded-lg bg-[#f1f3f4] p-1 dark:bg-muted">
<TabsTrigger
value="pages"
className="flex-1 rounded-md px-4 text-sm font-medium text-muted-foreground shadow-none data-[state=active]:bg-white data-[state=active]:text-[#1a73e8] data-[state=active]:shadow-sm dark:data-[state=active]:bg-background dark:data-[state=active]:text-[#8ab4f8]"
>
Pages
</TabsTrigger>
<TabsTrigger
value="pageless"
disabled
className="flex-1 rounded-md px-4 text-sm font-medium text-muted-foreground/50 shadow-none"
>
Sans pages
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="pages" className="mt-0 px-5 py-3">
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<div className="col-span-2 space-y-1">
<Label className={FIELD_LABEL}>Appliquer à</Label>
<Select value="document" disabled>
<SelectTrigger className={FIELD_CONTROL}>
<SelectValue>Au document entier</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="document">Au document entier</SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-2 space-y-1">
<Label className={FIELD_LABEL}>Orientation</Label>
<RadioGroup
value={draft.orientation}
onValueChange={(value: "portrait" | "landscape") =>
setDraft((prev) => ({ ...prev, orientation: value }))
}
className="flex h-9 items-center gap-5"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="portrait" id="docs-page-orientation-portrait" />
<Label htmlFor="docs-page-orientation-portrait" className="text-sm font-normal">
Portrait
</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="landscape" id="docs-page-orientation-landscape" />
<Label htmlFor="docs-page-orientation-landscape" className="text-sm font-normal">
Paysage
</Label>
</div>
</RadioGroup>
</div>
<div className="space-y-1">
<Label className={FIELD_LABEL}>Format de papier</Label>
<Select
value={draft.formatId}
onValueChange={(value: PageFormatId) =>
setDraft((prev) => ({ ...prev, formatId: value }))
}
>
<SelectTrigger className={FIELD_CONTROL}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PAGE_FORMATS.map((format) => (
<SelectItem key={format.id} value={format.id}>
{formatPaperSizeLabel(format)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className={FIELD_LABEL}>Couleur de la page</Label>
<div className="flex h-9 items-center">
<label className="relative inline-flex h-8 w-8 cursor-pointer items-center justify-center">
<span
className="block h-7 w-7 rounded-full border border-[#dadce0] dark:border-border"
style={{ backgroundColor: draft.pageColor }}
aria-hidden
/>
<input
type="color"
value={draft.pageColor}
onChange={(event) =>
setDraft((prev) => ({ ...prev, pageColor: event.target.value }))
}
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
aria-label="Couleur de la page"
/>
</label>
</div>
</div>
<div className="col-span-2 space-y-1">
<Label className={FIELD_LABEL}>Marges (centimètres)</Label>
<div className="grid grid-cols-4 gap-2">
{MARGIN_FIELDS.map(({ key, label }) => (
<div key={key} className="space-y-1">
<Label
htmlFor={`docs-page-margin-${key}`}
className="text-[11px] font-normal text-muted-foreground"
>
{label}
</Label>
<Input
id={`docs-page-margin-${key}`}
type="text"
inputMode="decimal"
className={cn(FIELD_CONTROL, "px-2 text-center text-sm")}
value={String(draft.marginsCm[key])}
onChange={(event) => updateMargin(key, event.target.value)}
/>
</div>
))}
</div>
</div>
</div>
</TabsContent>
</Tabs>
<DialogFooter
className={cn(
DRIVE_DIALOG_FOOTER,
"flex flex-wrap items-center justify-end gap-2 px-5 py-3 sm:justify-end"
)}
>
{matchesSavedDefaults ? (
<span className={cn("order-1 text-sm", DRIVE_TEXT_SECONDARY)}>
Valeurs par défaut enregistrées
</span>
) : (
<Button
type="button"
variant="link"
className="order-1 h-8 px-0 text-sm font-normal text-[#1a73e8] hover:text-[#174ea6]"
onClick={handleSaveDefaults}
>
Enregistrer comme valeurs par défaut
</Button>
)}
<Button
type="button"
variant="ghost"
className={cn(
DRIVE_BTN_GHOST,
"order-2 h-8 rounded-full px-4 text-[#1a73e8] hover:bg-transparent hover:text-[#174ea6]"
)}
onClick={() => onOpenChange(false)}
>
Annuler
</Button>
<Button
type="button"
className={cn(DRIVE_BTN_PRIMARY, "order-3 h-8 rounded-full px-5")}
onClick={handleApply}
>
OK
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}