LearnNewsExamplesServices
Frontmatter
id17241
titleThe dock''s visual language lives in apps/, not `src/dashboard`
stateOpen
labels
enhancementdesignairefactoringarchitecture
assigneesneo-opus-grace
createdAtAug 16, 2026, 9:16 PM
updatedAtAug 23, 2026, 9:07 PM
githubUrlhttps://github.com/neomjs/neo/issues/17241
authorneo-opus-grace
commentsCount2
parentIssue13158
subIssues
17538 The dock splitter''s visual language lives in apps, not the engine
17633 The dock edge rail''s paint lives in the workstation, not the engine
subIssuesCompleted2
subIssuesTotal2
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
milestonev13.2

The dock's visual language lives in apps/, not src/dashboard

Open Backlog/active-chunk-16 enhancementdesignairefactoringarchitecture
neo-opus-grace
neo-opus-grace commented on Aug 16, 2026, 9:16 PM

Context

Surfaced by external review of the docking system and independently verified 2026-08-16. The reported symptom was that the FleetCockpit's splitter is invisible (#17211, finding 1). The cause is not a missing style — it is a style that was written into an app instead of the engine.

resources/scss/src/dashboard/Container.scss is the engine home for Neo.dashboard.Container. What it gives .neo-dashboard-dock-splitter (line 113):

  • flex-shrink, position, touch-action, z-index
  • a ::before whose only job is to widen the hit target — no paint of any kind
  • cursor: ew-resize / ns-resize (line 149)

No background, no border, no handle, no hover state, no active state. Line 159 states the intent plainly: "Demo-consumable minimal hooks only — the real visual language lands with the F-tranche."

resources/scss/src/apps/workstation/Workspace.scss:31-82 — an application stylesheet — carries the whole affordance: band background, inset ring, a 36×2px ::after grip, :hover glow, :active state, all correctly driven by the sanctioned --dock-transition-* tokens.

This is now a two-leaf parent

Splitting was not planned — it happened twice, each time because a reviewer found the parent's close target over-reaching what a PR actually delivered.

leaf scope delivery
#17522 rail-tab structural paint — both apps declared the same structure and differed only in values PR #17524
#17538 splitter affordance floor — the engine's neutral default was a design decision, made by @neo-fable and recorded below PR #17531

Each leaf owns its own Contract Ledger for the token surface it introduces, because that is where the surface and its evidence live; a parent ledger duplicating both would stale out the moment either leaf moved. This is the disposition of @neo-gpt's RA-4 on PR #17531.

What this parent still retains, unowned by either leaf: the dock edge-zone / edge-row / edge-band / edge-rail chrome, the dark-theme home decision (resources/scss/theme-neo-dark/dashboard/ does not exist for any dashboard component), and the cross-cutting net-LOC criterion below.

Whether this ticket should convert enhancementepic now that it coordinates two leaves is a real taxonomy question and is deliberately left open here rather than decided mid-lane; it belongs to whoever closes the parent out.

The Problem

The workstation owns the dock's visual language, so every other consumer of the same engine component inherits an unpainted drag target. That is why the splitter is discoverable in the workstation and invisible in the cockpit — the two are not styled by the same layer at all.

It is broader than the splitter. Workspace.scss styles nine distinct neo-dashboard-dock-* internals:

dock-edge-zone · dock-edge-row · dock-splitter · dock-splitter-horizontal::after · dock-splitter-vertical::after · dock-edge-band · dock-edge-rail · dock-edge-rail-top .neo-dashboard-dock-rail-tab · dock-edge-rail-bottom .neo-dashboard-dock-rail-tab

And it is not the only app doing it. Three application stylesheets reach into dock internals: apps/workstation/Workspace.scss, apps/workstation/Viewport.scss, and apps/agentos/fleet/FleetCockpit.scss — the last carrying its own partial horizontal-splitter override at line 134.

So the dock's chrome is defined in three app stylesheets and its engine home defines almost none of it. Each new consumer either re-implements the language or ships unpainted, and there is no layer where "what a dock looks like" is answerable.

A second gap sits underneath: resources/scss/theme-neo-dark/dashboard/ does not exist. There is no dark-theme layer for any dashboard component. Whatever paint is promoted needs a decided home for its dark values (theme Globals tokens vs. a new theme directory), and that decision cannot be inferred from the current tree because the tree has never had to answer it.

The Architectural Reality

The .neo-dashboard token contract at the top of Container.scss is already the correct pattern and shows what is missing. It defines --dock-transition-duration / --dock-transition-easing in the engine, documents that a call-site literal is a contract violation, and lets the value be governed centrally through var() indirection. Motion is layered properly. Paint is not layered at all.

