Files
soleprint/docs/data/en/ui-reference.md

24 KiB

common/ui — reference

Generated from common/ui/src by make reference in common/ui (python3 -m common.slice reference). Do not edit this page: the next build replaces it. Prose goes in soleprint/common/ui/notes/<spot>.md, and make reference-spots lists every spot.

What this is

A panel framework, not a dashboard: boxes with a title and a status light, splits that resize, a feed that pushes typed events, and renderers for what arrives. It was built for high-frequency operational views — a minimal Grafana — and every piece below exists because an app needed it.

The contract is the framework; Vue is one binding of it. The contract is four things: the token names, the element layer (base.css), the class names (.panel, .panel-header, .split-pane, …) and the status words. Those are not whole yet: a feed reports idle, connecting, live, error, while the panel's dot shows idle, live, processing, error — so connecting has no dot, and a consumer has to map it by hand. That is a gap in the contract, listed below. common/theme/parts is the second binding: the same contract as plain CSS and JS, for pages that must open from a file with no build. docgen's site is not a binding yet: it bakes its colours from its own style slots, a third token vocabulary.

The layers, as use shaped them

layer what the evidence
contract tokens, element layer, class names, status words every consumer reads tokens.css; four apps pasted their own base.css before it shipped here
chrome Panel (+ maximize), SplitPane, ParameterEditor, VideoPlayer Panel and SplitPane are the only exports all five consumers use; nested splits, not LayoutGrid, turned out to be the layout
feed DataSource, SSEDataSource one stream, typed events, a terminal done
renderers LogRenderer, TableRenderer, FrameRenderer, TimeSeriesRenderer, GraphRenderer opt-in; the only two external packages arrive through these
app state not here mpr keeps its own Pinia stores; pinia is imported nowhere in this library
delivery slices, not this whole folder see below

How to take it

Not as this folder. A consumer takes a slice: the closure of the exports it uses, as readable source, with a barrel exporting only those and a tokens.css cut down to what is read — make ui scan <app> computes it, ctrl/ui.sh slice <run file> writes it, and check reports drift both ways. The "Contents" table is the price list.

How this document is made

Everything outside these notes is generated from src/ by make reference (in common/ui), so a prop that is removed disappears from here on the next build. The notes are the only hand-written part, one file per spot in soleprint/common/ui/notes/; a note whose spot disappears fails make check. make docs — anywhere in soleprint — launches this site.

What came in

common/ui/src — 24 file(s) seen, 24 read, 0 failed; 19 exports and 15 types in the barrel.

Contents

Who uses what, measured 2026-09-22 with make ui scan <app> over the five apps that consume this library. It is the one table here that is not generated — the apps live outside this repository — so re-run the scan before trusting a row.

export mpr detection mts meetus mts doocus unt nvi
Panel, SplitPane ✓ ✓ ✓ ✓ ✓
LogRenderer ✓ ✓
VideoPlayer ✓
ParameterEditor, ResizeHandle, all renderers but Log, SSEDataSource, useRegistry, useEditorExecution ✓
LayoutGrid, StaticDataSource, useDataSource, BBoxDrawPlugin, CrosshairPlugin

The last row has no users at all. Those exports stay in the library — they are small and a slice never carries them unless asked — but nothing should be built on them as if they were proven. ResizeHandle is imported by mpr and never rendered: it is what SplitPane replaced.

What each export costs a consumer that takes it: the files it pulls in, the packages it brings, and whether a plain-HTML part exists for it (common/theme/parts).

