300 lines
9.7 KiB
TypeScript
300 lines
9.7 KiB
TypeScript
"use client"
|
|
|
|
import type { Editor } from "@tiptap/react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import {
|
|
buildGradientCss,
|
|
DOCS_GRAPHIC_PLACEMENT_LABELS,
|
|
DOCS_GRAPHIC_WRAP_LABELS,
|
|
parseGraphicAttrs,
|
|
type DocsGraphicPlacement,
|
|
type DocsGraphicWrap,
|
|
} from "@/lib/drive/docs-graphic-types"
|
|
import { readGraphicToolbarActive } from "@/components/drive/richtext/docs-graphic-toolbar-menu"
|
|
|
|
export function DocsGraphicOptionsPanel({
|
|
editor,
|
|
disabled,
|
|
open,
|
|
onOpenChange,
|
|
}: {
|
|
editor: Editor | null
|
|
disabled?: boolean
|
|
open?: boolean
|
|
onOpenChange?: (open: boolean) => void
|
|
}) {
|
|
if (!editor || !readGraphicToolbarActive(editor)) return null
|
|
|
|
const name = editor.isActive("docsInlineGraphic") ? "docsInlineGraphic" : "docsGraphic"
|
|
const attrs = parseGraphicAttrs(editor.getAttributes(name) as Record<string, unknown>)
|
|
|
|
const update = (patch: Record<string, unknown>) => {
|
|
editor.chain().focus().updateDocsGraphic(patch).run()
|
|
}
|
|
|
|
const numField = (
|
|
label: string,
|
|
key: "width" | "height" | "x" | "y" | "rotationDeg",
|
|
step = 1
|
|
) => (
|
|
<div className="grid gap-1">
|
|
<Label className="text-xs text-muted-foreground">{label}</Label>
|
|
<Input
|
|
type="number"
|
|
className="h-8"
|
|
disabled={disabled}
|
|
value={attrs[key]}
|
|
step={step}
|
|
onChange={(e) => update({ [key]: Number(e.target.value) || 0 })}
|
|
/>
|
|
</div>
|
|
)
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={onOpenChange}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="docs-toolbar-btn h-7 shrink-0 px-2 text-xs"
|
|
disabled={disabled}
|
|
>
|
|
Options
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-72 space-y-3 p-3" align="start">
|
|
<p className="text-sm font-medium">
|
|
{attrs.graphicType === "image"
|
|
? "Options image"
|
|
: attrs.graphicType === "draw"
|
|
? "Options dessin"
|
|
: attrs.graphicType === "shape"
|
|
? "Options forme"
|
|
: "Options dégradé"}
|
|
</p>
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{numField("Largeur (px)", "width")}
|
|
{numField("Hauteur (px)", "height")}
|
|
{numField("X", "x")}
|
|
{numField("Y", "y")}
|
|
{numField("Rotation (°)", "rotationDeg")}
|
|
</div>
|
|
|
|
<div className="grid gap-1">
|
|
<Label className="text-xs text-muted-foreground">Habillage</Label>
|
|
<Select
|
|
disabled={disabled}
|
|
value={attrs.wrap}
|
|
onValueChange={(v) => update({ wrap: v as DocsGraphicWrap })}
|
|
>
|
|
<SelectTrigger className="h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(Object.keys(DOCS_GRAPHIC_WRAP_LABELS) as DocsGraphicWrap[]).map((wrap) => (
|
|
<SelectItem key={wrap} value={wrap}>
|
|
{DOCS_GRAPHIC_WRAP_LABELS[wrap]}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="grid gap-1">
|
|
<Label className="text-xs text-muted-foreground">Placement</Label>
|
|
<Select
|
|
disabled={disabled}
|
|
value={attrs.placement}
|
|
onValueChange={(v) => update({ placement: v as DocsGraphicPlacement })}
|
|
>
|
|
<SelectTrigger className="h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(Object.keys(DOCS_GRAPHIC_PLACEMENT_LABELS) as DocsGraphicPlacement[]).map(
|
|
(placement) => (
|
|
<SelectItem key={placement} value={placement}>
|
|
{DOCS_GRAPHIC_PLACEMENT_LABELS[placement]}
|
|
</SelectItem>
|
|
)
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{attrs.graphicType === "shape" ? (
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="grid gap-1">
|
|
<Label className="text-xs text-muted-foreground">Remplissage</Label>
|
|
<Input
|
|
type="color"
|
|
className="h-8 p-1"
|
|
disabled={disabled}
|
|
value={attrs.fill}
|
|
onChange={(e) => update({ fill: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-1">
|
|
<Label className="text-xs text-muted-foreground">Contour</Label>
|
|
<Input
|
|
type="color"
|
|
className="h-8 p-1"
|
|
disabled={disabled}
|
|
value={attrs.stroke}
|
|
onChange={(e) => update({ stroke: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="col-span-2 grid gap-1">
|
|
<Label className="text-xs text-muted-foreground">Épaisseur contour</Label>
|
|
<Input
|
|
type="number"
|
|
className="h-8"
|
|
disabled={disabled}
|
|
min={0}
|
|
value={attrs.strokeWidth}
|
|
onChange={(e) => update({ strokeWidth: Number(e.target.value) || 0 })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{attrs.graphicType === "gradient" ? (
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="grid gap-1">
|
|
<Label className="text-xs text-muted-foreground">Type</Label>
|
|
<Select
|
|
disabled={disabled}
|
|
value={attrs.gradientType}
|
|
onValueChange={(v) => {
|
|
const gradientType = v as "linear" | "radial"
|
|
update({
|
|
gradientType,
|
|
gradientCss: buildGradientCss(
|
|
attrs.gradientAngle,
|
|
attrs.gradientColor1,
|
|
attrs.gradientColor2,
|
|
gradientType
|
|
),
|
|
})
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="linear">Linéaire</SelectItem>
|
|
<SelectItem value="radial">Radial</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="grid gap-1">
|
|
<Label className="text-xs text-muted-foreground">Aperçu</Label>
|
|
<div
|
|
className="h-8 rounded-md border border-border"
|
|
style={{ background: attrs.gradientCss }}
|
|
aria-hidden
|
|
/>
|
|
</div>
|
|
<div className="grid gap-1">
|
|
<Label className="text-xs text-muted-foreground">Couleur 1</Label>
|
|
<Input
|
|
type="color"
|
|
className="h-8 p-1"
|
|
disabled={disabled}
|
|
value={attrs.gradientColor1}
|
|
onChange={(e) => {
|
|
const gradientColor1 = e.target.value
|
|
update({
|
|
gradientColor1,
|
|
gradientCss: buildGradientCss(
|
|
attrs.gradientAngle,
|
|
gradientColor1,
|
|
attrs.gradientColor2,
|
|
attrs.gradientType
|
|
),
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-1">
|
|
<Label className="text-xs text-muted-foreground">Couleur 2</Label>
|
|
<Input
|
|
type="color"
|
|
className="h-8 p-1"
|
|
disabled={disabled}
|
|
value={attrs.gradientColor2}
|
|
onChange={(e) => {
|
|
const gradientColor2 = e.target.value
|
|
update({
|
|
gradientColor2,
|
|
gradientCss: buildGradientCss(
|
|
attrs.gradientAngle,
|
|
attrs.gradientColor1,
|
|
gradientColor2,
|
|
attrs.gradientType
|
|
),
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
{attrs.gradientType === "linear" ? (
|
|
<div className="col-span-2 grid gap-1">
|
|
<Label className="text-xs text-muted-foreground">Angle (°)</Label>
|
|
<Input
|
|
type="number"
|
|
className="h-8"
|
|
disabled={disabled}
|
|
value={attrs.gradientAngle}
|
|
onChange={(e) => {
|
|
const gradientAngle = Number(e.target.value) || 0
|
|
update({
|
|
gradientAngle,
|
|
gradientCss: buildGradientCss(
|
|
gradientAngle,
|
|
attrs.gradientColor1,
|
|
attrs.gradientColor2,
|
|
attrs.gradientType
|
|
),
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
|
|
{attrs.graphicType === "image" ? (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full"
|
|
disabled={disabled}
|
|
onClick={() =>
|
|
update({ cropX: 0, cropY: 0, cropWidth: 1, cropHeight: 1 })
|
|
}
|
|
>
|
|
Réinitialiser le recadrage
|
|
</Button>
|
|
) : null}
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|