mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
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:
parent
9f8de4dfbe
commit
8fe8c2d6c4
3 changed files with 68 additions and 76 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue