Serves: #18553 — this is that ticket's AC-1, which its own body marks "Independently shippable, and should land first." Splitting it replaces that scope rather than adding to it: AC-1 is struck through on #18553 and pointed here.
Context
Raised by @tobiu against the Workstation topology bar — first as "the undo and redo buttons should indicate how many steps there are", then, against a screenshot of the bar, "undo and redo look a bit unstyled (ugly) => missing icon colors, and i really do not like the disabled state gray. for step counts => neo buttons do support badges."
The depth half and the paint half arrived as two complaints. They are one lane because the same two controls carry both, and because the second is a prerequisite for the first: a badge on a control the app never finished styling inherits the platform's colours rather than the product's.
The Problem
The controls say only whether they can be pressed. Enabled-ness cannot answer "how far back does this go", so a user presses hopefully and finds out by pressing — on the one control whose whole purpose is to make an action reversible.
The paint half is a single omission with two visible symptoms. resources/scss/src/apps/workstation/Workspace.scss sets five --button-* tokens: background, background-hover, border, height, text-color. No glyph token and no disabled token. So the icons carry no themed colour, and the disabled state falls through to the platform.
The Architectural Reality
⚠️ Two of the three pieces are readouts of data and tokens that already exist. Nothing here needs a new publisher.
The depth is already published. Neo.manager.Transaction#historyState emits canUndo · canRedo · historyCursor · historyDepth · historyLength to every window bound into the Group. apps/workstation/view/Workspace.mjs#createTopologyBar currently binds only disabled off that same leaf. cursor starts at -1 and canUndo is cursor > -1, so the steps behind the cursor are cursor + 1; canRedo is cursor < count - 1, so the steps ahead are count - 1 - cursor.
The widget already exists. src/button/Base.mjs ships badgeText_ / badgePosition_ rendering a .neo-button-badge node, and afterSetBadgeText sets removeDom from the value — so a null count removes the badge rather than painting a 0.
The engine documents the disabled half and assigns it to the consumer. resources/scss/src/button/Base.scss keeps color inheriting at the button root so the child glyph and text nodes can own themed colours; its own comment records that projecting the native disabled attribute activates the UA button:disabled cascade and that Chromium paints GrayText into the gap. Every token needed already exists: --button-glyph-color[-hover|-disabled], --button-text-color-disabled, --button-background-color[-disabled], --button-border-disabled, --button-opacity-disabled, --button-badge-background-color, --button-badge-color. The grey is not an engine defect and must not be repaired there.
One cascade trap sits in the way of the badge. .neo-toolbar paints its padding from --toolbar-padding; .workstation-topologybar declares a plain padding, which loses to it at equal specificity on source order. The bar therefore runs at the shared 5px, it is flush against the window's right edge, and the trailing control's badge — offset -10px outward by --button-badge-margin-left — lands past the viewport.
The Fix
- Bind
badgeText on both actions, from the already-published historyCursor / historyLength, through one shared helper so the two counts cannot drift into different notions of "available" and the missing-Group answer is decided once.
- Name the glyph, disabled and badge tokens in the workstation's own scope, in both themes.
- Route the bar's padding through
--toolbar-padding so the declaration is live and the trailing badge has room.
Acceptance Criteria
- AC-1 Undo and redo expose the steps remaining in their own direction, bound from the already-published
historyCursor / historyLength. No mirrored state and no new publisher.
- AC-2
historyDepth is not used as the count. It is the Group's configured cap, not a live measure, and would read as a constant.
- AC-3 A count of zero shows no badge at all, rather than a
0 on a control that is disabled for the same reason. A retired or unknown Group shows none either — the same fail-closed reading disabled already takes.
- AC-4 ⚠️ Both directions witnessed, in the state where they differ. One step back and one step forward must be asserted together: swapped, mirrored and off-by-one formulas all read alike anywhere else, and a constant survives any single state.
- AC-5 The controls carry themed glyph, text, background and border colours in both
theme-neo-dark and theme-neo-light, in enabled, hover and disabled states. The disabled state is a colour change, not the UA GrayText and not an opacity wash over the label.
- AC-6 The trailing control's badge is fully inside the viewport when the bar is flush right.
Out of Scope
Everything else on #18553: the modified-from-default indicator, the reset, its confirmation, and what reset does to the persisted record. Undo/redo behaviour itself — this ticket only reads what the Group already publishes. Any change to src/button/** or src/toolbar/**: the tokens exist, and the missing paint is the consumer's.
Avoided Traps
Do not mirror the depth onto the workspace. createTopologyBar's own comment records why: getData resolves to the Group leaf's core.Config and re-runs on change, with "no mirror to keep in step with setHistoryDepth, which publishes without a commit."
Do not repair the grey in button/Base.scss. The inheriting color at the root is deliberate and is what lets the child glyph and text nodes own themed colours; the consumer supplying them is the contract, not a workaround.
Do not certify the badge from createTopologyBar()'s return value. It is a config object — toolbar.Base materialises actions into buttons with a spacer, and a formatter called off the declaration exercises the arithmetic and none of the wiring. This is the exact shape that shipped the #18551 regression past two reviews.
Do not check the arithmetic against a hand-published cursor. A formula tested against numbers the test invented can only confirm the model it was written from; drive real writes and cursor moves so historyCursor and historyLength are the engine's.
Related
- Parent: #18553 — the auto-save/default/reset ticket this is AC-1 of
- Epic: #18456 — Workstation as a compact, idiomatic DockLayouts consumer
- #18551 / #18552 — the topology-bar regression whose false guard is this ticket's fourth avoided trap
- #18460 — the row that made the view declare its topology bar
Decision Record impact
none — consumer-side surfacing of published state, plus consumer-side tokenisation. No engine change.
Sweeps
Live latest-open sweep: checked the latest 20 open issues at 2026-09-09T18:20:14Z; nothing equivalent. A2A in-flight claim sweep over the latest 30 messages, all read-states: no claim on the topology bar's history affordances. Own-assignment sweep: 15 open, no overlapping leaf. Memory Core rationale sweep on the observed symptom returned only this session's own survey turns — no prior decision against it. Epic-layer sweep: N/A, not an epic filing. Labels confirmed live from #18553's own set.
Retrieval Hint: workstation topology bar undo redo badge historyCursor historyLength button glyph disabled token toolbar-padding
Origin Session ID: c4dc8abc-31fa-4ecd-9aef-5209ccfd16c0
Serves:#18553 — this is that ticket's AC-1, which its own body marks "Independently shippable, and should land first." Splitting it replaces that scope rather than adding to it: AC-1 is struck through on #18553 and pointed here.Context
Raised by @tobiu against the Workstation topology bar — first as "the undo and redo buttons should indicate how many steps there are", then, against a screenshot of the bar, "undo and redo look a bit unstyled (ugly) => missing icon colors, and i really do not like the disabled state gray. for step counts => neo buttons do support badges."
The depth half and the paint half arrived as two complaints. They are one lane because the same two controls carry both, and because the second is a prerequisite for the first: a badge on a control the app never finished styling inherits the platform's colours rather than the product's.
The Problem
The controls say only whether they can be pressed. Enabled-ness cannot answer "how far back does this go", so a user presses hopefully and finds out by pressing — on the one control whose whole purpose is to make an action reversible.
The paint half is a single omission with two visible symptoms.
resources/scss/src/apps/workstation/Workspace.scsssets five--button-*tokens: background, background-hover, border, height, text-color. No glyph token and no disabled token. So the icons carry no themed colour, and the disabled state falls through to the platform.The Architectural Reality
⚠️ Two of the three pieces are readouts of data and tokens that already exist. Nothing here needs a new publisher.
The depth is already published.
Neo.manager.Transaction#historyStateemitscanUndo · canRedo · historyCursor · historyDepth · historyLengthto every window bound into the Group.apps/workstation/view/Workspace.mjs#createTopologyBarcurrently binds onlydisabledoff that same leaf.cursorstarts at-1andcanUndoiscursor > -1, so the steps behind the cursor arecursor + 1;canRedoiscursor < count - 1, so the steps ahead arecount - 1 - cursor.The widget already exists.
src/button/Base.mjsshipsbadgeText_/badgePosition_rendering a.neo-button-badgenode, andafterSetBadgeTextsetsremoveDomfrom the value — so anullcount removes the badge rather than painting a0.The engine documents the disabled half and assigns it to the consumer.
resources/scss/src/button/Base.scsskeepscolorinheriting at the button root so the child glyph and text nodes can own themed colours; its own comment records that projecting the nativedisabledattribute activates the UAbutton:disabledcascade and that Chromium paintsGrayTextinto the gap. Every token needed already exists:--button-glyph-color[-hover|-disabled],--button-text-color-disabled,--button-background-color[-disabled],--button-border-disabled,--button-opacity-disabled,--button-badge-background-color,--button-badge-color. The grey is not an engine defect and must not be repaired there.One cascade trap sits in the way of the badge.
.neo-toolbarpaints its padding from--toolbar-padding;.workstation-topologybardeclares a plainpadding, which loses to it at equal specificity on source order. The bar therefore runs at the shared 5px, it is flush against the window's right edge, and the trailing control's badge — offset-10pxoutward by--button-badge-margin-left— lands past the viewport.The Fix
badgeTexton both actions, from the already-publishedhistoryCursor/historyLength, through one shared helper so the two counts cannot drift into different notions of "available" and the missing-Group answer is decided once.--toolbar-paddingso the declaration is live and the trailing badge has room.Acceptance Criteria
historyCursor/historyLength. No mirrored state and no new publisher.historyDepthis not used as the count. It is the Group's configured cap, not a live measure, and would read as a constant.0on a control that is disabled for the same reason. A retired or unknown Group shows none either — the same fail-closed readingdisabledalready takes.theme-neo-darkandtheme-neo-light, in enabled, hover and disabled states. The disabled state is a colour change, not the UAGrayTextand not an opacity wash over the label.Out of Scope
Everything else on #18553: the modified-from-default indicator, the reset, its confirmation, and what reset does to the persisted record. Undo/redo behaviour itself — this ticket only reads what the Group already publishes. Any change to
src/button/**orsrc/toolbar/**: the tokens exist, and the missing paint is the consumer's.Avoided Traps
Do not mirror the depth onto the workspace.
createTopologyBar's own comment records why:getDataresolves to the Group leaf'score.Configand re-runs on change, with "no mirror to keep in step withsetHistoryDepth, which publishes without a commit."Do not repair the grey in
button/Base.scss. The inheritingcolorat the root is deliberate and is what lets the child glyph and text nodes own themed colours; the consumer supplying them is the contract, not a workaround.Do not certify the badge from
createTopologyBar()'s return value. It is a config object —toolbar.Basematerialisesactionsinto buttons with a spacer, and a formatter called off the declaration exercises the arithmetic and none of the wiring. This is the exact shape that shipped the #18551 regression past two reviews.Do not check the arithmetic against a hand-published cursor. A formula tested against numbers the test invented can only confirm the model it was written from; drive real writes and cursor moves so
historyCursorandhistoryLengthare the engine's.Related
Decision Record impact
none— consumer-side surfacing of published state, plus consumer-side tokenisation. No engine change.Sweeps
Live latest-open sweep: checked the latest 20 open issues at 2026-09-09T18:20:14Z; nothing equivalent. A2A in-flight claim sweep over the latest 30 messages, all read-states: no claim on the topology bar's history affordances. Own-assignment sweep: 15 open, no overlapping leaf. Memory Core rationale sweep on the observed symptom returned only this session's own survey turns — no prior decision against it. Epic-layer sweep: N/A, not an
epicfiling. Labels confirmed live from #18553's own set.Retrieval Hint:
workstation topology bar undo redo badge historyCursor historyLength button glyph disabled token toolbar-paddingOrigin Session ID: c4dc8abc-31fa-4ecd-9aef-5209ccfd16c0