Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: shared Hint, ActionRow, Toolbar, EmptyState, ExternalLink and ExpandableCard replace per-page copies

Hint, ActionRow, Toolbar, EmptyState and ExternalLink replace the muted
text, button row, refresh bar, empty line and _blank anchor that each page
restyled under its own class. ExpandableCard (with a loading twin) holds the
builds and recent-runs cards, which had identical head and body CSS. The
create form's raw checkbox becomes CheckboxField. The components page shows
each new piece.
This commit is contained in:
iris 2026-11-06 17:54:24 +01:00 • committed by mara
commit 8a2e5203ad
68 changed files with 689 additions and 427 deletions

View file

@ -8,6 +8,5 @@
margin-top: 1em;
}
.claude-login-ok {
margin-top: 1em;
color: var(--green);
margin-block: 1em;
}

View file

@ -11,6 +11,8 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Button } from "../../ui/button/Button.js";
import { Dialog } from "../../ui/dialog/Dialog.js";
import { ExternalLink } from "../../ui/external-link/ExternalLink.js";
import { Hint } from "../../ui/hint/Hint.js";
import { Panel } from "../../ui/panel/Panel.js";
import { TextField } from "../../ui/text-field/TextField.js";
import {
@ -98,9 +100,7 @@ export function ClaudeLoginDialog({
<>
<p>
Open{" "}
<a href={step.url} target="_blank" rel="noopener noreferrer">
the claude login page
</a>
<ExternalLink href={step.url}>the claude login page</ExternalLink>
, sign in with this agent's account, and paste the code it shows.
</p>
<form class="claude-login-form" onSubmit={submit}>
@ -122,10 +122,10 @@ export function ClaudeLoginDialog({
</>
)}
{step.step === "stored" && (
<p class="claude-login-ok">
<Hint tone="ok" class="claude-login-ok">
Stored. <strong>{agent}</strong> picks the login up within a minute
and resumes.
</p>
</Hint>
)}
{step.step === "failed" && (
<ApiErrorPanel

View file

@ -17,11 +17,6 @@
flex-wrap: wrap;
gap: 0.4em 1em;
}
.ui-agent-detail-actions {
display: flex;
align-items: center;
gap: 0.75em;
}
/* The folded sections of an opened card sit one under another, each with the
shared disclosure look; the fields keep the grid above. */

View file

@ -11,7 +11,10 @@
// change or a link made here shows wherever else the same row is drawn.
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { Badge } from "@hive/shared/badge.js";
import { ActionRow } from "../../ui/action-row/ActionRow.js";
import { Button } from "../../ui/button/Button.js";
import { ExternalLink } from "../../ui/external-link/ExternalLink.js";
import { Hint } from "../../ui/hint/Hint.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import type { AgentActions } from "../roster/AgentActions.js";
import { AgentInference } from "./AgentInference.js";
@ -118,12 +121,10 @@ function OverviewRows({
<>
<dt>forge</dt>
<dd class="ui-agent-detail-links">
<a href={forgeLinks.profile} target="_blank" rel="noreferrer">
profile
</a>
<a href={forgeLinks.configRepo} target="_blank" rel="noreferrer">
<ExternalLink href={forgeLinks.profile}>profile</ExternalLink>
<ExternalLink href={forgeLinks.configRepo}>
config repo
</a>
</ExternalLink>
</dd>
</>
) : null}
@ -134,13 +135,9 @@ function OverviewRows({
tone="warning"
value={
row.config_pr.html_url ? (
<a
href={row.config_pr.html_url}
target="_blank"
rel="noreferrer"
>
<ExternalLink href={row.config_pr.html_url}>
#{row.config_pr.pr_number}
</a>
</ExternalLink>
) : (
`#${row.config_pr.pr_number}`
)
@ -211,7 +208,7 @@ function GrantRows({ agent }: { agent: string }) {
<dt>permissions</dt>
<dd>
{g.grant === null ? (
<span class="ui-agent-limits-muted">—</span>
<Hint as="span">—</Hint>
) : (
<>
{g.readFailure}
@ -257,13 +254,13 @@ function LinkAccountAction({
actions: AgentActions;
}) {
return (
<div class="ui-agent-detail-actions">
<ActionRow class="ui-agent-detail-actions">
<LinkAccountMenu
agent={row.name}
hasHive={row.hive !== null}
onSelect={(kind) => actions.openLink(row, kind)}
/>
</div>
</ActionRow>
);
}

View file

@ -17,8 +17,7 @@
.ui-agent-inference-source {
margin-left: -0.25em;
}
.ui-agent-inference-source,
.ui-agent-inference-muted {
.ui-agent-inference-source {
color: var(--muted);
}
.ui-agent-inference-form {
@ -28,12 +27,3 @@
gap: 0.75em;
margin-top: 1em;
}
.ui-agent-inference-problem {
margin: 0;
color: var(--red);
}
.ui-agent-inference-actions {
display: flex;
align-items: center;
gap: 0.75em;
}

View file

@ -15,7 +15,9 @@ import { useEffect, useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.js";
import { ActionRow } from "../../ui/action-row/ActionRow.js";
import { Dialog } from "../../ui/dialog/Dialog.js";
import { Hint } from "../../ui/hint/Hint.js";
import { Panel } from "../../ui/panel/Panel.js";
import { SelectField } from "../../ui/select-field/SelectField.js";
import { TextField } from "../../ui/text-field/TextField.js";
@ -171,7 +173,7 @@ export function InferenceFields({
pattern="[0-9]*"
onInput={(maxSubagents) => onChange({ ...draft, maxSubagents })}
/>
{problem ? <p class="ui-agent-inference-problem">{problem}</p> : null}
{problem ? <Hint tone="error">{problem}</Hint> : null}
</>
);
}
@ -215,7 +217,7 @@ export function AgentInference({ agent }: { agent: string }) {
/>
) : null;
if (view === null) {
return errorPanel ?? <span class="ui-agent-inference-muted">…</span>;
return errorPanel ?? <Hint as="span">…</Hint>;
}
const { configured, effective } = view;
@ -368,7 +370,7 @@ function InferenceForm({
{problem ? (
<ApiErrorPanel context="saving failed" problem={problem} />
) : null}
<div class="ui-agent-inference-actions">
<ActionRow>
<Button
variant="primary"
type="submit"
@ -382,7 +384,7 @@ function InferenceForm({
>
clear the swarm's setting
</Button>
</div>
</ActionRow>
</form>
</Panel>
);

View file

@ -38,9 +38,3 @@
border: 1px solid var(--red);
border-radius: 4px;
}
.ui-agent-instructions-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.75em;
}

View file

@ -16,6 +16,7 @@ import { useEffect, useRef, useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.js";
import { ActionRow } from "../../ui/action-row/ActionRow.js";
import { Button } from "../../ui/button/Button.js";
import { Dialog } from "../../ui/dialog/Dialog.js";
import { Panel } from "../../ui/panel/Panel.js";
@ -235,9 +236,9 @@ function InstructionsForm({
context="loading AGENTS.md failed"
problem={load.problem}
/>
<div class="ui-agent-instructions-actions">
<ActionRow class="ui-agent-instructions-actions">
<Button onClick={reload}>try again</Button>
</div>
</ActionRow>
</>
) : null}
{load.status === "ready" ? (
@ -263,18 +264,18 @@ function InstructionsForm({
<pre class="ui-agent-instructions-theirs" tabIndex={0}>
{theirs.content === "" ? "(empty)" : theirs.content}
</pre>
<div class="ui-agent-instructions-actions">
<ActionRow class="ui-agent-instructions-actions">
<Button onClick={keepMine}>
keep my text, replace {agent}'s changes
</Button>
<Button onClick={takeTheirs}>use {agent}'s version</Button>
</div>
</ActionRow>
</section>
) : null}
{problem ? (
<ApiErrorPanel context="saving failed" problem={problem} />
) : null}
<div class="ui-agent-instructions-actions">
<ActionRow class="ui-agent-instructions-actions">
<Button
variant="primary"
type="submit"
@ -305,7 +306,7 @@ function InstructionsForm({
>
reload from {agent}
</Button>
</div>
</ActionRow>
</form>
) : null}
</div>

View file

@ -23,10 +23,4 @@
}
.ui-agent-limits-hint {
margin: 0 0 0.5em;
color: var(--muted);
}
.ui-agent-limits-actions {
display: flex;
align-items: center;
gap: 0.75em;
}

View file

@ -16,8 +16,10 @@ import { useEffect, useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.js";
import { ActionRow } from "../../ui/action-row/ActionRow.js";
import { Button } from "../../ui/button/Button.js";
import { Dialog } from "../../ui/dialog/Dialog.js";
import { Hint } from "../../ui/hint/Hint.js";
import { Panel } from "../../ui/panel/Panel.js";
import { TextField } from "../../ui/text-field/TextField.js";
import { BadgeSkeleton } from "../../ui/skeleton/BadgeSkeleton.js";
@ -71,9 +73,9 @@ export function LimitsFields({
placeholder="e.g. 400%"
onInput={onCpu}
/>
<p class="ui-agent-limits-hint">
<Hint class="ui-agent-limits-hint">
A percentage of one core: 100% is one core, 400% is four.
</p>
</Hint>
<TextField
id={`${idPrefix}-memory-max`}
label="memory max"
@ -81,10 +83,10 @@ export function LimitsFields({
placeholder="e.g. 8G"
onInput={onMemory}
/>
<p class="ui-agent-limits-hint">
<Hint class="ui-agent-limits-hint">
A size such as 512M or 8G, a percentage of the machine's memory such as
50%, or infinity.
</p>
</Hint>
</>
);
}
@ -239,7 +241,7 @@ function LimitsForm({
{problem ? (
<ApiErrorPanel context="the caps were not saved" problem={problem} />
) : null}
<div class="ui-agent-limits-actions">
<ActionRow>
<Button variant="primary" type="submit" disabled={busy || empty}>
{busy ? "saving…" : "save"}
</Button>
@ -249,7 +251,7 @@ function LimitsForm({
>
clear the swarm's caps
</Button>
</div>
</ActionRow>
</form>
</Panel>
);

View file

@ -22,6 +22,7 @@ import { Badge } from "@hive/shared/badge.js";
import { BadgePopover } from "../../ui/badge-popover/BadgePopover.js";
import { Button } from "../../ui/button/Button.js";
import { BadgeSkeleton } from "../../ui/skeleton/BadgeSkeleton.js";
import { Hint } from "../../ui/hint/Hint.js";
import { MultiselectFilterOptions } from "../../ui/multiselect-filter/MultiselectFilter.js";
import {
requestPermissions,
@ -225,9 +226,9 @@ export function AgentPermissions({
/>
</div>
) : (
<span class="ui-agent-limits-muted" role="status">
<Hint as="span" role="status">
{catalogueError ? "could not load the permissions" : "loading…"}
</span>
</Hint>
)}
{saveError ? (
<ApiErrorPanel
@ -287,7 +288,7 @@ export function AgentInternet({ agent, g }: { agent: string; g: GrantState }) {
})
}
/>{" "}
<span class="ui-agent-limits-muted">{meaning}</span>
<Hint as="span">{meaning}</Hint>
</li>
))}
</ul>

View file

@ -24,6 +24,7 @@ import { useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.js";
import { ActionRow } from "../../ui/action-row/ActionRow.js";
import { BadgePopover } from "../../ui/badge-popover/BadgePopover.js";
import { Button } from "../../ui/button/Button.js";
import { CheckboxField } from "../../ui/checkbox-field/CheckboxField.js";
@ -232,7 +233,7 @@ export function AddForm({
{problem ? (
<ApiErrorPanel context="the domain was not added" problem={problem} />
) : null}
<div class="ui-agent-limits-actions">
<ActionRow>
<Button
variant="primary"
type="submit"
@ -240,7 +241,7 @@ export function AddForm({
>
{busy ? "adding…" : "add"}
</Button>
</div>
</ActionRow>
</form>
</Panel>
);

View file

@ -12,7 +12,6 @@
gap: 0.5em;
flex-wrap: wrap;
}
.ui-linked-accounts-host,
.ui-linked-accounts-muted {
.ui-linked-accounts-host {
color: var(--muted);
}

View file

@ -15,6 +15,7 @@ import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.js";
import { CheckboxField } from "../../ui/checkbox-field/CheckboxField.js";
import { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js";
import { Hint } from "../../ui/hint/Hint.js";
import { accountKindLabel } from "./account-kind.js";
import { deleteUrl, listUrl } from "./account-api.js";
import "./LinkedAccounts.css";
@ -145,13 +146,13 @@ export function LinkedAccounts({
<ApiErrorPanel context="listing linked accounts failed" problem={error} />
) : null;
if (accounts === null) {
return errorPanel ?? <span class="ui-linked-accounts-muted">…</span>;
return errorPanel ?? <Hint as="span">…</Hint>;
}
if (accounts.length === 0) {
return (
<>
{errorPanel}
<span class="ui-linked-accounts-muted">none linked</span>
<Hint as="span">none linked</Hint>
</>
);
}

View file

@ -7,6 +7,7 @@ import { useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
import { Button } from "../ui/button/Button.js";
import { ExternalLink } from "../ui/external-link/ExternalLink.js";
import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
import {
@ -16,6 +17,7 @@ import {
import { fetchJson, usePolled } from "../ui/polled/usePolled.js";
import { Table, type TableColumn } from "../ui/table/Table.js";
import { TableSkeleton } from "../ui/table/TableSkeleton.js";
import { Toolbar } from "../ui/toolbar/Toolbar.js";
import { activeSinceMs, alertKey, listedAlerts, type Alert } from "./alerts.js";
const STATE_TONE: Record<string, BadgeTone> = {
@ -90,10 +92,10 @@ function AlertsBody() {
return (
<>
<div class="ui-alerts-bar">
<a href={GRAFANA_ALERTS_URL} target="_blank" rel="noopener noreferrer">
<Toolbar>
<ExternalLink href={GRAFANA_ALERTS_URL}>
Grafana's alert list
</a>
</ExternalLink>
{showAll || hidden > 0 ? (
<Button onClick={() => setShowAll(!showAll)}>
{showAll ? "only firing" : `show ${hidden} more`}
@ -105,7 +107,7 @@ function AlertsBody() {
onChange={setIntervalMs}
refreshing={refreshing}
/>
</div>
</Toolbar>
{error ? (
<ApiErrorPanel
context="failed to load Grafana's alerts"

View file

@ -34,6 +34,15 @@ import {
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
import { Button, type ButtonVariant } from "../ui/button/Button.js";
import { Dialog } from "../ui/dialog/Dialog.js";
import { ActionRow } from "../ui/action-row/ActionRow.js";
import { EmptyState } from "../ui/empty-state/EmptyState.js";
import {
ExpandableCard,
ExpandableCardSkeleton,
} from "../ui/expandable-card/ExpandableCard.js";
import { ExternalLink } from "../ui/external-link/ExternalLink.js";
import { Hint, type HintTone } from "../ui/hint/Hint.js";
import { Toolbar } from "../ui/toolbar/Toolbar.js";
import { ConfirmDialog } from "../ui/confirm-dialog/ConfirmDialog.js";
import { Popover } from "../ui/popover/Popover.js";
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
@ -41,6 +50,8 @@ import { GearIcon } from "@hive/shared/icons.js";
import { Dropdown, type DropdownOption } from "@hive/shared/dropdown.js";
import "./ComponentsPage.css";
const HINT_TONES: HintTone[] = ["muted", "warn", "error", "ok"];
function Section({
title,
children,
@ -590,6 +601,62 @@ export function ComponentsPage() {
</div>
</Section>
<Section title="Hint">
<div class="components-chip-row">
{HINT_TONES.map((tone) => (
<Sample key={tone} label={tone}>
<Hint tone={tone}>A line of supporting text.</Hint>
</Sample>
))}
</div>
</Section>
<Section title="ActionRow, Toolbar, EmptyState, ExternalLink">
<Sample label="ActionRow: the buttons that end a form">
<ActionRow>
<Button variant="primary">save</Button>
<Button>cancel</Button>
</ActionRow>
</Sample>
<Sample label="Toolbar: the bar above a list">
<Toolbar>
<Button>refresh</Button>
</Toolbar>
</Sample>
<Sample label="EmptyState">
<EmptyState>Nothing here yet.</EmptyState>
</Sample>
<Sample label="ExternalLink">
<ExternalLink href="https://example.com">example.com</ExternalLink>
</Sample>
</Section>
<Section title="ExpandableCard">
<Sample label="closed, open, and the loading twin">
<ExpandableCard
id="components-expandable-closed"
open={false}
onToggle={() => {}}
title="build 1"
badge={<Badge value="ok" />}
meta="2 minutes ago"
>
body
</ExpandableCard>
<ExpandableCard
id="components-expandable-open"
open
onToggle={() => {}}
title="build 2"
badge={<Badge value="failed" />}
meta="an hour ago"
>
<Hint>The body appears while the card is open.</Hint>
</ExpandableCard>
<ExpandableCardSkeleton />
</Sample>
</Section>
<Section title="Dialog">
<Sample label="modal, caller-owned content">
<DialogSample />

View file

@ -94,7 +94,6 @@
flex: 1 1 auto;
min-width: 0;
margin-left: 0.25em;
color: var(--muted);
font-size: 0.9em;
}
.create-agent-summary {
@ -118,14 +117,6 @@
margin: 0;
overflow-wrap: anywhere;
}
.create-agent-result-ok {
color: var(--green);
}
/* The controller's refusal of the AGENTS.md text, under its box. */
.create-agent-field-error {
margin: 0;
color: var(--red);
}
/* The picked archetype's description, under its select. */
.create-agent-archetype-description {
margin: 0;

View file

@ -45,7 +45,9 @@ import {
InferenceFields,
type InferenceDraft,
} from "../agent/detail/AgentInference.js";
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
import { FormField } from "../ui/form-field/FormField.js";
import { Hint } from "../ui/hint/Hint.js";
import { MultiselectFilterOptions } from "../ui/multiselect-filter/MultiselectFilter.js";
import {
initialSelection,
@ -325,12 +327,12 @@ export function CreateAgentForm({
/>
)}
{done && (
<div class="create-agent-result create-agent-result-ok">
<Hint tone="ok" as="div" class="create-agent-result">
<p>
Queued as job node #{result.nodeId} —{" "}
<Link href="/jobs">watch it in jobs</Link>.
</p>
</div>
</Hint>
)}
{!done && step === "identity" && (
<div class="create-agent-fields">
@ -409,7 +411,9 @@ export function CreateAgentForm({
onInput={setAgentsMd}
/>
{refusal !== null && (
<p class="create-agent-field-error">{refusal}</p>
<Hint tone="error" class="create-agent-field-error">
{refusal}
</Hint>
)}
</div>
)}
@ -438,14 +442,12 @@ export function CreateAgentForm({
)}
</div>
</FormField>
<label>
<input
type="checkbox"
checked={internet}
onChange={() => setInternet((v) => !v)}
/>{" "}
Unrestricted internet
</label>
<CheckboxField
id="create-agent-internet"
label="Unrestricted internet"
checked={internet}
onChange={setInternet}
/>
</div>
)}
{!done && step === "resources" && (
@ -531,9 +533,9 @@ export function CreateAgentForm({
back
</Button>
{problem !== null && (step !== "identity" || name !== "") && (
<span class="create-agent-hint" role="status">
<Hint as="span" role="status" class="create-agent-hint">
{problem}
</span>
</Hint>
)}
{step === "summary" ? (
<Button

View file

@ -32,6 +32,7 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.js";
import { useLocalSetting } from "@hive/shared/settings-storage.js";
import { ExternalLink } from "../ui/external-link/ExternalLink.js";
import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
import { SelectField } from "../ui/select-field/SelectField.js";
@ -268,9 +269,7 @@ function IssueReportBody() {
ariaSort: ariaSortFor("number"),
render: (r) =>
r.html_url ? (
<a href={r.html_url} target="_blank" rel="noreferrer">
#{r.number}
</a>
<ExternalLink href={r.html_url}>#{r.number}</ExternalLink>
) : (
`#${r.number}`
),

View file

@ -14,8 +14,3 @@
flex-direction: column;
gap: 1em;
}
.ui-jobs-refresh {
display: flex;
justify-content: flex-end;
margin-block-end: 0.5em;
}

Some files were not shown because too many files have changed in this diff Show more