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. */}
+
+
+
+ );
+}
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 && }
+
+
+
+
+
+ );
+}
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 `