diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 7180f16d5..5ee0832aa 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -4195,3 +4195,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..a86300a01 100644 --- a/apps/web/src/lib/icons/index.tsx +++ b/apps/web/src/lib/icons/index.tsx @@ -51,7 +51,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 +70,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..20b3b4c6a --- /dev/null +++ b/apps/web/src/shell/sidebar-footer.tsx @@ -0,0 +1,164 @@ +// 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 = { + 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..6cb787448 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({ @@ -91,74 +83,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. */} - - - - - - - - + ); }