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;
.neo-load-mask-body {
.neo-loading-message { color: inherit; }
}
}
}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.scssBoth 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
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.
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:55const 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:And the mechanism is inheritance, which is why the symptom is specifically black-and-white.
isLoadingrenders 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: [...]})resolvesNeo.component.Base— it carries that base'sclassNameandntype, 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 fromclassName, so an instance that adds no class has no address at which per-theme values could be declared. Theclsentry is applied and no sheet in the repository defines it. (Corrected 2026-09-07 — the original wording said "noclassName, nontype", 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 asrc/structural sheet plus one file per theme;interaction/Preview.mjsis the exemplar:resources/scss/src/dashboard/dock/interaction/Preview.scss resources/scss/theme-{light,dark,cyberpunk,neo-dark,neo-light}/dashboard/dock/interaction/Preview.scssBoth 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.resources/scss/theme-*/dashboard/dock/window/VesselPlaceholder.scss— supplyingbackground-colorandcolor.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.scssis needed or wanted.Acceptance Criteria
Neo.dashboard.dock.window.VesselPlaceholderreplaces the inlineNeo.createatVesselEmbodiment.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-defaultbackground-colorandcolorin every theme — asserted per theme, not just the two the workstation loads.apps/workstation, the remaining body's computedbackground-colormatches the theme ground rather than the UA default. Captured from a live drag, not inferred.role: 'status'is preserved — this surface is announced to assistive tech and that must not regress.Constraints
background-color: inherit/color: inheritincomponent/Base.scssis 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..mjsfile —structural-pre-flightapplies;dock/window/is the sibling-matched home (VesselPark,VesselConversion,VesselEmbodiment,Placementall live there).#141a23ground,#d6dce6ink), 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-4a29b546df77Contract Ledger
Neo.dashboard.dock.window.VesselPlaceholderVesselEmbodiment.stage()is the sole producerntype: 'dock-vessel-placeholder'dock-edge-rail,dock-dragproxyisLoading(status text)Neo.component.Base#isLoading'Moving pane to another window…'--dock-vessel-placeholder-groundsrc/dashboard/dock/window/VesselPlaceholder.scss#1a212c(a copy oftheme-neo-dark, not a neutral):where()-scoped so an app projection always wins--dock-vessel-placeholder-ink#d6dce6(likewise)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
getComputedStylein a real browser against the built artifacts underdist/development/css/theme-*/. Workstation ships onlyneo-darkandneo-light, so the other three sheets were loaded into the live document and measured in a correctly-classed scope.neo-dark#1a212c#d6dce6rgb(26,33,44)/rgb(214,220,230)rgb(26,33,44)rgb(214,220,230)neo-light#f7f9fc#1f2733rgb(247,249,252)/rgb(31,39,51)rgb(247,249,252)rgb(31,39,51)dark#3c3f41#e0e0e0rgb(60,63,65)/rgb(224,224,224)rgb(60,63,65)rgb(224,224,224)light#fafafa#24292frgb(250,250,250)/rgb(36,41,47)rgb(250,250,250)rgb(36,41,47)cyberpunk#0d1117#c9d1d9rgb(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 isrgba(0,0,0,0)and its colour falls back to the ambientrgb(240,242,240). Only the element carryingneo-dashboard-dock-vessel-placeholderapplies 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 omittedneo-masked, andcomponent/Base.scssscopes 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.