LearnNewsExamplesServices
Frontmatter
id18414
titleThe cross-window pane placeholder is an inline Component with no stylesheet, so its load mask inherits black-on-white
stateClosed
labels
bugaiarchitecturedashboard
assigneesneo-opus-grace
createdAtSep 7, 2026, 1:44 AM
updatedAtSep 7, 2026, 8:28 AM
githubUrlhttps://github.com/neomjs/neo/issues/18414
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 7, 2026, 8:28 AM

The cross-window pane placeholder is an inline Component with no stylesheet, so its load mask inherits black-on-white

Closed Backlog/active-chunk-21 bugaiarchitecturedashboard
neo-opus-grace
neo-opus-grace commented on Sep 7, 2026, 1:44 AM

Context

Reported by @tobiu: dragging a tab outside the dock layout container leaves a remaining tab body rendered as a pitch-black panel with white text and a white spinner — against apps/workstation's dark/light green theme. Drag proxies and popup content are unaffected and were verified correct.

The Problem

src/dashboard/dock/window/VesselEmbodiment.mjs:55

const placeholder = Neo.create({
    module   : Component,
    cls      : ['neo-dashboard-dock-vessel-placeholder'],
    isLoading: 'Moving pane to another window…',
    role     : 'status'
});
…
sourceParent.insert(sourceIndex, placeholder, true);

That placeholder occupies the source slot for the duration of the cross-window move — it is the remaining tab body.

It has no stylesheet. Measured at dev@3ac48bb122:

.neo-dashboard-dock-vessel-placeholder   → 0 SCSS files, in every stylesheet
CONTROL .neo-dashboard-dock-edge-rail    → 2
CONTROL .neo-dock-dragproxy              → 1   (verified themed live, renders correctly)
applied in                               → exactly one place, VesselEmbodiment.mjs:56

And the mechanism is inheritance, which is why the symptom is specifically black-and-white. isLoading renders the framework load mask, resources/scss/src/component/Base.scss:31:

.neo-masked {
    .neo-load-mask {
        background-color: inherit;      // ← from the host
        .neo-load-mask-body {
            .neo-loading-message { color: inherit; }   // ← from the host
        }
    }
}

The mask is designed to take its ground and ink from its host component. The host here has no rules at all, so both inherit-chains fall through to defaults — black ground, white message, white .fa-spinner. The mask is behaving correctly; it has nothing to inherit from.

The Architectural Reality

An inline Neo.create({module: Component, cls: [...]}) resolves Neo.component.Base — it carries that base's className and ntype, and loads that base's stylesheet, which declares neither ground nor ink. What it lacks is a class of its own: stylesheet paths are derived from className, so an instance that adds no class has no address at which per-theme values could be declared. The cls entry is applied and no sheet in the repository defines it. (Corrected 2026-09-07 — the original wording said "no className, no ntype", which @neo-gpt-emmy falsified by instantiating the inline configuration and reading both off it. The conclusion is unchanged; the stated mechanism was wrong.) Every comparable dock surface is a registered class with a src/ structural sheet plus one file per theme; interaction/Preview.mjs is the exemplar:

resources/scss/src/dashboard/dock/interaction/Preview.scss
resources/scss/theme-{light,dark,cyberpunk,neo-dark,neo-light}/dashboard/dock/interaction/Preview.scss

Both workstation themes already carry 15 files each under component/, so the theme layer exists — this one surface is simply absent from it.

The Fix

Promote the placeholder to its own registered class, per @tobiu: "that guy could be an own component class. then it is easy to change per theme."

  • src/dashboard/dock/window/VesselPlaceholder.mjs — extends Neo.component.Base, className, ntype, the status message as a config rather than a literal.
  • resources/scss/src/dashboard/dock/window/VesselPlaceholder.scss — structure only.
  • One file per theme under resources/scss/theme-*/dashboard/dock/window/VesselPlaceholder.scss — supplying background-color and color.

Because the load mask inherits, setting those two properties on the host themes the mask, the message and the spinner together. No change to component/Base.scss is needed or wanted.

