Watch
0
0
Fork
You've already forked hyperhive
0

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:
iris 2026-08-18 23:43:30 +02:00 • committed by mara
commit a08aacfdf6

View file

@ -0,0 +1,61 @@
/* <SettingsMenu> — 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;
}