# Slice 8 Review — Message compose + WidgetConfigDialog mobile forms
**Change:** `mobile-responsive-parity` · **Slice:** 8 (R4.1, R4.2, R4.4)
**Reviewer mode:** fresh adversarial · **Date:** 2026-06-26
**Verdict: commit** (no blockers; two non-blocking suggestions)
---
## Verification commands
```
cd frontend && npm run lint → 0 errors, 2 warnings (PRE-EXISTING, confirmed via git stash)
cd frontend && npm run build → ✓ built (tsc -b + vite), 1977 modules
cd frontend && npm run test → 28 files, 116 tests passed
```
The two lint warnings (`react-hooks/exhaustive-deps` on `baseRows`/`rows` useMemo,
UsersPage.impl.tsx:150/189) exist on the committed Slice 7 tree and are unrelated
to this diff.
---
## 1. Desktop non-regression — CONFIRMED for BOTH components
### UsersPage compose (`UsersPage.impl.tsx`)
- The shared `composeBody` const (IIFE, lines ~820–985) bundles exactly the same
children the desktop `DialogContent` rendered before: `Progress` (when pending)
followed by `
`
containing the error/success alerts, queue banner, recipient badges, subject input,
formatting toolbar, body textarea, preview iframe, and attachment UI.
- Desktop branch (lines ~1029–1072) renders `` → `{composeBody}` →
`` with the same Cancel / `Send message` buttons, same `disabled`
condition (`sendUserMessage.isPending || !selectedDeliverableRows.length || !subject.trim()`),
and same `onClick={handleSend}`. Token-identical to the pre-slice output.
- **768–900px range preserved:** the desktop branch still applies
`isComposeMobile` (`useComposeViewport("(max-width: 900px)")`, line 139) as the
fullscreen className on `DialogContent`. In that band `isMobile` (768px) is false
and `isComposeMobile` (900px) is true → Dialog renders fullscreen. Unchanged.
### WidgetConfigDialog (`WidgetConfigDialog.tsx`)
- `draftBody` (lines ~284–470) is the shared const covering both the draft branch
and the list branch. Desktop path renders `{dialogTitle}`
then `{draftBody}`.
- `dialogTitle` (line ~459) reproduces the exact original ternary:
`draft ? (draft.id ? "Edit widget" : "Add widget") : "Dashboard widgets"`.
- The inline Back/Save buttons in the draft branch are gated by `{!isMobile ? (...) : null}`
(line ~332). On desktop `isMobile=false` → they render identically to before.
List branch content (sorted instance rows, reorder/enable/edit/delete actions,
Add-widget buttons, help text) is byte-for-byte the same JSX, only re-indented.
- Confirmed token-identical desktop output.
---
## 2. Compose SheetForm wiring — CONFIRMED
`UsersPage.impl.tsx` mobile branch (lines ~1006–1027):
- `title="Message selected users"` ✓
- `onSave={handleSend}` ✓ — `handleSend` (line 365) does `await mutateAsync` then
`setComposeOpen(false)` + clears state → **R4.5 close-on-success satisfied**
- `onCancel={closeCompose}` ✓ — `closeCompose` (line 317) closes + `sendUserMessage.reset()`
- `isPending={sendUserMessage.isPending}` ✓
- `saveDisabled={!selectedDeliverableRows.length || !subject.trim()}` ✓ (mirrors desktop)
- `saveLabel="Send message"` ✓
- Attachment UI (Paperclip + remove badges) is inside `composeBody`, preserved ✓
---
## 3. WidgetConfigDialog two-mode SheetForm — CONFIRMED
Mobile branch (lines ~471–488):
- `title={dialogTitle}` → "Dashboard widgets" (list) / "Add widget" | "Edit widget" (draft) ✓
- `onSave={draft ? saveDraft : () => handleClose(false)}` — list "Done" closes, draft saves ✓
- `onCancel={draft ? reset : () => handleClose(false)}` — **draft Cancel = reset (back to list, NOT close)**, list Cancel closes ✓
- `saveLabel={draft ? "Save widget" : "Done"}` ✓
- `isPending={draft ? saveWidget.isPending : false}` ✓
- `onOpenChange={(next) => { if (!next) handleClose(next); }}` ✓
- List↔draft↔save flow intact: `startAddBuiltIn`/`startAddService`/`startEdit` set
`draft` → footer/title reactive-swap to draft mode; `saveDraft` mutates then
`reset()` returns to list (sheet stays open); `reset` returns to list without closing ✓
- The "both close in list mode" redundancy (Done + Cancel both call `handleClose(false)`)
is functional and matches the documented intent ✓
---
## 4. Rules of Hooks — CONFIRMED clean
**WidgetConfigDialog:** all hooks (`useWidgetInstances`, `useServiceInstances`,
`useTasks`, `useSaveWidgetInstance`, `useDeleteWidgetInstance`, `useState`,
`useMemo`, `useIsMobile`) are called unconditionally at the top of the component
before the `if (isMobile) return …` early return. `useIsMobile()` is
placed after `draftBinding` (a plain derived value, not a hook) — no ordering
violation. ESLint `react-hooks/rules-of-hooks` produced **0 errors**.
**UsersPage:** the compose branch uses an IIFE `{(() => { … })()}` that declares
`composeBody` as a JSX const (no hooks, no state) and returns either ``
or `