Overview
The UI library (@clockwyrks/ui) is the shared frontend code for The Test
Cabinet’s two GUIs: the public gallery and the
console. It hosts the entire routed gallery
application plus the primitives those GUIs render, so both are thin hosts over
one application.
It lives in the ui-common repository beside @clockwyrks/run-stats, the
framework-free scoring rules whose counterparts the backend scores with, which
the library’s ratings module re-exports alongside its display metadata. The
repository builds against contracts and engines, and the gallery, the
console and The Spec Cabinet build against it.
It ships no service and runs in no process of its own. Each GUI mounts the shared app inside its own router and supplies it a data source, and the two hosts differ only in where that data comes from and which capabilities they enable. The router is a React Router data router, because only a data router can hold a navigation, and a form holding unsaved work asks before any navigation leaves it.
Subpath entries
Section titled “Subpath entries”A host imports only the entries it needs.
| Entry | Contents |
|---|---|
@clockwyrks/ui | Presentational primitives, the rating model, and model-id helpers. |
@clockwyrks/ui/app | The full routed gallery application and its data context. |
@clockwyrks/ui/client | The transport-agnostic client interfaces and their React contexts. |
@clockwyrks/ui/transport | The HTTP transports the console mounts. |
@clockwyrks/ui/tokens.css | The --tcab-* theme token defaults. |
The gallery application
Section titled “The gallery application”One routed application serves every host. It covers the gallery a reader browses, the run-execution surface an operator launches, watches, reviews and publishes runs from, and the notification subsystem. It reads its data and its capabilities from context, so a host varies it by what it provides rather than by swapping screens. Route paths are built through the exported route builders, so each path is defined in one place.
Back navigation
Section titled “Back navigation”Every on-page back control returns to the page the viewer was on before entering the current page, exactly as the browser’s own back button would. This covers the chevron beside a detail page’s title, a text link that leads back, and the brand control that leaves a mode such as gg analysis. A page spans every route under its own path, so its tabs and sub-views count as one page and back skips over them.
The app records the in-app history of the browser tab, and a back control takes the step by moving back through that history rather than pushing a new entry. The record survives a reload of the tab. A page opened with no in-app history behind it, such as a fresh deep link, falls back to a fixed parent route the control names.
A form that leads to the entity it saved or launched replaces itself in the history, so back from that entity returns to where the form was opened from.
A back control that must ask before leaving, such as an editor holding unsaved work, takes the same step once the question is answered.
The data and capability context
Section titled “The data and capability context”Each host builds the gallery data value from its own source. The gallery builds it from the public bucket’s index objects and documents. The console builds it live from a backend.
A canExecute flag on that value gates the run-execution surface, including
the signed-in identity and notifications. Optional capability members gate the rest.
arena is present on a host that can run adversarial matches and tournaments.
A host that omits one hides the corresponding surface.
The value also resolves a run’s media to loadable URLs, whichever host is
asking: proof-of-implementation media, an asset-generation run’s regenerated,
target and preview images with its action log, a voxel run’s per-part .glb and
rig.json, a particle run’s system.json, and case-scoped validation
baselines. The gallery resolves these from the public bucket, and a console
from the artifacts service, whether or not
the run is published.
A run’s inputs resolve one variant of one exact case version rendered for one engine, using the run’s own recorded version and engine. A console resolves them from the backend’s version and specs routes. The gallery resolves them from the bucket’s case document for that version.
Listing pages are answered through a paged, filtered, sorted query the host implements. A console forwards it to the backend’s offset endpoint. The gallery answers it from the index objects it holds in memory with the same semantics, so paging is identical on either host.
The case detail coordinate
Section titled “The case detail coordinate”A test case’s detail page is anchored to one selected coordinate: a version, a variant of that version, and an engine that version supports. The coordinate lives in the URL and travels across the whole page, so everything describing the deliverable describes the same one and any selection is a link someone else can open. Selection is canonical: an unknown version resolves to the latest, and a variant or engine the selected version does not declare resolves to that version’s default. A run is launched against the resolved coordinate.
A description ships with the version it describes, so the page shows the anchored version’s own description. While an older version is anchored, the page names the latest version and links to it, and the link re-anchors the page to that version.
The page’s run aggregations scope relative to the anchored coordinate rather than selecting one of their own, and that scope lives in the URL alongside it. Where the anchored version supports more than one engine, the engine scope offers each of those engines by name and then every engine at once. Choosing a named engine anchors the whole page to it, exactly as the page’s own engine selector does. A view widened across engines lists each engine separately rather than folding them, since runs under different engines measure different work.
The leaderboard ranks each model and harness pair by the average points its runs earned, and its metric columns are chosen from a column picker. As on the run log, the functional rating and the aesthetic tier are separate columns, and the aesthetic columns are hidden until a viewer enables them.
A case’s changelog and errata cover every version regardless of the anchor.
The run log
Section titled “The run log”Every listing of runs renders one shared log. A listing that includes runs still in flight lists them apart from the finished rows, since a run with no record yet has nothing to sort or page by.
A run’s duration counts from its startedAt and from nothing else, which keeps
queued time out of it, and the count begins when the run reaches starting. It
advances off one clock the whole log shares, and the log holds that clock only
while a duration is moving.
Columns
Section titled “Columns”A run’s two ratings are separate columns. Functionality holds the functional rating, a game jam’s overall grade in its place, or a failed run’s failure tier. Aesthetic holds the reviewer’s aesthetic tier and is hidden until a viewer enables it from the column picker.
Every column has a minimum width. When the shown columns need more than the log’s width, the log scrolls horizontally instead of narrowing a column below its minimum.
A gg run’s agents
Section titled “A gg run’s agents”A gg run’s row names its configuration in the model column, and an arrow beside the name lists the configuration’s agents beneath the row. Each agent’s line gives its name, its model, and the tokens and cost the run spent on it, in the same columns as the run’s totals. The primary agent comes first and is tagged as such. An agent the run never started is marked as not run. How many instances of an agent a run started varies from run to run of one configuration, so the line omits it.
The lines come from the run’s summary card. The backend builds them whenever it builds the card, from the run record’s session summary and the capability set it ran, so every recorded gg run carries them with nothing stored beyond its record. A run whose session wrote no summary, such as one stopped before the session ended, has no lines and no arrow.
An agent’s cost is priced at that agent’s own model, while a gg run’s comparable cost prices all of the run’s tokens at its primary model’s list price. The agents’ costs therefore need not sum to the run’s.
Deleting a run
Section titled “Deleting a run”An unpublished run is deletable and a published one is not, which is the rule the backend enforces and the only one there is. A surface offering the action decides from the run’s own publish state, read off the record or the summary card it has already resolved, rather than from the console’s produced worklist — that worklist is a cache of the runs it has caught up with, and it lags a run whose record is still being written, which a canceled run’s is for as long as its driver takes to stop the harness, drain telemetry and hand the partial record back. A surface holding nothing but a run id falls back to the worklist.
The affordance is hidden only where the host can delete no run at all, and is shown disabled with its reason wherever the host could delete but this run currently cannot be, so a state that will pass on its own is visible rather than absent.
A cancellation’s follow-up read is deferred accordingly: every refresh fired when the cancel returns is premature, so the console watches the canceled runs for the records their drivers hand back and re-reads as they land, whether the cancellation came from one run’s control or from a bulk sweep.
Asset viewers
Section titled “Asset viewers”A produced asset is rendered interactively rather than as a still image. A voxel run mounts the voxel runtime’s rig in a React Three Fiber canvas, where its joints are posed and its model-authored animations played. A particle run mounts the particle runtime’s player and simulates the effect live. Each 3D view falls back to the emitted preview image where WebGL is unavailable or reduced motion is requested, so a run stays reviewable.
Client interfaces and transports
Section titled “Client interfaces and transports”./client declares the backend and worker client interfaces the console is
written against, plus the React contexts that supply them and the identity
context. The app depends only on these interfaces.
./transport is the single implementation of the backend wire protocol: the
HTTP backend and execution clients and the HTTP arena client, each addressed by
a path of the host’s own origin. The console mounts these transports.
Immutable asset caching
Section titled “Immutable asset caching”A produced run’s artifacts do not change once created, so the app resolves each by URL through a process-wide cache that survives a component unmounting. Leaving a view and returning to it re-reads the cache rather than the network, and a cache hit renders without a loading state. A failed fetch is evicted from the cache so it is retried.
Loading, absence and failure
Section titled “Loading, absence and failure”A surface shows a loading state while its data is in flight, and names an entity as not found once the read has settled without it. A read that failed is reported as a failure, distinct from both.
A re-read in flight counts as unsettled for an entity the source has yet to resolve. The page of an entity created a moment ago therefore shows the loading state until the re-read that follows the creation settles, and names the entity as not found only if that re-read settles without it.
Those three states decide only what a surface with nothing to show renders. Data already resolved is rendered whatever the latest read did: a read that failed over a list already on screen is stale data, reported beside the rows rather than in place of them. A source keeps what it has resolved rather than emptying it on a refresh that failed, and a surface reads its own data before it reads that source’s load state.
A resolver reports the same three outcomes to the surface above it. It resolves
to nothing only where the store answered and holds no such entity — the store’s
own 404 — and fails for every other unanswered read, so no host turns an
unreachable store into an absence.
Every text input, textarea, dropdown and button a form renders is one of the
form primitives (Input, Textarea, Select and Button) or a native
control whose stylesheet includes the controls style mixins the primitives
are built from, so a control looks the same on every form and changes in one
place. A primitive’s size declarations carry no specificity, so a form’s own
class sets a control’s width or padding whatever order the stylesheets load
in. A Select draws its own
chevron so a dropdown reads identically across browser engines. A Button has
a primary, secondary, danger or link variant and a regular or small
size; the small size is the uppercase affordance a form’s secondary actions
use.
A control that sits beside its own action, such as a URL beside the button that
fetches it, is a ControlRow. The control takes the row’s spare width and the
row wraps when the two no longer fit, so the action drops under the control
instead of squeezing it.
A form lays its fields out one of two ways. A stacked field puts the label above
its control, and a grid of stacked fields reflows by the width available. A
SettingRow puts the setting’s name and description on the left and its control
on the right. The name column never shrinks below the width a label of a few
words needs, so the control column is what gives way when the row is cramped,
and below the medium breakpoint the control drops under its name at the row’s
full width. A form is
therefore usable at every viewport width from a phone to a desktop.
Numeric fields
Section titled “Numeric fields”A form’s numeric input holds what the operator typed, including nothing at all, so a value is replaced by clearing the field and typing. The form reports an empty or out-of-range field as invalid and refuses to submit it, and the backend validates the same bounds.
Submit outcomes
Section titled “Submit outcomes”Every save, launch and publish reports its outcome, covering the failure that stopped it and the progress and success of one that ran.
Dialogs
Section titled “Dialogs”Every question a destructive control asks is asked through the themed modal
rather than the browser’s own alert() and confirm(), so a destructive action
confirms before it runs and its question carries more than a line of plain text.
The dialog is modal and its confirmation is awaited at the call site. Both a confirmation and an alert may carry details beyond the question itself.
Theming
Section titled “Theming”Components are themed through the --tcab-* CSS custom properties. The
tokens.css entry supplies working defaults, and an app may override any
property in its own global styles. Every palette value comes from a token.