Skip to content

Feat/printer profile - #110

Merged
u8array merged 5 commits into
mainfrom
feat/printer-profile-epic
May 30, 2026
Merged

u8array merged 5 commits into
mainfrom
feat/printer-profile-epic

Conversation

@u8array

@u8array u8array commented May 30, 2026

Copy link
Copy Markdown
Owner

No description provided.

u8array added 4 commits May 30, 2026 08:53
Two small UX fixes for the Real-Time Clock setup field.

Hint text was misleading — said 'the printer does not continue
ticking' which suggested the printer's RTC stops after ^ST. The
^ST command actually just sets the printer's battery-backed
clock; the printer ticks normally after receipt. What I meant
was that the *input field* in the browser does not auto-tick.
Reworded across all 32 locales to make that explicit.

Plus added a 'Now' button next to the datetime-local input that
seeds the field with the current local wall-clock time. The
field itself stays static so the generated Setup-Script remains
reproducible — the button is a UI nudge, not a live-now mode
(that would be a separate opt-in checkbox if ever needed).

Button has explicit aria-label / title combining the field name
and 'Now' so screen-reader users get the full context (the
visible button text alone is too short out of context).

toLocalIsoString helper lives in lib/realtimeClock because
Date.prototype.toISOString returns UTC; the form input is
local. Helper accepts an optional Date so tests can pin a
fixed instance instead of relying on call-time clock. Round-
trip test asserts the concrete ZPL positional shape rather
than just non-null, so a regression that still parses would
still trip the test.

Plus CLAUDE.md: documents the branch-finalisation review rule
(spawn clean-code / clean-architecture / smells subagents
before push, plus zpl-spec when the change touches ZPL output;
findings amended before the first push, later review findings
land as follow-up commits).
Pulls 14 EEPROM-persistent printer-state fields (^JZ, ^JT, ~TA,
^ST, ^KD, ^SL, ^KL, ^SE, ^SZ, ^KN) out of labelConfig into a new
printerProfile store slice (src/types/PrinterProfile.ts). Design
files no longer leak per-installation state when shared; the data
model also matches the UX split between per-label and printer-wide
configuration.

Schema is a Zod object with a superRefine cross-field rule
(clockMode === 'TOL' ↔ clockTolerance defined). PRINTER_PROFILE_FIELDS
is derived from the schema's shape so a new optional field is
automatically picked up by both the v4→v5 migration and the import
loader.

Store action `patchPrinterProfile` runs every merged result through
the schema (`safeParse`). In dev / test it throws on rejection so
the bug surfaces; in prod it warn-and-drops so end users get a
degraded-but-alive store. `setLabelConfig` keeps its non-validating
merge — different semantics (LabelConfig has generator-applied
defaults; PrinterProfile models true three-state "absent = printer
default"). New `resetPrinterProfile` action surfaces the preview's
Clear button. The slice participates in zundo's temporal partialize
so Clear is Ctrl-Z undoable.

Migration v4→v5 hoists profile fields out of legacy persisted label
state via PRINTER_PROFILE_FIELDS set-membership, with a defensive
belt that seeds an empty profile whenever the version path bypasses
the hop.

Generator (src/lib/zplSetupScript.ts) gets a discriminated-union
registry: `kind: 'emit'` entries carry the channel + emit closure;
`kind: 'foldedInto'` entries are pure metadata for fields that ride
along on another field's emit (printerDescription rides on ^KN,
clockTolerance / clockLanguage ride on ^SL, useCurrentTimeForClock
rides on ^ST). ~TA emit now zero-pads the magnitude to exactly 3
digits per spec — the old `~TA5` form was silently dropped by
firmware ("if fewer than 3 chars, command is ignored").

Parser writes 10 setup-script handlers (JZ, JT, TA, ST, KD, KL,
SE, SZ, KN, SL) into a separate `printerProfile` accumulator on
ParsedZPL. SL handler `delete`s a stale clockTolerance when mode
flips from TOL so the returned result is contract-clean.

Cross-block fold in zplImportService uses a shared
`lib/pruneUndefined` helper that the store also leans on, so the
present-with-undefined leak path is closed at both consumers of
the parser output.

