Skip to content

Commit d2ae433

Browse files
authored
Merge pull request #110 from u8array/feat/printer-profile-epic
Feat/printer profile
2 parents 15e7fb9 + bbf9422 commit d2ae433

54 files changed

Lines changed: 1204 additions & 453 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/components/Output/ZPLOutput.tsx‎

Lines changed: 10 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { useState, type ReactNode } from 'react';
22
import { CheckIcon, ClipboardDocumentIcon, ChevronDownIcon, ChevronUpIcon, EyeIcon } from '@heroicons/react/16/solid';
33
import { useLabelStore, selectLabelaryNoticeRequired } from '../../store/labelStore';
44
import { generateMultiPageZPL } from '../../lib/zplGenerator';
5-
import { generateSetupScript } from '../../lib/zplSetupScript';
5+
import { useCopyToClipboard } from '../../lib/useCopyToClipboard';
66
import { useT } from '../../lib/useT';
77
import { LabelaryNoticeModal } from './LabelaryNoticeModal';
8+
import { ZplLine } from './ZplLine';
89

910
interface Props {
1011
collapsed?: boolean;
@@ -26,7 +27,6 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) {
2627

2728
const hasObjects = pages.some((p) => p.objects.length > 0);
2829
const zpl = hasObjects ? generateMultiPageZPL(label, pages, variables) : '';
29-
const setupScript = generateSetupScript(label);
3030

3131
const previewActive =
3232
previewMode.status === 'loading' || previewMode.status === 'active';
@@ -46,19 +46,12 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) {
4646
return (
4747
<div className="flex flex-col h-full">
4848
<OutputSection
49-
variant="primary"
5049
heading={t.output.zplHeading}
5150
content={zpl}
5251
emptyMessage={t.output.noObjects}
5352
collapsed={collapsed}
5453
onCollapseToggle={collapsed ? onExpand : onCollapse}
5554
collapseLabel={collapsed ? t.app.expand : t.app.collapse}
56-
/* Hidden-secondary indicator: when the panel is collapsed,
57-
the Setup-Script section is also hidden but still has
58-
emit-worthy content. A small accent dot in the header
59-
signals "there is more output below" so users do not
60-
silently lose visibility of their printer-state edits. */
61-
hiddenSecondaryActive={!!collapsed && !!setupScript}
6255
extraActions={labelaryEnabled && (
6356
<button
6457
onClick={togglePreview}
@@ -77,20 +70,6 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) {
7770
)}
7871
/>
7972

80-
{/* Setup-Script section only renders when at least one EEPROM-
81-
persistent printer-state field is set. The Printer Settings
82-
Modal is where users enable those (^JZ, ^JT, ~TA, plus the
83-
upcoming clock/encoding/identity commands); the output here
84-
shows what would be sent once to provision the printer. */}
85-
{!collapsed && setupScript && (
86-
<OutputSection
87-
variant="secondary"
88-
heading={t.output.setupScriptHeading}
89-
content={setupScript}
90-
emptyMessage=""
91-
/>
92-
)}
93-
9473
{showNotice && (
9574
<LabelaryNoticeModal
9675
onClose={() => setShowNotice(false)}
@@ -105,62 +84,32 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) {
10584
}
10685

10786
/** Single output pane: header (collapse toggle + heading + extra
108-
* actions + copy button) and a `<pre>` body. Reused for the main
109-
* per-label ZPL and the Setup-Script output so both sections share
110-
* the same look (and any future tweaks land in one place).
111-
*
112-
* `variant='primary'` fills the remaining flex space (used by the
113-
* main per-label ZPL). `variant='secondary'` gets a top border + a
114-
* bounded max-height so it sits compactly under the primary pane
115-
* (used by the Setup-Script output). The variant collapses two
116-
* separate booleans (`borderTop` / `maxHeightClass`) into one
117-
* honest discriminator. */
87+
* actions + copy button) and a `<pre>` body. The Setup-Script
88+
* output moved into the Printer Settings modal's docked preview
89+
* pane, so this component no longer needs to host a secondary
90+
* variant — it just shows the per-label ZPL. */
11891
function OutputSection({
119-
variant,
12092
heading,
12193
content,
12294
emptyMessage,
12395
collapsed,
12496
onCollapseToggle,
12597
collapseLabel,
12698
extraActions,
127-
hiddenSecondaryActive,
12899
}: {
129-
variant: 'primary' | 'secondary';
130100
heading: string;
131101
content: string;
132102
emptyMessage: string;
133103
collapsed?: boolean;
134104
onCollapseToggle?: () => void;
135105
collapseLabel?: string;
136106
extraActions?: ReactNode;
137-
/** When `true`, render a small accent dot next to the heading so
138-
* users know a hidden secondary section has emit-worthy content
139-
* even while this pane is collapsed. */
140-
hiddenSecondaryActive?: boolean;
141107
}) {
142-
const isSecondary = variant === 'secondary';
143-
const containerCls = isSecondary
144-
? 'border-t border-border'
145-
: 'flex-1 min-h-0';
146-
const bodyCls = isSecondary ? 'max-h-48' : 'flex-1';
147108
const t = useT();
148-
const [copied, setCopied] = useState(false);
149-
150-
const handleCopy = () => {
151-
if (!content) return;
152-
// `navigator.clipboard` is undefined in non-secure contexts
153-
// (plain HTTP, file://) and in some embedded WebViews. Bail
154-
// out instead of throwing — the copy button is non-essential.
155-
if (!navigator.clipboard) return;
156-
navigator.clipboard.writeText(content).then(() => {
157-
setCopied(true);
158-
setTimeout(() => setCopied(false), 1500);
159-
}).catch(() => { /* swallow: user-cancel or permission-denied */ });
160-
};
109+
const { copy, copied } = useCopyToClipboard(() => content);
161110

162111
return (
163-
<div className={`flex flex-col ${containerCls}`}>
112+
<div className="flex flex-col flex-1 min-h-0">
164113
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border shrink-0">
165114
<div className="flex items-center gap-2">
166115
{onCollapseToggle && (
@@ -174,18 +123,11 @@ function OutputSection({
174123
</button>
175124
)}
176125
<span className="font-mono text-[10px] text-muted uppercase tracking-widest">{heading}</span>
177-
{hiddenSecondaryActive && (
178-
<span
179-
className="w-1.5 h-1.5 rounded-full bg-accent"
180-
title={t.output.hiddenSecondaryHint}
181-
aria-label={t.output.hiddenSecondaryHint}
182-
/>
183-
)}
184126
</div>
185127
<div className="flex items-center gap-3">
186128
{extraActions}
187129
<button
188-
onClick={handleCopy}
130+
onClick={copy}
189131
disabled={!content}
190132
title={t.output.copy}
191133
className="flex items-center gap-1 font-mono text-[10px] text-muted hover:text-accent disabled:opacity-25 disabled:cursor-not-allowed transition-colors"
@@ -198,7 +140,7 @@ function OutputSection({
198140
</div>
199141

200142
{!collapsed && (
201-
<pre className={`overflow-auto p-3 font-mono text-xs leading-relaxed text-text m-0 bg-surface ${bodyCls}`}>
143+
<pre className="overflow-auto p-3 font-mono text-xs leading-relaxed text-text m-0 bg-surface flex-1">
202144
{content
203145
? content.split('\n').map((line, i) => (
204146
<ZplLine key={i} line={line} />
@@ -211,16 +153,3 @@ function OutputSection({
211153
);
212154
}
213155

214-
function ZplLine({ line }: { line: string }) {
215-
// highlight ^CMD tokens in amber, rest in default text color
216-
const parts = line.split(/([\^][A-Z0-9]+)/g);
217-
return (
218-
<span className="block">
219-
{parts.map((part, i) =>
220-
/^\^[A-Z0-9]+$/.test(part)
221-
? <span key={i} className="text-accent">{part}</span>
222-
: <span key={i} className="text-text">{part}</span>
223-
)}
224-
</span>
225-
);
226-
}

‎src/components/Output/ZplImportModal.tsx‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { readFileAsText } from '../../lib/readFile';
55
import { useLabelStore } from '../../store/labelStore';
66
import type { Page } from '../../types/Group';
77
import type { LabelConfig } from '../../types/ObjectType';
8+
import type { PrinterProfile } from '../../types/PrinterProfile';
89
import type { Variable } from '../../types/Variable';
910
import { formatReportAsText, type ImportReport, type ImportResult } from '../../lib/importReport';
1011
import { ImportSummaryBody } from './ImportSummary';
@@ -25,6 +26,7 @@ export function ZplImportModal({ onClose }: Props) {
2526
const fileInputRef = useRef<HTMLInputElement>(null);
2627
const loadDesign = useLabelStore((s) => s.loadDesign);
2728
const appendPages = useLabelStore((s) => s.appendPages);
29+
const patchPrinterProfile = useLabelStore((s) => s.patchPrinterProfile);
2830
const label = useLabelStore((s) => s.label);
2931
const pages = useLabelStore((s) => s.pages);
3032

@@ -37,6 +39,7 @@ export function ZplImportModal({ onClose }: Props) {
3739

3840
const applyImport = (
3941
labelConfig: Partial<LabelConfig>,
42+
printerProfile: Partial<PrinterProfile>,
4043
importedPages: Page[],
4144
importedVariables: Variable[],
4245
) => {
@@ -51,6 +54,13 @@ export function ZplImportModal({ onClose }: Props) {
5154
} else {
5255
loadDesign({ ...label, ...labelConfig }, importedPages, importedVariables);
5356
}
57+
// Setup-Script fields update the active profile regardless of
58+
// append/replace — they are per-installation state, not per-design,
59+
// so an import of a ZPL that carries Setup-Script commands should
60+
// reflect those commands' intent on the user's profile.
61+
if (Object.keys(printerProfile).length > 0) {
62+
patchPrinterProfile(printerProfile);
63+
}
5464
};
5565

5666
// Feedback through state change: when the import has no findings the
@@ -73,16 +83,21 @@ export function ZplImportModal({ onClose }: Props) {
7383
const processImport = (text: string) => {
7484
const {
7585
labelConfig,
86+
printerProfile,
7687
pages: importedPages,
7788
variables: importedVariables,
7889
report,
7990
} = importZplText(text, label.dpmm);
8091
const totalObjects = importedPages.reduce((s, p) => s + p.objects.length, 0);
81-
if (totalObjects === 0 && Object.keys(labelConfig).length === 0) {
92+
if (
93+
totalObjects === 0 &&
94+
Object.keys(labelConfig).length === 0 &&
95+
Object.keys(printerProfile).length === 0
96+
) {
8297
setError('No supported objects found in the ZPL code.');
8398
return;
8499
}
85-
applyImport(labelConfig, importedPages, importedVariables);
100+
applyImport(labelConfig, printerProfile, importedPages, importedVariables);
86101
finishImport(totalObjects, report);
87102
};
88103

‎src/components/Output/ZplLine.tsx‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/** One line of rendered ZPL with `^CMD` tokens highlighted in the
2+
* accent color. Shared between the per-label ZPL pane in
3+
* `ZPLOutput` and the Setup-Script preview pane in the Printer
4+
* Settings modal so any future tweak (e.g. handling `~CMD` or
5+
* lowercase commands) lands in one place. */
6+
export function ZplLine({ line }: { line: string }) {
7+
const parts = line.split(/([\^][A-Z0-9]+)/g);
8+
return (
9+
<span className="block">
10+
{parts.map((part, i) =>
11+
/^\^[A-Z0-9]+$/.test(part)
12+
? <span key={i} className="text-accent">{part}</span>
13+
: <span key={i} className="text-text">{part}</span>
14+
)}
15+
</span>
16+
);
17+
}

‎src/components/PrinterSettings/ClockAndTimeTab.tsx‎

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,29 +57,53 @@ const CLOCK_LANGUAGE_LABEL_KEYS = {
5757
* reproducible (no live now-snapshot on every copy/export). */
5858
export function ClockAndTimeTab() {
5959
const t = useT();
60-
const label = useLabelStore((s) => s.label);
61-
const setLabelConfig = useLabelStore((s) => s.setLabelConfig);
60+
const profile = useLabelStore((s) => s.printerProfile);
61+
const patchPrinterProfile = useLabelStore((s) => s.patchPrinterProfile);
6262
const loc = t.printerSettings.clockTime;
6363
const clockId = useId();
6464
const modeId = useId();
6565

6666
return (
67-
<div className="flex flex-col gap-4">
68-
{/* ^ST: hand-rolled instead of going through a primitive, since
69-
datetime-local is the only such input in the modal and
70-
adding a primitive for one caller is YAGNI. */}
67+
// gap-3 (not gap-4) so the tab fits inside the modal's ~290px form
68+
// budget even when TOL mode expands the ^SL sub-block. With gap-4
69+
// the TOL state spills past the docked preview into a scroll, and
70+
// the scrollbar inside a panel that's bordered top + bottom by
71+
// fixed UI is easy to miss.
72+
<div className="flex flex-col gap-3">
73+
{/* ^ST: two modes for the real-time-clock value.
74+
- Static: user types a value, generator emits it verbatim
75+
on every export (reproducible Setup-Script).
76+
- Live: user toggles the checkbox, generator captures the
77+
current wall-clock time at every export (Setup-Script
78+
actually "sets the clock to now"; not reproducible).
79+
The checkbox is colocated with the input so the active
80+
mode is visible at a glance. The input is disabled while
81+
live mode is on, but the stored value is kept so flipping
82+
back does not lose the typed value. */}
7183
<ZplField>
7284
<ZplCommandLabel text={loc.setRealtimeClock} command="^ST" htmlFor={clockId} />
7385
<input
7486
id={clockId}
7587
type="datetime-local"
7688
step="1"
77-
className={inputCls}
78-
value={label.setRealtimeClock ?? ""}
89+
className={`${inputCls} disabled:opacity-40 disabled:cursor-not-allowed`}
90+
value={profile.setRealtimeClock ?? ""}
91+
disabled={!!profile.useCurrentTimeForClock}
7992
onChange={(e) =>
80-
setLabelConfig({ setRealtimeClock: e.target.value || undefined })
93+
patchPrinterProfile({ setRealtimeClock: e.target.value || undefined })
8194
}
8295
/>
96+
<label className="flex items-center gap-2 mt-1 cursor-pointer">
97+
<input
98+
type="checkbox"
99+
className="accent-accent"
100+
checked={!!profile.useCurrentTimeForClock}
101+
onChange={(e) =>
102+
patchPrinterProfile({ useCurrentTimeForClock: e.target.checked || undefined })
103+
}
104+
/>
105+
<span className="text-xs text-text">{loc.clockUseCurrentTime}</span>
106+
</label>
83107
<span className={labelCls + " normal-case tracking-normal text-muted/70"}>
84108
{loc.setRealtimeClockHint}
85109
</span>
@@ -90,8 +114,8 @@ export function ClockAndTimeTab() {
90114
command="^KD"
91115
values={CLOCK_FORMAT_VALUES}
92116
isValid={isClockFormat}
93-
value={label.clockFormat}
94-
onChange={(v) => setLabelConfig({ clockFormat: v })}
117+
value={profile.clockFormat}
118+
onChange={(v) => patchPrinterProfile({ clockFormat: v })}
95119
defaultLabel={t.printerSettings.defaultOption}
96120
optionLabel={(m) => loc[CLOCK_FORMAT_LABEL_KEYS[m]]}
97121
/>
@@ -107,7 +131,7 @@ export function ClockAndTimeTab() {
107131
<select
108132
id={modeId}
109133
className={inputCls}
110-
value={label.clockMode ?? ""}
134+
value={profile.clockMode ?? ""}
111135
onChange={(e) => {
112136
const v = e.target.value;
113137
const nextMode = isClockMode(v) ? v : undefined;
@@ -118,25 +142,25 @@ export function ClockAndTimeTab() {
118142
// this the store would briefly hold an invalid combo
119143
// (TOL without tolerance) which the schema rejects.
120144
const nextTolerance = nextMode === 'TOL'
121-
? label.clockTolerance ?? CLOCK_TOLERANCE_DEFAULT
145+
? profile.clockTolerance ?? CLOCK_TOLERANCE_DEFAULT
122146
: undefined;
123-
setLabelConfig({ clockMode: nextMode, clockTolerance: nextTolerance });
147+
patchPrinterProfile({ clockMode: nextMode, clockTolerance: nextTolerance });
124148
}}
125149
>
126150
<option value="">{t.printerSettings.defaultOption}</option>
127151
{CLOCK_MODE_VALUES.map((m) => (
128152
<option key={m} value={m}>{loc[CLOCK_MODE_LABEL_KEYS[m]]}</option>
129153
))}
130154
</select>
131-
{label.clockMode === 'TOL' && (
155+
{profile.clockMode === 'TOL' && (
132156
<ZplSubField label={loc.clockTolerance}>
133157
{(id) => (
134158
<BoundedIntControl
135159
id={id}
136160
min={CLOCK_TOLERANCE_RANGE.min}
137161
max={CLOCK_TOLERANCE_RANGE.max}
138-
value={label.clockTolerance}
139-
onChange={(v) => setLabelConfig({ clockTolerance: v })}
162+
value={profile.clockTolerance}
163+
onChange={(v) => patchPrinterProfile({ clockTolerance: v })}
140164
/>
141165
)}
142166
</ZplSubField>
@@ -145,8 +169,8 @@ export function ClockAndTimeTab() {
145169
label={loc.clockLanguage}
146170
values={CLOCK_LANGUAGE_VALUES}
147171
isValid={isClockLanguage}
148-
value={label.clockLanguage}
149-
onChange={(v) => setLabelConfig({ clockLanguage: v })}
172+
value={profile.clockLanguage}
173+
onChange={(v) => patchPrinterProfile({ clockLanguage: v })}
150174
defaultLabel={t.printerSettings.defaultOption}
151175
optionLabel={(m) => `${m} – ${loc[CLOCK_LANGUAGE_LABEL_KEYS[m]]}`}
152176
/>

0 commit comments

Comments
 (0)