export kind file pulls in packages size plain part
DataSource datasource datasources/DataSource.ts — vue 1.3 KB feed
SSEDataSource datasource datasources/SSEDataSource.ts DataSource.ts vue 3.8 KB feed
StaticDataSource datasource datasources/StaticDataSource.ts DataSource.ts vue 2.4 KB —
useDataSource composable composables/useDataSource.ts DataSource.ts vue 2.0 KB —
useRegistry composable composables/useRegistry.ts — vue 1.9 KB —
useEditorExecution composable composables/useEditorExecution.ts — vue 1.4 KB —
Panel component components/Panel.vue — vue 4.3 KB maximize, panel
LayoutGrid component components/LayoutGrid.vue — vue 0.5 KB —
ResizeHandle component components/ResizeHandle.vue — vue 1.3 KB —
SplitPane component components/SplitPane.vue — vue 3.7 KB split
ParameterEditor component components/ParameterEditor.vue — vue 3.1 KB params
VideoPlayer component components/VideoPlayer.vue — vue 4.1 KB —
LogRenderer renderer renderers/LogRenderer.vue — vue 3.7 KB —
TimeSeriesRenderer renderer renderers/TimeSeriesRenderer.vue — uplot, vue 4.3 KB —
GraphRenderer renderer renderers/GraphRenderer.vue — @vue-flow/core, vue 7.9 KB —
FrameRenderer renderer renderers/FrameRenderer.vue — vue 4.9 KB —
TableRenderer renderer renderers/TableRenderer.vue — vue 2.4 KB —
BBoxDrawPlugin plugin plugins/BBoxDrawPlugin.ts InteractionPlugin.ts — 3.0 KB —
CrosshairPlugin plugin plugins/CrosshairPlugin.ts InteractionPlugin.ts — 2.5 KB —

Components

Panel

The one component everything uses — 13 instances in mpr, 7 across mts's two apps.

Maximize came from mts. mts's copy grew v-model:maximized and maximizable while spr's stood still, and it was brought upstream on 2026-09-22 — the framework spec's own rule ("a second application finds the holes") working as written. The reason it is the panel's job and not the video's is in the source: a fullscreen <video> paints over the panel's actions and overlays.

#actions is where panel buttons go. mts fills it in 6 of 7 panels; mpr fills it in none and absolutely-positions buttons inside the body instead. The slot is the answer. #overlay is filled by no consumer; it stays for the interaction plugins, which have no consumer either.

components/Panel.vue · 4.3 KB with its closure · packages: vue ^3.5 · plain part: maximize, panel

prop type required default
title string yes — —
status 'idle' | 'live' | 'processing' | 'error' no — Omit to show no indicator at all — see the note on .panel-status.
maximizable boolean no true Set false for panels that make no sense filling the window.
v-model type default
v-model:maximized boolean false

slots: #actions, #default, #overlay

reads: --font-size-sm --font-ui --panel-border --panel-header-height --panel-radius --space-1 --space-2 --space-3 --status-error --status-idle --status-live --status-processing --surface-1 --surface-2 --text-primary --text-secondary

LayoutGrid

components/LayoutGrid.vue · 0.5 KB with its closure · packages: vue ^3.5

prop type required default
columns number no 2 —
rows number no 2 —
gap string no 'var(--space-2)' —

slots: #default

ResizeHandle

components/ResizeHandle.vue · 1.3 KB with its closure · packages: vue ^3.5

prop type required default
direction 'horizontal' | 'vertical' yes — —
emits payload
resize [delta: number] —

reads: --text-dim

SplitPane

The real layout primitive. Every app lays itself out with nested splits — mpr three deep — and none uses LayoutGrid. sizeMode="ratio" is the common case (all of mts); "px" pins a sidebar (mpr). resizable: false has never been passed by anyone: a split that cannot move is a flex row.

components/SplitPane.vue · 3.7 KB with its closure · packages: vue ^3.5 · plain part: split

prop type required default
direction 'horizontal' | 'vertical' no 'horizontal' Split direction
initialSize number no 1 Initial size of the sized pane (px or flex ratio)
sizeMode 'px' | 'ratio' no 'ratio' Size mode: 'px' = sized pane fixed in pixels, 'ratio' = flex ratio
anchor 'first' | 'second' no 'first' Which pane is sized: 'first' or 'second'. Default: 'first'.
min number no 0.1 Min size (px in px-mode, ratio in ratio-mode)
max number no 10 Max size (px in px-mode, ratio in ratio-mode)
resizable boolean no true Whether the divider is draggable

slots: #first, #second

reads: --text-dim

ParameterEditor

Schema-driven: the same field list a pipeline stage declares renders its sliders. Two gaps, both fixed in the plain binding (params part) and not yet here: a field with options (an enum) renders nothing, because only int/float and bool are filtered for; and the reset event is declared but never emitted — mpr wires a handler to it that cannot fire.

components/ParameterEditor.vue · 3.1 KB with its closure · packages: vue ^3.5 · plain part: params

