Skip to content

Business Affiliates Stories — Canary Refactoring Specification

Specification for refactoring the Business Affiliates use case stories (src/use-cases/reference/business-affiliates/) to use canary components from src/components/canary/. The existing stories use hand-rolled UI for grids, drawers, dialogs, search, pagination, and column visibility. Canary equivalents now exist for most of these, proven by the Items and General Behaviors stories.

The refactoring preserves the existing directory structure, use-case IDs (BA-0001 through BA-0005, BR-0002), sidebar titles, MSW handlers, and mock data. Each story is updated in place — not duplicated alongside a “(Canary)” variant.

The following must be verified before implementation begins.

  1. Canary components available. All canary components referenced in the Component Sourcing table are implemented and rendering in the Storybook build on main.
  2. Existing checks green. Lint, TypeScript, Storybook build, and all unit tests pass on the working branch.
  3. Existing canary variants reviewed. The two proof-of-concept canary stories already in the codebase (view-suppliers-list-canary.stories.tsx and supplier-details-canary.stories.tsx) have been reviewed for patterns and lessons learned. These files are removed as part of this project since the primary stories will be refactored directly.

Stories must follow a strict sourcing hierarchy:

  1. Canary components (@/components/canary/) — preferred.
  2. Story-local composition — hand-rolled markup within the story file for behaviors that have no canary equivalent.
  3. Vendored components (@frontend/) — prohibited. All vendored imports in BA stories are replaced by canary equivalents.

The following canary components replace hand-rolled BA equivalents:

Hand-Rolled Component Canary Replacement Import Path
SuppliersPage (grid, search, pagination, column toggle) createEntityDataGrid() factory @/components/canary/organisms/shared/entity-data-grid/create-entity-data-grid
App shell: SidebarProvider + SidebarInset + AppSidebar Sidebar, SidebarHeader, SidebarNav, SidebarNavItem, SidebarUserMenu, AppHeader @/components/canary/organisms/sidebar/, molecules/sidebar/, organisms/app-header/, primitives/sidebar
SupplierDrawer (view mode — read-only fields) Drawer + ArdaFieldList @/components/canary/atoms/drawer/, @/components/canary/molecules/field-list/
SupplierDrawer (create/edit mode — form shell) Drawer (DrawerHeader, DrawerBody, DrawerFooter) @/components/canary/atoms/drawer/
ArdaConfirmDialog (focus trap, escape, backdrop) AlertDialog primitive @/components/canary/primitives/alert-dialog
ColumnVisibilityDropdown DropdownMenu primitive @/components/canary/primitives/dropdown-menu
SelectAllHeaderComponent + SelectionCheckboxCell Built into createEntityDataGrid() row selection (included in entity-data-grid factory)
Vendored Button Canary Button @/components/canary/atoms/button/button
Vendored Collapsible / CollapsibleTrigger / CollapsibleContent Canary Collapsible primitive @/components/canary/primitives/collapsible
Vendored ArdaGrid Canary DataGrid (via createEntityDataGrid) @/components/canary/molecules/data-grid/
Custom search input (300ms debounce) Built into createEntityDataGrid() search (included in entity-data-grid factory)
Custom pagination (prev/next buttons) Built into createEntityDataGrid() pagination (included in entity-data-grid factory)
Selection-driven action bar OverflowToolbar @/components/canary/molecules/overflow-toolbar/

The following behaviors have no canary equivalent and remain hand-rolled within story files. They are not vendored imports — they are story-local composition using canary primitives and atoms.

Behavior Reason Canary Atoms Used
Create form body (identity, contact, address, legal, notes fields) No canary form component exists Drawer shell, Button, Collapsible, HTML form elements
Edit form body (pre-populated fields, dirty tracking) No canary form component exists Drawer shell, Button, Collapsible, HTML form elements
AffiliateTypeahead (lookup + create-on-the-fly) No canary standalone typeahead exists (only grid cell editor) Button, HTML input/listbox. Wrapped in canary app shell for stories.
Role badge rendering in grid cells Simple inline cell renderer; canary Badge can be used for styling Badge atom (optional)

Each refactored story produces an updated *.stories.tsx file. No new directories or MDX files are created — the existing structure is preserved.

