From 8d9e6574cc4448f12dbd8f6fd002f3a7f49fac1a Mon Sep 17 00:00:00 2001 From: Anso Date: Mon, 22 Jun 2026 00:19:57 -0400 Subject: [PATCH] feat(appearance): add Calm/Signature visual style, readability mode, and chart palette (#1407) * feat(appearance): add Calm/Signature visual style, readability mode, and chart palette Turn the "too intense / italic headers hurt / the security graph fights my eyes" feedback into a token-driven Visual style with Calm as the new default and Signature one click back to the prior look. - Heading family routes through a `.font-heading` utility driven by `--font-heading`/`--heading-style`: operational headings render upright in the interface face under Calm and italic Instrument Serif under Signature. Base rule sets family + style only, so each call site keeps its own weight/tracking and Signature stays a true no-op; the Calm lift is a `[data-headings="clean"]` descendant rule. Brand lockup, empty-state heroes, and onboarding stay serif. - Severity charts resolve through `--sev-*` tokens with Muted, Heat, and Signature palettes; FindingsByType routes its series through the severity ramp plus a neutral so no brand-cyan sits next to rose. The risk trend flattens its gradient under Muted/Heat/reduced and keeps the gradient under Signature. - Appearance settings gain Visual style cards, a Security visualization palette, a Readability master toggle, a Motion & effects group, and a "Reset to default" button (restores the Calm axes, disabled while readability is on). Contrast moves under Readability and Ambient glow under Motion & effects. A card is selected only while the stored sub-axes match its preset, so a custom combination de-selects both. - The topbar Theme quick-switch swaps the interface/data font pickers for a Visual style switch and a Readability toggle (text size kept); its footer Settings link jumps straight to Appearance. - Readability is a sticky master that forces the calm resolution and a contrast lift at apply time without mutating the stored sub-axes. - New users default to Calm; any pre-existing persisted appearance state keeps the Signature look. The pre-paint script mirrors the store. - SegmentedControl gains a `disabled` prop and a nullable value (no active segment for a custom combination, with a roving-tabindex keyboard anchor). Adds unit/component coverage for the store, migration, chart shape logic, the disabled control, the reset/de-selection, and the quick-switch. * fix(appearance): migrate Blueprint serif headings and surface readability locks - Migrate the two operational Blueprint headings (catalog tile name, drift-policy option title) from font-serif italic to the .font-heading utility; the first pass only covered font-display, so Calm still left these italic. font-serif and font-display both resolve to the same display face, so this is the same fix. - Lock the Visual style cards under Readability (parity with the topbar switch and the on-screen guidance to turn Readability off to choose a style by hand). - Lock the Border brightness slider under Readability and show its forced +0.03 readout, since Readability overrides the stored value; dragging it previously appeared to do nothing. - Correct the Appearance docs sentence for the topbar quick switch (it listed fonts; the quick switch now carries visual style, readability, and text size). --- docs/features/appearance.mdx | 53 ++-- docs/reference/settings.mdx | 23 +- frontend/public/theme-init.js | 37 ++- frontend/src/components/AuditLogView.tsx | 2 +- .../components/AutoUpdateReadinessView.tsx | 10 +- frontend/src/components/EditorLayout.tsx | 4 +- .../EditorLayout/editor-view-blocks.tsx | 2 +- frontend/src/components/NodeSwitcher.tsx | 2 +- frontend/src/components/NotificationPanel.tsx | 2 +- .../components/ScheduledOperationsView.tsx | 2 +- .../blueprints/BlueprintCatalog.tsx | 2 +- .../components/blueprints/BlueprintEditor.tsx | 2 +- .../components/dashboard/HealthStatusBar.tsx | 2 +- .../components/dashboard/StackHealthTable.tsx | 2 +- .../src/components/fleet/FleetMasthead.tsx | 2 +- frontend/src/components/fleet/RoutingTab.tsx | 2 +- .../src/components/mobile/MobileFleet.tsx | 2 +- .../src/components/mobile/MobileSettings.tsx | 2 +- frontend/src/components/mobile/mobile-ui.tsx | 2 +- .../security/SecurityCharts.test.tsx | 146 +++++++++ .../components/security/SecurityCharts.tsx | 82 ++++-- .../components/settings/AppearanceSection.tsx | 276 +++++++++++++++--- .../__tests__/AppearanceSection.test.tsx | 102 +++++++ .../settings/__tests__/registry.test.ts | 7 + frontend/src/components/settings/registry.ts | 4 +- .../src/components/theme/ThemePreview.tsx | 2 +- .../theme/ThemeQuickSwitch.test.tsx | 48 +++ .../src/components/theme/ThemeQuickSwitch.tsx | 106 ++++--- frontend/src/components/ui/PageMasthead.tsx | 2 +- .../src/components/ui/fleet-action-card.tsx | 2 +- frontend/src/components/ui/modal.tsx | 2 +- .../src/components/ui/routing-node-card.tsx | 6 +- .../components/ui/segmented-control.test.tsx | 48 +++ .../src/components/ui/segmented-control.tsx | 19 +- frontend/src/components/ui/system-sheet.tsx | 2 +- frontend/src/hooks/use-theme.read.test.ts | 47 +++ frontend/src/hooks/use-theme.test.ts | 88 +++++- frontend/src/hooks/use-theme.ts | 146 ++++++++- frontend/src/index.css | 67 +++++ 39 files changed, 1191 insertions(+), 166 deletions(-) create mode 100644 frontend/src/components/security/SecurityCharts.test.tsx create mode 100644 frontend/src/components/settings/__tests__/AppearanceSection.test.tsx create mode 100644 frontend/src/components/theme/ThemeQuickSwitch.test.tsx create mode 100644 frontend/src/components/ui/segmented-control.test.tsx diff --git a/docs/features/appearance.mdx b/docs/features/appearance.mdx index fd3b7c52..f081cc58 100644 --- a/docs/features/appearance.mdx +++ b/docs/features/appearance.mdx @@ -1,16 +1,45 @@ --- title: Appearance -description: Personalize Sencho's look. Choose a theme and accent, fine-tune contrast, borders, and glow, swap the interface and data fonts, and set the text size. Every choice is saved to the current browser. +description: Personalize Sencho's look. Choose a visual style, tune readability and the security charts, pick a theme and accent, swap the interface and data fonts, and set the text size. Every choice is saved to the current browser. --- -**Settings → Appearance** is where you make Sencho look the way you want. Pick a theme and accent color, fine-tune contrast and the ambient glow, choose the interface and data fonts, and scale the text. A live preview at the top of the section reflects every change as you make it. +**Settings → Appearance** is where you make Sencho look the way you want. Choose between the Calm and Signature visual styles, dial in readability, set how the security charts are colored, pick a theme and accent, choose the interface and data fonts, and scale the text. A live preview reflects every change as you make it. -Most of these controls are also one click away from anywhere in the app: the **palette button** in the top bar, between the search and notification icons, opens a quick switcher for the theme mode, accent, and fonts. +Several of these controls are also one click away from anywhere in the app: the **palette button** in the top bar, between the search and notification icons, opens a quick switcher for the theme mode, accent, visual style, readability, and text size. Appearance choices are saved to **this browser only**. They follow you across tabs on the same browser, and every device remembers its own look. Nothing here changes what other operators see. +## Visual style + +The **Visual style** is the master switch for how intense the interface feels: + +- **Calm** (the default) keeps headings upright in the interface font, colors the security charts with a muted palette, and reduces material effects. It is the most readable setting. +- **Signature** uses italic Instrument Serif headings and the fully saturated chart palette for a more editorial look. + +Picking a style sets the heading, chart, and effects controls below in one move. You are free to adjust any of them afterward to build your own combination. When **Readability mode** is on it overrides these until you turn it off. + +## Readability + +**Readability mode** is a single switch for the most legible result: upright headings, the muted chart palette, reduced effects, and a small contrast lift, all at once. While it is on, the heading and chart controls are locked to their calm values. Turn it off to set them by hand. + +- **Header style** chooses **Clean** (the upright interface face) or **Signature** (italic Instrument Serif). Decorative moments such as the Sencho wordmark stay in the serif either way. +- **Contrast** is a master control that spreads the page tone, every border, and the text tiers together. Higher contrast darkens the page, brightens borders, and crisps up secondary text. Pairing high contrast with the OLED theme gives the sharpest reading on a true-black panel. + +## Security visualization + +The **Chart palette** sets the colors used on the Security page charts: + +- **Muted** desaturates the severity colors so critical, high, medium, and low read as a calm progression with no jarring color pairings. +- **Heat** is a single warm ramp from a deep red through amber, with no competing hues. +- **Signature** is the saturated severity set. + +## Motion & effects + +- **Reduced effects** flattens card bevels, the accent glow, and chart gradients for a calmer, flatter surface. +- **Ambient glow** sets the intensity of the soft, accent-tinted glow behind the page. Slide it to zero for a completely flat background. (Reduced effects already dims it.) + ## Theme The **Mode** control sets the overall surface palette: @@ -20,30 +49,18 @@ The **Mode** control sets the overall surface palette: - **Light** is the bright theme, with the same directional, opaque borders inverted for a white surface. - **Auto** follows your operating system, switching between Dim and Light as your OS does, and re-resolving live when the OS flips. -## Accent +The **accent** is Sencho's one data color. It drives charts, sparklines, progress bars, focus rings, active states, the masthead and sidebar rails, and the ambient page glow, so changing it recolors the whole product cohesively. Eight well-spaced hues are available: **Cyan** (the default and the signature), **Blue**, **Violet**, **Magenta**, **Orange**, **Amber**, **Lime**, and **Steel** (a deliberate near-neutral). Status colors (green for healthy, amber for warning, rose for error) stay constant regardless of accent so state always reads the same. -The **accent** is Sencho's one data color. It drives charts, sparklines, progress bars, focus rings, active states, the masthead and sidebar rails, and the ambient page glow, so changing it recolors the whole product cohesively. - -Eight well-spaced hues are available: **Cyan** (the default and the signature), **Blue**, **Violet**, **Magenta**, **Orange**, **Amber**, **Lime**, and **Steel** (a deliberate near-neutral). Status colors (green for healthy, amber for warning, rose for error) stay constant regardless of accent so state always reads the same. - -## Fine-tune - -Three sliders shape the surfaces and atmosphere. A **Reset fine-tune** button returns all three to their defaults. - -- **Contrast** is a master control that spreads the page tone, every border, and the text tiers together in one move. Higher contrast darkens the page, brightens borders, and crisps up secondary text. Pairing high contrast with the OLED theme gives the sharpest reading on a true-black panel. -- **Border brightness** lifts or softens every hairline on its own, so the separation between cards reads exactly how you like it. -- **Ambient glow** sets the intensity of the soft, accent-tinted glow behind the page. Slide it to zero for a completely flat background. +**Border brightness** lifts or softens every hairline on its own, so the separation between cards reads exactly how you like it. A **Reset fine-tune** button returns contrast, border brightness, and ambient glow to their defaults. ## Typography -Sencho uses three type contexts. The display face for hero headings stays fixed as the signature; the other two are yours to change. +Sencho uses three type contexts. The interface and data faces are yours to change; the heading style follows your Visual style choice. - **Interface font** sets the sans face used for body text, labels, navigation, and buttons. Choose **Geist** (default), **IBM Plex Sans**, or **Hanken Grotesk**. - **Data font** sets the monospace face used for the terminal, stat values, codes, and timestamps. Choose **Geist Mono** (default), **IBM Plex Mono**, or **Fira Code**. - **Text size** scales the entire interface from a single root multiplier. In Settings this is a continuous slider (default `1.00×`); the top-bar quick switcher offers **S / M / L / XL** presets. Both stay in sync. -Hero headings and featured names always render in Instrument Serif so the editorial voice stays consistent no matter which interface font you pick. - ## Display The **Display** group holds the remaining per-browser preferences: diff --git a/docs/reference/settings.mdx b/docs/reference/settings.mdx index 69b8031a..7885ad77 100644 --- a/docs/reference/settings.mdx +++ b/docs/reference/settings.mdx @@ -97,12 +97,33 @@ See [Two-Factor Authentication](/features/two-factor-authentication) for the enr **Scope:** This browser (preferences are saved to local storage) -Control how dense and how interactive the workspace feels. Each browser remembers its own choices so a compact laptop setup does not force the same rhythm on a larger desktop. +Control how Sencho looks and how dense the workspace feels. Each browser remembers its own choices so a compact laptop setup does not force the same rhythm on a larger desktop. See [Appearance](/features/appearance) for the full walkthrough. Appearance settings showing the Density selector +### Visual style and readability + +| Control | What it does | +|---------|--------------| +| **Visual style** | Master switch between **Calm** (upright headings, muted charts, reduced effects, the default) and **Signature** (italic Instrument Serif headings, saturated charts). | +| **Readability mode** | One switch for the most legible result: upright headings, muted charts, reduced effects, and a contrast lift. Locks the heading and chart controls while on. | +| **Header style** | Clean (upright interface face) or Signature (italic serif). | +| **Contrast** | Spreads the page tone, borders, and text tiers together. | + +### Charts and effects + +| Control | What it does | +|---------|--------------| +| **Chart palette** | Colors the Security page charts: **Muted**, **Heat** (one warm ramp), or **Signature** (saturated). | +| **Reduced effects** | Flattens card bevels, the accent glow, and chart gradients. | +| **Ambient glow** | Intensity of the accent-tinted page glow. | + +### Theme and typography + +The **Mode** (Dim, OLED, Light, Auto) and **Accent** (one of eight hues) set the surface palette and data color; **Border brightness** tunes every hairline. The **Interface font**, **Data font**, and **Text size** are yours to change; the heading style follows your Visual style choice. + ### Density | Value | When to pick it | diff --git a/frontend/public/theme-init.js b/frontend/public/theme-init.js index a89f039d..ed16da17 100644 --- a/frontend/public/theme-init.js +++ b/frontend/public/theme-init.js @@ -11,15 +11,30 @@ }; var UI_FONTS = { 'Geist': 1, 'IBM Plex Sans': 1, 'Hanken Grotesk': 1 }; var MONO_FONTS = { 'Geist Mono': 1, 'IBM Plex Mono': 1, 'Fira Code': 1 }; + var STYLES = { calm: 1, signature: 1 }; + var HEADINGS = { clean: 1, signature: 1 }; + var CHARTS = { muted: 1, heat: 1, signature: 1 }; + // Calm/Readability: a fresh user (absent key) gets CALM via DEFAULTS; an + // existing stored object fills missing appearance fields from SIGNATURE so a + // returning user looks unchanged. Mirrors the presets in src/hooks/use-theme.ts. + var SIG = { + visualStyle: 'signature', headingStyle: 'signature', chartStyle: 'signature', + reducedEffects: false, readability: false, + }; var DEFAULTS = { theme: 'dim', accent: 'cyan', borderBoost: 0, glow: 0.16, contrast: 0, uiFont: 'Geist', monoFont: 'Geist Mono', typeScale: 1, + visualStyle: 'calm', headingStyle: 'clean', chartStyle: 'muted', + reducedEffects: true, readability: false, }; function num(v, min, max, def) { if (typeof v !== 'number' || !isFinite(v)) return def; return Math.min(max, Math.max(min, v)); } + function bool(v, def) { + return typeof v === 'boolean' ? v : def; + } function read() { try { @@ -36,15 +51,22 @@ uiFont: UI_FONTS[p.uiFont] ? p.uiFont : DEFAULTS.uiFont, monoFont: MONO_FONTS[p.monoFont] ? p.monoFont : DEFAULTS.monoFont, typeScale: num(p.typeScale, 0.88, 1.2, DEFAULTS.typeScale), + visualStyle: STYLES[p.visualStyle] ? p.visualStyle : SIG.visualStyle, + headingStyle: HEADINGS[p.headingStyle] ? p.headingStyle : SIG.headingStyle, + chartStyle: CHARTS[p.chartStyle] ? p.chartStyle : SIG.chartStyle, + reducedEffects: bool(p.reducedEffects, SIG.reducedEffects), + readability: bool(p.readability, SIG.readability), }; } } // Legacy migration: old string key held only the mode (dark mapped to dim). + // A legacy key is a returning user, so the appearance axes fill from Signature. var legacy = localStorage.getItem(LEGACY_KEY); var legacyTheme = legacy === 'light' || legacy === 'auto' ? legacy : legacy === 'dark' ? 'dim' : null; if (legacyTheme) { var migrated = {}; for (var k in DEFAULTS) migrated[k] = DEFAULTS[k]; + for (var a in SIG) migrated[a] = SIG[a]; migrated.theme = legacyTheme; return migrated; } @@ -66,11 +88,20 @@ var s = read(); var resolved = resolveTheme(s.theme); var root = document.documentElement; + // Readability is a sticky master that forces the calm resolution + a contrast + // lift; otherwise the stored sub-axes apply. Resolved here only (no write-back). + var rd = s.readability; + var headings = rd ? 'clean' : s.headingStyle; + var chart = rd ? 'muted' : s.chartStyle; + var reduced = rd || s.reducedEffects; root.dataset.theme = resolved; root.dataset.accent = s.accent; - root.style.setProperty('--border-boost', String(s.borderBoost)); - root.style.setProperty('--glow', String(s.glow)); - root.style.setProperty('--contrast', String(s.contrast)); + root.dataset.headings = headings; + root.dataset.chartStyle = chart; + if (reduced) root.dataset.effects = 'reduced'; else delete root.dataset.effects; + root.style.setProperty('--border-boost', String(rd ? 0.03 : s.borderBoost)); + root.style.setProperty('--glow', String(reduced ? s.glow * 0.4 : s.glow)); + root.style.setProperty('--contrast', String(s.contrast + (rd ? 0.18 : 0))); root.style.setProperty('--ui-font', "'" + s.uiFont + "'"); root.style.setProperty('--mono-font', "'" + s.monoFont + "'"); root.style.setProperty('--type-scale', String(s.typeScale)); diff --git a/frontend/src/components/AuditLogView.tsx b/frontend/src/components/AuditLogView.tsx index 7a3fc3cb..4c99d92b 100644 --- a/frontend/src/components/AuditLogView.tsx +++ b/frontend/src/components/AuditLogView.tsx @@ -573,7 +573,7 @@ function StreamView({ stats, showStats, loading, groups, now, page, totalPages, {tile.label}
- + {tile.value === null ? '·' : idx === 2 diff --git a/frontend/src/components/AutoUpdateReadinessView.tsx b/frontend/src/components/AutoUpdateReadinessView.tsx index a5da3729..54adc7a4 100644 --- a/frontend/src/components/AutoUpdateReadinessView.tsx +++ b/frontend/src/components/AutoUpdateReadinessView.tsx @@ -206,7 +206,7 @@ function StackReadinessCard({ Stack - + {stack}
@@ -334,7 +334,7 @@ function ReadinessHero({ Fleet readiness - + {headline} {total > 0 && ( @@ -389,7 +389,7 @@ function NodeGroupSection({