Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 10 additions & 81 deletions src/components/Output/ZPLOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { useState, type ReactNode } from 'react';
import { CheckIcon, ClipboardDocumentIcon, ChevronDownIcon, ChevronUpIcon, EyeIcon } from '@heroicons/react/16/solid';
import { useLabelStore, selectLabelaryNoticeRequired } from '../../store/labelStore';
import { generateMultiPageZPL } from '../../lib/zplGenerator';
import { generateSetupScript } from '../../lib/zplSetupScript';
import { useCopyToClipboard } from '../../lib/useCopyToClipboard';
import { useT } from '../../lib/useT';
import { LabelaryNoticeModal } from './LabelaryNoticeModal';
import { ZplLine } from './ZplLine';

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

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

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

{/* Setup-Script section only renders when at least one EEPROM-
persistent printer-state field is set. The Printer Settings
Modal is where users enable those (^JZ, ^JT, ~TA, plus the
upcoming clock/encoding/identity commands); the output here
shows what would be sent once to provision the printer. */}
{!collapsed && setupScript && (
<OutputSection
variant="secondary"
heading={t.output.setupScriptHeading}
content={setupScript}
emptyMessage=""
/>
)}

{showNotice && (
<LabelaryNoticeModal
onClose={() => setShowNotice(false)}
Expand All @@ -105,62 +84,32 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) {
}

/** Single output pane: header (collapse toggle + heading + extra
* actions + copy button) and a `<pre>` body. Reused for the main
* per-label ZPL and the Setup-Script output so both sections share
* the same look (and any future tweaks land in one place).
*
* `variant='primary'` fills the remaining flex space (used by the
* main per-label ZPL). `variant='secondary'` gets a top border + a
* bounded max-height so it sits compactly under the primary pane
* (used by the Setup-Script output). The variant collapses two
* separate booleans (`borderTop` / `maxHeightClass`) into one
* honest discriminator. */
* actions + copy button) and a `<pre>` body. The Setup-Script
* output moved into the Printer Settings modal's docked preview
* pane, so this component no longer needs to host a secondary
* variant — it just shows the per-label ZPL. */
function OutputSection({
variant,
heading,
content,
emptyMessage,
collapsed,
onCollapseToggle,
collapseLabel,
extraActions,
hiddenSecondaryActive,
}: {
variant: 'primary' | 'secondary';
heading: string;
content: string;
emptyMessage: string;
collapsed?: boolean;
onCollapseToggle?: () => void;
collapseLabel?: string;
extraActions?: ReactNode;
/** When `true`, render a small accent dot next to the heading so
* users know a hidden secondary section has emit-worthy content
* even while this pane is collapsed. */
hiddenSecondaryActive?: boolean;
}) {
const isSecondary = variant === 'secondary';
const containerCls = isSecondary
? 'border-t border-border'
: 'flex-1 min-h-0';
const bodyCls = isSecondary ? 'max-h-48' : 'flex-1';
const t = useT();
const [copied, setCopied] = useState(false);

const handleCopy = () => {
if (!content) return;
// `navigator.clipboard` is undefined in non-secure contexts
// (plain HTTP, file://) and in some embedded WebViews. Bail
// out instead of throwing — the copy button is non-essential.
if (!navigator.clipboard) return;
navigator.clipboard.writeText(content).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}).catch(() => { /* swallow: user-cancel or permission-denied */ });
};
const { copy, copied } = useCopyToClipboard(() => content);

