Skip to content

feat(annotator): one notice surface top-right, and the forward actions become a pair - #484

Merged
JArmandoAnaya merged 1 commit into
mainfrom
feat/editor-notice-and-forward-bar
Aug 10, 2026
Merged

feat(annotator): one notice surface top-right, and the forward actions become a pair#484
JArmandoAnaya merged 1 commit into
mainfrom
feat/editor-notice-and-forward-bar

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

Closes #481.

What changed

One notice surface. frontend/ui-core/src/annotator/EditorNotice.tsx is a new
positioned column at the top-right of the stage, inset 16px (the md step) from its top
and right edges, over the picture at z-20. Everything the editor floats over the stage
now comes out of it:

Message Was Is
suggest session (six states) a card bottom-16 right-3, max-w-80 a notice in the column
a refused save save-state, a destructive badge inside the top bar's microtext save-refusal
a refused batch/job opening opening-refusal, a second badge beside it opening-refusal, in the column
a refused Skip / Un-skip / Accept / Finish job action-refusal, a full-bleed strip under the header action-refusal, in the column

The column is a stack, most-blocking first, because more than one can be true at
once — a suggest session is live and the save just refused. A single slot would have to
choose, and choosing means hiding a refusal behind a spinner. pointer-events-none on
the column with pointer-events-auto on each card, so an empty column is not an
invisible strip eating drags down the right edge of the canvas.

Every data-testid and every title (the kernel code, where a bug report can quote it)
is unchanged, so the existing refusal assertions in annotate.spec.ts still address the
same messages.

Width and wrapping. max-w-md (448px) and wrap-anywhere on the body. Width is
stated as a measurement in the module: at a 1280px viewport the rail takes 240px, the
editor's row spends 24px of padding and a 12px gap, and the side panel is a fixed 288px,
so the stage is 716px; inset 16px, a 448px card's left edge lands at 252px against a tool
strip that ends at 60px — roughly 190px of clearance, which is what "clear of the tool
strip" has to mean for a surface that may grow a line. 448px also fits a full
owner/model@revision on one line at text-meta, which 320px could not.

The wrap is the part that actually fixes it. A model reference is one unbroken token and
no fixed width guarantees the next one fits, so wrapping is the invariant and the
width is comfort. Asserted with a 120-character token in chromium.

The forward-action pair. The resolve group reads Skip · [primary next-action] · Save and stay. Save and stay is filled success (bg-success / text-success-foreground
/ hover:bg-success-hover, all tokens — no literal), it has lost the ⌘S keycap chip,
and the chord is taught by its tooltip instead. Its reabsorption is unchanged: still the
first control the bar gives up below xl, still with the exact inverse breakpoint on the
overflow row. It now leaves with the frame verbs once the job is closed — a filled
control that could never fire is a fill with nothing behind it — and the overflow copy
leaves with it so the control exists in one state, not two.

