feat: better icons and overlay panes

This commit is contained in:
Brooklyn Nicholson
2026-05-04 14:20:18 -05:00
parent ca8f2c7907
commit d1d0ed4016
71 changed files with 2043 additions and 363 deletions
@@ -1,6 +1,5 @@
import { Check, Palette } from 'lucide-react'
import { triggerHaptic } from '@/lib/haptics'
import { Check, Palette } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { useTheme } from '@/themes/context'
import { BUILTIN_THEMES } from '@/themes/presets'
+1 -2
View File
@@ -10,8 +10,7 @@ import {
Sparkles,
Sun,
Wrench
} from 'lucide-react'
} from '@/lib/icons'
import type { ThemeMode } from '@/themes/context'
import type { DesktopConfigSection } from './types'
+40 -77
View File
@@ -1,17 +1,20 @@
import { Download, KeyRound, Package, RotateCcw, Search, Upload, X } from 'lucide-react'
import { IconDownload, IconRefresh, IconUpload } from '@tabler/icons-react'
import { useEffect, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { getHermesConfigDefaults, getHermesConfigRecord, saveHermesConfig } from '@/hermes'
import { triggerHaptic } from '@/lib/haptics'
import { KeyRound, Package } from '@/lib/icons'
import { notifyError } from '@/store/notifications'
import { OverlayIconButton } from '../overlays/overlay-chrome'
import { OverlaySearchInput } from '../overlays/overlay-search-input'
import { OverlayMain, OverlayNavItem, OverlaySidebar, OverlaySplitLayout } from '../overlays/overlay-split-layout'
import { OverlayView } from '../overlays/overlay-view'
import { AppearanceSettings } from './appearance-settings'
import { ConfigSettings } from './config-settings'
import { SEARCH_PLACEHOLDER, SECTIONS } from './constants'
import { KeysSettings } from './keys-settings'
import { NavLink } from './primitives'
import { ToolsSettings } from './tools-settings'
import type { SettingsPageProps, SettingsQueryKey, SettingsView as SettingsViewId } from './types'
@@ -84,55 +87,26 @@ export function SettingsView({ onClose, onConfigSaved }: SettingsPageProps) {
}, [onClose])
return (
<div className="fixed inset-0 z-60 flex min-h-0 flex-col bg-background/98 p-0.75 backdrop-blur-xl">
<div className="pointer-events-none fixed inset-x-0 top-0 z-10 h-[calc(var(--titlebar-height)+0.1875rem)] [-webkit-app-region:drag]">
<div className="pointer-events-auto absolute left-1/2 top-[calc(1rem+var(--titlebar-height)/2)] w-[min(36rem,calc(100vw-32rem))] min-w-80 -translate-x-1/2 -translate-y-1/2 [-webkit-app-region:no-drag]">
<Search className="pointer-events-none absolute left-3 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground/80" />
<Input
className="h-9 rounded-full border-transparent bg-background py-2 pl-8 pr-20 text-sm shadow-header focus-visible:bg-background"
onChange={e => setQuery(e.target.value)}
placeholder={SEARCH_PLACEHOLDER[queryKey]}
ref={searchInputRef}
value={query}
/>
{query ? (
<Button
aria-label="Clear search"
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setQuery('')}
size="icon-xs"
variant="ghost"
>
<X className="size-3.5" />
</Button>
) : (
<span className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded-md bg-background/80 px-1.5 py-0.5 text-[0.62rem] leading-none text-muted-foreground shadow-xs">
Cmd P
</span>
)}
</div>
<Button
aria-label="Close settings"
className="pointer-events-auto absolute right-3.75 top-[calc(0.1875rem+var(--titlebar-height)/2)] h-7 w-7 -translate-y-1/2 rounded-lg text-muted-foreground hover:bg-accent/70 hover:text-foreground [-webkit-app-region:no-drag]"
onClick={() => {
triggerHaptic('close')
onClose()
}}
size="icon"
variant="ghost"
>
<X size={16} />
</Button>
</div>
<div className="grid min-h-0 flex-1 grid-cols-[13rem_minmax(0,1fr)] rounded-[1.0625rem] bg-background/90 pt-(--titlebar-height) max-[760px]:grid-cols-1">
<aside className="flex min-h-0 flex-col gap-0.5 overflow-y-auto bg-muted/20 px-4 py-5">
<OverlayView
closeLabel="Close settings"
headerContent={
<OverlaySearchInput
containerClassName="w-[min(36rem,calc(100vw-32rem))] min-w-80"
inputRef={searchInputRef}
onChange={setQuery}
placeholder={SEARCH_PLACEHOLDER[queryKey]}
value={query}
/>
}
onClose={onClose}
>
<OverlaySplitLayout>
<OverlaySidebar>
{SECTIONS.map(s => {
const view = `config:${s.id}` as SettingsViewId
return (
<NavLink
<OverlayNavItem
active={activeView === view && !queries.config.trim()}
icon={s.icon}
key={s.id}
@@ -142,56 +116,45 @@ export function SettingsView({ onClose, onConfigSaved }: SettingsPageProps) {
)
})}
<div className="my-2 h-px bg-border/30" />
<NavLink
<OverlayNavItem
active={activeView === 'keys'}
icon={KeyRound}
label="API Keys"
onClick={() => setActiveView('keys')}
/>
<NavLink
<OverlayNavItem
active={activeView === 'tools'}
icon={Package}
label="Skills & Tools"
onClick={() => setActiveView('tools')}
/>
<div className="mt-auto flex items-center gap-1 pt-2">
<Button
className="text-muted-foreground"
onClick={() => void exportConfig()}
size="icon-xs"
title="Export config"
variant="ghost"
>
<Download />
</Button>
<Button
className="text-muted-foreground"
<OverlayIconButton onClick={() => void exportConfig()} title="Export config">
<IconDownload className="size-3.5" />
</OverlayIconButton>
<OverlayIconButton
onClick={() => {
triggerHaptic('open')
importInputRef.current?.click()
}}
size="icon-xs"
title="Import config"
variant="ghost"
>
<Upload />
</Button>
<Button
className="text-muted-foreground"
<IconUpload className="size-3.5" />
</OverlayIconButton>
<OverlayIconButton
className="hover:text-destructive"
onClick={() => {
triggerHaptic('warning')
void resetConfig()
}}
size="icon-xs"
title="Reset to defaults"
variant="ghost"
>
<RotateCcw />
</Button>
<IconRefresh className="size-3.5" />
</OverlayIconButton>
</div>
</aside>
</OverlaySidebar>
<main className="flex min-h-0 flex-1 flex-col overflow-hidden">
<OverlayMain className="p-0">
{activeView === 'config:appearance' ? (
<AppearanceSettings />
) : activeView.startsWith('config:') ? (
@@ -206,9 +169,9 @@ export function SettingsView({ onClose, onConfigSaved }: SettingsPageProps) {
) : (
<ToolsSettings query={queries.tools} />
)}
</main>
</div>
</div>
</OverlayMain>
</OverlaySplitLayout>
</OverlayView>
)
}
@@ -1,9 +1,9 @@
import { Check, Eye, EyeOff, Save, Settings2, Trash2, X, Zap } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { deleteEnvVar, getEnvVars, revealEnvVar, setEnvVar } from '@/hermes'
import { Check, Eye, EyeOff, Save, Settings2, Trash2, X, Zap } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { notify, notifyError } from '@/store/notifications'
import type { EnvVarInfo } from '@/types/hermes'
@@ -21,6 +21,8 @@ import {
import { LoadingState, Pill, SectionHeading, SettingsContent } from './primitives'
import type { EnvPatch, EnvRowProps, ProviderGroup, SearchProps } from './types'
const SHOW_ADVANCED_STORAGE_KEY = 'desktop.settings.keys.show_advanced'
interface EnvActionsProps {
varKey: string
info: EnvVarInfo
@@ -218,7 +220,28 @@ export function KeysSettings({ query }: SearchProps) {
const [edits, setEdits] = useState<Record<string, string>>({})
const [revealed, setRevealed] = useState<Record<string, string>>({})
const [saving, setSaving] = useState<string | null>(null)
const [showAdvanced, setShowAdvanced] = useState(true)
const [showAdvanced, setShowAdvanced] = useState<boolean>(() => {
try {
const stored = window.localStorage.getItem(SHOW_ADVANCED_STORAGE_KEY)
if (stored === null) {
return false
}
return stored === 'true'
} catch {
return false
}
})
useEffect(() => {
try {
window.localStorage.setItem(SHOW_ADVANCED_STORAGE_KEY, showAdvanced ? 'true' : 'false')
} catch {
// Ignore persistence failures and keep in-memory preference.
}
}, [showAdvanced])
useEffect(() => {
let cancelled = false
+1 -1
View File
@@ -1,8 +1,8 @@
import type { LucideIcon } from 'lucide-react'
import type { ReactNode } from 'react'
import { PageLoader } from '@/components/page-loader'
import { Button } from '@/components/ui/button'
import type { LucideIcon } from '@/lib/icons'
import { cn } from '@/lib/utils'
export function SettingsContent({ children }: { children: ReactNode }) {
@@ -1,8 +1,8 @@
import { Brain, Wrench } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { Switch } from '@/components/ui/switch'
import { getSkills, getToolsets, toggleSkill } from '@/hermes'
import { Brain, Wrench } from '@/lib/icons'
import { notify, notifyError } from '@/store/notifications'
import type { SkillInfo, ToolsetInfo } from '@/types/hermes'
+1 -1
View File
@@ -1,6 +1,6 @@
import type { LucideIcon } from 'lucide-react'
import type { Dispatch, SetStateAction } from 'react'
import type { LucideIcon } from '@/lib/icons'
import type { EnvVarInfo } from '@/types/hermes'
export type SettingsView = 'keys' | 'tools' | `config:${string}`