| author | |
| committer | |
| log | 96906805f1677a3b064ebd2c40902cbb48337a23 |
| tree | 98a53b8da83377d5c92e0e697bd6d63251ed62bf |
| parent | 73b2e8fdd47807fcc0fc5bfe4928187035b71242 |
| signature | Signed by PGP key B5690EEEBB952194 |
6 files changed, 232 insertions(+), 119 deletions(-)
apps/web/src/components/WorkspaceBreadcrumb.tsx created+54| ... | @@ -0,0 +1,54 @@ | ||
| 1 | import type { ReactNode } from "react"; | ||
| 2 | |||
| 3 | import { cn } from "../lib/utils"; | ||
| 4 | |||
| 5 | interface WorkspaceBreadcrumbProps { | ||
| 6 | readonly ariaLabel: string; | ||
| 7 | readonly children: ReactNode; | ||
| 8 | readonly className?: string; | ||
| 9 | } | ||
| 10 | |||
| 11 | export function WorkspaceBreadcrumb({ ariaLabel, children, className }: WorkspaceBreadcrumbProps) { | ||
| 12 | return ( | ||
| 13 | <nav aria-label={ariaLabel} className={cn("min-w-0", className)}> | ||
| 14 | {/* Keep the flexible container draggable in Electron. Interactive | ||
| 15 | descendants are excluded by the shared .drag-region CSS rules. */} | ||
| 16 | <ol className="m-0 flex min-w-0 list-none items-center gap-2 p-0 text-sm sm:gap-3"> | ||
| 17 | {children} | ||
| 18 | </ol> | ||
| 19 | </nav> | ||
| 20 | ); | ||
| 21 | } | ||
| 22 | |||
| 23 | interface WorkspaceBreadcrumbItemProps { | ||
| 24 | readonly children: ReactNode; | ||
| 25 | readonly className?: string; | ||
| 26 | readonly current?: boolean; | ||
| 27 | } | ||
| 28 | |||
| 29 | export function WorkspaceBreadcrumbItem({ | ||
| 30 | children, | ||
| 31 | className, | ||
| 32 | current = false, | ||
| 33 | }: WorkspaceBreadcrumbItemProps) { | ||
| 34 | return ( | ||
| 35 | <li | ||
| 36 | aria-current={current ? "page" : undefined} | ||
| 37 | className={cn( | ||
| 38 | "flex min-w-0 items-center font-medium", | ||
| 39 | current ? "text-foreground" : "shrink-0 text-muted-foreground", | ||
| 40 | className, | ||
| 41 | )} | ||
| 42 | > | ||
| 43 | {children} | ||
| 44 | </li> | ||
| 45 | ); | ||
| 46 | } | ||
| 47 | |||
| 48 | export function WorkspaceBreadcrumbSeparator() { | ||
| 49 | return ( | ||
| 50 | <li aria-hidden="true" className="flex shrink-0 items-center text-icon-muted"> | ||
| 51 | / | ||
| 52 | </li> | ||
| 53 | ); | ||
| 54 | } | ||
apps/web/src/components/chat/ChatHeader.tsx+71-69| ... | @@ -36,6 +36,11 @@ import { useThreadActionMenu } from "~/hooks/useThreadActionMenu"; | ... | @@ -36,6 +36,11 @@ import { useThreadActionMenu } from "~/hooks/useThreadActionMenu"; |
| 36 | import { threadEnvironment } from "../../state/threads"; | 36 | import { threadEnvironment } from "../../state/threads"; |
| 37 | import { useAtomCommand } from "../../state/use-atom-command"; | 37 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 38 | import { ProjectFavicon } from "../ProjectFavicon"; | 38 | import { ProjectFavicon } from "../ProjectFavicon"; |
| 39 | import { | ||
| 40 | WorkspaceBreadcrumb, | ||
| 41 | WorkspaceBreadcrumbItem, | ||
| 42 | WorkspaceBreadcrumbSeparator, | ||
| 43 | } from "../WorkspaceBreadcrumb"; | ||
| 39 | import { cn } from "~/lib/utils"; | 44 | import { cn } from "~/lib/utils"; |
| 40 | 45 | ||
| 41 | interface ChatHeaderProps { | 46 | interface ChatHeaderProps { |
| ... | @@ -211,91 +216,88 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -211,91 +216,88 @@ export const ChatHeader = memo(function ChatHeader({ |
| 211 | className="@container/header-actions flex min-w-0 flex-1 items-center gap-2 sm:gap-3" | 216 | className="@container/header-actions flex min-w-0 flex-1 items-center gap-2 sm:gap-3" |
| 212 | onContextMenu={handleHeaderContextMenu} | 217 | onContextMenu={handleHeaderContextMenu} |
| 213 | > | 218 | > |
| 214 | <div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3"> | 219 | <WorkspaceBreadcrumb ariaLabel="Thread breadcrumb" className="flex-1"> |
| 215 | {/* The project always leads the header: knowing which project a | 220 | {/* The project always leads the header: knowing which project a |
| 216 | thread lives in is priority zero, and the thread title alone | 221 | thread lives in is priority zero, and the thread title alone |
| 217 | doesn't answer it. */} | 222 | doesn't answer it. */} |
| 218 | {activeProjectName ? ( | 223 | {activeProjectName ? ( |
| 219 | <span className="inline-flex shrink-0 items-center gap-2"> | 224 | <> |
| 225 | <WorkspaceBreadcrumbItem> | ||
| 226 | <Tooltip> | ||
| 227 | <TooltipTrigger | ||
| 228 | render={ | ||
| 229 | <button | ||
| 230 | type="button" | ||
| 231 | aria-label={`New thread in ${activeProjectName}`} | ||
| 232 | onClick={onNewThreadInProject} | ||
| 233 | className="inline-flex min-w-0 cursor-pointer items-center gap-1.5 rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" | ||
| 234 | /> | ||
| 235 | } | ||
| 236 | > | ||
| 237 | <ProjectFavicon | ||
| 238 | environmentId={activeThreadEnvironmentId} | ||
| 239 | cwd={activeProjectCwd ?? ""} | ||
| 240 | faviconPath={activeProjectFaviconPath} | ||
| 241 | className="size-3.5" | ||
| 242 | /> | ||
| 243 | <span className="max-w-40 truncate">{activeProjectName}</span> | ||
| 244 | </TooltipTrigger> | ||
| 245 | <TooltipPopup side="top">New thread in {activeProjectName}</TooltipPopup> | ||
| 246 | </Tooltip> | ||
| 247 | </WorkspaceBreadcrumbItem> | ||
| 248 | <WorkspaceBreadcrumbSeparator /> | ||
| 249 | </> | ||
| 250 | ) : null} | ||
| 251 | <WorkspaceBreadcrumbItem current className="flex-1"> | ||
| 252 | {renamingTitle !== null ? ( | ||
| 253 | <input | ||
| 254 | autoFocus | ||
| 255 | aria-label="Thread title" | ||
| 256 | className="min-w-0 flex-1 rounded-sm bg-transparent text-sm font-medium text-foreground outline-none ring-1 ring-ring/50 focus:ring-ring" | ||
| 257 | defaultValue={renamingTitle} | ||
| 258 | onBlur={(event) => { | ||
| 259 | if (renameCommittedRef.current) return; | ||
| 260 | commitRename(event.currentTarget.value); | ||
| 261 | }} | ||
| 262 | onFocus={(event) => event.currentTarget.select()} | ||
| 263 | onKeyDown={handleRenameKeyDown} | ||
| 264 | /> | ||
| 265 | ) : isServerThread ? ( | ||
| 220 | <Tooltip> | 266 | <Tooltip> |
| 221 | <TooltipTrigger | 267 | <TooltipTrigger |
| 222 | render={ | 268 | render={ |
| 223 | <button | 269 | <button |
| 270 | ref={titleButtonRef} | ||
| 224 | type="button" | 271 | type="button" |
| 225 | aria-label={`New thread in ${activeProjectName}`} | 272 | aria-label={`Thread actions for ${activeThreadTitle}`} |
| 226 | onClick={onNewThreadInProject} | 273 | aria-haspopup="menu" |
| 227 | className="inline-flex min-w-0 cursor-pointer items-center gap-1.5 rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" | 274 | onClick={openMenuFromTitle} |
| 275 | className="group/thread-title inline-flex min-w-0 max-w-full cursor-pointer items-center gap-1 rounded-sm text-left focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" | ||
| 228 | /> | 276 | /> |
| 229 | } | 277 | } |
| 230 | > | 278 | > |
| 231 | <ProjectFavicon | 279 | <h2 className="min-w-0 truncate">{activeThreadTitle}</h2> |
| 232 | environmentId={activeThreadEnvironmentId} | 280 | <ChevronDownIcon |
| 233 | cwd={activeProjectCwd ?? ""} | 281 | aria-hidden |
| 234 | faviconPath={activeProjectFaviconPath} | 282 | className="size-3.5 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover/thread-title:opacity-100 group-focus-visible/thread-title:opacity-100" |
| 235 | className="size-3.5" | ||
| 236 | /> | 283 | /> |
| 237 | <span className="max-w-40 truncate text-sm font-medium">{activeProjectName}</span> | ||
| 238 | </TooltipTrigger> | 284 | </TooltipTrigger> |
| 239 | <TooltipPopup side="top">New thread in {activeProjectName}</TooltipPopup> | 285 | <TooltipPopup side="top">{activeThreadTitle}</TooltipPopup> |
| 240 | </Tooltip> | 286 | </Tooltip> |
| 241 | <span aria-hidden className="text-icon-muted"> | 287 | ) : ( |
| 242 | / | 288 | <Tooltip> |
| 243 | </span> | 289 | <TooltipTrigger |
| 244 | </span> | 290 | render={ |
| 245 | ) : null} | 291 | <h2 aria-label={activeThreadTitle} className="min-w-0 flex-1 truncate"> |
| 246 | {renamingTitle !== null ? ( | 292 | {activeThreadTitle} |
| 247 | <input | 293 | </h2> |
| 248 | autoFocus | 294 | } |
| 249 | aria-label="Thread title" | ||
| 250 | className="min-w-0 flex-1 rounded-sm bg-transparent text-sm font-medium text-foreground outline-none ring-1 ring-ring/50 focus:ring-ring" | ||
| 251 | defaultValue={renamingTitle} | ||
| 252 | onBlur={(event) => { | ||
| 253 | if (renameCommittedRef.current) return; | ||
| 254 | commitRename(event.currentTarget.value); | ||
| 255 | }} | ||
| 256 | onFocus={(event) => event.currentTarget.select()} | ||
| 257 | onKeyDown={handleRenameKeyDown} | ||
| 258 | /> | ||
| 259 | ) : isServerThread ? ( | ||
| 260 | <Tooltip> | ||
| 261 | <TooltipTrigger | ||
| 262 | render={ | ||
| 263 | <button | ||
| 264 | ref={titleButtonRef} | ||
| 265 | type="button" | ||
| 266 | aria-label={`Thread actions for ${activeThreadTitle}`} | ||
| 267 | aria-haspopup="menu" | ||
| 268 | onClick={openMenuFromTitle} | ||
| 269 | className="group/thread-title inline-flex min-w-0 cursor-pointer items-center gap-1 rounded-sm text-left focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" | ||
| 270 | /> | ||
| 271 | } | ||
| 272 | > | ||
| 273 | <h2 className="min-w-0 truncate text-sm font-medium text-foreground"> | ||
| 274 | {activeThreadTitle} | ||
| 275 | </h2> | ||
| 276 | <ChevronDownIcon | ||
| 277 | aria-hidden | ||
| 278 | className="size-3.5 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover/thread-title:opacity-100 group-focus-visible/thread-title:opacity-100" | ||
| 279 | /> | 295 | /> |
| 280 | </TooltipTrigger> | 296 | <TooltipPopup side="top">{activeThreadTitle}</TooltipPopup> |
| 281 | <TooltipPopup side="top">{activeThreadTitle}</TooltipPopup> | 297 | </Tooltip> |
| 282 | </Tooltip> | 298 | )} |
| 283 | ) : ( | 299 | </WorkspaceBreadcrumbItem> |
| 284 | <Tooltip> | 300 | </WorkspaceBreadcrumb> |
| 285 | <TooltipTrigger | ||
| 286 | render={ | ||
| 287 | <h2 | ||
| 288 | aria-label={activeThreadTitle} | ||
| 289 | className="min-w-0 flex-1 truncate text-sm font-medium text-foreground" | ||
| 290 | > | ||
| 291 | {activeThreadTitle} | ||
| 292 | </h2> | ||
| 293 | } | ||
| 294 | /> | ||
| 295 | <TooltipPopup side="top">{activeThreadTitle}</TooltipPopup> | ||
| 296 | </Tooltip> | ||
| 297 | )} | ||
| 298 | </div> | ||
| 299 | <div | 301 | <div |
| 300 | data-chat-header-actions | 302 | data-chat-header-actions |
| 301 | className={cn( | 303 | className={cn( |
apps/web/src/components/settings/ProjectSettingsPanel.tsx+56-39| ... | @@ -13,6 +13,7 @@ import { | ... | @@ -13,6 +13,7 @@ import { |
| 13 | selectProjectGroupingSettings, | 13 | selectProjectGroupingSettings, |
| 14 | } from "../../logicalProject"; | 14 | } from "../../logicalProject"; |
| 15 | import type { | 15 | import type { |
| 16 | ContextMenuItem, | ||
| 16 | ModelSelection, | 17 | ModelSelection, |
| 17 | ProviderDriverKind, | 18 | ProviderDriverKind, |
| 18 | SidebarProjectGroupingMode, | 19 | SidebarProjectGroupingMode, |
| ... | @@ -25,7 +26,14 @@ import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings"; | ... | @@ -25,7 +26,14 @@ import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings"; |
| 25 | import { useCanGoBack, useNavigate } from "@tanstack/react-router"; | 26 | import { useCanGoBack, useNavigate } from "@tanstack/react-router"; |
| 26 | import * as Cause from "effect/Cause"; | 27 | import * as Cause from "effect/Cause"; |
| 27 | import { ChevronDownIcon, CopyIcon, PlusIcon, SettingsIcon, Trash2Icon } from "lucide-react"; | 28 | import { ChevronDownIcon, CopyIcon, PlusIcon, SettingsIcon, Trash2Icon } from "lucide-react"; |
| 28 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | 29 | import { |
| 30 | useCallback, | ||
| 31 | useEffect, | ||
| 32 | useMemo, | ||
| 33 | useRef, | ||
| 34 | useState, | ||
| 35 | type MouseEvent as ReactMouseEvent, | ||
| 36 | } from "react"; | ||
| 29 | 37 | ||
| 30 | import { useComposerDraftStore } from "../../composerDraftStore"; | 38 | import { useComposerDraftStore } from "../../composerDraftStore"; |
| 31 | import { isElectron } from "../../env"; | 39 | import { isElectron } from "../../env"; |
| ... | @@ -89,6 +97,11 @@ import { | ... | @@ -89,6 +97,11 @@ import { |
| 89 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; | 97 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; |
| 90 | import { SidebarInset } from "../ui/sidebar"; | 98 | import { SidebarInset } from "../ui/sidebar"; |
| 91 | import { stackedThreadToast, toastManager } from "../ui/toast"; | 99 | import { stackedThreadToast, toastManager } from "../ui/toast"; |
| 100 | import { | ||
| 101 | WorkspaceBreadcrumb, | ||
| 102 | WorkspaceBreadcrumbItem, | ||
| 103 | WorkspaceBreadcrumbSeparator, | ||
| 104 | } from "../WorkspaceBreadcrumb"; | ||
| 92 | import { | 105 | import { |
| 93 | SettingResetButton, | 106 | SettingResetButton, |
| 94 | SettingsPageContainer, | 107 | SettingsPageContainer, |
| ... | @@ -191,48 +204,52 @@ function ProjectSettingsBreadcrumb({ projectKey }: { projectKey: string }) { | ... | @@ -191,48 +204,52 @@ function ProjectSettingsBreadcrumb({ projectKey }: { projectKey: string }) { |
| 191 | const groups = useSettingsProjectGroups(); | 204 | const groups = useSettingsProjectGroups(); |
| 192 | const navigate = useNavigate(); | 205 | const navigate = useNavigate(); |
| 193 | const selected = groups.find((group) => group.projectKey === projectKey) ?? null; | 206 | const selected = groups.find((group) => group.projectKey === projectKey) ?? null; |
| 207 | const openProjectMenu = (event: ReactMouseEvent<HTMLButtonElement>) => { | ||
| 208 | const api = readLocalApi(); | ||
| 209 | if (!api) return; | ||
| 210 | |||
| 211 | const rect = event.currentTarget.getBoundingClientRect(); | ||
| 212 | const items: ContextMenuItem<string>[] = groups.map((group) => ({ | ||
| 213 | id: group.projectKey, | ||
| 214 | label: group.displayName, | ||
| 215 | })); | ||
| 216 | void settlePromise(() => | ||
| 217 | api.contextMenu.show(items, { x: rect.left, y: rect.bottom + 4 }), | ||
| 218 | ).then((clicked) => { | ||
| 219 | if (clicked._tag === "Failure" || clicked.value === null) return; | ||
| 220 | void navigate({ | ||
| 221 | to: "/projects/$projectKey", | ||
| 222 | params: { projectKey: clicked.value }, | ||
| 223 | replace: true, | ||
| 224 | hashScrollIntoView: false, | ||
| 225 | }); | ||
| 226 | }); | ||
| 227 | }; | ||
| 194 | 228 | ||
| 195 | return ( | 229 | return ( |
| 196 | <nav | 230 | <WorkspaceBreadcrumb ariaLabel="Project settings breadcrumb"> |
| 197 | aria-label="Project settings breadcrumb" | 231 | <WorkspaceBreadcrumbItem>Projects</WorkspaceBreadcrumbItem> |
| 198 | className="flex min-w-0 items-center gap-2 overflow-hidden [-webkit-app-region:no-drag] sm:gap-3" | 232 | <WorkspaceBreadcrumbSeparator /> |
| 199 | > | 233 | <WorkspaceBreadcrumbItem current> |
| 200 | <span className="shrink-0 text-sm font-medium text-muted-foreground">Projects</span> | 234 | {selected ? ( |
| 201 | <span aria-hidden="true" className="shrink-0 text-icon-muted"> | 235 | <button |
| 202 | / | 236 | type="button" |
| 203 | </span> | 237 | aria-haspopup="menu" |
| 204 | {selected ? ( | ||
| 205 | <Select | ||
| 206 | value={selected.projectKey} | ||
| 207 | onValueChange={(value) => | ||
| 208 | void navigate({ | ||
| 209 | to: "/projects/$projectKey", | ||
| 210 | params: { projectKey: String(value) }, | ||
| 211 | replace: true, | ||
| 212 | hashScrollIntoView: false, | ||
| 213 | }) | ||
| 214 | } | ||
| 215 | > | ||
| 216 | <SelectTrigger | ||
| 217 | aria-label="Switch project" | 238 | aria-label="Switch project" |
| 218 | className="h-auto min-h-0 min-w-0 max-w-64 gap-1 border-0 px-0 py-0 text-sm font-medium text-foreground sm:h-auto sm:min-h-0 sm:text-sm" | 239 | onClick={openProjectMenu} |
| 219 | size="xs" | 240 | className="group/project-title inline-flex min-w-0 max-w-64 cursor-pointer items-center gap-1 rounded-sm text-left focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" |
| 220 | variant="ghost" | ||
| 221 | > | 241 | > |
| 222 | <SelectValue>{selected.displayName}</SelectValue> | 242 | <span className="min-w-0 truncate">{selected.displayName}</span> |
| 223 | </SelectTrigger> | 243 | <ChevronDownIcon |
| 224 | <SelectPopup align="start" alignItemWithTrigger={false}> | 244 | aria-hidden |
| 225 | {groups.map((candidate) => ( | 245 | className="size-3.5 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover/project-title:opacity-100 group-focus-visible/project-title:opacity-100" |
| 226 | <SelectItem key={candidate.projectKey} hideIndicator value={candidate.projectKey}> | 246 | /> |
| 227 | {candidate.displayName} | 247 | </button> |
| 228 | </SelectItem> | 248 | ) : ( |
| 229 | ))} | 249 | <span className="truncate text-muted-foreground">Unavailable project</span> |
| 230 | </SelectPopup> | 250 | )} |
| 231 | </Select> | 251 | </WorkspaceBreadcrumbItem> |
| 232 | ) : ( | 252 | </WorkspaceBreadcrumb> |
| 233 | <span className="truncate text-sm text-muted-foreground">Unavailable project</span> | ||
| 234 | )} | ||
| 235 | </nav> | ||
| 236 | ); | 253 | ); |
| 237 | } | 254 | } |
| 238 | 255 |
apps/web/src/components/settings/SettingsBreadcrumb.tsx created+34| ... | @@ -0,0 +1,34 @@ | ||
| 1 | import { | ||
| 2 | WorkspaceBreadcrumb, | ||
| 3 | WorkspaceBreadcrumbItem, | ||
| 4 | WorkspaceBreadcrumbSeparator, | ||
| 5 | } from "../WorkspaceBreadcrumb"; | ||
| 6 | import { SETTINGS_SECTION_LABELS } from "./settingsSearch"; | ||
| 7 | |||
| 8 | const SETTINGS_BREADCRUMB_LABELS: Readonly<Record<string, string>> = { | ||
| 9 | ...SETTINGS_SECTION_LABELS, | ||
| 10 | "/settings/diagnostics": "Diagnostics", | ||
| 11 | }; | ||
| 12 | |||
| 13 | function settingsBreadcrumbLabel(pathname: string): string | null { | ||
| 14 | const normalizedPathname = pathname.replace(/\/+$/, "") || "/"; | ||
| 15 | return SETTINGS_BREADCRUMB_LABELS[normalizedPathname] ?? null; | ||
| 16 | } | ||
| 17 | |||
| 18 | export function SettingsBreadcrumb({ pathname }: { pathname: string }) { | ||
| 19 | const sectionLabel = settingsBreadcrumbLabel(pathname); | ||
| 20 | |||
| 21 | return ( | ||
| 22 | <WorkspaceBreadcrumb ariaLabel="Settings breadcrumb"> | ||
| 23 | {sectionLabel ? ( | ||
| 24 | <> | ||
| 25 | <WorkspaceBreadcrumbItem>Settings</WorkspaceBreadcrumbItem> | ||
| 26 | <WorkspaceBreadcrumbSeparator /> | ||
| 27 | </> | ||
| 28 | ) : null} | ||
| 29 | <WorkspaceBreadcrumbItem current className="truncate"> | ||
| 30 | {sectionLabel ?? "Settings"} | ||
| 31 | </WorkspaceBreadcrumbItem> | ||
| 32 | </WorkspaceBreadcrumb> | ||
| 33 | ); | ||
| 34 | } | ||
apps/web/src/components/usage/UsagePage.tsx+7-2| ... | @@ -16,6 +16,7 @@ import { | ... | @@ -16,6 +16,7 @@ import { |
| 16 | } from "@t3tools/shared/usageFormat"; | 16 | } from "@t3tools/shared/usageFormat"; |
| 17 | import { ScrollArea } from "../ui/scroll-area"; | 17 | import { ScrollArea } from "../ui/scroll-area"; |
| 18 | import { SidebarInset } from "../ui/sidebar"; | 18 | import { SidebarInset } from "../ui/sidebar"; |
| 19 | import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem } from "../WorkspaceBreadcrumb"; | ||
| 19 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar"; | 20 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar"; |
| 20 | import { UsageChartLegend, UsageProviderChart, type UsageChartMetric } from "./UsageProviderChart"; | 21 | import { UsageChartLegend, UsageProviderChart, type UsageChartMetric } from "./UsageProviderChart"; |
| 21 | import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders"; | 22 | import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders"; |
| ... | @@ -71,7 +72,9 @@ export function UsagePage() { | ... | @@ -71,7 +72,9 @@ export function UsagePage() { |
| 71 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | 72 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, |
| 72 | )} | 73 | )} |
| 73 | > | 74 | > |
| 74 | <h1 className="text-sm font-medium text-foreground">Usage</h1> | 75 | <WorkspaceBreadcrumb ariaLabel="Usage breadcrumb"> |
| 76 | <WorkspaceBreadcrumbItem current>Usage</WorkspaceBreadcrumbItem> | ||
| 77 | </WorkspaceBreadcrumb> | ||
| 75 | </header> | 78 | </header> |
| 76 | )} | 79 | )} |
| 77 | 80 | ||
| ... | @@ -82,7 +85,9 @@ export function UsagePage() { | ... | @@ -82,7 +85,9 @@ export function UsagePage() { |
| 82 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | 85 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, |
| 83 | )} | 86 | )} |
| 84 | > | 87 | > |
| 85 | <h1 className="text-sm font-medium text-foreground">Usage</h1> | 88 | <WorkspaceBreadcrumb ariaLabel="Usage breadcrumb"> |
| 89 | <WorkspaceBreadcrumbItem current>Usage</WorkspaceBreadcrumbItem> | ||
| 90 | </WorkspaceBreadcrumb> | ||
| 86 | </div> | 91 | </div> |
| 87 | )} | 92 | )} |
| 88 | 93 |
apps/web/src/routes/settings.tsx+10-9| ... | @@ -10,6 +10,7 @@ import { | ... | @@ -10,6 +10,7 @@ import { |
| 10 | import { useCallback, useEffect, useState } from "react"; | 10 | import { useCallback, useEffect, useState } from "react"; |
| 11 | 11 | ||
| 12 | import { useSettingsRestore } from "../components/settings/SettingsPanels"; | 12 | import { useSettingsRestore } from "../components/settings/SettingsPanels"; |
| 13 | import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb"; | ||
| 13 | import { Button } from "../components/ui/button"; | 14 | import { Button } from "../components/ui/button"; |
| 14 | import { SidebarInset } from "../components/ui/sidebar"; | 15 | import { SidebarInset } from "../components/ui/sidebar"; |
| 15 | import { isElectron } from "../env"; | 16 | import { isElectron } from "../env"; |
| ... | @@ -79,7 +80,7 @@ function SettingsContentLayout() { | ... | @@ -79,7 +80,7 @@ function SettingsContentLayout() { |
| 79 | )} | 80 | )} |
| 80 | > | 81 | > |
| 81 | <div className="flex w-full items-center gap-2"> | 82 | <div className="flex w-full items-center gap-2"> |
| 82 | <span className="text-sm font-medium text-foreground">Settings</span> | 83 | <SettingsBreadcrumb pathname={location.pathname} /> |
| 83 | {showRestoreDefaults ? ( | 84 | {showRestoreDefaults ? ( |
| 84 | <div className="ms-auto flex items-center gap-2"> | 85 | <div className="ms-auto flex items-center gap-2"> |
| 85 | <RestoreDefaultsButton onRestored={handleRestored} /> | 86 | <RestoreDefaultsButton onRestored={handleRestored} /> |
| ... | @@ -96,14 +97,14 @@ function SettingsContentLayout() { | ... | @@ -96,14 +97,14 @@ function SettingsContentLayout() { |
| 96 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | 97 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, |
| 97 | )} | 98 | )} |
| 98 | > | 99 | > |
| 99 | <span className="text-xs font-medium tracking-wide text-muted-foreground/70"> | 100 | <div className="flex w-full items-center gap-2"> |
| 100 | Settings | 101 | <SettingsBreadcrumb pathname={location.pathname} /> |
| 101 | </span> | 102 | {showRestoreDefaults ? ( |
| 102 | {showRestoreDefaults ? ( | 103 | <div className="ms-auto flex items-center gap-2"> |
| 103 | <div className="ms-auto flex items-center gap-2"> | 104 | <RestoreDefaultsButton onRestored={handleRestored} /> |
| 104 | <RestoreDefaultsButton onRestored={handleRestored} /> | 105 | </div> |
| 105 | </div> | 106 | ) : null} |
| 106 | ) : null} | 107 | </div> |
| 107 | </div> | 108 | </div> |
| 108 | )} | 109 | )} |
| 109 | 110 |