The workstation's implementation is also the right design — it reads --workstation-signal and --workstation-line, so it is already token-driven. It is in the wrong place. Its content is the promotion candidate almost verbatim; only its token names are app-specific.

That makes the fix net-negative: the engine grows a --dock-splitter-* token set plus the paint that reads it, and Workspace.scss's ~50 lines collapse to a handful of token overrides. The workstation keeps its exact look by re-skinning rather than re-implementing, and the cockpit inherits a visible splitter with no app-local CSS at all.

The Fix

Promote the dock's visual language from the app layer into resources/scss/src/dashboard/, expressed as neutral --dock-* tokens so each consumer skins rather than reimplements:

  • Engine defines the affordance (splitter band, grip, hover, active — and the rail/edge chrome that the same audit found in the app layer) reading --dock-* tokens.
  • apps/workstation/Workspace.scss reduces to token overrides mapping --dock-* onto --workstation-signal / --workstation-line.
  • apps/agentos/fleet/FleetCockpit.scss:134's partial override is re-read against the promoted base and kept only if it is a deliberate cockpit deviation.
  • The dark-theme home is decided and recorded — this is the open design question, not an implementation detail.

SCSS token/skin layers only; zero CSS-in-JS.

Decision Record impact

aligned-with ADR 0029 (learn/agentos/decisions/0029-docking-design.md — renamed 2026-08-21 from 0029-harness-docking-design.md by #17503; the harness misnomer is retired, persisted schema strings stay frozen). This ticket does not challenge the docking design — it moves that design's presentation layer into the substrate the ADR describes. Pickup should read the ADR in context; if it turns out the app-layer placement was a recorded decision rather than drift, this ticket converts to an amendment and says so.

Acceptance Criteria

  • .neo-dashboard-dock-splitter renders a discoverable affordance from the engine layer alone — verified in a consumer with no app-local dock CSS.
  • No application stylesheet defines dock chrome paint; the three named files contain token overrides or deliberate, commented deviations only. Verified by grep across resources/scss/src/apps/**, not only the three known files.
  • The workstation's rendered appearance is unchanged — proven against the existing visual baseline, both themes, not asserted. Instrument changed 2026-08-21, see #17518: the visual suite runs in no workflow and 8/8 goldens fail on an unmodified tree, so it cannot discriminate this change. Replaced by the criterion below, which is the better instrument for a token promotion regardless — it asserts the property rather than a rendering of it.
  • Computed styles are byte-identical before and after for the dock chrome in both themes: resting and hover background-color, color, border, box-shadow and font-family on .neo-dashboard-dock-rail-tab, captured in the workstation and FM scopes. Deterministic, and it fails on a token that resolves differently rather than on an anti-aliasing delta.
  • The promotion is tokens, not relocated declarations (per the Body-side architecture guide, #17514): the engine declares the structural paint and reads neutral --dock-* defaults; app layers set token VALUES only. Measured split — both apps currently duplicate background: transparent, border: 0, box-shadow: none, min-width: 0 and the glyph/text color: inherit fix, and differ solely on resting ink, hover background, hover ink and font-family.
  • The FleetCockpit splitter is visible and has hover/active states in both light and dark themes, with the dark-theme token home recorded in the PR body.
  • Net LOC across resources/scss/** is negative, or the increase is explained. Measured for the splitter slice (#17538 / PR #17531) at 033cc90339: +73 net — 134 added, 61 deleted. It is positive, and the composition is the explanation: the two app layers net −29 (workstation −32, FM +3) while the engine grows +102. That is the promotion working — paint leaving three app stylesheets and arriving once in the layer that owns it, with a token contract and its rationale attached. The original expectation of a negative total assumed a relocation; this is a promotion plus the neutral-default layer the relocation never had. The rail slice (#17522 / PR #17524) reports its own delta separately.
  • A mutation control: removing the promoted token block makes the engine-only consumer fail, so the test observes the promotion rather than the app override.

AC added 2026-08-21 after @neo-fable-clio flagged the PR #17505 seam, and verified against compiled selectors rather than SCSS source.

  • The promotion RESOLVES the rail-tab specificity tie rather than relocating it. The app rule compiles to .fm-fleet-cockpit .neo-dashboard-dock-rail-tab.neo-button and the engine rule to .neo-dashboard .neo-button.neo-dashboard-dock-rail-tabboth (0,3,0), which Container.scss:191 already names in-file: "Equal-specificity load-order ties are not a contract." Because CSS is emitted per source file rather than bundled, that tie is broken by runtime load order — which component instantiates first — not by any readable source order. Lifting the app block into src/dashboard/ puts both rules on the same side and changes their relative order, so a tie that resolves one way today could silently resolve the other. Voice and metrics must land as one rule, and the PR must state which declarations won and why.
  • A control proves the tie is gone, not merely re-sided: no two rules targeting .neo-dashboard-dock-rail-tab share equal specificity after the promotion — asserted on compiled selectors, since the source-level reading is (0,2,0) and wrong before SCSS nesting is applied.

Splitter promotion — the design answer (@neo-fable, 2026-08-22)

Recorded here rather than left in A2A: this is the decision the rail-tab leaf (#17522) deliberately did not make, and it is what the remaining half of this ticket implements.

Position: option 3 — the engine ships a discoverable affordance; an app opts out by token.

Discoverability is the engine's duty; aesthetics are the app's. A consumer who does nothing must get a findable drag target — the same fail-safe philosophy as the dock model's fail-closed restores. Shipping the minimal affordance instead would re-ship the #17211 defect as the engine default.

Three layers, not two. My original table treated the workstation splitter as one design to tokenise; it is two:

layer owner content
structure engine, untokened flex-shrink, position, touch-action, z-index, the hit-target ::before
affordance floor engine, token defaults modest visible band (--dock-splitter-bg), the 36×2 centred handle (--dock-splitter-handle-*), a plain hover raise, a functional active — without the pill ring and outer glow
identity app, token values workstation re-adds ring + glow; FM declares --dock-splitter-handle-size: 0

The ring and glow are workstation signal-language — they were born reading --workstation-signal — not neutral affordance. The affordance floor is what examples/dashboard/dock inherits, and it is what AC-1 is measured against.

FM's flat splitter becomes an explicit override, not an absence: --dock-splitter-handle-size: 0 as a design statement, grep-able. Whether FM keeps flat once a real handle exists is @neo-fable-clio's call on FM's design language, and the engine answer does not depend on it.

A debt to refuse at promotion time, not carry: the workstation active state currently uses opacity: 1 !important. That means something is being fought. The engine default must never need !important — if the promoted rule appears to, that is a specificity defect to fix inside the promotion.

  • The engine's affordance floor is discoverable with no app tokens set — asserted against examples/dashboard/dock, the consumer #17211 was filed about
  • Ring and glow live in workstation token values, not in the engine default
  • FM's flat splitter is an explicit --dock-splitter-handle-size: 0, not an omission
  • No !important in the promoted engine rule; if one seems required, the specificity cause is fixed instead
  • Computed styles unchanged in both apps, both themes

Out of Scope

  • #17211's rail-proportion and drawer-density work. This ticket owns the layering; #17211 owns cockpit ergonomics and consumes this. The splitter-visibility half of #17211 is discharged by this ticket landing.
  • #17209 (pane skins and drawer shell contract).
  • Dock behavior, drag mechanics, and the DockSplitter component API — presentation only.
  • apps/workstation/Viewport.scss's non-dock content.

Avoided Traps

  • Fixing the cockpit by copying the workstation's SCSS into FleetCockpit.scss. That is the fastest route to an invisible splitter and it is how the current state arose — a third copy makes the next consumer's bug certain.
  • Promoting the workstation's tokens as-is. --workstation-signal in an engine stylesheet inverts the dependency it is meant to fix.
  • Treating the missing dark-theme directory as an oversight to fill silently. No dashboard component has a dark layer today; adding one is a structural decision that belongs in the PR body.

Related

#17211 (cockpit ergonomics — consumes this; its splitter-visibility AC is discharged here) · #17209 (pane skins / drawer shell) · #13158 (QT-parity docking polish epic) · ADR 0029

Live latest-open sweep: latest 20 open issues checked 2026-08-16T19:14:37Z; no equivalent — #17211 and #17209 are the same escalation split by surface, and neither owns the engine/app layering. A2A in-flight claim sweep: 30 most recent messages, newest 2026-08-16T14:10Z; no claim on this scope.

Origin Session ID: b17338dd-b474-494f-b08c-683044de2ddb Retrieval Hint: "dock splitter visual language workstation Workspace.scss promote to dashboard Container.scss tokens"

tobiu referenced in commit 5d23f96 - "The dock splitter's visual language lives in apps, not the engine (#17531) on Aug 22, 2026, 3:52 PM
tobiu referenced in commit 55a714c - "Both apps re-declare the dock rail tab's structural paint (#17524) on Aug 22, 2026, 5:30 PM