feat: widget preview cards as primary selector surface#19
Conversation
WidgetSelector now uses preview cards as the primary selection surface. Each card is a button[role=switch] containing a live themed mini-render of the widget. Click toggles inclusion. Visual state mirrors selection (selected = full opacity + accent ring; unselected = dimmed). The redundant text-only checkbox row is removed. - Public component API unchanged (selection + onChange props) - Cards inherit theme via CSS vars from App's themeToStyleVars wrapper — swapping presets re-themes all 8 previews live - Bulk Select all / Clear and N/8 selected count retained - Export linkage unchanged: selection still flows through ExportPanel into every export format Tests focus on behavior + export linkage, not CSS internals: - one toggle per WIDGET_IDS entry, accessible names match WIDGET_LABELS - aria-checked reflects selection state - click flips selection - Select all / Clear behave correctly - count badge updates - end-to-end linkage: clicking cards → JSON export's widgets array contains exactly the toggled ids in alphabetical order - toggling a card off removes it from the next export AGENT_HANDOFF.md: new Batch 8 section at top documenting scope, disposition, validation, and next-recommended-batch candidates.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request replaces the text-only checkbox grid in the WidgetSelector with themed preview cards, providing a more visual selection interface. It introduces the WidgetPreview component for read-only mini-renderings and updates the WidgetSelector to use button-based toggles. Comprehensive tests for the new UI and export linkage have also been added. Feedback focuses on improving accessibility by hiding redundant preview content from screen readers and increasing text contrast on unselected cards, as well as enhancing type safety in the preview component.
| className={`${styles.cardBtn} ${checked ? styles.cardSelected : ''}`} | ||
| onClick={() => toggle(id)} | ||
| > | ||
| <span className={styles.cardPreview}> |
There was a problem hiding this comment.
The preview cards contain dummy text and visual elements that are redundant for screen reader users, as the button already has an explicit aria-label. To avoid cluttering the accessibility tree and prevent screen readers from reading out the internal preview content (like "Action", "Cancel", etc.), the preview container should be hidden from assistive technologies.
| <span className={styles.cardPreview}> | |
| <span className={styles.cardPreview} aria-hidden="true"> |
| font: inherit; | ||
| color: inherit; | ||
| text-align: left; | ||
| opacity: 0.55; |
There was a problem hiding this comment.
The opacity: 0.55 applied to unselected cards results in a contrast ratio of approximately 3.2:1 for the #333 text on the #fafafa background. This falls below the WCAG AA requirement of 4.5:1 for normal text, making the labels difficult to read for users with low vision. Increasing the opacity to at least 0.7 would bring the contrast ratio to approximately 4.7:1, meeting the standard.
| opacity: 0.55; | |
| opacity: 0.7; |
| widget: WidgetId | ||
| } | ||
|
|
||
| export default function WidgetPreview({ widget }: Props) { |
There was a problem hiding this comment.
The WidgetPreview component relies on an implicit return type and an exhaustive switch over the WidgetId union. If new widgets are added to the schema in the future but this component is not updated, it will silently return undefined, leading to empty preview areas in the UI. Explicitly defining the return type would improve maintainability by ensuring that TypeScript catches missing cases if the WidgetId union is expanded.
| export default function WidgetPreview({ widget }: Props) { | |
| export default function WidgetPreview({ widget }: Props): JSX.Element | null { |
Summary
Replaces the text-only checkbox grid in
WidgetSelectorwith clickable themed preview cards. Each card is a<button role=\"switch\">containing a small read-only mini-rendering of its widget (button, card, empty-state, input, kpi-tile, modal, navbar, table). Cards inherit theme tokens via CSS custom properties — swapping presets re-themes all 8 previews live.This closes the design ↔ visual confirmation ↔ export selection loop in one cohesive surface.
What's new
WidgetPreview.tsx+.module.css— pure presentational component, switches overWIDGET_IDS, returns one of 8 themed mini-snippets. No state, no handlers. Themed entirely via inherited CSS vars (--color-primary,--color-secondary,--color-background,--color-text,--font-family,--spacing-base).WidgetSelector.tsx— checkbox grid replaced with button-card grid. Public API unchanged (selection+onChangeprops). Each card is arole=\"switch\"toggle witharia-checkedandaria-labelmatchingWIDGET_LABELS[id]. BulkSelect all/ClearandN/8 selectedcount retained.WidgetSelector.module.css— obsolete.item/.itemOn/.checkbox/.labelrules removed; new.cardBtn/.cardSelected/.cardPreview/.cardLabel. Cards: dimmed (0.55 opacity) when unselected, full opacity with accent ring when selected.tests/components/WidgetSelector.test.tsx— 8 behavior + linkage tests using @testing-library/react. Focuses on behavior (one toggle per widget, accessible names, aria-checked state, click flips selection, Select-all/Clear, count updates) and export linkage (clicking cards yields JSON export with correct widgets array; toggling off removes from next export). No CSS-internal assertions.AGENT_HANDOFF.md— new Batch 8 section at top with scope, disposition, validation summary, key UX guarantees, next-recommended-batch candidates.Constraints honored
Validation
Browser verification (Vite dev :5173)
aria-checkedflips false, card dims, badge drops to7 widgets in export, JSON export drops\"table\"from widgets arrayClear→ all cards dim, badge0 widgets in export, no widgets in exportScreenshots in PR review.
Test plan
npm run lintnpm run typechecknpm run test(153/153)npm run build🤖 Generated with Claude Code