From 2d477799d54cc99980b988d4a5e51202d589acea Mon Sep 17 00:00:00 2001 From: YaelAnaya Date: Wed, 19 Aug 2026 16:27:54 -0700 Subject: [PATCH 1/3] docs(ui): relocate product, navigation and annotator rules out of DESIGN.md --- docs-site/src/sidebar.mjs | 10 +- docs/README.md | 14 +- docs/ui/annotator.md | 484 ++++++++++++++++++++++++++++++++++ docs/ui/navigation.md | 90 +++++++ docs/ui/product-principles.md | 286 ++++++++++++++++++++ 5 files changed, 879 insertions(+), 5 deletions(-) create mode 100644 docs/ui/annotator.md create mode 100644 docs/ui/navigation.md create mode 100644 docs/ui/product-principles.md diff --git a/docs-site/src/sidebar.mjs b/docs-site/src/sidebar.mjs index c41710a7..79934f85 100644 --- a/docs-site/src/sidebar.mjs +++ b/docs-site/src/sidebar.mjs @@ -91,7 +91,15 @@ export const sidebar = [ { slug: "api" }, { slug: "auth" }, { slug: "cli" }, - { slug: "ui" }, + { + label: "UI", + items: [ + { slug: "ui" }, + { slug: "ui/product-principles" }, + { slug: "ui/navigation" }, + { slug: "ui/annotator" }, + ], + }, { slug: "mcp" }, { slug: "mcp-tools" }, { slug: "mcp-walkthrough" }, diff --git a/docs/README.md b/docs/README.md index 84185f06..1163e029 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,10 +9,13 @@ top down. It covers the layer stack, each package, and the enforcement points fo the architectural boundaries. The pages below are the behavioural reference; the architecture tree is the map. -The **visual** contract is [DESIGN.md](../DESIGN.md) at the repository root. Read -it *before* building any screen; `frontend/ui-core/src/styles.css` implements that -contract. Read it alongside -[annotations.md](annotations.md), which covers the annotator itself. +The **visual** contract is [DESIGN.md](../DESIGN.md) at the repository root — the +design foundations every screen builds on. The product-behaviour half lives here: +[ui/product-principles.md](ui/product-principles.md) for what a screen must show and +offer, [ui/navigation.md](ui/navigation.md) for how a person moves through the app, +and [ui/annotator.md](ui/annotator.md) for the annotation workspace. Read them +*before* building any screen, alongside [annotations.md](annotations.md), which +covers the annotator's kernel-side behaviour. If you are new to VisionSet, start with [install.md](install.md), then continue to [tutorial.md](tutorial.md). @@ -43,6 +46,9 @@ If you are new to VisionSet, start with [install.md](install.md), then continue | [mcp-tools.md](mcp-tools.md) | The complete tool listing, **generated** from the server's own descriptions so it cannot drift from what an agent is told | | [mcp-walkthrough.md](mcp-walkthrough.md) | A session over MCP, start to finish: the cycle in the order an agent meets it, and then what twelve real agent runs did with it - where the coordinate frame held, how refusals were read, and the two pieces of friction that changed the tools | | [ui.md](ui.md) | The browser client: why no screen calls `fetch`, how a refusal is read (branch on `code`, and the two codes the client adds), where the token is kept and the three alternatives that were rejected, why a 401 is handled in one subscription, the loading/empty/error component, polling, and the dev proxy that keeps CORS out of production | +| [ui/product-principles.md](ui/product-principles.md) | VisionSet's product UI rules: data first, density on data surfaces, action-forward screens, never disabling without explanation, the product-wide status mapping, number formatting, list and section rules, and the screen rules for Home and the project view | +| [ui/navigation.md](ui/navigation.md) | How a person moves through the app: ancestor chains and breadcrumbs, what the rail carries, what belongs in the URL, and why the browser's Back button stays correct | +| [ui/annotator.md](ui/annotator.md) | The annotation workspace UX contract: the top bar's three zones, the side panel's three regions, the tool strip, the frame gallery, zoom and the device model, the read-only mode, the stage, and how shapes render | | [releasing.md](releasing.md) | Cutting a release: what ships, why the beta goes to PyPI as a pre-release, the npm scope, the order the steps have to happen in, and the one step that needs credentials this repository does not hold | | [inference.md](inference.md) | Where models run: the connections auto-labeling is configured against, why nothing is ever downloaded on your behalf, the two kinds and the parameters each carries, why the model revision is pinned, how weights are fetched and where they land, the `local-inference` extra, and why deleting a connection never touches the provenance on a label | | [cli.md](cli.md) | The command line: the whole cycle as a script, the three exit codes (and why one of them also means "no"), why stdout is data and stderr is prose, what `--json` promises and how it stays the API's shape, why `--workspace` follows the subcommand, and what `visionset init` and `visionset server` each do | diff --git a/docs/ui/annotator.md b/docs/ui/annotator.md new file mode 100644 index 00000000..d9c7cfe5 --- /dev/null +++ b/docs/ui/annotator.md @@ -0,0 +1,484 @@ +# The annotation workspace + +The UX contract for the one screen somebody sits in for an hour. The data flow behind it — +queries, saving, review moves, the suggest tool's server side — is +[`docs/ui.md`](../ui.md); the kernel's rules for the labels themselves are +[`docs/annotations.md`](../annotations.md); the visual foundations are +[`DESIGN.md`](../../DESIGN.md). This page describes the workspace as the current +implementation renders it, using the current implementation's token names. + +## The workspace is self-sufficient + +No flow may force navigation out of the editor, and no exit may lose work. Back saves +first; a class the schema lacks is created from the class list without leaving the page; +looking at the job's other frames is an **overlay, not an exit** — the grid button opens a +gallery over the workspace and the URL does not move. Every trip out of the editor is a +trip back through a list, a tab and a scroll position to the frame you were looking at, +which is why this rule is immovable. + +## The top bar + +One 44px row on `card` with a bottom hairline, 32px controls, in **three zones** — *where +you are*, *what changes the frame*, *the session*: + +| Zone | Contents | +| --- | --- | +| Left | back · pinned `v{n}` badge · the frame's identity as a label (the content-hash head — there is no filename on the wire) · the frame microtext `● annotated · Saved` | +| Centre | the **navigation cluster**: `[⊞] [‹] n/m [›] │ [Skip] [Save and next] [Save and stay]` | +| Right | `n / m annotated` · the review move (outline) · overflow `⋯` | + +**Everything that changes the picture on screen is in the centre cluster, and nothing else +is.** One hairline divider separates the two sub-groups — **browse | resolve** — so the +difference between *look at another frame* and *finish this one* is adjacency rather than +something learned. `‹` `›` browse: they move without settling progress, under the same +save-first guard as back and the gallery. `n/m` renders between them in tabular figures, so +walking a job does not shuffle the arrows under a cursor that has not moved. + +**The centre is anchored on the bar's geometric centre** — the header is a `1fr auto 1fr` +grid — and the side tracks yield: a label truncates, never a control. Two widths inside the +cluster are pinned so it is the same size on every frame: the resolution pair's minimum +(covering `Skip` and `Un-skip`) and the flow verb's minimum (the widest of `Next` / +`Save and next` / `Finish job` / `Finished`). The readout that gives way is a readout: +`n / m annotated` truncates, and no button is ever clipped. + +**The dominant slot is the flow verb.** After finishing a frame the right move is *this one +is done, show me the next* — the navigator's `›` is chrome rather than a verb. **Skip and +Save and next are siblings** — two ways to resolve this frame, skipped or annotated, both +advancing — and neither ever collapses into the overflow. + +- `Save and next` is the same save-first advance the navigator uses, so there is one save + pipeline and one place the no-lost-work rule is enforced. It reads **`Next` when no save + will happen** — an untouched frame — because the button never promises a save it will not + perform, and once the job is closed it is not rendered at all: there is no save-first + advance to offer, and `›` is what moves there. +- **On the last frame `Finish job` takes the dominant slot**, in place: `Save and next` is + not rendered there, and Finish job is not rendered anywhere else. Where it renders and + cannot be pressed, it carries why. The consequence is worth stating: `complete` is + reachable from the last frame only. +- **The review move is an outline control**, chosen from the frame's own `allowed_actions`: + `submit_for_review`, else `accept` — mutually exclusive by construction. `complete` is + deliberately not ranked against them: it is the *job's* action, and it co-declares with + `submit_for_review` on the commonest path, so ranking them would hide Finish job exactly + where most jobs end. Submitting carries a tooltip saying what it means, because this + product has no annotator identity — a submitted frame is marked for a review pass, not + routed to a person. +- **Save and stay is the second half of the forward gesture, and it sits beside the + first** as the third member of the resolve group — `Skip · [flow verb] · Save and stay`. + *Advance* and *persist in place* are one decision read two ways, so they are adjacent + rather than a bar apart. In the current implementation the flow verb is filled in + `primary` and Save and stay is filled in `success`; colour is what separates their + intent, since a second `primary` beside the first would read as a bar that could not + decide. It keeps the frame verbs' lifetime rather than the mode's: a closed batch or a + finished job has nothing to save on any frame, so it leaves with Skip and the flow verb; + inside a working job it holds its slot, disabled, so the cluster does not change width as + somebody walks a mixed job. +- **Reabsorption order when the bar runs out of room**: Save and stay first (below `xl`), + the review move second (below `lg`), into the overflow; the Skip/Save-and-next pair never + collapses. Each reabsorbed control carries the exact inverse of its button's breakpoint, + so it exists in exactly one place at any width. +- **Hotkey chips go on the ghost and outline controls and on nothing else** — `X` on Skip, + from `core/input/bindings.ts`. A chip is a lighter-than-the-surface treatment: inside a + filled control it inverts and reads as a smudge rather than as a key, so neither filled + control carries one. `⌘S` is taught by Save and stay's tooltip — the tool strip's own + pattern (`Box (B)`) — and `enter` is the one key with two meanings, the polygon ring + close while a shape is in progress and *finish the frame* otherwise. Both are in the + shortcut sheet, which derives its rows from the live binding registry rather than from a + hand-written table. +- The bar carries the frame microtext `● annotated · Saved`, which says *where the work + is*. The word is on the bar beside the save state because **status is never colour + alone**, and a tooltip is a place a word goes to not be read. After a refused save the + honest answer there is `unsaved`, and the reason is a sentence in the notice column. + +## In-editor messages, and waits + +**In-editor messages have one surface, top-right of the stage** (`EditorNotice`). Every +sentence the editor floats over the picture goes into one column inset 16px from the +stage's top and right edges: a suggest session, a refused save, a refused progress move, +and a batch or job that could not be opened. Top-right is the corner nothing else occupies: +the tool strip is top-left, the object counter bottom-left, the zoom cluster bottom-right. +The column is a stack, most-blocking first, because more than one of those can be true at +once. Its body wraps mid-token — a model reference is one unbroken string and no fixed +width guarantees the next one fits, so wrapping is the invariant and the width is comfort. + +**A wait is reported as soon as it starts, and once reported it stays for at least 250ms.** +Work that has begun and shows nothing is the state in which *working* and *broken* look +alike, so the trigger is the request leaving. The floor is the asymmetry: appearing is +free, and disappearing after two frames is the glitch. Where a wait has a second threshold +worth crossing, what appears is **prose**, never a second indicator. The worked example is +the suggest tool: the panel says `Looking at that…` from the moment the request is +dispatched, and past 1.5s adds a sentence saying that the first click on a frame is the +slow one. + +**A wait is reported in one place, and never at the cursor.** An indicator sitting on the +picture, next to the pointer, reads as the machine having *seized* rather than as work in +progress — it is in the way of the thing being looked at, and it moves with the hand. The +card is out of the way, says the same thing in words, and is where every other answer about +the tool already appears. **An indicator is never brand-coloured**: quiet neutrals, at low +opacity, at the place the person is already looking. + +## The classes region + +Class selection is a **list** in the side panel's upper region — what is being chosen +between is the ontology, and a list keeps all of it one click away, so *what can I draw +here* is always on screen. Rows carry swatch · name · geometry chips · hotkey badge, in the +**schema's authored order and only that**: a persistent list that reordered itself by +recency would move rows under the cursor, and the digits are schema positions. `c` focuses +its filter, Enter takes the first match, digits 1–9 activate directly, and the derived tool +follows the class. When nothing matches what was typed the last row is +`Create class ""`, which opens the add-a-class dialog on that name; an empty schema +renders an invitation instead of an empty list. **It shows the drawing class and never +follows the selection** — re-classing an existing annotation is an object row's menu, a +different question about a different object. On a frame nothing can be drawn on, the list +still renders — which classes exist stays true there — with every row disabled *and +carrying why*. + +**The drawing class's lifetime is the job**, not the frame: it survives moving to the next +asset, because somebody labelling one class across a clip picks it once, and it survives a +re-pin. It stops at the job's edge, the same scope the clipboard has and for the same +reason — a paste and a drawing class both belong to one pinned schema. + +**Every row's shapes are chips, and every chip is a press target.** A class accepts a *set* +of geometries, so arming a class does not pick the shape by itself. One chip per +**drawable** geometry, labelled with the geometry **word** — glyphs are not +self-describing at chip size on the one row whose job is telling shapes apart. What a press +does depends on the row, and the two readings are one rule — *this class, this shape*: + +- On the **armed** row the active chip is lit and pressing another switches the tool and + **never the class** — changing shape must not move somebody's labels to a class they did + not choose. +- On an **unarmed** row no chip is lit, and pressing one arms the class *with that shape*, + in one press. +- Pressing the **name** arms the class with its first drawable shape. + +The cost is that a row carrying chips is a `role="group"` with an inner name button rather +than one row-wide `