Overview
The UI library (@clockwyrks/ui, in packages/ui) is the shared frontend code
for The Test Cabinet’s two GUIs: the public
site and the web
console. It hosts the entire routed gallery
application plus the primitives those GUIs render, so both are thin hosts over
one application.
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.
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 web 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.
The data and capability context
Section titled “The data and capability context”Each host builds the gallery data value from its own source. The static site builds it from the build-time public snapshot. The web console builds it live from a backend.
A canExecute flag on that value gates the run-execution surface, including
authentication 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 site resolves these from snapshot assets, a console from the
backend for a published run and from the artifact
service for a produced one.
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 site resolves them from the snapshot’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 site answers it from its in-memory summary index 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. A view widened across engines lists each engine separately rather than folding them, since runs under different engines measure different work.
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.
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
authentication context. The app depends only on these interfaces.
./transport is the single implementation of the backend wire protocol: the
HTTP backend and execution clients, the HTTP arena client, and the helpers that
read the artifact, arena, snapshot, and Grafana URLs the backend reports from
GET /config. The web 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.