diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index 7956a77b..e4e56884 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -26,6 +26,7 @@ import { DocumentArrowDownIcon, TableCellsIcon, PrinterIcon, + Cog6ToothIcon, PaperAirplaneIcon, GlobeAltIcon, XMarkIcon, @@ -34,6 +35,7 @@ import { } from "@heroicons/react/16/solid"; import { useLabelStore, useHistory, selectLabelaryNoticeRequired } from "../store/labelStore"; import { LabelaryNoticeModal } from "./Output/LabelaryNoticeModal"; +import { PrinterSettingsModal } from "./PrinterSettings/PrinterSettingsModal"; import { localeNames } from "../locales"; import type { LocaleCode } from "../locales"; import { mmToUnit } from "../lib/units"; @@ -51,6 +53,7 @@ export function AppShell() { const pages = useLabelStore((s) => s.pages); const selectObject = useLabelStore((s) => s.selectObject); const addPage = useLabelStore((s) => s.addPage); + const setPrinterSettingsTab = useLabelStore((s) => s.setPrinterSettingsTab); const locale = useLabelStore((s) => s.locale); const setLocale = useLabelStore((s) => s.setLocale); const theme = useLabelStore((s) => s.theme); @@ -205,6 +208,13 @@ export function AppShell() { {t.app.importZpl} + + setPrinterSettingsTab("mediaFeed")} + > + {t.printerSettings.open} + )} + ); } diff --git a/src/components/PrinterSettings/MediaFeedTab.tsx b/src/components/PrinterSettings/MediaFeedTab.tsx new file mode 100644 index 00000000..3569efc4 --- /dev/null +++ b/src/components/PrinterSettings/MediaFeedTab.tsx @@ -0,0 +1,176 @@ +import { useId } from "react"; +import { useT } from "../../lib/useT"; +import { useLabelStore } from "../../store/labelStore"; +import { inputCls, labelCls } from "../Properties/styles"; +import { + MEDIA_FEED_VALUES, + MEDIA_TRACKING_VALUES, + isMediaFeedMode, + isMediaTracking, + type MediaFeedMode, + type MediaTracking, +} from "../../types/ObjectType"; +import { ZplCheckbox, ZplCommandLabel, ZplField } from "./zplFieldPrimitives"; + +type LocMediaFeed = ReturnType["printerSettings"]["mediaFeed"]; + +/** Static lookup table from enum value to the locale-key that + * describes it. Compile-time check via `satisfies` guarantees + * every enum value has a matching key and every key actually + * exists on the locale block, so a missing translation surfaces + * as a TS error rather than `undefined` at runtime. */ +const TRACKING_LABEL_KEYS = { + N: "mediaTrackingN", + Y: "mediaTrackingY", + W: "mediaTrackingW", + M: "mediaTrackingM", + A: "mediaTrackingA", +} as const satisfies Record; + +const FEED_LABEL_KEYS = { + F: "mediaFeedF", + C: "mediaFeedC", + L: "mediaFeedL", + N: "mediaFeedN", + S: "mediaFeedS", +} as const satisfies Record; + +/** Width preset for short-number inputs in the modal (max printer + * ^ML value is ~32000 dots, so 5 digits cover every case). Same + * visual size that every numeric setting in the modal will reuse. */ +const SHORT_NUMERIC_INPUT_WIDTH = "w-32"; + +/** Tab 1 of the Printer Settings Modal. All four fields write to + * the shared `labelConfig` store; the ZPL generator emits the + * corresponding ^MN / ^ML / ^MF / ^XB commands in the per-label + * header section. */ +export function MediaFeedTab() { + const t = useT(); + const label = useLabelStore((s) => s.label); + const setLabelConfig = useLabelStore((s) => s.setLabelConfig); + const loc = t.printerSettings.mediaFeed; + const trackingId = useId(); + const lengthId = useId(); + + return ( +
+ + + + + + + + {/* Compact input plus unit suffix: a short number in a wide + field reads as overengineered. The suffix fills the + remaining row width semantically rather than leaving + dead space. */} +
+
+ { + const n = Number.parseInt(e.target.value, 10); + setLabelConfig({ + maxLabelLength: Number.isFinite(n) && n > 0 ? n : undefined, + }); + }} + /> +
+ {t.printerSettings.dotsUnit} +
+
+ + {/* ^MF carries two positional params, so the row's "control" + slot is a 2-col grid instead of a single input. Same field + shape as the other commands (label + control), not an + indented sub-section. The indent convention is reserved + for checkbox-gated sub-settings (see FELDBLOCK), not for + commands with multiple params. */} + + +
+ setLabelConfig({ mediaFeedPowerUp: v })} + placeholder={t.printerSettings.defaultOption} + optionLabel={(m) => loc[FEED_LABEL_KEYS[m]]} + /> + setLabelConfig({ mediaFeedHeadClose: v })} + placeholder={t.printerSettings.defaultOption} + optionLabel={(m) => loc[FEED_LABEL_KEYS[m]]} + /> +
+
+ + setLabelConfig({ suppressBackfeed: v ? true : undefined })} + /> +
+ ); +} + +interface FeedSelectProps { + label: string; + value: MediaFeedMode | undefined; + onChange: (v: MediaFeedMode | undefined) => void; + placeholder: string; + optionLabel: (m: MediaFeedMode) => string; +} + +/** Inner select-with-ZPL-label helper. Extracted so the two ^MF + * positional params don't repeat the same select scaffolding. + * Uses `useId` instead of caller-supplied IDs so multi-instance + * mounting can't collide. */ +function FeedSelect({ label, value, onChange, placeholder, optionLabel }: FeedSelectProps) { + const id = useId(); + return ( +
+ + +
+ ); +} diff --git a/src/components/PrinterSettings/PrinterSettingsModal.tsx b/src/components/PrinterSettings/PrinterSettingsModal.tsx new file mode 100644 index 00000000..1f4f254c --- /dev/null +++ b/src/components/PrinterSettings/PrinterSettingsModal.tsx @@ -0,0 +1,140 @@ +import { useId, type FC } from "react"; +import { XMarkIcon } from "@heroicons/react/16/solid"; +import { useT } from "../../lib/useT"; +import { useLabelStore, type PrinterSettingsTab } from "../../store/labelStore"; +import { DialogShell } from "../ui/DialogShell"; +import { MediaFeedTab } from "./MediaFeedTab"; + +/** Tab IDs in display order. The "is this tab shipped?" state is + * derived from TAB_COMPONENTS membership below, so adding a tab + * is a one-line registry entry plus the locale strings — no dual + * source of truth to keep in sync. */ +const TABS: readonly PrinterSettingsTab[] = [ + "mediaFeed", + "printQuality", + "clockTime", + "encodingLanguage", + "identity", +]; + +/** Per-tab content registry. Tabs absent here render as disabled + * WIP rail entries; the rail prevents selection so the modal + * never has to handle a missing-component branch. Annotated + * (not `satisfies`) so `TAB_COMPONENTS[tab]` returns `FC | + * undefined` for any tab id without a narrowing cast. */ +const TAB_COMPONENTS: Partial> = { + mediaFeed: MediaFeedTab, +}; + +/** Modal-box class kept as a constant rather than inline so the + * width / shadow tuning is named and stays consistent if other + * modals adopt the same shell. */ +const MODAL_BOX_CLS = + "bg-surface border border-border rounded-lg shadow-2xl " + + "w-[720px] max-w-[95vw] max-h-[85vh] flex flex-col overflow-hidden"; + +/** Printer Settings modal — five tabs of pure-ZPL printer config. + * Vertical tab rail scales to N tabs and mirrors the + * Properties-Panel column-of-sections feel. Instant-apply + * semantics: edits commit live to labelConfig (no Save/Cancel), + * matching the rest of the editor. Each field's ZPL command sits + * docked rightwards via `ZplCommandLabel`. */ +export function PrinterSettingsModal() { + const t = useT(); + const tab = useLabelStore((s) => s.printerSettingsTab); + const setTab = useLabelStore((s) => s.setPrinterSettingsTab); + const titleId = useId(); + const subtitleId = useId(); + + if (!tab) return null; + + const ActiveTab = TAB_COMPONENTS[tab]; + + return ( + setTab(null)} + boxClassName={MODAL_BOX_CLS} + > +
+
+

+ {t.printerSettings.title} +

+

+ {t.printerSettings.subtitle} +

+
+ +
+ +
+ + +
+ {ActiveTab && } +
+
+ +
+ + {t.printerSettings.instantApply} + + +
+
+ ); +} diff --git a/src/components/PrinterSettings/zplFieldPrimitives.tsx b/src/components/PrinterSettings/zplFieldPrimitives.tsx new file mode 100644 index 00000000..e39e0e52 --- /dev/null +++ b/src/components/PrinterSettings/zplFieldPrimitives.tsx @@ -0,0 +1,68 @@ +import type { ReactNode } from "react"; +import { labelCls } from "../Properties/styles"; + +/** Shared muted-monospace class for ZPL command tags so the visual + * weight stays identical across the label-row, checkbox-row and + * any future field primitives that dock a tag rightwards. */ +const commandTagCls = "font-mono text-[10px] text-muted/60 tracking-tight"; + +/** Field label with a ghost-rendered ZPL command tag docked right. + * The distinctive design move of the printer-settings modal: users + * see exactly which command each control emits, turning the modal + * into a discoverable spec reference without crowding the form. + * Density matches the Properties Panel's `labelCls`. */ +export function ZplCommandLabel({ + text, + command, + htmlFor, +}: { + text: string; + command: string; + htmlFor?: string; +}) { + return ( +
+ + {command} +
+ ); +} + +/** Checkbox row with the same ZPL-command-docked-right treatment as + * `ZplCommandLabel`. The command tag sits outside the `