Refactored stories must:

  1. Replace all vendored imports (@frontend/) with canary equivalents.
  2. Remove the hand-rolled SuppliersPage and compose from createEntityDataGrid() + canary app shell.
  3. Replace SupplierDrawer view mode with Drawer + ArdaFieldList.
  4. Replace SupplierDrawer create/edit mode shell with canary Drawer (DrawerHeader/DrawerBody/DrawerFooter), keeping form body markup.
  5. Replace ArdaConfirmDialog with canary AlertDialog primitive.
  6. Preserve all existing play functions, updating selectors as needed for the new DOM structure.
  7. Preserve all existing story exports and sidebar titles.
File Action
suppliers-page.tsx Rewrite. Replace with a canary-composed page using createEntityDataGrid(), Sidebar, AppHeader. Export a SuppliersCanaryPage component. Existing column definitions move to inline factory config or a local column-defs.ts.
supplier-drawer.tsx Rewrite. Replace with canary Drawer composition. View mode uses ArdaFieldList. Create/edit modes use DrawerBody with hand-rolled form markup.
confirm-dialog.tsx Remove. Replace all usages with canary AlertDialog primitive.
grid-cell-renderers.tsx Remove. SelectAllHeaderComponent and SelectionCheckboxCell are handled by createEntityDataGrid() row selection. Role badge renderer moves inline or uses canary Badge atom.
column-defs.tsx Update. Adapt column definitions for createEntityDataGrid ColDef format. Remove references to custom cell renderers.
suppliers-sidebar.tsx Remove. Replaced by canary Sidebar organism + molecules composed in the page wrapper.
affiliate-typeahead.tsx Keep. No canary standalone typeahead exists. Minor updates: replace any vendored imports with canary atoms.
mock-data.ts Keep. No changes needed.
msw-handlers.ts Keep. No changes needed.
types.ts Keep. No changes needed.
story-step-delay.ts Keep. No changes needed.

The following files are removed after their patterns have been absorbed into the primary stories:

  • browse-and-search/view-suppliers-list-canary.stories.tsx
  • view-details/supplier-details-canary.stories.tsx

This is a refactoring project — every existing story is modified in place. The sidebar hierarchy, use-case IDs, and story export names are preserved. Visual output changes are expected (canary styling differs from vendored) but behavioral coverage must remain equivalent.

After refactoring, each story must:

  1. Render without errors in Storybook.
  2. Pass its existing play function (with selector updates as needed).
  3. Maintain the same sidebar title and position.
  4. Demonstrate the same user-visible behavior as before.

All refactored stories follow the full-app context composition pattern established by the Items canary stories:

Sidebar (organism)
├── SidebarHeader (molecule)
├── SidebarNav (molecule)
│ └── SidebarNavItem[] (molecule)
└── SidebarUserMenu (molecule)
content={
SidebarInset (primitive)
├── AppHeader (organism)
├── Main content area
│ └── EntityDataGrid (from factory)
└── Drawer (atom) ← detail/create/edit panel
}

The createEntityDataGrid<SupplierEntity>() factory replaces the hand-rolled SuppliersPage grid, search, pagination, and column visibility. Configuration:

  • displayName: 'SupplierGrid'
  • persistenceKeyPrefix: 'ba-supplier-grid'
  • columnDefs: adapted from existing column-defs.tsx
  • getEntityId: (s) => s.id (maps to businessAffiliateEntityId)
  • searchConfig.fields: ['name', 'contact', 'email', 'city']
  • Row selection enabled for delete flows
  • onRowClick callback for detail drawer

View mode:

Drawer
├── DrawerHeader → supplier name + close button
├── DrawerBody → ArdaFieldList with supplier fields
└── DrawerFooter → action buttons (Edit, Delete)

Create/edit mode:

Drawer
├── DrawerHeader → mode title + close button
├── DrawerBody → hand-rolled form (Collapsible sections)
└── DrawerFooter → Save/Cancel buttons

The hand-rolled ArdaConfirmDialog is replaced by the canary AlertDialog primitive, which provides equivalent functionality:

  • Focus trap (built into Radix AlertDialog)
  • Escape key dismissal
  • Backdrop click dismissal (configurable)
  • AlertDialogAction (confirm) + AlertDialogCancel (cancel) buttons

Existing play functions are preserved with minimal selector changes. Common adjustments:

  • Search input: canvas.getByRole('searchbox') (entity-data-grid uses role="searchbox")
  • Grid rows: [role="gridcell"] selectors remain compatible
  • Drawer: use screen.getAllByRole('dialog') for Radix portal-rendered content (same pattern as canary variant stories)
  • Confirm dialog: screen.getByRole('alertdialog') for AlertDialog

All stories continue to use mock data only via MSW handlers. The existing _shared/msw-handlers.ts and _shared/mock-data.ts are unchanged. The entity-data-grid factory receives data via props — MSW handlers are used in stories that demonstrate server interaction (create, edit, delete).

Existing sidebar titles are preserved exactly:

Use Cases/Reference/Business Affiliates/BA-0001 Browse and Search/{story}
Use Cases/Reference/Business Affiliates/BA-0002 View Details/{story}
Use Cases/Reference/Business Affiliates/BA-0003 Create Supplier/{story}
Use Cases/Reference/Business Affiliates/BA-0004 Edit Supplier/{story}
Use Cases/Reference/Business Affiliates/BA-0005 Delete Supplier/{story}
Use Cases/Reference/Business Affiliates/BR-0002 Affiliate Typeahead/{story}

Directory: browse-and-search/

The core SuppliersPage is replaced by createEntityDataGrid() + canary app shell. Search, pagination, column visibility, and row selection are handled by the factory.

Story File Sidebar Title Refactoring Scope
view-suppliers-list.stories.tsx 0001 View Suppliers List Replace SuppliersPage with canary page wrapper. Grid renders via createEntityDataGrid. Update play function selectors for entity-data-grid DOM.
search-by-name.stories.tsx 0002 Search by Name Search is now built into entity-data-grid. Story demonstrates the same behavior via factory search config. Update play function to use role="searchbox".
toggle-columns.stories.tsx 0003 Toggle Columns Column visibility handled by entity-data-grid or composed via DropdownMenu primitive. Update play function selectors.
select-multiple.stories.tsx 0005 Select Multiple Row selection built into entity-data-grid. Selection toolbar composed with OverflowToolbar. Update play function for new selection UI.
pagination.stories.tsx 0006 Pagination Pagination built into entity-data-grid. Story demonstrates the same page nav behavior. Update play function selectors.
deep-link.stories.tsx 0007 Deep Link Deep-link behavior (auto-open drawer on load) must be preserved via onRowClick + initial selection logic in the page wrapper.

Removed: view-suppliers-list-canary.stories.tsx (proof-of-concept absorbed into primary story).

Directory: view-details/

The SupplierDrawer in view mode is replaced by canary Drawer + ArdaFieldList. The supplier field mapping function converts entity data to FieldDef[].

Story File Sidebar Title Refactoring Scope
supplier-details-panel.stories.tsx 0001 Supplier Details Panel (Default, MinimalData, CloseDrawer, SectionCollapse) Replace SupplierDrawer view mode with Drawer + ArdaFieldList. Collapsible sections use canary Collapsible primitive. Multiple story exports preserved. Update play function for Radix portal selectors.

Removed: supplier-details-canary.stories.tsx (proof-of-concept absorbed into primary story).

Directory: create-supplier/

The drawer shell is replaced by canary Drawer atoms. The multi-step form body (identity, contact, address/legal, notes) remains hand-rolled inside DrawerBody. The createUseCaseStories framework is preserved.

Story File Sidebar Title Refactoring Scope
happy-path.stories.tsx Happy Path (Interactive / Stepwise / Automated) Replace drawer shell with canary Drawer. Form body stays hand-rolled inside DrawerBody. Replace vendored Button with canary. Framework stories preserved. Update play function selectors.
validation-errors.stories.tsx 0002 Validation Errors Same drawer shell replacement. Error display markup may use canary styling. Update selectors.
experimental-wizard.stories.tsx 0003 [Experimental] Wizard Same drawer shell replacement. Wizard step logic stays hand-rolled. Framework stories preserved.

Directory: edit-supplier/

The drawer transitions between view mode (ArdaFieldList) and edit mode (hand-rolled form in DrawerBody). The createUseCaseStories framework is preserved for the happy path.

