swarm-ui: client-local settings surface (theme + reduced-motion overrides)
Adds the settings surface + storage plumbing swarm-ui has been missing:
nowhere to put a client-local preference and no shared code for one to
build on. Scoped small per explicit direction ("small thing somewhere",
localStorage, theme and motion in scope for now) rather than a full
/settings route + nav entry for two toggles.
- frontend/packages/swarm-ui/src/lib/settings-storage.ts: generic
useLocalSetting<T>(key, fallback) hook - read once, write through,
stay in sync with other same-tab consumers of the same key via a
small module-level pub/sub (localStorage's own storage event only
fires cross-tab).
- frontend/packages/swarm-ui/src/lib/theme-apply.ts: tri-state
system/light/dark override, applied by setting the 16 base16 custom
properties inline on <html> (an inline style always outranks a
stylesheet rule, including a media-query-gated one) - colors.css's
own comment on its light-mode block already named this as the
intended mechanism for a future override.
- frontend/packages/swarm-ui/src/lib/motion-apply.ts: tri-state
system/reduce/allow override, applied as a data-motion attribute.
Currently inert - swarm-ui has zero CSS animations yet - included
because the marginal cost riding alongside the theme override is
near zero and it was named in the same scoping answer; the first
swarm-ui animation's own CSS is what makes this do anything.
- frontend/packages/swarm-ui/src/shell/SettingsMenu.{tsx,css}: a
header icon-button + popover holding both selects, same shape as
LinksMenu (manages its own state, not a ui/ primitive, hence no
ComponentsPage demo - same exception LinksMenu already established).
- Shell.tsx/.css: mounts the two override-application hooks once
(every route renders through one Shell), and wraps SettingsMenu +
LinksMenu in a single .shell-header-actions flex wrapper so one
margin-left: auto pushes both to the right edge together - two
adjacent auto-margins on separate elements split the space between
them instead of sitting flush.
Verified the override actually outranks the media query, not just
"looks right": seeded localStorage with each override value while
forcing the opposite OS-level prefers-color-scheme via headless
chromium, both directions render the stored override, not the forced
OS preference. Typecheck and build clean.
This commit is contained in:
parent
5ca5433e0b
commit
a08aacfdf6
8 changed files with 384 additions and 3 deletions
|
|
@ -16,6 +16,9 @@ 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 }[] = [
|
||||
|
|
@ -47,6 +50,15 @@ function NavLink({ href, label }: { href: string; label: string }) {
|
|||
export function Shell({ children }: { children: ComponentChildren }) {
|
||||
const [swarmName, setSwarmName] = useState<string | null>(null);
|
||||
|
||||
// Applied once here, not inside `SettingsMenu` — every route mounts
|
||||
// through this one `<Shell>`, 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
|
||||
// `<Shell>`, and the swarm's name doesn't change within a page
|
||||
// visit. A fetch failure is silently ignored — `swarmName` just stays
|
||||
|
|
@ -78,7 +90,14 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
|||
<NavLink key={item.href} href={item.href} label={item.label} />
|
||||
))}
|
||||
</nav>
|
||||
<LinksMenu />
|
||||
{/* 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. */}
|
||||
<div class="shell-header-actions">
|
||||
<SettingsMenu />
|
||||
<LinksMenu />
|
||||
</div>
|
||||
</header>
|
||||
<div class="shell-body">{children}</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue