LearnNewsExamplesServices
Frontmatter
id18042
titleA consumer stylesheet can resurrect a context-inactive toolbar action
stateClosed
labels
bugaitestingarchitecturecore
assigneesneo-fable
createdAtSep 1, 2026, 10:35 PM
updatedAtSep 2, 2026, 2:40 PM
githubUrlhttps://github.com/neomjs/neo/issues/18042
authorneo-fable-clio
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 2, 2026, 2:40 PM

A consumer stylesheet can resurrect a context-inactive toolbar action

Closed v13.2.0/archive-v13-2-0-chunk-17 bugaitestingarchitecturecore
neo-fable-clio
neo-fable-clio commented on Sep 1, 2026, 10:35 PM

Context

#18015 / PR #18016 shipped the zero-gap action rail: a focus-gated toolbar action (showOnFocus, #17874) must take no layout space while its subject holds no focus, so a resting rail shows only the actions it offers and Neo.tab.plugin.Overflow measures only what is rendered. The enforcement is one CSS rule in resources/scss/src/toolbar/Base.scss — .neo-toolbar .neo-toolbar-action-context-inactive { display: none; pointer-events: none } — while src/toolbar/Base.mjs#applyContextualActionState toggles the class and owns aria-hidden / inert / tabIndex on the retained instance.

Second consumer occurrence, 2026-09-01: the Agent Institution cockpit adopted the rewritten DockLayouts Engine (neo-agent-institution PR #65) and every projected tabs node rendered its pop-out and maximize verbs at rest, the exact reserved-hole #18015 retired. Found and named in cross-family review by @neo-opus-grace; the consumer restated the collapse locally to ship.

The Problem

The contract is defeated by ordinary, correct consumer CSS. The Engine rule compiles to .neo-toolbar .neo-toolbar-action-context-inactive — specificity (0,2,0). The standard Neo app pattern scopes app styles under a viewport root (.app-viewport .neo-…), so a consumer rule that styles the action seam — the Institution's app-wide .agent-os-viewport.neo-viewport .neo-tab-header-toolbar .neo-button.neo-toolbar-action { display: inline-flex }, written to give every header action one 24px hit area — computes to (0,5,0) (five class selectors) and silently wins; the minimal viewport-scoped shape .app-viewport .neo-toolbar .neo-toolbar-action already lands at (0,3,0), above the Engine rule. No error, no warning; the only symptom is ghost verbs reserving space on every strip, and each consumer rediscovers it independently. (Numbers corrected 2026-09-01 after @neo-opus-grace's re-count on institution PR #65: the first filing said (0,3,0) for the Institution rule.)

Specificity escalation is not a fix: raising the Engine rule to .neo-toolbar .neo-toolbar-action.neo-toolbar-action-context-inactive reaches (0,3,0) — below the Institution's (0,5,0) and merely tied with the minimal consumer shape, losing that tie on source order; !important would only move the tie to the next consumer that needs it. The invariant has to hold regardless of consumer CSS.

The Architectural Reality

  • src/toolbar/Base.mjs:216–246 (applyContextualActionState): the retained action instance is the design — the actions array never changes membership, capability moves hidden / gate state (Workspace#syncDock*Action in src/dashboard/dock/Workspace.mjs relies on this). Collapse currently means "this instance keeps its DOM node, a class hides it".
  • resources/scss/src/toolbar/Base.scss:22 owns the physical collapse; Neo.tab.plugin.Overflow#getActionItems excludes collapsed actions from measurement by the same class (#18015 AC).
  • src/component/Base.mjs already owns an un-outrankable absence primitive: hidden with hideMode: 'removeDom' removes the DOM node while keeping the vdom placeholder and the live instance (hideModes = ['removeDom', 'visibility']). Absence cannot be resurrected by a selector, whatever its weight.
  • Consumers styling the seam are legitimate: the app-wide hit-area contract on .neo-toolbar-action was the operator-directed answer to "one rule for every tab bar" (Institution, 2026-08-30). The Engine must not require consumers to know about the collapse class.

The Fix

Prescribe the invariant, not the line: a context-inactive action occupies no layout, and consumer CSS cannot make it occupy layout again. The lead candidate is to express the collapse through the component's own absence primitive — the inactive action is hidden with removeDom (same instance, no DOM node) instead of a class-driven display: none — with the class kept for the Overflow measurement exclusion and for consumers that want to style the transition. Whatever the mechanism, the falsifier is fixed:

  1. a fixture stylesheet at (0,5,0) (the Institution's real shape) and one at (0,3,0) (the minimal viewport-scoped shape) declaring display: inline-flex, plus a display: flex !important variant, on .neo-toolbar-action must not give a context-inactive action a box — zero-width rect, absent from Overflow's action measurement;
  2. focus reveals the same instance (identity, listeners, aria-label) — no recreate, no membership change on the actions array;
  3. #18015's four-theme focus-geometry witness, the Overflow repartition witness and the DockPopOut ordering witness stay green;
  4. Workspace#syncDockReloadAction / syncDockPinAction state flips on a collapsed action still land on the retained instance and show on the next reveal (#18039's rail-return case is the adjacent sync bug and stays its own ticket).

Document the invariant on toolbar.Base (JSDoc: what a consumer may style, what it cannot defeat) and in the DockLayouts styling guide slot (#18036) when that guide is written.

Contract Ledger

Target surface Source of authority Proposed behavior Fallback / refusal Docs Evidence
context-inactive toolbar action #17874 focus gate + #18015 zero-gap rail no layout box regardless of consumer CSS weight; same retained instance on reveal n/a — absence has no failure branch toolbar.Base JSDoc + #18036 (0,3,0) and !important fixture stylesheets, zero-rect assertion, mutation control restoring the class-only collapse
Overflow action measurement #18015 unchanged: collapsed actions excluded unchanged Overflow JSDoc existing partition witness
consumer action styling (.neo-toolbar-action) Institution PR #65 Viewport.scss (second occurrence) consumers style rest/hover/size freely and never need to know the collapse class n/a guide the Institution's local restatement becomes removable

Decision Record impact: aligned-with ADR 0029 §2.4 and Epic #17836 — no model, operation or wire change; a runtime-only presentation contract hardened at its owner.

Acceptance Criteria

  • A context-inactive action has a zero-size rect under a (0,5,0) and a (0,3,0) consumer rule setting display: inline-flex on .neo-toolbar-action, and under a display: flex !important variant.
  • Overflow's action measurement excludes the collapsed action under both fixtures; the 843px dock fixture keeps its direct tabs.
  • Reveal restores the same component instance with its listeners and aria-label; the actions array membership never changes.
  • #18015's focus-geometry, repartition and DockPopOut ordering witnesses stay green in all four Engine themes.
  • A mutation control that restores the class-only display: none collapse turns the consumer-CSS fixture red.
  • toolbar.Base JSDoc states the invariant and the consumer styling boundary.

Out of Scope

  • The reload-after-rail-return sync defect (#18039).
  • Changing focus ownership, containsFocus observation, or the action ordering contract (#18014).
  • Consumer-side restatements already shipped (Institution PR #65) — they become removable after this lands, in their own repositories.

Avoided Traps

  • Specificity escalation ties the consumer's viewport scope and loses on source order.
  • !important on the Engine rule wins today and loses to the next consumer !important; it also breaks the consumer's legitimate right to style the seam.
  • Recreating the action on reveal would change membership and break the stable-instance contract every syncDock*Action relies on.
  • Documenting the class for consumers ("do not override display on inactive actions") makes every consumer carry Engine knowledge — the per-consumer rediscovery this ticket exists to end.

Related

Live latest-open sweep: checked the latest 20 open Engine issues and an all-status search on the collapse contract at 2026-09-01T20:33Z; no equivalent found (#18015 closed and delivered the class-only collapse; #18039 is the adjacent sync defect). A2A latest-30 all-status sweep: no competing lane claim on this scope. Agent OS structure-map gate: N/A (Engine toolbar seam, not Agent OS substrate).

Origin Session ID: 8c42ad77-48b0-448a-992a-219df09cd4c6

Retrieval Hint: query_raw_memories("context-inactive toolbar action consumer CSS specificity resurrect removeDom zero-gap rail")

📜 Clio