From b3e42b6856beb18ae391005dac4bd08af8118bf0 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Tue, 29 Sep 2026 22:15:42 -0700 Subject: [PATCH 1/2] feat(web): sidebar footer has the profile menu with theme picker and a Tools button (CL-9560) --- apps/web/src/app.css | 217 ++++++++++++++++++++++++-- apps/web/src/lib/icons/index.tsx | 9 +- apps/web/src/shell/sidebar-footer.tsx | 201 ++++++++++++++++++++++++ apps/web/src/shell/sidebar.tsx | 87 +---------- 4 files changed, 418 insertions(+), 96 deletions(-) create mode 100644 apps/web/src/shell/sidebar-footer.tsx diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 7180f16d5..66f23aee2 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -18,7 +18,8 @@ (injected at runtime, unrelated to this stylesheet); this just stops react-ui's override from fighting it. Higher specificity than that rule wins the cascade on its own, so this is safe regardless of load order. */ -[data-sonner-toaster][data-x-position="right"] [data-sonner-toast].corbits-toast { +[data-sonner-toaster][data-x-position="right"] + [data-sonner-toast].corbits-toast { left: auto !important; transform: none !important; } @@ -39,7 +40,8 @@ html { /* Inputs in side drawers already have a visible control boundary. Color that edge on focus so the focus treatment reads as one control, not a second outline outside it. */ -[data-slot="dialog-content"][data-side="right"] [data-slot="input"]:focus-visible { +[data-slot="dialog-content"][data-side="right"] + [data-slot="input"]:focus-visible { outline: none; border-color: var(--accent-rail, var(--primary)); } @@ -112,7 +114,9 @@ select:disabled, Scoped to the sidebar so stage content keeps library defaults. */ .shell-sidebar button:not(:disabled):not([aria-disabled="true"]), .shell-sidebar [data-slot="button"]:not(:disabled):not([aria-disabled="true"]), -.shell-sidebar [data-slot="sidebar-item-row"] button:not(:disabled):not([aria-disabled="true"]), +.shell-sidebar + [data-slot="sidebar-item-row"] + button:not(:disabled):not([aria-disabled="true"]), .shell-sidebar .shell-ch-row:not(:disabled):not([aria-disabled="true"]) { cursor: pointer; } @@ -195,7 +199,10 @@ select:disabled, /* Shared dialog craft — layout/scroll/chrome on react-ui data slots only. Feature forms and dialog body content keep their own styles. */ [data-slot="dialog-content"][data-side="center"] { - width: min(calc(100vw - (var(--dialog-gutter) * 2)), var(--dialog-content-max-width)); + width: min( + calc(100vw - (var(--dialog-gutter) * 2)), + var(--dialog-content-max-width) + ); max-height: var(--dialog-content-max-height); overflow: hidden; } @@ -854,7 +861,8 @@ select:disabled, align-items: center; gap: 0.5rem; padding: 0.45rem 0.7rem; - border-bottom: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); + border-bottom: 1px solid + color-mix(in srgb, var(--foreground) 10%, transparent); } .shell-routine-new { @@ -1028,7 +1036,8 @@ select:disabled, justify-content: space-between; gap: 0.5rem; padding-bottom: 0.55rem; - border-bottom: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); + border-bottom: 1px solid + color-mix(in srgb, var(--foreground) 10%, transparent); } .shell-canvas-pane-heading { @@ -1656,7 +1665,9 @@ select:disabled, } .shell-sidebar .shell-panel-search [data-slot="input"]::placeholder, -.shell-sidebar [data-slot="sidebar-panel-body"] [data-slot="input"]::placeholder { +.shell-sidebar + [data-slot="sidebar-panel-body"] + [data-slot="input"]::placeholder { color: var(--shell-panel-faint); } @@ -1672,7 +1683,9 @@ select:disabled, box-shadow: none; } -.shell-sidebar [data-slot="sidebar-panel-body"] [data-slot="input"]:focus-visible { +.shell-sidebar + [data-slot="sidebar-panel-body"] + [data-slot="input"]:focus-visible { outline: none; border-color: var(--foreground); box-shadow: 0 0 0 1px var(--foreground); @@ -1703,14 +1716,19 @@ select:disabled, left edge. Selector doubles as the kill for react-ui's bg-primary/10 selected wash. */ .shell-sidebar [data-slot="sidebar-item-row"] > div.bg-primary\/10, -.shell-sidebar [data-slot="sidebar-item-row"] > div:has(button[aria-current="true"]) { +.shell-sidebar + [data-slot="sidebar-item-row"] + > div:has(button[aria-current="true"]) { background-color: var(--background); color: var(--shell-accent); box-shadow: inset 2px 0 0 0 var(--shell-accent); } .shell-sidebar [data-slot="sidebar-item-row"] > div.bg-primary\/10 > button, -.shell-sidebar [data-slot="sidebar-item-row"] > div:has(button[aria-current="true"]) > button { +.shell-sidebar + [data-slot="sidebar-item-row"] + > div:has(button[aria-current="true"]) + > button { font-weight: 600; } @@ -1748,7 +1766,12 @@ select:disabled, } /* Mock .count: the trailing meta slot when it is plain text. */ -.shell-sidebar [data-slot="sidebar-item-row"] > div > button > span + span:last-child { +.shell-sidebar + [data-slot="sidebar-item-row"] + > div + > button + > span + + span:last-child { font-size: 0.72rem; color: var(--shell-panel-faint); } @@ -2254,7 +2277,8 @@ select:disabled, attention-seeking animation, fully off under reduced motion. DESIGN.md's motion ceiling is 300ms — stays at 280ms. */ .onboarding-phase { - animation: onboarding-phase-in var(--duration-large, 300ms) var(--ease-out, ease) both; + animation: onboarding-phase-in var(--duration-large, 300ms) + var(--ease-out, ease) both; } /* The wait phases drop the usual title/subtitle/form stack for the shared @@ -2536,7 +2560,8 @@ select:disabled, .insights-grid > .insights-panel { border: 0; - border-bottom: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); + border-bottom: 1px solid + color-mix(in srgb, var(--foreground) 10%, transparent); } .insights-grid > .insights-panel:last-child { @@ -2834,7 +2859,11 @@ tr.insights-row-clickable:hover { } /* The current row's copy follows the row's accent ink. */ -.shell-sidebar [data-slot="sidebar-item-row"] button[aria-current="true"] .panel-row-copy strong { +.shell-sidebar + [data-slot="sidebar-item-row"] + button[aria-current="true"] + .panel-row-copy + strong { color: inherit; } @@ -3719,7 +3748,9 @@ tr.insights-row-clickable:hover { list-style: none; } /* A stopped schedule stays listed, dimmed, since only a new one revives it. */ -.workbench-info-schedule-list > li[data-stopped="true"] .workbench-info-cell-primary { +.workbench-info-schedule-list + > li[data-stopped="true"] + .workbench-info-cell-primary { color: var(--muted-foreground); text-decoration: line-through; } @@ -4195,3 +4226,159 @@ tr.insights-row-clickable:hover { grid-template-columns: 1fr; } } + +/* Sidebar bottom: profile button, Tools button, account menu. */ +.sb-bottom { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 8px 0; + margin-top: 6px; + border-top: 1px solid var(--line); + position: relative; +} +.sb-profile { + width: 36px; + height: 36px; + flex: 0 0 36px; + border: 0; + background: transparent; + border-radius: var(--r-md); + display: grid; + place-items: center; + cursor: pointer; + transition: background-color 120ms ease; +} +.sb-profile:hover { + background: var(--hover); +} +.sb-tools { + flex: 1; + height: 36px; + border: 0; + border-radius: var(--r-md); + background: var(--surface); + box-shadow: var(--raised); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font: inherit; + font-size: 13.5px; + font-weight: 700; + color: var(--ink); + cursor: pointer; + transition: + box-shadow 150ms ease-out, + transform 160ms var(--ease-out); +} +.sb-tools:hover { + box-shadow: var(--raised-hover); +} +.sb-tools:active { + transform: scale(0.97); +} +.sb-tools[data-active="true"] { + box-shadow: 0 0 0 1.5px var(--ink); +} +.sb-profile:focus-visible, +.sb-tools:focus-visible, +.sb-menu-item:focus-visible, +.sb-menu-theme button:focus-visible { + outline: 2px solid var(--ink); + outline-offset: 1px; +} +.sb-menu { + position: absolute; + left: 0; + right: 0; + bottom: calc(100% + 6px); + min-width: 220px; + padding: 4px; + z-index: 40; + background: var(--surface); + border-radius: var(--r-lg); + box-shadow: var(--floating); + transform-origin: bottom left; +} +.sb-menu-head { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; +} +.sb-menu-head b { + display: block; + font-size: 13.5px; + line-height: 1.2; +} +.sb-menu-head span { + display: block; + font-size: 12px; + color: var(--ink-3); +} +.sb-menu-sep { + height: 1px; + background: var(--line); + margin: 4px 6px; +} +.sb-menu-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + height: 34px; + border: 0; + background: transparent; + padding: 0 10px; + border-radius: var(--r-md); + font: inherit; + font-size: 13.5px; + text-align: left; + color: var(--ink); + cursor: pointer; +} +.sb-menu-item:hover { + background: var(--hover); +} +.sb-menu-item svg { + width: 1.05rem; + height: 1.05rem; + color: var(--ink-3); +} +.sb-menu-theme { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2px; + margin: 4px 6px; + padding: 2px; + border-radius: var(--r-md); + background: var(--hover); +} +.sb-menu-theme button { + height: 30px; + border: 0; + border-radius: 6px; + background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + font: inherit; + font-size: 12.5px; + font-weight: 700; + color: var(--ink-3); + cursor: pointer; +} +.sb-menu-theme button:hover { + color: var(--ink); +} +.sb-menu-theme button[data-active="true"] { + background: var(--surface); + color: var(--ink); + box-shadow: var(--raised); +} +.sb-menu-theme svg { + width: 0.9rem; + height: 0.9rem; +} diff --git a/apps/web/src/lib/icons/index.tsx b/apps/web/src/lib/icons/index.tsx index bcbe7983f..85fa04a3f 100644 --- a/apps/web/src/lib/icons/index.tsx +++ b/apps/web/src/lib/icons/index.tsx @@ -1,7 +1,11 @@ // Sparkle/Sparkles is banned outright — it read as a generic "AI" cliché. // A curated re-export, not a full pass-through, so a stray import can't // reach for an off-list icon or tiptoe around the stroke-width rule. -import { LucideProvider, type LucideIcon, type LucideProps } from "lucide-react"; +import { + LucideProvider, + type LucideIcon, + type LucideProps, +} from "lucide-react"; import type { ReactNode } from "react"; export type Icon = LucideIcon; @@ -51,7 +55,9 @@ export { MessageCircleMore as ChatCircleDots, Mic as Microphone, Minimize2 as ArrowsIn, + Moon, MoonStar as MoonStars, + Palette, Paperclip, Pencil as PencilSimple, Pin as PushPin, @@ -68,6 +74,7 @@ export { Square as Stop, SquareArrowOutUpRight as ArrowSquareOut, Star, + Sun, TriangleAlert as Warning, User, UserPlus, diff --git a/apps/web/src/shell/sidebar-footer.tsx b/apps/web/src/shell/sidebar-footer.tsx new file mode 100644 index 000000000..ea6eec0d6 --- /dev/null +++ b/apps/web/src/shell/sidebar-footer.tsx @@ -0,0 +1,201 @@ +// Profile menu (settings, theme, sign out) and the Tools button. + +import { + Moon, + Palette, + Plugs, + SignOut, + SlidersHorizontal, + Sun, + type Icon, +} from "@/lib/icons"; +import { useEffect, useRef, useState, type KeyboardEvent } from "react"; + +import { IdentityAvatar } from "../chat/avatar"; +import { useSessionUser, useSignOut } from "../navigation"; +import { matchesRoute, SETTINGS_PATH } from "../routes"; +import { + isTheme, + setTheme, + THEMES, + useTheme, + type Theme, +} from "../theme-store"; + +const THEME_OPTIONS: Record< + Theme, + { readonly label: string; readonly icon: Icon } +> = { + light: { label: "Light", icon: Sun }, + dark: { label: "Dark", icon: Moon }, + canvas: { label: "Canvas", icon: Palette }, +}; + +export function SidebarFooter({ + path, + onNavigate, +}: { + readonly path: string; + readonly onNavigate: (to: string) => void; +}) { + const user = useSessionUser(); + const onSignOut = useSignOut(); + const theme = useTheme(); + const [open, setOpen] = useState(false); + const rootRef = useRef(null); + const triggerRef = useRef(null); + const radioRefs = useRef>>( + {}, + ); + + useEffect(() => { + if (!open) return; + const onPointerDown = (event: PointerEvent) => { + if ( + event.target instanceof Node && + !rootRef.current?.contains(event.target) + ) + setOpen(false); + }; + document.addEventListener("pointerdown", onPointerDown); + return () => document.removeEventListener("pointerdown", onPointerDown); + }, [open]); + + const close = () => { + setOpen(false); + triggerRef.current?.focus(); + }; + + const onMenuKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.stopPropagation(); + close(); + } + }; + + const onRadioKeyDown = (event: KeyboardEvent) => { + const step = + event.key === "ArrowRight" || event.key === "ArrowDown" + ? 1 + : event.key === "ArrowLeft" || event.key === "ArrowUp" + ? -1 + : 0; + if (step === 0) return; + event.preventDefault(); + const next = + THEMES[(THEMES.indexOf(theme) + step + THEMES.length) % THEMES.length]; + if (next === undefined) return; + setTheme(next); + radioRefs.current[next]?.focus(); + }; + + const name = user?.name ?? "Account"; + const toolsActive = matchesRoute("/tools", path); + + return ( +
+ + + {open ? ( +
+
+ +
+ {name} + {user !== undefined ? {user.email} : null} +
+
+
+ +
+ {THEMES.map((key) => { + const { label, icon: ThemeIcon } = THEME_OPTIONS[key]; + const active = theme === key; + return ( + + ); + })} +
+ {onSignOut !== undefined ? ( + <> +
+ + + ) : null} +
+ ) : null} +
+ ); +} diff --git a/apps/web/src/shell/sidebar.tsx b/apps/web/src/shell/sidebar.tsx index 3e8664cf0..bd2c293a6 100644 --- a/apps/web/src/shell/sidebar.tsx +++ b/apps/web/src/shell/sidebar.tsx @@ -9,24 +9,16 @@ import { MenuTrigger, SidebarPanel, SidebarPanelBody, - SidebarPanelFooter, } from "@corbits/react-ui"; -import { - FlowArrow, - FolderOpen, - Lightning, - Plugs, - Plus, - Robot, - SlidersHorizontal, -} from "@/lib/icons"; +import { Plus } from "@/lib/icons"; import { useState } from "react"; import { useBench } from "../bench-context"; import { CreateAgentPanel } from "../pages/create-agent-panel"; import { AGENTS_PATH_PREFIX } from "../path-ids"; -import { matchesRoute, NEW_WORKBENCH_PATH, SETTINGS_PATH } from "../routes"; +import { NEW_WORKBENCH_PATH } from "../routes"; import { SidebarBrandMark } from "./brand-mark"; +import { SidebarFooter } from "./sidebar-footer"; import { WorkbenchList } from "./workbench-list"; export function Sidebar({ @@ -71,7 +63,9 @@ export function Sidebar({ > New Agent - onNavigate(NEW_WORKBENCH_PATH)}>New Workbench + onNavigate(NEW_WORKBENCH_PATH)}> + New Workbench +
@@ -91,74 +85,7 @@ export function Sidebar({ /> )} - - {/* Footer order: Artifacts, Skills, Tools, Workflows, Agents, then - one Settings row — Insights and the account menu (avatar, name, - sign out) now live inside Settings itself, not as separate - sidebar affordances. */} - - - - - - - - + ); } From 7bff051ac772d97d5c2f8119ab1ea3f86ab87869 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Tue, 29 Sep 2026 22:16:36 -0700 Subject: [PATCH 2/2] style(web): format the sidebar footer (CL-9560) --- apps/web/src/app.css | 61 +++++++-------------------- apps/web/src/lib/icons/index.tsx | 6 +-- apps/web/src/shell/sidebar-footer.tsx | 55 ++++-------------------- apps/web/src/shell/sidebar.tsx | 4 +- 4 files changed, 26 insertions(+), 100 deletions(-) diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 66f23aee2..5ee0832aa 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -18,8 +18,7 @@ (injected at runtime, unrelated to this stylesheet); this just stops react-ui's override from fighting it. Higher specificity than that rule wins the cascade on its own, so this is safe regardless of load order. */ -[data-sonner-toaster][data-x-position="right"] - [data-sonner-toast].corbits-toast { +[data-sonner-toaster][data-x-position="right"] [data-sonner-toast].corbits-toast { left: auto !important; transform: none !important; } @@ -40,8 +39,7 @@ html { /* Inputs in side drawers already have a visible control boundary. Color that edge on focus so the focus treatment reads as one control, not a second outline outside it. */ -[data-slot="dialog-content"][data-side="right"] - [data-slot="input"]:focus-visible { +[data-slot="dialog-content"][data-side="right"] [data-slot="input"]:focus-visible { outline: none; border-color: var(--accent-rail, var(--primary)); } @@ -114,9 +112,7 @@ select:disabled, Scoped to the sidebar so stage content keeps library defaults. */ .shell-sidebar button:not(:disabled):not([aria-disabled="true"]), .shell-sidebar [data-slot="button"]:not(:disabled):not([aria-disabled="true"]), -.shell-sidebar - [data-slot="sidebar-item-row"] - button:not(:disabled):not([aria-disabled="true"]), +.shell-sidebar [data-slot="sidebar-item-row"] button:not(:disabled):not([aria-disabled="true"]), .shell-sidebar .shell-ch-row:not(:disabled):not([aria-disabled="true"]) { cursor: pointer; } @@ -199,10 +195,7 @@ select:disabled, /* Shared dialog craft — layout/scroll/chrome on react-ui data slots only. Feature forms and dialog body content keep their own styles. */ [data-slot="dialog-content"][data-side="center"] { - width: min( - calc(100vw - (var(--dialog-gutter) * 2)), - var(--dialog-content-max-width) - ); + width: min(calc(100vw - (var(--dialog-gutter) * 2)), var(--dialog-content-max-width)); max-height: var(--dialog-content-max-height); overflow: hidden; } @@ -861,8 +854,7 @@ select:disabled, align-items: center; gap: 0.5rem; padding: 0.45rem 0.7rem; - border-bottom: 1px solid - color-mix(in srgb, var(--foreground) 10%, transparent); + border-bottom: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); } .shell-routine-new { @@ -1036,8 +1028,7 @@ select:disabled, justify-content: space-between; gap: 0.5rem; padding-bottom: 0.55rem; - border-bottom: 1px solid - color-mix(in srgb, var(--foreground) 10%, transparent); + border-bottom: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); } .shell-canvas-pane-heading { @@ -1665,9 +1656,7 @@ select:disabled, } .shell-sidebar .shell-panel-search [data-slot="input"]::placeholder, -.shell-sidebar - [data-slot="sidebar-panel-body"] - [data-slot="input"]::placeholder { +.shell-sidebar [data-slot="sidebar-panel-body"] [data-slot="input"]::placeholder { color: var(--shell-panel-faint); } @@ -1683,9 +1672,7 @@ select:disabled, box-shadow: none; } -.shell-sidebar - [data-slot="sidebar-panel-body"] - [data-slot="input"]:focus-visible { +.shell-sidebar [data-slot="sidebar-panel-body"] [data-slot="input"]:focus-visible { outline: none; border-color: var(--foreground); box-shadow: 0 0 0 1px var(--foreground); @@ -1716,19 +1703,14 @@ select:disabled, left edge. Selector doubles as the kill for react-ui's bg-primary/10 selected wash. */ .shell-sidebar [data-slot="sidebar-item-row"] > div.bg-primary\/10, -.shell-sidebar - [data-slot="sidebar-item-row"] - > div:has(button[aria-current="true"]) { +.shell-sidebar [data-slot="sidebar-item-row"] > div:has(button[aria-current="true"]) { background-color: var(--background); color: var(--shell-accent); box-shadow: inset 2px 0 0 0 var(--shell-accent); } .shell-sidebar [data-slot="sidebar-item-row"] > div.bg-primary\/10 > button, -.shell-sidebar - [data-slot="sidebar-item-row"] - > div:has(button[aria-current="true"]) - > button { +.shell-sidebar [data-slot="sidebar-item-row"] > div:has(button[aria-current="true"]) > button { font-weight: 600; } @@ -1766,12 +1748,7 @@ select:disabled, } /* Mock .count: the trailing meta slot when it is plain text. */ -.shell-sidebar - [data-slot="sidebar-item-row"] - > div - > button - > span - + span:last-child { +.shell-sidebar [data-slot="sidebar-item-row"] > div > button > span + span:last-child { font-size: 0.72rem; color: var(--shell-panel-faint); } @@ -2277,8 +2254,7 @@ select:disabled, attention-seeking animation, fully off under reduced motion. DESIGN.md's motion ceiling is 300ms — stays at 280ms. */ .onboarding-phase { - animation: onboarding-phase-in var(--duration-large, 300ms) - var(--ease-out, ease) both; + animation: onboarding-phase-in var(--duration-large, 300ms) var(--ease-out, ease) both; } /* The wait phases drop the usual title/subtitle/form stack for the shared @@ -2560,8 +2536,7 @@ select:disabled, .insights-grid > .insights-panel { border: 0; - border-bottom: 1px solid - color-mix(in srgb, var(--foreground) 10%, transparent); + border-bottom: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); } .insights-grid > .insights-panel:last-child { @@ -2859,11 +2834,7 @@ tr.insights-row-clickable:hover { } /* The current row's copy follows the row's accent ink. */ -.shell-sidebar - [data-slot="sidebar-item-row"] - button[aria-current="true"] - .panel-row-copy - strong { +.shell-sidebar [data-slot="sidebar-item-row"] button[aria-current="true"] .panel-row-copy strong { color: inherit; } @@ -3748,9 +3719,7 @@ tr.insights-row-clickable:hover { list-style: none; } /* A stopped schedule stays listed, dimmed, since only a new one revives it. */ -.workbench-info-schedule-list - > li[data-stopped="true"] - .workbench-info-cell-primary { +.workbench-info-schedule-list > li[data-stopped="true"] .workbench-info-cell-primary { color: var(--muted-foreground); text-decoration: line-through; } diff --git a/apps/web/src/lib/icons/index.tsx b/apps/web/src/lib/icons/index.tsx index 85fa04a3f..a86300a01 100644 --- a/apps/web/src/lib/icons/index.tsx +++ b/apps/web/src/lib/icons/index.tsx @@ -1,11 +1,7 @@ // Sparkle/Sparkles is banned outright — it read as a generic "AI" cliché. // A curated re-export, not a full pass-through, so a stray import can't // reach for an off-list icon or tiptoe around the stroke-width rule. -import { - LucideProvider, - type LucideIcon, - type LucideProps, -} from "lucide-react"; +import { LucideProvider, type LucideIcon, type LucideProps } from "lucide-react"; import type { ReactNode } from "react"; export type Icon = LucideIcon; diff --git a/apps/web/src/shell/sidebar-footer.tsx b/apps/web/src/shell/sidebar-footer.tsx index ea6eec0d6..20b3b4c6a 100644 --- a/apps/web/src/shell/sidebar-footer.tsx +++ b/apps/web/src/shell/sidebar-footer.tsx @@ -1,31 +1,14 @@ // Profile menu (settings, theme, sign out) and the Tools button. -import { - Moon, - Palette, - Plugs, - SignOut, - SlidersHorizontal, - Sun, - type Icon, -} from "@/lib/icons"; +import { Moon, Palette, Plugs, SignOut, SlidersHorizontal, Sun, type Icon } from "@/lib/icons"; import { useEffect, useRef, useState, type KeyboardEvent } from "react"; import { IdentityAvatar } from "../chat/avatar"; import { useSessionUser, useSignOut } from "../navigation"; import { matchesRoute, SETTINGS_PATH } from "../routes"; -import { - isTheme, - setTheme, - THEMES, - useTheme, - type Theme, -} from "../theme-store"; +import { isTheme, setTheme, THEMES, useTheme, type Theme } from "../theme-store"; -const THEME_OPTIONS: Record< - Theme, - { readonly label: string; readonly icon: Icon } -> = { +const THEME_OPTIONS: Record = { light: { label: "Light", icon: Sun }, dark: { label: "Dark", icon: Moon }, canvas: { label: "Canvas", icon: Palette }, @@ -44,18 +27,12 @@ export function SidebarFooter({ const [open, setOpen] = useState(false); const rootRef = useRef(null); const triggerRef = useRef(null); - const radioRefs = useRef>>( - {}, - ); + const radioRefs = useRef>>({}); useEffect(() => { if (!open) return; const onPointerDown = (event: PointerEvent) => { - if ( - event.target instanceof Node && - !rootRef.current?.contains(event.target) - ) - setOpen(false); + if (event.target instanceof Node && !rootRef.current?.contains(event.target)) setOpen(false); }; document.addEventListener("pointerdown", onPointerDown); return () => document.removeEventListener("pointerdown", onPointerDown); @@ -82,8 +59,7 @@ export function SidebarFooter({ : 0; if (step === 0) return; event.preventDefault(); - const next = - THEMES[(THEMES.indexOf(theme) + step + THEMES.length) % THEMES.length]; + const next = THEMES[(THEMES.indexOf(theme) + step + THEMES.length) % THEMES.length]; if (next === undefined) return; setTheme(next); radioRefs.current[next]?.focus(); @@ -103,11 +79,7 @@ export function SidebarFooter({ aria-label="Account" onClick={() => setOpen((value) => !value)} > - +