Context
Portal.view.shared.TabContainer (and others) use tabBarPosition: 'left'. For a left-docked tab header, src/tab/header/Toolbar.mjs getLayoutConfig() returns {direction: 'column-reverse', pack: 'end'} — column-reverse positions the buttons at the top of the vertical bar, but as a side effect it renders the items array inverted: the LAST array entry becomes the FIRST (top) tab. So a left-docked container's items array is the reverse of its visual order.
The Problem
The inversion is a cognitive footgun — "not easy to spot." In the news tab-order work it directly caused two bugs in a single change:
- #12319 / PR #12320: tabs ordered wrong because new views were appended at the array bottom (→ rendered at top).
- #12320 cycle-1: after reordering the array,
TabContainerController still mapped routes to the old indexes → routes activated the wrong content.
It is disproportionately confusing for LLM/agent maintainers: a human edits the array, sees the rendered output, and corrects course; an agent reasons about array order purely from source and cannot see that a non-obvious CSS column-reverse has flipped it. It couples every consumer's array order — and any controller route→index mapping — to an invisible reverse. This is a model-experience (MX) friction worth removing.
The Fix (operator-directed)
Make dock: 'left' keep the real (top-to-bottom display) item order — i.e. items[0] = top tab — by positioning the buttons at the top with a CSS technique that does not reverse the list (e.g. {direction: 'column', pack: 'start'}, or equivalent alignment). Array order == display order; no inversion.
Blast Radius / Migration (IMPORTANT)
src/tab/header/Toolbar.mjs is framework-wide: dropping column-reverse flips the display of every left-docked Neo.tab.Container, so the change MUST be coordinated with a consumer migration. The true blast radius is Neo.tab.Container consumers with tabBarPosition:'left' — three portal views:
apps/portal/view/shared/TabContainer.mjs (the shared base — sets tabBarPosition:'left'; subclasses inherit)
apps/portal/view/news/TabContainer.mjs (items reversed per PR #12320 → re-order to real: Release Notes, Tickets, Discussions, Blog, Medium, Pull Requests)
apps/portal/view/examples/TabContainer.mjs (inherits tabBarPosition:'left' from the shared base → does NOT match a literal tabBarPosition grep; live at apps/portal/view/Viewport.mjs:55; 4 env tabs [devmode, dist_dev, dist_esm, dist_prod] + a controller tabItems array that MUST mirror items order, and dragResortable:true so it's on the SortZone reverse-direction path)
Correction (2026-06-01, @neo-opus-ada): apps/{covid,sharedcovid}/view/country/{Table,HistoricalDataTable}.mjs are NOT affected — their dock:'left' is on table columns (left-pinned), served by the separate src/table/header/Toolbar.mjs getLayoutConfig(dock), not tab.header.Toolbar. My original ledger over-scoped them via a bare dock:'left' grep.
Contract Ledger
(Backfilled per learn/agentos/contract-ledger.md — this changes a consumed core framework surface + migrates consumers. Rows incorporate @neo-gpt's ticket-intake suggestions, #12321 comment.)
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
Neo.tab.header.Toolbar#getLayoutConfig() for dock:'left' |
This ticket + source V-B-A (src/tab/header/Toolbar.mjs) |
Returns a non-reversing column layout (e.g. {direction:'column', pack:'start'}); header buttons render in config array order, top-to-bottom — no column-reverse inversion |
Buttons still align at the TOP of the left bar; top/bottom/right docks unchanged |
getLayoutConfig JSDoc |
Unit coverage for the left-dock layout config + button order |
Neo.tab.Container left-docked consumers (the items→display contract) |
This ticket + src/tab/Container.mjs |
tabBarPosition:'left' consumers author items in real top-to-bottom order (array order == display order) |
Route→activeIndex activation stays correct after consumer reorder; obsolete inversion comments removed |
Consumer comments |
Portal news route→activeIndex spec (TabContainerController.spec.mjs) stays green |
The 3 portal tab.Container left-docked consumers (shared base, news, examples) |
Consumer sweep (§Blast Radius) |
Visual order preserved after each consumer's items (+ examples' controller tabItems) is migrated to real order; covid/sharedcovid are table-column docks, NOT affected |
Non-tab dock:'left' (table columns / toolbars) unaffected |
Local comments |
L3 / visual per affected app |
Tab-header drag-resort (src/draggable/tab/header/toolbar/SortZone.mjs) |
@neo-gpt intake flag + V-B-A |
Drag/resort still computes correct target positions after the layout de-reversal |
No explicit reverse/direction keyword handling found in SortZone → likely DOM-position-based (reverse-agnostic); confirm at implementation |
N/A |
Existing/new drag-sort unit OR targeted manual verification |
Acceptance Criteria
Out of Scope
- Any broader tab-header redesign beyond removing the inversion.
Related
MX note
This is an agent-experience friction-to-gold: the column-reverse inversion is low-cost for humans (who see the render) but high-cost for agents (who edit from source). Removing it makes left-docked tab containers reason-about-able from source alone.
Context
Portal.view.shared.TabContainer(and others) usetabBarPosition: 'left'. For a left-docked tab header,src/tab/header/Toolbar.mjsgetLayoutConfig()returns{direction: 'column-reverse', pack: 'end'}—column-reversepositions the buttons at the top of the vertical bar, but as a side effect it renders theitemsarray inverted: the LAST array entry becomes the FIRST (top) tab. So a left-docked container'sitemsarray is the reverse of its visual order.The Problem
The inversion is a cognitive footgun — "not easy to spot." In the news tab-order work it directly caused two bugs in a single change:
TabContainerControllerstill mapped routes to the old indexes → routes activated the wrong content.It is disproportionately confusing for LLM/agent maintainers: a human edits the array, sees the rendered output, and corrects course; an agent reasons about array order purely from source and cannot see that a non-obvious CSS
column-reversehas flipped it. It couples every consumer's array order — and any controller route→index mapping — to an invisible reverse. This is a model-experience (MX) friction worth removing.The Fix (operator-directed)
Make
dock: 'left'keep the real (top-to-bottom display) item order — i.e.items[0]= top tab — by positioning the buttons at the top with a CSS technique that does not reverse the list (e.g.{direction: 'column', pack: 'start'}, or equivalent alignment). Array order == display order; no inversion.Blast Radius / Migration (IMPORTANT)
src/tab/header/Toolbar.mjsis framework-wide: droppingcolumn-reverseflips the display of every left-dockedNeo.tab.Container, so the change MUST be coordinated with a consumer migration. The true blast radius isNeo.tab.Containerconsumers withtabBarPosition:'left'— three portal views:apps/portal/view/shared/TabContainer.mjs(the shared base — setstabBarPosition:'left'; subclasses inherit)apps/portal/view/news/TabContainer.mjs(items reversed per PR #12320 → re-order to real: Release Notes, Tickets, Discussions, Blog, Medium, Pull Requests)apps/portal/view/examples/TabContainer.mjs(inheritstabBarPosition:'left'from the shared base → does NOT match a literaltabBarPositiongrep; live atapps/portal/view/Viewport.mjs:55; 4 env tabs[devmode, dist_dev, dist_esm, dist_prod]+ a controllertabItemsarray that MUST mirroritemsorder, anddragResortable:trueso it's on the SortZone reverse-direction path)Correction (2026-06-01, @neo-opus-ada):
apps/{covid,sharedcovid}/view/country/{Table,HistoricalDataTable}.mjsare NOT affected — theirdock:'left'is on table columns (left-pinned), served by the separatesrc/table/header/Toolbar.mjs getLayoutConfig(dock), nottab.header.Toolbar. My original ledger over-scoped them via a baredock:'left'grep.Contract Ledger
(Backfilled per
learn/agentos/contract-ledger.md— this changes a consumed core framework surface + migrates consumers. Rows incorporate @neo-gpt's ticket-intake suggestions, #12321 comment.)Neo.tab.header.Toolbar#getLayoutConfig()fordock:'left'src/tab/header/Toolbar.mjs){direction:'column', pack:'start'}); header buttons render in config array order, top-to-bottom — nocolumn-reverseinversiontop/bottom/rightdocks unchangedgetLayoutConfigJSDocNeo.tab.Containerleft-docked consumers (theitems→display contract)src/tab/Container.mjstabBarPosition:'left'consumers authoritemsin real top-to-bottom order (array order == display order)activeIndexactivation stays correct after consumer reorder; obsolete inversion comments removedroute→activeIndexspec (TabContainerController.spec.mjs) stays greentab.Containerleft-docked consumers (shared base, news, examples)items(+ examples' controllertabItems) is migrated to real order; covid/sharedcovid are table-column docks, NOT affecteddock:'left'(table columns / toolbars) unaffectedsrc/draggable/tab/header/toolbar/SortZone.mjs)reverse/directionkeyword handling found inSortZone→ likely DOM-position-based (reverse-agnostic); confirm at implementationAcceptance Criteria
dock:'left'tab header renders items in array order (items[0]= top tab); no inversion.column-reverse+pack:endpreserved via the non-reversing technique).tab.Containerleft-docked consumers (shared base, news, examples — incl. examples' controllertabItemsmirror) audited and migrated to real display order; no visual regression (L3 per app). (covid/sharedcovid table-column docks excluded — see Correction.)TabContainerControllerroute→activeIndexresolution (route-lookup, PR #12320) needs no change — validating that route/id-lookup is reorder-safe (regression test stays green).Portal.view.news.TabContaineritems.SortZone) verified to compute correct positions after the layout de-reversal (no reverse-layout regression).Out of Scope
Related
MX note
This is an agent-experience friction-to-gold: the
column-reverseinversion is low-cost for humans (who see the render) but high-cost for agents (who edit from source). Removing it makes left-docked tab containers reason-about-able from source alone.