style(desktop): bring the install & update overlays onto the design system

Align the first-launch install overlay and the in-app update overlay to
apps/desktop/DESIGN.md, reusing in-bundle primitives only (no new deps):

- install overlay: Loader2 spinners -> Loader (fourier-flow); emerald check /
  AlertTriangle -> neutral Codicon check + canonical ErrorIcon; de-boxed the
  failure block; hairline (--stroke-nous) command/code chips; --ui-* tokens;
  BrandMark header; flat stage rows (only the active step opaque).
- update overlay: drop the redundant DialogContent border (base Dialog already
  supplies shadow-nous + --stroke-nous); de-box the changelog; hairline +
  primary-flash manual command block; on-brand BrandMark for "all set".
This commit is contained in:
Brooklyn Nicholson 2026-06-30 14:46:28 -05:00
parent 9f8de4dfbe
commit 8fe8c2d6c4
3 changed files with 68 additions and 76 deletions

View file

@ -10,7 +10,7 @@ import { Loader } from '@/components/ui/loader'
import type { DesktopUpdateCommit, DesktopUpdateStage, DesktopUpdateStatus } from '@/global'
import { useI18n } from '@/i18n'
import { buildCommitChangelog, type CommitGroup } from '@/lib/commit-changelog'
import { AlertCircle, Check, CheckCircle2, Copy, Terminal } from '@/lib/icons'
import { AlertCircle, Check, Copy, Terminal } from '@/lib/icons'
import { resolveUpdateCopy, type UpdateTarget } from '@/lib/update-copy'
import { cn } from '@/lib/utils'
import {
@ -94,10 +94,7 @@ export function UpdatesOverlay() {
return (
<Dialog onOpenChange={handleClose} open={open}>
<DialogContent
className="max-w-sm overflow-hidden border-border/70 p-0 gap-0"
showCloseButton={phase !== 'applying'}
>
<DialogContent className="max-w-sm overflow-hidden p-0 gap-0" showCloseButton={phase !== 'applying'}>
{phase === 'applying' && <ApplyingView apply={apply} isBackend={isBackend} />}
{phase === 'manual' && (
@ -204,7 +201,7 @@ function IdleView({
return (
<CenteredStatus
body={target === 'backend' ? u.latestBodyBackend : u.latestBody}
icon={<CheckCircle2 className="size-7 text-emerald-600 dark:text-emerald-400" />}
icon={<BrandMark className="size-12" />}
title={u.allSetTitle}
/>
)
@ -229,7 +226,7 @@ function IdleView({
<DialogDescription className="text-center text-sm">{body}</DialogDescription>
</div>
<div className="grid gap-3 rounded-xl border border-border/70 bg-muted/20 px-4 py-3">
<div className="grid gap-3">
{groups.map(group => (
<div key={group.id}>
<p className="text-[0.625rem] font-semibold uppercase tracking-wide text-muted-foreground">{group.label}</p>
@ -304,26 +301,25 @@ function ManualView({ command, message, onDone }: { command: string | null; mess
</div>
<button
className="group flex w-full items-center justify-between gap-3 rounded-xl border border-border/70 bg-muted/30 px-4 py-3 text-left transition-colors hover:border-border hover:bg-muted/50"
className={cn(
'group flex w-full items-center justify-between gap-3 rounded-md border px-4 py-3 text-left transition-colors',
copied ? 'border-primary/50' : 'border-(--stroke-nous) hover:border-(--ui-stroke-secondary)'
)}
onClick={handleCopy}
type="button"
>
<code className="select-all font-mono text-sm text-foreground">
<span className="text-muted-foreground">$ </span>
<code className="min-w-0 flex-1 truncate select-all font-mono text-sm text-foreground">
<span className="select-none text-muted-foreground">$ </span>
{command}
</code>
<span className="flex shrink-0 items-center gap-1 text-xs font-medium text-muted-foreground transition-colors group-hover:text-foreground">
{copied ? (
<>
<Check className="size-3.5 text-emerald-600 dark:text-emerald-400" />
{u.copied}
</>
) : (
<>
<Copy className="size-3.5" />
{u.copy}
</>
<span
className={cn(
'flex shrink-0 items-center gap-1 text-xs font-medium transition-colors',
copied ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground'
)}
>
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
{copied ? u.copied : u.copy}
</span>
</button>

View file

@ -1,6 +1,9 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { BrandMark } from '@/components/brand-mark'
import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { ErrorIcon } from '@/components/ui/error-state'
import { Loader } from '@/components/ui/loader'
import { LogView } from '@/components/ui/log-view'
import type {
@ -11,7 +14,7 @@ import type {
DesktopBootstrapState
} from '@/global'
import { useI18n } from '@/i18n'
import { AlertTriangle, Check, ChevronDown, ChevronRight, iconSize, Loader2 } from '@/lib/icons'
import { ChevronDown, ChevronRight, iconSize } from '@/lib/icons'
import { cn } from '@/lib/utils'
/**
@ -48,7 +51,6 @@ interface DesktopInstallOverlayProps {
interface StageRowProps {
descriptor: DesktopBootstrapStageDescriptor
result: DesktopBootstrapStageResult | undefined
isCurrent: boolean
now: number
}
@ -98,7 +100,7 @@ function formatElapsed(ms: number): string {
return `${m}:${String(s - m * 60).padStart(2, '0')}`
}
function StageRow({ descriptor, result, isCurrent, now }: StageRowProps) {
function StageRow({ descriptor, result, now }: StageRowProps) {
const { t } = useI18n()
const copy = t.install
const state: DesktopBootstrapStageState = result?.state || 'pending'
@ -109,52 +111,48 @@ function StageRow({ descriptor, result, isCurrent, now }: StageRowProps) {
const icon = useMemo(() => {
switch (state) {
case 'running':
return <Loader2 className={cn(iconSize.md, 'animate-spin text-primary')} />
return <Loader className="size-6" type="fourier-flow" />
case 'succeeded':
return <Check className={cn(iconSize.md, 'text-emerald-600')} />
case 'skipped':
return <Check className={cn(iconSize.md, 'text-muted-foreground')} />
return <Codicon className="text-muted-foreground" name="check" size="0.8125rem" />
case 'failed':
return <AlertTriangle className={cn(iconSize.md, 'text-destructive')} />
return <ErrorIcon size="1rem" />
case 'pending':
default:
return <div className="h-2 w-2 rounded-full border border-muted-foreground/40" />
return <div className="size-1.5 rounded-full border border-(--ui-stroke-secondary)" />
}
}, [state])
const reason = result?.json?.reason || result?.error || null
return (
<li
className={cn(
'flex items-start gap-3 rounded-md px-3 py-2 transition-colors',
isCurrent && 'bg-muted/60',
state === 'failed' && 'bg-destructive/10'
<li className="flex items-center gap-3 px-3 py-1">
{state === 'running' && (
<div className="-mr-2 -ml-4 flex size-6 flex-shrink-0 items-center justify-center">{icon}</div>
)}
>
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center">{icon}</div>
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-2">
<span className={cn('truncate text-sm font-medium', state === 'pending' && 'text-muted-foreground')}>
<div className="flex items-center gap-1.5">
<span className={cn('truncate text-sm', state === 'running' ? 'font-medium' : 'text-muted-foreground')}>
{formatStageName(descriptor.name)}
</span>
<span className="flex-shrink-0 text-xs tabular-nums text-muted-foreground">
{state === 'running'
? elapsed
? `${copy.stageStates[state]} · ${elapsed}`
: copy.stageStates[state]
: null}
{state === 'succeeded' || state === 'skipped' ? formatDuration(result?.durationMs) : null}
{state === 'failed' ? copy.stageStates[state] : null}
</span>
{state !== 'running' && <span className="flex size-4 shrink-0 items-center justify-center">{icon}</span>}
</div>
{reason && state !== 'pending' && <p className="mt-0.5 truncate text-xs text-muted-foreground">{reason}</p>}
</div>
<span className="flex-shrink-0 text-xs tabular-nums text-muted-foreground">
{state === 'running'
? elapsed
? `${copy.stageStates[state]} · ${elapsed}`
: copy.stageStates[state]
: null}
{state === 'succeeded' || state === 'skipped' ? formatDuration(result?.durationMs) : null}
{state === 'failed' ? copy.stageStates[state] : null}
</span>
</li>
)
}
@ -245,6 +243,7 @@ function applyEvent(state: DesktopBootstrapState, ev: DesktopBootstrapEvent): De
export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayProps) {
const { t } = useI18n()
const copy = t.install
const [state, setState] = useState<DesktopBootstrapState>(EMPTY_STATE)
const [logOpen, setLogOpen] = useState(false)
const [copied, setCopied] = useState(false)
@ -353,12 +352,12 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
return (
<div className="fixed inset-0 z-[1400] flex items-center justify-center bg-background/90 backdrop-blur-md">
<div className="w-full max-w-xl rounded-xl border border-(--stroke-nous) bg-card p-8 shadow-nous">
<h2 className="text-2xl font-semibold tracking-tight">{copy.oneTimeTitle}</h2>
<h2 className="text-xl font-semibold tracking-tight">{copy.oneTimeTitle}</h2>
<p className="mt-2 text-sm text-muted-foreground">{copy.unsupportedDesc(platformLabel)}</p>
<div className="mt-4">
<div className="mb-1.5 text-xs font-medium text-muted-foreground">{copy.installCommand}</div>
<pre className="overflow-x-auto rounded-md border bg-muted/50 px-3 py-2.5 font-mono text-[12px]">
<pre className="overflow-x-auto rounded-md border border-(--stroke-nous) px-3 py-2.5 font-mono text-[12px]">
<code>{ups.installCommand}</code>
</pre>
<div className="mt-2 flex items-center gap-2">
@ -383,9 +382,9 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
</div>
</div>
<div className="mt-6 flex items-center justify-between border-t pt-4">
<div className="mt-6 flex items-center justify-between pt-2">
<span className="text-xs text-muted-foreground">
{copy.installTo} <code className="rounded bg-muted/50 px-1 py-0.5 font-mono">{ups.activeRoot}</code>
{copy.installTo} <code className="font-mono text-(--ui-text-secondary)">{ups.activeRoot}</code>
</span>
<Button onClick={() => window.location.reload()} size="sm" variant="default">
{copy.retryAfterRun}
@ -417,11 +416,14 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
<div className="fixed inset-0 z-[1400] flex items-center justify-center bg-background/90 backdrop-blur-md p-4">
<div className="flex w-full max-w-2xl max-h-[90vh] flex-col rounded-xl border border-(--stroke-nous) bg-card shadow-nous">
{/* Header -- always visible, never scrolls */}
<div className="flex-shrink-0 p-8 pb-4">
<h2 className="text-2xl font-semibold tracking-tight">
{failed ? copy.failedTitle : state.active ? copy.settingUpTitle : copy.finishingTitle}
</h2>
<p className="mt-1.5 text-sm text-muted-foreground">{failed ? copy.failedDesc : copy.activeDesc}</p>
<div className="flex flex-shrink-0 items-start gap-4 p-8 pb-4">
{!failed && <BrandMark className="size-11 shrink-0" />}
<div className="min-w-0">
<h2 className="text-xl font-semibold tracking-tight">
{failed ? copy.failedTitle : state.active ? copy.settingUpTitle : copy.finishingTitle}
</h2>
<p className="mt-1.5 text-sm text-muted-foreground">{failed ? copy.failedDesc : copy.activeDesc}</p>
</div>
</div>
{/* Scrollable middle: progress, stages, error block, log */}
@ -436,7 +438,7 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
</span>
<span className="tabular-nums">{progressPct}%</span>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div className="h-1.5 w-full overflow-hidden rounded-full bg-(--ui-bg-tertiary)">
<div
className={cn('h-full transition-all duration-300', failed ? 'bg-destructive' : 'bg-primary')}
style={{ width: `${progressPct}%` }}
@ -447,31 +449,25 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
{totalCount === 0 && state.active && (
<div className="mb-4 flex items-center gap-2.5 text-sm text-muted-foreground">
<Loader className="size-5" type="lemniscate-bloom" />
<Loader className="size-5" type="fourier-flow" />
<span>{copy.fetchingManifest}</span>
</div>
)}
{failed && state.error && (
<div className="mb-4 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm">
<div className="mb-1 flex items-center gap-1.5 font-medium text-destructive">
<AlertTriangle className={iconSize.md} />
<span>{copy.error}</span>
<div className="mb-4 flex items-start gap-2 text-sm">
<ErrorIcon className="mt-0.5 shrink-0" size="1rem" />
<div className="min-w-0">
<div className="font-medium text-destructive">{copy.error}</div>
<p className="mt-0.5 whitespace-pre-wrap break-words text-foreground/90">{state.error}</p>
</div>
<p className="whitespace-pre-wrap break-words text-foreground/90">{state.error}</p>
</div>
)}
{stages.length > 0 && (
<ol className="mb-4 space-y-1">
<ol className="mb-4 space-y-0.5">
{stages.map(stage => (
<StageRow
descriptor={stage}
isCurrent={stage.name === currentStage}
key={stage.name}
now={now}
result={state.stages[stage.name]}
/>
<StageRow descriptor={stage} key={stage.name} now={now} result={state.stages[stage.name]} />
))}
</ol>
)}
@ -527,7 +523,7 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
size="sm"
variant="ghost"
>
{cancelling ? <Loader2 className={cn(iconSize.md, 'animate-spin')} /> : null}
{cancelling ? <Loader className="size-4" type="fourier-flow" /> : null}
{cancelling ? copy.cancelling : copy.cancelInstall}
</Button>
</div>
@ -540,7 +536,7 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-muted-foreground">
{copy.transcriptSaved}{' '}
<code className="rounded bg-muted/50 px-1 py-0.5 font-mono">%LOCALAPPDATA%\hermes\logs\</code>
<code className="font-mono text-(--ui-text-secondary)">%LOCALAPPDATA%\hermes\logs\</code>
</span>
<div className="flex gap-2">
<Button

View file

@ -294,10 +294,10 @@ const LOADER_CURVES: Record<LoaderType, LoaderCurve> = {
},
'fourier-flow': {
...baseCurve('Fourier Flow', {
durationMs: 8400,
durationMs: 2200,
particleCount: 92,
pulseDurationMs: 6800,
rotationDurationMs: 44000,
pulseDurationMs: 2000,
rotationDurationMs: 15000,
strokeWidth: 4.2,
trailSpan: 0.31
}),