hyperhive/frontend/packages/shared/src/terminal.css
iris 8bebd78895 frontend: add npm workspace scaffold under frontend/
Phase 1 of the backend/frontend code split (#273). Additive — no
existing code is touched; the legacy hive-c0re/assets, hive-ag3nt/
assets and hive-fr0nt/assets trees stay in place until the Rust
cutover later in this branch.

Layout:
  frontend/package.json                       npm workspaces root
  frontend/packages/shared/                   @hive/shared
    src/{base,terminal}.css + terminal.js     (ES module)
    src/index.js                              re-exports terminal.js
  frontend/packages/dashboard/                @hive/dashboard
    src/{index.html, app.js, dashboard.css}   ported from hive-c0re/assets
    build.mjs                                 esbuild config → dist/
  frontend/packages/agent/                    @hive/agent
    src/{index,stats,screen}.html + agent.css
        + {app,stats}.js                      ported from hive-ag3nt/assets
    build.mjs                                 esbuild config → dist/

Changes vs the existing assets:
- terminal.js is an ES module exporting { create, linkify } instead
  of assigning to window.HiveTerminal. The dashboard / agent app.js
  files re-expose them on window so the IIFE bodies keep working
  unchanged through Phase 1; the global aliases can be dropped in a
  follow-up once the IIFEs are unwrapped.
- marked is imported from the marked@4.3.0 npm package (replacing
  the vendored hive-fr0nt/assets/marked.umd.js bundle).
- chart.js is imported from chart.js@4.4.4 (replacing the jsDelivr
  CDN script tag on the per-agent stats page — page now works
  offline / on operator machines without internet egress).
- dashboard.css and agent.css both gain @import lines at the top
  that pull base.css + terminal.css from @hive/shared, replacing
  the runtime string concatenation in serve_css.
- index.html / stats.html collapse from three / two script tags to
  one type="module" tag pointing at the bundled output.

package-lock.json is intentionally omitted from this commit — npm
isn't available in the iris container yet (approval pending) and the
lockfile will land in the next commit on this branch once the
toolchain is in place. The PR will not be opened until it's there.

Phase 2 (nix derivations), Phase 3 (container plumbing + the
hyperhive.frontend.extraFiles option for per-agent layering), and
Phase 4 (Rust cutover to tower_http::ServeDir, delete hive-fr0nt
+ legacy assets dirs) land as follow-up commits on this same
branch.

Refs #273.
2026-05-23 14:51:01 +02:00

228 lines
8.3 KiB
CSS

/* Shared terminal pane: a scroll-sticky log of rows + a "↓ N new" pill.
Pages wrap their stream container in `.terminal-wrap` and give the log
itself the `.live` class; renderer JS appends `.row` (flat line) or
`details.row` (collapsible body) elements. Row-kind classes
(`.turn-start`, `.tool-use`, `.thinking`, etc.) carry the per-event
colour; pages that don't emit a given kind simply never produce that
class — the unused rule sits in the bundle harmlessly.
`.terminal-wrap` provides the crust-on-black phosphor chrome that makes
the agent page feel like a terminal. Pages can opt in by wrapping a
block in this class; or skip it and the rows still render with their
class colours, just without the frame.
No `.term-input` here — composers are a separate concern (see
hive-fr0nt::COMPOSER_CSS / COMPOSER_JS once introduced). */
.terminal-wrap {
position: relative;
background: rgba(17, 17, 27, 0.78);
-webkit-backdrop-filter: blur(8px) saturate(120%);
backdrop-filter: blur(8px) saturate(120%);
border: 1px solid var(--purple-dim);
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.7);
border-radius: 4px;
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace;
font-size: 0.92em;
color: var(--fg);
margin-top: 0.6em;
}
.live {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--purple-dim);
padding: 0.4em 0.6em;
overflow-y: auto;
max-height: 32em;
font-family: inherit;
}
.live.terminal {
background: transparent;
border: 0;
box-shadow: none;
border-radius: 0;
padding: 0.8em 1em 0.4em;
overflow-y: auto;
height: min(72vh, 60em);
max-height: none;
font-family: inherit;
font-size: inherit;
color: inherit;
}
.live .row,
.live details.row {
animation: row-fade-in 220ms ease-out both;
}
.live .row.no-anim,
.live details.row.no-anim {
animation: none;
}
@keyframes row-fade-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Unified prefix column for every row kind. The glyph (`→ ← · ◆ ✓ ✗ ⌁ !`)
is the first character of the row's text content; `padding-left` reserves
the column and `text-indent: -1.4em` pulls the glyph back into it. Wrapped
continuation lines then start under the body, not under the glyph, so
wraps don't blur into the next row. `details.row` summaries reuse the
same metrics below. */
.live .row {
white-space: pre-wrap;
word-break: break-word;
padding: 0.05em 0;
line-height: 1.45;
border-left: 2px solid transparent;
padding-left: 1.9em;
text-indent: -1.4em;
margin: 0.1em 0;
}
.live .row + .row { border-top: 0; }
/* Row-kind colours. Pages register renderers that emit these classes;
any class no page emits is just dead CSS, which is fine. Turn-framing
classes carry their signal entirely on the coloured border-left rule —
no bold, no top/bottom margins, no background tint. The chrome was
overweight for what's just a "this is a boundary" marker. */
.live .turn-start { color: var(--amber); border-left-color: var(--amber); }
/* turn-body is a child block under turn-start carrying the wake-prompt
body; reset text-indent so wrapped content stays under its own column
instead of pulling back into the parent's prefix. */
.live .turn-body { color: var(--fg); text-indent: 0; margin-top: 0.15em; }
/* Any child block (markdown body, nested details) resets the parent
row's hanging indent so the content lays out from column 0 of the
body area. */
.live .row .md, .live .row > details { text-indent: 0; }
.live .turn-end-ok { color: var(--green); border-left-color: var(--green); }
.live .turn-end-fail { color: var(--red); border-left-color: var(--red); }
.live .text { color: var(--fg); }
.live .thinking { color: var(--muted); font-style: italic; }
.live .tool-use { color: var(--cyan); }
.live .tool-result { color: var(--muted); }
.live .result { color: var(--green); }
.live .note { color: var(--muted); }
/* Distinguish stderr lines (orange) and operator-initiated notes
(mauve, lightly emphasised) from ambient harness chatter so the
eye picks out anomalies + operator actions in the scrollback. */
.live .note.stderr { color: var(--amber); }
.live .note.op { color: var(--purple); font-style: italic; }
/* The .sys catch-all fires when renderStream landed an event shape it
couldn't classify. Make it visually loud so silently-dropped event
types surface for follow-up. */
.live .sys { color: var(--amber); }
.live .unread-badge {
color: var(--amber);
font-weight: normal;
margin-left: 0.6em;
font-size: 0.85em;
text-shadow: 0 0 6px rgba(250, 179, 135, 0.55);
animation: badge-pulse 1.4s ease-in-out infinite;
}
@keyframes badge-pulse {
0%, 100% { opacity: 1; text-shadow: 0 0 6px rgba(250, 179, 135, 0.55); }
50% { opacity: 0.7; text-shadow: 0 0 14px rgba(250, 179, 135, 0.95); }
}
/* "↓ N new" pill: shown when new rows arrive while the operator is
scrolled up; click to jump to bottom. Positioned by the wrapper's
`position: relative` (terminal-wrap supplies it; pages that skip the
wrapper must add their own positioned ancestor). */
.tail-pill {
position: absolute;
right: 1em;
bottom: 4.2em;
background: var(--amber);
color: #11111b;
font-family: inherit;
font-size: 0.8em;
font-weight: bold;
letter-spacing: 0.08em;
border: 0;
border-radius: 999px;
padding: 0.35em 0.9em;
cursor: pointer;
box-shadow: 0 0 14px -2px rgba(250, 179, 135, 0.85);
opacity: 0;
transform: translateY(6px);
pointer-events: none;
transition: opacity 160ms ease, transform 160ms ease;
}
.tail-pill.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.tail-pill:hover { filter: brightness(1.1); }
/* Expandable rows reuse the flat-row prefix metrics (padding-left +
negative text-indent) so the disclosure glyph (`▸ / ▾`) lands in
exactly the same column as flat-row prefix glyphs (`→ ← · ◆ ✓ ✗`).
Summary text omits the per-row directional glyph (the row colour
already carries cyan = outbound tool, muted = inbound result) so
the prefix column doesn't have to fit two glyphs side-by-side. */
details.row {
white-space: normal;
}
details.row > summary {
cursor: pointer;
list-style: none;
white-space: pre-wrap;
word-break: break-word;
}
details.row > summary::before {
content: '▸ ';
color: inherit;
}
details.row[open] > summary::before { content: '▾ '; }
details.row > pre.diff-body,
details.row > pre.tool-body {
margin: 0.3em 0 0.4em 0;
padding: 0.4em 0.6em;
text-indent: 0;
background: rgba(255, 255, 255, 0.02);
border-left: 2px solid var(--purple-dim);
white-space: pre-wrap;
word-break: break-word;
max-height: 22em;
overflow-y: auto;
}
details.row > pre.tool-body { color: var(--fg); }
details.row > pre.diff-body .diff-add { color: var(--green); }
details.row > pre.diff-body .diff-del { color: var(--red); }
details.row > pre.diff-body .diff-ctx { color: var(--fg); }
/* Markdown body inside a row (assistant text, send/recv/ask/answer
message bodies). Inline elements get muted accents; block elements
reset the parent row's hanging indent so content lays out cleanly. */
.live .row .md p { margin: 0.2em 0; }
.live .row .md p:first-child { margin-top: 0; }
.live .row .md p:last-child { margin-bottom: 0; }
.live .row .md code {
background: rgba(255, 255, 255, 0.06);
padding: 0.05em 0.3em;
border-radius: 3px;
font-size: 0.95em;
}
.live .row .md pre {
margin: 0.3em 0;
padding: 0.4em 0.6em;
background: rgba(255, 255, 255, 0.04);
border-left: 2px solid var(--purple-dim);
text-indent: 0;
white-space: pre-wrap;
word-break: break-word;
}
.live .row .md pre code {
background: transparent;
padding: 0;
border-radius: 0;
}
.live .row .md a { color: var(--cyan); text-decoration: underline; }
/* Auto-linkified bare URLs in plain rows + tool-body blocks (issue #233). */
.live .row a { color: var(--cyan); text-decoration: underline; }
.live .row a:hover { color: var(--fg); }
.live .row .md strong { color: inherit; font-weight: bold; }
.live .row .md em { color: inherit; font-style: italic; }
.live .row .md ul, .live .row .md ol { margin: 0.2em 0 0.2em 1.4em; padding: 0; }
.live .row .md li { margin: 0.05em 0; }
.live .row .md blockquote {
margin: 0.2em 0;
padding-left: 0.6em;
border-left: 2px solid var(--purple-dim);
color: var(--muted);
}