Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: give every tab the wide body

The shell had two body widths, a 60em readable column and a 90em wide body
that a short list of table routes opted into. Every route now gets the
90em body, and the opt-in route list, its modifier class and its test go
away.
This commit is contained in:
iris 2026-10-15 18:34:59 +02:00
commit 89efda5ea2
4 changed files with 14 additions and 51 deletions

View file

@ -229,8 +229,10 @@
background: color-mix(in srgb, var(--fg) 10%, transparent); background: color-mix(in srgb, var(--fg) 10%, transparent);
} }
/* One width for every tab's contents: 90em still centers with visible margins
on a typical desktop viewport; it isn't full-bleed. */
.shell-body { .shell-body {
max-width: 60em; max-width: 90em;
margin: 0 auto; margin: 0 auto;
padding: 1.5em 1.25em; padding: 1.5em 1.25em;
/* Plain fade, deliberately — mara's call on review: a scale+overshoot /* Plain fade, deliberately — mara's call on review: a scale+overshoot
@ -239,15 +241,6 @@
having shipped something to walk back first. */ having shipped something to walk back first. */
animation: shell-page-enter 160ms ease; animation: shell-page-enter 160ms ease;
} }
/* Opt-in widening for a route that genuinely needs more than a
readable-line-length column (see Shell.tsx's `WIDE_BODY_ROUTES`).
Only overrides `max-width` — margin/padding/animation stay
`.shell-body`'s, so this reads as "the same body, just wider" rather
than a parallel layout to keep in sync. 90em still centers with
visible margins on a typical desktop viewport; it isn't full-bleed. */
.shell-body-wide {
max-width: 90em;
}
/* Full-bleed route (the agent terminal): the shell becomes a viewport-high /* Full-bleed route (the agent terminal): the shell becomes a viewport-high
column, the header keeps its natural height, and the body takes every column, the header keeps its natural height, and the body takes every
remaining pixel with no width cap or padding. `100dvh`, not `100vh` — remaining pixel with no width cap or padding. `100dvh`, not `100vh` —

View file

@ -35,7 +35,7 @@ import { SettingsMenu } from "@hive/shared/settings-menu.js";
import { HideDebugSetting } from "@hive/shared/hide-debug-setting.js"; import { HideDebugSetting } from "@hive/shared/hide-debug-setting.js";
import { useApplyThemeOverride } from "@hive/shared/theme-apply.js"; import { useApplyThemeOverride } from "@hive/shared/theme-apply.js";
import { useApplyMotionOverride } from "@hive/shared/motion-apply.js"; import { useApplyMotionOverride } from "@hive/shared/motion-apply.js";
import { isFullBleedRoute, isWideBodyRoute } from "./routes.js"; import { isFullBleedRoute } from "./routes.js";
import { useDynamicTabs } from "./useDynamicTabs.js"; import { useDynamicTabs } from "./useDynamicTabs.js";
import { useSwarmName } from "./useSwarmName.js"; import { useSwarmName } from "./useSwarmName.js";
import "./Shell.css"; import "./Shell.css";
@ -136,8 +136,7 @@ function NavLink({
// One open agent-terminal tab (`useDynamicTabs`) — deliberately // One open agent-terminal tab (`useDynamicTabs`) — deliberately
// its own small component, not folded into `NavLink` above: dynamic tabs // its own small component, not folded into `NavLink` above: dynamic tabs
// don't ride the sliding `.shell-nav-indicator` (there's no fixed set to // don't ride the sliding `.shell-nav-indicator` (there's no fixed set to
// hop between — see `WIDE_BODY_ROUTE_PATTERN`'s own comment on why they // hop between, so they can't share `NAV_ITEMS`'s fixed route list either), so "active"
// can't share `NAV_ITEMS`'s Set-based route list either), so "active"
// here is a plain class toggle, not a measured position. The close × // here is a plain class toggle, not a measured position. The close ×
// is a real sibling button, not nested inside the `<Link>` — a button // is a real sibling button, not nested inside the `<Link>` — a button
// inside an `<a>` is invalid HTML and (worse) means a click anywhere on // inside an `<a>` is invalid HTML and (worse) means a click anywhere on
@ -418,15 +417,10 @@ export function Shell({ children }: { children: ComponentChildren }) {
</div> </div>
</header> </header>
{/* Keyed by route so it remounts (and replays its entrance {/* Keyed by route so it remounts (and replays its entrance
animation) on every navigation — see the file-top comment. animation) on every navigation — see the file-top comment. */}
`shell-body-wide` is additive (see `WIDE_BODY_ROUTES` above),
not a swap — `.shell-body`'s padding/animation rules still
apply, only the max-width cap changes. */}
<div <div
class={ class={
"shell-body" + "shell-body" + (isFullBleedRoute(location) ? " shell-body-full" : "")
(isWideBodyRoute(location) ? " shell-body-wide" : "") +
(isFullBleedRoute(location) ? " shell-body-full" : "")
} }
key={location} key={location}
> >

