From db4fd734973b2531d810506ca4d58bef28b173ce Mon Sep 17 00:00:00 2001 From: u8array Date: Sat, 9 May 2026 10:10:46 +0200 Subject: [PATCH 1/4] chore: remove unused App.css MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leftover from the Vite template — defines .counter, .hero, #docs etc. which the app never references. Tailwind is loaded via index.css and the @tailwindcss/vite plugin. --- src/App.css | 184 ---------------------------------------------------- 1 file changed, 184 deletions(-) delete mode 100644 src/App.css diff --git a/src/App.css b/src/App.css deleted file mode 100644 index f90339d8..00000000 --- a/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} From 117f4e05f295c5fa01d3a20d5a15e1e7f9ee5e61 Mon Sep 17 00:00:00 2001 From: u8array Date: Sat, 9 May 2026 10:11:12 +0200 Subject: [PATCH 2/4] refactor(canvas): drop redundant text/serial prop casts After narrowing obj.type to 'text' or 'serial', TypeScript already narrows obj.props to TextProps | SerialProps via the LabelObject discriminated union. Both members declare fontHeight: number and rotation: 'N'|'R'|'I'|'B', so the inline cast was just widening rotation to string for no reason. --- src/components/Canvas/KonvaObject.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index 44ff1492..0ebade98 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -376,7 +376,7 @@ function KonvaObjectInner({ let displayY = obj.y; if (obj.positionType === "FT") { if (obj.type === "text" || obj.type === "serial") { - const p = obj.props as { fontHeight: number; rotation: string }; + const p = obj.props; // ^FT places the origin at the baseline of the first character. // The Konva anchor point after rotation sits at a different corner // of the visual bounding box than the ZPL FT baseline origin: @@ -402,7 +402,7 @@ function KonvaObjectInner({ // Konva rotates text around its top-left corner, but ZPL's ^FO anchor // shifts with rotation. 15 dots is an empirically determined fixed offset. if (obj.type === "text" || obj.type === "serial") { - const p = obj.props as { fontHeight: number; rotation: string }; + const p = obj.props; const ROTATION_OFFSET = 15; // dots — empirical canvas/ZPL alignment correction if (p.rotation === "I") { displayY -= ROTATION_OFFSET; @@ -435,7 +435,7 @@ function KonvaObjectInner({ let finalY = pxToDots(e.target.y() - offsetY, scale, dpmm); if (obj.type === "text" || obj.type === "serial") { - const p = obj.props as { fontHeight: number; rotation: string }; + const p = obj.props; const ROTATION_OFFSET = 15; if (p.rotation === "I") { finalY += ROTATION_OFFSET; @@ -451,7 +451,7 @@ function KonvaObjectInner({ // instead of the ZPL baseline coordinate, causing a vertical jump on re-render. if (obj.positionType === "FT") { if (obj.type === "text" || obj.type === "serial") { - const p = obj.props as { fontHeight: number; rotation: string }; + const p = obj.props; const renderedH = p.fontHeight / 1.3; if (p.rotation === "N") { finalY += p.fontHeight; From 03f68041d623d202aaadfa10c0c419f5438665a3 Mon Sep 17 00:00:00 2001 From: u8array Date: Sat, 9 May 2026 10:11:35 +0200 Subject: [PATCH 3/4] refactor(canvas): replace setState-in-effect with render-phase reset ImageObject reset its htmlImg state from inside useEffect with an eslint-disable for react-hooks/set-state-in-effect. Switch to the official React 'adjusting state on prop change during render' pattern: a useRef tracks the previous cached.dataUrl, and a render-time comparison resets htmlImg before paint. Same observable behaviour, no lint exception, and the next render naturally observes the updated ref so there's no loop. --- src/components/Canvas/KonvaObject.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index 0ebade98..852e9b20 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { getFontFamily } from "../../lib/fontCache"; import { useFontCacheVersion } from "../../hooks/useFontCacheVersion"; import { @@ -246,11 +246,18 @@ function ImageObject({ const y = offsetY + dotsToPx(obj.y, scale, dpmm); const [htmlImg, setHtmlImg] = useState(null); + // Reset the cached HTMLImageElement during render when the source changes, + // instead of inside an effect. The "set state during render on prop change" + // pattern is the official React workaround for what would otherwise be a + // setState-in-effect anti-pattern. The next render observes prevDataUrl + // already updated, so this does not loop. + const prevDataUrlRef = useRef(cached?.dataUrl); + if (prevDataUrlRef.current !== cached?.dataUrl) { + prevDataUrlRef.current = cached?.dataUrl; + setHtmlImg(null); + } useEffect(() => { - if (!cached) { - setHtmlImg(null); // eslint-disable-line react-hooks/set-state-in-effect - return; - } + if (!cached) return; let active = true; const img = new window.Image(); img.src = cached.dataUrl; @@ -260,7 +267,7 @@ function ImageObject({ return () => { active = false; }; - }, [cached?.dataUrl, cached]); + }, [cached]); const handleDragMove = (e: Konva.KonvaEventObject) => { onChange({ From 4c18afc309b2f33c7d12c9b751108d220865a8aa Mon Sep 17 00:00:00 2001 From: u8array Date: Sat, 9 May 2026 10:12:05 +0200 Subject: [PATCH 4/4] docs(canvas): explain bwipjs options cast in BarcodeObject MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cast bridges our Record options builder (which varies per barcode type) and bwip-js' strict literal-string union signature. Document the trade-off so future readers don't try to 'fix' the cast by re-typing buildBwipOptions per type — that would duplicate the per-type switch already inside buildBwipOptions. --- src/components/Canvas/BarcodeObject.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/Canvas/BarcodeObject.tsx b/src/components/Canvas/BarcodeObject.tsx index 172059ec..1fc88930 100644 --- a/src/components/Canvas/BarcodeObject.tsx +++ b/src/components/Canvas/BarcodeObject.tsx @@ -65,6 +65,11 @@ export function BarcodeObject({ if (opts) { const canvas = document.createElement("canvas"); try { + // buildBwipOptions returns Record on purpose: the + // option fields differ across barcode types (ean13 vs code128 vs …) + // and per-type narrowing would duplicate the switch already in + // buildBwipOptions. bwip-js' toCanvas signature uses a strict + // literal-string union, so the structural cast bridges the two. bwipjs.toCanvas(canvas, opts as unknown as Parameters[1]); barcodeCanvas = canvas; } catch (e) {