Tab components (Clock, Encoding, Identity, PrintQuality) call
`patchPrinterProfile` for profile fields and keep `setLabelConfig`
for the few per-label settings they still own. ZPLOutput no longer
renders a secondary Setup-Script section — that surface moves to
the Printer Settings modal's docked preview (next commit).

Extracted shared primitives: `components/Output/ZplLine` (syntax
highlighter shared between the per-label and Setup-Script panes)
and `lib/useCopyToClipboard` (clipboard hook with the `getPayload`
callback shape needed for ^ST live-clock mode).

Locales: previewHeading + previewClear added across all 32
languages plus the Sub-A live-clock / send-button strings inherited
from the prior branch.
Restructures the Printer Settings modal so the existing rail tabs
split into two top-tabs: "Per Label" (mediaFeed, printQuality) and
"Setup Script" (clockTime, encodingLanguage, identity). The rail
shows only the active top-tab's sub-tabs.

The Setup-Script top-tab gains a docked 160px preview pane at the
bottom of the modal with Clear / Copy / Send actions, replacing
the secondary Setup-Script section that previously sat under the
per-label ZPL panel in ZPLOutput. Send moves out of the modal
footer into the preview so the action and its artefact are co-
located; the footer keeps only Close + the instantApply hint.

Clear sits left of a vertical divider with a red hover state AND
a red focus-visible ring so keyboard users see the destructive
intent that the mouse hover gives. Order: Clear | Copy Send.

Top-tabs use plain aria-current="page" navigation, not role=
tablist, so the rail's button semantics stay consistent across
the strip and the rail (one page with two different "tab" models
is worse than two plain navs). Sub-tab assignment lives in
TOP_TAB_OF as a `satisfies Record<PrinterSettingsTab, TopTabId>`
literal — a new sub-tab id added to the union but forgotten here
fails compile. TABS_BY_TOP_TAB is derived from TOP_TAB_OF with
the seed coming from TOP_TAB_ORDER so a new TopTabId cannot cause
a runtime undefined.push.
…l invariants

Adds and updates tests for the printerProfile extraction:

- Migration v4→v5: three cases (legacy label with profile fields
  hoisted, legacy label without profile fields gets empty seed,
  defensive belt for missing slice on current version).
- patchPrinterProfile: merge + undefined-strip, atomic TOL+tolerance
  patch, dev-throw on schema-invalid patches, no-op under preview
  lock.
- resetPrinterProfile: clears all fields, no-op under preview lock.
- ~TA emit: 3-digit zero-padded magnitude across sign / boundary
  values (was emitting `~TA5` which firmware silently drops).
- SETUP_SCRIPT_EMITTERS structural invariant: every foldedInto.target
  points at a kind:'emit' entry, never another foldedInto (would
  create a fold chain with no actual emit producer).
- Type-retargeting: zplGenerator.printerSettings.test.ts assertions
  now read `parseZPL(...).printerProfile.x` instead of `.labelConfig.x`.
- reset() in labelStore.test.ts seeds `printerProfile: {}` so a test
  that mutates the slice cannot leak into the next via Zustand's
  partial-merge setState.
@u8array u8array changed the title Feat/printer profile epic Feat/printer profile May 30, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the printer configuration by extracting EEPROM-persistent printer-state fields from the per-label configuration into a dedicated printerProfile slice, preventing local printer settings from leaking into shared design files. It also introduces a docked preview pane for the Setup-Script in the PrinterSettingsModal, extracts the ZplLine component for reuse, and adds a useCopyToClipboard hook. Our feedback highlights a potential memory leak in the useCopyToClipboard hook where the setTimeout is not cleared upon component unmount, and we recommend adopting the provided cleanup implementation.

Comment thread src/lib/useCopyToClipboard.ts Outdated
Stores the timeout id in a ref so unmount (e.g. modal close right
after copy) cancels the pending setCopied(false), avoiding a stale
setState on a dead component tree. The same ref re-arms on rapid
re-click so the previous timer can't flip the flag back mid-
feedback.
@u8array
u8array merged commit d2ae433 into main May 30, 2026
2 checks passed
@u8array
u8array deleted the feat/printer-profile-epic branch May 30, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant