LearnNewsExamplesServices
Frontmatter
id17890
titleLeft/right drop overlays render far darker, and no test can observe overlay colour
stateClosed
labels
bugaitestingcore
assigneesneo-opus-grace
createdAtAug 30, 2026, 8:21 PM
updatedAtAug 31, 2026, 3:43 AM
githubUrlhttps://github.com/neomjs/neo/issues/17890
authorneo-opus-grace
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 31, 2026, 3:43 AM

Left/right drop overlays render far darker, and no test can observe overlay colour

neo-opus-grace
neo-opus-grace commented on Aug 30, 2026, 8:21 PM

Context

Reported by @tobiu on 2026-08-30 against apps/workstation: the drop-zone overlays for top, bottom and centre read correctly — a green border over a slightly tinted fill — while left and right render a far darker background, "almost looks like the full background gets the border color with no transparency."

He also asked why the e2e suite did not catch it. That half is answered below and is the more actionable finding.

Live latest-open sweep: latest 20 open issues checked at 2026-08-30T18:21:10Z; no equivalent. Structure-map gate: N/A, no ai/ surface and no new .mjs outside test/.

Why nothing caught it — three layers, each independently sufficient

layer what it asserts why it cannot fail here
test/playwright/e2e/** — e2e runs in no workflow at all. No test-e2e / playwright.config.e2e reference exists under .github/workflows. It has never executed in CI
e2e/workstation/WorkstationDockPreviewSymmetryNL.spec.mjs "compares the four thicknesses directly" (its own header) geometry only — structurally incapable of failing on a fill or opacity defect, even if it ran
unit/dashboard/DockPreview.spec.mjs fs.readFileSync(Preview.scss) + expect(scss).toContain(…) asserts the stylesheet as source text. A test that greps its own SCSS never observes what a browser composites

Nothing in the suite renders an affordance and reads a computed colour. That is the defect this ticket must close, independent of the visual cause.

What has been measured, and eliminated

All measured against the running workstation, driving the live dock-preview instance over RMA and reading getComputedStyle:

candidate result
per-edge stylesheet asymmetry eliminated — all four edges compute rgba(94,234,212,0.2) fill, identical border colour, opacity: 0.85, one 4px cut side
split group hitting the solid-bar rule eliminated — split-before/split-after also carry region and paint the same translucent fill
wrong per-edge classes eliminated — each edge carries its own edge-<dir> and the correctly inverted cut-<dir>
more than one preview host stacking RETRACTED 2026-08-30 — this row was scoped too narrowly. It searched for duplicate .neo-dock-preview hosts and found one. It never searched for a different overlay painting the same region, so it cannot support "nothing else stacks here". See candidate 2 below
affordance nodes accumulating eliminated — Preview.mjs:374 replaces vdom.cn with a single-node array
the neo-preview-lang-signal skin eliminated — the modifier is not active on this app
edge-band tokens using the wrong axis eliminated — left/right use inline-size, top/bottom use block-size, correctly
splitter currentColor divergence eliminated — every splitter resolves rgb(240,242,240) and the same background in both orientations

So the drop-preview overlay is not what renders dark. Something else paints during a live left/right drag.

CORRECTED 2026-08-31 — this conclusion was wrong, and the table above is why. Every row was measured against a current checkout, so the whole table answers "does HEAD paint asymmetrically" — not "what was @tobiu's browser painting". The drop-preview overlay is what rendered dark; it was applying a stale compiled stylesheet the source no longer matched. A source-vs-source elimination cannot see a source-vs-build skew. See the root cause section below.

The two surviving candidates (2026-08-30)

Candidate 1 — a split affordance reaching the solid-bar rule. .neo-dock-preview-split:not(.neo-dock-preview-region) paints the accent colour as a solid background. Mutating getAffordanceVdom's region predicate to group === 'edge' reproduces the report exactly:

Expected: "rgba(94, 234, 212, 0.2)"
Received: "rgb(94, 234, 212)"

rgb(94, 234, 212) is the accent at full opacity — "the full background gets the border color with no transparency", verbatim. Against it: resultRegionPreviews defaults true and no caller in apps/workstation sets it false, so the path that would drop the class is unidentified.

Candidate 2 — DockDropIndicators, a second overlay. apps/workstation/view/Workspace.mjs:462 mounts it as a persistent sibling of the preview. Container.scss:493 paints .neo-dashboard-dock-drop-indicator / -chip with var(--dock-preview-ground, rgb(28 33 40 / 92%)) — a near-opaque dark ground — inside a container at z-index: 30, inset: 0. A dark chip overlapping the left/right region but not the top/bottom one reads exactly as "a WAY darker background overlay".

Left/right resolve as in-pane split, top/bottom as root edge; either candidate can ride that asymmetry. The discriminator is elementsFromPoint under the pointer during a held drag, reading every layer's computed background — candidate 1 shows one node whose own fill lost its alpha, candidate 2 shows two stacked nodes.

Live held-drag measurement — 2026-08-31 (candidate 1 eliminated; defect DID NOT reproduce)

The held-drag reading AC-3 asks for has now been taken, against the running workstation with a real 1600×1000 viewport. Method: real pointerdown → incremental pointermove → read getComputedStyle while the button is still down → cancel.

zone affordance classes computed fill opacity rect
LEFT (in-pane) split split-before accepted **region** cut-right rgba(94, 234, 212, 0.2) 0.85 333×688 — left half
TOP (root) edge edge-top accepted **region** cut-bottom rgba(94, 234, 212, 0.2) 0.85 665×344 — top half

Identical fill, identical opacity, correct half-region geometry on both. The reported divergence did not reproduce in this environment (theme neo-theme-neo-dark, dev server, resultRegionPreviews: true read live off neo-dock-preview-1).

  • Candidate 1 — ELIMINATED. A live in-pane split does carry neo-dock-preview-region, so it never reaches the solid-bar rule. The mutation still reproduces the symptom exactly, which means the rule is the right description of what @tobiu saw — but nothing in the live config path drives a split there.
  • Candidate 2 — not supported as reported. The indicators exist and their chips do compute a near-opaque ground (rgba(26,33,44,0.92) dark / rgba(247,249,252,0.94) light), but the set is symmetric: 5 indicators (center/top/right/bottom/left) + 4 chips (top/right/bottom/left), all -off when idle. No left/right-only overlay.
  • Theme masking — eliminated. Both scopes define the tokens with no fallback: dark #5eead4 / rgba(94,234,212,0.20); light #0f766e / rgba(15,118,110,0.14).

Preview.scss:54 is the only rule in the repo that paints the accent as a solid background, and it is reachable only via a split without region, or via tab-before / tab-after. So whatever @tobiu saw came through one of those two doors.

Latent defect found on the way (real, separate from the report)

.neo-dock-preview-split:not(.neo-dock-preview-region),
.neo-dock-preview-tab-before,
.neo-dock-preview-tab-after {

The :not(.neo-dock-preview-region) guard binds to the first selector only. tab-before/tab-after are unguarded — and getAffordanceVdom never adds region for the tab group, so today they always paint solid, which is the intended thin-bar presentation. It is correct by accident: if tab placements ever gain region mode, this rule silently paints a full region solid. Worth pinning with an arm regardless of the reported bug.

✅ ROOT CAUSE — 2026-08-31: a STALE COMPILED THEME CSS, not a code defect

Full zone-grammar sweep (one held drag, sampling every zone) in a current checkout: all nine zones compute rgba(94,234,212,0.2) at opacity: 0.85. Nothing paints solid. It confirms the ticket's placement asymmetry — pane left/right resolve to split-before/split-after, pane top/bottom to edge-top/edge-bottom, centre to tab-into — but every one of them lands in the translucent family.

The divergence was the JS and the CSS coming from different revisions.

60592e6a85 ("Split previews paint the resulting region, not the cut line", #17870) landed 08-30 08:43 and did two things: added the region class + :not(.neo-dock-preview-region) guard, and changed split geometry from a 6px line to a half-pane region. @tobiu reported at ~18:00; his dist/ theme CSS was not rebuilt until 23:56, hours after the report.

In dev the JS is served from source, so getAffordanceVdom was already emitting region and the new region-sized geometry — while the browser still had the pre-60592e6a85 stylesheet, which read:

.neo-dock-preview-edge, .neo-dock-preview-tab-into  { … accept-FILL (translucent) }
.neo-dock-preview-split, .neo-dock-preview-tab-*    { … accept (SOLID) }   // no :not(region) guard

Zone for zone, that reproduces the report exactly:

reported resolves to old CSS matches report
top / bottom "look nice" edge-* translucent ✅
full zones "look nice" tab-into translucent ✅
left / right "WAY darker… border color with no transparency" split-* SOLID accent ✅

The old rule had no region selector, so the new class was inert; the new geometry meanwhile made splits paint a half-pane block, so the solid accent covered a large area instead of a 6px guide. Hence "a WAY darker background overlay" rather than a bar.

Confirmed fixed: @tobiu's compiled dist/development/css/src/dashboard/dock/interaction/Preview.css is now byte-identical to a current build (4 :not(region) guards), which is precisely why the defect could not be reproduced.

The real lesson — a cross-artifact contract with no guard

60592e6a85 was a coupled change: a JS class/geometry change whose correctness depends on a separately built stylesheet. Nothing fails when only one half is current — the app silently renders the old visual grammar against new geometry. That is the actual defect class worth owning, and it is not specific to this commit.

Reframed remaining work

  • The reported visual bug needs no code fix; it is a rebuild.
  • The coverage gap is still real and still worth closing (PR #17893) — and note it would NOT have caught this, since the suite builds its own CSS. Recorded so nobody claims otherwise.
  • The stale-build hazard deserves its own ticket rather than being smuggled in here.

The instrument the remaining work needs

Every measurement above was taken with the overlay driven directly, because the affordance only exists while a drag is held open — and a completed drag destroys it before it can be read.

test/playwright/component/dashboard/DockSplitterIframePane.spec.mjs already proves the technique: page.mouse.down(), move, measure while the button is still down, then up(). Applying that to a dock drag and reading every painted layer under the pointer — not just the affordance — is what will name the cause.

The Fix

  1. Land the missing coverage (this ticket's committed deliverable): test/playwright/component/dashboard/DockPreviewRegionRender.spec.mjs mounts the real renderer, drives one affordance per edge plus both split positions through the real config, and asserts computed fill, border and opacity equality — the property no existing layer observes. It runs in the components suite, which does run in CI.
  2. Extend it to a held drag so the measurement covers what a user actually triggers, and read every layer under the pointer rather than the affordance alone.
  3. Fix whatever that names.

Acceptance Criteria

Restated 2026-08-31 after the root cause landed, following a reviewer intake gate on PR #17893 that Advances #17890 is not a valid close target. AC-3 and AC-4 as originally written presumed a code defect that does not exist: the reported divergence was a stale compiled theme CSS, not a rendering bug (see ROOT CAUSE above). Leaving them as-is would have made this ticket permanently uncloseable against a cause that was never real. The originals are struck through with what actually satisfied them, so nothing is quietly dropped.

  • A component-suite arm renders a drop affordance per edge and asserts computed fill/border/opacity are equal across edges, failing if any edge diverges.
  • The arm is non-vacuous: it must fail when the four edges are made to differ, and must not pass on a node that never received the per-edge classes.
  • A held-drag arm measures the overlay stack under the pointer during a real left/right drag and during a top/bottom drag. → Restated: a held-drag measurement of the live overlay is taken and recorded on this ticket. Satisfied — the full zone-grammar sweep above (one held drag, all nine zones sampled with getComputedStyle while the button is down) is that measurement. It is deliberately not shipped as a spec: it measured a runtime environment, and the defect it was written to catch turned out not to live in the code the spec would guard. elementsFromPoint also proved the wrong instrument — it skips pointer-events: none, so the overlays must be read directly.
  • The visual divergence @tobiu reported is reproduced by that arm, then fixed. → Restated: the divergence is root-caused and its remedy identified. Satisfied — cause: new JS against a pre-60592e6a85 stylesheet, reproduced zone-for-zone above; remedy: the theme rebuild @tobiu already ran, after which his compiled CSS is byte-identical to a current build. No code fix is required, and none is shipped. Durable prevention is a separate leaf: #17900 / PR #17902 (npm run check-themes).
  • The e2e symmetry spec's colour blind spot is either closed or explicitly recorded as owned by the component arm.

Not in this ticket, and deliberately so: the latent :not(...) selector-binding trap recorded above is real but unrelated to the report — it needs its own leaf rather than riding this one to green.

Out of Scope

  • Wiring e2e into CI. It is the root of layer 1 and outlives this bug; it needs its own ticket rather than being smuggled in here.
  • #17889 (Pointer Events migration) and #17883 (iframe drag freeze) — adjacent drag work, different defects.

Avoided Traps

  • Trusting the symmetric stylesheet as proof of a symmetric render. The rules are provably identical; the reported defect is still real. Reading source and reading a composited pixel are different measurements, and this ticket exists because the suite only ever did the former.
  • Asserting the fill by grepping the SCSS. That is precisely how the current unit coverage misses it.
  • Measuring a completed drag. The overlay is gone by then; the measurement must be taken with the button down.

Related

  • #17883 / PR #17887 — iframe drag freeze; same surface, different defect.
  • #17889 — Pointer Events + capture migration.
  • #17836 — DockLayouts rebuild.

Handoff Retrieval Hints

Retrieval Hint: "left/right dock drop overlay renders darker, stylesheet symmetric, no test reads a computed overlay colour" Retrieval Hint: resources/scss/src/dashboard/dock/interaction/Preview.scss; src/dashboard/dock/interaction/Preview.mjs:374

Origin Session ID: 7fc710e4-f796-47bc-97b7-b61d1414b3a3