View file

@ -1,21 +1,12 @@
// Which layout a route gets inside the shell: the readable-width cap, the // Which routes are full-bleed inside the shell; every other route gets the
// wide body, or full-bleed. Kept apart from `Shell.tsx` so it is plain // same centred body. Kept apart from `Shell.tsx` so it is a plain function of
// functions of a path, with no DOM or CSS behind it, and can be tested. // a path, with no DOM or CSS behind it, and can be tested.
// Routes that opt out of `.shell-body`'s 60em readable-line-length cap
// — mara screenshotted the issue report's 8-column table getting
// clipped; that width is deliberate for the rest of the UI's cards/
// forms, but too narrow for a wide table. `.shell-body-wide` (Shell.css) is a
// modifier alongside the base class, not a replacement, so every other
// route's layout is untouched. `/agents` joined for the same reason —
// its own table view (plus the list+detail split) hits the same cap.
const WIDE_BODY_ROUTES = new Set(["/issues", "/agents", "/review"]);
// `/agents/:name/terminal` (and a subagent's, under // `/agents/:name/terminal` (and a subagent's, under
// `/agents/:name/subagents/:subagent/`) is full-bleed: the terminal fills // `/agents/:name/subagents/:subagent/`) is full-bleed: the terminal fills
// the whole viewport below the header bar, edge to edge (`shell-full` / // the whole viewport below the header bar, edge to edge (`shell-full` /
// `shell-body-full` in Shell.css). It can't join the `Set` above — it's a // `shell-body-full` in Shell.css). It's a parameterized path, so one literal
// parameterized path, one literal string can't match every agent name. // string can't match every agent name.
const FULL_BLEED_ROUTE_PATTERN = const FULL_BLEED_ROUTE_PATTERN =
/^\/agents\/[^/]+(\/subagents\/[^/]+)?\/terminal$/; /^\/agents\/[^/]+(\/subagents\/[^/]+)?\/terminal$/;
@ -24,7 +15,3 @@ const FULL_BLEED_ROUTE_PATTERN =
export function isFullBleedRoute(location: string): boolean { export function isFullBleedRoute(location: string): boolean {
return location === "/" || FULL_BLEED_ROUTE_PATTERN.test(location); return location === "/" || FULL_BLEED_ROUTE_PATTERN.test(location);
} }
export function isWideBodyRoute(location: string): boolean {
return WIDE_BODY_ROUTES.has(location);
}

View file

@ -1,8 +1,7 @@
// The shell's per-route layout: which paths are full-bleed, which get the // The shell's per-route layout: which paths are full-bleed. Run by `npm test`.
// wide body. Run by `npm test`.
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { test } from "node:test"; import { test } from "node:test";
import { isFullBleedRoute, isWideBodyRoute } from "../src/shell/routes.ts"; import { isFullBleedRoute } from "../src/shell/routes.ts";
test("the start page is full-bleed", () => { test("the start page is full-bleed", () => {
assert.equal(isFullBleedRoute("/"), true); assert.equal(isFullBleedRoute("/"), true);
@ -10,7 +9,6 @@ test("the start page is full-bleed", () => {
test("the hive directory moved off / and is not full-bleed", () => { test("the hive directory moved off / and is not full-bleed", () => {
assert.equal(isFullBleedRoute("/hives"), false); assert.equal(isFullBleedRoute("/hives"), false);
assert.equal(isWideBodyRoute("/hives"), false);
}); });
test("an agent's terminal and a subagent's are full-bleed", () => { test("an agent's terminal and a subagent's are full-bleed", () => {
@ -31,12 +29,3 @@ test("a terminal path with extra segments is not full-bleed", () => {
assert.equal(isFullBleedRoute("/agents/iris/terminal/extra"), false); assert.equal(isFullBleedRoute("/agents/iris/terminal/extra"), false);
assert.equal(isFullBleedRoute("/agents//terminal"), false); assert.equal(isFullBleedRoute("/agents//terminal"), false);
}); });
test("the wide-body routes are exactly the table pages", () => {
for (const path of ["/issues", "/agents", "/review"]) {
assert.equal(isWideBodyRoute(path), true, path);
}
for (const path of ["/", "/jobs", "/schedules", "/components"]) {
assert.equal(isWideBodyRoute(path), false, path);
}
});