From 969739899433870acb5ee6e96c9612fde38b6582 Mon Sep 17 00:00:00 2001 From: phanex Date: Sun, 30 Aug 2026 00:59:39 +0200 Subject: [PATCH] feat(web): add Date/Time dock tool with locale-aware presets and refresh --- .gitignore | 2 + .../editor/canvas/elements/text-element.tsx | 7 +- packages/web/src/editor/dock/dock.tsx | 82 ++++++++- .../inspector/sections/text-section.tsx | 98 ++++++++++- packages/web/src/lib/date-format.ts | 158 ++++++++++++++++++ packages/web/src/lib/keyboard.ts | 29 ++++ 6 files changed, 365 insertions(+), 11 deletions(-) create mode 100644 packages/web/src/lib/date-format.ts diff --git a/.gitignore b/.gitignore index 25c92e5..8aa1f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ node_modules/ dist/ bun.lockb +package-lock.json +.yarn/ *.tsbuildinfo # OS diff --git a/packages/web/src/editor/canvas/elements/text-element.tsx b/packages/web/src/editor/canvas/elements/text-element.tsx index 44cbce7..bffa5e5 100644 --- a/packages/web/src/editor/canvas/elements/text-element.tsx +++ b/packages/web/src/editor/canvas/elements/text-element.tsx @@ -4,6 +4,7 @@ import type Konva from "konva"; import type { BaseElement } from "../../../store/editor-store.ts"; import { useEditorV2Store } from "../../../store/editor-store.ts"; import { ElementWrapper } from "./element-wrapper.tsx"; +import { getDisplayText } from "../../../lib/date-format.ts"; interface Props { element: BaseElement; @@ -27,8 +28,12 @@ export function TextElement({ element, isSelected }: Props) { fill?: string; align?: string; italic?: boolean; + datePreset?: string; + dateLocale?: string; }; + const displayText = getDisplayText(p.text || "Text", p.datePreset as any, p.dateLocale); + const fontStyle = [p.italic ? "italic" : "", p.fontWeight && p.fontWeight >= 700 ? "bold" : ""] .filter(Boolean) @@ -154,7 +159,7 @@ export function TextElement({ element, isSelected }: Props) { y={element.y} width={element.width} rotation={element.rotation} - text={p.text || "Text"} + text={displayText} fontSize={p.fontSize || 18} fontFamily={p.fontFamily || "Inter"} fontStyle={fontStyle} diff --git a/packages/web/src/editor/dock/dock.tsx b/packages/web/src/editor/dock/dock.tsx index cb83c72..e0610e8 100644 --- a/packages/web/src/editor/dock/dock.tsx +++ b/packages/web/src/editor/dock/dock.tsx @@ -4,17 +4,20 @@ import { QrCode, Barcode, ImageIcon, + Sticker, Square, Minus, Layers, Folder, Settings, MoreHorizontal, + CalendarClock, } from "lucide-react"; import { DockBtn } from "./dock-btn.tsx"; import { DockGroup } from "./dock-group.tsx"; import { DockDivider } from "./dock-divider.tsx"; import { Kbd } from "./kbd.tsx"; +import { useEditorV2Store } from "../../store/editor-store.ts"; import { addTextEl, addQrEl, @@ -22,23 +25,38 @@ import { addImageEl, addRectEl, addLineEl, + addDateEl, } from "../../lib/keyboard.ts"; import { LayersFlyout } from "./flyouts/layers-flyout.tsx"; import { LibraryFlyout } from "./flyouts/library-flyout.tsx"; import { PrintSettingsFlyout } from "./flyouts/print-settings-flyout.tsx"; +import { IconsFlyout } from "./flyouts/icons-flyout.tsx"; -type FlyoutKey = "layers" | "library" | "print" | "more-tools" | null; +type FlyoutKey = "layers" | "library" | "print" | "icons" | "more-tools" | null; + +interface ReplaceIconDetail { + initialPrefix?: string | null; + targetElementId?: string | null; +} export function Dock() { const [openFlyout, setOpenFlyout] = useState(null); + const [replaceDetail, setReplaceDetail] = useState( + null + ); - const toggle = (key: Exclude) => + const toggle = (key: Exclude) => { + setReplaceDetail(null); setOpenFlyout((cur) => (cur === key ? null : key)); + }; // Close flyout on Escape useEffect(() => { const h = (e: KeyboardEvent) => { - if (e.key === "Escape") setOpenFlyout(null); + if (e.key === "Escape") { + setOpenFlyout(null); + setReplaceDetail(null); + } }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); @@ -51,12 +69,27 @@ export function Dock() { return () => window.removeEventListener("thermoprint:open-library", h); }, []); + // Listen for "open icons" & "replace icon" events + useEffect(() => { + const handleOpenIcons = (e: Event) => { + const customEvent = e as CustomEvent; + setReplaceDetail(customEvent.detail || null); + setOpenFlyout("icons"); + }; + window.addEventListener("thermoprint:open-icons", handleOpenIcons); + return () => + window.removeEventListener("thermoprint:open-icons", handleOpenIcons); + }, []); + return ( <> {openFlyout && (
setOpenFlyout(null)} + onClick={() => { + setOpenFlyout(null); + setReplaceDetail(null); + }} /> )} {openFlyout === "layers" && ( @@ -68,20 +101,35 @@ export function Dock() { {openFlyout === "print" && ( setOpenFlyout(null)} /> )} + {openFlyout === "icons" && ( + { + setOpenFlyout(null); + setReplaceDetail(null); + }} + initialPrefix={replaceDetail?.initialPrefix} + targetElementId={replaceDetail?.targetElementId} + /> + )} {openFlyout === "more-tools" && (
{[ { icon: Type, label: "Text", fn: addTextEl }, + { icon: CalendarClock, label: "Date", fn: addDateEl }, { icon: QrCode, label: "QR Code", fn: addQrEl }, { icon: Barcode, label: "Barcode", fn: addBarcodeEl }, { icon: ImageIcon, label: "Image", fn: addImageEl }, + { icon: Sticker, label: "Icons", fn: () => setOpenFlyout("icons") }, { icon: Square, label: "Rectangle", fn: addRectEl }, { icon: Minus, label: "Line", fn: addLineEl }, ].map((t) => (
diff --git a/packages/web/src/editor/inspector/sections/text-section.tsx b/packages/web/src/editor/inspector/sections/text-section.tsx index 77631ae..abe53d3 100644 --- a/packages/web/src/editor/inspector/sections/text-section.tsx +++ b/packages/web/src/editor/inspector/sections/text-section.tsx @@ -4,6 +4,8 @@ import { AlignLeft, AlignCenter, AlignRight, + RefreshCw, + X, } from "lucide-react"; import type { BaseElement } from "../../../store/editor-store.ts"; import { useEditorV2Store } from "../../../store/editor-store.ts"; @@ -17,6 +19,11 @@ import { SegGroup, ColorInput, } from "../fields.tsx"; +import { + type DatePreset, + DATE_PRESET_OPTIONS, + PRESET_FORMATS, +} from "../../../lib/date-format.ts"; interface Props { element: BaseElement; @@ -34,16 +41,94 @@ export function TextSection({ element }: Props) { fill?: string; align?: string; italic?: boolean; + uppercase?: boolean; + datePreset?: DatePreset; + dateLocale?: string; }; const update = (patch: Record) => updateElement(element.id, { props: patch }); + const isDate = !!p.datePreset; + + // Compute which preset matches the current text (auto-switch to Custom if edited) + const activePreset: DatePreset = (() => { + if (!p.datePreset) return "date"; + const text = p.text || ""; + for (const [key, fmt] of Object.entries(PRESET_FORMATS)) { + if (text === fmt) return key as DatePreset; + } + return "custom"; + })(); + + const handlePresetChange = (v: string) => { + const preset = v as DatePreset; + if (preset === "custom") return; // Can't select custom — it's computed + update({ datePreset: preset, text: PRESET_FORMATS[preset] }); + }; + + const handleRefresh = () => { + // Force canvas re-render with current timestamp + update({ _ts: Date.now() }); + }; + return ( -
+
- update({ text: v })} /> +
+ update({ text: v })} + placeholder={isDate ? "e.g. DD+7.MM.YYYY" : undefined} + /> +
+ + {/* Date controls — only shown for date elements */} + {isDate && ( +
+ +