Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/shell/Shell.css
iris 66df552f68 swarm-ui: terminal tab fills the viewport below the header
The agent terminal tab now takes the whole window except the header bar,
full width and height, instead of sitting in the padded content column.
The connection badge floats inside the terminal's top-left corner and the
toolbar row above it is gone on this route.
2026-10-04 16:15:06 +02:00

284 lines
9.5 KiB
CSS

/* <Shell> — header bar + nav row + the content column every route
renders into. Colour vars come from ../theme.css (same base16 slots
the per-hive dashboard uses, so this reads as recognizably
hyperhive) — the layout itself is this package's own, not
@hive/shared's chrome.css.
`flex-wrap` on the header and nav rather than a hamburger/collapse
menu: a narrow viewport (phone or a tiled desktop window) wraps onto
a second line instead of overflowing — "don't break, don't
over-invest" is the explicit scope here, not full mobile navigation
redesign.
Page-switch animation (see Shell.tsx's file-top comment for the JS
half): both the nav indicator's slide and the page-body pop follow
the same three-rule motion-guard shape `@hive/shared/motion-apply.js`'s
own doc comment specifies — a base rule,
`@media (prefers-reduced-motion: reduce)` to respect the OS default,
`:root[data-motion='reduce']` as the explicit override (same
specificity as the media rule, so source order after it wins), then
`:root[data-motion='allow']` last to re-enable despite an OS-level
reduce preference. */
/* Sticky + frosted glass (mara: "sticky header bar ... frosted glass")
— same `color-mix` + `backdrop-filter` recipe `.terminal-wrap`/
`.ui-badge` (@hive/shared) already use, dark-only same as those (a
light bg behind glass washes out). `z-index` keeps it above
`.shell-body` once content scrolls underneath. */
.shell-header {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 1.5em;
padding: 0.75em 1.25em;
border-bottom: 1px solid var(--border);
background: color-mix(in srgb, var(--bg-elev) 80%, transparent);
-webkit-backdrop-filter: blur(8px) saturate(120%);
backdrop-filter: blur(8px) saturate(120%);
position: sticky;
top: 0;
z-index: 10;
}
@media (prefers-color-scheme: light) {
.shell-header {
background: var(--bg-elev);
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
}
:root[data-theme="light"] .shell-header {
background: var(--bg-elev);
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
:root[data-theme="dark"] .shell-header {
background: color-mix(in srgb, var(--bg-elev) 80%, transparent);
-webkit-backdrop-filter: blur(8px) saturate(120%);
backdrop-filter: blur(8px) saturate(120%);
}
.shell-brand {
font-weight: 600;
/* Base colour only — Shell.tsx overrides it inline to the same
currently-active-tab accent `.shell-nav-indicator` uses (falling
back to this `--purple` before the first measurement lands, which
is also the "hives" default route's own accent, so there's no
visible mismatch at first paint). Transition lives here, same
140ms hop cadence + the same three-rule motion guard as the
indicator, so this and the indicator read as one thing changing,
not two independently-synced ones. */
color: var(--purple);
letter-spacing: 0.02em;
transition: color 140ms ease;
}
@media (prefers-reduced-motion: reduce) {
.shell-brand {
transition: none;
}
}
:root[data-motion="reduce"] .shell-brand {
transition: none;
}
:root[data-motion="allow"] .shell-brand {
transition: color 140ms ease;
}
.shell-nav {
display: flex;
flex-wrap: wrap;
gap: 1em;
position: relative; /* anchor for .shell-nav-indicator */
}
/* Holds `SettingsMenu` + `LinksMenu` — one `margin-left: auto` on the
wrapper, not one on each child (see Shell.tsx's comment for why two
adjacent auto margins don't sit flush together). */
.shell-header-actions {
display: flex;
align-items: center;
gap: 0.25em;
margin-left: auto;
}
/* Touch target and active-state underline are deliberately on two
different elements. An earlier version put both `min-height: 2.75em`
and `border-bottom` on the same box: centering the text within a
2.75em box pushes a bottom-edge border ~0.7em away from the text,
which argus caught by actually rendering it — a real visual
regression, not just a touch-target win. So: `.shell-nav-link` (the
`<a>`) owns the full-height tappable box (same 2.75em / ~44px WCAG
2.5.5 floor as the rest of the shared kit — this is the primary nav,
the single most-clicked/tapped element in the whole shell), and
`.shell-nav-link-text` (an inner span hugging just the text) owns
the underline, so the indicator stays directly under the label
regardless of how tall the tappable box around it is. */
.shell-nav-link {
display: flex;
align-items: center;
min-height: 2.75em;
color: var(--muted);
text-decoration: none;
}
.shell-nav-link:hover {
color: var(--fg);
}
.shell-nav-link-text {
padding-bottom: 0.25em; /* gap between the text and .shell-nav-indicator below it */
}
.shell-nav-link-active {
color: var(--fg);
}
/* Shared underline, one element instead of one border-bottom per link
— see Shell.tsx's file-top comment. `left`/`top`/`width` are set
inline per-render from a real measurement; only the *transition*
between those values lives here. 140ms matches Shell.tsx's `HOP_MS`
exactly, so one hop's CSS transition finishes right as the next
hop's JS-driven style update lands, instead of the two blending into
an averaged easing curve. */
.shell-nav-indicator {
position: absolute;
height: 2px;
border-radius: 1px;
transition:
left 140ms ease,
top 140ms ease,
width 140ms ease,
background-color 140ms ease;
}
@media (prefers-reduced-motion: reduce) {
.shell-nav-indicator {
transition: none;
}
}
:root[data-motion="reduce"] .shell-nav-indicator {
transition: none;
}
:root[data-motion="allow"] .shell-nav-indicator {
transition:
left 140ms ease,
top 140ms ease,
width 140ms ease,
background-color 140ms ease;
}
/* Suppresses the transition for exactly one render — the indicator's
very first real position, so it snaps into place instead of visibly
growing from the unmounted `left:0/width:0/transparent` fallback (see
Shell.tsx's `indicatorSettledOnce`). Two-class selector so it wins
over the plain `.shell-nav-indicator` rule above without `!important`,
same reasoning `:root[data-motion='reduce']`'s extra attribute
selector already relies on. */
.shell-nav-indicator.shell-nav-indicator-instant {
transition: none;
}
/* Dynamic tabs group — inline in `.shell-nav` itself, next to the
static route links (mara, PR review: "the dynamic tab should be in
the top bar, not a new one below" — an earlier cut had this as its
own sticky row under the header; that version and the reasoning for
it are still in this file's git history if the split ever needs to
come back). `margin-left: 0.5em` + `border-left` reads as "a group
attached to the nav, not another nav item" without a second landmark
or a fixed pixel gap that'd fight `.shell-nav`'s own `gap`. */
.shell-tabs {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin-left: 0.5em;
padding-left: 0.75em;
border-left: 1px solid var(--border);
}
.shell-tab {
display: flex;
align-items: center;
gap: 0.25em;
padding: 0.2em 0.4em 0.2em 0.7em;
border-radius: 0.3em;
background: var(--bg);
border: 1px solid var(--border);
}
.shell-tab-active {
border-color: var(--pink);
}
.shell-tab-link {
color: var(--muted);
text-decoration: none;
font-size: 0.9em;
}
.shell-tab-active .shell-tab-link {
color: var(--fg);
}
.shell-tab-link:hover {
color: var(--fg);
}
/* Same reset every icon-only chrome button in this codebase uses
(`Badge`'s own `<button>` case) — a bare `<button>` here isn't routed
through `Badge` since it's not a status/value pill, just a close
glyph, so the reset is inlined rather than reaching for a component
that doesn't fit this shape. */
.shell-tab-close {
all: unset;
cursor: pointer;
line-height: 1;
padding: 0.1em 0.3em;
border-radius: 0.2em;
color: var(--muted);
}
.shell-tab-close:hover {
color: var(--fg);
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.shell-body {
max-width: 60em;
margin: 0 auto;
padding: 1.5em 1.25em;
/* Plain fade, deliberately — mara's call on review: a scale+overshoot
"pop" (this rule's first cut) read as too much for a first pass.
Keeping it simple leaves room to make this fancier later without
having shipped something to walk back first. */
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
column, the header keeps its natural height, and the body takes every
remaining pixel with no width cap or padding. `100dvh`, not `100vh` —
`vh` includes mobile browser chrome that is on screen. */
.shell-full {
display: flex;
flex-direction: column;
height: 100dvh;
}
.shell-full .shell-header {
flex-shrink: 0;
}
.shell-body-full {
flex: 1;
min-height: 0;
max-width: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
@keyframes shell-page-enter {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.shell-body {
animation: none;
}
}
:root[data-motion="reduce"] .shell-body {
animation: none;
}
:root[data-motion="allow"] .shell-body {
animation: shell-page-enter 160ms ease;
}