Shell's page content now remounts on every navigation and plays a
scale+fade pop-in entrance. The active nav link's underline is now a
single shared element that slides to its new position instead of
snapping, re-colouring to a per-tab accent — a discrete cycle through
the existing base16 chromatic slots, not a continuous hue rotation.
Both are gated on the data-motion/prefers-reduced-motion plumbing
lib/motion-apply.ts already had wired and waiting for a first real
consumer.
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.
The previous fix put both the touch-target min-height and the
active-state border-bottom on the same <a>: centering the text
within a 2.75em box pushed the border ~0.7em away from it, a real
visible regression argus caught by actually rendering the CSS rather
than reasoning about the box model abstractly.
Split the two concerns onto two elements: the <a> (.shell-nav-link)
owns the full-height tappable box, an inner <span>
(.shell-nav-link-text) wrapping just the label owns the underline,
so the indicator stays directly under the text regardless of the
tappable box's height.
Verified: tsc clean, build succeeds, screenshotted close-up (600x100)
and at 320px — underline sits flush under the text in both, no gap.
The proposed scope on this issue named three things to give the
2.75em touch-target floor: nav links, buttons, and the links-menu
trigger. Only the links-menu trigger actually got it in the first
pass — the primary nav (hives/new agent/jobs/components), the
highest-traffic touch target in the whole shell, was still sitting at
padding: 0.25em 0 (~27px tall).
display: flex + min-height rather than more padding: padding alone
would have pushed the active-state border-bottom away from the text
to reach 2.75em; flex centres the text within the full-height box so
the indicator still sits directly under it.
Verified: build succeeds, screenshotted at desktop width and 320px —
active underline still sits correctly under the text at the taller
height, wrap behavior from the earlier commit unaffected.
Closes#3447.
- Shell.css: .shell-header and .shell-nav wrap onto a second line
below a breakpoint instead of forcing horizontal overflow — no
hamburger/collapse menu, per mara's own 'don't break, don't
over-invest' scoping.
- LinksMenu.css: bumped .links-menu-button and .links-menu-item to the
same 2.75em (~44px, WCAG 2.5.5) touch-target floor the shared ui/
kit's form controls and buttons already carry — this trigger is
bespoke, not built from that kit, but the floor applies regardless.
- Table.tsx/.css: wrapped the table in a .ui-table-scroll container
(overflow-x: auto) so a wide table scrolls within its own box on a
narrow viewport instead of forcing the whole page to overflow
horizontally — not named in the issue's two concrete spots, but the
same 'should not break' failure mode, and fixing it at the shared
Table primitive covers every future caller.
.create-agent-input's fixed width -> max-width was already fixed as
part of #3448's form kit.
Verified: tsc clean, build succeeds, screenshotted at 320px (hives
nav wraps, table scrolls internally, page itself doesn't overflow)
and 480px (create-agent form fields fit).
Closes#3460.
Renamed for accuracy — the page/nav item is the hive roster, not a
general dashboard overview. Nav label, page title, component name,
and file all renamed together so the internal name doesn't drift from
what's displayed (a component still called OverviewPage under a
'hives' nav label would be exactly the kind of stale prior-art that
makes the next contributor search harder, not less).
New GET /api/swarm on swarm-controller, backed by
services.hyperhive.swarm.name (SWARM_CONTROLLER_NAME env var, same
optionalAttrs-gated-on-option-resolving shape queueEnv/forgeEnv/etc.
already use). swarm-ui's <Shell> fetches it once and sets both
document.title and the header's brand text; falls back to the
existing static "hyperhive swarm" label when the operator never set
a name or the fetch fails.
Extracted the swarm-queue connect block out of main() into its own
connect_status_reader() fn to keep main() under clippy's line-count
lint after adding the new field wiring — no behavior change, same
comments moved as-is.
mara: the route should reflect creating an agent, and stay separate
from a future agent list page. Renamed AgentsPage -> CreateAgentPage
(file, component, css classes) and moved the route from /agents to
/create-agent - flat, not /agents/new, since index.html's relative
asset links only resolve correctly one path segment deep (filed
separately as a real bug, not fixed here). Leaves the bare /agents
path free for a future roster page.
Adds /agents: a name field that POSTs to swarm-controller's
POST /api/agents (from the CreateIdentity work), shows the queued
job node id, and links to /jobs to watch it settle. Scope matches
the issue exactly - no forge/deploy options, those aren't wired
server-side yet.
New /jobs route in swarm-ui, reusing the shared JobqGraph component
against swarm-controller's own GET /api/jobq/graph (same wire shape
hive-c0re's dashboard already consumes, different endpoint, no fork).
Converted JobqGraph.jsx to JobqGraph.tsx with real prop/wire types
(mirrors hive_jobq_wire's GraphNode/GraphDep/State by hand) instead of
a hand-maintained ambient .d.ts at the swarm-ui consumer side — the
.d.ts would duplicate the prop list and drift from the source the
moment the component's signature changes without the declaration being
touched. Both dashboard (untyped consumer, esbuild strips types) and
swarm-ui (tsc --noEmit) build/typecheck clean off the one file.
Single 🔗 button in the Shell header (direction 1 of 3 proposed on the
issue) — one fixed element regardless of link count, so it can't
clutter as the list grows, and visible on every route since Shell
wraps all of them. Opens a popover listing whatever GET /api/links
returns (icon + label, opens in a new tab); hidden entirely when the
list is empty, same "don't render a dead affordance" rule the old
dashboard's H0M3 tiles follow for Forge/Matrix.
Docs: docs/swarm/ui.md gets a short section on the feature + the
contribute-your-own-entry idiom.
Verified: npm run build + npm run typecheck clean, headless-chromium
screenshots of both the closed and (temporarily forced open for the
screenshot only) open states, pixel-sampled to confirm the popover is
actually using the dark theme vars and not just looking that way in a
downscaled preview.
New /components route showing every ui primitive (Panel, StatusChip,
Table) in each mode it supports: Panel with/without a title,
StatusChip's four tones, Table populated and empty. Sample data only,
no API calls, so it renders identically regardless of backend state.
Linked from the Shell nav next to "overview".
Verified with npm run build + npm run typecheck, and a headless
chromium screenshot of the built dist.
Per mara's review question on PR#3219 ('shouldnt the jsx files import
their css?'): each component now does its own import ('./Shell.css',
'./Panel.css', ...) instead of swarm-ui.css centrally @import-ing
every component's stylesheet. esbuild folds every .css reachable from
main.tsx's import graph into one main.css companion output next to
main.js — no separate build step, this is bundle:true's existing
behavior, just not exercised until now.
swarm-ui.css keeps only the shared base reset (@hive/shared/base.css)
since that isn't any one component's concern. Added src/css.d.ts
(ambient '*.css' module) since tsc otherwise rejects a side-effect
import of a non-JS/TS specifier.
Side benefit: a component nothing imports (yet) no longer ships its
CSS either — StatusChip/Table aren't referenced from App.tsx today,
and main.css correctly only carries Shell.css + Panel.css. The old
central-import approach shipped all four unconditionally.
npm run build + typecheck clean. Re-screenshotted the real dist —
pixel-identical to before this change.
Structural foundation split out of hyperhive#3118 per mara's steer:
structure first so the real overview page (hive roster) and later
routes (swarm-wide agent roster) land as content changes rather than
each reinventing chrome + nav + a table/panel/chip shape.
- <Shell>: header bar (branding) + nav row, wraps every route. Route
list lives in Shell itself (one small SPA, one place to know its
own nav).
- ui/Panel, ui/StatusChip, ui/Table: the three primitives the
overview page's actual scope (hive roster: name/domain/status,
linking out to each hive's own dashboard) calls for, nothing
speculative beyond that.
Preact-native styling (plain CSS files imported via swarm-ui.css, no
shadow DOM — this package renders into light DOM) — not
@hive/shared's chrome.css, which is the old MPA dashboard's visual
language. Same base16/Catppuccin color tokens via theme.css/colors.css
so it still reads as hyperhive.
npm run build (whole frontend workspace) + npm run typecheck both
clean. Verified with a real headless-chromium screenshot against the
built dist, not just source-reading.
Fixes hyperhive#3211