return (
<div className={`flex flex-col ${containerCls}`}>
<div className="flex flex-col flex-1 min-h-0">
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border shrink-0">
<div className="flex items-center gap-2">
{onCollapseToggle && (
Expand All @@ -174,18 +123,11 @@ function OutputSection({
</button>
)}
<span className="font-mono text-[10px] text-muted uppercase tracking-widest">{heading}</span>
{hiddenSecondaryActive && (
<span
className="w-1.5 h-1.5 rounded-full bg-accent"
title={t.output.hiddenSecondaryHint}
aria-label={t.output.hiddenSecondaryHint}
/>
)}
</div>
<div className="flex items-center gap-3">
{extraActions}
<button
onClick={handleCopy}
onClick={copy}
disabled={!content}
title={t.output.copy}
className="flex items-center gap-1 font-mono text-[10px] text-muted hover:text-accent disabled:opacity-25 disabled:cursor-not-allowed transition-colors"
Expand All @@ -198,7 +140,7 @@ function OutputSection({
</div>

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

function ZplLine({ line }: { line: string }) {
// highlight ^CMD tokens in amber, rest in default text color
const parts = line.split(/([\^][A-Z0-9]+)/g);
return (
<span className="block">
{parts.map((part, i) =>
/^\^[A-Z0-9]+$/.test(part)
? <span key={i} className="text-accent">{part}</span>
: <span key={i} className="text-text">{part}</span>
)}
</span>
);
}
19 changes: 17 additions & 2 deletions src/components/Output/ZplImportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { readFileAsText } from '../../lib/readFile';
import { useLabelStore } from '../../store/labelStore';
import type { Page } from '../../types/Group';
import type { LabelConfig } from '../../types/ObjectType';
import type { PrinterProfile } from '../../types/PrinterProfile';
import type { Variable } from '../../types/Variable';
import { formatReportAsText, type ImportReport, type ImportResult } from '../../lib/importReport';
import { ImportSummaryBody } from './ImportSummary';
Expand All @@ -25,6 +26,7 @@ export function ZplImportModal({ onClose }: Props) {
const fileInputRef = useRef<HTMLInputElement>(null);
const loadDesign = useLabelStore((s) => s.loadDesign);
const appendPages = useLabelStore((s) => s.appendPages);
const patchPrinterProfile = useLabelStore((s) => s.patchPrinterProfile);
const label = useLabelStore((s) => s.label);
const pages = useLabelStore((s) => s.pages);

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

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

// Feedback through state change: when the import has no findings the
Expand All @@ -73,16 +83,21 @@ export function ZplImportModal({ onClose }: Props) {
const processImport = (text: string) => {
const {
labelConfig,
printerProfile,
pages: importedPages,
variables: importedVariables,
report,
} = importZplText(text, label.dpmm);
const totalObjects = importedPages.reduce((s, p) => s + p.objects.length, 0);
if (totalObjects === 0 && Object.keys(labelConfig).length === 0) {
if (
totalObjects === 0 &&
Object.keys(labelConfig).length === 0 &&
Object.keys(printerProfile).length === 0
) {
setError('No supported objects found in the ZPL code.');
return;
}
applyImport(labelConfig, importedPages, importedVariables);
applyImport(labelConfig, printerProfile, importedPages, importedVariables);
finishImport(totalObjects, report);
};

Expand Down
17 changes: 17 additions & 0 deletions src/components/Output/ZplLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** One line of rendered ZPL with `^CMD` tokens highlighted in the
* accent color. Shared between the per-label ZPL pane in
* `ZPLOutput` and the Setup-Script preview pane in the Printer
* Settings modal so any future tweak (e.g. handling `~CMD` or
* lowercase commands) lands in one place. */
export function ZplLine({ line }: { line: string }) {
const parts = line.split(/([\^][A-Z0-9]+)/g);
return (
<span className="block">
{parts.map((part, i) =>
/^\^[A-Z0-9]+$/.test(part)
? <span key={i} className="text-accent">{part}</span>
: <span key={i} className="text-text">{part}</span>
)}
</span>
);
}
62 changes: 43 additions & 19 deletions src/components/PrinterSettings/ClockAndTimeTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,29 +57,53 @@ const CLOCK_LANGUAGE_LABEL_KEYS = {
* reproducible (no live now-snapshot on every copy/export). */
export function ClockAndTimeTab() {
const t = useT();
const label = useLabelStore((s) => s.label);
const setLabelConfig = useLabelStore((s) => s.setLabelConfig);
const profile = useLabelStore((s) => s.printerProfile);
const patchPrinterProfile = useLabelStore((s) => s.patchPrinterProfile);
const loc = t.printerSettings.clockTime;
const clockId = useId();
const modeId = useId();

return (
<div className="flex flex-col gap-4">
{/* ^ST: hand-rolled instead of going through a primitive, since
datetime-local is the only such input in the modal and
adding a primitive for one caller is YAGNI. */}
// gap-3 (not gap-4) so the tab fits inside the modal's ~290px form
// budget even when TOL mode expands the ^SL sub-block. With gap-4
// the TOL state spills past the docked preview into a scroll, and
// the scrollbar inside a panel that's bordered top + bottom by
// fixed UI is easy to miss.
<div className="flex flex-col gap-3">
{/* ^ST: two modes for the real-time-clock value.
- Static: user types a value, generator emits it verbatim
on every export (reproducible Setup-Script).
- Live: user toggles the checkbox, generator captures the
current wall-clock time at every export (Setup-Script
actually "sets the clock to now"; not reproducible).
The checkbox is colocated with the input so the active
mode is visible at a glance. The input is disabled while
live mode is on, but the stored value is kept so flipping
back does not lose the typed value. */}
<ZplField>
<ZplCommandLabel text={loc.setRealtimeClock} command="^ST" htmlFor={clockId} />
<input
id={clockId}
type="datetime-local"
step="1"
className={inputCls}
value={label.setRealtimeClock ?? ""}
className={`${inputCls} disabled:opacity-40 disabled:cursor-not-allowed`}
value={profile.setRealtimeClock ?? ""}
disabled={!!profile.useCurrentTimeForClock}
onChange={(e) =>
setLabelConfig({ setRealtimeClock: e.target.value || undefined })
patchPrinterProfile({ setRealtimeClock: e.target.value || undefined })
}
/>
<label className="flex items-center gap-2 mt-1 cursor-pointer">
<input
type="checkbox"
className="accent-accent"
checked={!!profile.useCurrentTimeForClock}
onChange={(e) =>
patchPrinterProfile({ useCurrentTimeForClock: e.target.checked || undefined })
}
/>
<span className="text-xs text-text">{loc.clockUseCurrentTime}</span>
</label>
<span className={labelCls + " normal-case tracking-normal text-muted/70"}>
{loc.setRealtimeClockHint}
</span>
Expand All @@ -90,8 +114,8 @@ export function ClockAndTimeTab() {
command="^KD"
values={CLOCK_FORMAT_VALUES}
isValid={isClockFormat}
value={label.clockFormat}
onChange={(v) => setLabelConfig({ clockFormat: v })}
value={profile.clockFormat}
onChange={(v) => patchPrinterProfile({ clockFormat: v })}
defaultLabel={t.printerSettings.defaultOption}
optionLabel={(m) => loc[CLOCK_FORMAT_LABEL_KEYS[m]]}
/>
Expand All @@ -107,7 +131,7 @@ export function ClockAndTimeTab() {
<select
id={modeId}
className={inputCls}
value={label.clockMode ?? ""}
value={profile.clockMode ?? ""}
onChange={(e) => {
const v = e.target.value;
const nextMode = isClockMode(v) ? v : undefined;
Expand All @@ -118,25 +142,25 @@ export function ClockAndTimeTab() {
// this the store would briefly hold an invalid combo
// (TOL without tolerance) which the schema rejects.
const nextTolerance = nextMode === 'TOL'
? label.clockTolerance ?? CLOCK_TOLERANCE_DEFAULT
? profile.clockTolerance ?? CLOCK_TOLERANCE_DEFAULT
: undefined;
setLabelConfig({ clockMode: nextMode, clockTolerance: nextTolerance });
patchPrinterProfile({ clockMode: nextMode, clockTolerance: nextTolerance });
}}
>
<option value="">{t.printerSettings.defaultOption}</option>
{CLOCK_MODE_VALUES.map((m) => (
<option key={m} value={m}>{loc[CLOCK_MODE_LABEL_KEYS[m]]}</option>
))}
</select>
{label.clockMode === 'TOL' && (
{profile.clockMode === 'TOL' && (
<ZplSubField label={loc.clockTolerance}>
{(id) => (
<BoundedIntControl
id={id}
min={CLOCK_TOLERANCE_RANGE.min}
max={CLOCK_TOLERANCE_RANGE.max}
value={label.clockTolerance}
onChange={(v) => setLabelConfig({ clockTolerance: v })}
value={profile.clockTolerance}
onChange={(v) => patchPrinterProfile({ clockTolerance: v })}
/>
)}
</ZplSubField>
Expand All @@ -145,8 +169,8 @@ export function ClockAndTimeTab() {
label={loc.clockLanguage}
values={CLOCK_LANGUAGE_VALUES}
isValid={isClockLanguage}
value={label.clockLanguage}
onChange={(v) => setLabelConfig({ clockLanguage: v })}
value={profile.clockLanguage}
onChange={(v) => patchPrinterProfile({ clockLanguage: v })}
defaultLabel={t.printerSettings.defaultOption}
optionLabel={(m) => `${m} – ${loc[CLOCK_LANGUAGE_LABEL_KEYS[m]]}`}
/>
Expand Down
Loading
Loading