prop type required default
fields ConfigField[] yes — —
values Record<string, unknown> yes — —
emits payload
update [name: string, value: unknown] —
reset [] —

reads: --space-2 --surface-3 --text-dim --text-primary --text-secondary

VideoPlayer

Agnostic seekable media player.

mts's, and only mts uses it. It came upstream with its #overlay slot on 2026-09-22. It deliberately does no fullscreen of its own: put it in a Panel and maximize that.

components/VideoPlayer.vue · 4.1 KB with its closure · packages: vue ^3.5

prop type required default
src string yes — Media source URL
currentTime number no 0 Playback position in seconds (v-model:currentTime)
playing boolean no false Whether the media is playing (v-model:playing)
emits payload
update:currentTime [value: number] —
update:playing [value: boolean] —
duration [value: number] Media duration once metadata has loaded
ended [] Playback reached the end

slots: #overlay

reads: --surface-0

Renderers

LogRenderer

Virtual scroll, and the only one in any of the apps: rows outside the viewport are not rendered, and auto-scroll pauses while the reader scrolls up. mts's long lists and mpr's React side render every row instead. rowHeight and autoScroll have never been passed by a consumer; the defaults are what is proven.

renderers/LogRenderer.vue · 3.7 KB with its closure · packages: vue ^3.5

prop type required default
entries LogEntry[] yes — —
rowHeight number no 24 —
autoScroll boolean no true —

reads: --font-mono --space-2 --space-6 --status-error --status-escalating --status-live --status-processing --text-dim

TimeSeriesRenderer

One of the only two exports that bring a package (uplot, with its stylesheet), and the reason a chart cannot be a plain part: baking uplot into every standalone page is the bundle this library refuses to be. A page that needs a real chart is a served page.

renderers/TimeSeriesRenderer.vue · 4.3 KB with its closure · packages: uplot ^1.6, vue ^3.5

prop type required default
series TimeSeriesSeries[] yes — Array of series configs (label + color)
data uPlot.AlignedData yes — Data: [timestamps[], series1[], series2[], ...]
title string no — Chart title (optional)
stacked boolean no false Stacked area mode

reads: --font-mono --surface-2 --surface-3 --text-primary --text-secondary

GraphRenderer

renderers/GraphRenderer.vue · 7.9 KB with its closure · packages: @vue-flow/core ^1.48.2, vue ^3.5

prop type required default
nodes GraphNode[] yes — —
mode GraphMode no 'observe' Interaction mode — changes visual treatment and click behavior
activeStage string | null no null Currently edited stage (highlighted in edit modes)
regionStages string[] no — Stages that have a region editor (bbox/polygon)
emits payload
open-region-editor [stage: string] —
open-stage-editor [stage: string] —
node-click [stage: string] —

reads: --font-mono --font-size-sm --panel-radius --status-processing --text-secondary

FrameRenderer

Known bug: box colours by confidence are returned as 'var(--conf-high)' strings and handed to a canvas strokeStyle. Canvas does not resolve custom properties, so those boxes draw in the context's previous colour. That is why the "Tokens" table shows the --conf-* tokens as read by nobody: in effect, they are not.

renderers/FrameRenderer.vue · 4.9 KB with its closure · packages: vue ^3.5

prop type required default
imageSrc string yes — Base64 JPEG image
boxes FrameBBox[] yes — Bounding boxes to overlay
overlays FrameOverlay[] no — Debug overlay layers (edge images, line visualizations, etc.)

reads: --text-dim

TableRenderer

renderers/TableRenderer.vue · 2.4 KB with its closure · packages: vue ^3.5

prop type required default
columns TableColumn[] yes — —
rows Record<string, unknown>[] yes — —
sortKey string no — —
sortDir 'asc' | 'desc' no — —
emits payload
sort [key: string] —

reads: --font-mono --font-size-sm --panel-border --space-1 --space-2 --space-3 --space-6 --surface-2 --surface-3 --text-dim --text-primary --text-secondary

Datasources

DataSource

datasources/DataSource.ts · 1.3 KB with its closure · packages: vue ^3.5 · plain part: feed

Base class for all data sources. A DataSource connects to some event stream, exposes reactive state, and lets consumers subscribe to typed events. Panels read from these reactively — they never touch the transport layer directly.

