Context
This is the Gate-0 close-and-replace successor to #14771 / PR #15062. I authored #14771 on 2026-07-04 with the projection half placed at the adapter tier (src/dashboard/DockLayoutAdapter.mjs) — a premise inherited from my reading of ADR 0029 §2.4. That premise was corrected by the team on 2026-07-12 (operator Gate-0 statement; @neo-gpt-emmy's D-packet correction withdrawing the dashboard-ownership sentence; @neo-gpt's #13158 activation brief): overflow is generic tab behavior. A heavy Neo.tab.Container anywhere — dock, cockpit, plain app — needs hidden-tab projection; nothing about it is docking-specific. The error was mine and this ticket owns it.
Grace's build on PR #15062 went through two full review cycles and proved the mechanics live (measure → pure-core split → removeDom hide → out-of-collection floating control → menu activation via the ordinary activeIndex path, verified end-to-end on examples/dashboard/dock). We are replacing the vehicle, not the engineering. Falsifier-preservation manifest: Grace's A2A MESSAGE:de178810 (2026-07-12 14:00Z), summarized below so this ticket stands alone.
The Problem
The shipped-shape in PR #15062 couples a generic affordance to the dashboard namespace in exactly four places:
src/dashboard/plugin/TabOverflow.mjs — the plugin class (Neo.dashboard.plugin.TabOverflow), whose implementation is already generic: owner is Neo.tab.header.Toolbar, measurement via owner.getDomRect, hide via button.hidden, activation via tabContainer.activeIndex.
DockLayoutAdapter.computeTabOverflow — the pure core as a static on the adapter, forcing the plugin to reach through the Neo.dashboard namespace at runtime to avoid an import cycle (the code comments this hack explicitly — the smell that marks the wrong home).
- The adapter-chain unit-test dependency: the pure-core spec rides
DockLayoutAdapter.spec.mjs, which needed the #15065 lazy-import surgery just to LOAD in the unit env.
- The
neo-dock-tab-overflow-control cls.
The Architectural Reality
- Subsystem-owned plugin directories are the established convention:
src/grid/plugin/, src/grid/header/plugin/, src/table/plugin/, src/list/plugin/, src/calendar/view/week/plugin/ all exist today. src/tab/plugin/ is a sibling-pattern lift (structural-pre-flight fast-path), not a novel directory.
Neo.tab.header.Toolbar (src/tab/header/Toolbar.mjs) is the measurement/attachment owner; Neo.tab.Container (src/tab/Container.mjs) is the activation owner. Both are tab-subsystem classes — the plugin's two seams never leave the subsystem.
- The dock's involvement is exactly ONE line:
DockLayoutAdapter.projectTabsNode injects plugins: [{module: Overflow}] into the projected headerToolbar config — the opt-in, with activation already riding the ordinary tab path. No dock-side overflow logic remains.
- The import cycle that forced the namespace-reach hack dissolves: plugin and pure core live together in the tab subsystem; the adapter imports the plugin (one direction only).
- The #15083/#15086 hidden-document render-queue drain (merged) unblocks the floating-control mount everywhere — the successor inherits that fix; nothing here is blocked.
The Fix
One implementation PR (staged implementers per team convergence: @neo-opus-grace authors, @neo-gpt reviews/pairs — self-select/swap valid) plus one carve-out PR that can land immediately, before the successor:
Carve-out PR (framework fixes, placement-independent — land first, do not hold hostage to the vehicle swap):
- RA-9:
resources/scss/src/button/Base.scss — .neo-button.neo-floating {position: fixed} cascade fix.
- RA-10:
src/main/DomAccess.mjs — null-offsetParent guards on ResizeObserver.observe/unobserve (fixed-positioned subjects have no offsetParent).
Successor PR (the vehicle swap):
src/tab/plugin/Overflow.mjs — Neo.tab.plugin.Overflow, ntype plugin-tab-overflow: Grace's plugin verbatim minus namespace; the pure core moves onto this class as static computeOverflow(...) (direct import in specs — the adapter chain and its #15065/#15017 loader pain exit the test path entirely).
- RA-13 restoration: the re-align-on-sync cut in
306874 is load-bearing (author's own flag) — restore it; ping Grace for the author walk-through on activation.
DockLayoutAdapter: delete computeTabOverflow + the plugin file under dashboard/; keep the one-line projected-headerToolbar opt-in, now importing Neo.tab.plugin.Overflow.
- cls rename:
neo-tab-overflow-control (grep the e2e).
- Tests carried per the falsifier-preservation manifest: the 6 re-entrancy specs (live-owner-error surfaced / teardown-race swallowed / concurrent-coalesce / all-fit teardown / error→success latch release / recreation) re-pointed at the tab-native class; the 7 pure-core specs move to a tab-subsystem spec with a direct import; the e2e assertion set (partition: visible+hidden === all tabs · owner-exact geometry · menu-selection activates ·
pageErrors === []) is preserved with a minimal generic tab.Container harness in-slice (a plain overflowing tab container — keeps the slice self-verifying); the full dock journey re-homes to the dense-workstation demo scene (see successor's sibling ticket, linked below).
examples/dashboard/dock keeps its heavy 7-tab main-tabs workspace (the dock opt-in's demonstrable surface).
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Behavior |
Fallback / Edge Case |
Docs |
Evidence |
Neo.tab.plugin.Overflow |
this ticket (successor of PR #15062 class) |
measure natural widths once-visible → pure-core split → hidden (removeDom) + one floating out-of-collection control at document.body |
teardown race swallowed; live-owner throw surfaced; coalesced re-run drains last state |
class JSDoc |
6 re-entrancy specs + generic e2e |
Overflow.computeOverflow (static) |
moved from DockLayoutAdapter.computeTabOverflow (PR #15062, behavior FROZEN) |
active-never-hidden packing; control width reserved only on overflow; hidden preserves items order; fail-soft on malformed widths |
degenerate single-wide-item stays visible |
method JSDoc |
the 7 pure-core specs, direct-import |
headerToolbar.plugins opt-in seam |
DockLayoutAdapter.projectTabsNode |
dock opts in; activation rides ordinary activeIndex |
non-dock users opt in with the same config on any tab.Container headerToolbar |
adapter comment |
projection-wiring assertion (existing) |
.neo-tab-overflow-control cls |
this ticket |
replaces .neo-dock-tab-overflow-control |
none — new surface, no consumers yet |
SCSS comment |
e2e selector |
Acceptance Criteria
Out of Scope
The dense-workstation demo scene (sibling ticket, carries the re-homed full dock journey) · grouped drag (#14770) · auto-hide rails · any behavior change to the frozen pure-core contract.
Decision Record impact
amends ADR 0029 §2.4 — placement clause only: the projection half's owner becomes the tab subsystem; the adapter opts in. The behavior contract ("projection affordance, NOT a model change"; selection via existing activeItemId; no dock-specific tab-container fork) is unchanged. The ADR §2.4 text edit rides the successor PR. Authority for the amendment: operator Gate-0 statement + @neo-gpt-emmy correction (2026-07-12 13:33Z) + team convergence (13:32Z).
Avoided Traps
- Built into
tab.Container (config-only, no plugin): rejected — forces a DOM-measurement runtime concern into the declarative container and violates pay-for-what-you-use; the plugin system exists for exactly this (Responsive/Resizable precedents).
src/plugin/TabOverflow.mjs (generic plugin home): rejected — "a generic plugin that happens to target tabs" is weaker ownership than the grid/table/list subsystem-dir convention; Emmy's correction language ("genuinely owned by the tab subsystem") names the stronger form.
- Rework PR #15062 in place: rejected — close-and-replace is the settled team disposition; a fresh PR gives the relocation clean provenance and lets the carve-out land independently.
- Keeping the pure core on the adapter: rejected — it is the source of both the namespace-reach hack and the unit-env loader pain (#15065/#15017).
Related
Replaces #14771 (closing with pointer) · freezes-and-supersedes PR #15062 (mechanics preserved) · parent #13158 · unblocked by merged #15083/#15086 · falsifier manifest: A2A MESSAGE:de178810 · consumed by the dense-workstation demo scene (sibling, blocked on this) · ADR 0029 §2.4.
Live latest-open sweep: latest 20 open at 2026-07-12T17:05Z + fresh latest-10 at 17:08Z, no equivalent; searches "overflow"/"tab-native" surface only #14771 itself. A2A in-flight sweep: full mailbox window read 17:00Z, [lane-claim] broadcast sent 17:08Z (MESSAGE:312bbc25), no competing claim — Grace's manifest is an explicit baton.
Origin Session ID: fc11bd7a-b599-46f1-b635-bdd1cd3b7a5e
Retrieval Hint: "tab-native overflow Gate-0 close-and-replace Neo.tab.plugin.Overflow falsifier preservation"
Context
This is the Gate-0 close-and-replace successor to #14771 / PR #15062. I authored #14771 on 2026-07-04 with the projection half placed at the adapter tier (
src/dashboard/DockLayoutAdapter.mjs) — a premise inherited from my reading of ADR 0029 §2.4. That premise was corrected by the team on 2026-07-12 (operator Gate-0 statement; @neo-gpt-emmy's D-packet correction withdrawing the dashboard-ownership sentence; @neo-gpt's #13158 activation brief): overflow is generic tab behavior. A heavyNeo.tab.Containeranywhere — dock, cockpit, plain app — needs hidden-tab projection; nothing about it is docking-specific. The error was mine and this ticket owns it.Grace's build on PR #15062 went through two full review cycles and proved the mechanics live (measure → pure-core split →
removeDomhide → out-of-collection floating control → menu activation via the ordinaryactiveIndexpath, verified end-to-end onexamples/dashboard/dock). We are replacing the vehicle, not the engineering. Falsifier-preservation manifest: Grace's A2AMESSAGE:de178810(2026-07-12 14:00Z), summarized below so this ticket stands alone.The Problem
The shipped-shape in PR #15062 couples a generic affordance to the dashboard namespace in exactly four places:
src/dashboard/plugin/TabOverflow.mjs— the plugin class (Neo.dashboard.plugin.TabOverflow), whose implementation is already generic: owner isNeo.tab.header.Toolbar, measurement viaowner.getDomRect, hide viabutton.hidden, activation viatabContainer.activeIndex.DockLayoutAdapter.computeTabOverflow— the pure core as a static on the adapter, forcing the plugin to reach through theNeo.dashboardnamespace at runtime to avoid an import cycle (the code comments this hack explicitly — the smell that marks the wrong home).DockLayoutAdapter.spec.mjs, which needed the #15065 lazy-import surgery just to LOAD in the unit env.neo-dock-tab-overflow-controlcls.The Architectural Reality
src/grid/plugin/,src/grid/header/plugin/,src/table/plugin/,src/list/plugin/,src/calendar/view/week/plugin/all exist today.src/tab/plugin/is a sibling-pattern lift (structural-pre-flight fast-path), not a novel directory.Neo.tab.header.Toolbar(src/tab/header/Toolbar.mjs) is the measurement/attachment owner;Neo.tab.Container(src/tab/Container.mjs) is the activation owner. Both are tab-subsystem classes — the plugin's two seams never leave the subsystem.DockLayoutAdapter.projectTabsNodeinjectsplugins: [{module: Overflow}]into the projectedheaderToolbarconfig — the opt-in, with activation already riding the ordinary tab path. No dock-side overflow logic remains.The Fix
One implementation PR (staged implementers per team convergence: @neo-opus-grace authors, @neo-gpt reviews/pairs — self-select/swap valid) plus one carve-out PR that can land immediately, before the successor:
Carve-out PR (framework fixes, placement-independent — land first, do not hold hostage to the vehicle swap):
resources/scss/src/button/Base.scss—.neo-button.neo-floating {position: fixed}cascade fix.src/main/DomAccess.mjs— null-offsetParentguards onResizeObserver.observe/unobserve(fixed-positioned subjects have no offsetParent).Successor PR (the vehicle swap):
src/tab/plugin/Overflow.mjs—Neo.tab.plugin.Overflow, ntypeplugin-tab-overflow: Grace's plugin verbatim minus namespace; the pure core moves onto this class asstatic computeOverflow(...)(direct import in specs — the adapter chain and its #15065/#15017 loader pain exit the test path entirely).306874is load-bearing (author's own flag) — restore it; ping Grace for the author walk-through on activation.DockLayoutAdapter: deletecomputeTabOverflow+ the plugin file underdashboard/; keep the one-line projected-headerToolbar opt-in, now importingNeo.tab.plugin.Overflow.neo-tab-overflow-control(grep the e2e).pageErrors === []) is preserved with a minimal generictab.Containerharness in-slice (a plain overflowing tab container — keeps the slice self-verifying); the full dock journey re-homes to the dense-workstation demo scene (see successor's sibling ticket, linked below).examples/dashboard/dockkeeps its heavy 7-tabmain-tabsworkspace (the dock opt-in's demonstrable surface).Contract Ledger Matrix
Neo.tab.plugin.Overflowhidden(removeDom) + one floating out-of-collection control atdocument.bodyOverflow.computeOverflow(static)DockLayoutAdapter.computeTabOverflow(PR #15062, behavior FROZEN)headerToolbar.pluginsopt-in seamDockLayoutAdapter.projectTabsNodeactiveIndextab.ContainerheaderToolbar.neo-tab-overflow-controlcls.neo-dock-tab-overflow-controlAcceptance Criteria
Neo.tab.plugin.Overflowships with the pure core as its own static;src/dashboard/plugin/andDockLayoutAdapter.computeTabOverfloware gone; the adapter's only overflow trace is the opt-in config line.tab.Containerharness proves the four preserved assertions; dock example still demonstrates the opt-in visually.Out of Scope
The dense-workstation demo scene (sibling ticket, carries the re-homed full dock journey) · grouped drag (#14770) · auto-hide rails · any behavior change to the frozen pure-core contract.
Decision Record impact
amends ADR 0029 §2.4 — placement clause only: the projection half's owner becomes the tab subsystem; the adapter opts in. The behavior contract ("projection affordance, NOT a model change"; selection via existing
activeItemId; no dock-specific tab-container fork) is unchanged. The ADR §2.4 text edit rides the successor PR. Authority for the amendment: operator Gate-0 statement + @neo-gpt-emmy correction (2026-07-12 13:33Z) + team convergence (13:32Z).Avoided Traps
tab.Container(config-only, no plugin): rejected — forces a DOM-measurement runtime concern into the declarative container and violates pay-for-what-you-use; the plugin system exists for exactly this (Responsive/Resizable precedents).src/plugin/TabOverflow.mjs(generic plugin home): rejected — "a generic plugin that happens to target tabs" is weaker ownership than the grid/table/list subsystem-dir convention; Emmy's correction language ("genuinely owned by the tab subsystem") names the stronger form.Related
Replaces #14771 (closing with pointer) · freezes-and-supersedes PR #15062 (mechanics preserved) · parent #13158 · unblocked by merged #15083/#15086 · falsifier manifest: A2A
MESSAGE:de178810· consumed by the dense-workstation demo scene (sibling, blocked on this) · ADR 0029 §2.4.Live latest-open sweep: latest 20 open at 2026-07-12T17:05Z + fresh latest-10 at 17:08Z, no equivalent; searches "overflow"/"tab-native" surface only #14771 itself. A2A in-flight sweep: full mailbox window read 17:00Z,
[lane-claim]broadcast sent 17:08Z (MESSAGE:312bbc25), no competing claim — Grace's manifest is an explicit baton.Origin Session ID: fc11bd7a-b599-46f1-b635-bdd1cd3b7a5e Retrieval Hint: "tab-native overflow Gate-0 close-and-replace Neo.tab.plugin.Overflow falsifier preservation"