Skip to content

Analysis: Phase 3.6c — Discovery Remediation

Phase 3.6c is an unplanned phase added after Phase 3.6 (SPA integration) completion. It captures issues discovered during late-stage review of arda-frontend-app#742 that must be addressed (or explicitly deferred) before Phase 3.7 (release) can proceed.

This phase is treated as a collaborative exploration: some items will be fixed here, others will be confirmed as gaps and deferred with an explicit decision. Fixes that span repositories will be developed against local package links first (npm link --no-save ../ux-prototype) and verified end-to-end against the dev backend via Playwright before any commits are pushed.

  • Phase 3.6 committed on PR #742 (CI green, all unit tests pass)
  • PR #742 rebased onto current main (PR #741 merged)
  • Local dev workflow functional: arda-frontend-app can consume a locally built ux-prototype via npm run dev:local
  • Dev backend reachable; test credentials miguel-new-dev@arda.cards available for E2E verification
  • End-to-end happy-path image upload flow verified on Amplify preview (pr-742.d38w5m1ngjza76.amplifyapp.com) — usable for business-user feedback
  • Each discovered issue is either:
    • Fixed and merged into PR #742 (or a dependent ux-prototype release), or
    • Explicitly deferred with a written decision recorded in the project decision log
  • Decisions on the deferred items (ItemCardEditor feature parity and Documint CDN authorization) are documented and agreed before Phase 3.7 begins

Three discovered issues and one feature-parity gap. Only items (1) and (2) are expected to produce code changes in this phase. Items (3) and (4) are exploration-only; their outcomes feed into a joint decision made after (1) and (2) land.

#ItemTypeExpected outcome in 3.6c
1Image hover preview sizingBug fixFix in ux-prototype, bump design-system, bump in arda-frontend-app
2Grid image double-click doesn’t open editorBug fixFix in ux-prototype (popover pointer-events: none), bump through
3ItemCardEditor feature parity gap (management#861)Gap confirmationWritten decision: block release or defer legacy ItemCard removal
4Documint CDN authorization for printExplorationOptions document; decision made jointly with (3)

Issue 1 — Image hover preview sizing — RESOLVED (already fixed on main)

Section titled “Issue 1 — Image hover preview sizing — RESOLVED (already fixed on main)”

Not an open issue. Verified by Playwright MCP on 2026-04-13 against the rebased PR #742 tip (head 9dab458) with official @arda-cards/design-system@4.11.2 running against the dev backend (https://dev.alpha002.io.arda.cards).

  • Hovering the image thumbnail of item tst-1 opens a Radix popover at 256 × 256 px, in viewport at (828, 370), with white background (bg-popover), visibility: visible, opacity: 1, pointer-events: auto, and the real CDN image rendered inside.
  • DOM dump confirms w-64 h-64 p-2 bg-popover border-border shadow-md all resolve to concrete CSS. No tree-shake regression on the current commit.
  • See screenshot scratch/issue1-hover-dev-backend.png.

The earlier notes in this project folder described two different symptoms — “popover renders at 18 px height” (Friday) and “nothing opens at all” (today-morning, against MSW in mock mode). Neither reproduces now:

  • The “18 px” observation predates the upgrade from design-system 4.11.0 to 4.11.2 and the main branch adopting the Tailwind v4 @source directive (see below). It was consistent with the tree-shake root cause described in earlier drafts.
  • The “nothing opens” observation was from a Playwright MCP session against --env=mock. Mock mode is a dead-end under Playwright MCP because MSW’s service worker refuses to register in the automation browser context (see knowledge-base/local-dev-and-preview-testing.md, §B). No items rendered in the grid at all, so hover could not trigger. This was a test-harness artifact, not a product bug.

Not by this project. Fix landed on main in commit 1376007 (“Replace local color picker with design system ColorPicker in card editor”) by nail60 on 2026-04-08, as a side effect of integrating the design-system ColorPicker. The fix is a single line in arda-frontend-app/src/app/globals.css:5:

@source "../../node_modules/@arda-cards/design-system/dist/*.js";

This is a Tailwind v4 @source directive telling the consumer’s Tailwind scanner to include the built design-system dist/*.js files when generating utility classes. Effect: every utility used in a canary component (w-64, h-64, top-[50%], translate-x-[-50%], etc.) is emitted in the consumer’s CSS regardless of whether the consumer’s own source uses the class name.

ux-prototype/src/components/canary/molecules/image-hover-preview/image-hover-preview.tsx:102 is unchanged — it still uses className={cn('w-64 h-64 p-2 bg-popover border-border shadow-md')}. The fix is entirely consumer-side.

Alignment with the options this document originally proposed

Section titled “Alignment with the options this document originally proposed”
OptionStatus
A — Inline styles on affected elementsNot used. Previously tried and reverted.
B — Bundled CSS module in ux-prototype (was the recommended default)Not used.
C — Shared Tailwind contract so the consumer scans design-system classesEffectively implemented, in lightweight form. Not a shared preset that consumers import and extend — instead, a single @source directive in globals.css. Achieves the same outcome (consumer Tailwind sees all canary classes) with much less configuration overhead.

The Option-C-lite implementation is arguably better than the originally recommended Option B because it solves the class of bug (every canary Tailwind utility resolves) rather than one instance. No further action needed for the sizing symptom on this PR.

  • Glob coupling. The @source target is a glob (dist/*.js). If ux-prototype restructures its publish layout (for example, moving canary builds into dist/canary/), the consumer glob must be updated or the scan silently misses classes.
  • Tailwind v4 only. @source is a Tailwind v4 CSS-first config feature. The consumer is already on 4.1.7; a future downgrade would require a substitute.
  • Opaque to the design-system. The design-system has no way to know which consumers correctly opted in. A consumer that forgets the @source line will reproduce the original bug and debug it from scratch. A future Option C full preset would make the coupling explicit at install-time.

If any of these become true, upgrade from the @source glob to a published @arda-cards/design-system/tailwind preset with a documented integration:

  1. A second consumer (beyond arda-frontend-app) starts importing canary components and must be onboarded.
  2. The design-system’s dist/ layout changes and breaks multiple consumers that glob-scanned it.
  3. The design-system begins to ship Tailwind plugin requirements (e.g. a custom @plugin for accent-color mapping) that must be adopted by every consumer.

A preset would live at ux-prototype/tailwind-preset.ts, export the theme/safelist/plugins used by the canary components, and would be imported by consumers in their globals.css via @import "@arda-cards/design-system/tailwind";. See the inline comment on the @source line in arda-frontend-app/src/app/globals.css for the tracking note. No GitHub ticket has been filed — the glob works today and no second consumer is queued.

ImageHoverPreview uses the Tailwind utility class h-64 on its popover content. This class is not present in the consuming app’s Tailwind build output because the consumer’s Tailwind content-scan does not reach the design-system canary bundle’s class names. Tailwind tree-shakes utilities it does not see, so h-64 is never generated and the element falls back to its intrinsic height (roughly the line-height of the loading placeholder — about 18 px).

The same root cause also affects Dialog centering via top-[50%], left-[50%], translate-x-[-50%], translate-y-[-50%], producing a subtly mis-centered dialog.

  • ux-prototype/src/components/canary/molecules/image-hover-preview/image-hover-preview.tsx:102
  • ux-prototype/src/components/canary/atoms/dialog/dialog.tsx:54

An inline style={{ height: '16rem' }} was tried and reverted. It worked but bypassed the design-system’s own styling contract.

Replace consumer-dependent Tailwind utilities on pre-built canary components with values the design-system owns and ships in its CSS bundle. Options to evaluate locally:

  • A — Inline styles on affected elements (simple; re-adopts the previously-reverted approach with a clearer justification)
  • B — Bundled CSS module class defined in ux-prototype and imported from the component (keeps styling declarative and owned by the design-system)
  • C — Publish a Tailwind preset from ux-prototype that consumers add to their content: paths (larger change, benefits every consumer)

Both options keep styling declarative and prevent the tree-shake regression, but they operate at very different layers. The questions that separate them: “whose CSS pipeline owns the class?” and “what happens when the consumer writes new markup that uses a design-system utility?”

Option B — bundled CSS module (or plain CSS file) shipped in the canary bundle

How it works: in ux-prototype, the affected component imports a local CSS file (e.g. image-hover-preview.module.css) with the hard-coded rules (height: 16rem, top: 50%, transform: translate(-50%, -50%)). Vite inlines that CSS into the canary bundle’s side-effect CSS import. Consumers don’t need to configure anything — importing the component pulls in the CSS.

Pros:

  • Zero consumer configuration. The fix is invisible to every consumer — no tailwind.config changes, no content-scan paths, no preset import.
  • Scoped to the components that need it. CSS Modules hash the class names, so no risk of colliding with consumer styles.
  • Fast iteration. Change the CSS, bump the design-system patch version, consumers get the fix on npm install.
  • Backward-compatible. Works for every consumer regardless of whether they use Tailwind, CSS Modules, styled-components, or plain CSS.
  • No runtime cost. CSS is inlined at build time; no extra network requests or JS overhead.

Cons:

  • Only fixes the known incidents. Every new canary component that uses a Tailwind utility risks the same tree-shake regression, unless we discipline ourselves to either use the bundled-CSS pattern or avoid Tailwind utilities in canary components entirely.
  • Style duplication. If a component uses h-64 in one place and the bundled CSS re-declares height: 16rem, we have two sources of truth for the same value. (Mitigated by the CSS pulling from the same design tokens if we expose them.)
  • Discoverability. A future contributor may not know why the component uses a CSS module instead of the team’s dominant Tailwind style, and may accidentally “clean it up” back to Tailwind utilities.

Option C — published Tailwind preset

How it works: ux-prototype exports @arda-cards/design-system/tailwind (a preset describing the theme/plugins/safelist), and consumers do two things:

  1. Add the preset to tailwind.config.ts: presets: [require('@arda-cards/design-system/tailwind')]
  2. Add the design-system’s component source (or its pre-built class list) to the content: array so Tailwind can scan it.

The design-system’s Tailwind utilities (h-64, top-[50%], etc.) then appear in the consumer’s generated CSS regardless of whether the consumer writes those class names in its own source.

Pros:

  • Fixes the class of bug, not the incident. Every current and future canary component that uses a Tailwind utility “just works” in consumers that install the preset — no per-component maintenance.
  • Single source of truth for design tokens. Theme extensions (colors, spacing, typography) live in the preset. Consumers inherit them automatically; divergence between design-system tokens and consumer tokens is no longer possible.
  • Aligns with the canary bundle’s existing assumption. The canary components were written assuming Tailwind is available in the consumer — Option C makes that assumption true by contract rather than by accident.
  • Surfaces Tailwind plugin requirements (e.g. arbitrary value support, container queries) at a single integration point.

Cons:

  • Breaking change for consumers. Every consumer must update its tailwind.config on the design-system bump. If a consumer has customized its theme in ways that conflict with the preset, resolution may be non-trivial.
  • Tailwind version coupling. The preset is tied to a specific Tailwind major version. arda-frontend-app runs Tailwind v4; if another consumer runs v3, we need two presets or a peer-dep constraint.
  • Consumer build-time cost. Adding the design-system source to the content: array expands the Tailwind scan set; in very large projects this is measurable.
  • Only helps Tailwind consumers. A consumer that chose a different styling stack gets nothing from this option.
  • Larger blast radius for the fix. A broken preset can break every downstream consumer’s build, not just the hover-preview feature.

Option B solves the immediate bug and every instance of “the canary bundle uses a Tailwind utility that the consumer doesn’t regenerate.” That is a real problem, but it is the same problem we will keep re-solving component-by-component every time a canary molecule is added or tweaked. The pattern is fragile: any future PR that writes className="w-96" in a canary component will silently regress in consumers until someone spots the 18 px popover again.

Option C changes the contract from “the consuming app’s Tailwind must happen to generate whatever classes the canary bundle uses” (implicit, fragile) to “the consuming app adopts the design-system’s Tailwind preset” (explicit, enforceable). It is the only option that makes the root cause go away rather than bandaging its symptoms.

However, C only pays off when:

  • There will be several future canary components with this shape (likely true — the canary namespace is actively growing), and
  • Consumers are on compatible Tailwind versions (currently true for the only in-tree consumer, arda-frontend-app).

If both conditions hold, C is the correct long-term investment and B is a temporary bandage. If the canary namespace is about to freeze or consumers diverge on Tailwind versions, B is sufficient.

Proposed path: do B now to unblock the PR #742 release, and open a follow-up ticket to evaluate C as a proper design-system-wide contract change in the next design-system iteration. That decision is also tied to the outcome of Issue 3 (ItemCardEditor parity) — if the design system is about to take on more consumer-facing work, C becomes more valuable.

Verify via Playwright MCP against make dev (with @arda-cards/design-system locally linked) that:

  • Hovering any grid image cell shows a preview of the configured size
  • The ImageUploadDialog is centered horizontally and vertically

Issue 2 — Grid image cell edit trigger does not open the upload dialog

Section titled “Issue 2 — Grid image cell edit trigger does not open the upload dialog”

On the items grid (/items, Published tab) against the dev backend, with tst-1 row selected, every attempted edit trigger fails to open ImageUploadDialog:

  • Double-click on the thumbnail (<img alt="Item">) — no dialog.
  • Double-click on the enclosing AG Grid cell [col-id="imageUrl"] — no dialog.
  • Single-click to select, then press Enter — no dialog.
  • Single-click to select, then press F2 (AG Grid’s default edit-start key) — no dialog.
  • Synthetic MouseEvent('dblclick', { bubbles: true }) dispatched on the cell — no dialog.

After each attempt, document.querySelectorAll('[role="dialog"][data-state="open"]') returns 0 elements and the cell’s AG Grid class stays ag-cell-not-inline-editing.

Root cause — current code inspection (no prior attempt in tree)

Section titled “Root cause — current code inspection (no prior attempt in tree)”

The wiring across ux-prototype and arda-frontend-app is:

LayerFileWhat it does
Column defarda-frontend-app/src/components/table/columnPresets.tsx:1273-1284editable: true, cellRenderer: ImageCellDisplay, cellEditor: createImageCellEditor(ITEM_IMAGE_CONFIG), cellEditorPopup: true. No onCellDoubleClicked, no suppressClickEdit.
Grid optionarda-frontend-app/src/app/items/ItemTableAGGrid.tsx:349singleClickEdit: false (default AG Grid double-click-to-edit).
Cell rendererux-prototype/src/components/canary/atoms/grid/image/image-cell-display.tsxWraps thumbnail in ImageHoverPreviewPopoverAnchor asChild → 28×28 div → ImageDisplay<img>.
Hover previewux-prototype/src/components/canary/molecules/image-hover-preview/image-hover-preview.tsxOuter <div> with onMouseEnter/onMouseLeave; inner Radix Popover with PopoverAnchor asChild around the children and PopoverContent positioned relative to the anchor. No click handlers, no stopPropagation, no pointer-events override anywhere in the tree.
Displayux-prototype/src/components/canary/molecules/image-display/image-display.tsxRenders as a plain <div> when onImageChange/config are not provided (the cell renderer path), or as a <button> with onDoubleClick when they are (standalone editable path).
Cell editorux-prototype/src/components/canary/atoms/grid/image/image-cell-editor.tsxImageCellEditor forwards ref, declares isPopup() => true, mounts ImageUploadDialog with dialogOpen=true on mount.

No attempted fix for this issue exists in the current code. The tree does not contain any pointer-events: none, onCellDoubleClicked, suppressClickEdit, or stopPropagation specifically related to the image cell. The wiring assumes AG Grid’s default double-click-to-edit behavior will fire because the column is editable: true.

Every keyboard and mouse path that should normally trigger AG Grid’s edit mode (dblclick, Enter, F2) was verified to fail on the live app. Because F2 and Enter fail just as dblclick does, the root cause is not Radix intercepting pointer events (a pointer-events fix would not affect F2). The likely proximate cause is one of:

  1. AG Grid’s click/keyboard edit activation is suppressed somewhere higher in the grid configuration (a gridOptions prop, a wrapper, or the suppressClickEdit/suppressRowClickSelection plumbing) that we have not yet located.
  2. The createImageCellEditor wrapper is not being recognized by AG Grid as a valid cell editor (wrong shape of returned component), so AG Grid declines to enter edit mode.
  3. A runtime error during startEditingCell silently aborts the edit (no console error was observed during reproduction, so this is less likely).

Suggested investigation before coding a fix

Section titled “Suggested investigation before coding a fix”
  • Instrument onCellDoubleClicked and onCellKeyDown on the grid to confirm AG Grid sees the events on the image cell.
  • Inspect AG Grid’s emitted events via params.api.addEventListener(...) during reproduction — specifically cellEditingStarted / cellEditingStopped.
  • Verify that createImageCellEditor(...) returns a component that passes AG Grid’s cellEditor contract check. Note: the factory uses forwardRef and returns the inner WrappedEditor, which also uses forwardRef — two layers of forwardRef. If AG Grid introspects the component (e.g. checks displayName or a specific prop shape), the double-wrap could confuse it.
  • Confirm the column definition reaching AG Grid at runtime via api.getColumnDef('imageUrl') — the transformation at ItemTableAGGrid.tsx:343-350 spreads ...col and overrides editable; confirm the spread does not lose cellEditor / cellEditorPopup.

Before writing any fix, reproduce the trigger with a grid-event listener attached, so we know which layer is swallowing the edit activation. The original hypothesis (Radix popover pointer capture) no longer fits the reproduction evidence — F2 should bypass the popover entirely, yet F2 also fails.

Verification criteria for the eventual fix

Section titled “Verification criteria for the eventual fix”
  • Single-click on an image cell selects the row.
  • Hover continues to show the preview at 256 × 256.
  • Double-click on an image cell opens ImageUploadDialog.
  • Focused cell + Enter opens the dialog (AG Grid’s keyboard-edit path).
  • Focused cell + F2 opens the dialog.
  • Confirming the dialog persists the image URL through the normal row-dirty / publishRow pipeline.

Resolution — bypass AG Grid cell-editor lifecycle (FD-20)

Section titled “Resolution — bypass AG Grid cell-editor lifecycle (FD-20)”

After several iterations against AG Grid 34.3.1’s editor lifecycle, the root incompatibility is structural: AG Grid assumes the editor renders within the cell’s focus scope, and a Radix-portalled modal does not. With cellEditorPopup:true AG Grid wraps the editor in a 0×0 container whose hidden subtree defeats Radix focus-trap; without it, stopEditingWhenCellsLoseFocus:true tears the editor down as soon as the Radix portal steals focus. Neither path produces a visible dialog in a real browser, and the isPopup() runtime handle only suppresses AG Grid’s tooltip feature — it does not exempt the editor from the focus-loss teardown.

FD-20 moves the image-edit trigger out of AG Grid’s editor lifecycle. columnPresets.tsx no longer marks the image column editable; ItemTableAGGrid captures double-click / Enter / F2 at the grid level, opens a wrapper-scoped ImageUploadDialog, and on confirm writes the new URL via applyTransaction({ update: [...] }) and marks the row dirty through the existing dirtyRowIdsRef / publishRow pipeline already used by onNotesSave and onCardNotesSave. Image edits therefore integrate with other cell edits in the same row through a single debounced publish. See FD-20 in the decision log for the full rationale, tradeoffs, and rejected alternatives.

Issue 2b — No-image cell hover shows empty gray background instead of “No Image Available”

Section titled “Issue 2b — No-image cell hover shows empty gray background instead of “No Image Available””

Row 2 in the items grid (tst-2) has no image set (imageUrl === null). Hovering its image cell shows a light gray rectangle only — no message, no caption.

Expected: a “No Image Available” caption centered in the middle of the hover area, so users understand the cell is an empty state rather than a broken image or a missing preview.

Two components combine to produce the current behavior:

  • ImageHoverPreview.handleMouseEnter (ux-prototype/.../image-hover-preview.tsx:77-82) early-returns when imageUrl === null, so the Radix popover does not open for empty-image cells. The user sees no popover at all.
  • ImageDisplay in the cell renders the initials placeholder (entity initial, e.g. I for Item) on a bg-muted background (ux-prototype/.../image-display.tsx:146-160). This is what the user perceives as “light gray” — the cell itself, not a popover.

So the current design is “no popover for no image; cell shows initials on gray.” The user is asking for “popover on hover, text ‘No Image Available’.”

Open question: where should “No Image Available” live?

  • Option P — Popover that opens for null images too. Remove the imageUrl === null early-return in handleMouseEnter. Render a popover of the same size with a centered “No Image Available” message instead of an ImageDisplay. Consistent with the hover- preview behavior for images that exist.
  • Option Q — Caption inside the cell itself. Show the caption in the cell renderer when imageUrl === null, replacing the initials placeholder. No popover change. Consistent with the cell’s self-descriptive nature but changes the cell layout.
  • Option R — Both. Show the caption in the cell and open a popover with the same caption on hover. Most discoverable; most churn.

Recommendation: Option P. Matches the existing “hover shows a bigger preview” pattern, keeps the cell compact, and introduces one small behavioral change (popover opens for null) rather than restructuring the cell renderer.

  • Hover on tst-2 (no image) → popover opens, centered text “No Image Available”, same 256 × 256 footprint, same entry delay (500 ms).
  • Hover on tst-1 (with image) → unchanged behavior.
  • Hover debounce and auto-close on mouse-leave behave identically for both.
  • Translation/i18n: if the project has a translation layer, the caption becomes a localized string; otherwise it is a literal.

Issue 3 — ItemCardEditor feature parity gap (management#861)

Section titled “Issue 3 — ItemCardEditor feature parity gap (management#861)”

Original gap statement (from project follow-up brief)

Section titled “Original gap statement (from project follow-up brief)”

The brief listed two items as blocking legacy ItemCard removal:

  • Three size selectorscardSize, labelSize, breadcrumbSize
  • Inline validation error display — validation messages surfaced only at form level, not adjacent to the offending input

Verification against current code (2026-04-13)

Section titled “Verification against current code (2026-04-13)”

Both items were audited against the rebased PR #742 tip. Neither is a regression caused by the ItemCardItemCardEditor substitution.

The three size selectors are rendered by ItemFormPanel itself, not by either card component:

  • arda-frontend-app/src/components/items/ItemFormPanel.tsx:2200-2260 — three <FormField> + <Select> blocks for cardSize, labelSize, breadcrumbSize, wired to form.cardSize / form.labelSize / form.breadcrumbSize via handleFormChange.
  • Legacy itemCard.tsx never rendered these fields — a grep of its inputs list shows only minQty, minUnit, orderQty, orderUnit, and supplier (static display). The three size fields were always outside the card, on the form panel.
  • ItemCardFields in ux-prototype/src/components/canary/organisms/item-card-editor/item-card-editor.tsx:23-31 likewise contains only title, minQty, minUnit, orderQty, orderUnit, imageUrl, accentColor — by design, card-layout fields only.

Conclusion: the size selectors are already present and functional in the current build. There is nothing to restore. No ux-prototype change is needed for this item, and the legacy ItemCard is not needed to preserve these selectors.

Current validation model (ItemFormPanel.tsx:829-849):

  • validateForm() pushes short summary strings (“Check your card details”, “Incompatible image format”) into a top-of-panel error banner (ItemFormPanel.tsx:1250-1268).
  • One exception: the image field has a dedicated inline error state (imageFieldError / DATA_URI_IMAGE_ERROR) surfaced at the image input, both before and after the switch to ItemCardEditor.
  • Neither itemCard.tsx nor item-card-editor.tsx exposes per-field error / helperText / invalid props. A grep across both shows no error-adjacent-to-field rendering.

Conclusion: there is no “inline validation regression” between ItemCard and ItemCardEditor — both follow the same summary-banner model, and the one genuinely inline case (image URL validation) works identically in both. If per-field inline validation is desired, it is a new feature rather than parity restoration, and it belongs to a design-system-wide UX initiative, not to Phase 3.

After verification, the actionable residual work from management#861 is considerably smaller than the brief suggested. Items that were audited:

Claimed gapVerification result
3 size selectors missing from ItemCardEditorNot a gap — rendered by ItemFormPanel, always was
Inline validation missing from ItemCardEditorNot a regression — legacy ItemCard had none either; one inline case (image URL) works in both

Open questions for management#861 scope, to confirm with the ticket’s author before Phase 3.7:

  • Is there any other feature of legacy ItemCard that ItemCardEditor does not cover? (Nothing surfaced from this audit; an end-to-end side-by-side comparison of the two card previews is the next step if doubt remains.)
  • Is per-field inline validation a desired new feature? If so, does it belong to a design-system-wide UX pass rather than to this project?

The audit removes the technical blocker that motivated keeping legacy ItemCard alive, but the legacy component will not be removed as part of this project. The cleanup is deferred to a separate follow-up to keep this project’s scope contained.

Phase 3.7 will:

  • Create a GitHub ticket in Arda-cards/arda-frontend-app (or the appropriate management tracker) titled “Remove legacy ItemCard after image-upload-frontend project” with the audit findings above as its rationale, and a link back to this analysis.
  • Retain src/components/items/itemCard.tsx, its @deprecated marker, and the currently-dead rollback scaffolding in ItemFormPanel.tsx unchanged in this release.
  • Close management#861 (or update its scope) to reflect that the originally-claimed gap does not exist, linking to the new removal ticket for tracking the actual remaining cleanup work.

Original consequence (retained for history)

Section titled “Original consequence (retained for history)”

Legacy ItemCard remains in arda-frontend-app (marked @deprecated) specifically because removing it would regress these features. The project cannot declare the ItemCardEditor migration complete until this gap closes.

This statement is no longer accurate given the audit above. The legacy component is retained for reasons of scope containment, not because a feature parity gap blocks its removal.

Confirmation only. Implementing the three selectors and inline validation in ItemCardEditor is a non-trivial change to the design system and is tracked under Arda-cards/management#861.

Decision to be made after Issues 1 and 2 land:

  • Option A: Implement parity now in this project (expand scope, delay Phase 3.7)
  • Option B: Ship Phase 3.7 with legacy ItemCard still present and @deprecated; close the gap as a follow-up project
  • Option C: Hybrid — ship minimal parity (size selectors only, no inline validation) in this project; defer inline validation

Default recommendation pending discussion: Option B — the deferral is already in place and does not block end-users; the gap is tracked.

Issue 4 — Documint CDN authorization for image print

Section titled “Issue 4 — Documint CDN authorization for image print”

Printing items via Documint requires Documint’s render servers to GET the image at its CDN URL. The CDN is CloudFront with signed cookies (FD-10), so unauthenticated requests receive HTTP 403. Documint has no mechanism to attach signed cookies or Authorization headers when fetching images, so images in printed documents will fail to render.

This was discovered after Phase 3.6 implementation and was not anticipated in the original specification.

  • Image assets must not be world-readable on CloudFront (a principal FD-10 decision) — switching to unsigned URLs is not acceptable without a separate authorization decision.
  • Documint accepts a fully-formed image URL in the template; it does not support per-request signing callbacks.
  • The operations backend already holds the CloudFront signing key (Secrets Manager) and mints signed cookies for the SPA.
  • Option A — Signed URLs for Documint-bound images. Generate a CloudFront signed URL (query-string style, not cookie) with a short TTL at print time. The print request to Documint includes the signed URL in the template payload. Pros: keeps CDN private; minimal infrastructure change. Cons: signed URLs leak in Documint logs and rendered PDF metadata; TTL must cover worst-case render latency.

  • Option B — Proxy via arda-frontend-app BFF. Add a BFF route (e.g., GET /api/print-asset/:id) that streams the image from CDN using server-side credentials. Documint fetches from this URL instead of CDN directly. Pros: CDN stays private; no signed URLs leak. Cons: bandwidth passes through the BFF; the route needs its own authentication model for Documint, which is an external service (IP allow-list, shared secret, or signed request token).

  • Option C — Origin access with a Documint-specific IAM principal. Grant Documint’s outbound fetchers access to the CDN distribution or origin via IP allow-list or a CloudFront viewer policy. Pros: no URL handling changes in the client. Cons: Documint’s egress IPs are not documented as stable; maintenance burden; weakens the “private by default” posture.

  • Option D — Render server-side and hand Documint a PDF. Skip Documint for image-bearing templates and render PDFs via an internal service (Puppeteer or similar) that can attach cookies. Pros: removes the Documint constraint. Cons: large scope; Documint is already integrated for the text/label flow.

  • Option E — Stateless pre-signed per-print URLs via short-lived tokens. Extend the operations signer to mint per-asset, per-print-job CloudFront signed URLs keyed to a rotating print-job token. Variant of Option A with tighter TTL/scope.

Exploration only. Produce a short options document (this section is the seed) and surface open questions. A decision is made jointly with Issue 3 and captured in the project decision log before Phase 3.7 begins.

Open questions to answer during exploration:

  1. What is Documint’s actual fetch behavior? (Headers? Retries? Caching?)
  2. Is Documint’s outbound IP range documented and stable enough for Option C?
  3. What is the 95th-percentile time between print-job creation and Documint’s image fetch? (Drives acceptable TTL for Options A / E.)
  4. Does Documint strip or preserve URL query strings in its generated PDFs’ metadata? (Affects signed-URL leakage risk.)
  5. Are image-bearing Documint templates already in production use, or is this a new capability gated by this project?
  1. Rebase PR #742 onto current main (PR #741 merged). Commit, push, monitor via /pr-steward until CI is green and Amplify preview pr-742.d38w5m1ngjza76.amplifyapp.com is live.
  2. Fix Issue 1 locally (npm link --no-save ../ux-prototype). Verify via Playwright MCP against make dev + dev backend.
  3. Fix Issue 2 locally in the same linked setup. Verify via Playwright MCP.
  4. Happy-path E2E verification against the dev backend: upload-via-file → upload-via-URL → grid display with CDN cookies → 403 recovery → double-click to edit → hover preview.
  5. Publish fixes: commit and push to ux-prototype (patch release @arda-cards/design-system@4.11.3 or equivalent), bump in arda-frontend-app, push to PR #742, monitor via /pr-steward.
  6. Confirm Issue 3 gap with a short written status; do not implement.
  7. Explore Issue 4 options; produce short decision memo.
  8. Joint decision on Issues 3 and 4. Record in project decision log.
  9. Proceed to Phase 3.7.
  • Implementing ItemCardEditor feature parity (3 size selectors + inline validation). Tracked separately under management#861.
  • Implementing the Documint/CDN authorization solution. Exploration only in this phase.
  • Removing the legacy in-app ItemCard. Deferred until management#861 resolves.