agent: add a settings menu with a theme override, defaulted to dark

Ports swarm-ui's `SettingsMenu` (mara: "agent terminal page should get
the settings panel from swarm ui as well") — same shape as `MetaNav`
already in this header: a `Badge` icon trigger ("⚙"), popover, close on
outside-click/Escape.

`theme-apply.ts` + `settings-storage.ts` are near-verbatim ports of
swarm-ui's own (duplicated rather than moved into `@hive/shared` for
this pass — lower risk than reworking swarm-ui's imports in the same
change). Defaults the stored override to 'dark', not 'system', for the
same reason as swarm-ui's own default flip: `prefers-color-scheme` has
no real "unset" value, so 'system' silently reads as light for anyone
who's never touched an OS dark-mode toggle.

Motion NOT ported — agent has no animation gated behind `data-motion`
yet, so that plumbing would have nothing to control.

Verified end-to-end: default dark on a fresh load, and an explicit
localStorage override to 'light' correctly re-themes the whole page via
the existing `colors.css` `:root[data-theme='light']` block (already
shipped, previously only reachable from swarm-ui).
This commit is contained in:
iris 2026-08-29 11:00:11 +02:00
commit b28e8f1c4e
5 changed files with 233 additions and 0 deletions

View file

@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
import type { BadgeTone } from '@hive/shared/badge.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';
@ -22,6 +23,7 @@ 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;
@ -53,6 +55,7 @@ function tokenTotal(u: TokenUsage | null): number | null {
}
export function Root() {
useApplyThemeOverride();
const { state, refresh } = useAgentState();
const { todos, refresh: refreshTodos } = useTodos();
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
@ -100,6 +103,7 @@ export function Root() {
dashboardBase={resolveDashboardBase(state.dashboard_port)}
/>
) : null}
<SettingsMenu />
</>
);
// Kept mounted regardless of `openPanel` (open/closed toggles just the