"use client"; import { type ReactNode } from "react"; import { Circle, Scan, SlidersHorizontal } from "lucide-react"; import { cn } from "@/lib/utils"; import { ColorField, NumberField, Panel, } from "@/components/slide-editor/shapes/ShapeToolbar"; import type { TemplateV2LayoutElement, TemplateV2LayoutToolbarBox, } from "@/components/slide-editor/layout/LayoutToolbar"; import { numericInputMode, preventInvalidNumberInput, sanitizeNumericInput, } from "@/components/slide-editor/toolbar/numericInput"; type RawRecord = Record; type ContainerPanelId = "fill" | "stroke" | "radius" | "padding" | "shadow"; const ALIGNMENT_MATRIX = [ { horizontal: "left", vertical: "top" }, { horizontal: "center", vertical: "top" }, { horizontal: "right", vertical: "top" }, { horizontal: "left", vertical: "middle" }, { horizontal: "center", vertical: "middle" }, { horizontal: "right", vertical: "middle" }, { horizontal: "left", vertical: "bottom" }, { horizontal: "center", vertical: "bottom" }, { horizontal: "right", vertical: "bottom" }, ] as const; function asRecord(value: unknown): RawRecord { return value && typeof value === "object" && !Array.isArray(value) ? (value as RawRecord) : {}; } function readNumber(value: unknown, fallback = 0) { return typeof value === "number" && Number.isFinite(value) ? value : fallback; } function readString(value: unknown, fallback: string) { return typeof value === "string" && value ? value : fallback; } function readColor(value: unknown, fallback: string) { const color = readString(value, fallback); return color.startsWith("#") ? color : `#${color}`; } function capitalize(value: string) { return value .replace("flex-", "") .replace(/(^|[-_])\w/g, (part) => part.replace(/[-_]/, "").toUpperCase()); } function clampNumber(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); } function ContainerControlButton({ children, className, open, title, onClick, }: { children: ReactNode; className?: string; open?: boolean; title: string; onClick: () => void; }) { return ( ); } function CompactNumberInput({ marker, min, max, step = 1, value, onCommit, label, }: { marker?: string; min?: number; max?: number; step?: number; value: number; onCommit: (value: number) => void; label: string; }) { const numericInputOptions = { allowDecimal: true, min, }; const commitNumber = (nextValue: number) => { if (!Number.isFinite(nextValue)) return; const bounded = clampNumber( nextValue, min ?? -Infinity, max ?? Infinity, ); onCommit(bounded); }; return ( ); } export function TemplateV2ContainerToolbarControls({ box, element, onChange, onToggle, openPanel, }: { box: TemplateV2LayoutToolbarBox; element: TemplateV2LayoutElement; onChange: (changes: RawRecord) => void; onToggle: (panel: ContainerPanelId) => void; openPanel: string | null; }) { const alignment = asRecord(element.alignment); const fill = asRecord(element.fill); const stroke = asRecord(element.stroke); const padding = asRecord(element.padding); const shadow = asRecord(element.shadow); const borderRadius = asRecord(element.border_radius); const horizontal = readString(alignment.horizontal, "left"); const vertical = readString(alignment.vertical, "top"); const radius = typeof element.border_radius === "number" ? element.border_radius : readNumber(borderRadius.radius, readNumber(borderRadius.tl)); const maxRadius = Math.max(0, Math.min(box.width, box.height) / 2); const fillColor = readColor(fill.color, "#4A6FF3"); const strokeColor = readColor(stroke.color, "#1A1F36"); const paddingX = readNumber( padding.x, (readNumber(padding.left) + readNumber(padding.right)) / 2, ); const paddingY = readNumber( padding.y, (readNumber(padding.top) + readNumber(padding.bottom)) / 2, ); const updatePaddingX = (value: number) => onChange({ padding: { ...padding, x: value, left: value, right: value, }, }); const updatePaddingY = (value: number) => onChange({ padding: { ...padding, y: value, top: value, bottom: value, }, }); return ( <>
onToggle("fill")} className="w-7 px-0" > {openPanel === "fill" ? ( onChange({ fill: { ...fill, color } })} /> onChange({ fill: { ...fill, opacity } })} /> ) : null}
onToggle("stroke")} className="w-7 px-0" > {openPanel === "stroke" ? ( onChange({ stroke: { ...stroke, color } })} /> onChange({ stroke: { ...stroke, width } })} /> onChange({ stroke: { ...stroke, opacity } })} /> ) : null}
onToggle("radius")} className="w-7 px-0" > {openPanel === "radius" ? (
Border Radius
onChange({ border_radius: { tl: value, tr: value, br: value, bl: value }, }) } />
{[0, 8, 16, 24].map((preset) => ( ))}
) : null}
onToggle("shadow")} className="px-3" > Shadow {openPanel === "shadow" ? ( onChange({ shadow: { ...shadow, offset_x, opacity: Math.max(0.05, readNumber(shadow.opacity, 0.24)), }, }) } /> onChange({ shadow: { ...shadow, offset_y, opacity: Math.max(0.05, readNumber(shadow.opacity, 0.24)), }, }) } /> onChange({ shadow: { ...shadow, blur } })} /> onChange({ shadow: { ...shadow, color } })} /> onChange({ shadow: { ...shadow, opacity: clampNumber(opacityPercent, 0, 100) / 100, }, }) } /> ) : null}
onToggle("padding")} className="w-7 px-0" > {openPanel === "padding" ? (
Alignment
{ALIGNMENT_MATRIX.map((point) => { const selected = point.horizontal === horizontal && point.vertical === vertical; return ( ); })}
) : null}
); }