agent: dynamic header height, badges+pills one row, composer alignment
mara, live: 'login panel still looks like it is behind', 'bottom input row text not aligned / inconsistent sizes', 'can we put the badges on the same row as the links button etc? then the header would be more compact.' - Real fix for the overlap this time, not another band-aid: the header's downstream offset (.agent-status-overlay, .agent-main's scroll padding) was reading a STATIC --agent-header-h guess (6em). Every prior fix on this PR removed one specific contributor to the header wrapping past that guess (meta-nav's inline list, then disconnected badge/pills clusters) but the guess itself was always the actual defect — any content that can wrap an extra line breaks it at whatever width triggers it, and no width is safe to promise. Header.tsx now measures its own rendered height via ResizeObserver and writes it to a new --agent-header-real-h var; downstream consumers prefer that over the static guess. Deliberately a SEPARATE var from --agent-header-h, not an overwrite: .agent-header's own min-height also reads that var, so overwriting it in place created an actual ResizeObserver feedback loop (confirmed live on the first pass of this fix, fixed by splitting the var). - Badges (StatusChips) + pills (inbox/todos/links/overflow) now share one row (.agent-state-row) instead of two visually disconnected clusters (old app.js markup's 3-column layout, which the new page had also carried over) — pills pushed to the row's right edge via a margin-left: auto rule scoped to that nesting, so app.js's own still-live separate-column usage of the same classes is untouched. - Composer text alignment: real cause was .prompt (1em) and .submit-hint (0.8em) each getting a different-sized default line-box under align-items: flex-start, so the same nominal padding-top landed at different absolute offsets. align-items: center sidesteps the whole line-box-size mismatch. This one's shared with app.js too (same markup/classes) — same bug there, now fixed for both. Verified at 480/500/768/1024/1400px — no overlap, no ResizeObserver loop, badges+pills share one row, composer aligned. tsc --noEmit clean, build clean, both pre-push lints clean.
This commit is contained in:
parent
3ce961e5bd
commit
f49e236c74
2 changed files with 99 additions and 31 deletions
|
|
@ -128,6 +128,19 @@ body.agent-shell {
|
|||
margin: 0;
|
||||
gap: 0.5em;
|
||||
}
|
||||
/* New Preact page only: `Header.tsx` nests `.agent-header-pills`
|
||||
INSIDE `.agent-state-row` (badges + pills sharing one row — mara,
|
||||
live: "can we put the badges on the same row as the links button
|
||||
etc? then the header would be more compact"), unlike the old
|
||||
app.js markup where `.agent-header-pills` is a separate header-
|
||||
level column (index.html line ~54). Scoped as a descendant
|
||||
selector so it only fires for that nesting — app.js's own
|
||||
still-live 3-column layout below is untouched. Pushes the pills
|
||||
cluster to the row's right edge; badges (`children`, rendered
|
||||
first) fill the space to its left. */
|
||||
.agent-state-row .agent-header-pills {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Right cluster — flyout pills stacked / inline with the overflow
|
||||
trigger. Vertically centred against the full-height icon, no
|
||||
|
|
@ -357,10 +370,16 @@ h2, h3 {
|
|||
|
||||
/* Login flow overlay: only rendered when status != online. Sits
|
||||
centred over the (likely-empty) terminal area; doesn't take chrome
|
||||
space in the normal online flow. */
|
||||
space in the normal online flow. `--agent-header-real-h` (set by
|
||||
the new Preact page's `Header.tsx` via ResizeObserver, falling back
|
||||
to the static `--agent-header-h` guess before the first
|
||||
measurement / on the old app.js page, which never sets it) is the
|
||||
header's ACTUAL rendered height, not an assumed one — see
|
||||
Header.tsx's file comment for why this matters (a static guess is
|
||||
wrong the moment the header wraps an extra line on some viewport). */
|
||||
.agent-status-overlay {
|
||||
position: absolute;
|
||||
top: calc(var(--agent-header-h) + 1.5em);
|
||||
top: calc(var(--agent-header-real-h, var(--agent-header-h)) + 1.5em);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-width: 44em;
|
||||
|
|
@ -716,9 +735,9 @@ pre.diff {
|
|||
and last rows reachable with extra padding inside the scroll
|
||||
area. scroll-padding-* keeps anchor-jumps (the `↓ N new` pill,
|
||||
focus restore) clear of the floats too. */
|
||||
padding-top: calc(var(--agent-header-h) + 0.8em);
|
||||
padding-top: calc(var(--agent-header-real-h, var(--agent-header-h)) + 0.8em);
|
||||
padding-bottom: calc(var(--agent-composer-h) + 0.8em);
|
||||
scroll-padding-top: calc(var(--agent-header-h) + 0.8em);
|
||||
scroll-padding-top: calc(var(--agent-header-real-h, var(--agent-header-h)) + 0.8em);
|
||||
scroll-padding-bottom: calc(var(--agent-composer-h) + 0.8em);
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
@ -739,7 +758,21 @@ pre.diff {
|
|||
rules below stay scoped to whichever ancestor owns it. */
|
||||
.term-input .sendform-term {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
/* mara, live: "bottom input row text not aligned / inconsistent
|
||||
sizes." Real cause: `.prompt` (1em, no line-height override) and
|
||||
`.submit-hint` (0.8em, no line-height override) each get a
|
||||
browser-default line-box at a different em-relative size, so
|
||||
`align-items: flex-start` (their old value) pinned each one's
|
||||
line-box top edge to the row's top — same nominal `padding-top`
|
||||
but a DIFFERENT absolute offset per element, since 0.25em means
|
||||
something different at 1em vs 0.8em font-size. `center` sidesteps
|
||||
the whole "line-box for text this size" mismatch by aligning each
|
||||
item's own box middle instead of a top edge that was never
|
||||
actually equal. Still correct once the textarea grows multi-line
|
||||
(a common composer-bar convention — Slack/Discord centre their
|
||||
send affordance against a grown box too, not pin it to the first
|
||||
line). */
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
/* The dashed in-frame separator is dropped — see the
|
||||
.agent-composer .term-input override above for the floating-bar
|
||||
|
|
@ -747,9 +780,6 @@ pre.diff {
|
|||
border-top: 1px dashed var(--purple-dim);
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
.term-input .prompt, .term-input .submit-hint {
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
.term-input .prompt {
|
||||
color: var(--green);
|
||||
text-shadow: 0 0 6px color-mix(in srgb, var(--green) 60%, transparent);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,28 @@
|
|||
// <Header> — the agent page's identity strip: icon, glyphic title, the
|
||||
// "swarm / hive" label row, and a slot for the status row
|
||||
// (`<StatusChips>` — ../status-chips/, kept separate so this component
|
||||
// has no opinion on what badges exist). Structurally the same
|
||||
// three-row shape as the old `#agent-header` markup in index.html
|
||||
// (icon · main · pills) — see `docs/web-ui.md::Per-agent page` — this
|
||||
// is a like-for-like layout port, the redesign work is in the children.
|
||||
// Reuses `agent.css`'s existing `.agent-header*`/`.agent-icon` rules
|
||||
// (loaded globally by index.html) rather than a component-scoped
|
||||
// stylesheet — no new visual language needed for the chrome itself.
|
||||
// has no opinion on what badges exist). Two columns (icon · main),
|
||||
// NOT the old `#agent-header` markup's three (icon · main · pills) —
|
||||
// see the `pills` prop doc below for why. Reuses `agent.css`'s
|
||||
// existing `.agent-header*`/`.agent-icon` rules (loaded globally by
|
||||
// index.html) rather than a component-scoped stylesheet — no new
|
||||
// visual language needed for the chrome itself.
|
||||
|
||||
// Measures its own rendered height via ResizeObserver, writes it to
|
||||
// `--agent-header-real-h` (agent.css's downstream consumers —
|
||||
// `.agent-status-overlay`, `.agent-main`'s scroll padding — prefer
|
||||
// this over the static `--agent-header-h` guess). A static guess was
|
||||
// the root cause of two "login card behind the header" reports on
|
||||
// this PR (mara, live): any header content that can wrap an extra
|
||||
// line breaks the guess at whatever width triggers it, and no width
|
||||
// can be promised safe. Deliberately a SEPARATE var, not an overwrite
|
||||
// of `--agent-header-h` in place: `.agent-header`'s own `min-height`
|
||||
// also reads that var — overwriting it in place would make the
|
||||
// header's own size react to its own just-measured height every
|
||||
// tick, a real ResizeObserver feedback loop (confirmed live on the
|
||||
// first pass of this fix). `--agent-header-h` stays the static floor;
|
||||
// `--agent-header-real-h` is output-only.
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
import type { ComponentChildren } from 'preact';
|
||||
|
||||
// Icon 404s when the agent has no `hyperhive.icon` override (no
|
||||
|
|
@ -26,35 +41,58 @@ function handleIconError(e: Event) {
|
|||
export interface HeaderProps {
|
||||
label: string;
|
||||
hiveLabel?: string | null;
|
||||
/** The status/badge row (`<StatusChips>`) — shares its row with
|
||||
* `pills` now (see below), not stacked in a row of its own. */
|
||||
children?: ComponentChildren;
|
||||
/** Right-cluster flyout triggers (inbox/todos/links pills, the
|
||||
* overflow menu button) — mirrors the old markup's
|
||||
* `.agent-header-pills` third column. Meta-nav (`<MetaNav>`) lives
|
||||
* here too, as a fixed-size trigger — NOT in the title row the old
|
||||
* markup put it in (`<nav id="meta-links">`, docs/web-ui/agent.md).
|
||||
* A variable-width inline link list there grows the title row's
|
||||
* actual height past the fixed `--agent-header-h` the rest of the
|
||||
* page's `position: fixed` header + `position: absolute` content
|
||||
* below it are offset against, so on a narrow viewport the header
|
||||
* silently overlaps the login-recovery card underneath it (mara,
|
||||
* live: "the login card looks like it is behind the header"). A
|
||||
* fixed-size trigger button can't do that regardless of how many
|
||||
* links the backend sends. */
|
||||
/** Flyout triggers (inbox/todos/links pills, the overflow menu
|
||||
* button). Old markup's `.agent-header-pills` was its own header-
|
||||
* level column, self-centered against the full header height — the
|
||||
* new page instead nests it inside the same row as `children`
|
||||
* (`.agent-state-row`), pushed to the row's right edge via
|
||||
* `margin-left: auto` (scoped to that nesting in agent.css so
|
||||
* app.js's still-live separate-column usage is untouched). mara,
|
||||
* live: "can we put the badges on the same row as the links button
|
||||
* etc? then the header would be more compact" — one shared row
|
||||
* instead of two disconnected clusters reads as one unit, and (as a
|
||||
* side effect) is one less row that could ever grow the header past
|
||||
* the fixed `--agent-header-h` the rest of the page is offset
|
||||
* against (see the overlap-bug history on this same PR). Meta-nav
|
||||
* (`<MetaNav>`) lives in here too, as a fixed-size trigger — NOT in
|
||||
* the title row the old markup put it in (`<nav id="meta-links">`,
|
||||
* docs/web-ui/agent.md) — same overlap-bug reasoning. */
|
||||
pills?: ComponentChildren;
|
||||
}
|
||||
|
||||
export function Header({ label, hiveLabel, children, pills }: HeaderProps) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el || typeof ResizeObserver === 'undefined') return;
|
||||
// getBoundingClientRect() (not ResizeObserver's own contentRect) —
|
||||
// we want the full visual box padding+border included, the thing
|
||||
// content below the fixed header actually needs to clear, not the
|
||||
// content-box-only measurement ResizeObserver's entry defaults to.
|
||||
const ro = new ResizeObserver(() => {
|
||||
document.documentElement.style.setProperty('--agent-header-real-h', `${el.getBoundingClientRect().height}px`);
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header class="agent-header">
|
||||
<header class="agent-header" ref={ref}>
|
||||
<img class="agent-icon" src="icon" alt="" onError={handleIconError} />
|
||||
<div class="agent-header-main">
|
||||
<div class="agent-header-row agent-header-title-row">
|
||||
<h2 class="agent-header-title">◆ {label} ◆</h2>
|
||||
</div>
|
||||
{hiveLabel ? <div class="agent-header-row agent-hive-row">{hiveLabel}</div> : null}
|
||||
<div class="agent-header-row">{children}</div>
|
||||
<div class="agent-header-row agent-state-row">
|
||||
{children}
|
||||
{pills ? <div class="agent-header-pills">{pills}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
{pills ? <div class="agent-header-pills">{pills}</div> : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue