diff --git a/docs/superpowers/plans/2026-07-02-chicorytv-spa-component-primitives.md b/docs/superpowers/plans/2026-07-02-chicorytv-spa-component-primitives.md new file mode 100644 index 000000000..3a34a6a82 --- /dev/null +++ b/docs/superpowers/plans/2026-07-02-chicorytv-spa-component-primitives.md @@ -0,0 +1,135 @@ +# ChicoryTV SPA Component Primitives Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement issue #80 by recreating the design-system primitive components as typed React components in the SPA and adding a gallery view for visual review across all three themes. + +**Architecture:** Components live under `web/src/components/` and are grouped by domain with a barrel export. Visual styling lives in one shared CSS file using existing ChicoryTV design tokens imported by `web/src/designSystem.ts`; gallery composition lives in `App.tsx` and exercises every primitive. + +**Tech Stack:** React 19, TypeScript, Vite, Vitest, Testing Library, lucide-react, existing `design-system/styles.css` tokens. + +--- + +### Task 1: Baseline and Failing Component Tests + +**Files:** +- Modify: `web/src/App.test.tsx` + +- [ ] **Step 1: Run baseline tests** + +Run: `npm test -- --run` from `web/`. + +Expected: existing SPA scaffold tests pass before new work. + +- [ ] **Step 2: Write failing tests** + +Add tests that import the upcoming component barrel, render the gallery, and verify key API behavior: + +```tsx +import { Button, Checkbox, Input, ProgressBar, Switch, Tabs, Tooltip } from './components'; + +it('exports typed primitives with expected interactions', () => { + const onSwitch = vi.fn(); + const onCheckbox = vi.fn(); + const onTab = vi.fn(); + + render( + <> + + + + {}} /> + + + + + ); + + expect(screen.getByRole('button', { name: 'Saving' })).toBeDisabled(); + fireEvent.click(screen.getByRole('switch', { name: 'Show disabled' })); + expect(onSwitch).toHaveBeenCalledWith(true); + expect(screen.getByRole('checkbox', { name: 'Select all' })).toHaveAttribute('aria-checked', 'mixed'); + expect(screen.getByText('Already in use')).toBeInTheDocument(); + expect(screen.getByText('62%')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('tab', { name: 'Streaming' })); + expect(onTab).toHaveBeenCalledWith('streaming'); + fireEvent.mouseEnter(screen.getByText('Reset')); + expect(screen.getByRole('tooltip')).toHaveTextContent('Reset playout'); +}); +``` + +- [ ] **Step 3: Verify red** + +Run: `npm test -- --run App.test.tsx` from `web/`. + +Expected: failure because `./components` does not exist yet. + +### Task 2: Implement Component Library + +**Files:** +- Create: `web/src/components/index.ts` +- Create: `web/src/components/components.css` +- Create: `web/src/components/forms.tsx` +- Create: `web/src/components/dataDisplay.tsx` +- Create: `web/src/components/feedback.tsx` +- Create: `web/src/components/navigation.tsx` + +- [ ] **Step 1: Implement typed exports** + +Implement these exports with props matching `design-system/components/**/*.d.ts`: `Button`, `IconButton`, `Input`, `Select`, `Switch`, `Checkbox`, `Badge`, `StatusDot`, `Tag`, `ChannelLogo`, `Card`, `Stat`, `ProgressBar`, `Toast`, `Tooltip`, `Spinner`, `NavItem`, `NavSection`, `Tabs`. + +- [ ] **Step 2: Implement token-driven CSS** + +Use `components.css` classes for variants, sizes, focus, hover, disabled, active, live pulse, indeterminate progress, and theme-aware colors through `--ctv-*` and semantic aliases. + +- [ ] **Step 3: Verify green** + +Run: `npm test -- --run App.test.tsx` from `web/`. + +Expected: new component behavior tests pass. + +### Task 3: Add Component Gallery + +**Files:** +- Modify: `web/src/App.tsx` +- Modify: `web/src/shell.css` +- Modify: `web/src/App.test.tsx` + +- [ ] **Step 1: Add failing gallery assertions** + +Assert the SPA renders "Component gallery", "Forms", "Data display", "Feedback", and "Navigation", and includes one visible sample of each primitive family. + +- [ ] **Step 2: Verify red** + +Run: `npm test -- --run App.test.tsx` from `web/`. + +Expected: failure because the gallery is not implemented. + +- [ ] **Step 3: Implement gallery** + +Compose the new primitives in `App.tsx`, matching the specimen cards' sample content and retaining the three-theme switcher. + +- [ ] **Step 4: Verify green** + +Run: `npm test -- --run App.test.tsx` from `web/`. + +Expected: App tests pass. + +### Task 4: Final Verification and Issue Completion + +**Files:** +- Inspect all changed files with `git diff`. + +- [ ] **Step 1: Run verification** + +Run from `web/`: + +```bash +npm test -- --run +npm run typecheck +npm run build +``` + +- [ ] **Step 2: Complete done workflow** + +Use the `done` skill for issue #80: read the issue, verify acceptance criteria, check docs impact, run `git status`, commit and push if files changed, comment on Gitea, and close the issue if complete. diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 90c56329f..eb95c785f 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -1,6 +1,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { App } from './App'; +import { Button, Checkbox, Input, ProgressBar, Switch, Tabs, Tooltip } from './components'; import { applyDesignSystemTheme, designSystemStylesheet, @@ -73,4 +74,61 @@ describe('ChicoryTV SPA scaffold', () => { 'true' ); }); + + it('renders the component gallery primitives', () => { + render(); + + expect(screen.getByRole('heading', { name: 'Component gallery' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Forms' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Data display' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Feedback' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Navigation' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Add Channel' })).toBeInTheDocument(); + expect(screen.getByText('HLS Segmenter')).toBeInTheDocument(); + expect(screen.getByText('Channel saved')).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Streaming' })).toHaveAttribute( + 'aria-selected', + 'true' + ); + }); + + it('exports typed primitives with expected interactions', () => { + const onSwitch = vi.fn(); + const onCheckbox = vi.fn(); + const onTab = vi.fn(); + + render( + <> + + + + {}} /> + + + + + + + ); + + expect(screen.getByRole('button', { name: 'Saving' })).toBeDisabled(); + fireEvent.click(screen.getByRole('switch', { name: 'Show disabled' })); + expect(onSwitch).toHaveBeenCalledWith(true); + expect(screen.getByRole('checkbox', { name: 'Select all' })).toHaveAttribute( + 'aria-checked', + 'mixed' + ); + expect(screen.getByText('Already in use')).toBeInTheDocument(); + expect(screen.getByText('62%')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('tab', { name: 'Streaming' })); + expect(onTab).toHaveBeenCalledWith('streaming'); + fireEvent.mouseEnter(screen.getByText('Reset')); + expect(screen.getByRole('tooltip')).toHaveTextContent('Reset playout'); + }); }); diff --git a/web/src/App.tsx b/web/src/App.tsx index 710703e96..ac7faa48e 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,15 +1,44 @@ import { useEffect, useState } from 'react'; import { + Activity, CalendarClock, + FolderTree, + Info, LayoutDashboard, Library, ListVideo, Play, + Plus, Radio, + RefreshCw, + Search, Settings, + Tag as TagIcon, + Trash2, Tv } from 'lucide-react'; import chicoryIconUrl from '../../design-system/assets/chicorytv-icon.svg'; +import { + Badge, + Button, + Card, + ChannelLogo, + Checkbox, + IconButton, + Input, + NavItem, + NavSection, + ProgressBar, + Select, + Spinner, + Stat, + StatusDot, + Switch, + Tabs, + Tag, + Toast, + Tooltip +} from './components'; import { applyDesignSystemTheme, designSystemThemes, @@ -18,12 +47,12 @@ import { } from './designSystem'; const navItems = [ - { label: 'Dashboard', icon: LayoutDashboard, active: true }, - { label: 'Channels', icon: Tv }, - { label: 'Guide', icon: ListVideo }, - { label: 'Schedules', icon: CalendarClock }, - { label: 'Libraries', icon: Library }, - { label: 'Settings', icon: Settings } + { label: 'Dashboard', icon: