style(desktop): migrate bespoke pills to shared Badge; tidy cron/titlebar

- Sidebar toggles in the titlebar no longer carry an active highlight —
  they're plain show/hide affordances now.
- Replace every bespoke rounded-full status pill (cron, messaging,
  settings, skills) with the shared Badge (adds a `warn` tone). App radius,
  one component.
- Cron row actions use Codicons (play/debug-pause/zap/edit/trash) to match
  the rest of the chrome instead of stray lucide glyphs.
This commit is contained in:
Brooklyn Nicholson
2026-06-03 23:52:51 -05:00
parent fd88d527af
commit e026fd88cd
6 changed files with 39 additions and 74 deletions

View File

@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { PageLoader } from '@/components/page-loader'
import { Badge, type BadgeProps } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import {
@ -25,7 +26,7 @@ import {
triggerCronJob,
updateCronJob
} from '@/hermes'
import { AlertTriangle, Clock, Pause, Pencil, Play, Trash2, Zap } from '@/lib/icons'
import { AlertTriangle, Clock } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { notify, notifyError } from '@/store/notifications'
@ -86,23 +87,16 @@ const SCHEDULE_OPTIONS: ReadonlyArray<ScheduleOption> = [
}
]
const STATE_TONE: Record<string, 'good' | 'muted' | 'warn' | 'bad'> = {
enabled: 'good',
scheduled: 'good',
running: 'good',
const STATE_VARIANT: Record<string, BadgeProps['variant']> = {
enabled: 'default',
scheduled: 'default',
running: 'default',
paused: 'warn',
disabled: 'muted',
error: 'bad',
error: 'destructive',
completed: 'muted'
}
const PILL_TONE: Record<'good' | 'muted' | 'warn' | 'bad', string> = {
good: 'bg-primary/10 text-primary',
muted: 'bg-muted text-muted-foreground',
warn: 'bg-amber-500/10 text-amber-600 dark:text-amber-300',
bad: 'bg-destructive/10 text-destructive'
}
const asText = (value: unknown): string => (typeof value === 'string' ? value : '')
const truncate = (value: string, max = 80): string => (value.length > max ? `${value.slice(0, max)}` : value)
@ -541,8 +535,14 @@ function CronJobRow({
>
<div className="flex flex-wrap items-center gap-2">
<span className="truncate text-sm font-medium">{jobTitle(job)}</span>
<StatePill tone={STATE_TONE[state] ?? 'muted'}>{state}</StatePill>
{deliver && deliver !== DEFAULT_DELIVER && <StatePill tone="muted">{deliver}</StatePill>}
<Badge className="capitalize" variant={STATE_VARIANT[state] ?? 'muted'}>
{state}
</Badge>
{deliver && deliver !== DEFAULT_DELIVER && (
<Badge className="capitalize" variant="muted">
{deliver}
</Badge>
)}
</div>
{hasName && prompt && <p className="mt-1 truncate text-xs text-muted-foreground">{truncate(prompt, 120)}</p>}
<div className="mt-1 flex flex-wrap items-center gap-x-4 gap-y-1 text-[0.68rem] text-muted-foreground">
@ -568,13 +568,13 @@ function CronJobRow({
onClick={onPauseResume}
title={isPaused ? 'Resume' : 'Pause'}
>
{isPaused ? <Play className="size-3.5" /> : <Pause className="size-3.5" />}
<Codicon name={isPaused ? 'play' : 'debug-pause'} size="0.875rem" />
</IconAction>
<IconAction aria-label="Trigger now" disabled={busy} onClick={onTrigger} title="Trigger now">
<Zap className="size-3.5" />
<Codicon name="zap" size="0.875rem" />
</IconAction>
<IconAction aria-label="Edit cron" onClick={onEdit} title="Edit">
<Pencil className="size-3.5" />
<Codicon name="edit" size="0.875rem" />
</IconAction>
<IconAction
aria-label="Delete cron"
@ -582,7 +582,7 @@ function CronJobRow({
onClick={onDelete}
title="Delete"
>
<Trash2 className="size-3.5" />
<Codicon name="trash" size="0.875rem" />
</IconAction>
</div>
</div>
@ -602,16 +602,6 @@ function IconAction({ children, className, ...props }: Omit<React.ComponentProps
)
}
function StatePill({ children, tone }: { children: string; tone: keyof typeof PILL_TONE }) {
return (
<span
className={cn('inline-flex items-center rounded-full px-1.5 py-0.5 text-[0.64rem] capitalize', PILL_TONE[tone])}
>
{children}
</span>
)
}
function EmptyState({
actionLabel,
description,

View File

@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { PageLoader } from '@/components/page-loader'
import { StatusDot, type StatusTone } from '@/components/status-dot'
import { Badge, type BadgeProps } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { DisclosureCaret } from '@/components/ui/disclosure-caret'
import { Input } from '@/components/ui/input'
@ -42,11 +43,11 @@ const STATE_LABELS: Record<string, string> = {
startup_failed: 'Startup failed'
}
const PILL_TONE: Record<StatusTone, string> = {
good: 'bg-primary/10 text-primary',
muted: 'bg-muted text-muted-foreground',
warn: 'bg-amber-500/10 text-amber-600 dark:text-amber-300',
bad: 'bg-destructive/10 text-destructive'
const TONE_VARIANT: Record<StatusTone, BadgeProps['variant']> = {
good: 'default',
muted: 'muted',
warn: 'warn',
bad: 'destructive'
}
const HINT_BY_STATE: Record<string, string> = {
@ -696,27 +697,13 @@ function PlatformHint({ platform }: { platform: MessagingPlatformInfo }) {
function StatePill({ children, tone }: { children: string; tone: StatusTone }) {
return (
<span
className={cn(
'inline-flex shrink-0 items-center gap-1.5 rounded-full px-2 py-0.5 text-[0.66rem] font-medium',
PILL_TONE[tone]
)}
>
<Badge variant={TONE_VARIANT[tone]}>
<StatusDot tone={tone} />
{children}
</span>
</Badge>
)
}
function SetupPill({ active, children }: { active: boolean; children: string }) {
return (
<span
className={cn(
'inline-flex items-center rounded-full px-2 py-0.5 text-[0.66rem] font-medium',
PILL_TONE[active ? 'good' : 'muted']
)}
>
{children}
</span>
)
return <Badge variant={active ? 'default' : 'muted'}>{children}</Badge>
}

View File

@ -1,6 +1,7 @@
import type { ReactNode } from 'react'
import { PageLoader } from '@/components/page-loader'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import type { IconComponent } from '@/lib/icons'
import { cn } from '@/lib/utils'
@ -18,16 +19,7 @@ export function SettingsContent({ children }: { children: ReactNode }) {
}
export function Pill({ tone = 'muted', children }: { tone?: 'muted' | 'primary'; children: ReactNode }) {
return (
<span
className={cn(
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[0.6875rem]',
tone === 'primary' ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'
)}
>
{children}
</span>
)
return <Badge variant={tone === 'primary' ? 'default' : 'muted'}>{children}</Badge>
}
export function SectionHeading({ icon: Icon, title, meta }: { icon: IconComponent; title: string; meta?: string }) {

View File

@ -73,17 +73,15 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
// Each titlebar button controls the pane physically on its side, so a flip
// swaps which pane each one toggles. Default: sessions left, file browser
// right. Flipped: file browser left, sessions right.
// `active` stays tied to the file browser (the toggleable extra pane) rather
// than the sessions sidebar, which has never shown a highlight.
const fileBrowserEdge = { active: fileBrowserOpen, open: fileBrowserOpen, toggle: toggleFileBrowserOpen }
const sessionsEdge = { active: undefined, open: sidebarOpen, toggle: toggleSidebarOpen }
// right. Flipped: file browser left, sessions right. Sidebar toggles never
// carry an active highlight — they're plain show/hide affordances.
const fileBrowserEdge = { open: fileBrowserOpen, toggle: toggleFileBrowserOpen }
const sessionsEdge = { open: sidebarOpen, toggle: toggleSidebarOpen }
const leftEdge = panesFlipped ? fileBrowserEdge : sessionsEdge
const rightEdge = panesFlipped ? sessionsEdge : fileBrowserEdge
const leftToolbarTools: TitlebarTool[] = [
{
active: leftEdge.active,
icon: <Codicon name="layout-sidebar-left" />,
id: 'sidebar',
label: `${leftEdge.open ? 'Hide' : 'Show'} left sidebar`,
@ -106,7 +104,6 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
]
const rightSidebarTool: TitlebarTool = {
active: rightEdge.active,
icon: <Codicon name="layout-sidebar-right" />,
id: 'right-sidebar',
label: `${rightEdge.open ? 'Hide' : 'Show'} right sidebar`,

View File

@ -2,6 +2,7 @@ import type * as React from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { PageLoader } from '@/components/page-loader'
import { Badge } from '@/components/ui/badge'
import { Switch } from '@/components/ui/switch'
import { TextTab, TextTabMeta } from '@/components/ui/text-tab'
import { getSkills, getToolsets, toggleSkill, toggleToolset } from '@/hermes'
@ -318,14 +319,11 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
function StatusPill({ active, children }: { active: boolean; children: string }) {
return (
<span
className={cn(
'inline-flex items-center rounded-full px-1.5 py-0.5 text-[0.64rem]',
active ? 'bg-(--ui-bg-tertiary) text-(--ui-text-secondary)' : 'bg-(--ui-bg-quinary) text-(--ui-text-tertiary)'
)}
<Badge
className={active ? 'bg-(--ui-bg-tertiary) text-(--ui-text-secondary)' : 'bg-(--ui-bg-quinary) text-(--ui-text-tertiary)'}
>
{children}
</span>
</Badge>
)
}

View File

@ -13,6 +13,7 @@ const badgeVariants = cva(
variant: {
default: 'bg-primary/10 text-primary',
muted: 'bg-muted text-muted-foreground',
warn: 'bg-amber-500/10 text-amber-600 dark:text-amber-300',
destructive: 'bg-destructive/10 text-destructive',
outline: 'border border-(--ui-stroke-secondary) text-muted-foreground'
}