From a9dd2769358d92de5067519a01e409d811b6701f Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 6 Nov 2026 19:53:31 +0100 Subject: [PATCH] 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. --- .../swarm-ui/src/pages/ComponentsPage.tsx | 34 +++++++++++++++++++ .../review-settings/ClassifierSettings.css | 16 ++------- .../review-settings/ClassifierSettings.tsx | 17 +++++----- .../review-tryout/ClassificationEditor.tsx | 5 +-- .../src/pages/review-tryout/ReviewTryOut.css | 23 +++---------- .../src/pages/review-tryout/ReviewTryOut.tsx | 5 +-- .../src/pages/schedules/ScheduleForm.css | 13 ++----- .../src/pages/schedules/ScheduleForm.tsx | 5 +-- .../swarm-ui/src/ui/field-grid/FieldGrid.css | 8 +++++ .../swarm-ui/src/ui/field-grid/FieldGrid.tsx | 30 ++++++++++++++++ 10 files changed, 99 insertions(+), 57 deletions(-) create mode 100644 frontend/packages/swarm-ui/src/ui/field-grid/FieldGrid.css create mode 100644 frontend/packages/swarm-ui/src/ui/field-grid/FieldGrid.tsx diff --git a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx index 3f02ef59..c5024a27 100644 --- a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx @@ -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() { +
+ + + {}} + /> + {}} + /> + {}} + /> + + {}} + /> + + + +
+
e.preventDefault()}> diff --git a/frontend/packages/swarm-ui/src/pages/review-settings/ClassifierSettings.css b/frontend/packages/swarm-ui/src/pages/review-settings/ClassifierSettings.css index 17f43a36..1d373ce1 100644 --- a/frontend/packages/swarm-ui/src/pages/review-settings/ClassifierSettings.css +++ b/frontend/packages/swarm-ui/src/pages/review-settings/ClassifierSettings.css @@ -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; diff --git a/frontend/packages/swarm-ui/src/pages/review-settings/ClassifierSettings.tsx b/frontend/packages/swarm-ui/src/pages/review-settings/ClassifierSettings.tsx index cc26bb8f..3ce8bd6a 100644 --- a/frontend/packages/swarm-ui/src/pages/review-settings/ClassifierSettings.tsx +++ b/frontend/packages/swarm-ui/src/pages/review-settings/ClassifierSettings.tsx @@ -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 ? (
void save(e)}> -
+ edit({ ...draft, max })} /> -
+ {draft.runtime === "acp" ? (
provider -
-
+ + edit({ ...draft, acpUrl })} /> -
-
+ + edit({ ...draft, acpModel })} /> -
+ -
+

Prices are in USD per million tokens.

diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/ClassificationEditor.tsx b/frontend/packages/swarm-ui/src/pages/review-tryout/ClassificationEditor.tsx index 63a4d63d..f28b6fd8 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/ClassificationEditor.tsx +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/ClassificationEditor.tsx @@ -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 })} /> -
+ onChange({ ...draft, threshold })} /> {onRemove ? : null} -
+ ); } diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.css b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.css index 33c11710..87917308 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.css +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.css @@ -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; diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.tsx b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.tsx index e7093a56..1178cf19 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.tsx +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOut.tsx @@ -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.

void run(e)}> -
+ {loadingRules ? "loading…" : "load the repo's rules"} -
+ {repoChoicesError ? ( .ui-form-field { - width: auto; - flex: 1 1 9em; -} .ui-schedule-form-hint { font-size: 0.85em; } diff --git a/frontend/packages/swarm-ui/src/pages/schedules/ScheduleForm.tsx b/frontend/packages/swarm-ui/src/pages/schedules/ScheduleForm.tsx index 15f1ff12..67275e97 100644 --- a/frontend/packages/swarm-ui/src/pages/schedules/ScheduleForm.tsx +++ b/frontend/packages/swarm-ui/src/pages/schedules/ScheduleForm.tsx @@ -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} /> -
+ -
+ {fires.length > 0 ? ( {repeat ? "next fires: " : "fires: "} diff --git a/frontend/packages/swarm-ui/src/ui/field-grid/FieldGrid.css b/frontend/packages/swarm-ui/src/ui/field-grid/FieldGrid.css new file mode 100644 index 00000000..5573feab --- /dev/null +++ b/frontend/packages/swarm-ui/src/ui/field-grid/FieldGrid.css @@ -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; +} diff --git a/frontend/packages/swarm-ui/src/ui/field-grid/FieldGrid.tsx b/frontend/packages/swarm-ui/src/ui/field-grid/FieldGrid.tsx new file mode 100644 index 00000000..3dbeb816 --- /dev/null +++ b/frontend/packages/swarm-ui/src/ui/field-grid/FieldGrid.tsx @@ -0,0 +1,30 @@ +// — 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 ( +
+ {children} +
+ ); +} + +export function FieldGridFull({ children }: { children: ComponentChildren }) { + return
{children}
; +}