Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: add CheckboxField and use it for the issue report toggle and the account delete option

This commit is contained in:
iris 2026-10-21 23:49:57 +02:00 • committed by mara
commit acf82beba3
7 changed files with 105 additions and 30 deletions

View file

@ -150,9 +150,11 @@ that needs it, not after a contributor styles it inline and leaves it
for later — a page reaching for a component that's already there has ready-made for later — a page reaching for a component that's already there has ready-made
blocks to build with, instead of the next contributor duplicating blocks to build with, instead of the next contributor duplicating
ad-hoc styling that someone then has to hunt down and consolidate. ad-hoc styling that someone then has to hunt down and consolidate.
`Panel`/`Badge`/`Table`/`TextField`/`SelectField`/`Button` are the `Panel`/`Badge`/`Table`/`TextField`/`SelectField`/`CheckboxField`/`Button`
primitives that exist; `/components` always has the current, complete are the primitives that exist; `/components` always has the current, complete
list — this doc won't try to keep a duplicate inventory in sync. list — this doc won't try to keep a duplicate inventory in sync. A yes/no
option in a form uses `CheckboxField`, which sizes its row like the other
fields, rather than a bare `<input type="checkbox">`.
**Every new `ui/` component gets a demo section on `/components` the **Every new `ui/` component gets a demo section on `/components` the
same day it lands** — no primitive without a place to see it. One same day it lands** — no primitive without a place to see it. One

View file

@ -13,6 +13,7 @@ import { useEffect, useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.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 { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js";
import { accountKindLabel } from "./account-kind.js"; import { accountKindLabel } from "./account-kind.js";
import "./LinkedAccounts.css"; import "./LinkedAccounts.css";
@ -136,17 +137,12 @@ export function LinkedAccounts({
removed from the swarm secret store. The agent is not told. removed from the swarm secret store. The agent is not told.
</p> </p>
{deleteTarget.kind === "matrix" ? ( {deleteTarget.kind === "matrix" ? (
<label> <CheckboxField
<input id="linked-accounts-revoke"
type="checkbox" label="also log the token out at the homeserver; if that fails, the account is kept"
checked={revoke} checked={revoke}
onChange={(e) => onChange={setRevoke}
setRevoke((e.target as HTMLInputElement).checked) />
}
/>{" "}
also log the token out at the homeserver; if that fails, the
account is kept
</label>
) : null} ) : null}
{deleteError ? ( {deleteError ? (
<ApiErrorPanel context="delete failed" problem={deleteError} /> <ApiErrorPanel context="delete failed" problem={deleteError} />

View file

@ -27,6 +27,7 @@ import {
SelectField, SelectField,
type SelectOption, type SelectOption,
} from "../ui/select-field/SelectField.js"; } from "../ui/select-field/SelectField.js";
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
import { Button, type ButtonVariant } from "../ui/button/Button.js"; import { Button, type ButtonVariant } from "../ui/button/Button.js";
import { Dialog } from "../ui/dialog/Dialog.js"; import { Dialog } from "../ui/dialog/Dialog.js";
import { ConfirmDialog } from "../ui/confirm-dialog/ConfirmDialog.js"; import { ConfirmDialog } from "../ui/confirm-dialog/ConfirmDialog.js";
@ -156,6 +157,18 @@ function SelectFieldSample() {
); );
} }
function CheckboxFieldSample() {
const [checked, setChecked] = useState(true);
return (
<CheckboxField
id="sample-checkbox-field"
label="hide blocked (open dependency)"
checked={checked}
onChange={setChecked}
/>
);
}
function RefreshIntervalPickerSample() { function RefreshIntervalPickerSample() {
const [value, setValue] = useState<RefreshIntervalMs>(30_000); const [value, setValue] = useState<RefreshIntervalMs>(30_000);
return ( return (
@ -517,6 +530,21 @@ export function ComponentsPage() {
</Sample> </Sample>
</Section> </Section>
<Section title="CheckboxField">
<Sample label="editable">
<CheckboxFieldSample />
</Sample>
<Sample label="disabled">
<CheckboxField
id="sample-checkbox-field-disabled"
label="also log the token out"
checked={false}
onChange={() => {}}
disabled
/>
</Sample>
</Section>
<Section title="Button"> <Section title="Button">
<div class="components-chip-row"> <div class="components-chip-row">
{BUTTON_VARIANTS.map((variant) => ( {BUTTON_VARIANTS.map((variant) => (

View file

@ -15,12 +15,6 @@
gap: 1.25em; gap: 1.25em;
margin-bottom: 1em; margin-bottom: 1em;
} }
.issue-report-toggle {
display: flex;
align-items: center;
gap: 0.4em;
cursor: pointer;
}
.issue-report-sort-btn { .issue-report-sort-btn {
background: none; background: none;
border: none; border: none;

View file

@ -33,6 +33,7 @@ import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.js"; import { Badge } from "@hive/shared/badge.js";
import { useLocalSetting } from "@hive/shared/settings-storage.js"; import { useLocalSetting } from "@hive/shared/settings-storage.js";
import { Panel } from "../ui/panel/Panel.js"; import { Panel } from "../ui/panel/Panel.js";
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
import { SelectField } from "../ui/select-field/SelectField.js"; import { SelectField } from "../ui/select-field/SelectField.js";
import { Table, type TableColumn } from "../ui/table/Table.js"; import { Table, type TableColumn } from "../ui/table/Table.js";
import { ALL_REPOS, reportUrl, repoOptions } from "./issue-report-repo.js"; import { ALL_REPOS, reportUrl, repoOptions } from "./issue-report-repo.js";
@ -378,16 +379,12 @@ export function IssueReportPage() {
onChange={setRepoFilter} onChange={setRepoFilter}
options={repoOptions(repos, repoFilter)} options={repoOptions(repos, repoFilter)}
/> />
<label class="issue-report-toggle"> <CheckboxField
<input id="issue-report-hide-blocked"
type="checkbox" label="hide blocked (open dependency)"
checked={hideBlocked} checked={hideBlocked}
onChange={(e) => onChange={setHideBlocked}
setHideBlocked((e.target as HTMLInputElement).checked) />
}
/>
hide blocked (open dependency)
</label>
</div> </div>
{reposError ? ( {reposError ? (
<ApiErrorPanel <ApiErrorPanel

View file

@ -0,0 +1,25 @@
/* <CheckboxField> — box and label on one row. `min-height` is the same
touch-target floor `.ui-form-control` sets (FormField.css has the
reasoning), so a checkbox beside a text or select field is as tall as it and
its label is centred on the same line. The box takes the accent colour from
the shared theme instead of the browser's blue. */
.ui-checkbox-field {
display: flex;
align-items: center;
gap: 0.5em;
min-height: 2.75em;
cursor: pointer;
color: var(--fg);
}
.ui-checkbox-field input {
accent-color: var(--purple);
width: 1.1em;
height: 1.1em;
margin: 0;
flex: none;
cursor: inherit;
}
.ui-checkbox-field:has(input:disabled) {
color: var(--muted);
cursor: not-allowed;
}

View file

@ -0,0 +1,33 @@
// <CheckboxField> — a labelled checkbox, the on/off sibling of `TextField` and
// `SelectField`. The label sits beside the box, not above it, and the whole
// row is one click target at the kit's touch-target height, so a checkbox next
// to a `SelectField` lines up with it and a bare browser checkbox no longer
// sets the row's size.
import "./CheckboxField.css";
export function CheckboxField({
id,
label,
checked,
onChange,
disabled,
}: {
id: string;
label: string;
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
}) {
return (
<label class="ui-checkbox-field" for={id}>
<input
id={id}
type="checkbox"
checked={checked}
disabled={disabled}
onChange={(e) => onChange((e.target as HTMLInputElement).checked)}
/>
<span>{label}</span>
</label>
);
}