two related flyout improvements — both small, single PR. #450 — full-height inbox dropped the .inbox max-height: 24em cap. the side-panel-body already provides overflow: auto, so the cap was just clamping the inbox list short of the available panel height on tall viewports. the inbox now fills as much of the panel as it needs and the panel itself scrolls. #451 — drag-to-resize side panel added a 6px hit-strip on the drawer's left edge. mousedown/move/ up handlers track the drag and update --side-panel-w on the drawer; the CSS variable defaults to min(760px, 94vw) (preserving pre-#451 behaviour) and is clamped to [320px, 96vw] so a bad stored value can never wedge the drawer off-screen. ergonomics - handle is invisible at rest, mauve glow on hover + during drag so the affordance is discoverable but doesn't compete with the 2px mauve border-left for the visual boundary. - body.side-panel-resizing class forces ew-resize cursor + kills user-select page-wide so the cursor doesn't flip back to default the moment it leaves the 6px band during a fast drag. - final width persists to localStorage (key hyperhive:side-panel-width) so it survives reload. window resize re-clamps so a stored width that exceeds the new 96vw shrinks accordingly. - handle is a separator role with aria-orientation: vertical + aria-label for screen readers. files - frontend/packages/dashboard/src/dashboard.css - .inbox: dropped max-height (#450). - .side-panel-drawer: width = var(--side-panel-w, min(760px, 94vw)) + min/max clamp (#451). - new .side-panel-resize + body.side-panel-resizing rules. - frontend/packages/dashboard/src/common.js - Panel.bind now also calls applyStoredWidth + bindResize. - resize handle is prepended to the drawer at bind time so every flyout (inbox, file preview, diff, journal) gets it. validation - npm run build --workspace=@hive/dashboard clean. CSS 40.9kb → 41.3kb. app.js + flow.js grew ~0.5kb each (resize handler). - browser smoke test isn't possible from inside iris's container; the resize math is straightforward (width = innerWidth - clientX, clamped) and the CSS variable + localStorage persistence are standard patterns.
1660 lines
50 KiB
CSS
1660 lines
50 KiB
CSS
/* Shared Catppuccin palette + body typography + terminal pane styles.
|
|
Bundled in front of the dashboard-only rules below via esbuild. */
|
|
@import "@hive/shared/base.css";
|
|
@import "@hive/shared/terminal.css";
|
|
|
|
/* ─── tabbed dashboard chrome (#369) ────────────────────────────────
|
|
Top-of-page sticky header with banner + tab strip. Tab routing is
|
|
hash-based; tab panes are show/hide via the `.tab-pane-active`
|
|
class. SSE stays alive across tab switches so count pills update
|
|
live on inactive tabs without losing pulse on what's happening
|
|
elsewhere. */
|
|
|
|
body.dashboard-shell {
|
|
/* Full-width layout (#416 — mara: drop the 90em cap so wide screens
|
|
don't waste real estate on empty side margins). `padding: 0 1.5em
|
|
1.5em` keeps a small gutter on the left/right so cards don't kiss
|
|
the viewport edge; `.dashboard-chrome { margin: 0 -1.5em ... }`
|
|
still pulls the chrome bar edge-to-edge through that gutter. */
|
|
margin: 0;
|
|
padding: 0 1.5em 1.5em;
|
|
}
|
|
|
|
.dashboard-chrome {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 25;
|
|
background: rgba(30, 30, 46, 0.86);
|
|
-webkit-backdrop-filter: blur(8px) saturate(120%);
|
|
backdrop-filter: blur(8px) saturate(120%);
|
|
border-bottom: 1px solid var(--purple-dim);
|
|
padding: 0.4em 0 0;
|
|
margin: 0 -1.5em 1em;
|
|
}
|
|
.banner-thin {
|
|
text-align: center;
|
|
margin: 0;
|
|
padding: 0 1em;
|
|
font-size: 0.78em;
|
|
color: var(--purple-dim);
|
|
letter-spacing: 0.15em;
|
|
white-space: pre;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.tabbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.2em;
|
|
padding: 0.5em 1em 0;
|
|
border-bottom: 1px solid var(--purple-dim);
|
|
}
|
|
.tabbar .tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5em;
|
|
padding: 0.55em 1em 0.45em;
|
|
margin-bottom: -1px; /* overlap the tabbar bottom border */
|
|
color: var(--muted);
|
|
font-family: inherit;
|
|
font-size: 0.92em;
|
|
letter-spacing: 0.08em;
|
|
text-decoration: none;
|
|
border: 1px solid transparent;
|
|
border-bottom: 0;
|
|
border-radius: 4px 4px 0 0;
|
|
cursor: pointer;
|
|
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
|
}
|
|
.tabbar .tab:hover {
|
|
color: var(--purple);
|
|
background: rgba(203, 166, 247, 0.06);
|
|
}
|
|
.tabbar .tab.active {
|
|
color: var(--purple);
|
|
border-color: var(--purple-dim);
|
|
background: var(--bg);
|
|
/* Lift the active tab visually — the bottom border of the tabbar
|
|
yields under it via the -1px margin above. */
|
|
box-shadow: 0 -2px 12px -4px rgba(203, 166, 247, 0.4);
|
|
}
|
|
.tab-label { font-weight: bold; }
|
|
.tab-count {
|
|
display: inline-block;
|
|
background: var(--purple-dim);
|
|
color: var(--purple);
|
|
border-radius: 999px;
|
|
padding: 0 0.55em;
|
|
min-width: 1.6em;
|
|
text-align: center;
|
|
font-size: 0.82em;
|
|
font-variant-numeric: tabular-nums;
|
|
font-weight: bold;
|
|
}
|
|
.tab-count-attn {
|
|
/* Y3R C4LL — operator-action-required pill colour, harder to miss. */
|
|
background: rgba(243, 139, 168, 0.22);
|
|
color: var(--red);
|
|
}
|
|
|
|
/* Notification controls cohabit with the tabs (always-on chrome). */
|
|
.tabbar #notif-row {
|
|
margin-left: auto;
|
|
display: flex;
|
|
gap: 0.5em;
|
|
align-items: center;
|
|
padding-right: 0.5em;
|
|
}
|
|
|
|
/* Tab pane visibility — show only the active one. The .tab-pane-active
|
|
class is set by app.js based on the URL hash; default (no hash)
|
|
resolves to SW4RM. */
|
|
.tab-pane { display: none; }
|
|
.tab-pane.tab-pane-active { display: block; }
|
|
|
|
/* FL0W is a separate page (`/flow.html`) — its full-viewport vibec0re
|
|
styling lives further down under `body.flow-shell`. */
|
|
.banner {
|
|
text-align: center;
|
|
margin: 0 0 1em 0;
|
|
font-size: 0.95em;
|
|
overflow-x: auto;
|
|
background: linear-gradient(
|
|
90deg,
|
|
var(--purple-dim) 0%,
|
|
var(--purple) 50%,
|
|
var(--purple-dim) 100%
|
|
);
|
|
background-size: 200% 100%;
|
|
background-position: 50% 0;
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
color: transparent;
|
|
filter: drop-shadow(0 0 6px rgba(203, 166, 247, 0.45));
|
|
}
|
|
.banner.active {
|
|
animation: banner-shimmer 1.8s linear infinite;
|
|
}
|
|
@keyframes banner-shimmer {
|
|
from { background-position: 200% 0; }
|
|
to { background-position: -100% 0; }
|
|
}
|
|
h1, h2 {
|
|
color: var(--purple);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.15em;
|
|
margin-top: 2em;
|
|
text-shadow: 0 0 8px rgba(203, 166, 247, 0.4);
|
|
}
|
|
.divider {
|
|
color: var(--purple-dim);
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
margin-bottom: 0.5em;
|
|
}
|
|
ul { list-style: none; padding-left: 0; }
|
|
li { padding: 0.5em 0; }
|
|
.glyph { color: var(--purple); margin-right: 0.5em; }
|
|
a {
|
|
color: var(--cyan);
|
|
text-decoration: none;
|
|
font-weight: bold;
|
|
text-shadow: 0 0 4px rgba(137, 220, 235, 0.5);
|
|
}
|
|
a:hover {
|
|
color: var(--fg);
|
|
text-shadow: 0 0 12px rgba(137, 220, 235, 0.9);
|
|
}
|
|
.role {
|
|
display: inline-block;
|
|
margin-left: 0.4em;
|
|
padding: 0.05em 0.5em;
|
|
border: 1px solid;
|
|
border-radius: 2px;
|
|
font-size: 0.8em;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
.role-m1nd { color: var(--pink); border-color: var(--pink); background: rgba(245, 194, 231, 0.08); }
|
|
.role-ag3nt { color: var(--amber); border-color: var(--amber); background: rgba(250, 179, 135, 0.08); }
|
|
/* Container rows: a full-height square agent icon on the left, the
|
|
identity / actions / drill-in lines stacked in the card body on the
|
|
right. Pending rows dim everything except the pending indicator. */
|
|
.containers { display: flex; flex-direction: column; gap: 0.4em; }
|
|
.container-row {
|
|
padding: 0.6em 0.8em;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
background: rgba(24, 24, 37, 0.55);
|
|
transition: opacity 200ms ease, border-color 200ms ease;
|
|
}
|
|
/* Topology indent (#363). Each depth level shifts the row right by one
|
|
step; the .tree-prefix span (drawn by app.js::treePrefix) carries
|
|
the ├─ / └─ glyph and any continuation lines that thread through
|
|
ancestor columns. When every container has parent=null (pre-#361
|
|
state) `[data-depth]` is absent on every row and these rules are
|
|
no-ops — the layout reads exactly like the legacy flat list.
|
|
Per-depth indent: hardcoded steps for 6 levels (sufficient for any
|
|
plausible hive topology) — the typed `attr()` function from CSS
|
|
Values 5 would collapse this to one rule, but browser support is
|
|
still partial (Chromium-only as of 2026). */
|
|
.container-row[data-depth] { position: relative; }
|
|
.container-row[data-depth="1"] { margin-left: 1.8em; }
|
|
.container-row[data-depth="2"] { margin-left: 3.6em; }
|
|
.container-row[data-depth="3"] { margin-left: 5.4em; }
|
|
.container-row[data-depth="4"] { margin-left: 7.2em; }
|
|
.container-row[data-depth="5"] { margin-left: 9em; }
|
|
.container-row[data-depth="6"] { margin-left: 10.8em; }
|
|
/* Tree prefix sits in the left margin and paints the connecting
|
|
├ / └ / │ lanes as CSS rules rather than text glyphs (#388). Each
|
|
ancestor depth gets its own `.tree-lane` so we can paint a
|
|
full-row-height vertical bar that extends through the
|
|
.containers row gap into the next sibling — text box-drawing
|
|
glyphs only fill one text line, which left visible breaks
|
|
between rows once cards grew taller than one line of text (5em
|
|
square icons + multi-line body). The horizontal stub at the row's
|
|
own joint lands at the icon midline so the L/T meets the icon
|
|
edge cleanly. */
|
|
.container-row .tree-prefix {
|
|
position: absolute;
|
|
/* Extend into the `.containers { gap: 0.4em }` below so vertical
|
|
bars connect to the next sibling's prefix without a visual gap. */
|
|
top: 0;
|
|
bottom: -0.4em;
|
|
display: flex;
|
|
flex-direction: row;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
color: var(--purple-dim);
|
|
}
|
|
/* Each depth step is one 1.8em lane wide — same step as the row's
|
|
own margin-left ladder above, so the rightmost lane (the joint)
|
|
sits flush against the row content (the icon). The prefix's left
|
|
edge is depth*1.8em LEFT of the row's left edge, so its right
|
|
edge meets the icon, and the leftmost lane lines up with the
|
|
top-level rows' icons at x=0. */
|
|
.container-row[data-depth="1"] .tree-prefix { left: -1.8em; }
|
|
.container-row[data-depth="2"] .tree-prefix { left: -3.6em; }
|
|
.container-row[data-depth="3"] .tree-prefix { left: -5.4em; }
|
|
.container-row[data-depth="4"] .tree-prefix { left: -7.2em; }
|
|
.container-row[data-depth="5"] .tree-prefix { left: -9em; }
|
|
.container-row[data-depth="6"] .tree-prefix { left: -10.8em; }
|
|
.tree-prefix .tree-lane {
|
|
flex: 0 0 1.8em;
|
|
position: relative;
|
|
}
|
|
/* Continuation bar — full row + gap below (so two adjacent
|
|
ancestor-line lanes from sibling rows visually merge into one
|
|
unbroken vertical line). Drawn at lane center (0.6em from left)
|
|
to keep it visually centered in the 1.8em column. */
|
|
.tree-prefix .lane-line::before,
|
|
.tree-prefix .lane-joint::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0.6em;
|
|
top: 0;
|
|
bottom: 0;
|
|
border-left: 1px solid currentColor;
|
|
}
|
|
/* Last-child joint (└): vertical bar stops at the icon midline (no
|
|
continuation below this row). 3.1em ≈ row padding-top 0.6em +
|
|
icon half-height 2.5em, matching the horizontal stub's y below. */
|
|
.tree-prefix .lane-joint-last::before {
|
|
bottom: auto;
|
|
height: 3.1em;
|
|
}
|
|
/* Horizontal stub from the joint's vertical bar across the lane
|
|
and through the row's padding-left to the icon left edge. Lands
|
|
at the icon midline so the ├/└ meets the icon cleanly.
|
|
Width = lane-right (1.2em from lane center) + row padding-left
|
|
(0.8em) = 2em. */
|
|
.tree-prefix .lane-joint::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0.6em;
|
|
top: 3.1em;
|
|
width: 2em;
|
|
border-top: 1px solid currentColor;
|
|
}
|
|
/* Live cards get the icon-left / body-right split; tombstone rows keep
|
|
the plain stacked block layout. The icon is a background-image div
|
|
with no intrinsic size, so its load state can never reflow the row
|
|
(issue #177). It used to `align-self: stretch` to fill the body
|
|
height, but with state badges / rate-limit pills / etc. wrapping the
|
|
head row, the body grew taller and the square icon grew with it —
|
|
so two cards with different content showed different-sized icons
|
|
(issue #344). Fixed at 5em now; height follows from aspect-ratio. */
|
|
.container-row:not(.tombstone) {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.7em;
|
|
}
|
|
.container-row:not(.tombstone) > .container-icon {
|
|
position: relative;
|
|
overflow: hidden;
|
|
flex: none;
|
|
width: 5em;
|
|
aspect-ratio: 1;
|
|
border-radius: 6px;
|
|
background-color: rgba(17, 17, 27, 0.6);
|
|
/* #443 — icon is the selection toggle. Cursor + hover ring make
|
|
that affordable without a chrome change. The :focus-visible ring
|
|
covers keyboard activation (Enter / Space). */
|
|
cursor: pointer;
|
|
transition: box-shadow 120ms ease, transform 120ms ease;
|
|
}
|
|
.container-row:not(.tombstone) > .container-icon:hover {
|
|
box-shadow: 0 0 0 2px var(--purple);
|
|
}
|
|
.container-row:not(.tombstone) > .container-icon:focus-visible {
|
|
outline: 2px solid var(--purple);
|
|
outline-offset: 2px;
|
|
}
|
|
/* Selected state — mauve ring on the icon + a matching outline on the
|
|
whole row so a glance at the list shows what's in the active set. */
|
|
.container-row.selected {
|
|
border-color: var(--purple);
|
|
box-shadow: inset 0 0 0 1px var(--purple);
|
|
background: rgba(203, 166, 247, 0.06);
|
|
}
|
|
.container-row.selected > .container-icon {
|
|
box-shadow: 0 0 0 2px var(--purple), 0 0 12px -4px var(--purple);
|
|
}
|
|
/* The icon image fills the square wrapper and is taken out of flow
|
|
(absolute) so its load state — pending, loaded, broken — can never
|
|
contribute intrinsic size or reflow the row. (issue #177) */
|
|
.container-row:not(.tombstone) > .container-icon > .container-icon-img {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
/* When the <img> fails to load it falls back to the dimmed hyperhive
|
|
mark, standing in for the unreachable agent icon (issues #195, #202). */
|
|
.container-row:not(.tombstone) > .container-icon.icon-unreachable {
|
|
filter: grayscale(1);
|
|
opacity: 0.4;
|
|
}
|
|
.container-row .card-body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.container-row.pending {
|
|
border-color: var(--amber);
|
|
background: rgba(250, 179, 135, 0.05);
|
|
}
|
|
.container-row.pending .actions { opacity: 0.4; pointer-events: none; }
|
|
.container-row .head {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 0.5em;
|
|
margin-bottom: 0.4em;
|
|
}
|
|
.container-row .head .name {
|
|
font-size: 1.05em;
|
|
font-weight: bold;
|
|
}
|
|
.container-row .head .meta { margin-left: auto; }
|
|
/* Icon-only nav strip in the head row — the per-container backend-
|
|
supplied link list (issue #262). Inline-flex + gap so a longer list
|
|
(e.g. with `dashboardLinks` extras) doesn't cram (issue #333). Each
|
|
link gets a comfortable hit target with a subtle hover so the
|
|
icons read as interactive rather than decorative. */
|
|
.container-row .head .nav-strip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 0.35em;
|
|
}
|
|
.nav-link {
|
|
color: var(--muted);
|
|
font-size: 0.95em;
|
|
line-height: 1;
|
|
padding: 0.15em 0.35em;
|
|
border-radius: 3px;
|
|
text-decoration: none;
|
|
transition: background 0.12s ease, color 0.12s ease;
|
|
}
|
|
.nav-link:hover {
|
|
background: rgba(203, 166, 247, 0.12);
|
|
color: var(--cyan);
|
|
text-shadow: 0 0 6px rgba(137, 220, 235, 0.5);
|
|
}
|
|
.container-row .actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.4em;
|
|
}
|
|
.container-row .actions form.inline { display: inline-block; margin: 0; }
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 0.05em 0.5em;
|
|
border: 1px solid;
|
|
border-radius: 2px;
|
|
font-size: 0.75em;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
.badge-warn {
|
|
color: var(--amber); border-color: var(--amber);
|
|
text-shadow: 0 0 6px rgba(250, 179, 135, 0.5);
|
|
}
|
|
.badge-rate-limited {
|
|
color: var(--red); border-color: var(--red);
|
|
text-shadow: 0 0 6px rgba(243, 139, 168, 0.5);
|
|
}
|
|
.badge-muted {
|
|
color: var(--muted); border-color: var(--purple-dim);
|
|
background: rgba(127, 132, 156, 0.08);
|
|
}
|
|
.badge-reminder {
|
|
color: var(--cyan); border-color: var(--cyan);
|
|
text-shadow: 0 0 6px rgba(137, 220, 235, 0.4);
|
|
}
|
|
/* Context-window usage badges on dashboard container rows. Thresholds
|
|
are derived per-container: yellow ≥ 50% and red ≥ 75% of the model's
|
|
context window (`ContainerView.context_window_tokens`), mirroring the
|
|
harness compaction watermarks. Falls back to fixed 100k / 150k when
|
|
the window is unknown. (issue #66) */
|
|
.badge-ctx-ok {
|
|
color: var(--green); border-color: var(--green);
|
|
opacity: 0.85;
|
|
}
|
|
.badge-ctx-caution {
|
|
color: var(--amber); border-color: var(--amber);
|
|
text-shadow: 0 0 6px rgba(250, 179, 135, 0.5);
|
|
}
|
|
.badge-ctx-warn {
|
|
color: var(--red); border-color: var(--red);
|
|
text-shadow: 0 0 6px rgba(243, 139, 168, 0.5);
|
|
}
|
|
.agent-status {
|
|
font-size: 0.82em;
|
|
color: var(--subtext0, #a6adc8);
|
|
padding: 0.1em 0.3em 0.25em;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.agent-status .status-icon { opacity: 0.65; }
|
|
.agent-status .status-age { opacity: 0.5; font-size: 0.9em; margin-left: 0.2em; }
|
|
|
|
.container-row.tombstone {
|
|
border-style: dashed;
|
|
background: rgba(24, 24, 37, 0.35);
|
|
opacity: 0.85;
|
|
}
|
|
.container-row.tombstone .name { color: var(--muted); }
|
|
/* Per-container journald viewer + applied-config viewer. Both open
|
|
in the side panel and lazy-fetch on open; output is monospace
|
|
inside a bordered <pre>, controls (unit select + refresh) above. */
|
|
.journal-controls {
|
|
display: flex;
|
|
gap: 0.5em;
|
|
margin-bottom: 0.4em;
|
|
align-items: center;
|
|
}
|
|
.journal-unit {
|
|
font-family: inherit;
|
|
font-size: 0.9em;
|
|
background: var(--bg-elev);
|
|
color: var(--fg);
|
|
border: 1px solid var(--border);
|
|
padding: 0.2em 0.4em;
|
|
}
|
|
.journal-refresh { font-size: 0.75em; padding: 0.15em 0.5em; }
|
|
.journal-output {
|
|
margin: 0;
|
|
background: #11111b;
|
|
color: var(--fg);
|
|
border: 1px solid var(--purple-dim);
|
|
padding: 0.5em 0.7em;
|
|
overflow-x: auto;
|
|
font-size: 0.85em;
|
|
line-height: 1.4;
|
|
white-space: pre;
|
|
word-break: normal;
|
|
}
|
|
|
|
/* Notification controls — sit between the banner and the
|
|
containers section. Hidden by JS when notifications are
|
|
unsupported, denied, or already in the right state. */
|
|
/* Port-collision banner: appears above the containers list when
|
|
two sub-agents hash to the same web UI port. Critical — without
|
|
resolution, one of the harnesses will restart-loop on
|
|
AddrInUse. */
|
|
.port-conflict {
|
|
background: rgba(243, 139, 168, 0.08);
|
|
border: 1px solid var(--red);
|
|
color: var(--red);
|
|
padding: 0.5em 0.8em;
|
|
margin-bottom: 0.6em;
|
|
border-radius: 4px;
|
|
text-shadow: 0 0 6px rgba(243, 139, 168, 0.4);
|
|
animation: questions-pulse 2.4s ease-in-out infinite;
|
|
}
|
|
.port-conflict strong { color: var(--red); }
|
|
|
|
.notif-row {
|
|
display: flex;
|
|
gap: 0.5em;
|
|
align-items: center;
|
|
margin: 0.5em 0;
|
|
font-size: 0.85em;
|
|
}
|
|
.btn-notif {
|
|
font-family: inherit;
|
|
font-size: 0.85em;
|
|
background: transparent;
|
|
color: var(--cyan);
|
|
border: 1px solid var(--cyan);
|
|
padding: 0.2em 0.7em;
|
|
border-radius: 999px;
|
|
cursor: pointer;
|
|
text-shadow: 0 0 4px currentColor;
|
|
}
|
|
.btn-notif:hover {
|
|
background: rgba(137, 220, 235, 0.1);
|
|
box-shadow: 0 0 10px -2px currentColor;
|
|
}
|
|
|
|
.pending-state {
|
|
color: var(--amber);
|
|
font-size: 0.85em;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
text-shadow: 0 0 6px rgba(250, 179, 135, 0.55);
|
|
animation: badge-pulse 1.6s ease-in-out infinite;
|
|
}
|
|
@keyframes badge-pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.7; }
|
|
}
|
|
.meta { color: var(--muted); font-size: 0.85em; margin-left: 0.4em; }
|
|
.id { color: var(--pink); font-weight: bold; margin-right: 0.4em; }
|
|
.agent { color: var(--amber); font-weight: bold; margin-right: 0.6em; }
|
|
.empty { color: var(--muted); font-style: italic; }
|
|
code {
|
|
color: var(--amber);
|
|
background: var(--bg-elev);
|
|
padding: 0.1em 0.4em;
|
|
border: 1px solid var(--border);
|
|
border-radius: 2px;
|
|
font-size: 0.9em;
|
|
}
|
|
/* Pending approval: a card with three stacked sections — identity
|
|
header, what-changed body, decision actions. */
|
|
.approvals { list-style: none; padding: 0; margin: 0.4em 0 0; }
|
|
.approval-card {
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-left: 3px solid var(--purple);
|
|
border-radius: 4px;
|
|
padding: 0.6em 0.8em;
|
|
margin-bottom: 0.6em;
|
|
}
|
|
.approval-head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
flex-wrap: wrap;
|
|
gap: 0.3em;
|
|
}
|
|
/* When the approval was requested — right-aligned in the head row;
|
|
goes amber once it has been pending ≥ 1h so a stale request stands
|
|
out at a glance (issue #272). */
|
|
.approval-ts {
|
|
margin-left: auto;
|
|
color: var(--muted);
|
|
font-size: 0.85em;
|
|
}
|
|
.approval-ts.stale {
|
|
color: var(--amber);
|
|
text-shadow: 0 0 6px rgba(250, 179, 135, 0.5);
|
|
}
|
|
.approval-body {
|
|
margin: 0.45em 0;
|
|
padding-left: 1.3em;
|
|
}
|
|
.approval-description {
|
|
font-size: 0.9em;
|
|
color: var(--fg);
|
|
white-space: pre-wrap;
|
|
margin-bottom: 0.35em;
|
|
}
|
|
.approval-actions {
|
|
display: flex;
|
|
gap: 0.5em;
|
|
padding-top: 0.45em;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.approval-actions form.inline { display: inline; }
|
|
/* Inline drill-in triggers (logs / config repo / view diff). */
|
|
.drill-ins {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.15em 1.1em;
|
|
margin-top: 0.4em;
|
|
}
|
|
.drill-ins .panel-trigger { margin-top: 0; }
|
|
/* Diff side-panel: base-toggle tabs above the diff host. */
|
|
.diff-panel { display: flex; flex-direction: column; gap: 0.6em; }
|
|
.diff-base-tabs { display: flex; flex-wrap: wrap; gap: 0.4em; }
|
|
.diff-base-tab {
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
color: var(--muted);
|
|
font: inherit;
|
|
font-size: 0.85em;
|
|
padding: 0.2em 0.7em;
|
|
cursor: pointer;
|
|
}
|
|
.diff-base-tab:hover { color: var(--fg); }
|
|
.diff-base-tab.active {
|
|
color: var(--purple);
|
|
border-color: var(--purple);
|
|
background: rgba(203, 166, 247, 0.08);
|
|
}
|
|
/* Image / tabbed file preview (issues #188, #192) */
|
|
.preview-host { margin-top: 0.5em; }
|
|
.img-preview {
|
|
display: block;
|
|
max-width: 100%;
|
|
height: auto;
|
|
margin: 0 auto;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
/* checkerboard so transparent regions of the image read clearly */
|
|
background: repeating-conic-gradient(#313244 0% 25%, #1e1e2e 0% 50%) 50% / 18px 18px;
|
|
}
|
|
.approval-tabs {
|
|
display: flex;
|
|
gap: 0.4em;
|
|
margin: 0.6em 0 0.4em;
|
|
}
|
|
.approval-tab {
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
color: var(--muted);
|
|
font: inherit;
|
|
font-size: 0.85em;
|
|
letter-spacing: 0.08em;
|
|
padding: 0.25em 0.9em;
|
|
cursor: pointer;
|
|
transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
|
|
}
|
|
.approval-tab:hover { color: var(--fg); }
|
|
.approval-tab.active {
|
|
color: var(--purple);
|
|
border-color: var(--purple);
|
|
background: rgba(203, 166, 247, 0.08);
|
|
text-shadow: 0 0 4px currentColor;
|
|
}
|
|
.approvals-history .status { font-size: 0.85em; padding: 0 0.5em; }
|
|
.status-approved { color: var(--green); }
|
|
.status-denied { color: var(--red); }
|
|
.status-failed { color: var(--amber); }
|
|
.glyph-approved { color: var(--green); }
|
|
.glyph-denied { color: var(--red); }
|
|
.glyph-failed { color: var(--amber); }
|
|
.meta-inputs {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0 0 0.8em;
|
|
display: grid;
|
|
gap: 0.2em;
|
|
}
|
|
.meta-inputs li {
|
|
padding: 0.25em 0.6em;
|
|
border: 1px solid var(--border);
|
|
background: rgba(24, 24, 37, 0.6);
|
|
}
|
|
.meta-inputs label {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.5em;
|
|
cursor: pointer;
|
|
font-size: 0.9em;
|
|
}
|
|
.meta-input-name { color: var(--amber); font-weight: bold; }
|
|
.meta-input-rev { color: var(--muted); }
|
|
.meta-input-ts { color: var(--muted); font-size: 0.85em; }
|
|
.meta-input-url {
|
|
color: var(--muted);
|
|
font-size: 0.85em;
|
|
margin-left: auto;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
/* Bulk select-all / -none control above the meta-inputs tree (#275). */
|
|
.meta-inputs-bulk {
|
|
margin: 0 0 0.5em;
|
|
font-size: 0.8em;
|
|
color: var(--muted);
|
|
}
|
|
.meta-bulk-btn {
|
|
font: inherit;
|
|
font-size: 1em;
|
|
background: transparent;
|
|
border: 1px solid var(--purple-dim);
|
|
color: var(--cyan);
|
|
padding: 0.1em 0.6em;
|
|
margin-right: 0.2em;
|
|
cursor: pointer;
|
|
}
|
|
.meta-bulk-btn:hover {
|
|
border-color: var(--cyan);
|
|
text-shadow: 0 0 6px currentColor;
|
|
}
|
|
/* Tree twig glyph prefixing a nested (sub-)input row (#275). */
|
|
.meta-input-twig {
|
|
color: var(--purple-dim);
|
|
margin-right: 0.1em;
|
|
}
|
|
.btn-meta-update {
|
|
background: rgba(203, 166, 247, 0.12);
|
|
border: 1px solid var(--purple);
|
|
color: var(--purple);
|
|
text-shadow: 0 0 4px currentColor;
|
|
padding: 0.3em 1em;
|
|
font: inherit;
|
|
font-size: 0.85em;
|
|
letter-spacing: 0.08em;
|
|
cursor: pointer;
|
|
transition: box-shadow 0.15s ease, background 0.15s ease;
|
|
}
|
|
.btn-meta-update:hover:not([disabled]) {
|
|
background: rgba(203, 166, 247, 0.22);
|
|
box-shadow: 0 0 10px -2px currentColor;
|
|
}
|
|
.btn-meta-update[disabled] {
|
|
opacity: 0.35;
|
|
cursor: not-allowed;
|
|
}
|
|
/* In-progress banner for the META INPUTS panel: shown while a
|
|
dashboard-triggered meta-update runs in the background (issue #259). */
|
|
.meta-update-running {
|
|
margin: 0 0 0.7em;
|
|
padding: 0.4em 0.7em;
|
|
border: 1px solid var(--purple);
|
|
background: rgba(203, 166, 247, 0.12);
|
|
color: var(--purple);
|
|
font-size: 0.85em;
|
|
animation: badge-pulse 1.6s ease-in-out infinite;
|
|
}
|
|
/* ─── rebuild queue panel ──────────────────────────────────────────────── */
|
|
.rebuild-queue {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
display: grid;
|
|
gap: 0.2em;
|
|
}
|
|
.rebuild-queue-entry {
|
|
padding: 0.3em 0.6em;
|
|
border: 1px solid var(--border);
|
|
background: rgba(24, 24, 37, 0.6);
|
|
font-size: 0.9em;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 0.4em;
|
|
}
|
|
.rebuild-queue-entry.rqe-child { margin-left: 1.6em; border-color: var(--purple-dim); }
|
|
.rebuild-queue-entry.rqe-running {
|
|
border-color: var(--purple);
|
|
background: rgba(203, 166, 247, 0.12);
|
|
animation: badge-pulse 1.6s ease-in-out infinite;
|
|
}
|
|
.rebuild-queue-entry.rqe-failed { border-color: var(--red); color: var(--red); }
|
|
.rebuild-queue-entry.rqe-cancelled { opacity: 0.6; }
|
|
.rebuild-queue-entry.rqe-done { opacity: 0.7; color: var(--green); }
|
|
.rqe-state { font-weight: bold; min-width: 1.2em; text-align: center; }
|
|
.rqe-kind { color: var(--cyan); }
|
|
.rqe-agent { color: var(--amber); font-weight: bold; }
|
|
.rqe-source {
|
|
font-size: 0.75em;
|
|
padding: 0.05em 0.45em;
|
|
border-radius: 0.7em;
|
|
border: 1px solid var(--border);
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
.rqe-source-manual { color: var(--cyan); border-color: var(--cyan); }
|
|
.rqe-source-meta_update { color: var(--purple); border-color: var(--purple); }
|
|
.rqe-source-auto_update { color: var(--muted); }
|
|
.rqe-source-crash_recover { color: var(--amber); border-color: var(--amber); }
|
|
.rqe-source-approval { color: var(--green); border-color: var(--green); }
|
|
.rqe-when { color: var(--muted); font-size: 0.85em; }
|
|
.rqe-reason { color: var(--muted); font-size: 0.85em; flex: 1 1 auto; }
|
|
.rqe-error {
|
|
flex-basis: 100%;
|
|
margin: 0.3em 0 0;
|
|
padding: 0.3em 0.5em;
|
|
background: rgba(243, 139, 168, 0.1);
|
|
border-left: 2px solid var(--red);
|
|
color: var(--red);
|
|
font-size: 0.8em;
|
|
white-space: pre-wrap;
|
|
}
|
|
.history-note {
|
|
margin-left: 1.8em;
|
|
margin-top: 0.2em;
|
|
color: var(--muted);
|
|
font-size: 0.85em;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
ul form.inline { display: inline-block; }
|
|
.btn {
|
|
font-family: inherit;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
background: transparent;
|
|
border: 1px solid;
|
|
padding: 0.25em 0.8em;
|
|
cursor: pointer;
|
|
text-shadow: 0 0 4px currentColor;
|
|
box-shadow: 0 0 0 0 currentColor;
|
|
transition: box-shadow 0.15s ease;
|
|
}
|
|
.btn:hover {
|
|
background: rgba(205, 214, 244, 0.06);
|
|
text-shadow: 0 0 10px currentColor;
|
|
box-shadow: 0 0 10px -2px currentColor;
|
|
}
|
|
.btn:disabled,
|
|
.btn[disabled] {
|
|
opacity: 0.32;
|
|
cursor: not-allowed;
|
|
text-shadow: none;
|
|
box-shadow: none;
|
|
}
|
|
.btn:disabled:hover,
|
|
.btn[disabled]:hover {
|
|
background: transparent;
|
|
text-shadow: none;
|
|
box-shadow: none;
|
|
}
|
|
.btn-approve { color: var(--green); border-color: var(--green); }
|
|
.btn-deny { color: var(--red); border-color: var(--red); }
|
|
.btn-destroy { color: var(--red); border-color: var(--red); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
|
.btn-rebuild { color: var(--amber); border-color: var(--amber); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
|
.btn-restart { color: var(--cyan); border-color: var(--cyan); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
|
.btn-stop { color: var(--pink); border-color: var(--pink); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
|
.btn-start { color: var(--green); border-color: var(--green); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
|
.btn-talk { color: var(--cyan); border-color: var(--cyan); }
|
|
.btn-spawn { color: var(--amber); border-color: var(--amber); }
|
|
.spawnform { display: flex; gap: 0.6em; align-items: stretch; margin: 0.5em 0; }
|
|
.spawnform input {
|
|
font-family: inherit;
|
|
font-size: 1em;
|
|
background: var(--bg-elev);
|
|
color: var(--fg);
|
|
border: 1px solid var(--border);
|
|
padding: 0.4em 0.6em;
|
|
flex: 1;
|
|
}
|
|
.spawnform input::placeholder { color: var(--muted); }
|
|
.spawnform input:focus { outline: 1px solid var(--purple); }
|
|
.role-pending { color: var(--amber); border-color: var(--amber); }
|
|
.btn-inline {
|
|
font-family: inherit;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
margin-left: 0.4em;
|
|
}
|
|
.btn-inline:hover { background: rgba(255, 184, 77, 0.1); }
|
|
.kind {
|
|
display: inline-block;
|
|
margin-left: 0.4em;
|
|
padding: 0.05em 0.5em;
|
|
border: 1px solid var(--purple-dim);
|
|
color: var(--purple-dim);
|
|
border-radius: 2px;
|
|
font-size: 0.75em;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
.kind-spawn { color: var(--amber); border-color: var(--amber); }
|
|
.spinner {
|
|
display: inline-block;
|
|
animation: spin 1s linear infinite;
|
|
color: var(--amber);
|
|
}
|
|
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
.talkform {
|
|
display: flex;
|
|
gap: 0.6em;
|
|
align-items: stretch;
|
|
margin-top: 0.5em;
|
|
}
|
|
.talkform select, .talkform input {
|
|
font-family: inherit;
|
|
font-size: 1em;
|
|
background: var(--bg-elev);
|
|
color: var(--fg);
|
|
border: 1px solid var(--border);
|
|
padding: 0.4em 0.6em;
|
|
}
|
|
.talkform select { color: var(--amber); }
|
|
.talkform input { flex: 1; }
|
|
.talkform input::placeholder { color: var(--muted); }
|
|
.talkform input:focus, .talkform select:focus { outline: 1px solid var(--purple); }
|
|
details { margin-top: 0.5em; }
|
|
summary {
|
|
cursor: pointer;
|
|
color: var(--muted);
|
|
font-size: 0.85em;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
}
|
|
summary:hover { color: var(--purple); }
|
|
.diff {
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
padding: 0.8em;
|
|
margin-top: 0.4em;
|
|
overflow-x: auto;
|
|
font-size: 0.85em;
|
|
line-height: 1.4;
|
|
color: var(--muted);
|
|
white-space: pre;
|
|
}
|
|
.diff span { display: block; }
|
|
.diff .diff-add { color: var(--green); }
|
|
.diff .diff-del { color: var(--red); }
|
|
.diff .diff-hunk { color: var(--cyan); }
|
|
.diff .diff-file { color: var(--purple); font-weight: bold; }
|
|
.diff .diff-ctx { color: var(--fg); }
|
|
.questions {
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--amber);
|
|
box-shadow: 0 0 12px -4px var(--amber);
|
|
padding: 0.6em 0.9em;
|
|
animation: questions-pulse 2.4s ease-in-out infinite;
|
|
}
|
|
@keyframes questions-pulse {
|
|
0%, 100% { box-shadow: 0 0 12px -4px rgba(250, 179, 135, 0.55); }
|
|
50% { box-shadow: 0 0 22px -2px rgba(250, 179, 135, 0.95); }
|
|
}
|
|
/* Reminders list — rendered from /api/reminders, separate from the
|
|
main /api/state snapshot. Each row stacks identity, head meta,
|
|
body, and a small cancel form. */
|
|
.reminders {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
.reminder-row {
|
|
padding: 0.4em 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.reminder-row:last-child { border-bottom: 0; }
|
|
.reminder-head { font-size: 0.9em; }
|
|
.reminder-body {
|
|
color: var(--fg);
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
margin: 0.3em 0;
|
|
}
|
|
.reminder-row.reminder-failed {
|
|
border-left: 2px solid var(--red, #f38ba8);
|
|
padding-left: 0.5em;
|
|
}
|
|
.reminder-error {
|
|
color: var(--red, #f38ba8);
|
|
background: rgba(243, 139, 168, 0.06);
|
|
border: 1px solid rgba(243, 139, 168, 0.25);
|
|
padding: 0.3em 0.5em;
|
|
font-size: 0.85em;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
margin: 0.2em 0;
|
|
}
|
|
.reminder-actions {
|
|
display: flex;
|
|
gap: 0.4em;
|
|
margin-top: 0.3em;
|
|
}
|
|
|
|
/* Path linkification — agents drop pointer strings into messages
|
|
constantly; clicking the anchor opens the file in the side panel,
|
|
lazy-loaded from /api/state-file. */
|
|
.path-link {
|
|
color: var(--blue, #89b4fa);
|
|
text-decoration: underline dotted;
|
|
cursor: pointer;
|
|
}
|
|
.path-link:hover { color: var(--amber); }
|
|
/* File-preview body — rendered inside the side panel. */
|
|
.path-preview-body {
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
padding: 0.5em 0.7em;
|
|
margin: 0;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
font-size: 0.85em;
|
|
color: var(--fg);
|
|
}
|
|
|
|
/* Filter chip row above the questions list. The active chip lights
|
|
up amber to match the rest of the dashboard's selection accents. */
|
|
.questions-filters {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.3em;
|
|
margin-bottom: 0.5em;
|
|
}
|
|
.q-filter-chip {
|
|
background: var(--bg);
|
|
color: var(--muted);
|
|
border: 1px solid var(--border);
|
|
border-radius: 999px;
|
|
padding: 0.15em 0.7em;
|
|
font: inherit;
|
|
font-size: 0.85em;
|
|
cursor: pointer;
|
|
}
|
|
.q-filter-chip:hover { color: var(--fg); }
|
|
.q-filter-chip.active {
|
|
color: var(--amber);
|
|
border-color: var(--amber);
|
|
}
|
|
/* Peer (agent-to-agent) question rows get a left rule + dim
|
|
target-name styling so they read distinctly from operator-bound
|
|
threads at a glance. */
|
|
.questions li.question-peer {
|
|
border-left: 2px solid var(--mauve, #cba6f7);
|
|
padding-left: 0.6em;
|
|
}
|
|
.questions .msg-to-peer { color: var(--mauve, #cba6f7); }
|
|
/* The override button on peer threads picks up a non-default colour
|
|
so the operator notices they're answering on someone's behalf. */
|
|
.btn-override { background: var(--mauve, #cba6f7) !important; color: var(--bg) !important; }
|
|
.questions li.question {
|
|
padding: 0.4em 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.questions li.question:last-child { border-bottom: 0; }
|
|
.questions .q-head { font-size: 0.9em; }
|
|
.questions .q-ttl {
|
|
color: var(--amber);
|
|
margin-left: 0.4em;
|
|
font-size: 0.95em;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
.questions .q-body {
|
|
color: var(--fg);
|
|
margin: 0.3em 0;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
.qform {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5em;
|
|
margin-top: 0.4em;
|
|
}
|
|
.qform .q-options {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25em;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 0.4em 0.6em;
|
|
}
|
|
.qform .q-option label { cursor: pointer; user-select: none; }
|
|
.qform .q-option input { margin-right: 0.4em; accent-color: var(--amber); }
|
|
.qform .q-free { display: flex; }
|
|
.qform .q-free textarea {
|
|
flex: 1;
|
|
font-family: inherit;
|
|
font-size: 1em;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
border: 1px solid var(--border);
|
|
padding: 0.4em 0.6em;
|
|
resize: vertical;
|
|
line-height: 1.4;
|
|
}
|
|
.qform .q-free textarea::placeholder { color: var(--muted); }
|
|
.qform .q-free textarea:focus { outline: 1px solid var(--amber); }
|
|
.qform button { align-self: flex-start; }
|
|
.qform-cancel { margin-top: 0.3em; }
|
|
.q-history {
|
|
margin-top: 0.8em;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 0.4em 0.7em;
|
|
}
|
|
.q-history summary { cursor: pointer; color: var(--muted); font-size: 0.9em; user-select: none; }
|
|
.questions-answered {
|
|
border: none;
|
|
box-shadow: none;
|
|
animation: none;
|
|
padding: 0;
|
|
margin-top: 0.5em;
|
|
}
|
|
.question-answered { opacity: 0.7; }
|
|
.question-answered .q-body { color: var(--muted); margin-bottom: 0.15em; }
|
|
.q-answer { font-size: 0.9em; color: var(--green, #a6e3a1); padding: 0.1em 0 0.4em 0; }
|
|
.q-answer-text { font-style: italic; }
|
|
.inbox {
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
padding: 0.5em 0.8em;
|
|
/* #450: no max-height cap — let the inbox grow to fill the
|
|
side-panel-body which already scrolls (`overflow: auto`). The
|
|
pre-#450 24em cap clamped the list well short of the available
|
|
panel height even on tall viewports. */
|
|
}
|
|
.inbox li {
|
|
padding: 0.25em 0;
|
|
border-bottom: 1px solid var(--border);
|
|
display: grid;
|
|
grid-template-columns: auto auto auto 1fr;
|
|
gap: 0.5em;
|
|
align-items: baseline;
|
|
}
|
|
.inbox li:last-child { border-bottom: 0; }
|
|
.inbox .msg-ts { color: var(--muted); font-size: 0.85em; }
|
|
.inbox .msg-from { color: var(--amber); }
|
|
.inbox .msg-sep { color: var(--muted); }
|
|
.inbox .msg-body { color: var(--fg); white-space: pre-wrap; word-break: break-word; }
|
|
/* `#msgflow` is a shared `.live` pane inside `.terminal-wrap` (see
|
|
hive-fr0nt::TERMINAL_CSS). The msgrow / msg-* rules below are
|
|
dashboard-specific: each broker event becomes a grid of timestamp +
|
|
arrow + from/sep/to + body inside the `.row` shell. */
|
|
/* Flex (not grid): the row carries the header chips (ts / arrow /
|
|
from / → / to / body) inline. Flex collapses whitespace-only text
|
|
nodes between items and gives `body` the remaining width via
|
|
`flex: 1`. Path references inside `body` are inline anchors that
|
|
open the side panel — no full-width sibling rows. */
|
|
.live .msgrow {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 0.5em;
|
|
padding: 0.1em 0;
|
|
/* Override the per-agent-terminal's hanging-indent metrics from
|
|
TERMINAL_CSS — the dashboard's broker rows are flex grids, not
|
|
glyph-prefixed text, and don't want the prefix column. */
|
|
text-indent: 0;
|
|
}
|
|
.live .msgrow .msg-body {
|
|
flex: 1 1 0;
|
|
/* min-width: 0 lets the body shrink below its longest token so
|
|
`word-break: break-word` actually kicks in instead of forcing
|
|
the whole flex line wider than the container. */
|
|
min-width: 0;
|
|
}
|
|
.live .msgrow.sent .msg-arrow { color: var(--cyan); }
|
|
.live .msgrow.delivered .msg-arrow { color: var(--green); }
|
|
/* Reply-thread rendering: indented border-left + muted reply tag. */
|
|
.live .msgrow.msg-reply {
|
|
padding-left: 1.2em;
|
|
border-left: 2px solid var(--border);
|
|
margin-left: 0.6em;
|
|
}
|
|
.msg-reply-tag {
|
|
color: var(--muted);
|
|
font-size: 0.8em;
|
|
white-space: nowrap;
|
|
order: -1; /* prepend before other flex items */
|
|
}
|
|
.msg-reply-tag a {
|
|
color: var(--muted);
|
|
text-shadow: none;
|
|
font-weight: normal;
|
|
}
|
|
.msg-reply-tag a:hover { color: var(--fg); }
|
|
/* Flash highlight when scrolled to from a reply link. */
|
|
@keyframes msg-highlight-fade {
|
|
from { background: rgba(203, 166, 247, 0.18); }
|
|
to { background: transparent; }
|
|
}
|
|
.msg-highlight { animation: msg-highlight-fade 1.5s ease-out forwards; }
|
|
.msg-ts { color: var(--muted); font-size: 0.85em; }
|
|
.msg-arrow { font-weight: bold; }
|
|
.msg-from { color: var(--amber); }
|
|
.msg-sep { color: var(--muted); }
|
|
.msg-to { color: var(--pink); }
|
|
.msg-body { color: var(--fg); white-space: pre-wrap; word-break: break-word; }
|
|
/* Compose box sits inside `.terminal-wrap`, below the `.live` log. The
|
|
dashed separator mirrors the agent terminal's prompt divider. */
|
|
.op-compose {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.6em;
|
|
padding: 0.55em 0.8em;
|
|
border-top: 1px dashed var(--purple-dim);
|
|
}
|
|
.op-compose-prompt {
|
|
color: var(--purple);
|
|
text-shadow: 0 0 4px currentColor;
|
|
font-weight: bold;
|
|
white-space: nowrap;
|
|
user-select: none;
|
|
padding-top: 0.15em;
|
|
}
|
|
.op-compose-input {
|
|
flex: 1;
|
|
background: transparent;
|
|
border: none;
|
|
outline: none;
|
|
color: var(--fg);
|
|
font: inherit;
|
|
font-size: 0.85em;
|
|
line-height: 1.5;
|
|
resize: none;
|
|
overflow: hidden;
|
|
min-height: 1.5em;
|
|
caret-color: var(--purple);
|
|
}
|
|
.op-compose-input::placeholder { color: var(--muted); }
|
|
.op-compose-suggest {
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 0.8em;
|
|
margin-bottom: 0.2em;
|
|
background: rgba(24, 24, 37, 0.95);
|
|
border: 1px solid var(--border);
|
|
font-size: 0.85em;
|
|
min-width: 12em;
|
|
max-height: 12em;
|
|
overflow-y: auto;
|
|
z-index: 10;
|
|
}
|
|
.op-compose-suggest .item {
|
|
padding: 0.2em 0.8em;
|
|
cursor: pointer;
|
|
color: var(--fg);
|
|
}
|
|
.op-compose-suggest .item.active,
|
|
.op-compose-suggest .item:hover {
|
|
background: rgba(203, 166, 247, 0.18);
|
|
color: var(--purple);
|
|
}
|
|
footer {
|
|
margin-top: 4em;
|
|
text-align: center;
|
|
color: var(--muted);
|
|
font-size: 0.9em;
|
|
}
|
|
footer a { color: var(--purple); }
|
|
/* Slug banner now lives at the page footer (#389 follow-up) — give
|
|
it a slim top margin so it doesn't crash into the prior content,
|
|
and bottom margin separating from the divider/link line. */
|
|
footer .banner-thin {
|
|
margin-bottom: 0.8em;
|
|
}
|
|
|
|
/* ─── side panel ─────────────────────────────────────────────────
|
|
Long content (file previews, diffs, journald, applied config)
|
|
opens in a drawer that swipes in from the right instead of
|
|
expanding inline. `.panel-trigger` is the inline affordance that
|
|
opens it. */
|
|
.panel-trigger {
|
|
background: none;
|
|
border: none;
|
|
color: var(--muted);
|
|
font-family: inherit;
|
|
font-size: 0.85em;
|
|
letter-spacing: 0.05em;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
margin-top: 0.5em;
|
|
display: inline-block;
|
|
text-align: left;
|
|
text-decoration: none;
|
|
}
|
|
.panel-trigger:hover { color: var(--cyan); }
|
|
|
|
.side-panel {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 50;
|
|
/* Closed: the wrapper ignores pointer events so the dashboard
|
|
underneath stays interactive; `.open` flips it back on. */
|
|
pointer-events: none;
|
|
}
|
|
.side-panel-backdrop {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.55);
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
.side-panel-drawer {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
/* #451: width is a CSS variable so the drag handle (added by
|
|
Panel.bind) can update it live, and so localStorage-persisted
|
|
widths apply on first paint. Default min(760px, 94vw) preserves
|
|
the pre-#451 behaviour for operators who never drag. The body
|
|
adds `data-spw="<px>"` once persisted; CSS doesn't read it
|
|
directly — JS sets `--side-panel-w` via inline style on the
|
|
drawer. */
|
|
width: var(--side-panel-w, min(760px, 94vw));
|
|
/* Clamp so a stored width can never push the drawer off-screen
|
|
or shrink it past readability. min content width matches the
|
|
panel-head's natural width with the close button visible. */
|
|
min-width: 320px;
|
|
max-width: 96vw;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg-elev);
|
|
border-left: 2px solid var(--purple);
|
|
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.45);
|
|
transform: translateX(100%);
|
|
transition: transform 0.25s ease;
|
|
}
|
|
.side-panel.open { pointer-events: auto; }
|
|
.side-panel.open .side-panel-backdrop { opacity: 1; }
|
|
.side-panel.open .side-panel-drawer { transform: translateX(0); }
|
|
/* #451: drag-to-resize handle on the drawer's left edge. The handle
|
|
itself is invisible until hover/drag so it doesn't compete with the
|
|
2px mauve `border-left` for the visual boundary. Pointer-cursor
|
|
tells the operator the edge is grabbable; the brighter glow during
|
|
drag (`body.side-panel-resizing`) is the affordance the eye
|
|
tracks. */
|
|
.side-panel-resize {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: 6px;
|
|
margin-left: -3px;
|
|
cursor: ew-resize;
|
|
z-index: 1;
|
|
background: transparent;
|
|
transition: background 120ms ease;
|
|
}
|
|
.side-panel-resize:hover,
|
|
body.side-panel-resizing .side-panel-resize {
|
|
background: rgba(203, 166, 247, 0.55);
|
|
}
|
|
/* While dragging: kill text selection + force the resize cursor
|
|
page-wide so the cursor doesn't flip back to default the moment it
|
|
leaves the 6px handle band. */
|
|
body.side-panel-resizing {
|
|
cursor: ew-resize !important;
|
|
user-select: none;
|
|
}
|
|
body.side-panel-resizing * { cursor: ew-resize !important; }
|
|
.side-panel-head {
|
|
flex: 0 0 auto;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1em;
|
|
padding: 0.7em 1em;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.side-panel-title {
|
|
color: var(--purple);
|
|
font-weight: bold;
|
|
letter-spacing: 0.05em;
|
|
word-break: break-all;
|
|
}
|
|
.side-panel-close {
|
|
flex: 0 0 auto;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
border: 1px solid var(--border);
|
|
font-family: inherit;
|
|
font-size: 1em;
|
|
line-height: 1;
|
|
padding: 0.25em 0.55em;
|
|
cursor: pointer;
|
|
}
|
|
.side-panel-close:hover { border-color: var(--red); color: var(--red); }
|
|
.side-panel-body {
|
|
flex: 1 1 auto;
|
|
overflow: auto;
|
|
padding: 1em;
|
|
}
|
|
/* Markdown file previews rendered by `marked`. TERMINAL_CSS scopes
|
|
its own `.md` rules to `.live .row`, so the panel needs its own. */
|
|
.side-panel-body .md { color: var(--fg); line-height: 1.5; }
|
|
.side-panel-body .md > :first-child { margin-top: 0; }
|
|
.side-panel-body .md > :last-child { margin-bottom: 0; }
|
|
.side-panel-body .md p { margin: 0.5em 0; }
|
|
.side-panel-body .md h1,
|
|
.side-panel-body .md h2,
|
|
.side-panel-body .md h3,
|
|
.side-panel-body .md h4 { color: var(--purple); margin: 0.9em 0 0.4em; }
|
|
.side-panel-body .md code {
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 3px;
|
|
padding: 0.05em 0.3em;
|
|
font-size: 0.9em;
|
|
}
|
|
.side-panel-body .md pre {
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 3px;
|
|
padding: 0.6em 0.8em;
|
|
overflow-x: auto;
|
|
}
|
|
.side-panel-body .md pre code { background: none; border: none; padding: 0; }
|
|
.side-panel-body .md a { color: var(--cyan); }
|
|
.side-panel-body .md ul,
|
|
.side-panel-body .md ol { margin: 0.4em 0; padding-left: 1.5em; }
|
|
.side-panel-body .md blockquote {
|
|
margin: 0.5em 0;
|
|
padding-left: 0.8em;
|
|
border-left: 2px solid var(--border);
|
|
color: var(--muted);
|
|
}
|
|
.side-panel-body .md table { border-collapse: collapse; margin: 0.5em 0; }
|
|
.side-panel-body .md th,
|
|
.side-panel-body .md td {
|
|
border: 1px solid var(--border);
|
|
padding: 0.2em 0.5em;
|
|
}
|
|
|
|
/* ─── /flow.html — full-page chat (#369) ───────────────────────────
|
|
The all-agents chat surface lives on its own page so it can claim
|
|
full-viewport vibec0re styling (operator @ #369#issuecomment-3437).
|
|
Same shape as the per-agent live page (#362): frosted-glass header
|
|
at top, frosted composer docked at bottom, terminal scrolls
|
|
behind both. Operator inbox lives behind a header pill that opens
|
|
the side-panel flyout — preserves the "inbox + chat in one view"
|
|
ergonomics without stealing terminal real estate. */
|
|
|
|
:root {
|
|
/* Approximate height of the flow chrome (tabbar + dashboard-chrome
|
|
padding). The banner-thin slug used to live in the chrome and
|
|
padded this up to 4.7em; with the slug moved out (#389 follow-up:
|
|
slug lives at the page footer on /, simply omitted on /flow.html
|
|
since there's no normal-flow footer position in the full-viewport
|
|
terminal), the chrome is just tabs now and shrinks accordingly. */
|
|
--flow-header-h: 3.6em;
|
|
--flow-composer-h: 3.6em;
|
|
--flow-frost-bg: rgba(30, 30, 46, 0.74);
|
|
--flow-frost-blur: blur(12px) saturate(140%);
|
|
}
|
|
|
|
body.flow-shell {
|
|
/* Override the dashboard's centred-max-width layout — flow owns
|
|
the whole viewport. */
|
|
max-width: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
background:
|
|
radial-gradient(ellipse 80% 60% at 50% 0%,
|
|
rgba(203, 166, 247, 0.06) 0%,
|
|
transparent 60%),
|
|
var(--bg);
|
|
}
|
|
|
|
/* Flow chrome (#383): reuses the dashboard's `.dashboard-chrome` +
|
|
tabbar so the operator can switch tabs from the flow page without
|
|
navigating back first. The chrome must be fixed-position (vs
|
|
sticky on the dashboard) since flow-shell has `overflow: hidden`
|
|
on body and the main area absolute-positions the terminal. */
|
|
body.flow-shell .dashboard-chrome.flow-chrome {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 30;
|
|
margin: 0;
|
|
padding: 0.4em 0 0;
|
|
background: var(--flow-frost-bg);
|
|
-webkit-backdrop-filter: var(--flow-frost-blur);
|
|
backdrop-filter: var(--flow-frost-blur);
|
|
border-bottom: 1px solid var(--purple-dim);
|
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
|
|
}
|
|
/* Active tab on the flow page is FL0W. Its `.tab-link` styling (the
|
|
tab strip's right-most entry on the dashboard, where it represents
|
|
the *link out* to /flow.html) needs to read as the active tab here.
|
|
`aria-current="page"` flags it semantically. */
|
|
body.flow-shell .tabbar .tab.active.tab-link {
|
|
color: var(--purple);
|
|
background: var(--bg);
|
|
border-color: var(--purple-dim);
|
|
box-shadow: 0 -2px 12px -4px rgba(203, 166, 247, 0.4);
|
|
}
|
|
/* Legacy `.flow-title` / `.flow-hint` / `.flow-back` rules were
|
|
removed in #383 — the flow page now uses the shared chrome with
|
|
the dashboard tab strip, no need for FL0W-specific title/hint
|
|
elements. The `.notif-row` styling lives under the shared
|
|
`.tabbar #notif-row` selector earlier in the file. */
|
|
|
|
/* Inbox pill — operator inbox flyout trigger. Sits right under the
|
|
header so it stays in the operator's gaze without crowding the
|
|
chat. Same shape as the agent page's pills (#362). */
|
|
.flow-pill {
|
|
position: fixed;
|
|
top: calc(var(--flow-header-h) + 0.8em);
|
|
right: 1em;
|
|
z-index: 25;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--purple-dim);
|
|
color: var(--fg);
|
|
font-family: inherit;
|
|
font-size: 0.85em;
|
|
letter-spacing: 0.04em;
|
|
border-radius: 999px;
|
|
padding: 0.3em 0.8em;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5em;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
|
transition: border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
|
|
}
|
|
.flow-pill:hover {
|
|
border-color: var(--purple);
|
|
color: var(--purple);
|
|
box-shadow: 0 0 12px -2px var(--purple);
|
|
}
|
|
.flow-pill-icon { font-size: 1.05em; line-height: 1; }
|
|
.flow-pill-label { color: var(--muted); }
|
|
.flow-pill-count {
|
|
background: rgba(250, 179, 135, 0.18);
|
|
color: var(--amber);
|
|
border-radius: 999px;
|
|
padding: 0 0.5em;
|
|
min-width: 1.6em;
|
|
text-align: center;
|
|
font-weight: bold;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.flow-main {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
overflow: hidden;
|
|
}
|
|
.flow-main .terminal-wrap {
|
|
position: absolute;
|
|
inset: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
box-shadow: none;
|
|
border-radius: 0;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
.flow-main .live {
|
|
position: absolute;
|
|
inset: 0;
|
|
height: auto;
|
|
max-height: none;
|
|
padding-top: calc(var(--flow-header-h) + 0.8em);
|
|
padding-bottom: calc(var(--flow-composer-h) + 0.8em);
|
|
scroll-padding-top: calc(var(--flow-header-h) + 0.8em);
|
|
scroll-padding-bottom: calc(var(--flow-composer-h) + 0.8em);
|
|
overflow: auto;
|
|
}
|
|
/* Tail pill (↓ N new): bottom offset clears the floating composer.
|
|
z-index escapes the stacking context the .terminal-wrap's
|
|
backdrop-filter creates (issue #375) — app.js anchors the pill
|
|
on .flow-main now (not .terminal-wrap), so this z-index reaches
|
|
the root stacking context and properly floats above the
|
|
composer at z-index 30. */
|
|
.flow-main .tail-pill {
|
|
bottom: calc(var(--flow-composer-h) + 0.6em);
|
|
z-index: 35;
|
|
}
|
|
|
|
.flow-composer {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 30;
|
|
min-height: var(--flow-composer-h);
|
|
background: var(--flow-frost-bg);
|
|
-webkit-backdrop-filter: var(--flow-frost-blur);
|
|
backdrop-filter: var(--flow-frost-blur);
|
|
border-top: 1px solid var(--purple-dim);
|
|
box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.35);
|
|
padding: 0.4em 1em;
|
|
}
|
|
|
|
/* Hidden inbox section on the flow page — the renderInbox path
|
|
wants `#inbox-section` in the DOM (legacy contract), but we
|
|
surface the messages via the pill/flyout instead. */
|
|
.flow-inbox-headless { display: none !important; }
|
|
|
|
/* Selection bar (#443). Sticky-bottom strip that surfaces bulk
|
|
actions when ≥1 agent is selected (click the icon). Visually
|
|
echoes the flow composer's frosted-mauve treatment so the chrome
|
|
reads as part of the same vibecore family. Hidden when empty —
|
|
leaves the page footer's normal-flow position untouched. */
|
|
.selection-bar {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 40;
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 0.6em;
|
|
padding: 0.55em 1em;
|
|
background: var(--flow-frost-bg);
|
|
-webkit-backdrop-filter: var(--flow-frost-blur);
|
|
backdrop-filter: var(--flow-frost-blur);
|
|
border-top: 1px solid var(--purple);
|
|
box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.4);
|
|
}
|
|
.selection-bar[hidden] { display: none; }
|
|
.selection-count {
|
|
color: var(--purple);
|
|
font-weight: bold;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
.selection-names {
|
|
color: var(--muted);
|
|
font-size: 0.85em;
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.selection-actions {
|
|
display: inline-flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.4em;
|
|
align-items: center;
|
|
}
|
|
.selection-actions .btn {
|
|
font-size: 0.75em;
|
|
padding: 0.2em 0.7em;
|
|
}
|
|
.selection-clear {
|
|
color: var(--muted);
|
|
border-color: var(--purple-dim);
|
|
font-size: 0.75em;
|
|
padding: 0.2em 0.7em;
|
|
}
|
|
/* Pad the dashboard body so the sticky bar doesn't cover the
|
|
bottom of the agent list. ~3.4em covers the bar's vertical
|
|
footprint with breathing room; only applies when the bar is
|
|
visible (`body.has-selection`, toggled by app.js when the
|
|
selection set is non-empty). */
|
|
body.dashboard-shell.has-selection { padding-bottom: 4.5em; }
|