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

333 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
- [x] 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.
- [x] Installed `@tanstack/react-table` (`^8.21.3`, dependency).
- [x] 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`).
- [x] Deleted `frontend/src/theme.ts` (no-op `getAppTheme` shim). Grep confirmed the
only reference was the file itself; no source file imported it.
- [x] 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.
- [x] 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`.**
- [x] 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).
- [x] Created `frontend/src/test/setup.ts` registering `@testing-library/jest-dom`
matchers (via the `/vitest` entry for runtime + TS augmentation).
- [x] 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`.
- [x] Added npm scripts: `"test": "vitest run"`, `"test:watch": "vitest"`,
`"test:node": "node --test tests/*.test.mjs"` (see Deviations).
- [x] Added one trivial passing component test: `src/components/ui/__tests__/badge.test.tsx`
(asserts a `success` Badge renders with the `chart-2` cue).
- [x] 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`).
- [x] **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 build` → **PASS** (`tsc -b` + `vite build`).
- `cd frontend && npm run lint` → **PASS** (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 test` → **PASS** (Vitest: 1 file, 1 test passed).
- `cd frontend && npm run test:node` → **PASS** (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.tsx` → `Card` 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.tsx` → `Card`/`CardContent` on the design ramp: label `text-sm`,
value `text-lg font-semibold`, subtext `text-xs text-muted-foreground`.
- `DiskSpaceCard.tsx` → `Card` + 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.tsx` → `Button 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` (`error`→`destructive`
variant) and `confirmVariant` (`outlined`→`outline`, `text`→`ghost`, 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.tsx` → `Card`/`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: `playing`→
`success`/chart-2, `paused`→`warning`/chart-3, idle/other→`secondary`) +
`Button` for the action. Row-click + action-button callbacks, status summary
row, and transcoding formatting preserved.
- `NowPlaying.tsx` → **already 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 slice** — `git 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 build` → **PASS** (`tsc -b` + `vite build`).
- `cd frontend && npm run lint` → **PASS** (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 test` → **PASS** (Vitest: **12 files, 22 tests** passed;
11 new component tests + the slice-1 Badge smoke test).
- `cd frontend && npm run test:node` → **PASS** (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 (`error`→`destructive`, `outlined`→`outline`, `text`→`ghost`). 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; `paused`→`warning`;
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.