swarm-ui: FieldGrid lays out a row of form fields on one grid for the forms that each hand-rolled it
Classifier settings, the try-out target row and classification rows, and the schedule cadence each wrote their own flex row with its own flex basis and gap. FieldGrid is one grid with equal columns and one gap, FieldGridFull spans a row, and the four forms use it, so a lone last field keeps its column width and the columns share their edges in every row.
This commit is contained in:
parent
c37205e8f5
commit
a9dd276935
10 changed files with 99 additions and 57 deletions
|
|
@ -41,6 +41,7 @@ import {
|
|||
ExpandableCardListSkeleton,
|
||||
} from "../ui/expandable-card/ExpandableCard.js";
|
||||
import { ExternalLink } from "../ui/external-link/ExternalLink.js";
|
||||
import { FieldGrid, FieldGridFull } from "../ui/field-grid/FieldGrid.js";
|
||||
import { FormStack } from "../ui/form-stack/FormStack.js";
|
||||
import { Hint, type HintTone } from "../ui/hint/Hint.js";
|
||||
import { FormResult } from "../ui/submit/FormResult.js";
|
||||
|
|
@ -649,6 +650,39 @@ export function ComponentsPage() {
|
|||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="FieldGrid">
|
||||
<Sample label="FieldGrid: equal columns, one gap; FieldGridFull spans the row">
|
||||
<FieldGrid>
|
||||
<TextField
|
||||
id="components-grid-a"
|
||||
label="input"
|
||||
value=""
|
||||
onInput={() => {}}
|
||||
/>
|
||||
<TextField
|
||||
id="components-grid-b"
|
||||
label="output"
|
||||
value=""
|
||||
onInput={() => {}}
|
||||
/>
|
||||
<TextField
|
||||
id="components-grid-c"
|
||||
label="cache read"
|
||||
value=""
|
||||
onInput={() => {}}
|
||||
/>
|
||||
<FieldGridFull>
|
||||
<TextField
|
||||
id="components-grid-d"
|
||||
label="URL"
|
||||
value=""
|
||||
onInput={() => {}}
|
||||
/>
|
||||
</FieldGridFull>
|
||||
</FieldGrid>
|
||||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="FormStack and FormResult">
|
||||
<Sample label="FormStack: fields and buttons in one column">
|
||||
<FormStack onSubmit={(e) => e.preventDefault()}>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,11 @@
|
|||
/* The settings form: the fields wrap in a row, the provider fields on a row of
|
||||
* their own under the ACP runtime, then the save action. */
|
||||
/* The settings form: the fields sit on a `FieldGrid`, the provider fields in a
|
||||
* group of their own under the ACP runtime, then the save action. */
|
||||
.ui-classifier-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
/* One grid for every group of fields: each field has the same width and the
|
||||
same edges at any width, and a label that wraps cannot push one input lower
|
||||
than its neighbour's. */
|
||||
.ui-classifier-settings-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(14em, 1fr));
|
||||
gap: 0.75em;
|
||||
}
|
||||
/* A field that needs the whole row (a URL, a model name). */
|
||||
.ui-classifier-settings-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
/* The provider fields, under their own heading. */
|
||||
.ui-classifier-settings-group {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { ActionRow } from "../../ui/action-row/ActionRow.js";
|
||||
import { Button } from "../../ui/button/Button.js";
|
||||
import { FieldGrid, FieldGridFull } from "../../ui/field-grid/FieldGrid.js";
|
||||
import { Hint } from "../../ui/hint/Hint.js";
|
||||
import { Disclosure } from "../../ui/disclosure/Disclosure.js";
|
||||
import { SelectField } from "../../ui/select-field/SelectField.js";
|
||||
|
|
@ -143,7 +144,7 @@ export function ClassifierSettings() {
|
|||
) : null}
|
||||
{draft ? (
|
||||
<form class="ui-classifier-settings" onSubmit={(e) => void save(e)}>
|
||||
<div class="ui-classifier-settings-fields">
|
||||
<FieldGrid>
|
||||
<SelectField
|
||||
id="classifier-runtime"
|
||||
label="runtime"
|
||||
|
|
@ -176,12 +177,12 @@ export function ClassifierSettings() {
|
|||
pattern="[0-9]+"
|
||||
onInput={(max) => edit({ ...draft, max })}
|
||||
/>
|
||||
</div>
|
||||
</FieldGrid>
|
||||
{draft.runtime === "acp" ? (
|
||||
<fieldset class="ui-classifier-settings-group">
|
||||
<legend>provider</legend>
|
||||
<div class="ui-classifier-settings-fields">
|
||||
<div class="ui-classifier-settings-wide">
|
||||
<FieldGrid>
|
||||
<FieldGridFull>
|
||||
<TextField
|
||||
id="classifier-acp-url"
|
||||
label="URL"
|
||||
|
|
@ -189,8 +190,8 @@ export function ClassifierSettings() {
|
|||
placeholder="https://llm.example/v1"
|
||||
onInput={(acpUrl) => edit({ ...draft, acpUrl })}
|
||||
/>
|
||||
</div>
|
||||
<div class="ui-classifier-settings-wide">
|
||||
</FieldGridFull>
|
||||
<FieldGridFull>
|
||||
<TextField
|
||||
id="classifier-acp-model"
|
||||
label="model"
|
||||
|
|
@ -198,7 +199,7 @@ export function ClassifierSettings() {
|
|||
placeholder="Qwen/Qwen3-35B"
|
||||
onInput={(acpModel) => edit({ ...draft, acpModel })}
|
||||
/>
|
||||
</div>
|
||||
</FieldGridFull>
|
||||
<TextField
|
||||
id="classifier-acp-price-input"
|
||||
label="input price"
|
||||
|
|
@ -227,7 +228,7 @@ export function ClassifierSettings() {
|
|||
edit({ ...draft, priceCacheWrite })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FieldGrid>
|
||||
<p class="ui-classifier-settings-note">
|
||||
Prices are in USD per million tokens.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// description (the prompt the model answers), which answer passes, and how
|
||||
// certain the model has to be for its answer to count.
|
||||
import { Button } from "../../ui/button/Button.js";
|
||||
import { FieldGrid } from "../../ui/field-grid/FieldGrid.js";
|
||||
import { SelectField } from "../../ui/select-field/SelectField.js";
|
||||
import { TextAreaField } from "../../ui/text-area-field/TextAreaField.js";
|
||||
import { TextField } from "../../ui/text-field/TextField.js";
|
||||
|
|
@ -41,7 +42,7 @@ export function ClassificationEditor({
|
|||
placeholder="what to look for in the PR, e.g. does it add a naive datetime?"
|
||||
onInput={(description) => onChange({ ...draft, description })}
|
||||
/>
|
||||
<div class="ui-tryout-classification-row">
|
||||
<FieldGrid min="12em" class="ui-tryout-classification-row">
|
||||
<SelectField
|
||||
id={`${id}-polarity`}
|
||||
label="polarity"
|
||||
|
|
@ -59,7 +60,7 @@ export function ClassificationEditor({
|
|||
onInput={(threshold) => onChange({ ...draft, threshold })}
|
||||
/>
|
||||
{onRemove ? <Button onClick={onRemove}>remove</Button> : null}
|
||||
</div>
|
||||
</FieldGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,17 +12,10 @@
|
|||
gap: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.ui-tryout-target {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75em;
|
||||
}
|
||||
.ui-tryout-target > * {
|
||||
flex: 1 1 14em;
|
||||
}
|
||||
/* The button sits at the foot of its cell, beside the inputs. */
|
||||
.ui-tryout-target > button {
|
||||
flex: none;
|
||||
align-self: flex-end;
|
||||
align-self: end;
|
||||
justify-self: start;
|
||||
}
|
||||
.ui-tryout-loaded {
|
||||
margin: 0;
|
||||
|
|
@ -37,16 +30,10 @@
|
|||
border-radius: 0.5em;
|
||||
}
|
||||
.ui-tryout-classification-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 0.75em;
|
||||
}
|
||||
.ui-tryout-classification-row > * {
|
||||
flex: 1 1 12em;
|
||||
align-items: end;
|
||||
}
|
||||
.ui-tryout-classification-row > button {
|
||||
flex: none;
|
||||
justify-self: start;
|
||||
}
|
||||
.ui-tryout-hint {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { ActionRow } from "../../ui/action-row/ActionRow.js";
|
||||
import { Button } from "../../ui/button/Button.js";
|
||||
import { FieldGrid } from "../../ui/field-grid/FieldGrid.js";
|
||||
import { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js";
|
||||
import { Disclosure } from "../../ui/disclosure/Disclosure.js";
|
||||
import { ExternalLink } from "../../ui/external-link/ExternalLink.js";
|
||||
|
|
@ -332,7 +333,7 @@ export function ReviewTryOut() {
|
|||
the repo's rules.
|
||||
</p>
|
||||
<form class="ui-tryout-form" onSubmit={(e) => void run(e)}>
|
||||
<div class="ui-tryout-target">
|
||||
<FieldGrid class="ui-tryout-target">
|
||||
<TextField
|
||||
id="tryout-repo"
|
||||
label="repo"
|
||||
|
|
@ -358,7 +359,7 @@ export function ReviewTryOut() {
|
|||
>
|
||||
{loadingRules ? "loading…" : "load the repo's rules"}
|
||||
</Button>
|
||||
</div>
|
||||
</FieldGrid>
|
||||
{repoChoicesError ? (
|
||||
<ApiErrorPanel
|
||||
context="the repos with an open PR could not be listed"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* The form fills its card; fields stack with the page's usual gap. The
|
||||
cadence controls sit in one wrapping row so "repeats / every / unit /
|
||||
starts at" read as one sentence on a wide card and stack on a narrow one. */
|
||||
cadence controls sit on a `FieldGrid`, so "repeats / every / unit / starts
|
||||
at" read as one row on a wide card and stack on a narrow one. */
|
||||
.ui-schedule-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -20,15 +20,6 @@
|
|||
max-height: none;
|
||||
overflow-y: visible;
|
||||
}
|
||||
.ui-schedule-form-cadence {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75em;
|
||||
}
|
||||
.ui-schedule-form-cadence > .ui-form-field {
|
||||
width: auto;
|
||||
flex: 1 1 9em;
|
||||
}
|
||||
.ui-schedule-form-hint {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { ActionRow } from "../../ui/action-row/ActionRow.js";
|
||||
import { Button } from "../../ui/button/Button.js";
|
||||
import { FieldGrid } from "../../ui/field-grid/FieldGrid.js";
|
||||
import { Card } from "../../ui/card/Card.js";
|
||||
import { Hint } from "../../ui/hint/Hint.js";
|
||||
import {
|
||||
|
|
@ -212,7 +213,7 @@ export function ScheduleForm({
|
|||
required
|
||||
onInput={setBody}
|
||||
/>
|
||||
<div class="ui-schedule-form-cadence">
|
||||
<FieldGrid min="12em">
|
||||
<SelectField
|
||||
id={`${idPrefix}-repeat`}
|
||||
label="repeats"
|
||||
|
|
@ -250,7 +251,7 @@ export function ScheduleForm({
|
|||
required
|
||||
onInput={setStart}
|
||||
/>
|
||||
</div>
|
||||
</FieldGrid>
|
||||
{fires.length > 0 ? (
|
||||
<Hint class="ui-schedule-form-hint">
|
||||
{repeat ? "next fires: " : "fires: "}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
.ui-field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--ui-field-min), 1fr));
|
||||
gap: 0.75em;
|
||||
}
|
||||
.ui-field-grid-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
30
frontend/packages/swarm-ui/src/ui/field-grid/FieldGrid.tsx
Normal file
30
frontend/packages/swarm-ui/src/ui/field-grid/FieldGrid.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// <FieldGrid> — a row or block of form fields on one grid: every field is the
|
||||
// same width and the columns have the same edges in every row, so a field's
|
||||
// width depends on the container and not on the siblings it wraps with. The
|
||||
// columns are as many as fit `min` wide; a lone last field keeps its column
|
||||
// width instead of stretching across the form. One gap, the same in every form.
|
||||
// A field that needs a whole row (a URL, a model name) goes in `FieldGridFull`.
|
||||
import type { ComponentChildren } from "preact";
|
||||
import "./FieldGrid.css";
|
||||
|
||||
export function FieldGrid({
|
||||
min = "14em",
|
||||
class: extraClass,
|
||||
children,
|
||||
}: {
|
||||
/** The narrowest a column gets, any CSS length. */
|
||||
min?: string;
|
||||
class?: string;
|
||||
children: ComponentChildren;
|
||||
}) {
|
||||
const classes = ["ui-field-grid", extraClass].filter(Boolean).join(" ");
|
||||
return (
|
||||
<div class={classes} style={{ "--ui-field-min": min }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FieldGridFull({ children }: { children: ComponentChildren }) {
|
||||
return <div class="ui-field-grid-full">{children}</div>;
|
||||
}
|
||||
Loading…
Reference in a new issue