From 7209ab8fdc01fb87d5b975a03b36abb739b6b3ad Mon Sep 17 00:00:00 2001
From: Don Kackman
Date: Mon, 7 Sep 2026 20:46:41 -0500
Subject: [PATCH] Refactor UI components for improved styling and functionality
- Updated JobPage to enhance media display with improved link and image handling.
- Enhanced JobsPage with visual indicators for running jobs and refined layout.
- Modified ModelsPage to improve button styling and added last used indicators.
- Improved PromptsPage layout and added directory display for better context.
- Refined ServerPage styling for consistency and clarity.
- Enhanced WorkflowPage with better proof display and improved JSON toggle functionality.
- Updated WorkflowsPage to improve layout, filtering, and card display for workflows.
- Added tests for WorkflowsPage to ensure correct filtering and display of workflow data.
- Standardized font usage across components for consistency.
---
ui/CLAUDE.md | 54 +++
ui/e2e/chrome.spec.ts | 28 +-
ui/e2e/smoke.spec.ts | 17 +-
ui/src/App.svelte | 363 +++++++++-------
ui/src/app.css | 327 +++++++++++----
ui/src/lib/FolderGroups.svelte | 12 +-
ui/src/lib/KeyboardHelp.svelte | 2 +-
ui/src/lib/StatusPopover.svelte | 3 +-
ui/src/lib/editor/FlowView.svelte | 17 +-
ui/src/lib/editor/QuantizationEditor.svelte | 2 +-
ui/src/lib/editor/StepEditor.svelte | 13 +-
ui/src/lib/pages/EditorPage.svelte | 7 +-
ui/src/lib/pages/GalleryPage.svelte | 56 ++-
ui/src/lib/pages/JobPage.svelte | 43 +-
ui/src/lib/pages/JobsPage.svelte | 31 +-
ui/src/lib/pages/ModelsPage.svelte | 25 +-
ui/src/lib/pages/PromptsPage.svelte | 28 +-
ui/src/lib/pages/ServerPage.svelte | 6 +-
ui/src/lib/pages/WorkflowPage.svelte | 211 +++++++---
ui/src/lib/pages/WorkflowsPage.svelte | 441 +++++++++++++-------
ui/src/lib/pages/WorkflowsPage.test.ts | 28 +-
ui/src/lib/schema/SchemaNode.svelte | 2 +-
22 files changed, 1165 insertions(+), 551 deletions(-)
diff --git a/ui/CLAUDE.md b/ui/CLAUDE.md
index 8198cb39..920679a4 100644
--- a/ui/CLAUDE.md
+++ b/ui/CLAUDE.md
@@ -21,4 +21,58 @@ job's files load from where they were written.
`main.ts` before the first request); a page refetches on a switch by reading
`workspace.current` inside its load effect.
+## Design system
+
+The look is "contact sheet", and it rests on two rules that `src/app.css`
+encodes. Follow them when adding UI.
+
+**Colour means machine state.** The greys are deliberately achromatic
+(equal-RGB, not blue-tinted slate) because this app's output is pictures and
+any chroma in the surrounding surfaces biases how a generated colour reads.
+So the chrome carries no accent: interactive elements are ink (`--accent` is
+the *interactive ink*, a near-black in light and a near-white in dark; the
+primary button is a solid ink fill, links are ink with an underline).
+`--live`, a darkroom safelight amber, is the one signal colour and marks only
+what is running, where keyboard focus is, and when VRAM is under pressure -
+never decoration. `--good` / `--bad` stay state colours too. Every token pair
+passes WCAG AA in both themes; keep it that way.
+
+**Mono is what the engine reads.** `--font-mono` for anything the engine
+resolves literally - a workflow or prompt name, a `variable:` / `asset:` /
+`prompt:` reference, a path, a seed, a run id, a measurement (add `.num` for
+tabular figures). `--font-sans` for anything written for a person -
+descriptions, hints, control labels. Headings are mono because they name
+things the engine resolves, and they are sentence case: no ALL-CAPS eyebrow
+labels and no tracked-out letter-spacing on small labels.
+
+Type scale is `--t-xs` .. `--t-xl` off a 15px base; radius says what a thing
+is (`--radius-frame` a picture, `--radius-2` a surface, 999px a chip) rather
+than being one global value. One filled button per view: the thing you came
+to do. `.quiet` is outlined, `.bare` is text-only.
+
+**Show the proof.** A list of things that produce images shows the images.
+The workflows catalog and the workflow page both read `api.gallery()` and
+match a workflow's name to an output entry's `folder` - a run writes to
+`///` and the gallery reports that identity with
+the run id already stripped, so the match is direct equality. Proofs load
+separately from the listing so the catalog never waits on them, and a
+workflow that has never run gets no frame at all rather than a grey
+placeholder (a fresh workspace would otherwise be a wall of empty plates).
+Within a folder, workflows that have produced something sort first.
+
+Every picture in the app sits in the global `.frame` (app.css): the media
+fills it edge to edge, with no inner padding and no rounding of its own,
+which is what makes it read as a proof on a sheet rather than as another
+rounded card. Gallery tiles, a workflow's recent outputs and a job's results
+all use it; the catalog card's `.cardframe` is the one variant, because it
+is a banner inside a card rather than a free-standing frame. Set the size on
+the frame - the media inside always fills it.
+
+Where `--live` appears, and nowhere else: the header's running link and VRAM
+pressure, a running job's row and status chip, the job page's progress bar
+and current step, the flow view's active step, a model download in flight,
+and the focus ring. Resting meters (free disk, VRAM under pressure's
+threshold) stay `--muted`; selection is the user's state, not the machine's,
+so it reads as a heavier ink edge instead.
+
See docs/SERVER.md.
diff --git a/ui/e2e/chrome.spec.ts b/ui/e2e/chrome.spec.ts
index e568251b..bcc43b03 100644
--- a/ui/e2e/chrome.spec.ts
+++ b/ui/e2e/chrome.spec.ts
@@ -1,20 +1,21 @@
import { expect, test } from '@playwright/test'
-test('the status strip carries worker state and docs, off the nav row', async ({
+test('worker state and docs sit on the nav row, in one header row', async ({
page,
}) => {
await page.goto('/')
- const strip = page.locator('header .statusbar')
- await expect(strip).toBeVisible()
- // worker state renders in the strip (fixture worker is idle at start)
- await expect(strip).toContainText(/idle|GB/)
- // docs links moved down out of the nav row
+ // One header row: the state the old second row carried now sits at the
+ // right of the nav, so no page spends a strip on the word "idle"
+ const row = page.locator('header .navrow')
+ await expect(row).toHaveCount(1)
+ const state = page.locator('header .state')
+ await expect(state).toBeVisible()
+ // worker state renders there (fixture worker is idle at start)
+ await expect(state).toContainText(/idle|GB/)
await expect(
- strip.getByRole('link', { name: 'documentation on GitHub' }),
- ).toBeVisible()
- await expect(
- page.locator('header .navrow').getByRole('link', { name: 'Workflows' }),
+ state.getByRole('link', { name: 'documentation on GitHub' }),
).toBeVisible()
+ await expect(row.getByRole('link', { name: 'Workflows' })).toBeVisible()
})
test('? opens the shortcuts overlay; Escape closes it; typing ? in a field does not', async ({
@@ -85,8 +86,11 @@ test('the status popover traps focus and returns it to its trigger button', asyn
test('tab order walks the nav row in reading order', async ({ page }) => {
await page.goto('/')
- // From the top of the document, Tab lands on the nav links in their
- // visual order - the baseline "rational tab order" check on the chrome
+ // From the top of the document, Tab lands on the header's links in their
+ // visual order - the baseline "rational tab order" check on the chrome.
+ // The wordmark is the first stop: it is a link home.
+ await page.keyboard.press('Tab')
+ await expect(page.locator('header .brand')).toBeFocused()
await page.keyboard.press('Tab')
await expect(
page.getByRole('link', { name: 'Workflows' }).first(),
diff --git a/ui/e2e/smoke.spec.ts b/ui/e2e/smoke.spec.ts
index 552d1f3e..7e331f79 100644
--- a/ui/e2e/smoke.spec.ts
+++ b/ui/e2e/smoke.spec.ts
@@ -14,18 +14,23 @@ test('workflow browser lists, describes and filters', async ({ page }) => {
await expect(page.getByRole('link', { name: /^z-image / })).toHaveCount(0)
})
-test('workflow page shows JSON and a run form', async ({ page }) => {
+test('workflow page leads with the run form and reveals JSON on request', async ({
+ page,
+}) => {
await page.goto('/#/workflows/models/z-image')
await expect(
page.getByRole('heading', { name: 'models/z-image' }),
).toBeVisible()
- // JSON is shown by default via Monaco
- await expect(page.locator('.monaco-editor').first()).toBeVisible({
- timeout: 20_000,
- })
// the variables form renders with defaults as placeholders
await expect(page.getByLabel('num_inference_steps')).toBeVisible()
await expect(page.getByRole('button', { name: /Run/ })).toBeEnabled()
+ // the definition is collapsed by default - this page is for setting
+ // variables and running, not for reading the file
+ await expect(page.locator('.monaco-editor')).toHaveCount(0)
+ await page.getByRole('button', { name: 'show JSON' }).click()
+ await expect(page.locator('.monaco-editor').first()).toBeVisible({
+ timeout: 20_000,
+ })
})
test('editor opens a workflow with introspected arguments', async ({
@@ -307,6 +312,8 @@ test('editor flags a dangling reference without asking the server', async ({
test('the theme toggle re-themes an open Monaco editor', async ({ page }) => {
// Playwright's default colour scheme is light, so "system" starts light
await page.goto('/#/workflows/models/z-image')
+ // the definition is collapsed by default, so ask for it first
+ await page.getByRole('button', { name: 'show JSON' }).click()
const editor = page.locator('.monaco-editor').first()
await expect(editor).toBeVisible({ timeout: 20_000 })
await expect(editor).toHaveClass(/\bvs\b/)
diff --git a/ui/src/App.svelte b/ui/src/App.svelte
index 712152a4..05d64142 100644
--- a/ui/src/App.svelte
+++ b/ui/src/App.svelte
@@ -120,11 +120,19 @@
+