SSEDataSource

One stream, many typed events: panels subscribe with on(type, handler) to the event types they draw, and the stream ends with a terminal done. status uses the same four words as a panel's dot, so wiring one to the other needs no mapping.

Two things the code does not do, both learned from mpr. It does not throttle: at a few hundred events a second, a handler that writes to the DOM per event stalls — the plain binding's feed part adds onFrame() (one call per animation frame, newest payload) for exactly this, and a Vue consumer has to coalesce the same way. Nobody unsubscribes: on() returns an unsubscribe function and every call site in mpr drops it, which only works because its panels live as long as the page.

datasources/SSEDataSource.ts · 3.8 KB with its closure · packages: vue ^3.5 · plain part: feed

DataSource backed by native EventSource (Server-Sent Events). Connects to a single SSE endpoint and demultiplexes events by type. Multiple panels can subscribe to different event types from the same source.

StaticDataSource

datasources/StaticDataSource.ts · 2.4 KB with its closure · packages: vue ^3.5

DataSource that replays a fixture array of events. Used for development and testing without a running backend. Events are emitted in sequence with optional delays.

Composables

useDataSource

Composable that connects a component to a DataSource.

composables/useDataSource.ts · 2.0 KB with its closure · packages: vue ^3.5

Composable that connects a component to a DataSource. Connects on mount, disconnects on unmount. Returns reactive refs for data, status, and error.

useRegistry

Generic registry composable — fetches typed data from a URL, caches it, exposes it reactively.

composables/useRegistry.ts · 1.9 KB with its closure · packages: vue ^3.5

useEditorExecution

composables/useEditorExecution.ts · 1.4 KB with its closure · packages: vue ^3.5

Generic editor execution pattern — debounced apply with auto-apply toggle, loading/error/timing state tracking. The caller provides the actual execution function. This composable handles the orchestration: debounce, auto-apply, loading state, timing.

Plugins

BBoxDrawPlugin

BBoxDrawPlugin — draw bounding boxes on the frame viewer.

plugins/BBoxDrawPlugin.ts · 3.0 KB with its closure · packages: none

CrosshairPlugin

CrosshairPlugin — synchronized vertical crosshair across time-series panels.

plugins/CrosshairPlugin.ts · 2.5 KB with its closure · packages: none

Types

Types ride with the file that declares them: taking the export takes its types.

type declared in
DataSourceStatus datasources/DataSource.ts type
EditorExecutionOptions composables/useEditorExecution.ts interface
ConfigField components/ParameterEditor.vue interface
FrameBBox renderers/FrameRenderer.vue interface
FrameOverlay renderers/FrameRenderer.vue interface
LogEntry renderers/LogRenderer.vue interface
GraphNode renderers/GraphRenderer.vue interface
GraphMode renderers/GraphRenderer.vue type
TableColumn renderers/TableRenderer.vue interface
TimeSeriesSeries renderers/TimeSeriesRenderer.vue interface
InteractionPlugin plugins/InteractionPlugin.ts interface
PluginContext plugins/InteractionPlugin.ts interface
BBoxResult plugins/BBoxDrawPlugin.ts interface
BBoxCallback plugins/BBoxDrawPlugin.ts type
CrosshairCallback plugins/CrosshairPlugin.ts type

Tokens

There are two definitions, and the ⚠ rows are where they disagree. src/tokens.css is the library's own, dark-only; the theme contract in common/theme derives the same names from its own palette. They agree on every colour but --surface-3 and on neither font stack. A Vue slice uses the library's values by default so swapping a consumer's copy for a slice changes no pixel; theme = "soleprint" opts into the contract, and that is a visible change. Retiring the library's file in favour of the contract is the open question — it changes what every current consumer renders.

"read by nobody" means no stylesheet in the library reads it. Apps read tokens directly too — make ui scan reports between 14 and 23 per app — and a slice carries those.

Every custom property the library defines or reads. library is src/tokens.css, what Vue consumers render today; theme is the contract plain pages use (common/theme/tokens.css + themes/soleprint.css). A row where the two differ is a visible change for a consumer that switches.