Tokens. success-hover (#3a896b) and success-foreground (#ffffff) join
styles.css and tokens.ts. The hover is derived rather than picked: primary-hover
lifts primary by (+12, +12, +16) per channel, and the same deltas applied to success
give this, so the bar's two filled controls brighten by the same amount under a pointer.
tokens.test.ts's both-directions parity is green.

DESIGN.md. One filled button per view gains the recorded exception with its
reasoning; the count test is restated as two counts rather than relaxed into none. The
annotation-workspace top-bar prose is rewritten for the shipped bar (zone table, the
Save-and-stay paragraph, the hotkey-chip rule, the notice surface, the three-reading save
state).

Step 0.3 — does Next persist before advancing?

Yes, and the label is already the honest one, so A3's relabel is a no-op:
AnnotationPage's flow verb is go(1)goToattemptcommit
save.mutateAsync(plan) when dirty, which is the same save-first path the navigator's
, the gallery tiles and the back arrow all use. The label is already derived —
!readOnly && (dirty || drawn > 0) ? "Save and next" : "Next" — which is a stronger
version of what A3 asks for: the button never promises a save it will not perform, so it
reads Next on an untouched frame rather than claiming a save that would not happen.
No change was made to it.

Stop-and-flag inventory: sonner

The condition did not fire. The editor renders no error through a toast: the only two
toast calls on AnnotationPage are toast.success (a class added, a job finished), and
grep finds no toast.error anywhere in ui-core or app. Nothing had to be moved off
a disappearing surface, and no design call about which surface owns which error class was
needed.

Tests

New:

  • frontend/ui-core/src/annotator/editorNotice.test.tsx — the unification as a
    containment check: the column is inside canvas-stage and not inside the header, and a
    refused progress move, a refused opening and the suggest session all land in it. Also
    that the bar's microtext says unsaved rather than a refusal, and that no refusal
    testid survives inside <header>.
  • topBar.test.tsxthe forward-action pair — document order inside the cluster is
    skip, save-and-next, save-and-stay; exactly one bg-primary and exactly one
    bg-success on the bar, on a middle frame and on the last frame; both copies leave
    when the job closes. The harness gained a closedBatch toggle for the last of those.
  • annotate.spec.ts — three chromium scenarios: the 16px inset measured against the
    stage's inner edge and clear of both occupied corners; a 120-character model reference
    wrapping with scrollWidth <= clientWidth, the card inside the stage and the page not
    scrolling sideways; the tooltip carrying Save and stay (⌘S | CtrlS) on hover with no
    kbd left in the button. The stub gained a refuseSave lifecycle option — a code with
    no REFUSAL_PROSE entry falls through to the server's message, which is the only way to
    put an arbitrary token on screen.

Changed: topBar.test.tsx's chip assertion now says neither fill carries one.

Geometry is deliberately not asserted in jsdom — getBoundingClientRect, scrollWidth
and clientWidth all answer zero there, so a component test for "the notice does not
overflow" would pass with the wrap rule deleted and the anchor moved back.

Mutation verification

Each new rule was broken and the guard that names it went red. Anchors asserted unique
before patching and present after; each reverted by its own recorded diff (git apply -R),
never by checking out a path. The work was committed first, so a revert could not take the
implementation with it.

Mutation Guard Result
notice anchor back to bottom-16 right-3 anchors top-right of the stage (chromium) red — inset read 674 instead of 16
wrap-anywhere removed from the body wraps a model reference no fixed width could have fitted red
variant="success"variant="ghost" leaves exactly one primary fill and exactly one success fill red
the ⌘S keycap chip re-added carries no hotkey chip, and neither does the filled control beside it red
action-refusal renamed out of the notice column is where a refused progress move lands red

The chip mutation aborted on its first spelling — the harness found three occurrences of
the anchor and refused to patch ambiguously rather than silently patching the wrong one.
Re-run with a unique three-line anchor; red as above.

Local gate

Staged against this box's ~10-minute command ceiling, pytest split by directory derived
from ls tests/ at run time. Every stage's exit code:

Stage Exit
pytest tests/architecture 0
pytest tests/cli 0
pytest tests/examples 0
pytest tests/fixtures 0
pytest tests/formats 0
pytest tests/inference 0
pytest tests/jobs 0
pytest tests/kernel 0
pytest tests/mcp 0
pytest tests/packaging 0
pytest tests/scripts 5
pytest tests/server 0
pytest tests/test_versioning.py 0
ruff check . 0
ruff format --check . 0
mypy src/visionset/kernel 0
lint-imports 0
check.sh frontend generated 0
check.sh browser 0 — 237 e2e passed (was 234), 1 cycle passed

tests/scripts exits 5 by design — nothing pytest-shaped lives there; it is node --test
and runs under check.sh generated.

Found, not fixed

  • The viewport resets after every save. Filed as Saving resets the editor viewport (zoom/pan) #482 and fixed in its own PR — the
    mechanism is AnnotatorCanvas's initial-fit layout effect re-firing on a document
    object the save replaces. Untouched here.

…s become a pair

Every sentence the editor floats over the stage now comes out of one anchor
(`EditorNotice`), inset 16px from the stage's top and right edges. It replaces
four placements in three treatments: two destructive badges inside the top bar's
microtext, a full-bleed strip under the header, and the suggest card
bottom-right, which had been clearing the zoom cluster with a hard-coded offset.
The column is a stack, most-blocking first, because a suggest session and a
refused save can both be true at once.

Its body wraps mid-token and its width is `max-w-md`. A model reference is one
unbroken string, so no fixed width guarantees the next one fits: wrapping is the
invariant and the width is comfort.

`Save and stay` moves to the resolve group, immediately after the primary
next-action, filled in `success`. Advance and persist-in-place are two halves of
one gesture and were a bar apart, the second of them the quietest control on the
row. `DESIGN.md` records the two-fill exception, and the count test now sweeps
both fills rather than one. The `⌘S` keycap goes — a chip inverts into a smudge
inside a fill — and the chord is taught by the button's tooltip instead.

The top bar's save state keeps three readings, not four: it says where the work
is, and after a refused save the honest answer there is `unsaved`.
@JArmandoAnaya
JArmandoAnaya merged commit 5f2c61c into main Aug 10, 2026
13 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the feat/editor-notice-and-forward-bar branch August 10, 2026 04:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Editor notices anchor top-right; forward-action bar gains a success-filled "Save and stay"

1 participant