Acceptance Criteria

  • A registered Neo.dashboard.dock.window.VesselPlaceholder replaces the inline Neo.create at VesselEmbodiment.mjs:55; git grep "module *: *Component" in that file returns nothing.
  • .neo-dashboard-dock-vessel-placeholder (or the class's own selector) resolves a non-default background-color and color in every theme — asserted per theme, not just the two the workstation loads.
  • During a cross-window pane move in apps/workstation, the remaining body's computed background-color matches the theme ground rather than the UA default. Captured from a live drag, not inferred.
  • The status text remains a config, so a consumer can change the wording without subclassing.
  • role: 'status' is preserved — this surface is announced to assistive tech and that must not regress.

Constraints

  • Do not touch the load mask. background-color: inherit / color: inherit in component/Base.scss is correct and is what makes the one-line-per-theme fix sufficient. Changing it would style every masked component in the framework to fix one placeholder.
  • New .mjs file — structural-pre-flight applies; dock/window/ is the sibling-matched home (VesselPark, VesselConversion, VesselEmbodiment, Placement all live there).
  • Verified live for the negative case only: the drag proxy renders correctly themed (#141a23 ground, #d6dce6 ink), which is why this ticket is scoped to the placeholder alone.

Relates #18304 (dock folders receiving behaviour that never moved) and #15252 (the workstation demo journey, where this is visible).

— filed by @neo-opus-grace, origin session 70502f9a-5b14-4dcf-bcdf-4a29b546df77

Contract Ledger

Surface Authority Default Theme override Verification
Neo.dashboard.dock.window.VesselPlaceholder this ticket — n/a VesselEmbodiment.stage() is the sole producer
ntype: 'dock-vessel-placeholder' this ticket — n/a sibling-matched to dock-edge-rail, dock-dragproxy
isLoading (status text) overrides Neo.component.Base#isLoading 'Moving pane to another window…' n/a — a config, set per consumer rejected with the trailing underscore, which is how the override form was found
--dock-vessel-placeholder-ground src/dashboard/dock/window/VesselPlaceholder.scss #1a212c (a copy of theme-neo-dark, not a neutral) one values sheet per theme, :where()-scoped so an app projection always wins computed per theme, table below
--dock-vessel-placeholder-ink same #d6dce6 (likewise) same same

A future theme must add its own values sheet; the fallbacks make an unthemed host land on the dock's default dark presentation rather than flashing white, but they are one theme's values and must not be read as a neutral.

AC-2 receipt — computed in all five themes

Read from getComputedStyle in a real browser against the built artifacts under dist/development/css/theme-*/. Workstation ships only neo-dark and neo-light, so the other three sheets were loaded into the live document and measured in a correctly-classed scope.

theme ground ink host bg / color mask bg (inherited) message
neo-dark #1a212c #d6dce6 rgb(26,33,44) / rgb(214,220,230) rgb(26,33,44) rgb(214,220,230)
neo-light #f7f9fc #1f2733 rgb(247,249,252) / rgb(31,39,51) rgb(247,249,252) rgb(31,39,51)
dark #3c3f41 #e0e0e0 rgb(60,63,65) / rgb(224,224,224) rgb(60,63,65) rgb(224,224,224)
light #fafafa #24292f rgb(250,250,250) / rgb(36,41,47) rgb(250,250,250) rgb(36,41,47)
cyberpunk #0d1117 #c9d1d9 rgb(13,17,23) / rgb(201,209,217) rgb(13,17,23) rgb(201,209,217)

Control — the same markup without the placeholder class: the token still reads #1a212c, because :where(.neo-theme-neo-dark) declares it on the theme scope and it inherits to every descendant. But the element's background is rgba(0,0,0,0) and its colour falls back to the ambient rgb(240,242,240). Only the element carrying neo-dashboard-dock-vessel-placeholder applies the paint — a declared token is not an applied one, and that transparent background is exactly the pre-fix symptom.

A first pass at this table was wrong and is worth recording: every theme reported mask: rgba(0,0,0,0). The fixture omitted neo-masked, and component/Base.scss scopes the inherit rule as .neo-masked .neo-load-mask — the fixture lacked the class the rule keys on, so it measured a rule that never applied.

AC-3 is not covered by any of this and is tracked separately; see the follow-up below.