token library theme read by
--border #2e2e38 #2e2e38 tokens.css
--conf-high #3ecf8e #3ecf8e nobody
--conf-low #f06565 #f06565 nobody
--conf-mid #f5a623 #f5a623 nobody
--font-mono ⚠ 'JetBrains Mono', 'Fira Code', monospace "JetBrains Mono", "Cascadia Mono", Consolas, monospace GraphRenderer.vue, LogRenderer.vue, TableRenderer.vue, TimeSeriesRenderer.vue
--font-size-base 13px 13px base.css
--font-size-lg 15px 15px nobody
--font-size-sm 11px 11px GraphRenderer.vue, Panel.vue, TableRenderer.vue
--font-ui ⚠ 'Inter', system-ui, sans-serif Inter, "Segoe UI", system-ui, -apple-system, Arial, sans-serif Panel.vue, base.css
--panel-border 1px solid #2e2e38 1px solid #2e2e38 Panel.vue, TableRenderer.vue, base.css
--panel-header-height 36px 36px Panel.vue
--panel-radius 6px 6px GraphRenderer.vue, Panel.vue, base.css
--space-1 4px 4px Panel.vue, TableRenderer.vue, base.css
--space-2 8px 8px LogRenderer.vue, Panel.vue, ParameterEditor.vue, TableRenderer.vue
--space-3 12px 12px Panel.vue, TableRenderer.vue, base.css
--space-4 16px 16px nobody
--space-6 24px 24px LogRenderer.vue, TableRenderer.vue
--space-8 32px 32px nobody
--status-error #f06565 #f06565 LogRenderer.vue, Panel.vue
--status-escalating #f5a623 #f5a623 LogRenderer.vue, tokens.css
--status-idle #555568 #555568 Panel.vue
--status-live #3ecf8e #3ecf8e LogRenderer.vue, Panel.vue, tokens.css
--status-processing #4f9cf9 #4f9cf9 GraphRenderer.vue, LogRenderer.vue, Panel.vue, tokens.css
--surface-0 #0d0d0f #0d0d0f VideoPlayer.vue, base.css, theme.css
--surface-1 #16161a #16161a Panel.vue
--surface-2 #1e1e24 #1e1e24 Panel.vue, TableRenderer.vue, TimeSeriesRenderer.vue, base.css
--surface-3 ⚠ #26262f #2e2e38 ParameterEditor.vue, TableRenderer.vue, TimeSeriesRenderer.vue, base.css
--text-dim #555568 #555568 FrameRenderer.vue, LogRenderer.vue, ParameterEditor.vue, ResizeHandle.vue, SplitPane.vue, TableRenderer.vue
--text-primary #e8e8f0 #e8e8f0 Panel.vue, ParameterEditor.vue, TableRenderer.vue, TimeSeriesRenderer.vue, base.css
--text-secondary #8888a0 #8888a0 GraphRenderer.vue, Panel.vue, ParameterEditor.vue, TableRenderer.vue, TimeSeriesRenderer.vue

What came out

nodes: 1 barrel · 6 component · 3 composable · 3 datasource · 19 export · 3 package · 6 part · 3 plugin · 5 renderer · 3 stylesheet · 30 token · 15 type

edges: 7 binds · 28 imports · 70 reads · 17 requires

  • ok 24 file(s) read produced 24 file node(s)
  • ok 19 export(s) and 15 type(s) in the barrel, each with a node
  • ok 11 of 11 component(s) have their surface read
  • ok every note lands on a spot

Where it stands

  • Done: the reference is generated and checked; slices are cut per consumer with a two-way drift check; mts's additions (maximize, the video overlay) are upstream.
  • Behind: the copies in mpr, unt and nvi date from 2026-04 and lack maximize, VideoPlayer, base.css and theme.css. Their .spr stamp names a commit that no longer exists. Replacing each with a slice is the next step for each.
  • Open: one token definition instead of two (three, counting docgen's style slots); the connecting status the panel cannot show; --accent, which nvi and doocus read and nothing defines; the FrameRenderer colour bug; ParameterEditor's enum fields.

Rebuilding this

make reference in common/ui. make check fails when this page is stale, when a generated file was edited by hand, or when a note no longer has its spot. Extraction reads the source with patterns, not a TypeScript parser; common/slice/selftest.py pins what the patterns must find, so one that stops matching fails the suite rather than thinning this page.