diff --git a/docs/web-ui/css-vars.md b/docs/web-ui/css-vars.md index 36d54669..a7d42f22 100644 --- a/docs/web-ui/css-vars.md +++ b/docs/web-ui/css-vars.md @@ -17,23 +17,18 @@ reference the **semantic** names and must **not** redeclare them or reach for the raw `--baseNN` slots directly. (`base.css` holds only the shared `body` typography — it references the palette but no longer defines it.) -`colors.css` declares each palette's 16 hex values exactly once — -`--mocha-baseNN` (dark, Catppuccin Mocha) and `--latte-baseNN` (light, -Catppuccin-Latte-derived; `base08`-`base0F` are darkened from stock -Latte to actually clear WCAG AA against how this app uses them — real -contrast failures found in review, see the comment above those -declarations in `colors.css` for the numbers and why). Everything else -in the file just points the active `--baseNN` slot at one of those two -raw palettes via `var()`, never a second copy of a hex value: an -unconditional default (Mocha), a row gated behind `@media -(prefers-color-scheme: light)` (Latte, the OS/browser-level default), -and two `:root[data-theme='light'|'dark']` blocks a per-user override -sets (swarm-ui's `SettingsMenu`) — an attribute selector always -outranks the plain `:root` inside the media query, so the override wins -regardless of the actual OS preference. Absent an override, the media -query decides; that's what a client with no more specific theme -decision gets (see "Theme swapping" below for how a stylix deployment -supersedes all of this at once). +`colors.css` ships two rows of the same 16 slots: an unconditional +Catppuccin Mocha (dark) default, and a Catppuccin-Latte-derived (light) +row gated behind `@media (prefers-color-scheme: light)` — both use the +identical slot mapping, so which row is active never changes what a slot +means. The light row's `base00`-`base07` (surfaces/foreground) are stock +Latte hexes; `base08`-`base0F` (the eight chromatic slots) are darkened +from stock Latte to actually clear WCAG AA against how this app uses +them (real contrast failures found in review — see the comment above +those declarations in `colors.css` for the numbers and why). This is the +OS/browser-level default only; it's what a client with no more specific +theme decision gets (see "Theme swapping" below for how a stylix +deployment or a future per-user override supersede it). Both are deliberately standalone, not `@import`ed into the page bundles: each package re-exports them (`src/{colors,theme}.css` → @@ -132,13 +127,12 @@ to `theme.css` (the "Derives from" column above) — a generator never needs to know our var names, and `theme.css` + the page bundles stay untouched. -**Two palettes, one contract.** `colors.css` ships both a dark default +**Two rows, one contract.** `colors.css` ships both a dark default (Catppuccin Mocha, unconditional) and a light default (Catppuccin Latte, -behind `@media (prefers-color-scheme: light)`), plus a per-user override -path (`:root[data-theme]`) — a theme generator that overrides the file -wholesale (e.g. the stylix path, which writes a single unconditional -`:root` block with no media query, no `--mocha-*`/`--latte-*` vars, no -`[data-theme]` blocks) supersedes all of it at once, same as today. +behind `@media (prefers-color-scheme: light)`) — a theme generator that +overrides the file wholesale (e.g. the stylix path, which writes a single +unconditional `:root` block with no media query) supersedes both rows at +once, same as today. `colors.css` base16 slot defaults (Catppuccin Mocha, dark default): diff --git a/frontend/packages/shared/src/colors.css b/frontend/packages/shared/src/colors.css index bf78ee55..25181d13 100644 --- a/frontend/packages/shared/src/colors.css +++ b/frontend/packages/shared/src/colors.css @@ -7,174 +7,93 @@ with nothing else to regenerate. Kept as its own standalone stylesheet (not @import-ed into theme.css) - so a swap replaces just this file — `theme.css` and the page bundles - stay untouched. Every page links this BEFORE `theme.css`. See - docs/web-ui/css-vars.md. - - Two raw palettes, one declaration each — `--mocha-baseNN` (dark, - Catppuccin Mocha) and `--latte-baseNN` (light, Catppuccin-Latte- - derived; base08-0F are darkened from stock Latte, see the comment - above those declarations for why). The four blocks below this one - never restate a hex value — each just points the active `--baseNN` - slot at one of these two raw palettes via `var()`, so "which theme is - active" and "what a theme's colours are" can't drift apart the way a - hand-duplicated copy could. */ + so a swap replaces just this ~16-line file — `theme.css` and the page + bundles stay untouched. Every page links this BEFORE `theme.css`. The + hex defaults are Catppuccin Mocha. Standard base16 slot meanings are + in the trailing comments. See docs/web-ui/css-vars.md. */ :root { - --mocha-base00: #1e1e2e; /* default bg */ - --mocha-base01: #181825; /* lighter bg (elevated surfaces) */ - --mocha-base02: #313244; /* selection / surface — borders */ - --mocha-base03: #45475a; /* comments / dim surface */ - --mocha-base04: #585b70; /* dark foreground */ - --mocha-base05: #cdd6f4; /* default foreground */ - --mocha-base06: #f5e0dc; /* light foreground */ - --mocha-base07: #b4befe; /* lightest */ - --mocha-base08: #f38ba8; /* red */ - --mocha-base09: #fab387; /* orange / peach */ - --mocha-base0A: #f9e2af; /* yellow */ - --mocha-base0B: #a6e3a1; /* green */ - --mocha-base0C: #89dceb; /* cyan (our sky — note: Catppuccin's base0C is teal) */ - --mocha-base0D: #89b4fa; /* blue */ - --mocha-base0E: #cba6f7; /* magenta / mauve */ - --mocha-base0F: #f5c2e7; /* extra accent — pink */ - - --latte-base00: #eff1f5; /* default bg */ - --latte-base01: #e6e9ef; /* lighter bg (elevated surfaces) */ - --latte-base02: #ccd0da; /* selection / surface — borders */ - --latte-base03: #bcc0cc; /* comments / dim surface */ - --latte-base04: #acb0be; /* dark foreground */ - --latte-base05: #4c4f69; /* default foreground */ - --latte-base06: #dc8a78; /* light foreground */ - --latte-base07: #7287fd; /* lightest */ - /* base08-0F: darkened from stock Catppuccin Latte's own accent hexes - (same hue/saturation, lower HSL lightness), not a literal upstream - port like base00-07 above. A review round computed real WCAG - contrast ratios and found the literal Latte accents fail badly as - `StatusChip` fill-text (green/amber/red/yellow on - `--purple-dim`/base03: 1.4:1-3:1, need 4.5:1) and even as plain - text on `--bg`/base00 in the agent/dashboard packages that also - consume this same file (2.3:1-4.8:1). Root cause: Latte's own - accent colors are calibrated against Latte's near-white - `base`/`crust`, not against a mid-gray `surface1` — the Mocha - palette doesn't have this problem because Mocha's pastel accents - are already *light*, so they contrast fine against a *dark* - surface1; Latte's saturated-but-mid-brightness accents don't have - the equivalent headroom against Latte's own (much lighter) - surface1. Each value below was picked by holding the stock Latte - hue+saturation fixed and binary-searching HSL lightness down to - the point real contrast against base03 clears 4.5:1 with a small - margin (also verified against base00: all land above 7:1 there) — - same hue identity, same base16 role, just dark/saturated enough to - actually be legible as filled-chip or plain text either way. - Yellow and peach read closer to olive/brown than a bright - yellow/orange once darkened this far — an inherent property of - darkening a warm hue in sRGB (Bezold-Brücke shift), not a mapping - mistake; boosting saturation further didn't rescue it (verified). */ - --latte-base08: #9c0b2a; /* red */ - --latte-base09: #883201; /* orange / peach */ - --latte-base0A: #6d450e; /* yellow */ - --latte-base0B: #235818; /* green */ - --latte-base0C: #025374; /* cyan (our sky — note: Catppuccin's base0C is teal) */ - --latte-base0D: #0843b8; /* blue */ - --latte-base0E: #6311ce; /* magenta / mauve */ - --latte-base0F: #8f166e; /* extra accent — pink */ - - /* Default activation: Mocha, unless the media query below or a - per-user override (further below) says otherwise. */ - --base00: var(--mocha-base00); - --base01: var(--mocha-base01); - --base02: var(--mocha-base02); - --base03: var(--mocha-base03); - --base04: var(--mocha-base04); - --base05: var(--mocha-base05); - --base06: var(--mocha-base06); - --base07: var(--mocha-base07); - --base08: var(--mocha-base08); - --base09: var(--mocha-base09); - --base0A: var(--mocha-base0A); - --base0B: var(--mocha-base0B); - --base0C: var(--mocha-base0C); - --base0D: var(--mocha-base0D); - --base0E: var(--mocha-base0E); - --base0F: var(--mocha-base0F); + --base00: #1e1e2e; /* default bg */ + --base01: #181825; /* lighter bg (elevated surfaces) */ + --base02: #313244; /* selection / surface — borders */ + --base03: #45475a; /* comments / dim surface */ + --base04: #585b70; /* dark foreground */ + --base05: #cdd6f4; /* default foreground */ + --base06: #f5e0dc; /* light foreground */ + --base07: #b4befe; /* lightest */ + --base08: #f38ba8; /* red */ + --base09: #fab387; /* orange / peach */ + --base0A: #f9e2af; /* yellow */ + --base0B: #a6e3a1; /* green */ + --base0C: #89dceb; /* cyan (our sky — note: Catppuccin's base0C is teal) */ + --base0D: #89b4fa; /* blue */ + --base0E: #cba6f7; /* magenta / mauve */ + --base0F: #f5c2e7; /* extra accent — pink */ } -/* OS/browser-level light-mode default. This is a *default*, not a user - preference store: it only applies when nothing more specific has - already decided the palette. A stylix-generated colors.css - (nix/host-modules/hive-c0re/theme.nix) replaces this entire file - wholesale — a fixed, unconditional `:root` block with no media query - in it at all — so a stylix-themed deployment is naturally unaffected - by, and never fights, this block. Some people need light for - low-vision/contrast reasons, others need dark for photosensitivity, - so respect the OS/browser signal by default the same way - `prefers-reduced-motion` already is elsewhere — rather than always - forcing the bundled dark palette on a client with no stylix session - of its own (e.g. a phone browser hitting swarm-ui directly). */ +/* Light-mode default. base00-07 (surfaces/foreground) are stock + Catppuccin Latte — no contrast issue there, they're the ends of the + bg/fg scale. base08-0F (the eight chromatic "tone" slots: red through + pink) are deliberately NOT stock Latte hexes — see the comment right + above those declarations below for why; don't reuse stock Catppuccin + Latte's own accent hexes there if this ever gets regenerated. + + This is a *default*, not a user preference store: it only applies + when nothing more specific has already decided the palette. A + stylix-generated colors.css (nix/host-modules/hive-c0re/theme.nix) + replaces this entire file wholesale — a fixed, unconditional `:root` + block with no media query in it at all — so a stylix-themed + deployment is naturally unaffected by, and never fights, this block. + Some people need light for low-vision/contrast reasons, others need + dark for photosensitivity, so respect the OS/browser signal by + default the same way `prefers-reduced-motion` already is elsewhere — + rather than always forcing the bundled dark palette on a client with + no stylix session of its own (e.g. a phone browser hitting swarm-ui + directly). A future per-user override takes precedence over this + media query by simply setting the vars later in the cascade (e.g. + via an inline `style` on `:root`, which always outranks a + stylesheet rule). */ @media (prefers-color-scheme: light) { :root { - --base00: var(--latte-base00); - --base01: var(--latte-base01); - --base02: var(--latte-base02); - --base03: var(--latte-base03); - --base04: var(--latte-base04); - --base05: var(--latte-base05); - --base06: var(--latte-base06); - --base07: var(--latte-base07); - --base08: var(--latte-base08); - --base09: var(--latte-base09); - --base0A: var(--latte-base0A); - --base0B: var(--latte-base0B); - --base0C: var(--latte-base0C); - --base0D: var(--latte-base0D); - --base0E: var(--latte-base0E); - --base0F: var(--latte-base0F); + --base00: #eff1f5; /* default bg */ + --base01: #e6e9ef; /* lighter bg (elevated surfaces) */ + --base02: #ccd0da; /* selection / surface — borders */ + --base03: #bcc0cc; /* comments / dim surface */ + --base04: #acb0be; /* dark foreground */ + --base05: #4c4f69; /* default foreground */ + --base06: #dc8a78; /* light foreground */ + --base07: #7287fd; /* lightest */ + /* base08-0F: darkened from stock Catppuccin Latte's own accent + hexes (same hue/saturation, lower HSL lightness), not a literal + upstream port like base00-07 above. argus's review on this PR + computed real WCAG contrast ratios and found the literal Latte + accents fail badly as `StatusChip` fill-text (green/amber/red/ + yellow on `--purple-dim`/base03: 1.4:1-3:1, need 4.5:1) and even + as plain text on `--bg`/base00 in the agent/dashboard packages + that also consume this same file (2.3:1-4.8:1). Root cause: + Latte's own accent colors are calibrated against Latte's + near-white `base`/`crust`, not against a mid-gray `surface1` — + the Mocha row doesn't have this problem because Mocha's pastel + accents are already *light*, so they contrast fine against a + *dark* surface1; Latte's saturated-but-mid-brightness accents + don't have the equivalent headroom against Latte's own + (much lighter) surface1. Each value below was picked by holding + the stock Latte hue+saturation fixed and binary-searching HSL + lightness down to the point real contrast against base03 clears + 4.5:1 with a small margin (also verified against base00: all + land above 7:1 there) — same hue identity, same base16 role, + just dark/saturated enough to actually be legible as filled-chip + or plain text either way. Yellow and peach read closer to + olive/brown than a bright yellow/orange once darkened this far — + an inherent property of darkening a warm hue in sRGB (Bezold- + Brücke shift), not a mapping mistake; boosting saturation + further didn't rescue it (verified). */ + --base08: #9c0b2a; /* red */ + --base09: #883201; /* orange / peach */ + --base0A: #6d450e; /* yellow */ + --base0B: #235818; /* green */ + --base0C: #025374; /* cyan (our sky — note: Catppuccin's base0C is teal) */ + --base0D: #0843b8; /* blue */ + --base0E: #6311ce; /* magenta / mauve */ + --base0F: #8f166e; /* extra accent — pink */ } } - -/* Per-user override (swarm-ui's `SettingsMenu` / `lib/theme-apply.ts` - sets `data-theme` on ``; unset/`"system"` removes the - attribute and lets the media query above decide, same as before this - existed). `:root[data-theme='light']` has higher specificity than - the plain `:root` inside the media query above (an attribute - selector always outranks none), so this wins regardless of the - actual OS preference or source order — no `!important`, no inline - style, and critically no second copy of either palette's hex values: - both blocks below just re-point at the same `--mocha-baseNN` / - `--latte-baseNN` custom properties declared once at the top of this - file. */ -:root[data-theme='light'] { - --base00: var(--latte-base00); - --base01: var(--latte-base01); - --base02: var(--latte-base02); - --base03: var(--latte-base03); - --base04: var(--latte-base04); - --base05: var(--latte-base05); - --base06: var(--latte-base06); - --base07: var(--latte-base07); - --base08: var(--latte-base08); - --base09: var(--latte-base09); - --base0A: var(--latte-base0A); - --base0B: var(--latte-base0B); - --base0C: var(--latte-base0C); - --base0D: var(--latte-base0D); - --base0E: var(--latte-base0E); - --base0F: var(--latte-base0F); -} -:root[data-theme='dark'] { - --base00: var(--mocha-base00); - --base01: var(--mocha-base01); - --base02: var(--mocha-base02); - --base03: var(--mocha-base03); - --base04: var(--mocha-base04); - --base05: var(--mocha-base05); - --base06: var(--mocha-base06); - --base07: var(--mocha-base07); - --base08: var(--mocha-base08); - --base09: var(--mocha-base09); - --base0A: var(--mocha-base0A); - --base0B: var(--mocha-base0B); - --base0C: var(--mocha-base0C); - --base0D: var(--mocha-base0D); - --base0E: var(--mocha-base0E); - --base0F: var(--mocha-base0F); -} diff --git a/frontend/packages/swarm-ui/src/lib/motion-apply.ts b/frontend/packages/swarm-ui/src/lib/motion-apply.ts deleted file mode 100644 index 42c19f7e..00000000 --- a/frontend/packages/swarm-ui/src/lib/motion-apply.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Applies the stored reduced-motion override (see `settings-storage.ts`) -// as a `data-motion="reduce"|"allow"` attribute on ``, cleared -// when the override is "system" (letting `prefers-reduced-motion` -// alone decide, same as today). -// -// Currently inert: swarm-ui has zero CSS animations as of this file's -// writing (the design guide's "playful whimsy" motion — the matrix-rain -// home background — lives in the dashboard package, not swarm-ui). This -// hook exists because the settings surface itself was scoped to cover -// theme *and* motion together (both named in the same go-ahead), and the -// marginal cost of the storage/attribute plumbing is near zero riding -// alongside the theme override's real wiring — but there is genuinely -// nothing in swarm-ui for `data-motion` to gate yet. The first swarm-ui -// animation's own CSS is what makes this do anything, e.g.: -// @media (prefers-reduced-motion: reduce) { ... } -// :root[data-motion='reduce'] { ... same rule ... } -// :root[data-motion='allow'] { /* opt back in despite OS-level reduce */ } -import { useEffect } from 'preact/hooks'; -import { useLocalSetting } from './settings-storage.js'; - -export type MotionOverride = 'system' | 'reduce' | 'allow'; - -export const MOTION_OVERRIDE_KEY = 'swarm-ui:motion-override'; - -export function useMotionOverride() { - return useLocalSetting(MOTION_OVERRIDE_KEY, 'system'); -} - -// Mounted once alongside `useApplyThemeOverride` — see that module's -// comment for why `Shell` is the right single mount point. -export function useApplyMotionOverride(): void { - const [override] = useMotionOverride(); - useEffect(() => { - const root = document.documentElement; - if (override === 'system') { - delete root.dataset.motion; - } else { - root.dataset.motion = override; - } - }, [override]); -} diff --git a/frontend/packages/swarm-ui/src/lib/settings-storage.ts b/frontend/packages/swarm-ui/src/lib/settings-storage.ts deleted file mode 100644 index e5b6beba..00000000 --- a/frontend/packages/swarm-ui/src/lib/settings-storage.ts +++ /dev/null @@ -1,75 +0,0 @@ -// Generic localStorage-backed setting — the shared plumbing the theme -// override and reduced-motion override both need ("read a stored value, -// react to it changing, write a new one") rather than each reinventing -// its own key handling. Not settings-page-specific: any future -// client-local preference reaches for `useLocalSetting` directly. -// -// `localStorage`'s own `storage` event only fires in *other* tabs, never -// the tab that made the write — so two components in the same tab both -// watching the same key (e.g. `SettingsMenu` writing, a top-level effect -// reading) need their own same-tab signal. The tiny module-level pub/sub -// below is that signal; it's deliberately not exported, callers only see -// the hook. -import { useEffect, useState } from 'preact/hooks'; - -const listeners = new Map void>>(); - -function subscribe(key: string, onChange: () => void): () => void { - let set = listeners.get(key); - if (!set) { - set = new Set(); - listeners.set(key, set); - } - set.add(onChange); - return () => { - set!.delete(onChange); - if (set!.size === 0) listeners.delete(key); - }; -} - -function notify(key: string): void { - listeners.get(key)?.forEach((fn) => fn()); -} - -// Reads outside a component (e.g. an early top-level check) can call -// this directly; `useLocalSetting` builds on it rather than duplicating -// the try/catch. -export function readLocalSetting(key: string, fallback: T): T { - try { - const raw = localStorage.getItem(key); - return raw === null ? fallback : (JSON.parse(raw) as T); - } catch { - // Private-browsing storage bans, a corrupted value, quota errors — - // all the same answer: behave as if nothing were stored. - return fallback; - } -} - -function writeLocalSetting(key: string, value: T): void { - try { - localStorage.setItem(key, JSON.stringify(value)); - } catch { - // Best-effort: the setting just won't persist this time, not worth - // surfacing an error for a client-local preference. - } - notify(key); -} - -// `[value, setValue]`, same shape as `useState` — deliberately, so a -// caller that later needs to swap a plain `useState` for a persisted -// setting (or vice versa) changes one line, not its whole call site. -// -// `setValue` only writes + notifies; it doesn't also call this -// component's own `setValue` state setter directly. The subscription -// below already fires for a same-instance write (it's in the same -// `listeners` set as every other subscriber), so a self-write reaches -// this component's state through the identical "react to a change" -// path every other subscriber uses — one path, not two that have to -// agree. -export function useLocalSetting(key: string, fallback: T): [T, (value: T) => void] { - const [value, setValue] = useState(() => readLocalSetting(key, fallback)); - - useEffect(() => subscribe(key, () => setValue(readLocalSetting(key, fallback))), [key]); - - return [value, (next: T) => writeLocalSetting(key, next)]; -} diff --git a/frontend/packages/swarm-ui/src/lib/theme-apply.ts b/frontend/packages/swarm-ui/src/lib/theme-apply.ts deleted file mode 100644 index a18608e8..00000000 --- a/frontend/packages/swarm-ui/src/lib/theme-apply.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Applies the stored theme override (see `settings-storage.ts`) by -// setting `data-theme="light"|"dark"` on ``, cleared when the -// override is "system" (letting the `prefers-color-scheme` media -// query in `@hive/shared/colors.css` resume control). No palette -// values live here — `colors.css`'s own `:root[data-theme='light']` / -// `:root[data-theme='dark']` blocks are what actually apply the -// colours, by re-pointing at the same `--mocha-baseNN` / -// `--latte-baseNN` custom properties its media-query block already -// uses. This file only ever toggles the attribute; the values come -// from colors.css, not a duplicate copy here. -import { useEffect } from 'preact/hooks'; -import { useLocalSetting } from './settings-storage.js'; - -export type ThemeOverride = 'system' | 'light' | 'dark'; - -export const THEME_OVERRIDE_KEY = 'swarm-ui:theme-override'; - -export function useThemeOverride() { - return useLocalSetting(THEME_OVERRIDE_KEY, 'system'); -} - -// Mounted once, high in the tree (`Shell`) — every route renders -// through one ``, so one mount point applies the override -// regardless of which page is showing, and `useLocalSetting`'s -// same-tab subscription means `SettingsMenu` changing the value -// re-runs this effect without either component needing to know about -// the other directly. -export function useApplyThemeOverride(): void { - const [override] = useThemeOverride(); - useEffect(() => { - const root = document.documentElement; - if (override === 'system') { - delete root.dataset.theme; - } else { - root.dataset.theme = override; - } - }, [override]); -} diff --git a/frontend/packages/swarm-ui/src/shell/LinksMenu.css b/frontend/packages/swarm-ui/src/shell/LinksMenu.css index 268d6096..ddb2fd89 100644 --- a/frontend/packages/swarm-ui/src/shell/LinksMenu.css +++ b/frontend/packages/swarm-ui/src/shell/LinksMenu.css @@ -4,11 +4,10 @@ as the shared `ui/` kit (2.75em ≈ 44px, WCAG 2.5.5) even though it isn't built from that kit — a bespoke icon trigger, not a form control, but the floor applies regardless of which component drew - it. No `margin-left: auto` here — `Shell.tsx`'s `.shell-header-actions` - wrapper owns pushing the whole action group to the right edge now - that `SettingsMenu` sits alongside this one. */ + it. */ .links-menu { position: relative; + margin-left: auto; } .links-menu-button { display: flex; diff --git a/frontend/packages/swarm-ui/src/shell/SettingsMenu.css b/frontend/packages/swarm-ui/src/shell/SettingsMenu.css deleted file mode 100644 index 06c711df..00000000 --- a/frontend/packages/swarm-ui/src/shell/SettingsMenu.css +++ /dev/null @@ -1,61 +0,0 @@ -/* — same header icon-button + popover chrome as - `LinksMenu` (quiet until interacted with, 2.75em touch-target floor - on the trigger). Kept as its own stylesheet rather than sharing - `LinksMenu.css` classes — two small, independently-evolving popovers - that happen to look alike today, not one component with two skins. */ -.settings-menu { - position: relative; -} -.settings-menu-button { - display: flex; - align-items: center; - justify-content: center; - width: 2.75em; - height: 2.75em; - padding: 0; - border: 1px solid transparent; - border-radius: 0.4em; - background: none; - color: var(--fg); - font-size: 1em; - line-height: 1; - cursor: pointer; -} -.settings-menu-button:hover, -.settings-menu-button[aria-expanded='true'] { - border-color: var(--border); - background: var(--bg-elev); -} -.settings-menu-popover { - position: absolute; - top: calc(100% + 0.4em); - right: 0; - z-index: 10; - display: flex; - flex-direction: column; - gap: 0.3em; - min-width: 12em; - padding: 0.5em; - border: 1px solid var(--border); - border-radius: 0.5em; - background: var(--bg-elev); - box-shadow: 0 0.25em 0.75em rgba(0, 0, 0, 0.3); -} -.settings-menu-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75em; - min-height: 2.75em; - padding: 0 0.3em; - color: var(--fg); -} -.settings-menu-row select { - min-height: 2.75em; - padding: 0 0.4em; - border: 1px solid var(--border); - border-radius: 0.35em; - background: var(--bg); - color: var(--fg); - font: inherit; -} diff --git a/frontend/packages/swarm-ui/src/shell/SettingsMenu.tsx b/frontend/packages/swarm-ui/src/shell/SettingsMenu.tsx deleted file mode 100644 index 1e0fb16b..00000000 --- a/frontend/packages/swarm-ui/src/shell/SettingsMenu.tsx +++ /dev/null @@ -1,87 +0,0 @@ -// — a single header icon-button + popover holding the -// client-local overrides swarm-ui currently has (theme, reduced -// motion). Mirrors `LinksMenu`'s shape (icon button, popover, close on -// outside-click/Escape) — same "quiet chrome, not another nav item" -// affordance, not a coincidence: the operator, when asked how big this -// settings surface should be, said "small thing somewhere" since there -// are only two overrides right now — a full `/settings` route + nav -// entry would be over-building for two selects. Grows into a real page -// only if the setting count outgrows a popover; nothing here assumes -// it can't. -import { useEffect, useRef, useState } from 'preact/hooks'; -import { useThemeOverride, type ThemeOverride } from '../lib/theme-apply.js'; -import { useMotionOverride, type MotionOverride } from '../lib/motion-apply.js'; -import './SettingsMenu.css'; - -const THEME_OPTIONS: ThemeOverride[] = ['system', 'light', 'dark']; -const MOTION_OPTIONS: MotionOverride[] = ['system', 'allow', 'reduce']; - -export function SettingsMenu() { - const [open, setOpen] = useState(false); - const rootRef = useRef(null); - const [theme, setTheme] = useThemeOverride(); - const [motion, setMotion] = useMotionOverride(); - - // Close on an outside click or Escape — only listens while open, same - // pattern (and same rationale) as `LinksMenu`. - useEffect(() => { - if (!open) return; - function onPointerDown(e: MouseEvent) { - if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false); - } - function onKeyDown(e: KeyboardEvent) { - if (e.key === 'Escape') setOpen(false); - } - document.addEventListener('pointerdown', onPointerDown); - document.addEventListener('keydown', onKeyDown); - return () => { - document.removeEventListener('pointerdown', onPointerDown); - document.removeEventListener('keydown', onKeyDown); - }; - }, [open]); - - return ( -
- - {open ? ( - - ) : null} -
- ); -} diff --git a/frontend/packages/swarm-ui/src/shell/Shell.css b/frontend/packages/swarm-ui/src/shell/Shell.css index 615e40f7..b3bfffc4 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.css +++ b/frontend/packages/swarm-ui/src/shell/Shell.css @@ -28,15 +28,6 @@ flex-wrap: wrap; gap: 1em; } -/* Holds `SettingsMenu` + `LinksMenu` — one `margin-left: auto` on the - wrapper, not one on each child (see Shell.tsx's comment for why two - adjacent auto margins don't sit flush together). */ -.shell-header-actions { - display: flex; - align-items: center; - gap: 0.25em; - margin-left: auto; -} /* Touch target and active-state underline are deliberately on two different elements. An earlier version put both `min-height: 2.75em` and `border-bottom` on the same box: centering the text within a diff --git a/frontend/packages/swarm-ui/src/shell/Shell.tsx b/frontend/packages/swarm-ui/src/shell/Shell.tsx index cdd5d22d..70538e0b 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.tsx +++ b/frontend/packages/swarm-ui/src/shell/Shell.tsx @@ -16,9 +16,6 @@ import { useEffect, useState } from 'preact/hooks'; import type { ComponentChildren } from 'preact'; import { Link, useRoute } from 'wouter-preact'; import { LinksMenu } from './LinksMenu.js'; -import { SettingsMenu } from './SettingsMenu.js'; -import { useApplyThemeOverride } from '../lib/theme-apply.js'; -import { useApplyMotionOverride } from '../lib/motion-apply.js'; import './Shell.css'; const NAV_ITEMS: { href: string; label: string }[] = [ @@ -50,15 +47,6 @@ function NavLink({ href, label }: { href: string; label: string }) { export function Shell({ children }: { children: ComponentChildren }) { const [swarmName, setSwarmName] = useState(null); - // Applied once here, not inside `SettingsMenu` — every route mounts - // through this one ``, so the override takes effect regardless - // of which page is showing or whether the menu's ever been opened, - // and `useLocalSetting`'s same-tab subscription (settings-storage.ts) - // means `SettingsMenu` changing the stored value re-runs these - // effects without either component needing a reference to the other. - useApplyThemeOverride(); - useApplyMotionOverride(); - // Fetched once here, not per-page: every route mounts inside one // ``, and the swarm's name doesn't change within a page // visit. A fetch failure is silently ignored — `swarmName` just stays @@ -90,14 +78,7 @@ export function Shell({ children }: { children: ComponentChildren }) { ))} - {/* Single `margin-left: auto` on the wrapper, not on each menu - individually — two adjacent flex items both set to - `margin-left: auto` split the available space between them - instead of sitting flush together at the right edge. */} -
- - -
+
{children}