Files
manage/openspec/changes/web-ui-rework/apply-progress.md
T
Developer 109e74db41 feat(frontend): slice 2 — migrate 11 shared components to shadcn/Tailwind
Web UI rework. Shared-components slice (drift prevention):
- Migrate SectionCard, SelectionRailCard, TabbedCard, MetricCard,
  DiskSpaceCard, HoverEditButton, DialogFooter, ConfirmDialog,
  LibraryOverview, NowPlaying, SessionActivityPanel off @mui
- HoverEditButton: MUI IconButton + EditOutlined -> Button + lucide Pencil
- Status mapping uses the success Badge variant (chart-2) for healthy
- Exported APIs preserved so consuming pages still compile (no page edits)
- 11 behavioral Vitest component tests added

Gate: build + lint + test green.
2026-06-17 12:33:47 +00:00

20 KiB
Raw Blame History

Apply Progress — web-ui-rework

Cumulative apply log. Updated by sdd-apply after each slice/sub-slice. Strict TDD is not active for this change (no strict-tdd directive in openspec/config.yaml); this run used standard mode against design.md / tasks.md.

Status context consumed

  • schemaName: gentle-pi.sdd-status, changeName: web-ui-rework.
  • applyState reported by the status engine: blocked (blockedReasons: domain specs missing/partial; legacy flat spec.md present without domain specs). This is a planning-completeness gap, not a safety/actionContext blocker.
  • actionContext: mode: repo-local, workspaceRoot: /home/user/Manage_01, allowedEditRoots: ["/home/user/Manage_01"], warnings: [] — safe.
  • This run executed the explicitly delegated Slice 1 (Foundation) scope per the parent acceptance contract. The foundation work is fully specified in tasks.md (Slice 1) and does not depend on the missing domain specs, so it proceeds under the parent's explicit delegation. The instructions.apply line itself says "Implement only unchecked tasks from the tasks artifact."
  • artifactStore: openspec; persisted task checkboxes updated in tasks.md (13 → [x]).

Slice 1 — Foundation (primitives, TanStack Table, Vitest, token/Badge cleanup) — COMPLETE

All 13 Slice-1 tasks in tasks.md are marked - [x]. Summary below.

