LearnNewsExamplesServices
Frontmatter
id17418
titleAdd flat actions to TabContainer header toolbars
stateClosed
labels
enhancementaitestingcoreaccessibility
assigneesneo-gpt-emmy
createdAtAug 20, 2026, 3:53 PM
updatedAtAug 20, 2026, 9:57 PM
githubUrlhttps://github.com/neomjs/neo/issues/17418
authorneo-gpt-emmy
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[ ] 17419 Route Dock tab close actions through model policy
closedAtAug 20, 2026, 9:57 PM

Add flat actions to TabContainer header toolbars

Closed Backlog/active-chunk-17 enhancementaitestingcoreaccessibility
neo-gpt-emmy
neo-gpt-emmy commented on Aug 20, 2026, 3:53 PM

Context

Discussion #17415 converged on flat, optional actions inside existing Neo.tab.Container header toolbars. The design began from two shipped precedents:

  • Neo.dialog.header.Toolbar already materializes actions / actionMap into flat toolbar buttons and emits intent while Neo.dialog.Base owns effects.
  • Neo.code.LivePreview#onConstructed() manually appends a spacer plus fullscreen/popout buttons to a plain TabContainer header and keeps those instances stable while active-tab and popup-window state change. Closed #5272 is the historical ticket that intentionally introduced the top-right non-header button.

The Discussion's non-author cycle at comment 18093464 promoted LivePreview from precedent to the required migration witness and sharpened the SortZone membership and mid-drag geometry contracts.

Live latest-open sweep and exact-title searches were clean at creation time; no equivalent open ticket or PR exists.

The Problem

Mixed toolbar composition renders today, but TabContainer semantics still treat raw tabBar.items as the tab set. Count, lookup, insert, removal, movement, pressed state, Overflow, and drag sorting can therefore mistake a spacer or action for a tab.

Action materialization is also Dialog-local. A host such as LivePreview must manually create buttons, append them after construction, retain references, and update hidden / disabled state itself. There is no general focus contract for contextual actions, so hiding on a body-level focusLeave can remove the button while focus is moving into it.

The current tab SortZone can already opt into .neo-tab-header-button through dragHandleSelector; every tab header carries that class. The remaining hole is consistency: the generic insert hook marks every inserted item draggable, while initiation, sortableItems, indexMap, and target selection must share one membership answer.

The Architectural Reality

  • Flat composition is the existing Neo idiom. A nested actions toolbar, sibling header shell, sidecar, or floating rail adds ownership/lifecycle or ancestry changes without a v1 capability that needs them.
  • Neo.tab.header.Button already owns .neo-tab-header-button; no migration of TabContainer consumers is required to select real tabs.
  • ignoreDragSelector gates gesture initiation only. It cannot exclude an action from sortableItems, indexMap, or target resolution.
  • Neo.manager.Focus preserves the closest common component on internal focus movement. The active body should arm contextual actions; the TabContainer logical realm should retain them.
  • Focus transitions must not resize the header. Contextual actions reserve their extent while visually and semantically inactive, avoiding Overflow churn and stale rendered geometry during a held-open drag.
  • This ticket owns generic TabContainer mechanics only. Dock document mutation and closable policy belong to the dependent Dock integration ticket.

The Fix

  1. Generalize optional flat action materialization from Neo.dialog.header.Toolbar into the existing toolbar hierarchy: actions, actionMap, fresh config resolution, stable instances, semantic lookup, runtime host contribution, and generic intent signaling.
  2. Keep Dialog's existing title/spacer/action order, handler precedence, event name/payload, and close/maximize effect ownership compatible.
  3. Add flat actions to Neo.tab.header.Toolbar after one spacer. Expose explicit tab/action views; raw items remains the component collection, never the semantic tab collection.
  4. Route every TabContainer count/index/add/insert/remove/move/mounted/pressed-state path through the tab-button subset.
  5. Configure the tab SortZone with the existing tab-button selector and make insertion reuse the same membership decision as initial marking, drag initiation, snapshots, sortableItems, indexMap, and targets.
  6. Separate tab-only local sort/overdrag geometry from any outer host/tear-out boundary consumed by Dock subclasses.
  7. Make Overflow consume the tab subset and the action-exclusive available extent.
  8. Implement body-arm / TabContainer-retain focus semantics with stable, accessible actions. Persistent actions remain independent of contextual engagement.
  9. Migrate Neo.code.LivePreview to the new API without changing fullscreen, popout, active-tab, or popup-window lifecycle behavior.

Contract Ledger

Target surface Source of authority Proposed behavior Fallback Docs Evidence
Generic toolbar actions dialog.header.Toolbar precedent Optional flat materialization with stable action instances and generic intent No actions preserves current toolbar behavior JSDoc on configs/methods Dialog compatibility specs
Tab semantic collection tab.Container getTabButtons() is the sole tab count/index/order authority; actions remain ordinary toolbar items No actions remains byte-equivalent TabContainer JSDoc mutation matrix with actions present
Action collection converged D#17415 OQ1/OQ4/OQ11 getActionItems() plus construction-time/runtime host contribution Empty collection Toolbar JSDoc LivePreview migration
Tab SortZone membership existing dragHandleSelector + tab button base class one selector/predicate governs initial and inserted items, initiation, snapshots, indices, and targets non-tab SortZones keep their current defaults SortZone JSDoc trace/target falsifiers
Sort geometry D#17415 OQ3/OQ10 reserved action extent; local tab-only sort geometry distinct from outer host boundary pre-feature headers unchanged SortZone/toolbar intent docs held-drag geometry arm
Focus/accessibility manager.Focus body arms; group retains; outside disarms; hidden contextual controls leave tab/a11y order persistent actions stay available TabContainer JSDoc pointer, keyboard, popup matrix
Dialog compatibility existing public configs/events exact current configs, handler precedence, payload, and effects survive N/A existing docs focused regression specs

Decision Record impact

None. This is a feature-level extension of existing toolbar, focus, and SortZone contracts.

Decision Record

Not needed. Source Discussion #17415 resolved this as low-blast feature reuse.

Signal Ledger

  • GPT author family: low-blast graduation fold in D#17415.
  • Claude non-author family: substantive peer cycle at DC_kwDODSospM4BFBWY.

Unresolved Dissent

None.

Unresolved Liveness

None for this low-blast gate. Inactive families were not counted as approval and are not required for this feature-class graduation.

Discussion Criteria Mapping

  • OQ1/OQ4/OQ11 → flat actions, semantic collections, and runtime host contribution.
  • OQ2/OQ3 → body-arm/group-retain focus with reserved geometry.
  • OQ5 → generic intent only; explicit handlers own effects.
  • OQ8 → all four orientations.
  • OQ9/OQ10 → selector-consistent membership and distinct sort/host boundaries.
  • Grace's LivePreview and mid-drag findings → mandatory migration and held-drag falsifier ACs.

Acceptance Criteria

  • Optional toolbar actions materialize flat after one spacer; no nested/floating action component is introduced.
  • Dialog header action configs, custom handler precedence, headerAction payloads, close/maximize effects, and current consumers remain compatible.
  • getTabButtons() and getActionItems() expose stable semantic collections; every TabContainer count/index/mutation/pressed-state path ignores actions and the spacer.
  • Construction-time and runtime host action contribution preserves stable instances and consumer-owned handlers/availability.
  • Body focus reveals contextual actions; focus moving body ↔ tab ↔ action ↔ logically-owned popup retains them; leaving the TabContainer hides them.
  • Inactive contextual actions reserve geometry but are absent from keyboard and accessibility navigation. Persistent actions are unaffected.
  • Initial and dynamically inserted actions/spacers never receive .neo-draggable, enter sortableItems / indexMap, become targets, or appear in tab drag traces.
  • Dragging the last tab through action coordinates cannot reorder an action. Local tab-sort geometry excludes actions while an outer Dock host boundary remains separately usable.
  • A held-open tab drag with action visibility/width changes either receives current DOM geometry within the gesture or continues from an explicitly stable pre-gesture tab-only snapshot.
  • Overflow lists/measures tabs only, reserves visible action extent, keeps the active tab reachable, and passes narrow/wide plus over-wide-active cases.
  • Top, right, bottom, and left tab bars keep correct trailing actions, tab order, Overflow behavior, and drag geometry.
  • Neo.code.LivePreview uses the new action API and preserves fullscreen, popout, active-index visibility, stable handler identity, connect/disconnect behavior, and existing appearance.
  • Focused unit/component coverage and a Whitebox E2E journey prove the full pointer/keyboard/focus/drag/Overflow contract.

Out of Scope

  • DockLayout close execution and closable policy.
  • Active-card-contributed action lists or automatic container/card precedence merging.
  • Application-specific action effects.
  • Inline per-tab close icons.
  • Persisting functions or action configs in Dock documents.
  • A nested toolbar, header shell, sidecar, or floating action rail.

Avoided Traps

  • Raw tabBar.items as tab identity: renders simple cases but creates phantom tab/count/drag semantics.
  • ignoreDragSelector as membership: prevents initiation only; actions remain sortable targets.
  • Nested/floating action ownership: adds DOM, focus, theme, alignment, and destroy lifecycle without a v1 benefit.
  • Reclaimed focus-time width: causes tab/Overflow movement and can consult stale rendered rects during drag.
  • Recreating actions on state changes: loses handlers, popup ownership, focus, and LivePreview continuity.

Related

Origin Session ID: 0f8b5b8e-3f01-45c8-889e-1c2fd90b0584

Retrieval Hint: "TabContainer flat header actions LivePreview dragHandleSelector reserved geometry"

Emmy (GPT-5.6 Sol Ultra, Codex) · @neo-gpt-emmy 🪡

tobiu referenced in commit 6ee96ac - "feat(tab): add flat header actions (#17418) (#17423) on Aug 20, 2026, 9:57 PM
tobiu closed this issue on Aug 20, 2026, 9:57 PM