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.
Entry Criteria
Section titled “Entry Criteria”- 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-appcan consume a locally builtux-prototypevianpm run dev:local - Dev backend reachable; test credentials
miguel-new-dev@arda.cardsavailable for E2E verification
Exit Criteria
Section titled “Exit Criteria”- 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.
| # | Item | Type | Expected outcome in 3.6c |
|---|---|---|---|
| 1 | Image hover preview sizing | Bug fix | Fix in ux-prototype, bump design-system, bump in arda-frontend-app |
| 2 | Grid image double-click doesn’t open editor | Bug fix | Fix in ux-prototype (popover pointer-events: none), bump through |
| 3 | ItemCardEditor feature parity gap (management#861) | Gap confirmation | Written decision: block release or defer legacy ItemCard removal |
| 4 | Documint CDN authorization for print | Exploration | Options 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)”Outcome
Section titled “Outcome”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-1opens 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-mdall resolve to concrete CSS. No tree-shake regression on the current commit. - See screenshot
scratch/issue1-hover-dev-backend.png.
Historical symptoms (reconciled)
Section titled “Historical symptoms (reconciled)”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
mainbranch adopting the Tailwind v4@sourcedirective (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 (seeknowledge-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.
How the fix was applied
Section titled “How the fix was applied”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”| Option | Status |
|---|---|
| A — Inline styles on affected elements | Not 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 classes | Effectively 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.
Known limitations of the current fix
Section titled “Known limitations of the current fix”- Glob coupling. The
@sourcetarget is a glob (dist/*.js). Ifux-prototyperestructures its publish layout (for example, moving canary builds intodist/canary/), the consumer glob must be updated or the scan silently misses classes. - Tailwind v4 only.
@sourceis 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
@sourceline will reproduce the original bug and debug it from scratch. A future Option C full preset would make the coupling explicit at install-time.
When to upgrade to full Option C
Section titled “When to upgrade to full Option C”If any of these become true, upgrade from the @source glob to a
published @arda-cards/design-system/tailwind preset with a documented
integration:
- A second consumer (beyond
arda-frontend-app) starts importing canary components and must be onboarded. - The design-system’s
dist/layout changes and breaks multiple consumers that glob-scanned it. - The design-system begins to ship Tailwind plugin requirements
(e.g. a custom
@pluginfor 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.
Root cause
Section titled “Root cause”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.
Affected files
Section titled “Affected files”ux-prototype/src/components/canary/molecules/image-hover-preview/image-hover-preview.tsx:102ux-prototype/src/components/canary/atoms/dialog/dialog.tsx:54
Prior attempt (reverted)
Section titled “Prior attempt (reverted)”An inline style={{ height: '16rem' }} was tried and reverted. It worked
but bypassed the design-system’s own styling contract.
Proposed fix
Section titled “Proposed fix”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-prototypeand imported from the component (keeps styling declarative and owned by the design-system) - C — Publish a Tailwind preset from
ux-prototypethat consumers add to theircontent:paths (larger change, benefits every consumer)
B vs C — detailed comparison
Section titled “B vs C — detailed comparison”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.configchanges, 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-64in one place and the bundled CSS re-declaresheight: 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:
- Add the preset to
tailwind.config.ts:presets: [require('@arda-cards/design-system/tailwind')] - 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.configon 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-appruns 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.
Why C might be worth the complexity
Section titled “Why C might be worth the complexity”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.
Verification
Section titled “Verification”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
ImageUploadDialogis 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”Symptom (reproduced 2026-04-13)
Section titled “Symptom (reproduced 2026-04-13)”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:
| Layer | File | What it does |
|---|---|---|
| Column def | arda-frontend-app/src/components/table/columnPresets.tsx:1273-1284 | editable: true, cellRenderer: ImageCellDisplay, cellEditor: createImageCellEditor(ITEM_IMAGE_CONFIG), cellEditorPopup: true. No onCellDoubleClicked, no suppressClickEdit. |
| Grid option | arda-frontend-app/src/app/items/ItemTableAGGrid.tsx:349 | singleClickEdit: false (default AG Grid double-click-to-edit). |
| Cell renderer | ux-prototype/src/components/canary/atoms/grid/image/image-cell-display.tsx | Wraps thumbnail in ImageHoverPreview → PopoverAnchor asChild → 28×28 div → ImageDisplay → <img>. |
| Hover preview | ux-prototype/src/components/canary/molecules/image-hover-preview/image-hover-preview.tsx | Outer <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. |
| Display | ux-prototype/src/components/canary/molecules/image-display/image-display.tsx | Renders 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 editor | ux-prototype/src/components/canary/atoms/grid/image/image-cell-editor.tsx | ImageCellEditor 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:
- AG Grid’s click/keyboard edit activation is suppressed somewhere
higher in the grid configuration (a
gridOptionsprop, a wrapper, or thesuppressClickEdit/suppressRowClickSelectionplumbing) that we have not yet located. - The
createImageCellEditorwrapper 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. - A runtime error during
startEditingCellsilently 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
onCellDoubleClickedandonCellKeyDownon 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 — specificallycellEditingStarted/cellEditingStopped. - Verify that
createImageCellEditor(...)returns a component that passes AG Grid’s cellEditor contract check. Note: the factory usesforwardRefand returns the innerWrappedEditor, which also usesforwardRef— two layers of forwardRef. If AG Grid introspects the component (e.g. checksdisplayNameor 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 atItemTableAGGrid.tsx:343-350spreads...coland overrideseditable; confirm the spread does not losecellEditor/cellEditorPopup.
Proposed next step
Section titled “Proposed next step”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 /
publishRowpipeline.
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””Symptom (reported 2026-04-13)
Section titled “Symptom (reported 2026-04-13)”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.
Root cause
Section titled “Root cause”Two components combine to produce the current behavior:
ImageHoverPreview.handleMouseEnter(ux-prototype/.../image-hover-preview.tsx:77-82) early-returns whenimageUrl === null, so the Radix popover does not open for empty-image cells. The user sees no popover at all.ImageDisplayin the cell renders the initials placeholder (entity initial, e.g.IforItem) on abg-mutedbackground (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’.”
Proposed fix shape
Section titled “Proposed fix shape”Open question: where should “No Image Available” live?
- Option P — Popover that opens for null images too. Remove the
imageUrl === nullearly-return inhandleMouseEnter. Render a popover of the same size with a centered “No Image Available” message instead of anImageDisplay. 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.
Verification
Section titled “Verification”- 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 selectors —
cardSize,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 ItemCard → ItemCardEditor substitution.
Size selectors — not a gap
Section titled “Size selectors — not a gap”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 toform.cardSize/form.labelSize/form.breadcrumbSizeviahandleFormChange.- Legacy
itemCard.tsxnever rendered these fields — agrepof its inputs list shows onlyminQty,minUnit,orderQty,orderUnit, andsupplier(static display). The three size fields were always outside the card, on the form panel. ItemCardFieldsinux-prototype/src/components/canary/organisms/item-card-editor/item-card-editor.tsx:23-31likewise contains onlytitle,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.
Inline validation — not a regression
Section titled “Inline validation — not a regression”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 toItemCardEditor. - Neither
itemCard.tsxnoritem-card-editor.tsxexposes per-fielderror/helperText/invalidprops. Agrepacross 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.
Revised gap statement
Section titled “Revised gap statement”After verification, the actionable residual work from management#861 is considerably smaller than the brief suggested. Items that were audited:
| Claimed gap | Verification result |
|---|---|
3 size selectors missing from ItemCardEditor | Not a gap — rendered by ItemFormPanel, always was |
Inline validation missing from ItemCardEditor | Not 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
ItemCardthatItemCardEditordoes 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?
Consequence (revised)
Section titled “Consequence (revised)”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 legacyItemCardafter 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@deprecatedmarker, and the currently-dead rollback scaffolding inItemFormPanel.tsxunchanged 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
ItemCardremains inarda-frontend-app(marked@deprecated) specifically because removing it would regress these features. The project cannot declare theItemCardEditormigration 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.
Scope in 3.6c
Section titled “Scope in 3.6c”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
ItemCardstill 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”Problem
Section titled “Problem”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.
Constraints
Section titled “Constraints”- 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
operationsbackend already holds the CloudFront signing key (Secrets Manager) and mints signed cookies for the SPA.
Options to explore (not yet decided)
Section titled “Options to explore (not yet decided)”-
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-appBFF. 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
operationssigner 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.
Scope in 3.6c
Section titled “Scope in 3.6c”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:
- What is Documint’s actual fetch behavior? (Headers? Retries? Caching?)
- Is Documint’s outbound IP range documented and stable enough for Option C?
- What is the 95th-percentile time between print-job creation and Documint’s image fetch? (Drives acceptable TTL for Options A / E.)
- Does Documint strip or preserve URL query strings in its generated PDFs’ metadata? (Affects signed-URL leakage risk.)
- Are image-bearing Documint templates already in production use, or is this a new capability gated by this project?
Work Order
Section titled “Work Order”- Rebase PR #742 onto current
main(PR #741 merged). Commit, push, monitor via/pr-stewarduntil CI is green and Amplify previewpr-742.d38w5m1ngjza76.amplifyapp.comis live. - Fix Issue 1 locally (
npm link --no-save ../ux-prototype). Verify via Playwright MCP againstmake dev+devbackend. - Fix Issue 2 locally in the same linked setup. Verify via Playwright MCP.
- Happy-path E2E verification against the
devbackend: upload-via-file → upload-via-URL → grid display with CDN cookies → 403 recovery → double-click to edit → hover preview. - Publish fixes: commit and push to
ux-prototype(patch release@arda-cards/design-system@4.11.3or equivalent), bump inarda-frontend-app, push to PR #742, monitor via/pr-steward. - Confirm Issue 3 gap with a short written status; do not implement.
- Explore Issue 4 options; produce short decision memo.
- Joint decision on Issues 3 and 4. Record in project decision log.
- Proceed to Phase 3.7.
Non-goals
Section titled “Non-goals”- Implementing
ItemCardEditorfeature 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.
References
Section titled “References”- PR: arda-frontend-app#742
- Issue: management#861 (ItemCardEditor feature parity)
- Decision log: decision-log.md — FD-01, FD-10, FD-15, FD-16, FD-17
- Phase 3.6 specification: ../3.6-spa-integration/specification.md
- Phase 3.6b specification: ../36b-backend-validation/specification.md
- Phase 3.7 specification: ../37-release/specification.md
Copyright: © Arda Systems 2025-2026, All rights reserved