From 5fb3bb2ebc5ed7767dae67efa0f8b8e68c5c24e2 Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 2 Jul 2026 18:21:59 +0200 Subject: [PATCH] feat(web): add ChicoryTV app shell refs #82 --- .../2026-07-02-chicorytv-spa-app-shell.md | 77 ++ web/src/App.test.tsx | 72 +- web/src/App.tsx | 877 ++++++++++++------ web/src/shell.css | 751 +++++++++------ 4 files changed, 1182 insertions(+), 595 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-02-chicorytv-spa-app-shell.md diff --git a/docs/superpowers/plans/2026-07-02-chicorytv-spa-app-shell.md b/docs/superpowers/plans/2026-07-02-chicorytv-spa-app-shell.md new file mode 100644 index 000000000..fbe9c072d --- /dev/null +++ b/docs/superpowers/plans/2026-07-02-chicorytv-spa-app-shell.md @@ -0,0 +1,77 @@ +# ChicoryTV SPA App Shell 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:** Build the ChicoryTV React admin shell for Gitea issue #82: fixed sidebar, compact top bar, client-side screen slots, Connect menu, and mounted theme switcher. + +**Architecture:** Keep the shell in `web/src/App.tsx` because the current SPA is still small and the issue is about top-level chrome. Use local React state plus `history.pushState`/`popstate` for lightweight client-side routing without adding dependencies. Keep styling in the existing `web/src/shell.css` stub and reuse the ported design-system primitives. + +**Tech Stack:** React 19, TypeScript, Vite, Vitest, Testing Library, Lucide React, ChicoryTV design-system CSS tokens. + +--- + +### Task 1: Shell Behavior Tests + +**Files:** +- Modify: `web/src/App.test.tsx` + +- [ ] **Step 1: Replace scaffold expectations with shell expectations** + +Add tests that render `App`, assert the 232px sidebar/top-bar shell content is present, verify navigation moves between placeholder screens, verify the Connect menu exposes M3U/XMLTV endpoints, and keep the existing theme persistence coverage. + +- [ ] **Step 2: Run tests to verify failure** + +Run: `cd web && npm test -- --run src/App.test.tsx` + +Expected: tests fail because the current scaffold still renders "SPA foundation ready" and does not implement routed placeholder screens or the Connect menu. + +### Task 2: App Shell Implementation + +**Files:** +- Modify: `web/src/App.tsx` +- Modify: `web/src/shell.css` + +- [ ] **Step 1: Implement shell structure** + +Replace the scaffold/gallery with: +- Sidebar brand row, primary/media/system nav, health footer. +- Top bar with page title, search input, Connect menu, help/notification/avatar controls, and per-screen primary action slot. +- Theme switcher mounted in the shell. +- Placeholder content for Dashboard, Channels, New Channel, Guide, Schedules, Playouts, Collections, Libraries, and Settings. + +- [ ] **Step 2: Implement routing** + +Use a route table with paths under `/app`, `window.location.pathname`, `history.pushState`, and `popstate` to update active nav and rendered screen without a full reload. + +- [ ] **Step 3: Style to prototype** + +Rewrite `shell.css` so the sidebar is 232px, top bar is 52px, active nav uses accent rail plus soft tint, content scrolls independently, and the layout remains usable on narrow screens. + +- [ ] **Step 4: Run focused tests** + +Run: `cd web && npm test -- --run src/App.test.tsx` + +Expected: all `App` tests pass. + +### Task 3: Verification + +**Files:** +- No code changes expected. + +- [ ] **Step 1: Run web test suite** + +Run: `cd web && npm test -- --run` + +Expected: all tests pass. + +- [ ] **Step 2: Run lint** + +Run: `cd web && npm run lint` + +Expected: no lint errors. + +- [ ] **Step 3: Run production build** + +Run: `cd web && npm run build` + +Expected: TypeScript and Vite build successfully. diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 9a957c8a6..b3cfc18ed 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -11,11 +11,13 @@ import { describe('ChicoryTV SPA scaffold', () => { afterEach(() => { cleanup(); + window.history.replaceState(null, '', '/'); }); beforeEach(() => { window.localStorage.clear(); document.documentElement.removeAttribute('data-theme'); + window.history.replaceState(null, '', '/app'); vi.restoreAllMocks(); vi.spyOn(window, 'fetch').mockResolvedValue( new Response(JSON.stringify([]), { @@ -25,15 +27,62 @@ describe('ChicoryTV SPA scaffold', () => { ); }); - it('renders the placeholder shell and loads the design system stylesheet', () => { + it('renders the admin shell chrome and loads the design system stylesheet', () => { render(); - expect(screen.getByRole('heading', { name: 'ChicoryTV' })).toBeInTheDocument(); + expect(screen.getByRole('img', { name: 'ChicoryTV' })).toBeInTheDocument(); expect(screen.getByRole('navigation', { name: 'Primary' })).toBeInTheDocument(); - expect(screen.getByText('SPA foundation ready')).toBeInTheDocument(); + expect(screen.getByRole('banner')).toBeInTheDocument(); + expect(screen.getByRole('searchbox', { name: 'Search' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Connect' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Add Channel' })).toBeInTheDocument(); + expect(screen.getByText('Healthy')).toBeInTheDocument(); expect(designSystemStylesheet).toBe('../../design-system/styles.css'); }); + it('routes between shell screen slots without a page reload', () => { + render(); + + fireEvent.click(screen.getByRole('link', { name: 'Channels' })); + + expect(screen.getByRole('heading', { name: 'Channels' })).toBeInTheDocument(); + expect(screen.getByText('Channel list workspace')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Channels' })).toHaveAttribute( + 'aria-current', + 'page' + ); + expect(window.location.pathname).toBe('/app/channels'); + + fireEvent.click(screen.getByRole('link', { name: 'Schedules' })); + + expect(screen.getByRole('heading', { name: 'Schedules' })).toBeInTheDocument(); + expect(screen.getByText('Schedule editor workspace')).toBeInTheDocument(); + expect(window.location.pathname).toBe('/app/schedules'); + }); + + it('selects the active screen from the current location', () => { + window.history.replaceState(null, '', '/app/guide'); + + render(); + + expect(screen.getByRole('heading', { name: 'Guide' })).toBeInTheDocument(); + expect(screen.getByText('EPG grid workspace')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Guide' })).toHaveAttribute('aria-current', 'page'); + }); + + it('opens the Connect menu with player endpoints and enabled-only option', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Connect' })); + + expect(screen.getByRole('dialog', { name: 'Connect a player' })).toBeInTheDocument(); + expect(screen.getByText('M3U playlist')).toBeInTheDocument(); + expect(screen.getByText('XMLTV guide')).toBeInTheDocument(); + expect(screen.getByText('/iptv/channels.m3u?enabled=1')).toBeInTheDocument(); + expect(screen.getByText('/iptv/xmltv.xml?enabled=1')).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: 'Only enabled channels' })).toBeChecked(); + }); + it('applies the warm design-system default by removing the theme attribute', () => { document.documentElement.dataset.theme = 'cool'; @@ -69,21 +118,12 @@ describe('ChicoryTV SPA scaffold', () => { ); }); - it('renders the component gallery primitives', () => { + it('renders routed placeholder cards for the downstream screen work', () => { 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' - ); + expect(screen.getByText('On air now')).toBeInTheDocument(); + expect(screen.getByText('Health checks')).toBeInTheDocument(); + expect(screen.getByText('Recent activity')).toBeInTheDocument(); }); it('renders live channel data from the typed API client', async () => { diff --git a/web/src/App.tsx b/web/src/App.tsx index a2d332ec4..5ff243c29 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,20 +1,23 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState, type MouseEvent, type ReactNode } from 'react'; import { - Activity, + Bell, CalendarClock, + Cast, + Check, + ChevronDown, + CircleHelp, + ClipboardCopy, FolderTree, Info, LayoutDashboard, + LayoutGrid, 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'; @@ -23,21 +26,14 @@ import { Button, Card, ChannelLogo, - Checkbox, IconButton, - Input, NavItem, NavSection, ProgressBar, - Select, Spinner, Stat, StatusDot, - Switch, - Tabs, - Tag, - Toast, - Tooltip + Tag } from './components'; import { useChannelsQuery } from './api'; import { @@ -47,310 +43,583 @@ import { type DesignSystemThemeId } from './designSystem'; -const navItems = [ - { label: 'Dashboard', icon: