feat: glass ui pass
This commit is contained in:
@@ -2,7 +2,6 @@ import { useStore } from '@nanostores/react'
|
||||
import type { CSSProperties, ReactNode } from 'react'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
|
||||
import { Backdrop } from '@/components/Backdrop'
|
||||
import { PaneShell } from '@/components/pane-shell'
|
||||
import { SidebarProvider } from '@/components/ui/sidebar'
|
||||
import {
|
||||
@@ -21,9 +20,11 @@ import { TitlebarControls, type TitlebarTool } from './titlebar-controls'
|
||||
|
||||
interface AppShellProps {
|
||||
children: ReactNode
|
||||
commandCenterOpen?: boolean
|
||||
leftStatusbarItems?: readonly StatusbarItem[]
|
||||
leftTitlebarTools?: readonly TitlebarTool[]
|
||||
onOpenSettings: () => void
|
||||
onOpenSearch: () => void
|
||||
overlays?: ReactNode
|
||||
statusbarItems?: readonly StatusbarItem[]
|
||||
titlebarTools?: readonly TitlebarTool[]
|
||||
@@ -46,9 +47,11 @@ const viewportIsFullscreen = () =>
|
||||
|
||||
export function AppShell({
|
||||
children,
|
||||
commandCenterOpen = false,
|
||||
leftStatusbarItems,
|
||||
leftTitlebarTools,
|
||||
onOpenSettings,
|
||||
onOpenSearch,
|
||||
overlays,
|
||||
statusbarItems,
|
||||
titlebarTools
|
||||
@@ -70,9 +73,9 @@ export function AppShell({
|
||||
? 0
|
||||
: titlebarControls.left + TITLEBAR_HEIGHT + Math.round(TITLEBAR_HEIGHT / 2)
|
||||
|
||||
// The static system cluster (file-browser, haptics, settings) is hardcoded
|
||||
// in TitlebarControls. Pane-supplied tools (preview's group) render in a
|
||||
// separate cluster anchored further left.
|
||||
// The static system cluster (haptics, profiles, settings, right-sidebar) is
|
||||
// hardcoded in TitlebarControls. Pane-supplied tools (preview's group) render
|
||||
// in a separate cluster anchored further left.
|
||||
//
|
||||
// Width math has to include the `gap-x-1` (0.25rem) between buttons:
|
||||
// N buttons + (N - 1) inner gaps, plus one extra 0.25rem of breathing room
|
||||
@@ -105,7 +108,7 @@ export function AppShell({
|
||||
|
||||
return (
|
||||
<SidebarProvider
|
||||
className="h-screen min-h-0 bg-background"
|
||||
className="h-screen min-h-0 flex-col bg-background"
|
||||
onOpenChange={setSidebarOpen}
|
||||
open={sidebarOpen}
|
||||
style={
|
||||
@@ -127,10 +130,15 @@ export function AppShell({
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<TitlebarControls leftTools={leftTitlebarTools} onOpenSettings={onOpenSettings} tools={titlebarTools} />
|
||||
<TitlebarControls
|
||||
commandCenterOpen={commandCenterOpen}
|
||||
leftTools={leftTitlebarTools}
|
||||
onOpenSearch={onOpenSearch}
|
||||
onOpenSettings={onOpenSettings}
|
||||
tools={titlebarTools}
|
||||
/>
|
||||
|
||||
<Backdrop />
|
||||
<main className="relative z-3 flex h-screen w-full flex-col overflow-hidden pr-0.75 pt-0.75 transition-none">
|
||||
<main className="relative z-3 flex min-h-0 w-full flex-1 flex-col overflow-hidden bg-(--glass-chat-surface-background) transition-none">
|
||||
<PaneShell className="min-h-0 flex-1">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -3,16 +3,14 @@ import { useMemo } from 'react'
|
||||
|
||||
import type { CommandCenterSection } from '@/app/command-center'
|
||||
import { GatewayMenuPanel } from '@/app/shell/gateway-menu-panel'
|
||||
import { Activity, AlertCircle, Clock, Command, Cpu, FolderOpen, GitBranch, Hash, Loader2, Sparkles } from '@/lib/icons'
|
||||
import { Activity, AlertCircle, Clock, Command, Cpu, Hash, Loader2, Sparkles } from '@/lib/icons'
|
||||
import type { RuntimeReadinessResult } from '@/lib/runtime-readiness'
|
||||
import { compactPath, contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar'
|
||||
import { contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $desktopActionTasks } from '@/store/activity'
|
||||
import { $previewServerRestartStatus } from '@/store/preview'
|
||||
import {
|
||||
$busy,
|
||||
$currentBranch,
|
||||
$currentCwd,
|
||||
$currentModel,
|
||||
$currentProvider,
|
||||
$currentUsage,
|
||||
@@ -30,7 +28,6 @@ import type { StatusbarItem } from '../statusbar-controls'
|
||||
|
||||
interface StatusbarItemsOptions {
|
||||
agentsOpen: boolean
|
||||
browseSessionCwd: () => Promise<void>
|
||||
commandCenterOpen: boolean
|
||||
extraLeftItems: readonly StatusbarItem[]
|
||||
extraRightItems: readonly StatusbarItem[]
|
||||
@@ -45,7 +42,6 @@ interface StatusbarItemsOptions {
|
||||
|
||||
export function useStatusbarItems({
|
||||
agentsOpen,
|
||||
browseSessionCwd,
|
||||
commandCenterOpen,
|
||||
extraLeftItems,
|
||||
extraRightItems,
|
||||
@@ -58,8 +54,6 @@ export function useStatusbarItems({
|
||||
toggleCommandCenter
|
||||
}: StatusbarItemsOptions) {
|
||||
const busy = useStore($busy)
|
||||
const currentBranch = useStore($currentBranch)
|
||||
const currentCwd = useStore($currentCwd)
|
||||
const currentModel = useStore($currentModel)
|
||||
const currentProvider = useStore($currentProvider)
|
||||
const currentUsage = useStore($currentUsage)
|
||||
@@ -79,10 +73,10 @@ export function useStatusbarItems({
|
||||
const gatewayMenuContent = useMemo(
|
||||
() => (
|
||||
<GatewayMenuPanel
|
||||
logLines={gatewayLogLines}
|
||||
onOpenSystem={() => openCommandCenterSection('system')}
|
||||
gatewayState={gatewayState}
|
||||
inferenceStatus={inferenceStatus}
|
||||
logLines={gatewayLogLines}
|
||||
onOpenSystem={() => openCommandCenterSection('system')}
|
||||
statusSnapshot={statusSnapshot}
|
||||
/>
|
||||
),
|
||||
@@ -108,6 +102,7 @@ export function useStatusbarItems({
|
||||
const gatewayConnecting = gatewayState === 'connecting'
|
||||
const inferenceReady = gatewayOpen && inferenceStatus?.ready === true
|
||||
const gatewayDegraded = gatewayOpen || gatewayConnecting
|
||||
|
||||
const gatewayDetail = gatewayOpen
|
||||
? inferenceStatus?.ready
|
||||
? 'ready'
|
||||
@@ -117,6 +112,7 @@ export function useStatusbarItems({
|
||||
: gatewayConnecting
|
||||
? 'connecting'
|
||||
: 'offline'
|
||||
|
||||
const gatewayClassName = inferenceReady
|
||||
? undefined
|
||||
: gatewayDegraded
|
||||
@@ -277,31 +273,12 @@ export function useStatusbarItems({
|
||||
title: currentProvider ? `Switch model · ${currentProvider}: ${currentModel || ''}` : 'Open model picker',
|
||||
variant: 'action'
|
||||
},
|
||||
{
|
||||
icon: <FolderOpen className="size-3" />,
|
||||
id: 'cwd',
|
||||
label: currentCwd ? compactPath(currentCwd) : 'No project cwd',
|
||||
onSelect: () => void browseSessionCwd(),
|
||||
title: currentCwd ? `Change working directory · ${currentCwd}` : 'Choose working directory',
|
||||
variant: 'action'
|
||||
},
|
||||
{
|
||||
hidden: !currentBranch,
|
||||
icon: <GitBranch className="size-3" />,
|
||||
id: 'branch',
|
||||
label: currentBranch,
|
||||
title: currentBranch ? `Current branch: ${currentBranch}` : undefined,
|
||||
variant: 'text'
|
||||
},
|
||||
versionItem
|
||||
],
|
||||
[
|
||||
browseSessionCwd,
|
||||
busy,
|
||||
contextBar,
|
||||
contextUsage,
|
||||
currentBranch,
|
||||
currentCwd,
|
||||
currentModel,
|
||||
currentProvider,
|
||||
sessionStartedAt,
|
||||
|
||||
@@ -2,20 +2,17 @@ import type * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface SidebarPanelLabelProps extends React.ComponentProps<'span'> {
|
||||
dotClassName?: string
|
||||
}
|
||||
type SidebarPanelLabelProps = React.ComponentProps<'span'>
|
||||
|
||||
export function SidebarPanelLabel({ children, className, dotClassName, ...props }: SidebarPanelLabelProps) {
|
||||
export function SidebarPanelLabel({ children, className, ...props }: SidebarPanelLabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'flex min-w-0 items-center gap-2 text-[0.64rem] font-semibold uppercase tracking-[0.16em] text-sidebar-foreground/72',
|
||||
'flex min-w-0 items-center gap-1.5 text-[0.6875rem] font-medium text-sidebar-foreground/55',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span aria-hidden="true" className={cn('dither inline-block size-2 shrink-0 rounded-[1px]', dotClassName)} />
|
||||
<span className="min-w-0 truncate leading-none">{children}</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ export function StatusbarControls({ className, leftItems = [], items = [], ...pr
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
'flex h-7 shrink-0 items-stretch justify-between gap-2 border-t border-border/55 bg-[color-mix(in_srgb,var(--dt-muted)_45%,var(--dt-card))] px-1 py-0 text-muted-foreground/95 [-webkit-app-region:no-drag]',
|
||||
'flex h-5 shrink-0 items-stretch justify-between gap-2 border-t border-(--ui-stroke-tertiary) bg-(--glass-sidebar-surface-background) px-1 py-0 text-(--ui-text-tertiary) [-webkit-app-region:no-drag]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -89,7 +89,7 @@ function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate:
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex h-full cursor-pointer items-center gap-1 rounded-none px-1.5 text-[0.68rem] text-muted-foreground/95 transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground disabled:cursor-default disabled:opacity-45',
|
||||
'inline-flex h-full cursor-pointer items-center gap-1 rounded-none px-1.5 text-[0.6875rem] text-(--ui-text-tertiary) transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground disabled:cursor-default disabled:opacity-45',
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
@@ -150,7 +150,7 @@ function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate:
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex h-full items-center gap-1 px-1.5 text-[0.68rem] text-muted-foreground/90',
|
||||
'inline-flex h-full items-center gap-1 px-1.5 text-[0.6875rem] text-(--ui-text-tertiary)',
|
||||
item.className
|
||||
)}
|
||||
>
|
||||
@@ -163,7 +163,7 @@ function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate:
|
||||
return (
|
||||
<a
|
||||
className={cn(
|
||||
'inline-flex h-full cursor-pointer items-center gap-1 rounded-none px-1.5 text-[0.68rem] text-muted-foreground/95 transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground disabled:cursor-default disabled:opacity-45',
|
||||
'inline-flex h-full cursor-pointer items-center gap-1 rounded-none px-1.5 text-[0.6875rem] text-(--ui-text-tertiary) transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground disabled:cursor-default disabled:opacity-45',
|
||||
item.className
|
||||
)}
|
||||
href={item.href}
|
||||
@@ -179,7 +179,7 @@ function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate:
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex h-full cursor-pointer items-center gap-1 rounded-none px-1.5 text-[0.68rem] text-muted-foreground/95 transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground disabled:cursor-default disabled:opacity-45',
|
||||
'inline-flex h-full cursor-pointer items-center gap-1 rounded-none px-1.5 text-[0.6875rem] text-(--ui-text-tertiary) transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground disabled:cursor-default disabled:opacity-45',
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react'
|
||||
import type { ComponentProps, ReactNode } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { triggerHaptic } from '@/lib/haptics'
|
||||
import { FolderOpen, NotebookTabs, Settings, Users, Volume2, VolumeX } from '@/lib/icons'
|
||||
import { Volume2, VolumeX } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $hapticsMuted, toggleHapticsMuted } from '@/store/haptics'
|
||||
import { $fileBrowserOpen, $sidebarOpen, toggleFileBrowserOpen, toggleSidebarOpen } from '@/store/layout'
|
||||
@@ -40,10 +41,18 @@ export type SetTitlebarToolGroup = (id: string, tools: readonly TitlebarTool[],
|
||||
interface TitlebarControlsProps extends ComponentProps<'div'> {
|
||||
leftTools?: readonly TitlebarTool[]
|
||||
tools?: readonly TitlebarTool[]
|
||||
commandCenterOpen?: boolean
|
||||
onOpenSettings: () => void
|
||||
onOpenSearch: () => void
|
||||
}
|
||||
|
||||
export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: TitlebarControlsProps) {
|
||||
export function TitlebarControls({
|
||||
leftTools = [],
|
||||
tools = [],
|
||||
commandCenterOpen = false,
|
||||
onOpenSettings,
|
||||
onOpenSearch
|
||||
}: TitlebarControlsProps) {
|
||||
const navigate = useNavigate()
|
||||
const hapticsMuted = useStore($hapticsMuted)
|
||||
const fileBrowserOpen = useStore($fileBrowserOpen)
|
||||
@@ -63,7 +72,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
|
||||
|
||||
const leftToolbarTools: TitlebarTool[] = [
|
||||
{
|
||||
icon: <NotebookTabs />,
|
||||
icon: <Codicon name="layout-sidebar-left" />,
|
||||
id: 'sidebar',
|
||||
label: sidebarOpen ? 'Hide sidebar' : 'Show sidebar',
|
||||
onSelect: () => {
|
||||
@@ -71,21 +80,33 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
|
||||
toggleSidebarOpen()
|
||||
}
|
||||
},
|
||||
{
|
||||
active: commandCenterOpen,
|
||||
icon: <Codicon name="search" />,
|
||||
id: 'search',
|
||||
label: 'Search',
|
||||
onSelect: () => {
|
||||
triggerHaptic('open')
|
||||
onOpenSearch()
|
||||
},
|
||||
title: 'Search sessions, views, and actions'
|
||||
},
|
||||
...leftTools
|
||||
]
|
||||
|
||||
const rightSidebarTool: TitlebarTool = {
|
||||
active: fileBrowserOpen,
|
||||
icon: <Codicon name="layout-sidebar-right" />,
|
||||
id: 'right-sidebar',
|
||||
label: fileBrowserOpen ? 'Hide right sidebar' : 'Show right sidebar',
|
||||
onSelect: () => {
|
||||
triggerHaptic('tap')
|
||||
toggleFileBrowserOpen()
|
||||
}
|
||||
}
|
||||
|
||||
// Static system tools — always pinned to the screen's right edge.
|
||||
const systemTools: TitlebarTool[] = [
|
||||
{
|
||||
active: fileBrowserOpen,
|
||||
icon: <FolderOpen />,
|
||||
id: 'file-browser',
|
||||
label: fileBrowserOpen ? 'Hide file browser' : 'Show file browser',
|
||||
onSelect: () => {
|
||||
triggerHaptic('tap')
|
||||
toggleFileBrowserOpen()
|
||||
}
|
||||
},
|
||||
{
|
||||
active: hapticsMuted,
|
||||
icon: hapticsMuted ? <VolumeX /> : <Volume2 />,
|
||||
@@ -94,7 +115,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
|
||||
onSelect: toggleHaptics
|
||||
},
|
||||
{
|
||||
icon: <Settings />,
|
||||
icon: <Codicon name="settings-gear" />,
|
||||
id: 'settings',
|
||||
label: 'Open settings',
|
||||
onSelect: () => {
|
||||
@@ -113,7 +134,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
|
||||
<>
|
||||
<div
|
||||
aria-label="Window controls"
|
||||
className="fixed left-(--titlebar-controls-left) top-(--titlebar-controls-top) z-70 flex translate-y-[2px] flex-row items-center gap-x-1 pointer-events-auto select-none [-webkit-app-region:no-drag]"
|
||||
className="fixed left-(--titlebar-controls-left) top-(--titlebar-controls-top) z-70 flex translate-y-0.5 flex-row items-center gap-x-1 pointer-events-auto select-none [-webkit-app-region:no-drag]"
|
||||
>
|
||||
{leftToolbarTools
|
||||
.filter(tool => !tool.hidden)
|
||||
@@ -150,6 +171,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
|
||||
))}
|
||||
<ProfilesMenuButton navigate={navigate} />
|
||||
{settingsTool && <TitlebarToolButton navigate={navigate} tool={settingsTool} />}
|
||||
<TitlebarToolButton navigate={navigate} tool={rightSidebarTool} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@@ -169,7 +191,7 @@ function ProfilesMenuButton({ navigate }: { navigate: ReturnType<typeof useNavig
|
||||
title="Profiles"
|
||||
type="button"
|
||||
>
|
||||
<Users />
|
||||
<Codicon name="account" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64" sideOffset={8}>
|
||||
@@ -186,7 +208,7 @@ function ProfilesMenuButton({ navigate }: { navigate: ReturnType<typeof useNavig
|
||||
navigate(PROFILES_ROUTE)
|
||||
}}
|
||||
>
|
||||
<Users className="size-4" />
|
||||
<Codicon name="account" size="1rem" />
|
||||
<span>Manage profiles</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -198,6 +220,7 @@ function TitlebarToolButton({ navigate, tool }: { navigate: ReturnType<typeof us
|
||||
const className = cn(
|
||||
titlebarButtonClass,
|
||||
'grid place-items-center bg-transparent select-none [&_svg]:size-4',
|
||||
tool.active && 'bg-(--ui-bg-tertiary)! text-foreground!',
|
||||
tool.className
|
||||
)
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ export const TITLEBAR_FALLBACK_WINDOW_BUTTON_X = 24
|
||||
export const TITLEBAR_EDGE_INSET = 14
|
||||
|
||||
export const titlebarButtonClass =
|
||||
'h-[var(--titlebar-control-height)] w-[var(--titlebar-control-size)] rounded-md text-muted-foreground hover:bg-accent hover:text-foreground'
|
||||
'h-[var(--titlebar-control-height)] w-[var(--titlebar-control-size)] rounded-md text-muted-foreground/85 transition-colors hover:bg-(--ui-bg-tertiary) hover:text-foreground'
|
||||
|
||||
export const titlebarHeaderBaseClass =
|
||||
'pointer-events-none relative z-3 flex h-(--titlebar-height) shrink-0 items-center justify-start gap-3 bg-background/70 px-[max(0.75rem,var(--titlebar-content-inset,0px))] backdrop-blur-sm'
|
||||
'pointer-events-none relative z-3 flex h-(--titlebar-height) shrink-0 items-center justify-start gap-3 border-b border-(--ui-stroke-tertiary) bg-(--glass-chat-surface-background) px-[max(0.75rem,var(--titlebar-content-inset,0rem))]'
|
||||
|
||||
export const titlebarHeaderShadowClass =
|
||||
"shadow-header after:pointer-events-none after:absolute after:left-0 after:right-0 after:top-full after:h-10 after:bg-linear-to-b after:from-background after:via-background/80 after:to-transparent after:content-['']"
|
||||
"after:pointer-events-none after:absolute after:left-0 after:right-0 after:top-full after:h-4 after:bg-linear-to-b after:from-(--glass-chat-surface-background) after:to-transparent after:content-['']"
|
||||
|
||||
export function titlebarControlsPosition(
|
||||
windowButtonPosition: HermesConnection['windowButtonPosition'] | undefined,
|
||||
|
||||
Reference in New Issue
Block a user