Skip to content

Commit af53d4c

Browse files
feat(web): Settings opens as a dialog with its left nav, no breadcrumbs (CL-9672) (#1077)
1 parent 75e5075 commit af53d4c

5 files changed

Lines changed: 131 additions & 48 deletions

File tree

‎apps/web/src/app.tsx‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Button, EmptyState } from "@corbits/react-ui";
66
import { WorkbenchLoadingState } from "@/chat";
77
import { QueryClientProvider } from "@tanstack/react-query";
88
import { BoldIconProvider, WarningCircle } from "@/lib/icons";
9-
import { useMemo } from "react";
9+
import { useMemo, useState } from "react";
1010

1111
import { AuthScreen } from "./auth-screen";
1212
import { BenchProvider } from "./bench-context";
@@ -18,7 +18,8 @@ import { OnboardingPage } from "./pages/onboarding-page";
1818
import { ProvisioningErrorPage } from "./pages/provisioning-error-page";
1919
import { createAppQueryClient } from "./query-client";
2020
import { Redirect } from "./redirect";
21-
import { APP_ROUTES, LOGIN_PATH, matchesRoute, ONBOARDING_PATH } from "./routes";
21+
import { SettingsDialog } from "./pages/settings-dialog";
22+
import { APP_ROUTES, LOGIN_PATH, matchesRoute, ONBOARDING_PATH, SETTINGS_PATH } from "./routes";
2223
import type { SessionState, SessionUser } from "./session";
2324
import { AppShell } from "./shell/app-shell";
2425
import { ComposerInsertionProvider } from "./shell/composer-insertion";
@@ -62,17 +63,30 @@ function Shell({
6263
// cache; wired to `onSignOut` so a session going invalid anywhere routes
6364
// the whole shell back to login, not just one stuck panel.
6465
const queryClient = useMemo(() => createAppQueryClient(onSignOut), [onSignOut]);
65-
const route = APP_ROUTES.find((candidate) => matchesRoute(candidate.path, path));
66+
// Settings is a dialog over the last page, so that page stays mounted
67+
// behind it; a cold load of /settings falls back to /new.
68+
const settingsOpen = matchesRoute(SETTINGS_PATH, path);
69+
const [backdropPath, setBackdropPath] = useState(settingsOpen ? "/new" : path);
70+
if (!settingsOpen && backdropPath !== path) setBackdropPath(path);
71+
const pagePath = settingsOpen ? backdropPath : path;
72+
const route = APP_ROUTES.find((candidate) => matchesRoute(candidate.path, pagePath));
6673
return (
6774
<QueryClientProvider client={queryClient}>
6875
<NavigationProvider navigate={navigate} onSignOut={onSignOut} user={user}>
6976
<BenchProvider>
7077
<ComposerInsertionProvider>
7178
<ShellChromeProvider path={path} navigate={navigate}>
7279
<CommandPaletteProvider path={path} navigate={navigate}>
73-
<AppShell path={path} user={user} onSignOut={onSignOut}>
74-
{route === undefined ? <NotFoundPage /> : route.render(path, navigate)}
80+
<AppShell path={pagePath} user={user} onSignOut={onSignOut}>
81+
{route === undefined ? <NotFoundPage /> : route.render(pagePath, navigate)}
7582
</AppShell>
83+
{settingsOpen ? (
84+
<SettingsDialog
85+
path={path}
86+
navigate={navigate}
87+
onClose={() => navigate(backdropPath)}
88+
/>
89+
) : null}
7690
</CommandPaletteProvider>
7791
</ShellChromeProvider>
7892
</ComposerInsertionProvider>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/* Settings as a dialog: raised card, left section nav + one section body. */
2+
[data-slot="dialog-content"].settings-dialog {
3+
display: flex;
4+
flex-direction: column;
5+
gap: 0;
6+
width: min(calc(100vw - 2rem), 960px);
7+
max-width: none;
8+
height: 80vh;
9+
max-height: none;
10+
padding: 0;
11+
border: 0;
12+
border-radius: 12px;
13+
background: var(--card, var(--popover));
14+
box-shadow: var(--raised);
15+
}
16+
17+
.settings-dialog-head {
18+
padding: 14px 56px 12px 20px;
19+
border-bottom: 1px solid var(--border);
20+
}
21+
22+
.settings-dialog-title {
23+
font-size: 16px;
24+
font-weight: 800;
25+
letter-spacing: -0.01em;
26+
}
27+
28+
.settings-dialog-layout {
29+
flex: 1;
30+
min-height: 0;
31+
}
32+
33+
.settings-dialog-body {
34+
flex: 1;
35+
min-width: 0;
36+
padding: 20px;
37+
}
38+
39+
@media (max-width: 640px) {
40+
[data-slot="dialog-content"].settings-dialog {
41+
width: 100vw;
42+
height: 100dvh;
43+
border-radius: 0;
44+
top: 0;
45+
left: 0;
46+
translate: none;
47+
max-width: none;
48+
}
49+
50+
.settings-dialog-layout {
51+
flex-direction: column;
52+
}
53+
54+
.settings-dialog-layout .settings-nav {
55+
width: 100%;
56+
flex-direction: row;
57+
flex-shrink: 0;
58+
gap: 0.5rem;
59+
padding: 0.5rem;
60+
border-right: 0;
61+
border-bottom: 1px solid var(--border);
62+
overflow: auto hidden;
63+
}
64+
65+
.settings-dialog-layout .settings-nav-group {
66+
flex-direction: row;
67+
flex-shrink: 0;
68+
gap: 0.25rem;
69+
}
70+
71+
.settings-dialog-layout .settings-nav [data-slot="sidebar-item-row"] {
72+
flex-shrink: 0;
73+
white-space: nowrap;
74+
}
75+
76+
.settings-dialog-layout .settings-nav-heading,
77+
.settings-dialog-layout .settings-nav-advanced {
78+
display: none;
79+
}
80+
}
Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
// Thin mount: this file only adapts the app's bench-selection state and
2-
// the URL into the shape `@/settings`'s shell expects.
1+
// Thin mount: adapts the bench-selection state and the URL into the shape
2+
// `@/settings`'s shell expects, and presents it over whatever page is behind.
33

44
import { flattenSettingsSections, resolveActiveSection, SettingsShell } from "@/settings";
5-
import { PageShell } from "@corbits/react-ui";
5+
import {
6+
Dialog,
7+
DialogBody,
8+
DialogContent,
9+
DialogHeader,
10+
DialogTitle,
11+
} from "@corbits/react-ui/ui/dialog";
612

713
import { useBench } from "../bench-context";
814
import { useSignOut } from "../navigation";
@@ -14,15 +20,17 @@ import {
1420
} from "../path-ids";
1521
import { resolveAppSettingsSectionGroups } from "../settings-groups";
1622
import { SettingsNav } from "./settings-nav";
17-
import { StageTopBar } from "../shell/stage-top-bar";
23+
import "./settings-dialog.css";
1824
import { useSettingsAccess } from "../settings-access";
1925

20-
export function SettingsRoute({
26+
export function SettingsDialog({
2127
path,
2228
navigate,
29+
onClose,
2330
}: {
2431
readonly path: string;
2532
readonly navigate: (to: string) => void;
33+
readonly onClose: () => void;
2634
}) {
2735
const { selectedTenantId, selectedPrincipalId } = useBench();
2836
const onSignOut = useSignOut();
@@ -58,16 +66,19 @@ export function SettingsRoute({
5866
}
5967

6068
return (
61-
<div className="flex h-full min-h-0 flex-col">
62-
<StageTopBar crumbs={[{ label: "Settings" }]} />
63-
<div className="settings-page-head">
64-
<h1>Settings</h1>
65-
<p>Personal and shared, side by side.</p>
66-
</div>
67-
<div className="settings-layout min-h-0 flex-1">
68-
<SettingsNav path={path} onNavigate={navigate} />
69-
<div className="min-h-0 flex-1 overflow-y-auto">
70-
<PageShell width="full" className="page-fill">
69+
<Dialog
70+
open
71+
onOpenChange={(next) => {
72+
if (!next) onClose();
73+
}}
74+
>
75+
<DialogContent className="settings-dialog" aria-describedby={undefined}>
76+
<DialogHeader className="settings-dialog-head">
77+
<DialogTitle className="settings-dialog-title">Settings</DialogTitle>
78+
</DialogHeader>
79+
<div className="settings-layout settings-dialog-layout">
80+
<SettingsNav path={path} onNavigate={navigate} />
81+
<DialogBody className="settings-dialog-body">
7182
<SettingsShell
7283
sections={sections}
7384
activeId={activeSection?.id ?? null}
@@ -79,9 +90,9 @@ export function SettingsRoute({
7990
...(onSignOut !== undefined ? { onSignOut } : {}),
8091
}}
8192
/>
82-
</PageShell>
93+
</DialogBody>
8394
</div>
84-
</div>
85-
</div>
95+
</DialogContent>
96+
</Dialog>
8697
);
8798
}

‎apps/web/src/routes.tsx‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@ const ToolsRoute = lazy(async () => ({
5959
const InsightsRoute = lazy(async () => ({
6060
default: (await import("./pages/insights-page")).InsightsRoute,
6161
}));
62-
const SettingsRoute = lazy(async () => ({
63-
default: (await import("./pages/settings-page")).SettingsRoute,
64-
}));
6562
const SkillDetailRoute = lazy(async () => ({
6663
default: (await import("./pages/skill-detail-page")).SkillDetailRoute,
6764
}));
@@ -259,12 +256,12 @@ export const APP_ROUTES: readonly AppRoute[] = [
259256
render: (path: string) => <InsightsRoute path={path} />,
260257
},
261258
{
259+
// Presented as a dialog over the page behind it by `Shell` (app.tsx); the
260+
// route exists so the palette and path matching still know Settings.
262261
path: SETTINGS_PATH,
263262
label: "Settings",
264263
icon: <SlidersHorizontal />,
265-
render: (path: string, navigate: (to: string) => void) => (
266-
<SettingsRoute path={path} navigate={navigate} />
267-
),
264+
render: () => <></>,
268265
},
269266
];
270267

‎apps/web/src/settings/styles.css‎

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -748,28 +748,9 @@
748748
color: var(--ink-2);
749749
}
750750

751-
.settings-page-head {
752-
flex-shrink: 0;
753-
padding: 24px 28px 16px;
754-
}
755-
756751
/* react-ui's SettingsPanel card: ring-as-border like the mockup's raised cards. */
757752
.settings-stage-body > section.border {
758753
border-color: transparent;
759754
border-radius: 12px;
760755
box-shadow: var(--raised);
761756
}
762-
763-
.settings-page-head h1 {
764-
margin: 0;
765-
font-size: 26px;
766-
font-weight: 800;
767-
letter-spacing: -0.02em;
768-
line-height: 1.2;
769-
}
770-
771-
.settings-page-head p {
772-
margin: 6px 0 0;
773-
color: var(--ink-2);
774-
font-size: 15px;
775-
}

0 commit comments

Comments
 (0)