Settings-dialog round 1 staging repair: the provider-rewiring files

Round 1 (27d4f33) listed sidebar.tsx, keyboard-shortcuts.tsx,
app/settings/page.tsx and the globals.css keyframes in its message but
they were never staged. This commits exactly that content: the sidebar
Settings row as a button calling the dialog provider, the mod+, chord
opening the dialog, /settings redirecting to /chat/new, and the
settings-pop keyframes (reduced-motion aware). No new changes —
catch-up staging only. (next-env.d.ts churn left untracked-in-commit as
dev-server noise.)
This commit is contained in:
amosblomqvist
2026-09-19 22:29:17 +03:00
parent fd63e4a575
commit 0629e71a50
4 changed files with 55 additions and 20 deletions
+24
View File
@@ -389,6 +389,30 @@ body {
} }
} }
/* Settings dialog pop (settings-app wave, 2026-09-20): a short fade +
2% settle-scale — understated by design, the app's motion taste
(200ms sidebar, 150ms here); honors reduced-motion by appearing. */
@keyframes settings-pop {
from {
opacity: 0;
scale: 0.98;
}
to {
opacity: 1;
scale: 1;
}
}
.settings-dialog-pop {
animation: settings-pop 150ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.settings-dialog-pop {
animation: none;
}
}
/* ───────────────────────────────────────────────────────────────────────── /* ─────────────────────────────────────────────────────────────────────────
Electron merged chrome (owner, 2026-09-17). The system title bar is Electron merged chrome (owner, 2026-09-17). The system title bar is
hidden (titleBarStyle: "hiddenInset" in electron/main.cjs); the app's hidden (titleBarStyle: "hiddenInset" in electron/main.cjs); the app's
+9 -6
View File
@@ -1,8 +1,11 @@
/** The settings route (thin settings wave): server shell over the client import { redirect } from "next/navigation";
* page — same shape as the chat routes (AppShell wraps via the root
* layout, so the sidebar + its Settings entry come along). */
import { SettingsPage } from "@/components/settings/settings-page.tsx";
export default function Page() { /**
return <SettingsPage />; * The settings surface graduated into a centered dialog over the app
* (settings-app wave, 2026-09-20 — owner decision: modal-only, no page).
* The URL stays routed but redirects: nothing should land on a dead
* page; the dialog opens via the sidebar row or mod+,.
*/
export default function SettingsPage() {
redirect("/chat/new");
} }
+10 -5
View File
@@ -34,18 +34,21 @@ import {
KEYBOARD_ACTIONS, KEYBOARD_ACTIONS,
} from "@/src/core"; } from "@/src/core";
import { isDictationActive } from "@/components/dictation"; import { isDictationActive } from "@/components/dictation";
import { useSettingsDialog } from "@/components/settings/settings-dialog.tsx";
export function KeyboardShortcuts() { export function KeyboardShortcuts() {
const router = useRouter(); const router = useRouter();
const [cheatOpen, setCheatOpen] = useState(false); const [cheatOpen, setCheatOpen] = useState(false);
const { open: openSettings } = useSettingsDialog();
// Router + cheat state inside a ref so the keydown listener (attached // Router + cheat + settings state inside a ref so the keydown listener
// once) never goes stale. // (attached once) never goes stale.
const handlersRef = useRef<{ const handlersRef = useRef<{
router: ReturnType<typeof useRouter>; router: ReturnType<typeof useRouter>;
setCheatOpen: (v: boolean) => void; setCheatOpen: (v: boolean) => void;
}>({ router, setCheatOpen }); openSettings: () => void;
handlersRef.current = { router, setCheatOpen }; }>({ router, setCheatOpen, openSettings });
handlersRef.current = { router, setCheatOpen, openSettings };
useEffect(() => { useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
@@ -95,7 +98,9 @@ export function KeyboardShortcuts() {
handlersRef.current.router.push("/chat/new"); handlersRef.current.router.push("/chat/new");
break; break;
case "open-settings": case "open-settings":
handlersRef.current.router.push("/settings"); // Settings-app wave: the dialog is the surface, the URL is
// gone — /settings redirects to the draft shell.
handlersRef.current.openSettings();
break; break;
case "jump-graph": case "jump-graph":
handlersRef.current.router.push("/"); handlersRef.current.router.push("/");
+11 -8
View File
@@ -8,6 +8,7 @@ import { ActivitySection } from "@/components/activity-dock";
import { LhMark } from "@/components/lh-mark"; import { LhMark } from "@/components/lh-mark";
import { BrailleSpinner } from "@/components/braille-spinner"; import { BrailleSpinner } from "@/components/braille-spinner";
import { KeyboardShortcuts } from "@/components/keyboard-shortcuts"; import { KeyboardShortcuts } from "@/components/keyboard-shortcuts";
import { SettingsDialogProvider, useSettingsDialog } from "@/components/settings/settings-dialog.tsx";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -265,6 +266,7 @@ function SessionsSidebar() {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const { open: openSettings } = useSettingsDialog();
/* New session (owner, 2026-09-13): NO POST — the button opens the /* New session (owner, 2026-09-13): NO POST — the button opens the
sessionless draft view; the session is created only when the first sessionless draft view; the session is created only when the first
@@ -483,18 +485,19 @@ function SessionsSidebar() {
learner opted out (no ambient signal, user decision). */} learner opted out (no ambient signal, user decision). */}
<ActivitySection /> <ActivitySection />
{/* Settings entry (thin settings wave): the same row language {/* Settings entry (settings-app wave): same row language as
as New session — quiet, bottom-pinned, one chord away is New session — quiet, bottom-pinned, opens the centered
mod+, (the keyboard registry's open-settings action). */} dialog (also mod+, in the keyboard registry). */}
<div className="px-3 pb-3 pt-1"> <div className="px-3 pb-3 pt-1">
<Link <button
href="/settings" type="button"
onClick={openSettings}
data-testid="settings-entry" data-testid="settings-entry"
className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs uppercase tracking-wider text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs uppercase tracking-wider text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
> >
<SettingsIcon className="size-3" /> <SettingsIcon className="size-3" />
Settings Settings
</Link> </button>
</div> </div>
</div> </div>
</aside> </aside>
@@ -504,13 +507,13 @@ function SessionsSidebar() {
/** The app frame: sidebar inline + content. Rendered once in the root layout. */ /** The app frame: sidebar inline + content. Rendered once in the root layout. */
export function AppShell({ children }: { children: React.ReactNode }) { export function AppShell({ children }: { children: React.ReactNode }) {
return ( return (
<> <SettingsDialogProvider>
<SessionsSidebar /> <SessionsSidebar />
<main className="min-w-0 flex-1">{children}</main> <main className="min-w-0 flex-1">{children}</main>
{/* The single document-level keydown listener (keyboard wave A) — {/* The single document-level keydown listener (keyboard wave A) —
after the content in the tree, but it's window-level so order only after the content in the tree, but it's window-level so order only
matters for the events it dispatches, never for DOM priority. */} matters for the events it dispatches, never for DOM priority. */}
<KeyboardShortcuts /> <KeyboardShortcuts />
</> </SettingsDialogProvider>
); );
} }