LearnNewsExamplesServices
Frontmatter
id18571
titleA stored portal theme reaches the Viewport config and nothing downstream follows
stateClosed
labels
bugaiportal
assigneesneo-opus-ada
createdAtSep 10, 2026, 12:00 AM
updatedAtSep 12, 2026, 3:14 PM
githubUrlhttps://github.com/neomjs/neo/issues/18571
authorneo-opus-grace
commentsCount3
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 12, 2026, 3:14 PM

A stored portal theme reaches the Viewport config and nothing downstream follows

neo-opus-grace
neo-opus-grace commented on Sep 10, 2026, 12:00 AM

Context

Found while trying to boot the portal light to witness the second direction of #18569's AC-2. It is not #18569 — that one is a Monaco consumer calling the wrong accessor, and it is fixed in PR #18570. This is upstream of it and breaks the body class too.

The Problem

Measured at dev@87e3786673, local portal, one browser session:

  1. localStorage.setItem('portalTheme', 'neo-theme-neo-light')
  2. reload

apps/portal/view/ViewportController.mjs:193 reads the key and calls setTheme(value, false) on a truthy value, and :364 does me.component.theme = theme. That lands — Portal.view.Viewport#theme reads neo-theme-neo-light afterwards, confirmed through the Neural Link.

Nothing else follows:

surface value
localStorage.portalTheme neo-theme-neo-light
Portal.view.Viewport#theme neo-theme-neo-light
document.body class neo-theme-neo-dark
every .monaco-editor vs-dark
every descendant's theme config null

So the app renders dark while its Viewport says light. The page is internally inconsistent, and a reader who set a light preference simply does not get it.

The Architectural Reality

container.Base#afterSetTheme:306 propagates to live items and deliberately leaves raw item configs to createItem:

value && this.items?.forEach(item => {
    if (!Neo.isString(item) && Neo.typeOf(item) !== 'Object') {
        item.theme = value
    }
})

The storage read is a promise — readLocalStorageItem(...).then(...) — so setTheme lands at an unspecified point in boot. The failure is consistent with the assignment happening while the tree is still raw configs: the forEach skips them, createItem later resolves precedence against a Viewport whose theme did change, and the observed result is that neither the body class nor any descendant carries it.

⚠️ That mechanism is inferred, not measured. What is measured is the table above. I have not instrumented the ordering, and whoever takes this should establish it rather than inherit it — the ticket is the observation, not the cause.

The body class is the second half and may be a different path again: whatever writes neo-theme- onto document.body did not react either.

The Fix

Unknown, deliberately. The shape of the answer depends on which of the two is true — a propagation that ran too early over an empty item list, or a theme assignment that never reaches the body-class writer at all. Both are cheap to separate with one instrumented boot.

Acceptance Criteria

  • AC-1 With portalTheme: neo-theme-neo-light stored, a cold boot renders the portal light: document.body carries neo-theme-neo-light and no surface still reads dark.
  • AC-2 ⚠️ The Viewport config and the rendered body class agree, asserted together. They disagree today, and an arm that checks only one of them passes on exactly this defect.
  • AC-3 The dark preference and the no-preference default are unchanged. Neo.config.prefersDarkTheme boots dark today and must keep doing so.
  • AC-4 ⚠️ The cause is established before the fix, and stated. This ticket carries an observation and an inferred mechanism; a fix built on the inference without checking it is how the wrong path gets repaired.
  • AC-5 A descendant created after the stored theme lands inherits it — that is the case the promise makes possible and the one a fix is most likely to miss.

Out of Scope

#18569 / PR #18570 — Monaco's own accessor, already fixed, and its editors now follow whatever theme the chain actually reports. The live toggle path, which works: driving Viewport#theme on a mounted tree repaints the body and the editors correctly. This is specifically the stored preference at boot.

Avoided Traps

Do not fix this by making container.Base#afterSetTheme stamp raw item configs. Its comment records why it does not — createItem resolves the config's own theme, itemDefaults, the class default and then the container's, and stamping here pre-empts the first three.

Do not assert the fix on Viewport#theme alone. It is already correct today, which is exactly what makes this defect invisible from the worker side.

Related

  • #18569 / PR #18570 — the Monaco consumer, fixed; this is upstream of it.
  • #18106 / #18111 — getTheme() walking components, and the null-by-design contract for an inheriting child.

Decision Record impact

none pending AC-4 — if the cause turns out to be the propagation contract rather than the portal's boot sequence, that judgement belongs with whoever measures it.

Sweeps

Live latest-open sweep at 2026-09-09T21:48:43Z plus the filing of #18569 since: nothing equivalent open. All-state search on monaco theme dark editorTheme returned #8845 and #8727, both closed and both the origin of the theme-sync feature rather than this. A2A claim sweep: no open claim on portal theming.

Retrieval Hint: portal stored theme localStorage portalTheme setTheme viewport config body class descendants do not follow boot

Origin Session ID: c4dc8abc-31fa-4ecd-9aef-5209ccfd16c0