Component Implementation Specification
Implementation specification for the 19 components defined in the
UI Components document. Each component produces a standard
set of deliverables following the conventions established in the canary library
(see atoms/grid/boolean/ for the reference pattern). Workflow integration
stories are out of scope here and will be addressed separately.
Deliverables per Component
Section titled “Deliverables per Component”Every new component directory contains the following artifacts:
| Artifact | File | Purpose |
|---|---|---|
| Component | <name>.tsx | Production React component |
| Unit tests | <name>.test.tsx | Vitest + React Testing Library tests |
| Stories | <name>.stories.tsx | Storybook stories (may split into multiple files if large) |
| Documentation | <name>.mdx | MDX doc page with Canvas embeds and props tables |
| Index | index.ts | Barrel re-exports for the component directory |
For modified components (Badge, Avatar, item-grid-columns), the existing files are extended rather than replaced.
Directory Structure
Section titled “Directory Structure”All components live under src/components/canary/ with subdirectories for
primitives, atoms, molecules, and organisms. Each of these may have
additional subdirectories (e.g., grid) depending on functional groupings.
Each level of the directory hierarchy has a descriptive mdx file that serves
as an index for its contents.
All utilities live under src/types/canary/utilities/ with a single mdx
documentation file at that directory level (may be split in the future).
Sidebar Ordering
Section titled “Sidebar Ordering”The sidebar follows the directory structure. Within each section, the first
entry is the index document for that subdirectory (*.mdx), followed by other
files in alphabetical order, then subdirectories in alphabetical order.
Component sections (leaf directories) follow this order:
- Component documentation
- Stories documentation (if present)
- Stories in alphabetical order except the Playground story
- Playground story last
Story Conventions
Section titled “Story Conventions”Every component story file follows this structure:
- MDX description (
<component>.mdx) — imports stories, provides prose overview, embeds keyCanvasblocks, and documents props via HTML<table>elements (no Markdown tables in MDX). - Playground story — fully instrumented with
argTypescontrols across all props. This is the primary interactive story for stakeholders. - Specialized stories — named exports that each highlight one key aspect of the component’s behavior or visual state.
Story title paths follow: Components/Canary/{Tier}/{ComponentName}.
All stories use mock data only — no backend connections. Upload workflows use delayed Promises simulating presigned-POST success. URL reachability checks use mock responses. Image URLs use placeholder services.
Unit Test Conventions
Section titled “Unit Test Conventions”Tests use Vitest + React Testing Library + userEvent following the
patterns established in the canary library (see boolean.test.tsx,
badge.test.tsx).
import { render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';import { describe, expect, it, vi } from 'vitest';import '@testing-library/jest-dom/vitest';Test categories applied per component (where applicable):
- Rendering — component mounts without error, renders expected elements in each visual state.
- Props and variants — each prop value and variant produces the
correct DOM output, classes, or
data-*attributes. - User interaction — clicks, hovers, keyboard events trigger the
correct callbacks (
vi.fn()assertions). - Edge cases — null/undefined props, empty strings, broken URLs, missing optional callbacks.
- Accessibility — ARIA attributes, roles, focus management where applicable.
- Ref handles —
useImperativeHandlecontracts (AG Grid cell editors).
All diagrams in MDX documentation use PlantUML format.
Prerequisites
Section titled “Prerequisites”Before implementation begins, the following must be in place.
Gate: All existing checks (lint, build, tests) must pass before making any changes. This is verified at the start of Wave 0.
Dependency hygiene: Update current dependencies to their latest versions unless major breaking changes would require disproportionate effort.
New npm Dependencies
Section titled “New npm Dependencies”| Package | Purpose | Status |
|---|---|---|
react-dropzone | ImageDropZone foundation | Already installed (v15) |
react-easy-crop | ImagePreviewEditor crop/zoom/rotate | To install |
browser-image-compression | Auto-compression in ImageUploadDialog | To install |
heic2any | HEIC-to-JPEG conversion (lazy-loaded) | To install |
radix-ui | Unified Radix package | Already installed (v1.4) |
class-variance-authority | CVA variants | Already installed (v0.7) |
New Primitives (ShadCN, used as-is)
Section titled “New Primitives (ShadCN, used as-is)”These ShadCN primitives are referenced by components but do not currently exist
in the primitives/ directory. They must be added before the components that
consume them.
| Primitive | Consumed By | Notes |
|---|---|---|
alert-dialog.tsx | ImageUploadDialog (Warn state), ImageFormField (remove confirmation) | ShadCN AlertDialog — focus trap, no click-outside dismiss |
checkbox.tsx | CopyrightAcknowledgment | ShadCN Checkbox |
popover.tsx | ImageHoverPreview | ShadCN Popover — positioning for hover preview |
progress.tsx | ImageUploadDialog (upload progress bar) | ShadCN Progress |
slider.tsx | ImagePreviewEditor (zoom control) | ShadCN Slider |
aspect-ratio.tsx | ImageDisplay, ImagePreviewEditor | ShadCN AspectRatio |
Shared Types and Utilities
Section titled “Shared Types and Utilities”As part of establishing the utilities/ directory, move the contents of the
existing src/types/canary/utils.ts, pagination.ts, and date-time.ts into
the new src/types/canary/utilities/ directory and update all import paths.
| File | Contents |
|---|---|
src/types/canary/utilities/image-field-config.ts | ImageMimeType, ImageFieldStaticConfig, ImageFieldInitConfig, ImageFieldConfig, ImageInput, ImageUploadResult, CropData, PixelCrop |
src/types/canary/utilities/get-initials.ts | getInitials(name: string): string — extracted from item-grid-columns.tsx |
src/types/canary/utilities/get-cropped-image.ts | getCroppedImage(...) — canvas helper returning Promise<Blob> |
src/types/canary/utilities/index.ts | Barrel for utilities |
Mock Data Module
Section titled “Mock Data Module”A shared mock data module provides consistent sample data across all stories:
File: src/components/canary/__mocks__/image-story-data.ts
// Placeholder image URLs (via picsum.photos, stable seeds)export const MOCK_ITEM_IMAGE = 'https://picsum.photos/seed/arda-item-1/400/400';export const MOCK_ITEM_IMAGE_ALT = 'https://picsum.photos/seed/arda-item-2/400/400';export const MOCK_BROKEN_IMAGE = 'https://picsum.photos/seed/zzz-broken/0/0'; // triggers errorexport const MOCK_LARGE_IMAGE = 'https://picsum.photos/seed/arda-large/2048/2048';
export const ITEM_IMAGE_CONFIG: ImageFieldConfig = { aspectRatio: 1, acceptedFormats: ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif'], maxFileSizeBytes: 10 * 1024 * 1024, maxDimension: 2048, entityTypeDisplayName: 'Item', propertyDisplayName: 'Product Image',};
// Simulated presigned-POST upload (happy path, ~1.5s)export async function mockUpload(file: Blob): Promise<string> { await new Promise((r) => setTimeout(r, 1500)); return URL.createObjectURL(file);}
// Simulated URL reachability check (happy path, ~500ms)export async function mockReachabilityCheck(url: string): Promise<boolean> { await new Promise((r) => setTimeout(r, 500)); return !url.includes('broken');}
// Sample items for grid storiesexport interface MockItem { id: string; name: string; sku: string; imageUrl: string | null; unitCost: number;}
export const MOCK_ITEMS: MockItem[] = [ { id: '1', name: 'Hex Bolt M10x30', sku: 'HB-1030', imageUrl: MOCK_ITEM_IMAGE, unitCost: 0.45 }, { id: '2', name: 'Flat Washer 3/8"', sku: 'FW-0375', imageUrl: MOCK_ITEM_IMAGE_ALT, unitCost: 0.12 }, { id: '3', name: 'Spring Pin 4x20', sku: 'SP-0420', imageUrl: null, unitCost: 0.28 }, { id: '4', name: 'Tee Nut 1/4-20', sku: 'TN-2520', imageUrl: MOCK_BROKEN_IMAGE, unitCost: 0.65 },];Resolved Decisions
Section titled “Resolved Decisions”Grid cell adapter location: atoms/grid/image/ (carve-out confirmed).
ImageCellDisplay and ImageCellEditor live under atoms/grid/image/ alongside
all other grid cell types, maintaining symmetry with boolean, text, number,
date, select, memo, and color. The delegation to molecules is documented in
the index and MDX but does not change the directory location.
Implementation Waves
Section titled “Implementation Waves”Components are ordered by dependency. Each wave can only begin after its predecessors are complete.
Wave 0a Infrastructure (deps, dependency updates, existing checks green) |Wave 0b Structural scaffolding (primitives, types, utilities, directory moves, mocks) |Wave 1 Foundation components (no deps on other new components) | getInitials, getCroppedImage, Badge (error-overlay), Avatar (refactor), | CopyrightAcknowledgment, ImageDisplay, ImageDropZone, ImagePreviewEditor |Wave 2 Composition components (depend on Wave 1) | ImageHoverPreview, ImageInspectorOverlay, ImageComparisonLayout, ImageFormField |Wave 3 Grid atoms + Organism (depend on Waves 1-2) | ImageCellDisplay, ImageCellEditor, ImageUploadDialog |Wave 4 Integration (modify existing components) item-grid-columns, barrel exportsWave gate: At the end of each wave, the Storybook must build without errors, all unit tests must pass, and the linter must pass. After a successful gate, all changes are committed locally.
Wave 0a — Infrastructure
Section titled “Wave 0a — Infrastructure”- Verify all existing checks pass (lint, Storybook build, unit tests).
- Update current dependencies to latest versions (unless major breaking changes require disproportionate effort).
- Install new npm dependencies:
react-easy-crop,browser-image-compression,heic2any. - Verify checks still pass after dependency changes.
Wave 0b — Structural Scaffolding
Section titled “Wave 0b — Structural Scaffolding”Directory and file moves
Section titled “Directory and file moves”- Create
src/types/canary/utilities/directory. - Move
src/types/canary/utils.ts(cn()helper),pagination.ts, anddate-time.tsintoutilities/. Update all import paths across the codebase. - Create
src/types/canary/utilities/index.tsbarrel. - Create
src/components/canary/molecules/form/directory. - Create
src/components/canary/atoms/grid/image/directory withindex.ts. - Scaffold
src/components/canary/organisms/shared/image-upload-dialog/directory.
Shared types
Section titled “Shared types”- Create
src/types/canary/utilities/image-field-config.tswithImageMimeType,ImageFieldStaticConfig,ImageFieldInitConfig,ImageFieldConfig,ImageInput,ImageUploadResult,CropData,PixelCrop.
New ShadCN primitives
Section titled “New ShadCN primitives”- Add
alert-dialog.tsx,checkbox.tsx,popover.tsx,progress.tsx,slider.tsx,aspect-ratio.tsxtosrc/components/canary/primitives/.
Badge error-overlay variant
Section titled “Badge error-overlay variant”- Add
error-overlayvariant to Badge CVA definition inbadge-base.tsx.
Extract getInitials
Section titled “Extract getInitials”- Extract
getInitialsfromitem-grid-columns.tsxtosrc/types/canary/utilities/get-initials.ts. Updateitem-grid-columns.tsxto import from the new location.
Create getCroppedImage
Section titled “Create getCroppedImage”- Create
src/types/canary/utilities/get-cropped-image.ts— canvas helper returningPromise<Blob>.
Verify shimmer animation
Section titled “Verify shimmer animation”- Confirm that the existing
skeletonprimitive provides the shimmer@keyframesvia Tailwind. If not, add it to@themeinglobals.css.
Mock data
Section titled “Mock data”- Create
src/components/canary/__mocks__/image-story-data.tswith shared mock data module.
Primitive context stories (lightweight)
Section titled “Primitive context stories (lightweight)”These stories familiarize stakeholders with the ShadCN primitives used throughout the image components. They are lightweight — one story file per primitive with 2–3 named exports showing the primitive in image-upload context.
P-1. skeleton
Section titled “P-1. skeleton”Story file: src/components/canary/primitives/skeleton-image.stories.tsx
Title: Components/Canary/Primitives/Skeleton (Image Context)
| Story | Purpose |
|---|---|
ImagePlaceholder | Skeleton sized as a 64×64 square image cell — the loading state stakeholders will see in grids |
ImagePlaceholderLarge | Skeleton sized at 256×256 — the loading state in preview/inspector contexts |
ShimmerAnimation | Skeleton in a simulated container that toggles loaded/loading every 2s to demonstrate the transition |
P-2. tabs
Section titled “P-2. tabs”Story file: src/components/canary/primitives/tabs-image.stories.tsx
Title: Components/Canary/Primitives/Tabs (Image Context)
| Story | Purpose |
|---|---|
CurrentVsNew | Two tabs labeled “Current” / “New” each containing a placeholder image — preview of the mobile comparison UX |
WithPlaceholder | “Current” tab shows initials placeholder, “New” tab shows an actual image |
P-3. alert-dialog
Section titled “P-3. alert-dialog”Story file: src/components/canary/primitives/alert-dialog-image.stories.tsx
Title: Components/Canary/Primitives/AlertDialog (Image Context)
| Story | Purpose |
|---|---|
RemoveConfirmation | “Remove Product Image?” dialog with destructive action button — the exact L&F of the image removal prompt |
DiscardChanges | “Discard unsaved image?” dialog — the Warn state confirmation from ImageUploadDialog |
Wave 1 — Foundation Components
Section titled “Wave 1 — Foundation Components”1. getInitials (Utility)
Section titled “1. getInitials (Utility)”No Storybook story — pure function. Documented in a shared utilities MDX page with an input/output reference table.
MDX file: src/types/canary/utilities/utilities.mdx
Title: Components/Canary/Utilities
Contents: Table of getInitials examples ("Hex Bolt" → "HB",
"A" → "A", "" → fallback symbol). Table of getCroppedImage
parameters and return type.
Unit tests: src/types/canary/utilities/get-initials.test.ts
| Test | Assertion |
|---|---|
| two-word name | "Hex Bolt" → "HB" |
| three-word name (takes first two) | "Hex Bolt M10" → "HB" |
| single-word name | "Bolt" → "B" |
| single character | "A" → "A" |
| empty string | returns fallback symbol |
| whitespace-only string | returns fallback symbol |
| leading/trailing whitespace | trims before splitting |
| mixed case | "hex bolt" → "HB" (uppercased) |
2. getCroppedImage (Utility)
Section titled “2. getCroppedImage (Utility)”No Storybook story — documented alongside getInitials in the
utilities MDX page.
Unit tests: src/types/canary/utilities/get-cropped-image.test.ts
| Test | Assertion |
|---|---|
| returns a Blob | result is instanceof Blob |
| output MIME type matches parameter | outputFormat: 'image/jpeg' → Blob type is image/jpeg |
| default quality is 0.85 | Blob size is smaller than uncompressed (smoke test) |
| handles zero rotation | no error, produces valid Blob |
| handles 90-degree rotation | produces valid Blob (dimensions swapped) |
Note: tests use a small in-memory canvas image, not external URLs.
3. Badge (Modified Atom)
Section titled “3. Badge (Modified Atom)”Story file: src/components/canary/atoms/badge/badge.stories.tsx
(extend existing)
MDX file: src/components/canary/atoms/badge/badge.mdx (extend existing)
Title: Components/Canary/Atoms/Badge (unchanged)
New stories added to the existing file:
| Story | Purpose |
|---|---|
Playground | (existing) — add error-overlay to variant select options |
ErrorOverlay | Badge in error-overlay variant positioned over a bg-muted 64×64 container simulating an image cell. Shows warning icon + “!” indicator |
ErrorOverlayInContext | Side-by-side: normal image cell vs. failed image cell with error-overlay badge — demonstrates the visual distinction between “no image” and “broken image” |
AllVariants | (existing) — add error-overlay row to the variant gallery |
MDX additions: new “Error Overlay Variant” section with Canvas
embed showing ErrorOverlay and ErrorOverlayInContext.
Unit tests: extend src/components/canary/atoms/badge/badge.test.tsx
| Test | Assertion |
|---|---|
| renders error-overlay variant | data-variant="error-overlay" attribute present |
| error-overlay has absolute positioning class | className contains absolute |
| error-overlay renders warning icon | SVG icon element present |
| existing variants unchanged | existing tests continue to pass (regression) |
4. Avatar (Modified Atom)
Section titled “4. Avatar (Modified Atom)”Story file: src/components/canary/atoms/avatar/avatar.stories.tsx
(extend existing)
MDX file: src/components/canary/atoms/avatar/avatar.mdx (extend existing)
Title: Components/Canary/Atoms/Avatar (unchanged)
New stories added:
| Story | Purpose |
|---|---|
InitialsFromName | Avatar rendered with entityName prop — demonstrates getInitials producing fallback initials automatically instead of requiring pre-computed children. Shows: “Hex Bolt M10” → “HB”, “A” → “A”, empty → fallback icon |
AllSizesWithImage | sm/default/lg side-by-side with a real image |
AllSizesWithFallback | sm/default/lg side-by-side with initials |
AllSizesWithBadge | sm/default/lg with status badge overlay |
ImageLoadError | Avatar with broken image URL — shows fallback transition with Radix delayMs |
Existing stories (Playground, Default, WithImage, WithFallback,
WithBadge, Sizes, Group) remain unchanged.
Unit tests: extend existing Avatar tests (or add
avatar-initials.test.tsx if file is large)
| Test | Assertion |
|---|---|
| renders initials from entityName prop | "Hex Bolt" → text content "HB" |
| renders fallback icon for empty name | fallback icon element present |
| existing fallback behavior unchanged | passing children still renders them (regression) |
| image load error shows fallback | broken src → fallback content visible |
5. CopyrightAcknowledgment (New Atom)
Section titled “5. CopyrightAcknowledgment (New Atom)”Story file: src/components/canary/atoms/copyright-acknowledgment/copyright-acknowledgment.stories.tsx
MDX file: src/components/canary/atoms/copyright-acknowledgment/copyright-acknowledgment.mdx
Title: Components/Canary/Atoms/CopyrightAcknowledgment
| Story | Purpose |
|---|---|
Playground | Full controls: acknowledged (boolean toggle), onAcknowledge (action logger). Stakeholders can toggle the checkbox and see the action logged |
Unchecked | Default unchecked state with full legal text visible |
Checked | Checked state — shows visual confirmation |
DisabledUnchecked | Disabled + unchecked — demonstrates opacity-50 treatment when parent disables interaction |
InDialogContext | CopyrightAcknowledgment rendered inside a mock dialog footer alongside a disabled “Confirm” button. Checking the box enables the button — demonstrates the gate behavior stakeholders will see in the upload flow |
MDX contents:
- Overview: purpose, legal text, gate behavior
CanvasofPlayground- “Gate Behavior” section with
CanvasofInDialogContext - Props table
Unit tests: copyright-acknowledgment.test.tsx
| Test | Assertion |
|---|---|
| renders unchecked by default | checkbox role present, not checked |
| renders legal text | expected text content visible |
| calls onAcknowledge on click | vi.fn() called with true |
| toggles back to unchecked | second click calls with false |
| disabled state prevents interaction | click does not call onAcknowledge |
| disabled state applies opacity | container has opacity-50 class |
| checkbox has accessible label | aria-labelledby or associated <label> |
6. ImageDisplay (New Molecule)
Section titled “6. ImageDisplay (New Molecule)”Story file: src/components/canary/molecules/image-display/image-display.stories.tsx
MDX file: src/components/canary/molecules/image-display/image-display.mdx
Title: Components/Canary/Molecules/ImageDisplay
| Story | Purpose |
|---|---|
Playground | Controls: imageUrl (text — paste any URL), entityTypeDisplayName (text), propertyDisplayName (text). Container size control (64/128/256px). Stakeholders can try different URLs and see states change |
Loaded | Happy-path image rendered in a 128×128 container |
Loading | Skeleton shimmer in a 128×128 container (image URL set but <img> load simulated as pending) |
ErrorState | Broken URL — shows initials placeholder with error-overlay Badge. Demonstrates the distinction from “no image” |
NoImage | imageUrl: null — initials placeholder without error badge |
AllStates | 2×2 grid showing Loaded, Loading, Error, NoImage side-by-side for quick visual comparison |
SizeResponsiveness | Same image rendered in 32px, 64px, 128px, 256px containers — demonstrates that initials and error badge scale proportionally |
WhiteImageContrast | A white/light image on bg-muted container — demonstrates the deliberate no-border design decision (bg-muted provides sufficient contrast) |
MDX contents:
- Overview: foundational rendering molecule, three visual states
CanvasofPlayground- “Visual States” section with
CanvasofAllStates - “Design Decision: No Border” section with
CanvasofWhiteImageContrast - Props table (Static/Init/Runtime sections)
Unit tests: image-display.test.tsx
| Test | Assertion |
|---|---|
| renders img element when imageUrl provided | <img> with correct src |
| shows skeleton during loading | skeleton element visible before load event |
| shows loaded state after img load | skeleton removed, img visible |
| shows error state on img error | initials placeholder + error badge visible |
| shows initials placeholder when imageUrl is null | initials text visible, no error badge |
| initials derived from entityTypeDisplayName | "Item" → "I" |
| no border on container | container does not have border class (deliberate) |
| fills parent container | width: 100%, height: 100% styles present |
| applies object-cover to img | img has object-cover class |
7. ImageDropZone (New Molecule)
Section titled “7. ImageDropZone (New Molecule)”Story file: src/components/canary/molecules/image-drop-zone/image-drop-zone.stories.tsx
MDX file: src/components/canary/molecules/image-drop-zone/image-drop-zone.mdx
Title: Components/Canary/Molecules/ImageDropZone
| Story | Purpose |
|---|---|
Playground | Controls: acceptedFormats (multi-select). Actions panel logs onInput and onDismiss events. Stakeholders can drag files, paste images, type URLs |
IdleState | Default idle appearance — dashed border, instructional text, upload button, URL field |
DragOverHighlight | Simulated drag-over state (border-primary, bg-accent) — uses a decorator that triggers the highlight on story mount for visual inspection |
UrlEntry | Shows the URL text field focused with a sample HTTPS URL typed in — demonstrates the URL input path |
ValidationError | Drop zone after an invalid file type was provided — shows inline text-destructive error message with plain-language explanation |
InputClassification | Interactive demo: a panel below the drop zone shows the classified ImageInput result. Stakeholders can try each input method and see the system’s classification in real time |
MDX contents:
- Overview: unified input surface, 6 input methods
- “Try It” section with
CanvasofPlayground - “Input Methods” section explaining each method with
CanvasofInputClassification - “Drag Highlight” section with
CanvasofDragOverHighlight - “Error Handling” section with
CanvasofValidationError - Props table
Unit tests: image-drop-zone.test.tsx
| Test | Assertion |
|---|---|
| renders drop area with dashed border | container with border-dashed class |
| renders upload button | button with expected label visible |
| renders URL text field | input element with URL-related placeholder |
| calls onInput with file data on drop | simulate drop event, vi.fn() called with file input |
| calls onInput with URL on text submit | type URL + submit, callback receives URL input |
| shows error for invalid file type | provide wrong MIME type, error message in text-destructive visible |
| calls onDismiss on dismiss click | dismiss button click calls callback |
| drag-over adds highlight classes | simulate dragenter, container gets border-primary class |
| drag-leave removes highlight | simulate dragleave, border-primary removed |
| rejects non-HTTPS URL | type http://..., error message shown |
8. ImagePreviewEditor (New Molecule)
Section titled “8. ImagePreviewEditor (New Molecule)”Story file: src/components/canary/molecules/image-preview-editor/image-preview-editor.stories.tsx
MDX file: src/components/canary/molecules/image-preview-editor/image-preview-editor.mdx
Title: Components/Canary/Molecules/ImagePreviewEditor
| Story | Purpose |
|---|---|
Playground | Controls: aspectRatio (number), imageData (select from preset URLs). Actions panel logs onCropChange data. Stakeholders can crop, zoom, rotate, pan, reset |
DefaultView | Image loaded at 1:1 aspect ratio with toolbar visible — initial state before any edits |
ZoomControl | Image pre-zoomed to ~2× with the Slider control visible — demonstrates zoom range and slider interaction |
RotateSteps | Four side-by-side renders of the same image at 0°, 90°, 180°, 270° — demonstrates 90-degree rotation increments |
CropAndPan | Image with a visible crop overlay and the pan cursor active — demonstrates repositioning within the locked aspect ratio |
ResetBehavior | Interactive: user makes edits (zoom + rotate), clicks Reset, image reverts — demonstrates the reset control |
UrlImageLoading | ImagePreviewEditor with a slow-loading URL — shows shimmer placeholder with timeout message after 5s |
MDX contents:
- Overview: locked aspect ratio, edit operations (crop, zoom, rotate, pan, reset)
CanvasofPlayground- “Edit Operations” section with
CanvasofZoomControl,RotateSteps - “Reset” section with
CanvasofResetBehavior - Props table
Unit tests: image-preview-editor.test.tsx
| Test | Assertion |
|---|---|
| renders crop area with locked aspect ratio | container with correct aspect ratio class |
| renders toolbar with edit controls | zoom slider, rotate button, reset button visible |
| calls onCropChange when crop state changes | callback receives crop data object |
| rotate button increments by 90 degrees | click rotate, callback receives rotation value |
| reset button calls onReset | click reset, onReset callback called |
| shows shimmer during URL image load | skeleton visible when image data is a URL |
Note: react-easy-crop internal behavior (drag/pinch) is not unit-tested — those are integration concerns verified via stories.
Wave 2 — Composition Components
Section titled “Wave 2 — Composition Components”9. ImageHoverPreview (New Molecule)
Section titled “9. ImageHoverPreview (New Molecule)”Story file: src/components/canary/molecules/image-hover-preview/image-hover-preview.stories.tsx
MDX file: src/components/canary/molecules/image-hover-preview/image-hover-preview.mdx
Title: Components/Canary/Molecules/ImageHoverPreview
| Story | Purpose |
|---|---|
Playground | Controls: imageUrl (text), entityTypeDisplayName (text), propertyDisplayName (text). Child is a 64×64 ImageDisplay. Hover to see popover |
HoverToPreview | A small thumbnail (64×64) that opens a ~256×256 popover on hover after 500ms delay. Annotated with timing note for stakeholders |
NoPreviewOnError | Thumbnail with broken URL — hover does NOT open the popover. Demonstrates the suppression behavior |
MultipleInRow | Three thumbnails in a horizontal row — hover each one independently. Demonstrates that only one popover is open at a time |
MDX contents:
- Overview: lightweight hover popover, 500ms delay, not a modal
CanvasofHoverToPreview- “Error Suppression” section with
CanvasofNoPreviewOnError - Props table
Unit tests: image-hover-preview.test.tsx
| Test | Assertion |
|---|---|
| renders children (trigger element) | child content visible |
| does not show popover initially | popover content not in DOM |
| shows popover on hover after delay | mouseenter + wait, popover content visible |
| hides popover on mouse leave | mouseleave, popover content removed |
| does not show popover when imageUrl is broken/error | hover over error state, no popover |
| renders ImageDisplay inside popover | popover contains img element |
10. ImageInspectorOverlay (New Molecule)
Section titled “10. ImageInspectorOverlay (New Molecule)”Story file: src/components/canary/molecules/image-inspector-overlay/image-inspector-overlay.stories.tsx
MDX file: src/components/canary/molecules/image-inspector-overlay/image-inspector-overlay.mdx
Title: Components/Canary/Molecules/ImageInspectorOverlay
| Story | Purpose |
|---|---|
Playground | Controls: imageUrl (text), open (boolean), onEdit (action/undefined toggle). Button to open the overlay |
ViewOnly | Overlay with image at large size, close button, no Edit button (onEdit undefined). Dismiss via Escape, click-outside, or close button |
WithEditButton | Same overlay but with Edit button visible (onEdit provided). Clicking Edit logs the action and closes the overlay |
DismissMethods | Interactive: instructions panel showing three dismiss methods (Escape, click-outside, close button) with action log confirming each |
MDX contents:
- Overview: read-only full-size image modal, optional edit transition
CanvasofPlayground- “Inspector to Edit Transition” section with
CanvasofWithEditButton - “Dismiss Methods” section with
CanvasofDismissMethods - Props table
Unit tests: image-inspector-overlay.test.tsx
| Test | Assertion |
|---|---|
| renders nothing when open is false | no dialog in DOM |
| renders image when open is true | dialog with img element visible |
| calls onClose on Escape key | press Escape, onClose called |
| calls onClose on close button click | click close button, onClose called |
| shows Edit button when onEdit provided | button with “Edit” label visible |
| hides Edit button when onEdit undefined | no Edit button in DOM |
| calls onEdit on Edit button click | click Edit, onEdit called |
| image has correct src | img src matches imageUrl prop |
11. ImageComparisonLayout (New Molecule)
Section titled “11. ImageComparisonLayout (New Molecule)”Story file: src/components/canary/molecules/image-comparison-layout/image-comparison-layout.stories.tsx
MDX file: src/components/canary/molecules/image-comparison-layout/image-comparison-layout.mdx
Title: Components/Canary/Molecules/ImageComparisonLayout
| Story | Purpose |
|---|---|
Playground | Controls: existingImageUrl (text — set to null to remove comparison), entityTypeDisplayName (text), propertyDisplayName (text). Children: a static ImagePreviewEditor |
DesktopSideBySide | Wide viewport: existing image (small) alongside new image (large) with “Current”/“New” labels |
MobileTabs | Narrow viewport (via Storybook parameters.viewport): tabbed view showing “Current” and “New” tabs. Stakeholders use Storybook viewport addon to see both layouts |
NoExistingImage | existingImageUrl: null — only the new image preview is shown, no comparison layout |
ExistingImageBroken | Existing image URL is broken — shows initials placeholder with error badge in the “Current” panel |
MDX contents:
- Overview: responsive comparison layout, desktop vs. mobile
CanvasofDesktopSideBySide- “Mobile Layout” note directing stakeholders to use viewport addon
- “No Existing Image” section with
CanvasofNoExistingImage - Props table
Unit tests: image-comparison-layout.test.tsx
| Test | Assertion |
|---|---|
| renders side-by-side on desktop viewport | both “Current” and “New” labels visible |
| renders existing image via ImageDisplay | existing image src present |
| renders children (new image preview) | children content visible |
| hides comparison when existingImageUrl is null | only children rendered, no “Current” label |
| shows initials placeholder for broken existing image | initials + error badge in “Current” panel |
| labels are correct | “Current” and “New” text present |
12. ImageFormField (New Molecule)
Section titled “12. ImageFormField (New Molecule)”Story file: src/components/canary/molecules/form/image/image-form-field.stories.tsx
MDX file: src/components/canary/molecules/form/image/image-form-field.mdx
Title: Components/Canary/Molecules/Form/ImageFormField
| Story | Purpose |
|---|---|
Playground | Controls: imageUrl (text), disabled (boolean), config (JSON — uses ITEM_IMAGE_CONFIG default). Actions log onChange |
WithImage | Form field showing a current image with hover affordances (eye, pencil, trash icons) |
WithoutImage | Form field showing initials placeholder with hover affordances (eye suppressed, pencil visible, trash hidden) |
HoverAffordances | Annotated story: hover over the field to see action icons appear. Labels identify each icon’s function |
Disabled | Field in disabled state — opacity-50, no interaction |
RemoveFlow | Interactive: field with image → hover → click trash → AlertDialog confirmation → confirm → field shows placeholder. Demonstrates the full remove interaction |
EditFlow | Interactive: field with image → hover → click pencil → ImageUploadDialog opens (mock). Demonstrates the edit entry point |
MDX contents:
- Overview: form field renderer, unified hover interaction model
CanvasofPlayground- “Hover Affordances” section with
CanvasofHoverAffordances - “Remove Flow” section with
CanvasofRemoveFlow - Props table (config, imageUrl, onChange, disabled)
Unit tests: image-form-field.test.tsx
| Test | Assertion |
|---|---|
| renders ImageDisplay with current image | img element with correct src |
| renders initials placeholder when no image | initials visible |
| shows action icons on hover | mouseenter, eye/pencil/trash icons appear |
| hides action icons when not hovered | icons not visible by default |
| eye icon suppressed when no image | hover, only pencil visible |
| trash icon hidden when no image | hover, no trash icon |
| pencil click calls upload dialog trigger | click pencil, expected callback fired |
| trash click opens remove confirmation | click trash, AlertDialog content visible |
| confirm remove calls onChange with null | confirm in AlertDialog, onChange called with null |
| disabled state shows opacity and blocks interaction | opacity-50 class, hover does not show icons |
| applies config display names | entityTypeDisplayName used for placeholder |
Wave 3 — Grid Atoms + Organism
Section titled “Wave 3 — Grid Atoms + Organism”13. ImageCellDisplay (New Atom)
Section titled “13. ImageCellDisplay (New Atom)”Story file: src/components/canary/atoms/grid/image/image.stories.tsx
MDX file: src/components/canary/atoms/grid/image/image.mdx
Title: Components/Canary/Atoms/Grid/Image
Uses a simplified 4-column AG Grid: Image, Name, SKU, Unit Cost.
| Story | Purpose |
|---|---|
Playground | Controls on config fields. 4-row grid with MOCK_ITEMS data. Stakeholders can interact with image cells |
GridDisplay | Static grid showing all four visual states across rows: loaded image, alternate image, no image (placeholder), broken image (error badge) |
HoverPreview | Instruction: hover over a loaded image cell. After 500ms the ImageHoverPreview popover appears. Hover off to dismiss |
HoverActionIcons | Instruction: hover over an image cell. Eye and pencil icons appear. Annotated labels show what each icon does |
InspectorFromGrid | Instruction: hover → click eye icon → ImageInspectorOverlay opens showing full-size image. Close with Escape |
DoubleClickEdit | Instruction: double-click an image cell. AG Grid enters edit mode and ImageUploadDialog (mock) opens |
KeyboardEdit | Instruction: navigate to image cell with Tab/arrow keys, press Enter. Edit mode activates |
ErrorCellNoInspector | Broken image cell — hover shows action icons but eye icon is suppressed (no URL to inspect). Only pencil icon is available |
MDX contents:
- Overview: AG Grid cell renderer, Display + Editor pair pattern
CanvasofGridDisplay- “Interactions” section: HoverPreview, InspectorFromGrid, DoubleClickEdit
- “Keyboard Navigation” section with
CanvasofKeyboardEdit - Props table (config, value, data)
Unit tests: image-cell-display.test.tsx
| Test | Assertion |
|---|---|
| renders ImageDisplay with value as imageUrl | img element with correct src |
| renders placeholder when value is null | initials visible |
| passes config to ImageDisplay | entityTypeDisplayName from config used |
| shows hover affordances on mouseenter | action icon elements appear |
| hides affordances on mouseleave | icons removed |
| eye icon suppressed for null/error images | no eye icon when no valid URL |
Note: AG Grid integration (double-click, startEditingCell, keyboard nav) is tested via stories and manual verification, not unit tests, due to AG Grid’s DOM complexity.
14. ImageCellEditor (New Atom)
Section titled “14. ImageCellEditor (New Atom)”The cell editor is an invisible wrapper. Its story is embedded in the
ImageCellDisplay grid stories above (DoubleClickEdit, KeyboardEdit).
One additional standalone story demonstrates the factory pattern:
Story file: same as ImageCellDisplay (image.stories.tsx)
| Story | Purpose |
|---|---|
EditorFactoryDemo | Code snippet + rendered grid showing column definition with createImageCellEditor(ITEM_IMAGE_CONFIG). Double-click opens ImageUploadDialog mock. On confirm, cell value updates. On cancel, no change |
Unit tests: image-cell-editor.test.tsx
| Test | Assertion |
|---|---|
| exposes getValue via ref | ref.current.getValue() returns current value |
| getValue returns original value on cancel | after cancel, returns initial value |
| getValue returns new URL on confirm | after simulated confirm, returns new URL |
| renders invisible placeholder | component renders with no visible content |
| createImageCellEditor factory returns component | factory call produces a valid React component |
15. ImageUploadDialog (New Organism)
Section titled “15. ImageUploadDialog (New Organism)”Story file: src/components/canary/organisms/shared/image-upload-dialog/image-upload-dialog.stories.tsx
MDX file: src/components/canary/organisms/shared/image-upload-dialog/image-upload-dialog.mdx
Title: Components/Canary/Organisms/Shared/ImageUploadDialog
| Story | Purpose |
|---|---|
Playground | Controls: open (boolean), existingImageUrl (text/null), config (JSON). Actions log onConfirm, onCancel |
EmptyImageState | Dialog open with no existing image. Shows ImageDropZone in its initial state |
ProvidedImageState | Dialog open with a mock file already “provided” — shows ImagePreviewEditor with crop controls, CopyrightAcknowledgment unchecked, Confirm button disabled |
ComparisonMode | Dialog open with existingImageUrl set — shows ImageComparisonLayout with existing image alongside new image preview |
ValidationError | Dialog showing inline error after invalid file type — plain-language error message in text-destructive, retry affordance |
CopyrightGate | Interactive: image provided, copyright unchecked → Confirm disabled. Check copyright → Confirm enables. Demonstrates the gate |
UploadProgress | Mock upload in progress — shows progress bar (bg-primary fill on bg-muted track), Confirm button replaced by progress indicator |
WarnOnDiscard | Interactive: image is staged (ProvidedImage state) → click Cancel → AlertDialog “Discard unsaved image?” appears → “Discard” closes everything, “Go Back” returns to ProvidedImage |
FullHappyPath | End-to-end: open dialog → drag/select file → preview appears → crop/zoom → check copyright → click Confirm → progress bar → dialog closes → onConfirm logged with result |
MDX contents:
- Overview: state machine orchestrator, 4 interaction states
- State diagram (PlantUML):
CanvasofEmptyImageState,ProvidedImageState,ComparisonMode- “Validation” section with
CanvasofValidationError - “Copyright Gate” section with
CanvasofCopyrightGate - “Warn on Discard” section with
CanvasofWarnOnDiscard - “Happy Path” section with
CanvasofFullHappyPath - Props table
Unit tests: image-upload-dialog.test.tsx
| Test | Assertion |
|---|---|
| renders nothing when open is false | no dialog in DOM |
| renders ImageDropZone in EmptyImage state | drop zone visible when open, no image |
| transitions to ProvidedImage on image input | simulate file input, preview area visible |
| shows CopyrightAcknowledgment | checkbox with legal text visible |
| Confirm button disabled when copyright unchecked | button has disabled attribute |
| Confirm button enabled when copyright checked | check box, button no longer disabled |
| calls onCancel on cancel click (no staged image) | cancel click, onCancel called |
| shows Warn dialog on cancel with staged image | stage image, cancel, AlertDialog visible |
| Warn discard calls onCancel | click Discard in Warn, onCancel called |
| Warn go-back returns to ProvidedImage | click Go Back, back to preview |
| shows validation error for invalid file | provide invalid file, error text in text-destructive visible |
| calls onConfirm with result on confirm | check copyright, click confirm, onConfirm called |
| shows progress bar during upload | mock upload in progress, progress element visible |
| comparison layout shown when existingImageUrl set | existing image panel + new image panel |
| no comparison layout when existingImageUrl null | only drop zone / preview shown |
Wave 4 — Integration
Section titled “Wave 4 — Integration”16. item-grid-columns (Modified Molecule)
Section titled “16. item-grid-columns (Modified Molecule)”No new story file. The existing item-grid stories are updated to use
ImageCellDisplay instead of the inline ImageCellRenderer. Visual output
is identical — this is a structural replacement, not a new story.
17. Barrel Exports
Section titled “17. Barrel Exports”After all components are implemented, src/canary.ts is updated with exports
for all new components and types. node tools/update-package-contents.js is
run to regenerate package contents documentation.
Artifact Summary
Section titled “Artifact Summary”| # | Component | Wave | Test File | Tests | Stories | MDX |
|---|---|---|---|---|---|---|
| U1 | getInitials | 1 | utilities/get-initials.test.ts | 8 | — | shared |
| U2 | getCroppedImage | 1 | utilities/get-cropped-image.test.ts | 5 | — | shared |
| 3 | Badge | 1 | badge.test.tsx (extend) | +4 | +3 stories | extend |
| 4 | Avatar | 1 | avatar test (extend) | +4 | +5 stories | extend |
| 5 | CopyrightAcknowledgment | 1 | copyright-acknowledgment.test.tsx | 7 | 5 stories | new |
| 6 | ImageDisplay | 1 | image-display.test.tsx | 9 | 8 stories | new |
| 7 | ImageDropZone | 1 | image-drop-zone.test.tsx | 10 | 6 stories | new |
| 8 | ImagePreviewEditor | 1 | image-preview-editor.test.tsx | 6 | 7 stories | new |
| 9 | ImageHoverPreview | 2 | image-hover-preview.test.tsx | 6 | 4 stories | new |
| 10 | ImageInspectorOverlay | 2 | image-inspector-overlay.test.tsx | 8 | 4 stories | new |
| 11 | ImageComparisonLayout | 2 | image-comparison-layout.test.tsx | 6 | 5 stories | new |
| 12 | ImageFormField | 2 | image-form-field.test.tsx | 11 | 7 stories | new |
| 13 | ImageCellDisplay | 3 | image-cell-display.test.tsx | 6 | 8 stories | new |
| 14 | ImageCellEditor | 3 | image-cell-editor.test.tsx | 5 | 1 story | shared |
| 15 | ImageUploadDialog | 3 | image-upload-dialog.test.tsx | 15 | 9 stories | new |
| P-1 | skeleton (context) | 0 | — | — | 3 stories | — |
| P-2 | tabs (context) | 0 | — | — | 2 stories | — |
| P-3 | alert-dialog (context) | 0 | — | — | 2 stories | — |
| Totals | 15 test files (12 new + 3 extend) | ~115 tests | 79 stories | 12 MDX |
Open Items
Section titled “Open Items”| # | Item | Notes |
|---|---|---|
| 1 | HEIC/HEIF browser testing | heic2any is lazy-loaded; stories should include a note that HEIC testing requires an actual .heic file (not mockable via URL) |
| 2 | Popover vs. Tooltip for ImageHoverPreview | Stories assume Popover primitive; if Tooltip is chosen instead, positioning behavior may differ slightly |
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved