Skip to content

Run 4: Cell Editor Upgrade

Promote callil’s SelectCellEditor from molecules/item-grid/ to atoms/grid/select/ as a first-class canary atom, replacing the jmpicnic EnumCellEditor. Generalizes the options format to accept both SelectOption[] and Record<string, string>.

Working directory: /Users/jmp/code/arda/ux-prototype/ (main clone, branch jmpicnic/component-consolidation)

Governs: analysis/grid-integration.md (decision #2)

#CriterionVerification CommandExpected Output
1Run 3 exit gate passedbash validate-exit.sh (Run 3)ALL CHECKS PASSED
2Branch jmpicnic/component-consolidation is checked outgit -C /Users/jmp/code/arda/ux-prototype branch --show-currentjmpicnic/component-consolidation
3Working tree is cleangit -C /Users/jmp/code/arda/ux-prototype status --porcelain(empty)
4SelectCellEditor molecule exists (Run 3 deliverable)test -f /Users/jmp/code/arda/ux-prototype/src/components/canary/molecules/item-grid/select-cell-editor.tsx && echo EXISTSEXISTS
5EnumCellEditor atom exists (to be replaced)test -d /Users/jmp/code/arda/ux-prototype/src/components/canary/atoms/grid/enum && echo EXISTSEXISTS
ArtifactPath (relative to ux-prototype/src/)FormatDescription
SelectCellEditor componentcomponents/canary/atoms/grid/select/select-cell-editor.tsxTSXPromoted cell editor with generalized options format
SelectCellDisplay componentcomponents/canary/atoms/grid/select/select-cell-display.tsxTSXDisplay renderer for select values (migrated from EnumCellDisplay)
Factory functioncomponents/canary/atoms/grid/select/select-cell-editor.tsxTSXcreateSelectCellEditor factory with StaticConfig baked in
Barrel exportcomponents/canary/atoms/grid/select/index.tsTSPublic exports for SelectCellEditor, SelectCellDisplay, factory, types
Storiescomponents/canary/atoms/grid/select/select.stories.tsxTSXDefault, WithManyOptions, KeyboardNavigation, BothFormats
Unit testscomponents/canary/atoms/grid/select/select.test.tsxTSXHighlight cycling, Enter/Escape, ARIA roles, checkmark, options format
VRT baselinePlaywright VRT configPNGSelectCellEditor popup screenshot
#TaskPersonaDepends OnStatusAcceptance Criteria
4.1Create atoms/grid/select/ directory with SelectCellEditor componentfront-end-engineerPendingselect-cell-editor.tsx exists with promoted SelectCellEditor; compiles cleanly; accepts both SelectOption[] and Record<string, string>
4.2Generalize options format to accept both input typesfront-end-engineer4.1PendingComponent normalizes Record<string, string> to SelectOption[] internally; both formats render identically
4.3Add StaticConfig/RuntimeConfig split and createSelectCellEditor factoryfront-end-engineer4.2PendingSelectCellEditorStaticConfig, SelectCellEditorProps, createSelectCellEditor factory exported; consistent with createTextCellEditor pattern
4.4Create SelectCellDisplay for non-editing modefront-end-engineer4.1Pendingselect-cell-display.tsx renders selected value label; handles missing value gracefully
4.5Add stories: Default, WithManyOptions, KeyboardNavigation, BothFormatsfront-end-engineer4.3, 4.4PendingAll 4 stories render in Storybook; stories demonstrate key behaviors
4.6Add unit tests: highlight cycling, Enter/Escape, ARIA roles, checkmark, options formatfront-end-engineer4.3PendingAll tests pass; coverage includes both options formats, keyboard navigation wrap-around, cancel via Escape
4.7Add play function with step DSLfront-end-engineer4.5PendingPlay function uses step DSL; steps: open editor -> Arrow Down 3x -> Enter -> verify value changed
4.8Update all EnumCellEditor consumers to use SelectCellEditorfront-end-engineer4.3PendingAll files that import from atoms/grid/enum updated to import from atoms/grid/select; npx tsc --noEmit passes
4.9Delete atoms/grid/enum/ directoryfront-end-engineer4.8PendingDirectory removed; no remaining references to EnumCellEditor or createEnumCellEditor in canary code
4.10Update canary barrel exportsfront-end-engineer4.9Pendingcanary.ts exports SelectCellEditor/SelectCellDisplay/factory/types instead of Enum equivalents
4.11Full verification gatefront-end-engineer4.1-4.10Pendingnpm run lint + npx tsc --noEmit + npm run test + npm run build-storybook all pass; no EnumCellEditor references in canary code; existing grid stories with enum columns still render
4.12Playwright MCP visual verificationfront-end-engineer4.11PendingSelectCellEditor popup renders correctly; keyboard navigation works (Arrow Down cycling, Enter to select, Escape to cancel); checkmark on selected item; existing enum grid stories show no regression
4.13VRT baseline for SelectCellEditor popupfront-end-engineer4.12PendingVRT baseline captured for SelectCellEditor popup story; npx playwright test --project=vrt passes

Source reference: /Users/jmp/code/arda/ux-prototype/src/components/canary/molecules/item-grid/select-cell-editor.tsx (Run 3 deliverable)

Create src/components/canary/atoms/grid/select/select-cell-editor.tsx by promoting the molecule-level SelectCellEditor. The promoted version:

  1. Moves from molecules/item-grid/ to atoms/grid/select/ (the canonical location for grid cell editor atoms).
  2. Retains all existing behavior: keyboard navigation (ArrowUp/ArrowDown cycling, Enter to select, Escape to cancel), checkmark indicator, scroll-into-view, ARIA listbox/option roles, useGridCellEditor integration, popup styling (max-height 240px, bg-popover).
  3. The values prop type is generalized in Task 4.2.

The current SelectCellEditor accepts only SelectOption[] (with { label: string, value: string }). Per the grid-integration decision, generalize to also accept Record<string, string>:

export type SelectOptions = SelectOption[] | Record<string, string>;
// Internal normalization:
function normalizeOptions(options: SelectOptions): SelectOption[] {
if (Array.isArray(options)) return options;
return Object.entries(options).map(([value, label]) => ({ value, label }));
}

The component normalizes internally, so all rendering and keyboard logic uses SelectOption[] regardless of input format. The Record<string, string> format is a convenience for simple enum-like values (key = stored value, value = display label), matching the pattern used by the existing EnumCellEditor.

Task 4.3: StaticConfig/RuntimeConfig Split and Factory

Section titled “Task 4.3: StaticConfig/RuntimeConfig Split and Factory”

Follow the existing canary cell editor convention (see atoms/grid/text/, atoms/grid/enum/ for reference):

/** Design-time configuration baked into the factory. */
export interface SelectCellEditorStaticConfig {
/** Available options. Can be SelectOption[] or Record<string, string>. */
options: SelectOptions;
}
/** Runtime props from AG Grid. */
export interface SelectCellEditorProps {
value: string | null;
onValueChange: (value: string | null) => void;
stopEditing: (cancel?: boolean) => void;
}
/** Handle exposed via useGridCellEditor. */
export interface SelectCellEditorHandle {
isCancelAfterEnd: () => boolean;
}
/** Factory: bakes static config into a cell editor component. */
export function createSelectCellEditor(
staticConfig: SelectCellEditorStaticConfig
): React.ComponentType<SelectCellEditorProps>;

The factory returns a component with options baked in from static config, matching the pattern of createEnumCellEditor, createTextCellEditor, etc.

Migrate from atoms/grid/enum/enum-cell-display.tsx. The display component renders the label for a stored value:

export interface SelectCellDisplayStaticConfig {
options: SelectOptions;
}
export interface SelectCellDisplayProps {
value: string | null;
}

Behavior: looks up the value in the normalized options array and renders the label. If value is not found, renders the raw value (or empty for null).

Create src/components/canary/atoms/grid/select/select.stories.tsx with 4 stories:

  1. Default: Grid with a select column using 4-5 options. Click a cell to open the editor.
  2. WithManyOptions: 20+ options to demonstrate scroll behavior with the 240px max-height.
  3. KeyboardNavigation: Same as Default but the play function demonstrates Arrow Down/Up cycling, Enter to select, Escape to cancel.
  4. BothFormats: Two grids side by side — one using SelectOption[], one using Record<string, string> — to show both formats produce identical UI.

All stories use the createSelectCellEditor factory with cellEditorPopup: true in the column definition.

Create src/components/canary/atoms/grid/select/select.test.tsx testing:

  1. Highlight cycling: Arrow Down wraps from last to first; Arrow Up wraps from first to last.
  2. Enter selects highlighted: Enter calls onValueChange with the highlighted option’s value and calls stopEditing.
  3. Escape cancels: Escape sets isCancelAfterEnd to true and calls stopEditing(true).
  4. ARIA roles: Container has role="listbox", each option has role="option", selected option has aria-selected="true".
  5. Checkmark rendering: The currently selected value shows a Check icon.
  6. Both options formats: Record<string, string> is normalized correctly and renders identically to SelectOption[].
  7. SelectCellDisplay: Renders label for known value, raw value for unknown, empty for null.

Add to the KeyboardNavigation story:

play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Open the select editor', async () => {
// Double-click on a cell with the select editor
});
await storyStepDelay();
await step('Navigate down 3 times', async () => {
await userEvent.keyboard('{ArrowDown}{ArrowDown}{ArrowDown}');
});
await storyStepDelay();
await step('Select with Enter and verify value changed', async () => {
await userEvent.keyboard('{Enter}');
// Verify the cell shows the newly selected value
});
};

Files that reference EnumCellEditor, createEnumCellEditor, or import from atoms/grid/enum:

Canary code (must update):

  • src/canary.ts — barrel exports
  • src/components/canary/molecules/data-grid/data-grid.stories.tsx — story using enum editor
  • src/components/canary/atoms/grid/enum/enum.stories.tsx — replaced by new stories
  • src/components/canary/atoms/grid/enum/enum.test.tsx — replaced by new tests

Extras code (do NOT modify — extras layer has its own copy):

  • src/components/extras/atoms/grid/enum/ — leave as-is

Canary-refactor code (update if referencing canary enum):

  • src/canary-refactor/components/columnPresets.tsx — update import

Documentation (update references):

  • src/docs/workflows/using-the-design-system.mdx — update enum references to select

For each consumer: replace EnumCellEditor with SelectCellEditor, createEnumCellEditor with createSelectCellEditor, EnumCellDisplay with SelectCellDisplay, and update import paths from atoms/grid/enum to atoms/grid/select.

Task 4.9: Delete atoms/grid/enum/ Directory

Section titled “Task 4.9: Delete atoms/grid/enum/ Directory”

Remove src/components/canary/atoms/grid/enum/ and all contents:

  • enum-cell-display.tsx
  • enum-cell-editor.tsx
  • enum.stories.tsx
  • enum.test.tsx
  • index.ts

Verify no remaining references: grep -r 'atoms/grid/enum' src/ --include='*.ts' --include='*.tsx' | grep -v extras/ | grep -v node_modules

Important: Do NOT delete src/components/extras/atoms/grid/enum/ — the extras layer maintains its own copy.

In src/canary.ts, replace:

// Cell atoms: enum
export {
EnumCellDisplay,
EnumCellEditor,
createEnumCellEditor,
} from './components/canary/atoms/grid/enum';
export type {
EnumCellDisplayProps,
EnumCellDisplayStaticConfig,
EnumCellEditorProps,
EnumCellEditorStaticConfig,
EnumCellEditorHandle,
} from './components/canary/atoms/grid/enum';

With:

// Cell atoms: select
export {
SelectCellDisplay,
SelectCellEditor,
createSelectCellEditor,
} from './components/canary/atoms/grid/select';
export type {
SelectCellDisplayProps,
SelectCellDisplayStaticConfig,
SelectCellEditorProps,
SelectCellEditorStaticConfig,
SelectCellEditorHandle,
SelectOption,
SelectOptions,
} from './components/canary/atoms/grid/select';

Run all checks in sequence:

  1. npm run lint — pass
  2. npx tsc --noEmit — pass
  3. npm run test — pass (new select tests + existing tests)
  4. npm run build-storybook — pass
  5. Verify no EnumCellEditor references remain in canary code:
    Terminal window
    grep -r 'EnumCellEditor\|createEnumCellEditor\|atoms/grid/enum' src/ --include='*.ts' --include='*.tsx' | grep -v extras/ | grep -v node_modules
    Expected: zero matches.
  6. Verify existing grid stories that previously used enum columns still render (the stories should now use SelectCellEditor transparently).

Task 4.12: Playwright MCP Visual Verification

Section titled “Task 4.12: Playwright MCP Visual Verification”

Start Storybook (npm run dev in ux-prototype/) and use Playwright MCP to:

  1. Navigate to SelectCellEditor > Default story. Click a cell to open the popup. Screenshot.
  2. Navigate to SelectCellEditor > WithManyOptions story. Open popup, scroll to bottom. Screenshot.
  3. Navigate to SelectCellEditor > KeyboardNavigation story. Run the play function. Verify:
    • Arrow Down moves highlight downward
    • Highlight wraps from bottom to top
    • Enter selects the highlighted option
    • Escape cancels without changing value
  4. Navigate to SelectCellEditor > BothFormats story. Verify both grids render identically.
  5. Navigate to an existing grid story that previously used EnumCellEditor (e.g., DataGrid stories). Verify no visual regression.

Generate VRT baseline for the SelectCellEditor popup:

Terminal window
npx playwright test --project=vrt --grep "SelectCellEditor"

This captures the custom dropdown popup appearance (replacing the native <select> that EnumCellEditor used, which VRT never captured because native dropdowns are OS-rendered).

4.1 (create atom) ──→ 4.2 (generalize options) ──→ 4.3 (factory + config)
4.1 ──→ 4.4 (display component)
4.3, 4.4 ──→ 4.5 (stories)
4.3 ──→ 4.6 (unit tests)
4.5 ──→ 4.7 (play function)
4.3 ──→ 4.8 (update consumers)
4.8 ──→ 4.9 (delete enum/)
4.9 ──→ 4.10 (barrel exports)
4.1-4.10 ──→ 4.11 (verification gate)
4.11 ──→ 4.12 (Playwright MCP)
4.12 ──→ 4.13 (VRT baseline)

Tasks 4.1-4.4 form the component creation phase. Tasks 4.5-4.7 form the test/story phase (can overlap with 4.8). Tasks 4.8-4.10 form the migration/cleanup phase. Tasks 4.11-4.13 are sequential verification.

#CriterionVerification CommandExpected Output
1Lint passesnpm run lint (in ux-prototype/)Exit 0
2TypeScript compilesnpx tsc --noEmit (in ux-prototype/)Exit 0
3All tests passnpm run test (in ux-prototype/)Exit 0, all tests pass
4Storybook buildsnpm run build-storybook (in ux-prototype/)Exit 0
5SelectCellEditor atom existstest -f src/components/canary/atoms/grid/select/index.ts && echo EXISTSEXISTS
6EnumCellEditor directory deletedtest -d src/components/canary/atoms/grid/enum && echo EXISTS || echo DELETEDDELETED
7No EnumCellEditor refs in canary codegrep -r 'EnumCellEditor|createEnumCellEditor' src/ --include='*.ts' --include='*.tsx' | grep -v extras/ | grep -v node_modules | wc -l0
8No atoms/grid/enum imports in canary codegrep -r 'atoms/grid/enum' src/ --include='*.ts' --include='*.tsx' | grep -v extras/ | grep -v node_modules | wc -l0
9New stories rendernpm run build-storybook passes (covers story indexing)Exit 0
10Existing grid stories with enum columns still workDataGrid stories build + test passIncluded in criteria 3 and 4
11VRT baseline capturedtest -f test-results/vrt/*.png && echo EXISTS (path may vary by VRT config)Baseline PNG exists
12canary.ts exports SelectCellEditorgrep 'SelectCellEditor' src/canary.tsMatch found
13Extras enum untouchedtest -f src/components/extras/atoms/grid/enum/index.ts && echo EXISTSEXISTS

You are promoting the callil SelectCellEditor from a molecule to a first-class canary atom, replacing the EnumCellEditor. Follow the existing cell editor atom conventions in atoms/grid/text/ and atoms/grid/number/ for structure.

Key decisions (already made, do not revisit):

  • SelectCellEditor replaces EnumCellEditor entirely (per grid-integration.md decision #2)
  • Options accept both SelectOption[] and Record<string, string> formats
  • Follow StaticConfig/RuntimeConfig split with createSelectCellEditor factory
  • Extras layer enum (src/components/extras/atoms/grid/enum/) is NOT touched

File structure for atoms/grid/select/:

select-cell-editor.tsx -- component + factory + types
select-cell-display.tsx -- display renderer + types
select.stories.tsx -- 4 stories with play functions using step DSL
select.test.tsx -- unit tests
index.ts -- barrel exports

Consumer update scope (canary code only, not extras):

  • src/canary.ts — barrel exports
  • src/components/canary/molecules/data-grid/data-grid.stories.tsx — story references
  • src/canary-refactor/components/columnPresets.tsx — import update
  • src/docs/workflows/using-the-design-system.mdx — documentation references

After all changes, run the full verification gate: npm run lint, npx tsc --noEmit, npm run test, npm run build-storybook.

Verify zero EnumCellEditor references remain in canary code (excluding extras/): grep -r 'EnumCellEditor\|createEnumCellEditor\|atoms/grid/enum' src/ --include='*.ts' --include='*.tsx' | grep -v extras/ | grep -v node_modules

At run completion, write the following to implementation/run-4-cell-editor-upgrade/:

ArtifactFileDescription
Run summarysummary.mdWhat was done, decisions made during implementation, deviations from plan
Byproducts logbyproducts.mdDiscovered issues, TODOs, observations for later runs
Validation outputvalidation-output.txtStdout from validate-exit.sh execution
Session logsession-log.mdAgent session IDs, timestamps, notable events

Path: documentation/src/content/docs/roadmap/backlog/requested/callil-consolidation/implementation/run-4-cell-editor-upgrade/

ArtifactSource RunPath/Location
SelectCellEditor molecule (source reference)Run 3src/components/canary/molecules/item-grid/select-cell-editor.tsx
EnumCellEditor atom (to be replaced)Pre-existingsrc/components/canary/atoms/grid/enum/
EnumCellDisplay atom (pattern reference)Pre-existingsrc/components/canary/atoms/grid/enum/enum-cell-display.tsx
Cell editor factory conventionPre-existingsrc/components/canary/atoms/grid/text/, atoms/grid/number/
Canary barrel exportsPre-existingsrc/canary.ts
ArtifactConsumer RunPath/Location
SelectCellEditor atomRun 5, Run 6src/components/canary/atoms/grid/select/
createSelectCellEditor factoryRun 5 (entity-data-grid), Run 6 (item-grid)src/components/canary/atoms/grid/select/
Updated canary.ts barrelRun 7 (final barrel review)src/canary.ts
VRT baseline for SelectCellEditor popupRun 8 (acceptance comparison)VRT test results