` block + its 9 icons are
+left verbatim for 6b** so the file still compiles. All 4 Slice-6a task lines in
+`tasks.md` are now `- [x]`. Cumulative change task progress: 38 → **42/71**.
+
+### Status context consumed
+
+- `applyState` reported by the status engine: **blocked** (`blockedReasons`:
+ domain specs missing/partial; legacy flat `spec.md` present without domain
+ specs). Same planning-completeness gap as slices 1–5 — **not** a safety or
+ `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 6a** scope per the parent
+ acceptance contract, which also supplied the resolved delivery path
+ (force-split, 6a-only). `design.md` §1 (mapping table), §2.3 (status→Badge
+ variant map), §5 (icon map), and §8 (6a/6b split guidance) are authoritative
+ and do not depend on the missing domain specs.
+- `artifactStore: openspec`; persisted task checkboxes updated in `tasks.md`
+ (Slice 6a: 0 → 4 `[x]`).
+
+### Completed tasks (persisted checkboxes updated)
+
+- [x] **Directory surface migrated** — `UsersPage.impl.tsx` outer `Stack` →
+ `flex flex-col gap-6`; page header `Typography` → ``/` `; the 6
+directory
+ `Alert` banners (load-error / Jellyseerr-not-configured / Jellyseerr-error /
+ no-enrichment / queue-load-error / queue-status) → shadcn `Alert` +
+ `AlertDescription` (error→`destructive`, others→`default`, matching the
+ slice-5 Alert-variant flattening); metrics `Box` grid → responsive CSS grid
+ (`grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4`); "User list" `Paper` →
+ bordered `rounded-lg border bg-card p-4` surface; toolbar `Chip`s → `Badge`
+ (`outline` for selected count; `success` when deliverable>0) and the
+ "Message selected" / "Clear selection" `Button`s → shadcn `Button`
+ (`default` / `ghost`). The user table → shadcn `Table` family
+ (`TableHeader`/`TableBody`/`TableRow`/`TableHead`/`TableCell`) inside a
+ `max-h-[660px] overflow-auto rounded-lg border` scroll surface with a
+ **sticky opaque header** (`sticky top-0 z-10 bg-card`); `Checkbox` → shadcn
+ `Checkbox`; row `Chip`s → `Badge` with status variants (see mapping below);
+ `Avatar` → shadcn `Avatar`/`AvatarImage`/`AvatarFallback`.
+- [x] **`Drawer` → `Sheet side="right"`** for the user-detail drawer; `open`/
+ `onOpenChange` wired (`onOpenChange(false)` → `setSearchParams({})`);
+ `showCloseButton={false}` + the explicit `Close` button preserved (faithful
+ to the original drawer, which had no built-in X). `buildUserDrawerModel`
+ rendering preserved verbatim (identity / activity / contact-actions /
+ permissions sections → bordered `bg-card` panels).
+- [x] **Selection-across-pagination + search/filter preserved** —
+ `selectedUserIds`, `selectedIdSet`, `toggleUserSelected`,
+ `toggleVisibleSelection`, `mergeUsersWithActivity`, the row-level text
+ matching across all 15 fields, and `resolveUserSelection` are **byte-for-byte
+ unchanged**. The selected-id set still survives paging/filtering (a row
+ checkbox toggles membership in the set; the header checkbox toggles all
+ *visible* rows' membership). The search box stays an MUI `TextField`
+ (6b-owned component) — its `onChange`/`search` state is unchanged.
+- [x] **Status → Badge variant mapping** (design §2.3): `Playing` → `success`
+ (chart-2, healthy/active), `Paused` → `warning` (chart-3), other activity →
+ `secondary` (chart-5 neutral); Jellyseerr `Linked` → `success`, `Base only` →
+ `secondary`; `Contactable Yes` → `success`, `No` → `secondary`. Toolbar
+ "deliverable" count → `success` when >0.
+
+### Compose dialog — STRUCTURE left for 6b; shared leaf components migrated in 6a
+
+**Finalization repair:** the first 6a pass left the entire compose ``
+block intact. But the acceptance contract's `mui-free-6a` criterion is strict:
+the file "may still use `@mui` ONLY for the compose-dialog subset
+(Dialog/DialogActions/DialogContent/DialogTitle + TextField + Divider +
+IconButton + the 9 icons)". That narrow set is the same list the parent's
+dispatch named as 6b-owned, so it is the authoritative 6b boundary. The intact
+compose block additionally used `Alert/Box/Button/Chip/LinearProgress/Paper/
+Stack/Tooltip/Typography/useMediaQuery` — none of which are in the allowed
+ceiling. To satisfy the contract, the compose **content's shared leaf
+components** were migrated to shadcn/Tailwind in 6a, leaving **only the named
+6b structure** on `@mui`.
+
+**What now remains `@mui/material` (exactly the contract's narrow set, all used):**
+`Dialog` (×1), `DialogActions` (×1), `DialogContent` (×1), `DialogTitle` (×1),
+`Divider` (×1, drawer divider), `IconButton` (×5: close + 4 formatting),
+`TextField` (×3: search box + subject + html body). Plus the 9
+`@mui/icons-material` icons. Verified: `grep -nE '^ +(Alert|Box|Button|Chip|
+LinearProgress|Paper|Stack|Tooltip|Typography|Avatar|Checkbox|Drawer|Table…).*@mui'`
+→ NONE.
+
+**What was migrated inside the compose content (shared leaf components):**
+`LinearProgress`→shadcn `Progress` (indeterminate via `animate-pulse`);
+`Stack`→`flex flex-col gap-4`; `Alert`→shadcn `UIAlert`/`AlertDescription`
+(error→`destructive`, others→`default`); `Box`→`` flex; `Typography`→
+semantic `
`/``; `Chip`→`Badge` (recipient/queue/attachment chips);
+`Tooltip`→shadcn `Tooltip`/`TooltipTrigger asChild`/`TooltipContent` (wrapping
+the 6b-owned MUI `IconButton`; the app's global `TooltipProvider` in `App.tsx`
+covers it); `Paper`→bordered `rounded-lg border bg-muted/40 p-4`; `Button`→
+shadcn `UiButton` (Cancel→`ghost`, Send→`default`, Add-attachment→`outline
+asChild` wrapping a `` so the hidden ` ` still fires);
+MUI `useMediaQuery`→a dependency-free local `useIsMobile()` `matchMedia` hook
+(for the Dialog `fullScreen` mobile behavior).
+
+**6b's remaining, intact, untouched work** (the named 6b structure):
+`Dialog`/`DialogTitle`/`DialogContent`/`DialogActions`→shadcn `Dialog`;
+`TextField`→`Input`/`Textarea` (+ `inputRef`→`ref` for the `insertMarkup`
+cursor logic); `Divider`→`Separator`; the 5 `IconButton`s→`Button
+variant="ghost" size="icon"`; the 9 icons→lucide. The compose behavior
+(subject + html body, 4 markup actions w/ cursor restore, `FormData`
+attachments + per-chip remove, queue polling display, `useSendUserMessage`
+send + inline success/error) is **preserved verbatim** — only the leaf
+component shells swapped; the 6b-owned `TextField`/`IconButton`/`Dialog`*
+and all state/handlers are unchanged.
+
+### Files changed (this slice)
+
+- `frontend/src/pages/UsersPage.impl.tsx` — directory surface + drawer
+ rewritten to shadcn/Tailwind; compose **content** shared leaf components
+ migrated to shadcn/Tailwind; only the named 6b structure (`Dialog*`/
+ `TextField`/`Divider`/`IconButton` + 9 icons) remains `@mui`.
+- `frontend/src/pages/__tests__/UsersPage.test.tsx` — new (6 behavioral tests).
+- `openspec/changes/web-ui-rework/tasks.md` — 4 Slice-6a checkboxes `- [ ]` → `- [x]`.
+
+No other file was edited (scope-clean: `git status --porcelain frontend/src |
+grep -vE 'pages/UsersPage.impl|__tests__'` → `scope-clean`). No `components/*`,
+no `components/ui/*` primitive, no other page, no `package.json` touched.
+
+### Tests added (6, co-located under `src/pages/__tests__/`)
+
+`UsersPage.test.tsx` mocks `useUsers`/`useActivity`/`useUserMessageQueueStatus`/
+`useSendUserMessage`/`react-router-dom` (`useSearchParams`) + stubs
+`SessionActivityPanel` + polyfills `window.matchMedia` (the local `useIsMobile`
+`matchMedia` hook). Covers: (1) directory table + metric counts render; (2)
+**selection toggle persists** (click row checkbox → "1 selected", again →
+"0 selected"); (3) **header select-all** selects all visible rows ("2
+selected"); (4) **drawer opens on row click** (`setSearchParams` called with
+`{ user: }`); (5) **status → Badge variant mapping** (Playing→
+`data-variant="success"`, Paused→`"warning"`); (6) **drawer (Sheet) renders
+`buildUserDrawerModel`** content (heading + Identity/Contact-actions sections +
+the activity panel stub) when `?user=`. (The compose dialog is not rendered
+in tests because `composeOpen` is false and `openCompose` no-ops with no
+selection — so the compose migration is covered by build/lint, not behavior
+assertions.)
+
+### Gate results (run from `frontend/`)
+
+| Command | Result | Notes |
+|---|---|---|
+| `npm run build` (`tsc -b && vite build`) | **pass (exit 0)** | 3314 modules; pre-existing >500 kB chunk warning only. |
+| `npm run lint` (`eslint .`) | **pass (exit 0)** | 0 errors. 2 pre-existing `react-hooks/exhaustive-deps` **warnings** in `UsersPage.impl.tsx` (the `baseRows` + `metrics` memos) — present since before slice 1, out of 6a scope. |
+| `npm test` (`vitest run`) | **pass (exit 0)** | **20 files / 45 tests** (+1 file, +6 tests vs slice-5 baseline of 19/39). |
+| `npm run test:node` (`node --test tests/*.test.mjs`) | **pass (exit 0)** | 4/4 legacy node suites (untouched). |
+| Runtime checks (parent) | **pass** | `sheet-usage` → `Sheet`/`SheetContent side="right"` present; `drawer-gone` → 2 substring hits, both non-component (the `buildUserDrawerModel` import + the `drawerModel` variable; the MUI `Drawer` component is gone); `scope` → `scope-clean`; `@mui/material` imports = exactly the narrow set `{Dialog,DialogActions,DialogContent,DialogTitle,Divider,IconButton,TextField}` + 9 icons (all used). |
+
+> Note on `node --test tests`: the literal bare-directory form mis-resolves
+> `tests` as a module entry on Node v22 (every slice). The working command is
+> the npm script `test:node` = `node --test tests/*.test.mjs` (matches the
+> slice-1 harness), which is green. `npm test` covers the component suite.
+
+### Deviations from design / notes
+
+1. **`Tooltip` + `LinearProgress` migrated to shadcn in the compose content**
+ (finalization repair). The task's directory-surface line lists them, but in
+ the source both were used **only inside the compose dialog** (formatting
+ toolbar + `sendUserMessage.isPending`). They were migrated to shadcn
+ `Tooltip` / `Progress` during the 6a finalization repair (above) to satisfy
+ the `mui-free-6a` narrow-import ceiling. `Progress` renders an
+ indeterminate cue via `value={100} className="animate-pulse"` (full-width
+ pulsing bar).
+2. **`Alert` variant flattening.** shadcn `Alert` exposes only `default` +
+ `destructive`; MUI `severity="error"` → `destructive`, info/warning/success
+ → `default`. All **message text is preserved verbatim**; only the
+ severity→color cue is flattened (consistent with the slice-5 deviation).
+3. **Header select-all checkbox has no indeterminate dash.** The shadcn
+ `Checkbox` indicator hardcodes a check icon (no native dash); rather than
+ hack the shared primitive or show a misleading check for partial selection,
+ the header checkbox reflects `allVisibleSelected` only. This is a minor
+ visual change — the **selection-set semantics are fully preserved** (which
+ is what the task requires); the toggle-all-visible behavior is unchanged and
+ is covered by a test.
+4. **6b-owned icons stay MUI inside shadcn shells.** `CloseIcon` (drawer close
+ - compose dialog close), `MailOutlinedIcon` ("Message selected"),
+ `SendIcon`/`AttachFileIcon`/`DeleteOutlinedIcon` (compose), and the 4
+ formatting icons stay as 6b-owned `@mui/icons-material` imports, rendered
+ inside shadcn `Button`/`IconButton`/`Badge` shells as appropriate. 6b swaps
+ them for lucide.
+5. **Search `TextField` + drawer `Divider` + compose `Dialog*`/`TextField`/
+ `IconButton` stay MUI** — exactly the contract's narrow 6b set. `TextField`
+ is kept so the `htmlBody` `inputRef` cursor logic (`insertMarkup`) is
+ byte-for-byte unchanged; 6b will move it to `Textarea` + `ref`.
+6. **`useMediaQuery` (MUI) → local `useIsMobile()`** matchMedia hook (the
+ Dialog `fullScreen` mobile prop); removes the last non-listed `@mui/material`
+ import.
+
+### Parity preserved (behavior)
+
+- Selection-across-pagination: the `selectedUserIds` set survives filtering /
+ paging; row + header toggle logic unchanged; `Clear selection` resets it.
+- Search/filter: `mergeUsersWithActivity` + the 15-field row-level text match +
+ `resolveUserSelection` unchanged.
+- Drawer open/close (`?user=` ↔ `setSearchParams({})`) + `buildUserDrawerModel`
+ identity/contact/permissions rendering unchanged.
+- All hooks (`useUsers`/`useActivity`/`useUserMessageQueueStatus`/
+ `useSendUserMessage`/`useMediaQuery`) and the compose machinery (`openCompose`,
+ `closeCompose`, `insertMarkup`, `addLink`, `handleAttachments`,
+ `removeAttachment`, `handleSend`) are **unchanged** — the compose block is
+ untouched.
+
+### Workload / PR boundary
+
+Single sub-slice (the 6a half of the force-split): the impl diff is **495
+insertions / 638 deletions (net −143)** in `UsersPage.impl.tsx` — the directory
+surface + drawer + the compose-content shared leaf components were all migrated
+this slice (the compose-content migration was required to satisfy the
+`mui-free-6a` narrow-import ceiling; see the finalization repair above). Plus
+1 new test file (~190 lines). This is comfortably the larger half of slice 6;
+the parent owns the commit/PR shape and 6b is the matching second sub-PR
+(smaller now — only the named 6b structure remains). Nothing committed here.
+
+### Top risk for 6b
+
+**The compose-dialog STRUCTURE** is the remaining MUI surface in this file,
+now narrowed to exactly the contract set: `Dialog`/`DialogTitle`/
+`DialogContent`/`DialogActions` → shadcn `Dialog`; `TextField` → `Input`/
+`Textarea` (+ `inputRef`→`ref` for the `insertMarkup` cursor logic, in both the
+subject field and the `htmlBody`); `Divider` → `Separator`; the 5 `IconButton`s
+→ `Button variant="ghost" size="icon"`; the 9 `@mui/icons-material` → lucide
+(pin verified in slice 1). The shared leaf components (`Alert`/`Box`/`Button`/
+`Chip`/`Paper`/`Stack`/`Typography`/`Tooltip`/`LinearProgress`) were **already
+migrated in 6a**, so 6b's remaining diff is the Dialog structure + inputs +
+icons only. Behavior to preserve (already intact from 6a): subject + html body,
+the 4 markup-insertion actions w/ cursor restore, `FormData` attachments +
+per-chip remove, queue polling, `useSendUserMessage` send + inline alerts.
+After 6b, the file's `@mui/material` + `@mui/icons-material` imports collapse
+to nothing (slice 8 removes the deps entirely).
+
+### Structured status note
+
+Overall change `applyState` remains **blocked** per the status engine (domain
+specs missing/partial; legacy flat `spec.md`). This is a planning-completeness
+ gap only — `design.md` supplied the authoritative mapping and
+ `actionContext` is `repo-local` with `allowedEditRoots` covering the
+ workspace. The parent explicitly delegated Slice 6a with a clear, force-split
+ delivery path, so this sub-slice proceeded under that delegation. Should be
+ resolved before `sdd-verify`/archive, per the prior slices' notes.
diff --git a/openspec/changes/web-ui-rework/tasks.md b/openspec/changes/web-ui-rework/tasks.md
index 4369c0a..f8902d2 100644
--- a/openspec/changes/web-ui-rework/tasks.md
+++ b/openspec/changes/web-ui-rework/tasks.md
@@ -192,10 +192,10 @@ Each slice section restates this gate as its final task.
### Slice 6a — directory table, selection, drawer
-- [ ] Migrate `frontend/src/pages/UsersPage.impl.tsx` directory surface: user table (`Table`/`TableBody`/`TableCell`/`TableContainer`/`TableHead`/`TableRow` + `Checkbox`/`Chip`/`Avatar`/`Tooltip` + `LinearProgress`) → shadcn `Table` family + `Checkbox` + `Badge` (status cues) + `Avatar` + `Tooltip` + `Progress`.
-- [ ] Replace MUI `Drawer` with shadcn `Sheet side="right"` for the user detail drawer; preserve `buildUserDrawerModel` rendering.
-- [ ] Preserve selection-across-pagination semantics (selected-user-id set survives paging/filtering) and the search/filter logic (`mergeUsersWithActivity`, row-level text matching).
-- [ ] Wire status → Badge variant mapping (healthy/activity = `success` cue) consistently with design §2.3.
+- [x] Migrate `frontend/src/pages/UsersPage.impl.tsx` directory surface: user table (`Table`/`TableBody`/`TableCell`/`TableContainer`/`TableHead`/`TableRow` + `Checkbox`/`Chip`/`Avatar`/`Tooltip` + `LinearProgress`) → shadcn `Table` family + `Checkbox` + `Badge` (status cues) + `Avatar` + `Tooltip` + `Progress`.
+- [x] Replace MUI `Drawer` with shadcn `Sheet side="right"` for the user detail drawer; preserve `buildUserDrawerModel` rendering.
+- [x] Preserve selection-across-pagination semantics (selected-user-id set survives paging/filtering) and the search/filter logic (`mergeUsersWithActivity`, row-level text matching).
+- [x] Wire status → Badge variant mapping (healthy/activity = `success` cue) consistently with design §2.3.
### Slice 6b — compose dialog, formatting actions, attachments