Image Upload Components — Learnings
Knowledge discovered during the implementation of 19 image upload components
in the ux-prototype canary library, useful for future tasks.
L-01: Bake interactions into components, don’t externalize them
Section titled “L-01: Bake interactions into components, don’t externalize them”The initial implementation (Runs 1-3) externalized dialog-opening and state management into stories and consumers. This led to TODO stubs, duplicative hover overlays, and broken user-facing actions (ImageFormField edit/inspect buttons that did nothing).
The post-run refinement phase baked interactions directly into components:
ImageDisplay gained onImageChange + config props that internally open
ImageUploadDialog on double-click/Enter. ImageInspectorOverlay gained an
uncontrolled mode. ImageComparisonLayout gained baked-in action buttons.
Principle: Components should be self-contained interaction units. If a
consumer must wire a dialog, manage open/close state, or compose multiple
components to achieve a basic flow, the abstraction boundary is wrong.
Externalize only the result (callbacks like onImageChange), not the
mechanism (dialog state, trigger handlers).
L-02: AG Grid cell editors require imperative patterns that diverge from React composition
Section titled “L-02: AG Grid cell editors require imperative patterns that diverge from React composition”ImageCellEditor could not reuse ImageDisplay’s baked-in dialog flow
because AG Grid’s cell editor lifecycle requires:
isPopup()returningtrueto prevent focus-loss from stopping editinggetValue()for imperative value extractionstopEditing()for programmatic editing termination- Immediate dialog open on mount (not on user double-click)
This is a justified duplication of the dialog wiring pattern. Document intentional divergences from composition patterns when framework constraints require them.
L-03: pointer-events-none + pointer-events-auto for overlay affordances
Section titled “L-03: pointer-events-none + pointer-events-auto for overlay affordances”When hover action icons (eye, trash) must overlay an interactive element
(like ImageDisplay’s button), use pointer-events-none on the overlay
container and pointer-events-auto only on the individual icon buttons.
This allows clicks on the image area to pass through to the underlying
button while still allowing clicks on the icons.
L-04: Mock imports in production code are a silent correctness hazard
Section titled “L-04: Mock imports in production code are a silent correctness hazard”During the mock-data-first implementation approach, ImageUploadDialog
imported mockUpload and mockReachabilityCheck from __mocks__/. This
compiled and ran correctly in Storybook — there was no lint error, no type
error, and no visible bug. The hazard is invisible until someone ships the
component to a real application.
Mitigation: Use dependency injection (props or config) for any function
that will have a real implementation. Never import from __mocks__/ in
production component files. ESLint no-restricted-imports could enforce
this statically.
L-05: stopEditingWhenCellsLoseFocus must be false for popup editors
Section titled “L-05: stopEditingWhenCellsLoseFocus must be false for popup editors”AG Grid’s default stopEditingWhenCellsLoseFocus: true causes popup cell
editors (like ImageCellEditor with isPopup()) to immediately stop
editing when the popup receives focus, because focus leaves the grid cell.
Setting stopEditingWhenCellsLoseFocus: false on the DataGrid allows
popup editors to function correctly.
L-06: createWorkflowStories generalizes the use-case framework
Section titled “L-06: createWorkflowStories generalizes the use-case framework”The original use-case story framework was tightly coupled to specific story
shapes. Extracting createWorkflowStories as a generic factory that
accepts renderScene (for stepwise) and renderLive (for interactive)
callbacks enables any multi-step workflow — dialog flows, component
interactions, form wizards — to produce Interactive/Stepwise/Automated story
variants without duplicating the framework infrastructure.
L-07: Storybook sidebar ordering requires explicit enforcement
Section titled “L-07: Storybook sidebar ordering requires explicit enforcement”With 19 new components across 15 story files, sidebar ordering became
inconsistent. The convention “Playground story always last” required a
custom enforce-story-order.js script (tools/enforce-story-order.js)
that validates story export order across all files. Run this as part of the
lint pass for projects with many story files.
L-08: ShadCN primitives need selector fixes after vendoring
Section titled “L-08: ShadCN primitives need selector fixes after vendoring”Vendored ShadCN primitives may use incorrect Radix data attribute selectors. Two examples found:
- Tabs:
data-horizontal:flex-colshould bedata-[orientation=horizontal]:flex-col(Radix setsdata-orientation="horizontal", notdata-horizontal). - Slider: track height was conditional on orientation but should be unconditional for the common horizontal-only case.
Always verify Radix data attribute selector syntax against the actual DOM output when vendoring ShadCN primitives.
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved