settings: move the theme/motion panel into @hive/shared, add motion to agent

Review feedback on this PR (mara): "i think the component should be
shared. motion setting is missing." Both addressed:

- `settings-storage.ts` (generic localStorage hook), `theme-apply.ts`,
  `motion-apply.ts`, and `SettingsMenu.tsx`/`.css` all move from
  swarm-ui's `lib/`/`shell/` into `@hive/shared/src/settings/` —
  agent's previous local copies are deleted outright rather than kept
  as a second implementation. One component, `Badge` trigger
  everywhere (already used elsewhere in swarm-ui, so not a new visual
  language there either) — storage keys stay caller-owned (`themeKey`/
  `motionKey` props + matching `useApplyThemeOverride`/
  `useApplyMotionOverride` calls at each package's single mount point)
  so agent and swarm-ui keep fully independent, non-colliding
  persisted settings.
- Agent's settings menu now includes the motion row, matching
  swarm-ui's. No animation in the agent package is gated behind
  `data-motion` yet — same as when swarm-ui first built this plumbing
  ahead of having a consumer — so it's currently inert there, ready for
  whenever agent grows a motion-guarded animation.
- swarm-ui's own theme default flips to `'dark'` as part of this move
  (`theme-apply.ts`'s new default), superseding PR #3715 — that PR
  becomes redundant once this lands and will be closed rather than
  merged, to avoid the two colliding on the same file.

Verified end-to-end with real screenshots on both pages: shared
component renders identically (Badge trigger, theme+motion rows, dark
default) on agent's mock server and a static rebuild of swarm-ui's
dist.
This commit is contained in:
iris 2026-08-29 11:11:39 +02:00
commit 9720bdfad0
16 changed files with 230 additions and 487 deletions

View file

@ -6,9 +6,11 @@
// optional polish.
import { useEffect, useRef, useState } from 'preact/hooks';
import type { BadgeTone } from '@hive/shared/badge.js';
import { SettingsMenu } from '@hive/shared/settings-menu.js';
import { useApplyThemeOverride } from '@hive/shared/theme-apply.js';
import { useApplyMotionOverride } from '@hive/shared/motion-apply.js';
import { Header } from './components/Header.js';
import { MetaNav } from './components/MetaNav.js';
import { SettingsMenu } from './components/SettingsMenu.js';
import { StatusChips } from './components/StatusChips.js';
import { LiveStream, type LiveStreamHandle } from './components/LiveStream.js';
import { HeaderPill } from './components/HeaderPill.js';
@ -23,11 +25,17 @@ import { fmtAge, fmtTokens } from './lib/format.js';
import { resolveDashboardBase } from './lib/dashboardBase.js';
import { submitPauseResume } from './lib/pauseAction.js';
import { postModel, postEffort } from './lib/modelEffort.js';
import { useApplyThemeOverride } from './lib/theme-apply.js';
import type { TokenUsage } from './types.js';
type OpenPanel = 'inbox' | 'todos' | null;
// Storage keys this page owns for `@hive/shared`'s settings mechanism —
// kept here, not derived, so `useApplyThemeOverride`/`useApplyMotionOverride`
// (mounted once below) and `<SettingsMenu>` (in `pills`, further down)
// always agree on which browser-local key they're reading/writing.
const THEME_KEY = 'agent:theme-override';
const MOTION_KEY = 'agent:motion-override';
const ALIVE_LABELS: Record<string, { glyph: string; text: string; tone: BadgeTone }> = {
online: { glyph: '●', text: 'alive', tone: 'positive' },
rate_limited: { glyph: '⊘', text: 'rate limited', tone: 'warning' },
@ -55,7 +63,8 @@ function tokenTotal(u: TokenUsage | null): number | null {
}
export function Root() {
useApplyThemeOverride();
useApplyThemeOverride(THEME_KEY);
useApplyMotionOverride(MOTION_KEY);
const { state, refresh } = useAgentState();
const { todos, refresh: refreshTodos } = useTodos();
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
@ -103,7 +112,7 @@ export function Root() {
dashboardBase={resolveDashboardBase(state.dashboard_port)}
/>
) : null}
<SettingsMenu />
<SettingsMenu themeKey={THEME_KEY} motionKey={MOTION_KEY} />
</>
);
// Kept mounted regardless of `openPanel` (open/closed toggles just the