From 3ef2c38eb4eaa5887c3b1b33921a855c3b15a3a2 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 27 Sep 2026 01:33:37 +0200 Subject: [PATCH] feat(design): dialogs open by purpose and keep their commit row pinned (E28 S3f1) Modal takes a required initialFocus (field / close / action), a field that loads late takes the focus while it still rests on the close button, and ModalFooter portals a dialog's commit row under the scrolling body from any depth. The header and footer fades follow the body's scroll timeline, and the viewport's interactive-widget=resizes-content keeps the footer above an on-screen keyboard. --- frontend/index.html | 8 +- frontend/src/components/About.tsx | 1 + frontend/src/components/AccountPanel.tsx | 10 +- frontend/src/components/ConfirmProvider.tsx | 9 +- .../src/components/DebugRecorder.dom.test.tsx | 6 +- frontend/src/components/DebugRecorder.tsx | 8 +- frontend/src/components/DownloadDialog.tsx | 10 +- frontend/src/components/ErrorDialog.tsx | 8 +- frontend/src/components/IssueComposer.tsx | 13 +- frontend/src/components/LibraryPlayer.tsx | 2 +- .../OnboardingWizard.escape.dom.test.tsx | 2 +- .../src/components/PersonalViewsModal.tsx | 7 +- .../src/components/PlexCollectionEditor.tsx | 6 +- frontend/src/components/PlexPlaylistAdd.tsx | 6 +- frontend/src/components/ProfileEditor.tsx | 7 +- frontend/src/components/ReleaseNotes.tsx | 7 +- frontend/src/components/ShareDialog.tsx | 2 +- frontend/src/components/TagManager.tsx | 2 +- frontend/src/components/UploadDialog.tsx | 12 +- frontend/src/components/VideoEditor.tsx | 13 +- frontend/src/components/downloadAdmin.tsx | 12 +- frontend/src/components/downloadModals.tsx | 21 +- frontend/src/components/ui/CommandPalette.tsx | 9 +- frontend/src/components/ui/KeyboardHelp.tsx | 7 +- frontend/src/components/ui/Modal.dom.test.tsx | 42 ++-- .../ui/Modal.fullscreen.dom.test.tsx | 6 +- .../components/ui/Modal.purpose.dom.test.tsx | 209 ++++++++++++++++++ .../ui/Modal.strictmode.dom.test.tsx | 2 +- frontend/src/components/ui/Modal.tsx | 94 +++++++- frontend/src/components/ui/README.md | 8 +- frontend/src/components/ui/dialogFocus.ts | 15 +- frontend/src/design-lab/batchB.tsx | 12 +- frontend/src/design-lab/batchC.tsx | 6 +- frontend/src/index.css | 36 +++ 34 files changed, 505 insertions(+), 113 deletions(-) create mode 100644 frontend/src/components/ui/Modal.purpose.dom.test.tsx diff --git a/frontend/index.html b/frontend/index.html index 39d73cbf..c1e2ee23 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,13 @@ - + + diff --git a/frontend/src/components/About.tsx b/frontend/src/components/About.tsx index 7eb4078e..853ebfad 100644 --- a/frontend/src/components/About.tsx +++ b/frontend/src/components/About.tsx @@ -45,6 +45,7 @@ export default function About({ return ( {t("about.title")} diff --git a/frontend/src/components/AccountPanel.tsx b/frontend/src/components/AccountPanel.tsx index c1bb1a58..5442f1ed 100644 --- a/frontend/src/components/AccountPanel.tsx +++ b/frontend/src/components/AccountPanel.tsx @@ -105,8 +105,14 @@ export default function AccountPanel({ const a: AdminUserAccess | undefined = q.data; return ( - -
{user.email}
+ +
{user.email}
{q.isError && !a ? ( q.refetch()}> diff --git a/frontend/src/components/ConfirmProvider.tsx b/frontend/src/components/ConfirmProvider.tsx index f8fd2980..fb58a293 100644 --- a/frontend/src/components/ConfirmProvider.tsx +++ b/frontend/src/components/ConfirmProvider.tsx @@ -1,6 +1,6 @@ import { useCallback, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import Modal from "./ui/Modal"; +import Modal, { ModalFooter } from "./ui/Modal"; import { Button } from "./ui/Button"; import { ChoiceContext, type ChoiceFn, type ChoiceOptions } from "./confirmContext"; @@ -35,6 +35,7 @@ export function ConfirmProvider({ children }: { children: ReactNode }) { {children} {state && ( close(null)} maxWidth={choices.length > 1 ? "max-w-md" : "max-w-sm"} @@ -43,8 +44,8 @@ export function ConfirmProvider({ children }: { children: ReactNode }) { {/* One action: the familiar Cancel/Confirm row. Two or more: a column, because side by side they wrap raggedly and a wrapped button reads as a second, lesser group — and these are peers. Ordered least to most destructive, top to bottom. */} -
+
)} diff --git a/frontend/src/components/DebugRecorder.dom.test.tsx b/frontend/src/components/DebugRecorder.dom.test.tsx index 0da31545..0db9d6d4 100644 --- a/frontend/src/components/DebugRecorder.dom.test.tsx +++ b/frontend/src/components/DebugRecorder.dom.test.tsx @@ -143,7 +143,7 @@ function DialogToggle() { <> {open && ( - setOpen(false)}> + setOpen(false)}>

inside

)} @@ -197,7 +197,7 @@ describe("the recorder pill", () => { const onClose = vi.fn(); mount( { debug_session: session() }, - +

inside

, ); @@ -215,7 +215,7 @@ describe("the recorder pill", () => { it("Tab from the step prompt stays inside the dialog the pill sits in", () => { mount( { debug_session: session() }, - {}}> + {}}>

inside

, ); diff --git a/frontend/src/components/DebugRecorder.tsx b/frontend/src/components/DebugRecorder.tsx index 14ea0335..cc65a693 100644 --- a/frontend/src/components/DebugRecorder.tsx +++ b/frontend/src/components/DebugRecorder.tsx @@ -23,7 +23,7 @@ import { qk } from "../lib/queryKeys"; import { dockUnderToasts } from "../lib/toastDock"; import { useMe } from "../lib/useMe"; import { Button } from "./ui/Button"; -import Modal from "./ui/Modal"; +import Modal, { ModalFooter } from "./ui/Modal"; import { LoadingState, StateMessage } from "./ui/QueryState"; import SegmentedToggle from "./ui/SegmentedToggle"; import IssueComposer from "./IssueComposer"; @@ -97,7 +97,7 @@ function StartDialog() { const minutes = me.diagnostics.session_minutes; const levels: readonly DebugSessionLevel[] = ["DEBUG", "TRACE"]; return ( - +

{t("recorder.start.intro")}

@@ -122,7 +122,7 @@ function StartDialog() { {t("recorder.start.cap", { count: minutes })}

-
+ @@ -134,7 +134,7 @@ function StartDialog() { > {t("recorder.start.confirm")} -
+
); diff --git a/frontend/src/components/DownloadDialog.tsx b/frontend/src/components/DownloadDialog.tsx index bd428689..2ecf8f6f 100644 --- a/frontend/src/components/DownloadDialog.tsx +++ b/frontend/src/components/DownloadDialog.tsx @@ -2,7 +2,7 @@ import { lazy, Suspense, useState } from "react"; import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import Modal from "./ui/Modal"; +import Modal, { ModalFooter } from "./ui/Modal"; import { Button } from "./ui/Button"; import { api } from "../lib/api"; import { invalidateDownloads } from "../lib/downloads"; @@ -63,7 +63,7 @@ export default function DownloadDialog({ }; return ( - + {title &&

{title}

}