Mobile message compose + WidgetConfigDialog SheetForms (Slice 8)

Below md, both the message-compose Dialog and the WidgetConfigDialog
render inside a SheetForm instead of a centered Dialog.

Message compose (UsersPage.impl.tsx): the form body (subject, formatting
toolbar, HTML textarea, preview, attachments) is extracted into a shared
composeBody const consumed by both SheetForm (mobile) and Dialog
(desktop). SheetForm wired with title, onSave=handleSend (which already
closes on success per R4.5), onCancel=closeCompose, isPending,
saveDisabled, saveLabel='Send message'.

WidgetConfigDialog: the draftBody const is shared between branches. The
two-mode flow (list vs draft) maps to dynamic SheetForm props -- list
mode ('Dashboard widgets' / Done / Cancel both close), draft mode
('Add/Edit widget' / Save widget / Cancel=reset back to list). The
inline Back/Save buttons are hidden on mobile (!isMobile) since the
SheetForm footer provides them.

Desktop (md+) is token-identical for both components -- the
isComposeMobile (900px) fullscreen styling on compose is preserved for
the 768-900px band. The large diff (~860 lines) is dominated by
extraction/re-indentation of shared form bodies into consts; the
behavioral delta is ~80 lines.

Tests: 3 new (compose mobile send/subject, WidgetConfigDialog desktop +
mobile titles/Done). 116 tests pass; lint/build green.

Refs openspec/changes/mobile-responsive-parity/ (spec R4, tasks slice 8).
This commit is contained in:
Developer
2026-06-26 14:17:30 +00:00
parent e805c624b2
commit 7808822a55
4 changed files with 528 additions and 394 deletions
@@ -286,7 +286,7 @@ describe("UsersPage (slice 6b — compose dialog formatting actions)", () => {
});
});
describe("UsersPage (mobile card layout — slice 5)", () => {
describe("UsersPage (mobile card layout — slice 5)", () => {
beforeEach(() => {
window.matchMedia = ((query: string) => ({
matches: query.includes("768"),
@@ -342,4 +342,35 @@ describe("UsersPage (slice 6b — compose dialog formatting actions)", () => {
// drawer opens via a card-body tap, not via the checkbox.
expect(screen.queryByTestId("session-panel-stub")).not.toBeInTheDocument();
});
it("renders compose in a SheetForm below md with send button", async () => {
users = [
userFixture({
jellyfin_id: "u1",
display_name: "Alice",
email: "alice@example.com",
}),
];
render(
<TooltipProvider>
<UsersPage />
</TooltipProvider>,
);
// Select the deliverable user via the mobile card checkbox.
await userEvent.click(
screen.getByRole("checkbox", { name: /Select Alice/i }),
);
await userEvent.click(
screen.getByRole("button", { name: "Message selected" }),
);
// On mobile, compose opens in a SheetForm (not a Dialog). The SheetForm
// header carries the title and the footer carries the Send button.
expect(screen.getByText("Message selected users")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Send message" }),
).toBeInTheDocument();
expect(screen.getByLabelText("Subject")).toBeInTheDocument();
});
});