Skip to content

Commit 15e7fb9

Browse files
authored
Merge pull request #109 from u8array/feat/printer-modal-tab3-sl-clock-mode
Feat/printer modal tab3 sl clock mode
2 parents d588889 + a788cf0 commit 15e7fb9

39 files changed

Lines changed: 991 additions & 53 deletions

‎src/components/PrinterSettings/ClockAndTimeTab.tsx‎

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,52 @@ import { useLabelStore } from "../../store/labelStore";
44
import { inputCls, labelCls } from "../ui/formStyles";
55
import {
66
CLOCK_FORMAT_VALUES,
7+
CLOCK_LANGUAGE_VALUES,
8+
CLOCK_MODE_VALUES,
9+
CLOCK_TOLERANCE_DEFAULT,
10+
CLOCK_TOLERANCE_RANGE,
711
isClockFormat,
12+
isClockLanguage,
13+
isClockMode,
814
type ClockFormat,
15+
type ClockLanguage,
16+
type ClockMode,
917
} from "../../types/ObjectType";
1018
import {
19+
BoundedIntControl,
1120
ZplCommandLabel,
1221
ZplEnumSelect,
22+
ZplEnumSubSelect,
1323
ZplField,
24+
ZplSubField,
1425
} from "./zplFieldPrimitives";
1526

1627
type LocClockTime = ReturnType<typeof useT>["printerSettings"]["clockTime"];
1728

18-
/** Explicit value → locale-key map. `satisfies` makes both a
19-
* missing enum-value entry (here) and a missing locale key (in
20-
* the locale shape) compile errors — the earlier template-literal
21-
* helper used an `as`-cast that bypassed the second direction. */
29+
/** Explicit value → locale-key maps for all three enums in this
30+
* tab. `satisfies` makes both a missing enum-value entry (here)
31+
* and a missing locale key (in the locale shape) compile errors. */
2232
const CLOCK_FORMAT_LABEL_KEYS = {
2333
'0': 'clockFormat0',
2434
'1': 'clockFormat1',
2535
'2': 'clockFormat2',
2636
'3': 'clockFormat3',
2737
} as const satisfies Record<ClockFormat, keyof LocClockTime>;
2838

39+
const CLOCK_MODE_LABEL_KEYS = {
40+
S: 'clockModeS',
41+
T: 'clockModeT',
42+
TOL: 'clockModeTOL',
43+
} as const satisfies Record<ClockMode, keyof LocClockTime>;
44+
45+
const CLOCK_LANGUAGE_LABEL_KEYS = {
46+
'1': 'clockLanguage1', '2': 'clockLanguage2', '3': 'clockLanguage3',
47+
'4': 'clockLanguage4', '5': 'clockLanguage5', '6': 'clockLanguage6',
48+
'7': 'clockLanguage7', '8': 'clockLanguage8', '9': 'clockLanguage9',
49+
'10': 'clockLanguage10', '11': 'clockLanguage11', '12': 'clockLanguage12',
50+
'13': 'clockLanguage13',
51+
} as const satisfies Record<ClockLanguage, keyof LocClockTime>;
52+
2953
/** Tab 3 of the Printer Settings Modal — clock/time setup commands.
3054
* All three live in the Setup-Script output (EEPROM-persistent),
3155
* not the per-label ZPL block. ^ST is modelled as a static user-
@@ -37,6 +61,7 @@ export function ClockAndTimeTab() {
3761
const setLabelConfig = useLabelStore((s) => s.setLabelConfig);
3862
const loc = t.printerSettings.clockTime;
3963
const clockId = useId();
64+
const modeId = useId();
4065

4166
return (
4267
<div className="flex flex-col gap-4">
@@ -70,6 +95,65 @@ export function ClockAndTimeTab() {
7095
defaultLabel={t.printerSettings.defaultOption}
7196
optionLabel={(m) => loc[CLOCK_FORMAT_LABEL_KEYS[m]]}
7297
/>
98+
99+
{/* All three ^SL inputs live under one parent tag — mode +
100+
(conditional) tolerance share the first positional slot,
101+
language is the second. ZplSubField keeps the per-row
102+
labels without duplicating the ^SL command tag (same
103+
pattern as ^KN name+description in IdentityTab and ^PR
104+
triple in PrintQualityTab). */}
105+
<ZplField>
106+
<ZplCommandLabel text={loc.clockMode} command="^SL" htmlFor={modeId} />
107+
<select
108+
id={modeId}
109+
className={inputCls}
110+
value={label.clockMode ?? ""}
111+
onChange={(e) => {
112+
const v = e.target.value;
113+
const nextMode = isClockMode(v) ? v : undefined;
114+
// Schema's cross-field rule pairs `clockMode === 'TOL'`
115+
// with a defined `clockTolerance` (and vice versa). Seed
116+
// the default the moment the user picks TOL, and clear
117+
// the tolerance the moment they leave TOL — without
118+
// this the store would briefly hold an invalid combo
119+
// (TOL without tolerance) which the schema rejects.
120+
const nextTolerance = nextMode === 'TOL'
121+
? label.clockTolerance ?? CLOCK_TOLERANCE_DEFAULT
122+
: undefined;
123+
setLabelConfig({ clockMode: nextMode, clockTolerance: nextTolerance });
124+
}}
125+
>
126+
<option value="">{t.printerSettings.defaultOption}</option>
127+
{CLOCK_MODE_VALUES.map((m) => (
128+
<option key={m} value={m}>{loc[CLOCK_MODE_LABEL_KEYS[m]]}</option>
129+
))}
130+
</select>
131+
{label.clockMode === 'TOL' && (
132+
<ZplSubField label={loc.clockTolerance}>
133+
{(id) => (
134+
<BoundedIntControl
135+
id={id}
136+
min={CLOCK_TOLERANCE_RANGE.min}
137+
max={CLOCK_TOLERANCE_RANGE.max}
138+
value={label.clockTolerance}
139+
onChange={(v) => setLabelConfig({ clockTolerance: v })}
140+
/>
141+
)}
142+
</ZplSubField>
143+
)}
144+
<ZplEnumSubSelect
145+
label={loc.clockLanguage}
146+
values={CLOCK_LANGUAGE_VALUES}
147+
isValid={isClockLanguage}
148+
value={label.clockLanguage}
149+
onChange={(v) => setLabelConfig({ clockLanguage: v })}
150+
defaultLabel={t.printerSettings.defaultOption}
151+
optionLabel={(m) => `${m} – ${loc[CLOCK_LANGUAGE_LABEL_KEYS[m]]}`}
152+
/>
153+
<span className={`${labelCls} normal-case tracking-normal text-muted/70`}>
154+
{loc.clockSlPersistenceHint}
155+
</span>
156+
</ZplField>
73157
</div>
74158
);
75159
}

‎src/components/PrinterSettings/MediaFeedTab.tsx‎

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useT } from "../../lib/useT";
22
import { useLabelStore } from "../../store/labelStore";
3-
import { inputCls } from "../ui/formStyles";
43
import {
54
MAX_LABEL_LENGTH_RANGE,
65
MEDIA_FEED_VALUES,
@@ -21,8 +20,8 @@ import {
2120
ZplCheckbox,
2221
ZplCommandLabel,
2322
ZplEnumSelect,
23+
ZplEnumSubSelect,
2424
ZplField,
25-
ZplSubField,
2625
} from "./zplFieldPrimitives";
2726

2827
type LocMediaFeed = ReturnType<typeof useT>["printerSettings"]["mediaFeed"];
@@ -123,19 +122,23 @@ export function MediaFeedTab() {
123122
<ZplField>
124123
<ZplCommandLabel text={loc.mediaFeedHeading} command="^MF" />
125124
<div className="grid grid-cols-2 gap-2">
126-
<FeedSubSelect
125+
<ZplEnumSubSelect
127126
label={loc.mediaFeedPowerUp}
127+
values={MEDIA_FEED_VALUES}
128+
isValid={isMediaFeedMode}
128129
value={label.mediaFeedPowerUp}
129130
onChange={(v) => setLabelConfig({ mediaFeedPowerUp: v })}
130131
defaultLabel={t.printerSettings.defaultOption}
131-
optionLabel={(m) => loc[FEED_LABEL_KEYS[m]]}
132+
optionLabel={(m) => `${m} ${loc[FEED_LABEL_KEYS[m]]}`}
132133
/>
133-
<FeedSubSelect
134+
<ZplEnumSubSelect
134135
label={loc.mediaFeedHeadClose}
136+
values={MEDIA_FEED_VALUES}
137+
isValid={isMediaFeedMode}
135138
value={label.mediaFeedHeadClose}
136139
onChange={(v) => setLabelConfig({ mediaFeedHeadClose: v })}
137140
defaultLabel={t.printerSettings.defaultOption}
138-
optionLabel={(m) => loc[FEED_LABEL_KEYS[m]]}
141+
optionLabel={(m) => `${m} ${loc[FEED_LABEL_KEYS[m]]}`}
139142
/>
140143
</div>
141144
</ZplField>
@@ -150,43 +153,3 @@ export function MediaFeedTab() {
150153
);
151154
}
152155

153-
/** Inner sub-select for the two ^MF positional params. Wraps a
154-
* bare `<select>` in ZplSubField so the parent ^MF tag stays the
155-
* single command label for both slots. ZplEnumSelect would render
156-
* a redundant tag spot here. */
157-
function FeedSubSelect({
158-
label,
159-
value,
160-
onChange,
161-
defaultLabel,
162-
optionLabel,
163-
}: {
164-
label: string;
165-
value: MediaFeedMode | undefined;
166-
onChange: (v: MediaFeedMode | undefined) => void;
167-
defaultLabel: string;
168-
optionLabel: (m: MediaFeedMode) => string;
169-
}) {
170-
return (
171-
<ZplSubField label={label}>
172-
{(id) => (
173-
<select
174-
id={id}
175-
className={inputCls}
176-
value={value ?? ""}
177-
onChange={(e) => {
178-
const raw = e.target.value;
179-
onChange(isMediaFeedMode(raw) ? raw : undefined);
180-
}}
181-
>
182-
<option value="">{defaultLabel}</option>
183-
{MEDIA_FEED_VALUES.map((m) => (
184-
<option key={m} value={m}>
185-
{m} {optionLabel(m)}
186-
</option>
187-
))}
188-
</select>
189-
)}
190-
</ZplSubField>
191-
);
192-
}

‎src/components/PrinterSettings/zplFieldPrimitives.tsx‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,53 @@ export function BoundedIntControl({
128128
);
129129
}
130130

131+
/** Inner enum-backed select for grids that share one parent ZPL
132+
* tag across multiple positional params (^MF pair, ^SL mode +
133+
* language). Same input shape as `ZplEnumSelect` minus the
134+
* `command` tag — the tag lives at the parent `ZplField` so
135+
* duplicating it on each sub-row would visually suggest two
136+
* separate commands. */
137+
export function ZplEnumSubSelect<T extends string>({
138+
label,
139+
values,
140+
isValid,
141+
value,
142+
onChange,
143+
defaultLabel,
144+
optionLabel,
145+
}: {
146+
label: string;
147+
values: readonly T[];
148+
isValid: (v: string) => v is T;
149+
value: T | undefined;
150+
onChange: (next: T | undefined) => void;
151+
defaultLabel: string;
152+
optionLabel: (v: T) => string;
153+
}) {
154+
return (
155+
<ZplSubField label={label}>
156+
{(id) => (
157+
<select
158+
id={id}
159+
className={inputCls}
160+
value={value ?? ""}
161+
onChange={(e) => {
162+
const raw = e.target.value;
163+
onChange(isValid(raw) ? raw : undefined);
164+
}}
165+
>
166+
<option value="">{defaultLabel}</option>
167+
{values.map((v) => (
168+
<option key={v} value={v}>
169+
{optionLabel(v)}
170+
</option>
171+
))}
172+
</select>
173+
)}
174+
</ZplSubField>
175+
);
176+
}
177+
131178
/** Generic enum-backed select row: label + ZPL-tag header + select
132179
* with a leading "default" placeholder. Replaces the hand-rolled
133180
* select scaffolding in MediaFeedTab and PrintQualityTab so the

‎src/lib/zplParser.ts‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { CustomFontMapping, LabelConfig } from "../types/ObjectType";
22
import {
3+
CLOCK_TOLERANCE_RANGE,
34
DARKNESS_INSTANT_RANGE,
45
DARKNESS_PERMANENT_RANGE,
56
HEAD_TEST_INTERVAL_RANGE,
@@ -8,6 +9,7 @@ import {
89
SPEED_RANGE,
910
TEAR_OFF_ADJUST_RANGE,
1011
isClockFormat,
12+
isClockLanguage,
1113
isMediaFeedMode,
1214
isMediaMode,
1315
isMediaTracking,
@@ -2066,9 +2068,36 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL {
20662068
labelConfig.printerDescription = desc;
20672069
}
20682070
},
2069-
// ^SL belongs in Tab 3 Clock & Time (Mode S/T + Language
2070-
// 1-13), not Identity — handler will land with that follow-
2071-
// up PR. See memory:project_ticket_sl_clock_formatting.
2071+
// ^SL `a`,`b` — Set Mode and Language for ^FC clock fields.
2072+
// The `a` slot accepts three shapes: 'S' (Start-Time), 'T'
2073+
// (Time-Now), or numeric 1..999 (Time-Now with tolerance
2074+
// seconds). The schema models the three shapes as two
2075+
// fields (clockMode enum + clockTolerance numeric); the
2076+
// parser fans the wire value out into both.
2077+
SL(p) {
2078+
const a = strParam(p[0]);
2079+
if (a === "S" || a === "T") {
2080+
labelConfig.clockMode = a;
2081+
// Clear any tolerance left over from a previous ^SL parse —
2082+
// schema's cross-field rule forbids `tolerance && mode !== 'TOL'`,
2083+
// and the parser writes raw values without re-running the
2084+
// schema, so a stale tolerance from an earlier ^SL60,1 would
2085+
// make the persisted state un-saveable.
2086+
labelConfig.clockTolerance = undefined;
2087+
} else {
2088+
const tol = inRange(parseIntOrUndef(a), CLOCK_TOLERANCE_RANGE);
2089+
if (tol !== undefined) {
2090+
labelConfig.clockMode = "TOL";
2091+
labelConfig.clockTolerance = tol;
2092+
}
2093+
}
2094+
// Language only when the mode parse landed — orphan language
2095+
// (mode invalid, b valid) would be write-only state (emit
2096+
// gated on mode anchor, parser would silently re-drop).
2097+
if (labelConfig.clockMode === undefined) return;
2098+
const b = strParam(p[1]);
2099+
if (isClockLanguage(b)) labelConfig.clockLanguage = b;
2100+
},
20722101

20732102
// ^CW {alias},{path} — register an alias for a printer-resident font.
20742103
// Subsequent ^A{alias} fields resolve to {path} via the fontAliases

0 commit comments

Comments
 (0)