Completed tasks (persisted checkboxes updated)

  • Added the missing shadcn primitives (13): tabs table dialog input label checkbox switch progress separator avatar textarea dropdown-menu scroll-area via npx shadcn@latest add …. The 9 already-present primitives (button tooltip sheet card badge alert select skeleton collapsible) were not re-added.
  • Installed @tanstack/react-table (^8.21.3, dependency).
  • Removed orphaned charting deps: recharts, d3, and the companion @types/d3 from package.json (zero from "recharts" / from "d3" / from "d3-*" imports existed in frontend/src).
  • Deleted frontend/src/theme.ts (no-op getAppTheme shim). Grep confirmed the only reference was the file itself; no source file imported it.
  • Added the success Badge variant (chart-2) and the warning variant (chart-3) to frontend/src/components/ui/badge.tsx, mirroring the destructive soft-cue pattern exactly per design §2.3.
  • Documented the chart-1..5 role mapping as an inline comment block above the tokens in both the @theme block and .dark block of frontend/src/index.css (chart-1=info/brand, chart-2=success, chart-3=warning, chart-4=destructive, chart-5=neutral-accent). No token value changed; no token dropped. Primary #4f8cff.
  • Created frontend/vitest.config.ts (separate from vite.config.ts): vitest/config defineConfig, @vitejs/plugin-react, environment: "jsdom", globals: true, setupFiles: ["./src/test/setup.ts"], the @ path alias from tsconfig.app.json, and include: ["src/**/*.{test,spec}.{ts,tsx}"] (does NOT claim the frontend/tests/*.mjs node suites).
  • Created frontend/src/test/setup.ts registering @testing-library/jest-dom matchers (via the /vitest entry for runtime + TS augmentation).
  • Installed dev deps: vitest@^4.1.9, @testing-library/react@^16.3.2, @testing-library/jest-dom@^6.9.1, @testing-library/user-event@^14.6.1, jsdom@^29.1.1.
  • Added npm scripts: "test": "vitest run", "test:watch": "vitest", "test:node": "node --test tests/*.test.mjs" (see Deviations).
  • Added one trivial passing component test: src/components/ui/__tests__/badge.test.tsx (asserts a success Badge renders with the chart-2 cue).
  • Verified lucide-react 1.14.0 exports every name used by later slices: Pencil, X, Paperclip, Bold, Italic, Link, List, Mail, Send, Trash2, DatabaseBackup, ExternalLink. DatabaseBackup IS exported — no fallback needed (slice-3 nav will use DatabaseBackup).
  • Exit gate green: npm run build + npm run lint + npm test + npm run test:node all pass; no theme/getAppTheme imports remain.

Files changed (tracked)

Modified:

  • frontend/package.json (added @tanstack/react-table, vitest/testing-library/jsdom dev deps, test scripts; removed recharts, d3, @types/d3)
  • frontend/package-lock.json (regenerated by npm)
  • frontend/src/components/ui/badge.tsx (added success + warning variants)
  • frontend/src/components/ui/tabs.tsx (vendored primitive; un-exported tabsListVariants to satisfy react-refresh/only-export-components — see Deviations)
  • frontend/src/index.css (chart-1..5 role-mapping comment blocks in @theme + .dark)

Deleted:

  • frontend/src/theme.ts

Added (new):

  • frontend/vitest.config.ts
  • frontend/src/test/setup.ts
  • frontend/src/components/ui/__tests__/badge.test.tsx
  • 13 new shadcn primitives under frontend/src/components/ui/: tabs.tsx, table.tsx, dialog.tsx, input.tsx, label.tsx, checkbox.tsx, switch.tsx, progress.tsx, separator.tsx, avatar.tsx, textarea.tsx, dropdown-menu.tsx, scroll-area.tsx

Untouched (no-unintended-edits respected): the 22 MUI page/component files in Slice 1's scope (all frontend/src/pages/* and frontend/src/components/*.tsx outside components/ui/) — git status --porcelain frontend/src/pages 'frontend/src/components/*.tsx' | grep -v 'components/ui/' returns nothing.

Also touched (gitignored generated docs, not tracked changes): frontend/src/.pi-map.md and frontend/src/.pi-map.index.md were refreshed to remove the now-deleted theme.ts / getAppTheme references (the project_map_patch tool is unavailable in this session, so the stale generated maps were minimally reconciled to match source).

Commands run (validation)

  • npx shadcn@latest add tabs table dialog input label checkbox switch progress separator avatar textarea dropdown-menu scroll-area --yes → created 14 files (13 primitives + a duplicate button written to a stray @/ path; stray dir removed, dup button discarded).
  • npm i @tanstack/react-table → ok.
  • npm i -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom → ok.
  • npm uninstall recharts d3 @types/d3 → ok.
  • cd frontend && npm run buildPASS (tsc -b + vite build).
  • cd frontend && npm run lintPASS (0 errors, 0 errors; 2 pre-existing react-hooks/exhaustive-deps warnings in UsersPage.impl.tsx that are out of Slice-1 scope).
  • cd frontend && npm testPASS (Vitest: 1 file, 1 test passed).
  • cd frontend && npm run test:nodePASS (legacy node:test suites green).

Deviations from design / task text (documented)

  1. tabs.tsx variant export. The vendored shadcn tabs.tsx exported tabsListVariants (a cva function) alongside components, which violates react-refresh/only-export-components and broke npm run lint. The project's own convention (button.tsx, select.tsx) is to not export the cva variant helper, so tabsListVariants was removed from the export { … } statement (the const is retained for internal use by TabsList). This keeps the component API intact for all later slices (TabbedCard uses <TabsList variant=…>, not the helper).
  2. test:node script form. The task text specifies "test:node": "node --test tests", but on Node v22.22.2 a bare directory argument is resolved as a CommonJS module entry point (Error: Cannot find module '…/tests'). The functional equivalent "test:node": "node --test tests/*.test.mjs" is used instead, which targets exactly the legacy node:test suites (users.test.mjs, userState.test.mjs) and passes. node --test auto-discovery also passes; the glob form is the explicit, minimal choice.
  3. shadcn CLI path artifact. The CLI resolved the @/ alias to a literal frontend/@/ directory. All 13 primitives were moved to frontend/src/components/ui/ and the stray frontend/@/ tree was deleted; the duplicate generated button.tsx was discarded to keep the existing tracked button.tsx unchanged.
  4. Stale generated .pi-map.md files referenced the deleted theme.ts. Since the project_map_patch tool is unavailable this session, they were minimally reconciled to match source (these are gitignored generated docs, not tracked changes).

Slice boundary / PR

  • Slice 1 is the foundation slice (~vendored primitives dominate the line count). Per the Review Workload Forecast this slice is "at/over" 400 lines and may be taken as a vendored-primitives size exception or sub-split 1a/1b. The majority of the diff is generated shadcn primitives (mechanical, vendored). No delivery decision was provided in this run's delegation (the parent delegated a single foundation slice); this run implemented the whole foundation in one slice. The parent should confirm the PR shape (1a/1b split vs. generated-code size exception) when opening the PR.

Remaining tasks (Slices 28, 58 unchecked)

Slice 1 is the only completed slice. The remaining unchecked work (Slices 28) is listed verbatim in tasks.md. The first unchecked items are (Slice 2 — Shared components):

  • Migrate frontend/src/components/SectionCard.tsx
  • Migrate frontend/src/components/SelectionRailCard.tsx
  • … (see tasks.md Slice 2 → Slice 8)

Dependency order is fixed (DAG in tasks.md): 1 → 2 → {3,4,5,6}; 4 ← 3; {3,4,5,6} → 7; {1..7} → 8. Next recommended: Slice 2 (Shared components) — it is unblocked now that all primitives, TanStack Table, the Vitest harness, the success/ warning Badge variants, and the cleared theme.ts are in place.

Risks / notes for the parent

  • The overall change applyState is blocked on missing domain specs (legacy flat spec.md present without domain specs). This does not block Slice 1 (done) but should be resolved before sdd-verify/archive. Recommended: convert/supplement spec.md into domain specs, or have the orchestrator confirm the flat spec is acceptable.
  • The two react-hooks/exhaustive-deps warnings in UsersPage.impl.tsx are pre-existing and out of Slice-1 scope (lint still passes — warnings, not errors).
  • npm audit reports 8 vulnerabilities in the dependency tree (pre-existing across the MUI/emotion/react stack); not introduced by this slice and out of scope.
  • DatabaseBackup from lucide-react is available — Slice 3 nav should use it directly.

Slice 2 — Shared components (lock the building-block language) — COMPLETE

All 12 Slice-2 task lines in tasks.md are now - [x]. The 11 shared components are MUI-free and the exported APIs are preserved so every consuming page still compiles unchanged (no frontend/src/pages/* file was edited this slice).

Status context consumed

  • applyState reported by the status engine: blocked (blockedReasons: domain specs missing/partial; legacy flat spec.md present without domain specs). This is a planning-completeness gap, not a safety/actionContext blocker. actionContext: mode: repo-local, allowedEditRoots: ["/home/user/Manage_01"], warnings: [] — safe.
  • This run executed the explicitly delegated Slice 2 (Shared components) scope per the parent acceptance contract. Slice-2 work is fully specified in tasks.md + design.md (§1 mapping table, §2 typography ramp, §2.3 success Badge variant) and does not depend on the missing domain specs. The instructions.apply line says "Implement only unchecked tasks from the tasks artifact." → proceeded under the parent's explicit slice delegation.
  • artifactStore: openspec; persisted task checkboxes updated in tasks.md (Slice 2: 0 → 12 [x]). Cumulative change task progress: 13 → 25/71 complete.

What was migrated (10 files rewritten; 1 already-clean)

  • SectionCard.tsx → shadcn Card/CardContent; gap-4 comfortable density; section title text-base font-semibold, description text-sm text-muted-foreground.
  • SelectionRailCard.tsxCard with titled header / scrollable body / footer. Preserved minHeight (inline style), footer, scrollable body. Legacy contentSx/bodySx MUI-sx props retained in the interface as no-ops so Actions/Settings compile unchanged.
  • TabbedCard.tsx → shadcn Tabs (TabsList variant="line") on a Card. value/onChange stay string-typed (controlled); tabs stays ReactElement[]; legacy contentSx/tabsSx retained as no-op props.
  • MetricCard.tsxCard/CardContent on the design ramp: label text-sm, value text-lg font-semibold, subtext text-xs text-muted-foreground.
  • DiskSpaceCard.tsxCard + CSS grid (grid-cols-1 sm:grid-cols-3 gap-4)
    • shadcn Progress; used/free/total/percent breakdown preserved; the progress color cue (chart-2 success / chart-3 warning / destructive) is emitted via full static [&_[data-slot=progress-indicator]]:bg-* class strings (verified present in the built CSS).
  • HoverEditButton.tsxButton variant="ghost" size="icon-sm" + lucide Pencil. Kept the rail-edit class + opacity-0/transition-opacity base so the existing hover-reveal rules in Actions/Settings (&:hover .rail-edit { opacity: 1 }) still target it until those pages migrate (slices 5). MUI IconButton+ EditOutlined removed.
  • DialogFooter.tsx → horizontal Button row (flex flex-row items-center gap-2). All legacy props preserved: confirmColor (errordestructive variant) and confirmVariant (outlinedoutline, textghost, else default) are mapped internally onto shadcn variants; confirmBusyLabel, confirmDisabled, confirmStartIcon, secondaryAction, cancelLabel all honored.
  • ConfirmDialog.tsx → shadcn Dialog family (DialogContent/DialogHeader/ DialogTitle/DialogDescription) reusing the migrated DialogFooter. Esc / overlay click routes to onCancel via onOpenChange. Same exported props.
  • LibraryOverview.tsxCard/CardContent on a responsive CSS grid (grid grid-cols-1 md:grid-cols-2 gap-4); movie/TV counts render verbatim.
  • SessionActivityPanel.tsx → shadcn Table family on a bordered rounded scrollable surface + Badge (status variant map per design §2.3: playingsuccess/chart-2, pausedwarning/chart-3, idle/other→secondary) + Button for the action. Row-click + action-button callbacks, status summary row, and transcoding formatting preserved.
  • NowPlaying.tsxalready MUI-free (it only imports ../types + ./SessionActivityPanel); left unchanged. Its empty-state message contract ("No recent user activity sessions right now.") is exercised by a new test.

Component tests added (11 files, co-located under src/components/__tests__/)

  • MetricCard (label/value/subtext + subtext-omitted), DiskSpaceCard (used/ free/total + percent headline), HoverEditButton (onClick fires, custom label), DialogFooter (cancel/confirm callbacks, busy label, error→destructive, secondary action), ConfirmDialog (title/message render + confirm/cancel, closed renders nothing), SectionCard (title/description/action/children), SelectionRailCard (title/body/footer + minHeight applied), TabbedCard (renders triggers + reports selection change), LibraryOverview (movie/TV cards + counts), SessionActivityPanel (status→Badge variant mapping playing/paused/idle, empty-state, row-click + action-button callbacks), NowPlaying (dashboard empty-state message contract).

Files changed (tracked)

Modified (10 components):

  • frontend/src/components/{SectionCard,SelectionRailCard,TabbedCard,MetricCard, DiskSpaceCard,HoverEditButton,DialogFooter,ConfirmDialog,LibraryOverview, SessionActivityPanel}.tsx
  • openspec/changes/web-ui-rework/tasks.md (Slice 2 checkboxes 0 → 12 [x])

Added (new, 11 test files):

  • frontend/src/components/__tests__/{MetricCard,DiskSpaceCard,HoverEditButton, DialogFooter,ConfirmDialog,SectionCard,SelectionRailCard,TabbedCard, LibraryOverview,SessionActivityPanel,NowPlaying}.test.tsx

Untouched (no-unintended-edits respected): no frontend/src/pages/* file edited this slicegit status --porcelain frontend/src/pages is empty. NowPlaying.tsx is unchanged (already MUI-free). No components/ui/* primitive was modified.

Commands run (validation) — all green

  • grep -rlE '@mui/(material|icons-material)' <11 files>ALL 11 MUI-FREE.
  • cd frontend && npm run buildPASS (tsc -b + vite build).
  • cd frontend && npm run lintPASS (0 errors; the only 2 items are the pre-existing react-hooks/exhaustive-deps warnings in UsersPage.impl.tsx, out of Slice-2 scope).
  • cd frontend && npm testPASS (Vitest: 12 files, 22 tests passed; 11 new component tests + the slice-1 Badge smoke test).
  • cd frontend && npm run test:nodePASS (legacy node:test: 4 tests, 0 fail).
  • Verified Tailwind emitted the DiskSpaceCard [data-slot=progress-indicator] chart-2/3/destructive utilities into the built CSS (arbitrary-variant classes written as full static strings).

API-preserving compromises (design constraints accepted to keep pages compiling)

  1. SelectionRailCard / TabbedCard legacy *Sx props. Kept contentSx/bodySx/tabsSx in the prop interfaces as documented no-ops (MUI sx objects have no Tailwind equivalent). They are intentionally not destructured into locals (avoids no-unused-vars) and are ignored at render. Consumers (Actions, Settings) pass them today and compile unchanged.
  2. DialogFooter color/variant mapping. confirmColor/confirmVariant are MUI-only concepts; they are retained on the API and mapped to shadcn Button variants (errordestructive, outlinedoutline, textghost). All current consumers pass at most confirmColor="error" and secondaryAction, which map cleanly. No consumer passes confirmVariant explicitly today.
  3. HoverEditButton hover reveal. Implemented with a Tailwind opacity-0/ transition-opacity base (not inline style) so the not-yet-migrated pages' MUI-sx &:hover .rail-edit { opacity: 1 } rules still override it on hover (higher specificity) during the interim. The rail-edit class is preserved.
  4. SessionActivityPanel status→Badge mapping. playing (active/healthy) → success per the task's "healthy=success" + design §2.3; pausedwarning; idle/unknown→secondary. This is the documented cue map (no app semantics changed — the prior MUI Chip used primary/warning/default coloring).

Carry-over risk / top risk for slice 3 (and interim)

  • TabbedCard interim tab-bar rendering. Until Applications (slice 4) and Settings/Actions (slice 5) migrate, those pages still pass MUI <Tab> elements as the tabs prop, which now render inside a shadcn <TabsList>. MUI <Tab> outside an MUI <Tabs> does not throw (renders with a console warning) and the page content still swaps via the page's external value state, but the tab "active" highlight is cosmetic-only until those pages migrate. Build / lint / test are unaffected. This is an expected interim state of the chained model and resolves fully once slices 45 land. (Pages are intentionally not edited in slice 2.)
  • Slice-2 PR budget. Review churn ≈ 775 lines on the 10 rewritten components (303 ins / 472 del; mostly MUI teardown) + 335 lines of new tests ≈ ~1,110 changed lines, over the ≤400 budget. The forecast authorizes sub-split 2a (cards/buttons/dialogs) → 2b (tables/panels) on overrun. The parent delegated the whole slice as one unit and owns the commit/PR, so this run delivered it in one piece; the parent may split 2a/2b at PR time or take a size exception (mirroring the slice-1 precedent). Functionally the slice is complete and all gates are green.
  • Overall change applyState remains blocked on missing domain specs (legacy flat spec.md); does not block Slice 2 (done) but should be resolved before sdd-verify/archive.

Remaining tasks (Slices 38, 46 unchecked)

Slice 2 is complete (25/71 tasks). Next in dependency order: Slice 3 — Backups cluster + nav/IA (uses slice-2 Table/Badge/Tabs/cards and lands the /backups nav item + /applications/media redirect; DatabaseBackup icon confirmed available). See tasks.md Slices 38 for the verbatim unchecked list.