# Apparel Designer β€” Bug Report This is the active bug tracker. New regressions or freshly discovered defects belong here; once a bug is fixed, it moves to `CHANGES.md` (which is the historical record) and is removed from this file. --- ## πŸ”΄ Critical _None outstanding._ ## 🟠 Major ### B1 β€” `Cmd/Ctrl+D` on a multi-selection produces N undo entries **File:** `src/App.jsx` (keyboard-shortcut effect, around the Cmd+D branch) The handler iterates the selection set and calls `duplicateElement(id)` once per id. Each `duplicateElement` call commits its own history entry, so duplicating 3 selected elements produces 3 separate undo steps β€” the user has to press Cmd-Z three times to "undo the duplicate" they just did. The single-select path is correct (one duplicate β†’ one undo); only multi-select is wrong. Fix shape: add `duplicateMany(ids)` to `useDesignEditor` that mirrors `deleteMany`'s "single history entry, set selection to the new ids" pattern, and have the Cmd+D handler call that when `selectedIds.size > 1`. ## 🟑 Minor ### B2 β€” `[` / `]` z-order shortcuts are inert in multi-select mode **File:** `src/App.jsx` The handler is gated on `selectedId` (the single-select derived value, which is `null` whenever `selectedIds.size > 1`). With multiple elements selected, `[` and `]` do nothing. Arrow-key nudging has the same gap β€” it operates on `selectedId` only. Reasonable interpretations: - Iterate the selection (each element moves one step in z-order) β€” note that for adjacent selected layers this can produce confusing reorderings. - Treat the multi-selection as a contiguous block and shift it as a unit. Either is a real call; the current behavior (silent no-op) is the worst. A related stale-comment issue: the same handler still carries a comment "Cmd/Ctrl+A 'select all' is intentionally NOT bound here … makes sense once multi-select lands (S3)". S3 has now landed, so the comment misrepresents current state. ### B3 β€” `ImageElement.jsx` filter `useEffect` over-invalidates on width/height changes **File:** `src/components/canvas/ImageElement.jsx` The effect's dep array is `[filter, src, width, height]`, but the body only references `filter` and `src`. width/height are listed because Konva's filter cache is dimension-sensitive β€” but Konva ALSO re-caches automatically when the node's size changes, so listing them in the deps causes a redundant `node.cache()` + `clearCache()` cycle on every resize. During a continuous transformer drag this fires once per intermediate `onUpdate`, which on filtered images becomes a noticeable hot path. Fix shape: drop width/height from the deps and rely on Konva's built-in re-cache. Verify with a brief drag of a filtered image that the filter still updates correctly. ### B4 β€” `useBackgroundRemoval.removeBackground` early-return doesn't toggle `loading` **File:** `src/hooks/useBackgroundRemoval.js` When `loadModel()` fails (network error fetching the 86MB model), the early-return path returns `null` without calling `setLoading(false)`. `loadModel` itself does call `setLoading(false)` in its catch block, so today the user-visible state is consistent β€” but the contract between the two functions is fragile: if `loadModel` is ever changed to return false without resetting loading, this caller silently strands the spinner. Fix shape: defensive `setLoading(false)` in the early-return path of `removeBackground`. ### B5 β€” `useBackgroundRemoval` progress is non-monotonic and confusing **File:** `src/hooks/useBackgroundRemoval.js` Model-download phase reports 0–50 (scaled from the HF progress callback). Then `removeBackground` jumps straight to `setProgress(50)`, then `70`, `90`, `100` at fixed pipeline checkpoints. The 50β†’70β†’90 jumps are visible to the user as a stuttering bar that pauses, leaps, pauses, leaps. It still finishes β€” just looks broken. Fix shape: either (a) replace the progress bar with an indeterminate spinner once the model is loaded β€” the per-image inference is fast enough that fake-stepping doesn't help, or (b) genuinely time-budget the post-load steps and report linearly. ### B6 β€” `UploadTab.jsx` uses bare `alert()` for validation errors **File:** `src/components/sidebar/UploadTab.jsx` Three `alert()` calls β€” file-type rejection, file-size rejection, upload-failure β€” bypass the app-wide toast system that #19 introduced. These are the only `alert()` calls left in the codebase. The toast system has the right "info / error" kinds for these messages. Fix shape: thread `showToast` into UploadTab (via Sidebar prop drilling, or via a context) and replace the `alert` calls. Note: this IS a user-visible change (alert vs toast looks different) β€” log it here rather than fix immediately, per the "no user-facing changes" instruction. ### B7 β€” `UploadTab.placeImage` hard-codes 300 for canvas centering **File:** `src/components/sidebar/UploadTab.jsx` The drop-zone path computes `(300 - width) / 2` and `(300 - height) / 2` to center an uploaded photo. 300 is the current `canvasSize`, but `getActiveProduct().canvasSize` is the source of truth. Drift risk: if the active product is ever swapped to one with a different canvas size, uploaded photos won't center correctly. Fix shape: `import { getActiveProduct } from '../../constants/products'` and read `canvasSize` once at module top (or per-call). ### B8 β€” `OfflineIndicator` initializes state without an SSR guard **File:** `src/components/OfflineIndicator.jsx` `useState(!navigator.onLine)` runs at module-execution time inside the function component. There's no guard for a missing `navigator`. The app isn't SSR'd today, but this is the pattern that breaks first if anyone tries β€” and it's gratuitous; the rest of the codebase consistently guards (`typeof window === 'undefined'`). Fix shape: `useState(() => typeof navigator !== 'undefined' ? !navigator.onLine : false)`. ### B9 β€” `SlotPlaceholder` mouseLeave forces `cursor: 'default'` **File:** `src/components/canvas/SlotPlaceholder.jsx` `handleMouseLeave` sets `stage.container().style.cursor = 'default'`. This stomps on whatever cursor was inherited (e.g. another shape that sets `'pointer'` via its own enter handler may not get to restore on the next move event because we just hard-set 'default'). The standard idiom is `'auto'` or `''` to clear the inline style and fall back to the inherited cascade. In practice the bug is hard to trigger today (the only other shape-level cursor handler IS this same one), so it's classified Minor β€” but it's a latent foot-gun. ### B10 β€” `persistence.js` `stripUnpersistable` filter narrowness **File:** `src/utils/persistence.js` The filter only inspects `type === 'image'` elements. Today stickers (`type: 'sticker'`) have data URLs, not blob URLs, so this works. But the contract isn't symmetric with reality β€” if any future code path produces a sticker with a `blob:` src (e.g. a sticker authored from a blob URL on first paste), it won't be filtered, and reload will produce a broken sticker. Fix shape: filter on src URL prefix, not type β€” `if (typeof el.src === 'string' && el.src.startsWith('blob:')) drop`. Same intent, narrower-than-needed gate today. ### B11 β€” `useExport` progress simulator is misleading **File:** `src/hooks/useExport.js` `progressInterval` advances `progress` 0β†’90 in 10% steps every 200ms while the export is in flight, regardless of actual upload/render progress. On a fast export, the user sees 30% before the request even arrives at the server. On a slow export, the bar pegs at 90% and waits silently for whatever's actually slow. Two real fixes: 1. Wire to fetch's upload-progress events (XHR has them; fetch+ReadableStream support is patchy but adequate for our use case). Server-render time is opaque from the client, so progress will still cap at "uploaded" and then sit indeterminate. 2. Replace the bar with an indeterminate spinner β€” honest about the fact that we don't know how far along the server is. ### B12 β€” `handleShare` has hardcoded English strings not in the i18n catalog **File:** `src/App.jsx` (`handleShare`) `'My Pawfectly Yours design'` (share title) and `'Check out the shirt I made!'` (share text) are passed directly to `navigator.share`. The S23 catalog covers toast messages and button labels but missed these because they don't render in the DOM β€” they go straight to the OS share sheet. Should be added to the catalog and pulled via `t()`. Same applies to several inline strings in App.jsx render β€” FAB aria-labels (`'Close customization options'` / `'Open customization options'`), export-toast verbiage (`'Saving design…'`, `'⚠️ Save failed:'`, `'βœ… Saved!'`, `'Download'`, `'Dismiss'`). All logged collectively under the S10 i18n-sweep follow-on, but B12 specifically calls out the share strings since they're easy to miss in a DOM-text grep. --- ## Closed since the May 2026 audit The original May 2026 audit identified twelve critical (C1–C12), seven major (M1–M7), and nine minor (m1–m9) bugs. **All of them have been fixed.** See `CHANGES.md` for the per-bug summary, the files touched, and the verification steps. The performance contract for the drag/rotate hot path (M4) is the most operationally important section β€” it documents the constraints any future change to `DesignCanvas.jsx`'s bound functions must respect. Subsequent fix rounds (the polish/UX work tracked outside this file under issue numbers like #1–#35) are also captured in `CHANGES.md`. Items addressed there but originally listed in `SUGGESTIONS.md` have been removed from that file too. --- ## Notes on items deliberately not flagged as bugs These are observations from the audit that were *not* bugs at the time and are kept here as ongoing watch-items so future contributors don't re-audit ground we've already considered. - **Express 4 wildcard routes** (`app.all('/api/*')`, `app.get('*')`) are correct for the pinned `^4.18.2`. These would break under Express 5's path-to-regexp v6 and are worth revisiting if/when the project upgrades, but they aren't bugs today. - **`canvas` package version `^2.11.2`** is older than the current 3.x line. 2.x is still maintained and works; mentioning here only for transparency. - **CORS in dev allows all origins** β€” intentional, gated on `IS_PRODUCTION`. Not a bug. - **`saveToHistory` JSON-stringify dedupe** β€” uses string equality on `JSON.stringify(elements)`, which is sensitive to numeric formatting (`0` vs `0.0`, key ordering between `Object.assign` results, etc.). In practice React's setState updaters produce stable object shapes so this is fine, but if a future code path constructs elements differently, identical-content snapshots could end up as separate history entries. Worth keeping aware of, not worth fixing speculatively. - **`useBackgroundRemoval.hasModel`** β€” returned from the hook but ignored by every consumer (only `BackgroundRemovalButton` calls the hook). Not a user-visible bug; logged as S21 in suggestions for cleanup. - **`useTemplate.templateRef`** β€” set in `loadTemplate` and `clearTemplate` but never read anywhere. Dead state; logged as S22 in suggestions.