Story File Sidebar Title Refactoring Scope
happy-path.stories.tsx Happy Path (Interactive / Stepwise / Automated) View mode → canary Drawer + ArdaFieldList. Edit mode → canary Drawer shell + hand-rolled form. Mode toggle logic preserved. Framework stories preserved. Update play function selectors.
validation-errors.stories.tsx 0002 Validation Errors Same pattern. Error display and cancel-discard behavior preserved. Update selectors.

Directory: delete-supplier/

The hand-rolled ArdaConfirmDialog is replaced by canary AlertDialog primitive. The grid uses createEntityDataGrid() with row selection.

Story File Sidebar Title Refactoring Scope
delete-from-list.stories.tsx 0001 Delete from List Grid selection via entity-data-grid. Confirm dialog → canary AlertDialog. Selection toolbar via OverflowToolbar. Update play function for alertdialog role.
delete-from-panel.stories.tsx 0002 Delete from Detail Panel Detail drawer via canary Drawer + ArdaFieldList. Delete button triggers AlertDialog. Update play function selectors.
delete-error.stories.tsx 0003 Delete Error Same AlertDialog replacement. Server error handling (MSW 500 response) preserved. Update selectors.

Directory: affiliate-typeahead/

The AffiliateTypeahead component has no canary equivalent and stays hand-rolled. The stories are updated to use the canary app shell and replace any vendored atom imports.

Story File Sidebar Title Refactoring Scope
create-on-the-fly.stories.tsx 0002 Create on the Fly (SelectExisting, CreateNew, LoadingState, EmptySearch, KeyboardNav, EscapeDismiss) Replace vendored Button with canary. Wrap in canary app shell if not already. Typeahead component itself unchanged. Update play function selectors if atoms change.

Directory: pages/

Story File Sidebar Title Refactoring Scope
suppliers-list-view.stories.tsx Ignore/Pages/Suppliers List View Composite story. Replace with canary-composed full page. This story combines browse, select, drawer, and delete in one view. Refactored to compose all canary replacements together.
Use Case Story Files Stories Modified Stories Removed
BA-0001 Browse and Search 6 6 1 (canary variant)
BA-0002 View Details 1 1 1 (canary variant)
BA-0003 Create Supplier 3 3 0
BA-0004 Edit Supplier 2 2 0
BA-0005 Delete Supplier 3 3 0
BR-0002 Affiliate Typeahead 1 1 0
Pages (support) 1 1 0
Total 17 17 2

Plus shared module changes: 4 files rewritten, 2 files removed, 1 file updated, 4 files unchanged.

The existing directory structure is preserved. Files marked with ❌ are removed; files marked with ✎ are modified.

