304 lines
10 KiB
TypeScript
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>
|
|
)
|
|
}
|