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.
This commit is contained in:
@@ -168,3 +168,165 @@ unblocked now that all primitives, TanStack Table, the Vitest harness, the `succ
|
||||
- `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 4–5 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 3–8, 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 3–8 for the verbatim unchecked list.
|
||||
|
||||
Reference in New Issue
Block a user