From b5fb8da7ea19cb19c48f17f58e25de669ce093ce Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Tue, 19 May 2026 12:39:09 -0400 Subject: [PATCH] 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 --- web/README.md | 40 ++++++++++++ web/package.json | 2 +- web/src/App.tsx | 28 ++++----- web/src/components/AutoField.tsx | 4 +- web/src/components/BottomPickSheet.tsx | 2 +- web/src/components/ChatSidebar.tsx | 10 +-- web/src/components/LanguageSwitcher.tsx | 4 +- web/src/components/ModelInfoCard.tsx | 12 ++-- web/src/components/ModelPickerDialog.tsx | 4 +- web/src/components/OAuthProvidersCard.tsx | 16 ++--- web/src/components/SidebarFooter.tsx | 4 +- web/src/components/SidebarStatusStrip.tsx | 12 ++-- web/src/components/SlashPopover.tsx | 2 +- web/src/components/ThemeSwitcher.tsx | 10 +-- web/src/components/ToolCall.tsx | 10 +-- web/src/index.css | 6 +- web/src/pages/AnalyticsPage.tsx | 10 +-- web/src/pages/ChatPage.tsx | 17 +++--- web/src/pages/ConfigPage.tsx | 18 +++--- web/src/pages/EnvPage.tsx | 34 +++++------ web/src/pages/LogsPage.tsx | 6 +- web/src/pages/ModelsPage.tsx | 74 +++++++++++------------ web/src/pages/PluginsPage.tsx | 32 +++++----- web/src/pages/ProfilesPage.tsx | 5 +- web/src/pages/SessionsPage.tsx | 22 +++---- web/src/pages/SkillsPage.tsx | 28 ++++----- web/src/plugins/PluginPage.tsx | 4 +- 27 files changed, 227 insertions(+), 189 deletions(-) diff --git a/web/README.md b/web/README.md index d8127f96e0..1e533834ff 100644 --- a/web/README.md +++ b/web/README.md @@ -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 ``s, `

`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 ` {t.app.brand} @@ -498,7 +498,7 @@ export default function App() { 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" > @@ -542,7 +542,7 @@ export default function App() { @@ -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 }) { {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 ? ( diff --git a/web/src/components/AutoField.tsx b/web/src/components/AutoField.tsx index 0f96d42042..4e3451c10f 100644 --- a/web/src/components/AutoField.tsx +++ b/web/src/components/AutoField.tsx @@ -11,8 +11,8 @@ function FieldHint({ schema, schemaKey }: { schema: Record; sch return (

- {keyPath && {keyPath}} - {description && {description}} + {keyPath && {keyPath}} + {description && {description}}
); } diff --git a/web/src/components/BottomPickSheet.tsx b/web/src/components/BottomPickSheet.tsx index 1490f4090c..459857eada 100644 --- a/web/src/components/BottomPickSheet.tsx +++ b/web/src/components/BottomPickSheet.tsx @@ -200,7 +200,7 @@ export function BottomPickSheet({ {title} diff --git a/web/src/components/ChatSidebar.tsx b/web/src/components/ChatSidebar.tsx index c311673faf..a115d887ec 100644 --- a/web/src/components/ChatSidebar.tsx +++ b/web/src/components/ChatSidebar.tsx @@ -304,13 +304,13 @@ export function ChatSidebar({ channel, className }: ChatSidebarProps) { return (