src/use-cases/reference/business-affiliates/
├── _shared/
│ ├── affiliate-typeahead.tsx ✏ minor vendored import updates
│ ├── column-defs.tsx ✏ adapt for createEntityDataGrid
│ ├── confirm-dialog.tsx ✘ removed (replaced by AlertDialog)
│ ├── grid-cell-renderers.tsx ✘ removed (handled by factory)
│ ├── mock-data.ts (unchanged)
│ ├── msw-handlers.ts (unchanged)
│ ├── story-step-delay.ts (unchanged)
│ ├── supplier-drawer.tsx ✏ rewrite: canary Drawer composition
│ ├── suppliers-page.tsx ✏ rewrite: canary app shell + factory
│ ├── suppliers-sidebar.tsx ✘ removed (replaced by Sidebar organism)
│ └── types.ts (unchanged)
├── affiliate-typeahead/
│ ├── affiliate-typeahead.mdx (unchanged)
│ └── create-on-the-fly.stories.tsx ✏ canary atom imports
├── browse-and-search/
│ ├── browse-and-search.mdx (unchanged)
│ ├── deep-link.stories.tsx ✏ canary page wrapper
│ ├── pagination.stories.tsx ✏ canary page wrapper
│ ├── search-by-name.stories.tsx ✏ canary page wrapper
│ ├── select-multiple.stories.tsx ✏ canary page wrapper + OverflowToolbar
│ ├── toggle-columns.stories.tsx ✏ canary page wrapper + DropdownMenu
│ ├── view-suppliers-list.stories.tsx ✏ canary page wrapper
│ └── view-suppliers-list-canary.stories.tsx ✘ removed
├── create-supplier/
│ ├── create-supplier.mdx (unchanged)
│ ├── experimental-wizard.stories.tsx ✏ canary Drawer shell
│ ├── happy-path.stories.tsx ✏ canary Drawer shell
│ └── validation-errors.stories.tsx ✏ canary Drawer shell
├── delete-supplier/
│ ├── delete-error.stories.tsx ✏ AlertDialog + canary page
│ ├── delete-from-list.stories.tsx ✏ AlertDialog + canary page
│ ├── delete-from-panel.stories.tsx ✏ AlertDialog + canary Drawer
│ ├── delete-supplier.mdx (unchanged)
│ ├── deletable-suppliers-page.tsx ✏ rewrite: canary composition
│ └── panel-deletable-suppliers-page.tsx ✏ rewrite: canary composition
├── edit-supplier/
│ ├── editable-suppliers-page.tsx ✏ rewrite: canary composition
│ ├── edit-supplier.mdx (unchanged)
│ ├── happy-path.stories.tsx ✏ canary Drawer (view + edit modes)
│ └── validation-errors.stories.tsx ✏ canary Drawer
├── pages/
│ ├── import-suppliers-modal.tsx ✏ canary Dialog if applicable
│ └── suppliers-list-view.stories.tsx ✏ full canary rewrite
├── view-details/
│ ├── supplier-details-panel.stories.tsx ✏ canary Drawer + ArdaFieldList
│ ├── supplier-details-canary.stories.tsx ✘ removed
│ └── view-details.mdx (unchanged)
├── business-affiliates.mdx (unchanged)
├── mock-data.ts (unchanged)
├── msw-handlers.ts (unchanged)
└── types.ts (unchanged)

Stories are ordered by dependency: the shared module must be refactored first, then stories that depend only on the grid, then stories that also depend on the drawer, and finally stories that combine grid + drawer + dialog.

Rewrite the shared components that all stories depend on.

Scope:

  • Rewrite _shared/suppliers-page.tsx → canary app shell + createEntityDataGrid()
  • Rewrite _shared/supplier-drawer.tsx → canary Drawer composition (view/create/edit modes)
  • Update _shared/column-defs.tsx for entity-data-grid format
  • Remove _shared/confirm-dialog.tsx, _shared/grid-cell-renderers.tsx, _shared/suppliers-sidebar.tsx
  • Update _shared/affiliate-typeahead.tsx vendored imports

Gate: npm run lint, npx tsc --noEmit pass. No stories need to render yet (they will be updated in subsequent waves).

Wave 2 — Browse and Search Stories (BA-0001)

Section titled “Wave 2 — Browse and Search Stories (BA-0001)”

Refactor the grid-focused stories that depend on the shared page wrapper.

Scope:

  • Refactor all 6 stories in browse-and-search/
  • Remove view-suppliers-list-canary.stories.tsx
  • Update play functions for entity-data-grid selectors

Gate: Storybook builds. All BA-0001 stories render. All play functions pass.

Wave 3 — View Details and Edit Stories (BA-0002, BA-0004)

Section titled “Wave 3 — View Details and Edit Stories (BA-0002, BA-0004)”

Refactor stories that depend on the drawer in view and edit modes.

Scope:

  • Refactor view-details/supplier-details-panel.stories.tsx
  • Remove view-details/supplier-details-canary.stories.tsx
  • Refactor edit-supplier/happy-path.stories.tsx and edit-supplier/validation-errors.stories.tsx
  • Refactor edit-supplier/editable-suppliers-page.tsx

Gate: Storybook builds. All BA-0002 and BA-0004 stories render. All play functions pass.

Wave 4 — Create and Delete Stories (BA-0003, BA-0005)

Section titled “Wave 4 — Create and Delete Stories (BA-0003, BA-0005)”

Refactor stories that use the drawer in create mode and the confirm dialog.

Scope:

  • Refactor all 3 stories in create-supplier/
  • Refactor all 3 stories in delete-supplier/ + wrapper pages
  • Refactor pages/suppliers-list-view.stories.tsx
  • Refactor pages/import-suppliers-modal.tsx if applicable

