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
14 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>
</>
);
}