Skip to content

feat: widget preview cards as primary selector surface#19

Merged
simonsangla merged 3 commits intomainfrom
feat/widget-preview-cards
Apr 16, 2026
Merged

feat: widget preview cards as primary selector surface#19
simonsangla merged 3 commits intomainfrom
feat/widget-preview-cards

Conversation

@simonsangla
Copy link
Copy Markdown
Owner

Summary

Replaces the text-only checkbox grid in WidgetSelector with 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 over WIDGET_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 + onChange props). Each card is a role=\"switch\" toggle with aria-checked and aria-label matching WIDGET_LABELS[id]. Bulk Select all / Clear and N/8 selected count retained.

WidgetSelector.module.css — obsolete .item/.itemOn/.checkbox/.label rules 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

  • Manifest model unchanged
  • Schema unchanged
  • Persistence unchanged
  • Export core unchanged (still pure / Node-safe)
  • No new IDs, no per-widget config, no DnD, no layout, no routing
  • App.tsx untouched (already provides themed CSS-var context)
  • The 31 widget tests from PR feat: widget-builder layer (+carry-forward of T-010..T-039 batch) #18 stay untouched and passing

Validation

  • lint: 0 errors
  • typecheck: pass
  • test: 153/153 (145 prior + 8 new)
  • build: pass (vite ~97ms)

Browser verification (Vite dev :5173)

  • 8 themed preview cards render below the bulk-action row
  • Pays Basque preset → previews recolor (red primary, green secondary, cream bg, Georgia serif)
  • Click "Table" card → aria-checked flips false, card dims, badge drops to 7 widgets in export, JSON export drops \"table\" from widgets array
  • Clear → all cards dim, badge 0 widgets in export, no widgets in export

Screenshots in PR review.

Test plan

  • npm run lint
  • npm run typecheck
  • npm run test (153/153)
  • npm run build
  • Browser: theme reactivity (preset swap)
  • Browser: card toggle → export JSON updates
  • Browser: bulk Select all / Clear
  • CI green

🤖 Generated with Claude Code

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.
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
theme-forge Ready Ready Preview, Comment Apr 16, 2026 3:37pm

@simonsangla simonsangla merged commit 8894e6a into main Apr 16, 2026
3 checks passed
@simonsangla simonsangla deleted the feat/widget-preview-cards branch April 16, 2026 15:38
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
<span className={styles.cardPreview}>
<span className={styles.cardPreview} aria-hidden="true">

font: inherit;
color: inherit;
text-align: left;
opacity: 0.55;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
opacity: 0.55;
opacity: 0.7;

widget: WidgetId
}

export default function WidgetPreview({ widget }: Props) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
export default function WidgetPreview({ widget }: Props) {
export default function WidgetPreview({ widget }: Props): JSX.Element | null {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant