Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
node_modules/
dist/
bun.lockb
package-lock.json
.yarn/
*.tsbuildinfo

# OS
Expand Down
7 changes: 6 additions & 1 deletion packages/web/src/editor/canvas/elements/text-element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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}
Expand Down
82 changes: 74 additions & 8 deletions packages/web/src/editor/dock/dock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,59 @@ 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,
addBarcodeEl,
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<FlyoutKey>(null);
const [replaceDetail, setReplaceDetail] = useState<ReplaceIconDetail | null>(
null
);

const toggle = (key: Exclude<FlyoutKey, null>) =>
const toggle = (key: Exclude<FlyoutKey, null>) => {
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);
Expand All @@ -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<ReplaceIconDetail>;
setReplaceDetail(customEvent.detail || null);
setOpenFlyout("icons");
};
window.addEventListener("thermoprint:open-icons", handleOpenIcons);
return () =>
window.removeEventListener("thermoprint:open-icons", handleOpenIcons);
}, []);

return (
<>
{openFlyout && (
<div
className="fixed inset-0 z-20"
onClick={() => setOpenFlyout(null)}
onClick={() => {
setOpenFlyout(null);
setReplaceDetail(null);
}}
/>
)}
{openFlyout === "layers" && (
Expand All @@ -68,20 +101,35 @@ export function Dock() {
{openFlyout === "print" && (
<PrintSettingsFlyout onClose={() => setOpenFlyout(null)} />
)}
{openFlyout === "icons" && (
<IconsFlyout
onClose={() => {
setOpenFlyout(null);
setReplaceDetail(null);
}}
initialPrefix={replaceDetail?.initialPrefix}
targetElementId={replaceDetail?.targetElementId}
/>
)}
{openFlyout === "more-tools" && (
<div className="fixed inset-x-2 bottom-20 md:hidden bg-ink-850/95 backdrop-blur-sm border border-white/8 rounded-lg shadow-panel z-40 overflow-hidden">
<div className="grid grid-cols-4 gap-1 p-2">
{[
{ 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) => (
<button
key={t.label}
onClick={() => { t.fn(); setOpenFlyout(null); }}
onClick={() => {
t.fn();
if (t.label !== "Icons") setOpenFlyout(null);
}}
className="flex flex-col items-center gap-1 py-3 rounded-lg text-ink-200 hover:bg-ink-800 hover:text-ink-50 hover-fade"
>
<t.icon size={20} />
Expand All @@ -108,20 +156,34 @@ export function Dock() {
<div className="hidden md:contents">
<DockGroup label="Add">
<DockBtn icon={Type} label="Text" shortcut="T" onClick={addTextEl} />
<DockBtn icon={CalendarClock} label="Date" shortcut="D" onClick={addDateEl} />
<DockBtn icon={QrCode} label="QR" shortcut="Q" onClick={addQrEl} />
<DockBtn icon={Barcode} label="Barcode" shortcut="B" onClick={addBarcodeEl} />
<DockBtn icon={ImageIcon} label="Image" shortcut="I" onClick={addImageEl} />
<DockBtn
icon={Sticker}
label="Icons"
shortcut="C"
onClick={() => toggle("icons")}
active={openFlyout === "icons"}
/>
<DockBtn icon={Square} label="Rect" shortcut="R" onClick={addRectEl} />
<DockBtn icon={Minus} label="Line" shortcut="L" onClick={addLineEl} />
</DockGroup>
<DockDivider />
</div>

{/* Mobile: Text + Image + More */}
{/* Mobile: Text + Image + Icons + More */}
<div className="md:hidden contents">
<DockGroup label="Add">
<DockBtn icon={Type} label="Text" onClick={addTextEl} />
<DockBtn icon={ImageIcon} label="Image" onClick={addImageEl} />
<DockBtn
icon={Sticker}
label="Icons"
onClick={() => toggle("icons")}
active={openFlyout === "icons"}
/>
<DockBtn
icon={MoreHorizontal}
label="More"
Expand Down Expand Up @@ -165,9 +227,13 @@ export function Dock() {
<Kbd>⌘</Kbd>+<Kbd>scroll</Kbd> zoom
</span>
<span className="text-ink-700">•</span>
<span>
<button
onClick={() => useEditorV2Store.setState({ paletteOpen: true })}
className="hover:text-accent hover-fade flex items-center gap-1 cursor-pointer outline-none"
title="Click or press ⌘K to open Command Palette"
>
<Kbd>⌘K</Kbd> commands
</span>
</button>
</div>
</div>
</>
Expand Down
98 changes: 96 additions & 2 deletions packages/web/src/editor/inspector/sections/text-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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<string, unknown>) =>
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 (
<Section title="Text">
<Section title={isDate ? "Date" : "Text"}>
<Field label="Content">
<TextInput value={p.text || ""} onChange={(v) => update({ text: v })} />
<div title={isDate ? `Format codes:\nDD = day, MM = month, YYYY, YY = year, HH:mm = time\nMMMM, MMM = month name, dddd, ddd = weekday name\nFuture dates: add +days (e.g. DD+7, MM+1, YYYY+1)` : undefined}>
<TextInput
value={p.text || ""}
onChange={(v) => update({ text: v })}
placeholder={isDate ? "e.g. DD+7.MM.YYYY" : undefined}
/>
</div>
</Field>

{/* Date controls — only shown for date elements */}
{isDate && (
<div className="mt-1.5 space-y-1.5">
<Field label="Preset">
<Select
value={activePreset}
onChange={handlePresetChange}
options={DATE_PRESET_OPTIONS}
/>
</Field>
<div className="flex gap-1.5 items-center">
<div className="flex-1 min-w-0">
<Field label="Locale">
<div className="relative" title={`Language code: en, uk, de, etc.\nAffects month/day names and date order.`}>
<TextInput
value={p.dateLocale || ""}
onChange={(v) => update({ dateLocale: v || undefined })}
placeholder="auto"
/>
{p.dateLocale && (
<button
type="button"
onClick={() => update({ dateLocale: undefined })}
className="absolute right-1 top-1/2 -translate-y-1/2 w-5 h-5 flex items-center justify-center rounded text-ink-400 hover:text-ink-100 cursor-pointer"
title="Reset to auto (browser language)"
>
<X size={10} />
</button>
)}
</div>
</Field>
</div>
<button
type="button"
onClick={handleRefresh}
className="h-7 px-2.5 rounded-md bg-ink-800 border border-white/5 text-ink-300 hover:text-ink-50 hover:border-white/10 flex items-center gap-1.5 text-ui-xs font-medium cursor-pointer transition-colors shrink-0"
title="Re-evaluate all tokens to current date/time"
>
<RefreshCw size={12} />
<span>Refresh Dates</span>
</button>
</div>
</div>
)}

<div className="grid grid-cols-2 gap-1.5 mt-1.5">
<Field label="Size" mono>
<NumInput
Expand Down Expand Up @@ -95,6 +180,15 @@ export function TextSection({ element }: Props) {
<Italic size={14} />
</SegBtn>
</SegGroup>
<SegGroup>
<SegBtn
active={!!p.uppercase}
onClick={() => update({ uppercase: !p.uppercase })}
title="All Caps (TT)"
>
<span className="font-bold text-[11px] font-mono leading-none px-1">TT</span>
</SegBtn>
</SegGroup>
<SegGroup>
<SegBtn
active={p.align === "left"}
Expand Down
Loading