Gate: Storybook builds. All BA-0003, BA-0005, and Pages stories render. All play functions pass.

Wave 5 — Typeahead and Final Verification

Section titled “Wave 5 — Typeahead and Final Verification”

Refactor the typeahead stories and perform final verification.

Scope:

  • Refactor affiliate-typeahead/create-on-the-fly.stories.tsx
  • Full regression: all BA stories render, all play functions pass
  • VRT check: run VRT tests, evaluate visual diffs, retake baselines if acceptable

Gate: All checks pass: npm run lint, npx tsc --noEmit, npm run build-storybook, npm run test, VRT tests.

At the end of each wave:

  1. Dead code sweep. Verify no orphaned imports, unused exports, or unreferenced files remain from the refactoring. Remove any pre-existing dead code discovered during the wave.
  2. npm run lint passes (catches unused imports/variables).
  3. npx tsc --noEmit passes (catches dangling references to removed files).
  4. npm run build-storybook completes without errors.
  5. All unit tests pass (npm run test).
  6. All story play functions in the wave scope pass in the Storybook test runner.
  7. VRT check (Wave 5 only). Run VRT tests (npx playwright test --project=vrt). Evaluate visual diffs — canary styling differs from vendored, so diffs are expected. Retake baselines (./tools/generate-vrt-baselines.sh) for acceptable changes.
  8. Changes are committed locally before proceeding to the next wave.
Canary Component Stories That Use It
createEntityDataGrid() All BA-0001 stories, BA-0005 (delete from list), Pages composite
Sidebar + molecules All stories (app shell)
AppHeader All stories (app shell)
Drawer + atoms BA-0002 (view), BA-0003 (create), BA-0004 (edit), BA-0005 (delete from panel)
ArdaFieldList BA-0002 (view mode), BA-0004 (view mode before edit)
AlertDialog BA-0005 (all delete stories)
OverflowToolbar BA-0001 select-multiple, BA-0005 delete-from-list
DropdownMenu BA-0001 toggle-columns
Button All stories
Collapsible BA-0002 (section collapse), BA-0003/BA-0004 (form sections)

Each wave includes a dead code sweep. Code is considered dead if it is:

  • Unreferenced — no import or usage anywhere in the BA story tree after refactoring.
  • Superseded — replaced by a canary component (e.g., vendored imports that were the sole reason a utility existed).
  • Pre-existing dead code — code that was already unused before the refactoring began.
Category Examples Wave
Deleted shared files confirm-dialog.tsx, grid-cell-renderers.tsx, suppliers-sidebar.tsx Wave 1
Deleted canary variant stories view-suppliers-list-canary.stories.tsx, supplier-details-canary.stories.tsx Wave 2, Wave 3
Orphaned imports and type aliases Vendored component imports, unused type re-exports, stale interface fields Each wave (during refactoring)
Unused helper functions Functions in shared modules that lose all callers after refactoring Each wave (during refactoring)
Duplicate files at root level business-affiliates/mock-data.ts and business-affiliates/msw-handlers.ts (duplicates of _shared/ versions — verify and remove if unused) Wave 1
Pre-existing dead code Any unreferenced exports, unused variables, or commented-out blocks found during refactoring Each wave (opportunistic)

After each wave, run npx tsc --noEmit to confirm no dangling references. ESLint no-unused-vars and no-unused-imports rules catch orphaned imports. If a removed file is still referenced, the TypeScript build will fail at the wave gate.

The following are explicitly excluded from this specification:

  • New canary component creation. No new atoms, molecules, or organisms are created. Only existing canary components are used.
  • Form component. No canary form field/layout components exist. Create and edit form bodies remain hand-rolled.
  • Standalone typeahead component. The AffiliateTypeahead stays hand-rolled. A canary typeahead atom/molecule would be a separate project.
  • MDX documentation changes. Existing .mdx files are not modified.
  • Mock data or MSW handler changes. Existing mocks are preserved as-is.
  • New stories. No new story files are created. This is a refactoring of existing stories only.
  • Use-case ID or directory restructuring. The existing hierarchy is preserved exactly.

Copyright: (c) Arda Systems 2025-2026, All rights reserved