feat(annotator): class selection panel in the workspace side panel; top bar sheds the class field - #422
Merged
Merged
Conversation
…op bar sheds the class field The class selector leaves the top bar and becomes the upper region of the right-hand panel, above the objects list. Two stacked regions, no tabs and no draggable splitter: classes on top with a content-driven height — three rows' worth minimum, one row per class, eight maximum, then fixed with internal scroll — and objects below taking all remaining height with its own scroller. Position and design change, not behaviour: `c` focuses the class filter, Enter takes the first match, hotkeys 1-9 stay global and fixed to the first nine classes in schema order whatever the filter shows, selection stays sticky, geometry still switches the tool, and the create row still seeds the add-class dialog with what was typed. It also fixes the defect the field had regressed into. The top-bar instance was clipped by the `w-48 overflow-hidden` reservation #416 wrapped it in: the listbox is `position: absolute` at `top-9` inside a 32px-tall slot, so it rendered entirely outside the clip and never appeared, and the 256px open-state input was cut to 192px. The `Combobox` primitive is sound and untouched. With the field's 192px out of the cluster, the width-based reabsorption #419 introduced is reverted: Save and stay is back to `xl` and the review move to `lg`, so both are visible buttons at every supported width. Measured in chromium at 1440 — cluster 423px, each side offered 462px against 460px of demand. Closes #420
The panel carries `min-h-0` so its lower region can scroll inside a container of definite height, which is what it gets in the app. The demo's aside is a scrolling column, where `min-h-0` also licenses shrinking the panel below its content — the overflow then landed on the debug panels underneath and every scenario in e2e/panel.spec.ts failed with `<aside> intercepts pointer events`.
JArmandoAnaya
added a commit
that referenced
this pull request
Aug 21, 2026
…op bar sheds the class field (#422) * feat(annotator): class selection panel in the workspace side panel; top bar sheds the class field The class selector leaves the top bar and becomes the upper region of the right-hand panel, above the objects list. Two stacked regions, no tabs and no draggable splitter: classes on top with a content-driven height — three rows' worth minimum, one row per class, eight maximum, then fixed with internal scroll — and objects below taking all remaining height with its own scroller. Position and design change, not behaviour: `c` focuses the class filter, Enter takes the first match, hotkeys 1-9 stay global and fixed to the first nine classes in schema order whatever the filter shows, selection stays sticky, geometry still switches the tool, and the create row still seeds the add-class dialog with what was typed. It also fixes the defect the field had regressed into. The top-bar instance was clipped by the `w-48 overflow-hidden` reservation #416 wrapped it in: the listbox is `position: absolute` at `top-9` inside a 32px-tall slot, so it rendered entirely outside the clip and never appeared, and the 256px open-state input was cut to 192px. The `Combobox` primitive is sound and untouched. With the field's 192px out of the cluster, the width-based reabsorption #419 introduced is reverted: Save and stay is back to `xl` and the review move to `lg`, so both are visible buttons at every supported width. Measured in chromium at 1440 — cluster 423px, each side offered 462px against 460px of demand. Closes #420 * fix(demo): the showcase aside must not shrink the two-region panel The panel carries `min-h-0` so its lower region can scroll inside a container of definite height, which is what it gets in the app. The demo's aside is a scrolling column, where `min-h-0` also licenses shrinking the panel below its content — the overflow then landed on the debug panels underneath and every scenario in e2e/panel.spec.ts failed with `<aside> intercepts pointer events`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #420
The class selector leaves the top bar and becomes the upper region of the right-hand
panel, above the objects list.
Position and design, not behaviour:
cfocuses the class filter, Enter takes the firstmatch, hotkeys 1–9 stay global and fixed to the first nine classes in schema order
whatever the filter shows, selection stays sticky, geometry still switches the tool, and
the no-match row still seeds the add-class dialog with what was typed, with its session
semantics untouched.
The combobox diagnosis — the instance, not the primitive
Probed in chromium at
f49428bbefore anything was removed, because the symptomdisappears with the evidence. The numbers:
f49428bx=434 y=6 192×32,overflow: hiddenx=370 y=6 **256**×32x=370 y=**42** 256×148The only clipping ancestor between the listbox and the page is
DIV.flex.w-48.shrink-0.justify-end overflow=hidden— thew-48slot #416 wrapped thefield in. The mechanism is ordinary CSS and entirely the caller's: the listbox is
position: absoluteattop-9, so it begins 36px down a slot that is 32px tall and isclipped away in full.
document.elementFromPointat the listbox's own coordinates doesnot find it —
listVisibleToUser: false. The 256px input is clipped to 192px from theleft, which is why the placeholder read
…asses….So removal resolves it, and the
Comboboxprimitive is sound and untouched: it hasexactly one consumer in tree, its own dedicated test file, and no other caller inherits
anything. It stays exported.
ClassField.tsxis deleted with the slot — its only consumer was the bar, and itsonly coverage was through a page that no longer mounts it.
classChoicesgoes with it;the panel's list is schema order, and the recency ordering that function existed for is
deliberately not reproduced (see below).
The right zone, re-measured in chromium at 1440
On an
annotatedframe — the state that declaressubmit_for_review, which is theheavier of the two reabsorbable controls and the reason the zone overflowed at all:
more-actionsjob-progress=85 · save-and-stay=164 · submit-for-review=151 · more-actions=36, every oneat full width. So the breakpoints are reverted:
Save and stayback toxl,the review move back to
lg, both visible buttons at every supported width.It clears by one pixel, and that is stated rather than smoothed over. The margin is
thin, but the failure mode is not:
n / m annotatedcarriestruncate, so what gives wayunder pressure is the tail of a readout and never a button. Pinned by a new browser
scenario asserting both controls are visible and the overflow trigger is at its declared
36px.
The panel
Two stacked regions, no tabs and no draggable splitter.
ClassRegionisshrink-0andsizes itself in rows; the objects region is
min-h-0 flex-1and takes the rest. Twoscrollers, one per region.
The height rule is stated in rows, so
CLASS_ROW_PXis an explicith-9rather thanwhatever the content came to — a row whose height depended on a hotkey badge being drawn
would make the region eight-and-a-bit rows tall.
classListHeightis exported and pure,which is what stops the arithmetic becoming a second spelling of what the component does.
Computed from the schema's class count, never the filtered one. A height that tracked
the filter would resize this region — and reflow the objects region under it — on every
keystroke, which is the controls-moving-under-the-cursor problem #416 was spent on.
Hotkeys are
hotkeyForClassand nothing else: schema position, capped at nine, so thebadge on a row and the digit the input layer claims cannot disagree, and filtering does not
remap them. Past the ninth a row carries no chip — a key badge on a row the key does not
reach is the lie
ReassignMenurefuses to tell one column over.Read-only frames render the list. Which classes exist stays true on an accepted frame,
so the rows are there,
disabledand carryingtitlewith the page's own banner sentence(
withheld ?? settledBecause ?? …) — principle 9 rather than a grey box. This is a changefrom the field it replaces, which was not rendered at all while read-only.
ClassListRowwas extended, not duplicated —countbecame optional (omit it for thecompact single-line variant), plus
hotkey,refusalandtestId. The schema editor andthe annotator showing "the selected class" two different ways is exactly the drift worth
spending a prop to avoid.
Mutation verification
Implementation committed first; each anchor asserted present-and-unique before applying and
restored after by its exact diff.
Math.min(…, MAX_CLASS_ROWS)dropped)the height rule > stops growing at the cap and scrolls the surplus insteadthe height rule > gives a one-class schema three rows' worth of height, not oneshown.lengthinstead ofschema.classes.lengththe height rule > does not move when the filter narrows the listFound and fixed on the way
Skipis 104px andUn-skip96px, so a skipped frame pulled the whole centredcluster 4px sideways — pre-existing, and newly the only remaining width variable once
the class field's reservation left. Both now carry
min-w-27. 108px, not 104: afloor equal to the wider label is not a floor, because
Skipactually measures 104.09and clamping
Un-skipto 104 left 0.39px of drift — caught by a browser assertion, andinvisible to a person.
min-h-0so its lower region can scroll inside a container of definite height, which iswhat it gets in the app. The showcase's aside is a scrolling column, where
min-h-0also licenses shrinking it below its content — every scenario in
e2e/panel.spec.tsfailed with
<aside> intercepts pointer events. Fixed in the demo, in its own commit.Tests
classRegion.test.tsx(new, 10): the height rule at 1 / 5 / 8 / 12 classes, thefilter not moving it, the exported arithmetic, hotkeys past the ninth, and the refusal
rendering and refusing Enter.
topBar.test.tsx: the class-field describe is rewritten in the panel's terms —arming a row, the digits shown in schema order, the digits not remapped by a filter,
geometry switching the tool,
cfocusing the panel filter, Enter typeahead, the createrow, and the read-only refusal. Plus a new assertion that the cluster holds no instrument
sub-group.
e2e/annotate.spec.ts, 4 new): the regions stack and the objects regionreaches the panel's bottom; the two scrollers move independently and the header and
filter do not scroll away; a digit arms the schema-order class while the filter hides it;
both reabsorbable controls visible at 1440; the cluster is the same width and position
whichever resolution verb the frame offers. The Annotation top bar: consolidate image navigation into a centered cluster #416 centring scenario is unchanged and
still fails on a broken track — re-verified by mutation in that PR, and its 0.5px
tolerance is untouched here.
kept, relocated to the panel's testids.
DESIGN.md§"The annotation workspace": the top-bar enumeration, the class-field entry(now the classes region), and the side-panel paragraph (now the two-region spec with the
3/8-row rule and the no-splitter statement). No other section touched.
Test plan
bash scripts/check.shin stages, against the harness's ~10-minute ceiling. Exit codesverbatim:
@visionset/annotator863 passed (unchanged),@visionset/ui-core734 passed (was 720).No Python touched;
openapi.jsonand the generated client are byte-identical.Found, not fixed
readout truncates, no button clips — but a future control on that zone has no room, and
the next one to be added will need this measured again rather than assumed.