refactor(web): dashboard typography & contrast pass

Why
- Whole dashboard was force-uppercased by a single \`uppercase\` on the
  App.tsx root, which inherited into every page and forced ~23
  \`normal-case\` opt-outs across 7 files just to keep dynamic content
  (model names, theme names, etc.) readable.
- Micro-typography (\`text-[0.55rem]\` / \`text-[0.6rem]\` / \`text-[9px]\` /
  \`text-[10px]\` / \`text-[11px]\`) combined with stacked alpha
  (\`text-muted-foreground/60\` over a 55%-alpha base, \`opacity-30\` on
  nav headers) produced text that fails WCAG AA at small sizes.
- Per-theme \`--theme-font-sans\` was being clobbered by a hard-coded
  \`font-mondwest\` on the App.tsx root.

Changes
- Drop global \`uppercase\` + \`font-mondwest\` from the App.tsx root; default
  to \`text-text-primary\` so body content inherits the theme font.
- Map \`--color-muted-foreground\` to \`--color-text-secondary\` so the
  long tail of \`text-muted-foreground\` call sites get a WCAG-AA-targeted
  color instead of 55%-alpha midground.
- Apply the new DS \`text-display\` utility on intentional brand chrome
  (sidebar nav section labels, page titles, mobile header brand,
  segmented filters, badges, ChatSidebar headings).
- Remove the 23 \`normal-case\` opt-outs that only existed to fight the
  global \`uppercase\`. Retain \`normal-case\` on the 4 DS \`Button\`
  instances that legitimately display dynamic content.
- Bump every \`text-[0.55-0.7rem]\` / \`text-[9-11px]\` to \`text-xs\` (12px
  floor) across PluginsPage, ConfigPage, SkillsPage, ModelsPage,
  SessionsPage, AnalyticsPage, LogsPage, EnvPage, ChatPage,
  ChatSidebar, ToolCall, ModelInfoCard, ModelPickerDialog,
  SidebarStatusStrip, SidebarFooter, OAuthProvidersCard, SlashPopover,
  ThemeSwitcher, LanguageSwitcher, BottomPickSheet, AutoField.
- Replace stacked-alpha refs (\`text-muted-foreground/60\`,
  \`text-midground/70\`, \`opacity-30/50/60\` on text) with semantic tokens
  (\`text-text-secondary\`, \`text-text-tertiary\`, \`text-text-disabled\`).
- Bump \`@nous-research/ui\` to 0.16.0 (which adds the \`text-display\`
  utility and semantic text tokens this PR depends on).
- Add a Typography & contrast rules section to \`web/README.md\` codifying
  the 12px text floor, 0.7 opacity floor on text, "uppercase via
  text-display only" rule, and "prefer semantic tokens" guideline so
  the dashboard doesn't drift back.

This pairs with NousResearch/design-language#22 which provides the
\`text-display\` utility and semantic text tokens.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Austin Pickett
2026-05-19 12:39:09 -04:00
parent 5a3317693c
commit b5fb8da7ea
27 changed files with 227 additions and 189 deletions
+40
View File
@@ -46,3 +46,43 @@ src/
├── main.tsx # React entry point
└── index.css # Tailwind imports and theme variables
```
## Typography & contrast rules
Read before adding or editing UI styles. These rules keep the dashboard legible across all built-in themes and stop drift back into the patterns the design system was just refactored out of.
### Text size floor
- **Minimum body size: `text-xs` (12px / 0.75rem).** Do not use arbitrary `text-[0.6rem]`, `text-[0.65rem]`, `text-[9px]`, `text-[10px]`, or `text-[11px]` on copy, hints, labels, counts, or badges. Use the standard scale: `text-xs`, `text-sm`, `text-base`.
- Smaller sizes are only acceptable on **decorative overlays** (chart stripes, empty-state icons) — never on text the user is meant to read.
### Opacity floor on text
- **Never apply opacity below 0.7 to text.** No `opacity-30`, `opacity-50`, `opacity-60` on `<span>`s, `<p>`s, labels, etc.
- **Do not stack opacity tokens.** Patterns like `text-muted-foreground/60`, `text-midground/70`, `text-foreground/50` create unpredictable WCAG failures because the parent token already has alpha.
- Use the **semantic text tokens** from `@nous-research/ui`'s `globals.css`:
- `text-text-primary` — default body text.
- `text-text-secondary` — subtitles, meta, inactive nav.
- `text-text-tertiary` — small chrome labels, counts, footnotes.
- `text-text-disabled` — disabled states.
- `text-text-on-accent` — text on filled accent surfaces.
### Brand uppercase via `text-display`, never `uppercase`
- The dashboard preserves the Nous brand uppercase aesthetic, but it is **opt-in per element, not global**.
- Apply uppercase via the DS utility `text-display` on **brand chrome only** — page titles, nav section headings, badges, brand wordmark. DS components (`Button`, `Badge`, `Tabs`, `Segmented`, etc.) already self-apply `text-display`.
- **Do not** add `uppercase` (the literal Tailwind class) anywhere in `hermes-agent/web/src`.
- **Do not** add `normal-case` to "opt out" of inheritance — the app shell no longer forces uppercase, so opt-outs aren't needed. The only legitimate `normal-case` overrides are on DS `<Button>` instances that display **dynamic, user-typed content** (model slugs, theme names, language names, profile names).
### Fonts
- `font-mondwest` is the brand display font. Apply it explicitly only on brand chrome (paired with `text-display`).
- Body content inherits the active theme's `--theme-font-sans`. Do not hardcode `font-mondwest` on body text or pages — it locks out per-theme typography.
- Code/terminal text uses `font-mono` / `font-mono-ui` (both resolve through `--theme-font-mono`).
### Color tokens
- Prefer **semantic tokens** (`text-text-*`, `bg-card`, `border-border`, `text-foreground`, `text-destructive`, `text-success`, `text-warning`) over raw layer references (`text-midground`, `text-foreground`).
- `text-muted-foreground` is now wired to `--color-text-secondary`, so existing call sites stay correct, but new code should prefer the semantic name.
- When you genuinely need a non-token color (icon de-emphasis on a chart, terminal foreground via inline style), keep alpha at `≥ 0.7` for any text.
+1 -1
View File
@@ -10,7 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@nous-research/ui": "0.14.0",
"@nous-research/ui": "0.16.0",
"@observablehq/plot": "^0.6.17",
"@react-three/fiber": "^9.6.0",
"@tailwindcss/vite": "^4.2.1",
+14 -14
View File
@@ -416,7 +416,7 @@ export default function App() {
return (
<div
data-layout-variant={layoutVariant}
className="font-mondwest flex h-dvh max-h-dvh min-h-0 flex-col overflow-hidden bg-black uppercase text-midground antialiased"
className="flex h-dvh max-h-dvh min-h-0 flex-col overflow-hidden bg-black text-text-primary antialiased"
>
<SelectionSwitcher />
<Backdrop />
@@ -442,13 +442,13 @@ export default function App() {
aria-label={t.app.openNavigation}
aria-expanded={mobileOpen}
aria-controls="app-sidebar"
className="text-midground/70 hover:text-midground"
className="text-text-secondary hover:text-midground"
>
<Menu />
</Button>
<Typography
className="font-bold text-[0.95rem] leading-[0.95] tracking-[0.05em] text-midground"
className="text-display font-bold text-[0.95rem] leading-[0.95] tracking-[0.05em] text-midground"
style={{ mixBlendMode: "plus-lighter" }}
>
{t.app.brand}
@@ -498,7 +498,7 @@ export default function App() {
<PluginSlot name="header-left" />
<Typography
className="font-bold text-[1.125rem] leading-[0.95] tracking-[0.0525rem] text-midground"
className="text-display font-bold text-[1.125rem] leading-[0.95] tracking-[0.0525rem] text-midground"
style={{ mixBlendMode: "plus-lighter" }}
>
Hermes
@@ -512,7 +512,7 @@ export default function App() {
size="icon"
onClick={closeMobile}
aria-label={t.app.closeNavigation}
className="lg:hidden text-midground/70 hover:text-midground"
className="lg:hidden text-text-secondary hover:text-midground"
>
<X />
</Button>
@@ -542,7 +542,7 @@ export default function App() {
<span
className={cn(
"px-5 pt-2.5 pb-1",
"font-mondwest text-[0.6rem] tracking-[0.15em] uppercase opacity-30",
"font-mondwest text-display text-xs tracking-[0.12em] text-text-tertiary",
)}
id="hermes-sidebar-plugin-nav-heading"
>
@@ -671,10 +671,10 @@ function SidebarNavLink({ closeMobile, item, t }: SidebarNavLinkProps) {
cn(
"group relative flex items-center gap-3",
"px-5 py-2.5",
"font-mondwest text-[0.8rem] tracking-[0.12em]",
"font-mondwest text-display text-sm tracking-[0.12em]",
"whitespace-nowrap transition-colors cursor-pointer",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground",
isActive ? "text-midground" : "opacity-60 hover:opacity-100",
isActive ? "text-midground" : "text-text-secondary hover:text-midground",
)
}
style={{
@@ -746,7 +746,7 @@ function SidebarSystemActions({ onNavigate }: { onNavigate: () => void }) {
<span
className={cn(
"px-5 pt-0.5 pb-0.5",
"font-mondwest text-[0.6rem] tracking-[0.15em] uppercase opacity-30",
"font-mondwest text-display text-xs tracking-[0.12em] text-text-tertiary",
)}
>
{t.app.system}
@@ -772,12 +772,12 @@ function SidebarSystemActions({ onNavigate }: { onNavigate: () => void }) {
active={busy}
className={cn(
"gap-3 px-5 py-1.5 whitespace-nowrap",
"font-mondwest text-[0.75rem] tracking-[0.1em]",
"transition-opacity",
"font-mondwest text-display text-xs tracking-[0.1em]",
"transition-colors",
busy
? "text-midground opacity-100"
: "opacity-60 hover:opacity-100",
"disabled:opacity-30",
? "text-midground"
: "text-text-secondary hover:text-midground",
"disabled:text-text-disabled",
)}
>
{isPending ? (
+2 -2
View File
@@ -11,8 +11,8 @@ function FieldHint({ schema, schemaKey }: { schema: Record<string, unknown>; sch
return (
<div className="flex flex-col gap-0.5">
{keyPath && <span className="text-[10px] font-mono text-muted-foreground/50">{keyPath}</span>}
{description && <span className="text-xs text-muted-foreground/70">{description}</span>}
{keyPath && <span className="text-xs font-mono text-text-tertiary">{keyPath}</span>}
{description && <span className="text-xs text-text-secondary">{description}</span>}
</div>
);
}
+1 -1
View File
@@ -200,7 +200,7 @@ export function BottomPickSheet({
<Typography
mondwest
className="text-[0.65rem] tracking-[0.15em] uppercase text-midground/70"
className="text-display text-xs tracking-[0.12em] text-text-tertiary"
>
{title}
</Typography>
+5 -5
View File
@@ -304,13 +304,13 @@ export function ChatSidebar({ channel, className }: ChatSidebarProps) {
return (
<aside
className={cn(
"flex h-full w-full min-w-0 shrink-0 flex-col gap-3 overflow-y-auto overflow-x-hidden pr-1 normal-case lg:w-80",
"flex h-full w-full min-w-0 shrink-0 flex-col gap-3 overflow-y-auto overflow-x-hidden pr-1 lg:w-80",
className,
)}
>
<Card className="flex items-center justify-between gap-2 px-3 py-2">
<div className="min-w-0">
<div className="text-xs uppercase tracking-wider text-muted-foreground">
<div className="text-display text-xs tracking-wider text-text-tertiary">
model
</div>
@@ -321,7 +321,7 @@ export function ChatSidebar({ channel, className }: ChatSidebarProps) {
onClick={() => setModelOpen(true)}
suffix={
canPickModel ? (
<ChevronDown className="opacity-60" />
<ChevronDown className="text-text-secondary" />
) : undefined
}
className="self-start min-w-0 px-0 py-0 normal-case tracking-normal text-sm font-medium hover:underline disabled:no-underline"
@@ -357,13 +357,13 @@ export function ChatSidebar({ channel, className }: ChatSidebarProps) {
)}
<Card className="flex min-h-0 flex-none flex-col px-2 py-2">
<div className="px-1 pb-2 text-xs uppercase tracking-wider text-muted-foreground">
<div className="text-display px-1 pb-2 text-xs tracking-wider text-text-tertiary">
tools
</div>
<div className="flex min-h-0 flex-col gap-1.5">
{tools.length === 0 ? (
<div className="px-2 py-4 text-center text-xs text-muted-foreground">
<div className="px-2 py-4 text-center text-xs text-text-secondary">
no tool calls yet
</div>
) : (
+2 -2
View File
@@ -68,13 +68,13 @@ export function LanguageSwitcher({ dropUp = false }: LanguageSwitcherProps) {
aria-label={t.language.switchTo}
aria-haspopup="listbox"
aria-expanded={open}
className="px-2 py-1 normal-case tracking-normal font-normal text-xs text-muted-foreground hover:text-foreground"
className="px-2 py-1 normal-case tracking-normal font-normal text-xs text-text-secondary hover:text-foreground"
>
<span className="inline-flex items-center gap-1.5">
<LocaleFlagIcon countryCode={current.flagCountryCode} />
<Typography
mondwest
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
className="hidden sm:inline text-display tracking-wide text-xs"
>
{locale === "en" ? "EN" : current.name}
</Typography>
+6 -6
View File
@@ -60,11 +60,11 @@ export function ModelInfoCard({
{formatTokenCount(info.effective_context_length)}
</span>
{info.config_context_length > 0 ? (
<span className="text-amber-500/80 text-[10px]">
<span className="text-amber-500 text-xs">
(override auto: {formatTokenCount(info.auto_context_length)})
</span>
) : (
<span className="text-muted-foreground/60 text-[10px]">
<span className="text-text-tertiary text-xs">
auto-detected
</span>
)}
@@ -86,22 +86,22 @@ export function ModelInfoCard({
{hasCaps && (
<div className="flex flex-wrap items-center gap-1.5 pt-0.5">
{caps.supports_tools && (
<span className="inline-flex items-center gap-1 bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium text-emerald-600 dark:text-emerald-400">
<span className="inline-flex items-center gap-1 bg-emerald-500/10 px-2 py-0.5 text-xs font-medium text-emerald-600 dark:text-emerald-400">
<Wrench className="h-2.5 w-2.5" /> Tools
</span>
)}
{caps.supports_vision && (
<span className="inline-flex items-center gap-1 bg-blue-500/10 px-2 py-0.5 text-[10px] font-medium text-blue-600 dark:text-blue-400">
<span className="inline-flex items-center gap-1 bg-blue-500/10 px-2 py-0.5 text-xs font-medium text-blue-600 dark:text-blue-400">
<Eye className="h-2.5 w-2.5" /> Vision
</span>
)}
{caps.supports_reasoning && (
<span className="inline-flex items-center gap-1 bg-purple-500/10 px-2 py-0.5 text-[10px] font-medium text-purple-600 dark:text-purple-400">
<span className="inline-flex items-center gap-1 bg-purple-500/10 px-2 py-0.5 text-xs font-medium text-purple-600 dark:text-purple-400">
<Brain className="h-2.5 w-2.5" /> Reasoning
</span>
)}
{caps.model_family && (
<span className="inline-flex items-center gap-1 bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
<span className="inline-flex items-center gap-1 bg-muted px-2 py-0.5 text-xs font-medium text-text-secondary">
{caps.model_family}
</span>
)}
+2 -2
View File
@@ -366,7 +366,7 @@ function ProviderColumn({
<span className="font-medium truncate">{p.name}</span>
{p.is_current && <CurrentTag />}
</div>
<div className="text-[0.65rem] text-muted-foreground/80 font-mono truncate">
<div className="text-xs text-text-secondary font-mono truncate">
{p.slug} · {p.total_models ?? p.models?.length ?? 0} models
</div>
</div>
@@ -453,7 +453,7 @@ function ModelColumn({
function CurrentTag() {
return (
<span className="text-[0.6rem] uppercase tracking-wider text-primary/80 shrink-0">
<span className="text-display text-xs tracking-wider text-primary shrink-0">
current
</span>
);
+8 -8
View File
@@ -154,32 +154,32 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
<span className="font-medium text-sm">{p.name}</span>
<Badge
tone="outline"
className="text-[11px] uppercase tracking-wide"
className="text-xs tracking-wide"
>
{t.oauth.flowLabels[p.flow]}
</Badge>
{p.status.logged_in && (
<Badge tone="success" className="text-[11px]">
<Badge tone="success" className="text-xs">
{t.oauth.connected}
</Badge>
)}
{expiresLabel === "expired" && (
<Badge tone="destructive" className="text-[11px]">
<Badge tone="destructive" className="text-xs">
{t.oauth.expired}
</Badge>
)}
{expiresLabel && expiresLabel !== "expired" && (
<Badge tone="outline" className="text-[11px]">
<Badge tone="outline" className="text-xs">
{expiresLabel}
</Badge>
)}
</div>
{p.status.logged_in && p.status.token_preview && (
<code className="text-xs font-mono-ui truncate">
<span className="opacity-50">token </span>
<span className="text-text-tertiary">token </span>
{p.status.token_preview}
{p.status.source_label && (
<span className="opacity-40">
<span className="text-text-tertiary">
{" "}
· {p.status.source_label}
</span>
@@ -187,7 +187,7 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
</code>
)}
{!p.status.logged_in && (
<span className="text-xs text-muted-foreground/80">
<span className="text-xs text-text-secondary">
{t.oauth.notConnected.split("{command}")[0]}
<code className="text-foreground bg-secondary/40 px-1">
{p.cli_command}
@@ -245,7 +245,7 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
</Button>
)}
{p.status.logged_in && p.flow === "external" && (
<span className="text-[11px] text-muted-foreground italic px-2">
<span className="text-xs text-text-tertiary italic px-2">
<Terminal className="h-3 w-3 inline mr-0.5" />
{t.oauth.managedExternally}
</span>
+2 -2
View File
@@ -17,7 +17,7 @@ export function SidebarFooter() {
>
<Typography
mondwest
className="font-mono-ui text-[0.7rem] tabular-nums tracking-[0.1em] text-muted-foreground/70 lowercase"
className="font-mono-ui text-xs tabular-nums tracking-[0.08em] text-text-tertiary lowercase"
>
{status?.version != null ? `v${status.version}` : "—"}
</Typography>
@@ -27,7 +27,7 @@ export function SidebarFooter() {
target="_blank"
rel="noopener noreferrer"
className={cn(
"font-mondwest text-[0.65rem] tracking-[0.15em] text-midground",
"font-mondwest text-display text-xs tracking-[0.12em] text-midground",
"transition-opacity hover:opacity-90",
"focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/40",
)}
+6 -6
View File
@@ -27,21 +27,21 @@ export function SidebarStatusStrip() {
className={cn(
"block text-left",
"px-5 pb-2 pt-0.5",
"text-muted-foreground/70",
"transition-colors hover:text-muted-foreground/90",
"text-text-secondary",
"transition-colors hover:text-midground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/40",
"focus-visible:ring-inset",
)}
>
<div className="flex flex-col gap-1 font-mondwest text-[0.55rem] leading-snug tracking-[0.12em]">
<div className="flex flex-col gap-1 font-mondwest text-xs leading-snug tracking-[0.08em]">
<p className="break-words">
<span className="text-muted-foreground/50">{gatewayStatusLabel}</span>{" "}
<span className="text-text-tertiary">{gatewayStatusLabel}</span>{" "}
<span className={cn("font-medium", gw.tone)}>{gw.label}</span>
</p>
<p className="break-words">
<span className="text-muted-foreground/50">{activeSessionsLabel}</span>{" "}
<span className="tabular-nums text-muted-foreground/70">
<span className="text-text-tertiary">{activeSessionsLabel}</span>{" "}
<span className="tabular-nums text-text-secondary">
{status.active_sessions}
</span>
</p>
+1 -1
View File
@@ -158,7 +158,7 @@ export const SlashPopover = forwardRef<SlashPopoverHandle, Props>(
</span>
{it.meta && (
<span className="text-[0.7rem] text-muted-foreground/70 truncate ml-auto">
<span className="text-xs text-text-tertiary truncate ml-auto">
{it.meta}
</span>
)}
+5 -5
View File
@@ -65,7 +65,7 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
<Button
ghost
onClick={() => setOpen((o) => !o)}
className="px-2 py-1 normal-case tracking-normal font-normal text-xs text-muted-foreground hover:text-foreground"
className="px-2 py-1 normal-case tracking-normal font-normal text-xs text-text-secondary hover:text-foreground"
title={t.theme?.switchTheme ?? "Switch theme"}
aria-label={t.theme?.switchTheme ?? "Switch theme"}
aria-expanded={open}
@@ -76,7 +76,7 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
<Typography
mondwest
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
className="hidden sm:inline text-display tracking-wide text-xs"
>
{label}
</Typography>
@@ -115,7 +115,7 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
<div className="border-b border-current/20 px-3 py-2">
<Typography
mondwest
className="text-[0.65rem] tracking-[0.15em] uppercase text-midground/70"
className="text-display text-xs tracking-[0.12em] text-text-tertiary"
>
{sheetTitle}
</Typography>
@@ -166,12 +166,12 @@ function ThemeSwitcherOptions({
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<Typography
mondwest
className="truncate text-[0.75rem] tracking-wide uppercase"
className="truncate text-display text-xs tracking-wide"
>
{th.label}
</Typography>
{th.description && (
<Typography className="truncate text-[0.65rem] normal-case tracking-normal text-midground/50">
<Typography className="truncate text-xs tracking-normal text-text-tertiary">
{th.description}
</Typography>
)}
+5 -5
View File
@@ -104,7 +104,7 @@ export function ToolCall({ tool }: { tool: ToolEntry }) {
<span className="font-mono font-medium shrink-0">{tool.name}</span>
<span className="font-mono text-muted-foreground/80 truncate min-w-0 flex-1">
<span className="font-mono text-text-secondary truncate min-w-0 flex-1">
{tool.context ?? ""}
</span>
@@ -128,7 +128,7 @@ export function ToolCall({ tool }: { tool: ToolEntry }) {
)}
{elapsed && (
<span className="font-mono text-[0.65rem] text-muted-foreground tabular-nums shrink-0">
<span className="font-mono text-xs text-text-tertiary tabular-nums shrink-0">
{elapsed}
</span>
)}
@@ -186,8 +186,8 @@ function Section({
return (
<div className="flex gap-3">
<span
className={`uppercase tracking-wider text-[0.6rem] shrink-0 w-14 pt-0.5 ${
tone === "error" ? "text-destructive/80" : "text-muted-foreground/60"
className={`text-display tracking-wider text-xs shrink-0 w-14 pt-0.5 ${
tone === "error" ? "text-destructive" : "text-text-tertiary"
}`}
>
{label}
@@ -224,5 +224,5 @@ function diffLineClass(line: string): string {
if (line.startsWith("-") && !line.startsWith("---"))
return "text-destructive";
if (line.startsWith("@@")) return "text-primary";
return "text-muted-foreground/80";
return "text-text-secondary";
}
+5 -1
View File
@@ -139,7 +139,11 @@ code { font-size: 0.875rem; }
--color-secondary: color-mix(in srgb, var(--midground-base) 6%, var(--background-base));
--color-secondary-foreground: var(--midground);
--color-muted: color-mix(in srgb, var(--midground-base) 8%, var(--background-base));
--color-muted-foreground: color-mix(in srgb, var(--midground-base) 55%, transparent);
/* Routes the shadcn `muted-foreground` slot through the DS semantic
text-secondary token (defaults to midground 80%) so legacy call
sites that use `text-muted-foreground` get a readable color
instead of the old 55%-transparent default. */
--color-muted-foreground: var(--color-text-secondary);
--color-accent: color-mix(in srgb, var(--midground-base) 10%, var(--background-base));
--color-accent-foreground: var(--midground);
--color-destructive: #fb2c36;
+5 -5
View File
@@ -119,7 +119,7 @@ function SortHeader({
<ArrowDown className="h-3.5 w-3.5 text-foreground/80 shrink-0" />
)
) : (
<ArrowUpDown className="h-3 w-3 text-muted-foreground/40 shrink-0" />
<ArrowUpDown className="h-3 w-3 text-text-tertiary shrink-0" />
)}
</span>
</th>
@@ -177,7 +177,7 @@ function TokenBarChart({ daily }: { daily: AnalyticsDailyEntry[] }) {
style={{ height: CHART_HEIGHT_PX }}
>
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block z-10 pointer-events-none">
<div className="bg-card border border-border px-2.5 py-1.5 text-[10px] text-foreground shadow-lg whitespace-nowrap">
<div className="bg-card border border-border px-2.5 py-1.5 text-xs text-foreground shadow-lg whitespace-nowrap">
<div className="font-medium">{formatDate(d.day)}</div>
<div>
{t.analytics.input}: {formatTokens(d.input_tokens)}
@@ -207,7 +207,7 @@ function TokenBarChart({ daily }: { daily: AnalyticsDailyEntry[] }) {
})}
</div>
<div className="flex justify-between mt-2 text-[10px] text-muted-foreground">
<div className="flex justify-between mt-2 text-xs text-text-tertiary">
<span>{daily.length > 0 ? formatDate(daily[0].day) : ""}</span>
{daily.length > 2 && (
<span>{formatDate(daily[Math.floor(daily.length / 2)].day)}</span>
@@ -432,7 +432,7 @@ export default function AnalyticsPage() {
setAfterTitle(
<span className="flex items-center gap-2">
{loading && <Spinner className="shrink-0 text-base text-primary" />}
<Badge tone="secondary" className="text-[10px]">
<Badge tone="secondary" className="text-xs">
{periodLabel}
</Badge>
</span>,
@@ -586,7 +586,7 @@ export default function AnalyticsPage() {
<div className="flex flex-col items-center text-muted-foreground">
<BarChart3 className="h-8 w-8 mb-3 opacity-40" />
<p className="text-sm font-medium">{t.analytics.noUsageData}</p>
<p className="text-xs mt-1 text-muted-foreground/60">
<p className="text-xs mt-1 text-text-tertiary">
{t.analytics.startSession}
</p>
</div>
+7 -10
View File
@@ -233,8 +233,8 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
aria-controls="chat-side-panel"
className={cn(
"shrink-0 rounded border border-current/20",
"px-2 py-1 text-[0.65rem] font-medium tracking-wide normal-case",
"text-midground/80 hover:text-midground hover:bg-midground/5",
"px-2 py-1 text-xs font-medium tracking-wide",
"text-text-secondary hover:text-midground hover:bg-midground/5",
)}
>
<span className="inline-flex items-center gap-1.5">
@@ -708,9 +708,6 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
// model badge, tool-call list, model picker. Best-effort: if the
// sidecar fails to connect the terminal pane keeps working.
//
// `normal-case` opts out of the dashboard's global `uppercase` rule on
// the root `<div>` in App.tsx — terminal output must preserve case.
//
// Mobile model/tools sheet is portaled to `document.body` so it stacks
// above the app sidebar (`z-50`) and mobile chrome (`z-40`). The main
// dashboard column uses `relative z-2`, which traps `position:fixed`
@@ -769,7 +766,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
size="icon"
onClick={closeMobilePanel}
aria-label={t.app.closeModelTools}
className="text-midground/70 hover:text-midground"
className="text-text-secondary hover:text-midground"
>
<X />
</Button>
@@ -789,7 +786,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
);
return (
<div className="flex min-h-0 flex-1 flex-col gap-2 normal-case">
<div className="flex min-h-0 flex-1 flex-col gap-2">
<PluginSlot name="chat:top" />
{mobileModelToolsPortal}
@@ -824,9 +821,9 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
"absolute z-10",
"rounded border border-current/30",
"bg-black/20 backdrop-blur-sm",
"opacity-60 hover:opacity-100 hover:border-current/60",
"transition-opacity duration-150 normal-case font-normal tracking-normal",
"bottom-2 right-2 px-2 py-1 text-[0.65rem] sm:bottom-3 sm:right-3 sm:px-2.5 sm:py-1.5 sm:text-xs",
"opacity-70 hover:opacity-100 hover:border-current/60",
"transition-opacity duration-150",
"bottom-2 right-2 px-2 py-1 text-xs sm:bottom-3 sm:right-3 sm:px-2.5 sm:py-1.5",
"lg:bottom-4 lg:right-4",
)}
style={{ color: TERMINAL_THEME.foreground }}
+9 -9
View File
@@ -534,13 +534,13 @@ export default function ConfigPage() {
<div className="sm:sticky sm:top-4">
<div className="flex flex-col border border-border bg-muted/20">
<div className="hidden sm:flex items-center gap-2 px-3 py-2 border-b border-border">
<Filter className="h-3 w-3 text-muted-foreground" />
<span className="font-mondwest text-[0.65rem] tracking-[0.12em] uppercase text-muted-foreground">
<Filter className="h-3 w-3 text-text-tertiary" />
<span className="font-mondwest text-display text-xs tracking-[0.12em] text-text-secondary">
{t.config.filters}
</span>
</div>
<div className="hidden sm:block px-3 pt-2 pb-1 font-mondwest text-[0.6rem] tracking-[0.12em] uppercase text-muted-foreground/70">
<div className="hidden sm:block px-3 pt-2 pb-1 font-mondwest text-display text-xs tracking-[0.12em] text-text-tertiary">
{t.config.sections}
</div>
@@ -556,7 +556,7 @@ export default function ConfigPage() {
setSearchQuery("");
setActiveCategory(cat);
}}
className="rounded-sm whitespace-nowrap px-2 py-1 text-[11px]"
className="rounded-sm whitespace-nowrap px-2 py-1 text-xs"
>
<CategoryIcon
category={cat}
@@ -566,10 +566,10 @@ export default function ConfigPage() {
{prettyCategoryName(cat)}
</span>
<span
className={`text-[10px] tabular-nums ${
className={`text-xs tabular-nums ${
isActive
? "text-foreground/60"
: "text-muted-foreground/50"
? "text-text-secondary"
: "text-text-tertiary"
}`}
>
{categoryCounts[cat] || 0}
@@ -591,7 +591,7 @@ export default function ConfigPage() {
<Search className="h-4 w-4" />
{t.config.searchResults}
</CardTitle>
<Badge tone="secondary" className="text-[10px]">
<Badge tone="secondary" className="text-xs">
{searchMatchedFields.length}{" "}
{t.config.fields.replace(
"{s}",
@@ -622,7 +622,7 @@ export default function ConfigPage() {
/>
{prettyCategoryName(activeCategory)}
</CardTitle>
<Badge tone="secondary" className="text-[10px]">
<Badge tone="secondary" className="text-xs">
{activeFields.length}{" "}
{t.config.fields.replace(
"{s}",
+17 -17
View File
@@ -133,12 +133,12 @@ function EnvVarRow({
// Compact inline row for unset, non-editing keys (used inside provider groups)
if (compact && !info.is_set && !isEditing) {
return (
<div className="flex items-center justify-between gap-3 py-1.5 min-w-0 overflow-hidden opacity-50 hover:opacity-100 transition-opacity">
<div className="flex items-center justify-between gap-3 py-1.5 min-w-0 overflow-hidden text-text-secondary hover:text-foreground transition-colors">
<div className="flex items-center gap-2 min-w-0">
<span className="font-mono-ui text-[0.7rem] text-muted-foreground">
<span className="font-mono-ui text-xs">
{varKey}
</span>
<span className="text-[0.65rem] text-muted-foreground/60 truncate hidden sm:block">
<span className="text-xs text-text-tertiary truncate hidden sm:block">
{info.description}
</span>
</div>
@@ -148,7 +148,7 @@ function EnvVarRow({
href={info.url}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-[0.65rem] text-primary hover:underline"
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
>
{t.env.getKey} <ExternalLink className="h-2.5 w-2.5" />
</a>
@@ -169,12 +169,12 @@ function EnvVarRow({
// Non-compact unset row
if (!info.is_set && !isEditing) {
return (
<div className="flex items-center justify-between gap-3 border border-border/50 px-4 py-2.5 min-w-0 overflow-hidden opacity-60 hover:opacity-100 transition-opacity">
<div className="flex items-center justify-between gap-3 border border-border/50 px-4 py-2.5 min-w-0 overflow-hidden text-text-secondary hover:text-foreground transition-colors">
<div className="flex items-center gap-3 min-w-0">
<Label className="font-mono-ui text-[0.7rem] text-muted-foreground">
<Label className="font-mono-ui text-xs">
{varKey}
</Label>
<span className="text-[0.65rem] text-muted-foreground/60 truncate hidden sm:block">
<span className="text-xs text-text-tertiary truncate hidden sm:block">
{info.description}
</span>
</div>
@@ -184,7 +184,7 @@ function EnvVarRow({
href={info.url}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-[0.65rem] text-primary hover:underline"
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
>
{t.env.getKey} <ExternalLink className="h-2.5 w-2.5" />
</a>
@@ -207,7 +207,7 @@ function EnvVarRow({
<div className="grid gap-2 border border-border p-4 min-w-0 overflow-hidden">
<div className="flex items-center justify-between gap-2 flex-wrap">
<div className="flex items-center gap-2">
<Label className="font-mono-ui text-[0.7rem]">{varKey}</Label>
<Label className="font-mono-ui text-xs">{varKey}</Label>
<Badge tone={info.is_set ? "success" : "outline"}>
{info.is_set ? t.common.set : t.env.notSet}
</Badge>
@@ -217,7 +217,7 @@ function EnvVarRow({
href={info.url}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-[0.65rem] text-primary hover:underline"
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
>
{t.env.getKey} <ExternalLink className="h-2.5 w-2.5" />
</a>
@@ -232,7 +232,7 @@ function EnvVarRow({
<Badge
key={tool}
tone="secondary"
className="text-[0.6rem] py-0 px-1.5"
className="text-xs py-0 px-1.5"
>
{tool}
</Badge>
@@ -396,7 +396,7 @@ function ProviderGroupCard({
{group.name === "Other" ? t.common.other : group.name}
</span>
{hasAnyConfigured && (
<Badge tone="success" className="text-[0.6rem]">
<Badge tone="success" className="text-xs">
{configuredCount} {t.common.set.toLowerCase()}
</Badge>
)}
@@ -407,13 +407,13 @@ function ProviderGroupCard({
href={keyUrl}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-[0.65rem] text-primary hover:underline"
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
{t.env.getKey} <ExternalLink className="h-2.5 w-2.5" />
</a>
)}
<span className="text-[0.65rem] text-muted-foreground/60">
<span className="text-xs text-text-tertiary">
{t.env.keysCount
.replace("{count}", String(group.entries.length))
.replace("{s}", group.entries.length !== 1 ? "s" : "")}
@@ -546,7 +546,7 @@ export default function EnvPage() {
key={s.id}
type="button"
onClick={() => scrollTo(s.id)}
className="shrink-0 cursor-pointer px-2 py-0.5 text-[10px] uppercase tracking-wider text-muted-foreground hover:text-foreground border border-border/50 hover:border-foreground/30 transition-colors"
className="shrink-0 cursor-pointer px-2 py-0.5 text-display text-xs tracking-wider text-text-secondary hover:text-foreground border border-border/50 hover:border-foreground/30 transition-colors"
>
{s.label}
</button>
@@ -745,7 +745,7 @@ export default function EnvPage() {
<p className="text-sm text-muted-foreground">
{t.env.description} <code>~/.hermes/.env</code>
</p>
<p className="text-[0.7rem] text-muted-foreground/70">
<p className="text-xs text-text-tertiary">
{t.env.changesNote}
</p>
</div>
@@ -904,7 +904,7 @@ function CollapsibleUnset({
prefix={collapsed ? <ChevronRight /> : <ChevronDown />}
onClick={() => setCollapsed(!collapsed)}
aria-expanded={!collapsed}
className="self-start mt-1 normal-case tracking-normal text-xs text-muted-foreground hover:text-foreground"
className="self-start mt-1 normal-case tracking-normal text-xs text-text-secondary hover:text-foreground"
>
{t.env.notConfigured.replace("{count}", String(unsetEntries.length))}
</Button>
+3 -3
View File
@@ -40,7 +40,7 @@ const LINE_COLORS: Record<string, string> = {
error: "text-destructive",
warning: "text-warning",
info: "text-foreground",
debug: "text-muted-foreground/60",
debug: "text-text-tertiary",
};
const toOptions = <T extends string>(values: readonly T[]) =>
@@ -87,7 +87,7 @@ export default function LogsPage() {
setAfterTitle(
<span className="flex items-center gap-2">
{loading && <Spinner className="shrink-0 text-base text-primary" />}
<Badge tone="secondary" className="text-[10px]">
<Badge tone="secondary" className="text-xs">
{file} · {level} · {component}
</Badge>
</span>,
@@ -104,7 +104,7 @@ export default function LogsPage() {
{t.logs.autoRefresh}
</Label>
{autoRefresh && (
<Badge tone="success" className="text-[10px]">
<Badge tone="success" className="text-xs">
<span className="mr-1 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
{t.common.live}
</Badge>
+37 -37
View File
@@ -123,7 +123,7 @@ function TokenBar({
</div>
{/* Legend */}
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-[10px] text-muted-foreground">
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-xs text-text-secondary">
{segments.map((s, i) => (
<span key={i} className="flex items-center gap-1">
<span className={`inline-block h-1.5 w-1.5 rounded-full ${s.dotColor}`} />
@@ -150,22 +150,22 @@ function CapabilityBadges({
return (
<div className="flex flex-wrap items-center gap-1.5">
{capabilities.supports_tools && (
<span className="inline-flex items-center gap-1 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-medium text-emerald-600 dark:text-emerald-400">
<span className="inline-flex items-center gap-1 bg-emerald-500/10 px-1.5 py-0.5 text-xs font-medium text-emerald-600 dark:text-emerald-400">
<Wrench className="h-2.5 w-2.5" /> Tools
</span>
)}
{capabilities.supports_vision && (
<span className="inline-flex items-center gap-1 bg-blue-500/10 px-1.5 py-0.5 text-[10px] font-medium text-blue-600 dark:text-blue-400">
<span className="inline-flex items-center gap-1 bg-blue-500/10 px-1.5 py-0.5 text-xs font-medium text-blue-600 dark:text-blue-400">
<Eye className="h-2.5 w-2.5" /> Vision
</span>
)}
{capabilities.supports_reasoning && (
<span className="inline-flex items-center gap-1 bg-purple-500/10 px-1.5 py-0.5 text-[10px] font-medium text-purple-600 dark:text-purple-400">
<span className="inline-flex items-center gap-1 bg-purple-500/10 px-1.5 py-0.5 text-xs font-medium text-purple-600 dark:text-purple-400">
<Brain className="h-2.5 w-2.5" /> Reasoning
</span>
)}
{capabilities.model_family && (
<span className="inline-flex items-center bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
<span className="inline-flex items-center bg-muted px-1.5 py-0.5 text-xs font-medium text-text-secondary">
{capabilities.model_family}
</span>
)}
@@ -235,7 +235,7 @@ function UseAsMenu({
outlined
onClick={() => setOpen((v) => !v)}
disabled={busy}
className="text-[10px] h-6 px-2"
className="text-xs h-6 px-2"
prefix={busy ? <Spinner /> : null}
>
Use as <ChevronDown className="h-3 w-3" />
@@ -253,13 +253,13 @@ function UseAsMenu({
Main model
</span>
{isMain && (
<span className="text-[9px] uppercase tracking-wider text-primary/80">
<span className="text-display text-xs tracking-wider text-primary">
current
</span>
)}
</button>
<div className="border-t border-border/50 px-3 py-1.5 text-[9px] uppercase tracking-wider text-muted-foreground">
<div className="border-t border-border/50 px-3 py-1.5 text-display text-xs tracking-wider text-text-tertiary">
Auxiliary task
</div>
@@ -282,7 +282,7 @@ function UseAsMenu({
>
<span>{t.label}</span>
{mainAuxTask === t.key && (
<span className="text-[9px] uppercase tracking-wider text-primary/80">
<span className="text-display text-xs tracking-wider text-primary">
current
</span>
)}
@@ -290,7 +290,7 @@ function UseAsMenu({
))}
{error && (
<div className="px-3 py-2 text-[10px] text-destructive border-t border-border/50">
<div className="px-3 py-2 text-xs text-destructive border-t border-border/50">
{error}
</div>
)}
@@ -343,36 +343,36 @@ function ModelCard({
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-muted-foreground/50 text-xs font-mono">
<span className="text-text-tertiary text-xs font-mono">
#{rank}
</span>
<CardTitle className="text-sm font-mono-ui truncate">
{shortModelName(entry.model)}
</CardTitle>
{isMain && (
<span className="inline-flex items-center gap-0.5 bg-primary/15 px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wider text-primary">
<span className="inline-flex items-center gap-0.5 bg-primary/15 px-1.5 py-0.5 text-display text-xs font-medium tracking-wider text-primary">
<Star className="h-2.5 w-2.5" /> main
</span>
)}
{mainAuxTask && (
<span className="inline-flex items-center bg-purple-500/10 px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wider text-purple-600 dark:text-purple-400">
<span className="inline-flex items-center bg-purple-500/10 px-1.5 py-0.5 text-display text-xs font-medium tracking-wider text-purple-600 dark:text-purple-400">
aux · {mainAuxTask}
</span>
)}
</div>
<div className="flex items-center gap-2 mt-1">
{provider && (
<Badge tone="secondary" className="text-[9px]">
<Badge tone="secondary" className="text-xs">
{provider}
</Badge>
)}
{caps.context_window && caps.context_window > 0 && (
<span className="text-[10px] text-muted-foreground">
<span className="text-xs text-text-secondary">
{formatTokenCount(caps.context_window)} ctx
</span>
)}
{caps.max_output_tokens && caps.max_output_tokens > 0 && (
<span className="text-[10px] text-muted-foreground">
<span className="text-xs text-text-secondary">
{formatTokenCount(caps.max_output_tokens)} out
</span>
)}
@@ -384,7 +384,7 @@ function ModelCard({
<div className="text-xs font-mono font-semibold">
{formatTokens(totalTokens)}
</div>
<div className="text-[10px] text-muted-foreground">
<div className="text-xs text-text-tertiary">
{t.models.tokens}
</div>
</div>
@@ -394,7 +394,7 @@ function ModelCard({
<div className="text-xs font-mono font-semibold">
{entry.sessions}
</div>
<div className="text-[10px] text-muted-foreground">
<div className="text-xs text-text-tertiary">
{t.models.sessions}
</div>
</div>
@@ -423,7 +423,7 @@ function ModelCard({
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="text-center">
<div className="font-mono font-semibold">{entry.sessions}</div>
<div className="text-[10px] text-muted-foreground">
<div className="text-xs text-text-tertiary">
{t.models.sessions}
</div>
</div>
@@ -431,7 +431,7 @@ function ModelCard({
<div className="font-mono font-semibold">
{formatTokens(entry.avg_tokens_per_session)}
</div>
<div className="text-[10px] text-muted-foreground">
<div className="text-xs text-text-tertiary">
{t.models.avgPerSession}
</div>
</div>
@@ -439,7 +439,7 @@ function ModelCard({
<div className="font-mono font-semibold">
{entry.api_calls > 0 ? formatTokens(entry.api_calls) : "—"}
</div>
<div className="text-[10px] text-muted-foreground">
<div className="text-xs text-text-tertiary">
{t.models.apiCalls}
</div>
</div>
@@ -447,7 +447,7 @@ function ModelCard({
</>
)}
<div className="flex items-center justify-between text-[10px] text-muted-foreground border-t border-border/30 pt-2">
<div className="flex items-center justify-between text-xs text-text-secondary border-t border-border/30 pt-2">
<div className="flex items-center gap-3">
{showTokens && entry.estimated_cost > 0 && (
<span className="flex items-center gap-0.5">
@@ -537,7 +537,7 @@ function AuxiliaryTasksModal({
<div className="flex items-center justify-between gap-3 pr-8">
<h2
id="aux-modal-title"
className="font-display text-base tracking-wider uppercase"
className="text-display font-display text-base tracking-wider"
>
Auxiliary Tasks
</h2>
@@ -546,13 +546,13 @@ function AuxiliaryTasksModal({
outlined
onClick={() => setConfirmReset(true)}
disabled={resetBusy}
className="text-[10px] h-6"
className="text-xs h-6"
prefix={resetBusy ? <Spinner /> : null}
>
Reset all to auto
</Button>
</div>
<p className="text-[10px] text-muted-foreground/80 mt-2">
<p className="text-xs text-text-secondary mt-2">
Auxiliary tasks handle side-jobs like vision, session search, and
compression. <span className="font-mono">auto</span> means
&quot;use the main model&quot;. Override per-task when you want a
@@ -573,11 +573,11 @@ function AuxiliaryTasksModal({
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-2">
<span className="text-xs font-medium">{t.label}</span>
<span className="text-[10px] text-muted-foreground/60">
<span className="text-xs text-text-tertiary">
{t.hint}
</span>
</div>
<div className="text-[10px] font-mono text-muted-foreground truncate">
<div className="text-xs font-mono text-text-secondary truncate">
{isAuto
? "auto (use main model)"
: `${cur?.provider} · ${cur?.model || "(provider default)"}`}
@@ -587,7 +587,7 @@ function AuxiliaryTasksModal({
size="sm"
outlined
onClick={() => setPicker({ kind: "aux", task: t.key })}
className="text-[10px] h-6"
className="text-xs h-6"
>
Change
</Button>
@@ -673,7 +673,7 @@ function ModelSettingsPanel({
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
<Settings2 className="h-4 w-4 shrink-0 text-muted-foreground" />
<CardTitle className="text-sm">Model Settings</CardTitle>
<span className="max-w-full min-w-0 text-[10px] text-muted-foreground [overflow-wrap:anywhere]">
<span className="max-w-full min-w-0 text-xs text-text-secondary [overflow-wrap:anywhere]">
applies to new sessions
</span>
</div>
@@ -685,11 +685,11 @@ function ModelSettingsPanel({
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 mb-0.5">
<Star className="h-3 w-3 text-primary" />
<span className="text-xs font-medium uppercase tracking-wider">
<span className="text-display text-xs font-medium tracking-wider">
Main model
</span>
</div>
<div className="text-xs font-mono text-muted-foreground truncate">
<div className="text-xs font-mono text-text-secondary truncate">
{mainProv || "(unset)"}
{mainProv && mainModel && " · "}
{mainModel || "(unset)"}
@@ -708,12 +708,12 @@ function ModelSettingsPanel({
<div className="flex min-w-0 flex-col gap-2 bg-muted/20 border border-border/50 px-3 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 mb-0.5">
<Cpu className="h-3 w-3 text-muted-foreground" />
<span className="text-xs font-medium uppercase tracking-wider">
<Cpu className="h-3 w-3 text-text-tertiary" />
<span className="text-display text-xs font-medium tracking-wider">
Auxiliary tasks
</span>
</div>
<div className="text-xs font-mono text-muted-foreground truncate">
<div className="text-xs font-mono text-text-secondary truncate">
{auxOverrideCount > 0
? `${auxOverrideCount} override${auxOverrideCount > 1 ? "s" : ""} · ${AUX_TASKS.length - auxOverrideCount} auto`
: `${AUX_TASKS.length} tasks · all auto`}
@@ -821,7 +821,7 @@ export default function ModelsPage() {
setAfterTitle(
<span className="flex items-center gap-2">
{loading && <Spinner className="shrink-0 text-base text-primary" />}
<Badge tone="secondary" className="text-[10px]">
<Badge tone="secondary" className="text-xs">
{periodLabel}
</Badge>
</span>,
@@ -924,7 +924,7 @@ export default function ModelsPage() {
/>
</div>
{!showTokens && (
<p className="mt-4 text-[10px] text-muted-foreground/70 leading-relaxed">
<p className="mt-4 text-xs text-text-tertiary leading-relaxed">
Token & cost analytics are hidden because the local counts
exclude auxiliary calls (compression, vision, web extract,
) and provider retries, so they diverge from your provider
@@ -975,7 +975,7 @@ export default function ModelsPage() {
<div className="flex flex-col items-center text-muted-foreground">
<Cpu className="h-8 w-8 mb-3 opacity-40" />
<p className="text-sm font-medium">{t.models.noModelsData}</p>
<p className="text-xs mt-1 text-muted-foreground/60">
<p className="text-xs mt-1 text-text-tertiary">
{t.models.startSession}
</p>
</div>
+16 -16
View File
@@ -160,7 +160,7 @@ export default function PluginsPage() {
<Card>
<CardHeader>
<CardTitle>{t.pluginsPage.providersHeading}</CardTitle>
<p className="text-[0.7rem] tracking-[0.08em] text-midground/55 normal-case">
<p className="text-xs tracking-[0.08em] text-text-tertiary">
{t.pluginsPage.providersHint}
</p>
</CardHeader>
@@ -227,7 +227,7 @@ export default function PluginsPage() {
<Card>
<CardHeader>
<CardTitle>{t.pluginsPage.installHeading}</CardTitle>
<p className="text-[0.7rem] tracking-[0.08em] text-midground/55 normal-case">
<p className="text-xs tracking-[0.08em] text-text-tertiary">
{t.pluginsPage.installHint}
</p>
</CardHeader>
@@ -240,7 +240,7 @@ export default function PluginsPage() {
<Label htmlFor="install-url">{t.pluginsPage.identifierLabel}</Label>
<Input
className="normal-case font-sans lowercase"
className="font-sans lowercase"
id="install-url"
placeholder="owner/repo or https://..."
spellCheck={false}
@@ -256,7 +256,7 @@ export default function PluginsPage() {
<Switch checked={installForce} onCheckedChange={setInstallForce} />
<span className="text-[0.7rem] tracking-[0.06em] text-midforeground/85 normal-case">
<span className="text-xs tracking-[0.06em] text-text-secondary">
{t.pluginsPage.forceReinstall}
</span>
</div>
@@ -265,7 +265,7 @@ export default function PluginsPage() {
<Switch checked={installEnable} onCheckedChange={setInstallEnable} />
<span className="text-[0.7rem] tracking-[0.06em] text-midforeground/85 normal-case">
<span className="text-xs tracking-[0.06em] text-text-secondary">
{t.pluginsPage.enableAfterInstall}
</span>
</div>
@@ -281,11 +281,11 @@ export default function PluginsPage() {
{t.pluginsPage.installBtn}
</Button>
<p className="text-[0.65rem] tracking-[0.06em] text-midforeground/55 normal-case">
<p className="text-xs tracking-[0.06em] text-text-tertiary">
{t.pluginsPage.rescanHint}
</p>
<p className="text-[0.65rem] tracking-[0.06em] text-midforeground/55 normal-case">
<p className="text-xs tracking-[0.06em] text-text-tertiary">
{t.pluginsPage.removeHint}
</p>
</CardContent>
@@ -293,20 +293,20 @@ export default function PluginsPage() {
<div className="flex flex-col gap-3">
<h3 className="font-mondwest text-[0.75rem] tracking-[0.12em] text-midground/85">
<h3 className="font-mondwest text-display text-xs tracking-[0.12em] text-text-secondary">
{t.pluginsPage.pluginListHeading}
</h3>
{loading ? (
<div className="flex items-center gap-2 py-8 text-[0.8rem] text-midforeground/65">
<div className="flex items-center gap-2 py-8 text-xs text-text-tertiary">
<Spinner />
<span>{t.common.loading}</span>
</div>
) : rows.length === 0 ? (
<p className="text-[0.75rem] text-midforeground/55 normal-case">{t.common.noResults}</p>
<p className="text-xs text-text-tertiary">{t.common.noResults}</p>
) : (
<ul className="flex flex-col gap-3">
@@ -331,7 +331,7 @@ export default function PluginsPage() {
<div className="flex flex-col gap-3 opacity-95">
<h3 className="font-mondwest text-[0.75rem] tracking-[0.12em] text-midforeground/85">
<h3 className="font-mondwest text-display text-xs tracking-[0.12em] text-text-secondary">
{t.pluginsPage.orphanHeading}
</h3>
@@ -339,7 +339,7 @@ export default function PluginsPage() {
{hub!.orphan_dashboard_plugins.map((m) => (
<li className="text-[0.7rem] normal-case opacity-85" key={m.name}>
<li className="text-xs text-text-secondary" key={m.name}>
{m.label ?? m.name} {m.description || m.tab?.path}
@@ -470,7 +470,7 @@ function PluginRowCard(props: PluginRowCardProps) {
className={cn(
"inline-flex items-center rounded-none px-3 py-1.5",
"border border-current/25 hover:bg-current/10",
"font-mondwest text-[0.65rem] tracking-[0.1em] uppercase",
"font-mondwest text-display text-xs tracking-[0.1em]",
)}
to={tabPath}
>
@@ -535,14 +535,14 @@ function PluginRowCard(props: PluginRowCardProps) {
</div>
{row.description ? (
<p className="min-w-0 w-full text-[0.7rem] tracking-[0.06em] text-midforeground/75 normal-case break-words">
<p className="min-w-0 w-full text-xs tracking-[0.06em] text-text-secondary break-words">
{row.description}
</p>
) : null}
{dm?.slots?.length ? (
<p className="text-[0.65rem] tracking-[0.05em] text-midforeground/55 normal-case">
<p className="text-xs tracking-[0.05em] text-text-tertiary">
{t.pluginsPage.dashboardSlots}: {dm.slots.join(", ")}
</p>
) : null}
@@ -557,7 +557,7 @@ function PluginRowCard(props: PluginRowCardProps) {
{!row.has_dashboard_manifest && !dm ? (
<p className="text-[0.65rem] italic text-midforeground/45 normal-case">
<p className="text-xs italic text-text-disabled">
{t.pluginsPage.noDashboardTab}
</p>
) : null}
+1 -4
View File
@@ -242,10 +242,7 @@ export default function ProfilesPage() {
}
return (
// Profile names, model slugs, and paths are case-sensitive; opt out of
// the app shell's global ``uppercase`` so they render as the user typed.
// Children that explicitly opt back in (Badges, etc.) keep their casing.
<div className="flex flex-col gap-6 normal-case">
<div className="flex flex-col gap-6">
<Toast toast={toast} />
<DeleteConfirmDialog
+11 -11
View File
@@ -83,7 +83,7 @@ function SnippetHighlight({ snippet }: { snippet: string }) {
parts.push(snippet.slice(last));
}
return (
<p className="mt-0.5 min-w-0 max-w-full truncate text-xs text-muted-foreground/80">
<p className="mt-0.5 min-w-0 max-w-full truncate text-xs text-text-secondary">
{parts}
</p>
);
@@ -191,12 +191,12 @@ function MessageBubble({
<div className="flex items-center gap-2 mb-1">
<span className={`text-xs font-semibold ${style.text}`}>{label}</span>
{isHit && (
<Badge tone="warning" className="text-[9px] py-0 px-1.5">
<Badge tone="warning" className="text-xs py-0 px-1.5">
{t.common.match}
</Badge>
)}
{msg.timestamp && (
<span className="text-[10px] text-muted-foreground">
<span className="text-xs text-text-tertiary">
{timeAgo(msg.timestamp)}
</span>
)}
@@ -322,7 +322,7 @@ function SessionRow({
: t.sessions.untitledSession}
</span>
{session.is_active && (
<Badge tone="success" className="shrink-0 text-[10px]">
<Badge tone="success" className="shrink-0 text-xs">
<span className="mr-1 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
{t.common.live}
</Badge>
@@ -350,7 +350,7 @@ function SessionRow({
</div>
{snippet && <SnippetHighlight snippet={snippet} />}
<div className="flex flex-wrap items-center gap-2">
<Badge tone="outline" className="text-[10px]">
<Badge tone="outline" className="text-xs">
{session.source ?? "local"}
</Badge>
{resumeInChatEnabled && (
@@ -692,7 +692,7 @@ export default function SessionsPage() {
? "destructive"
: "outline"
}
className="text-[10px] shrink-0"
className="text-xs shrink-0"
>
{actionStatus?.running
? t.status.running
@@ -708,7 +708,7 @@ export default function SessionsPage() {
ghost
size="icon"
onClick={dismissLog}
className="shrink-0 opacity-60 hover:opacity-100"
className="shrink-0 text-text-secondary hover:text-foreground"
aria-label={t.common.close}
>
<X />
@@ -717,7 +717,7 @@ export default function SessionsPage() {
<pre
ref={logScrollRef}
className="max-h-72 overflow-auto px-3 py-2 font-mono-ui text-[11px] leading-relaxed whitespace-pre-wrap break-all"
className="max-h-72 overflow-auto px-3 py-2 font-mono-ui text-xs leading-relaxed whitespace-pre-wrap break-all"
>
{actionStatus?.lines && actionStatus.lines.length > 0
? actionStatus.lines.join("\n")
@@ -761,7 +761,7 @@ export default function SessionsPage() {
</span>
{s.preview && (
<p className="min-w-0 max-w-full text-xs leading-snug text-muted-foreground/70 [overflow-wrap:anywhere]">
<p className="min-w-0 max-w-full text-xs leading-snug text-text-tertiary [overflow-wrap:anywhere]">
{s.preview}
</p>
)}
@@ -769,7 +769,7 @@ export default function SessionsPage() {
<Badge
tone="outline"
className="shrink-0 self-start text-[10px] sm:self-center"
className="shrink-0 self-start text-xs sm:self-center"
>
<Database className="mr-1 h-3 w-3" />
{s.source ?? "local"}
@@ -787,7 +787,7 @@ export default function SessionsPage() {
{search ? t.sessions.noMatch : t.sessions.noSessions}
</p>
{!search && (
<p className="text-xs mt-1 text-muted-foreground/60">
<p className="text-xs mt-1 text-text-tertiary">
{t.sessions.startConversation}
</p>
)}
+14 -14
View File
@@ -258,8 +258,8 @@ export default function SkillsPage() {
<div className="sm:sticky sm:top-0">
<div className="flex flex-col rounded-none border border-border bg-muted/20">
<div className="hidden sm:flex items-center gap-2 px-3 py-2 border-b border-border">
<Filter className="h-3 w-3 text-muted-foreground" />
<span className="font-mondwest text-[0.65rem] tracking-[0.12em] uppercase text-muted-foreground">
<Filter className="h-3 w-3 text-text-tertiary" />
<span className="font-mondwest text-display text-xs tracking-[0.12em] text-text-secondary">
{t.skills.filters}
</span>
</div>
@@ -290,7 +290,7 @@ export default function SkillsPage() {
!isSearching &&
allCategories.length > 0 && (
<div className="hidden sm:flex flex-col border-t border-border">
<div className="px-3 pt-2 pb-1 font-mondwest text-[0.6rem] tracking-[0.12em] uppercase text-muted-foreground/70">
<div className="px-3 pt-2 pb-1 font-mondwest text-display text-xs tracking-[0.12em] text-text-tertiary">
{t.skills.categories}
</div>
<div className="flex flex-col p-2 pt-1 gap-px max-h-[calc(100vh-340px)] overflow-y-auto">
@@ -304,14 +304,14 @@ export default function SkillsPage() {
onClick={() =>
setActiveCategory(isActive ? null : key)
}
className="rounded-none px-2 py-1 text-[11px]"
className="rounded-none px-2 py-1 text-xs"
>
<span className="flex-1 truncate">{name}</span>
<span
className={`text-[10px] tabular-nums ${
className={`text-xs tabular-nums ${
isActive
? "text-foreground/60"
: "text-muted-foreground/50"
? "text-text-secondary"
: "text-text-tertiary"
}`}
>
{count}
@@ -335,7 +335,7 @@ export default function SkillsPage() {
<Search className="h-4 w-4" />
{t.skills.title}
</CardTitle>
<Badge tone="secondary" className="text-[10px]">
<Badge tone="secondary" className="text-xs">
{t.skills.resultCount
.replace("{count}", String(searchMatchedSkills.length))
.replace(
@@ -379,7 +379,7 @@ export default function SkillsPage() {
)
: t.skills.all}
</CardTitle>
<Badge tone="secondary" className="text-[10px]">
<Badge tone="secondary" className="text-xs">
{t.skills.skillCount
.replace("{count}", String(activeSkills.length))
.replace("{s}", activeSkills.length !== 1 ? "s" : "")}
@@ -437,18 +437,18 @@ export default function SkillsPage() {
</span>
<Badge
tone={ts.enabled ? "success" : "outline"}
className="text-[10px]"
className="text-xs"
>
{ts.enabled
? t.common.active
: t.common.inactive}
</Badge>
</div>
<p className="text-xs text-muted-foreground mb-2">
<p className="text-xs text-text-secondary mb-2">
{ts.description}
</p>
{ts.enabled && !ts.configured && (
<p className="text-[10px] text-amber-300/80 mb-2">
<p className="text-xs text-amber-300 mb-2">
{t.skills.setupNeeded}
</p>
)}
@@ -458,7 +458,7 @@ export default function SkillsPage() {
<Badge
key={tool}
tone="secondary"
className="text-[10px] font-mono"
className="text-xs font-mono"
>
{tool}
</Badge>
@@ -466,7 +466,7 @@ export default function SkillsPage() {
</div>
)}
{ts.tools.length === 0 && (
<span className="text-[10px] text-muted-foreground/60">
<span className="text-xs text-text-tertiary">
{ts.enabled
? t.skills.toolsetLabel.replace(
"{name}",
+2 -2
View File
@@ -35,7 +35,7 @@ export function PluginPage({ name }: { name: string }) {
<div
className={cn(
"max-w-lg p-4",
"font-mondwest text-sm tracking-[0.08em] text-midground/80",
"font-mondwest text-sm tracking-[0.08em] text-text-secondary",
)}
role="alert"
>
@@ -48,7 +48,7 @@ export function PluginPage({ name }: { name: string }) {
<div
className={cn(
"flex items-center gap-2 p-4",
"font-mondwest text-sm tracking-[0.1em] text-midground/60",
"font-mondwest text-sm tracking-[0.1em] text-text-tertiary",
)}
>
<Spinner className="shrink-0" />