feat(image): printer-storage UI for ~DY+^XG upload/recall#85
Conversation
Makes the ~DY+^XG feature from PR #84 usable for new images in the designer — until now it only kicked in when importing existing labels that already used the upload+recall pattern. Properties-panel section at the end of the image properties, with a consistent 'Printer storage' header (info icon, border-t separator) visible in both states: - Off-state: 'Activate' button. Toggling on sets storedAs to { device: 'R', name: 'IMG_xxxx' } with an auto-generated 4-char UUID. - On-state: device dropdown (R/E/B/A as plain letters), name input (max 8 chars, auto-uppercase, [A-Z0-9_] filter, empty-guard against broken ZPL), path preview, 'Ship bytes' checkbox + hint (decouples upload from recall: off = ^XG only, on = ~DY+^XG), 'Embed inline' button as the toggle-off. Parser extension: ^XG without a preceding ~DY is now a valid import (recall-only). Creates an image object with storedAs.embedInZpl=false and no cached bytes; surfaced as a partial finding so the import report flags the degraded preview. Cache deletion: trash button next to the image-source dropdown opens a ConfirmDialog (destructive, autoFocus on Cancel) explaining that the bytes vanish for ALL labels referencing this image. Previously only the image *object* could be removed (Del key), not the cached bytes — so old uploads accumulated in localStorage with no UI to clean them up. Canvas resize via handles: the image registry had no commitTransform, so Konva applied sx/sy as a visual scale but nothing translated that back into widthDots — the only way to resize was the properties panel. Now: cached PNG = aspect-locked via the dominant-axis drag (Math.abs heuristic so all 8 handles work for both grow and shrink); no cache (placeholder) = free-form via heightDots so the user can shape the box as a layout placeholder. Empty source selection: handleImageSelect bailed early on an empty imageId, leaving the 'Select image…' placeholder option functionally dead. Now it clears imageId + _gfaCache, which is the right answer for recall-only setups where the user wants a storedAs path without local preview bytes. Canvas placeholder: 🖼 emoji replaced by a Konva.Path rendering the Heroicons 'photo' outline. The emoji rendered inconsistently across OSes (color on macOS, monochrome on Linux, missing on some Windows configurations); the vector path is deterministic. Shared infrastructure that fell out of the work: - src/lib/storagePath.ts extended with STORAGE_DEVICES, StorageDevice, MAX_STORAGE_NAME_LEN, STORAGE_NAME_FILTER_RE, defaultStorageName — all storage-path concepts living in one canonical place so the parser, emitter, and image registry stay in lockstep. - src/components/Properties/styles.ts: new buttonCls for secondary- action buttons (Upload, toggle, Embed inline), eliminating the four-times-repeated Tailwind string across image.tsx and text.tsx. ZPL codes (^XG, ~DY) are kept to tooltips only, never in user-visible labels. New locale keys (storage, storeOnPrinter, storeOnPrinterHint, storeInline, embedInZpl, embedInZplHint, removeFromCache, removeFromCacheConfirm) cover all 32 locales.
There was a problem hiding this comment.
Code Review
This pull request introduces a "recall-only" mode for images, enabling the use of graphics already stored on a printer via the ^XG command without re-uploading them in every ZPL job. Key updates include modifications to the ZPL parser and generator to support these references, the introduction of a cross-platform PlaceholderIcon, and a new UI for managing printer storage settings like device selection and filename. The PR also adds functionality to remove images from the local cache and improves resizing behavior for placeholders. Reviewers identified a potential runtime error with crypto.randomUUID() in non-secure contexts and noted bugs where the embedInZpl state was not preserved during UI updates.
Device-selector and name-input both rebuilt storedAs as
{ device, name } without the existing embedInZpl flag. Silent
regression: a user setting recall-only (embedInZpl=false) and then
changing the device would silently flip back to the default (true),
re-emitting ~DY uploads the user explicitly opted out of.
Spread the existing storedAs so embedInZpl survives partial updates.
No description provided.