feat(dock): recede the inactive group's tabs in a split dock #119

Merged
lz merged 1 commit from feat/inactive-group-recede into main 2026-09-05 12:04:38 +02:00
Owner

Closes the known limitation shipped in #112 and #118: with all four --dv-*-tab-background-color set to transparent (so the session wash is a tab's only fill), nothing distinguished the focused dock group from an unfocused one. In a split there was no way to tell which half the next panel would open into.

Approach

Rather than add chrome, the group you are not acting in steps down one tier, using the three things the tab already paints: wash, underline, name colour.

dockview keeps exactly one .dv-active-group at all times — it is last-interacted selection, not DOM focus (BaseGrid.doSetGroupActive), and it never clears when focus leaves the dock. So an inactive group exists if and only if there are two or more, and these rules are inert until the dock is actually split. No group counting, no JS, no new tokens.

24 lines of CSS in one file.

The three levers are not equal

Measured across all eight SESSION_PALETTE colours, worst case:

lever measured verdict
wash ×0.5 1.10 : 1 --surface vs --surface-2 is 1.04:1 — decoration, not signal
icon + micro-caps @ 80% 4.67 : 1 AA
name --text--muted 5.13 : 1 (from 14.4) AA — the load-bearing lever
underline @ 45% 2.29 : 1 decorative, no floor to meet

The ordering is the reverse of the intuitive one. The name carries the signal; the wash cannot, and is present only to keep the four states coherent.

Two floors this respects:

  • Hue text stops at 80%. Below it the coral session's 9.5px micro-caps line drops under 4.5:1, and coral is the palette entry that governs.
  • Non-current tabs stay at --muted. Stepping them to --muted-2 measures 3.27:1 — under AA, for very little gain.

The underline is explicitly exempt: it is decorative, and the tab is already identified by its name's emphasis.

Why not the obvious alternatives

  • A darker fill on the inactive group's tabs — dead on arrival. Our three neutral surfaces measure 1.04–1.05:1 apart; you cannot see the boundary between them. This is also why dockview's own group-focus signal was already invisible here: three of its four cells mapped onto exactly those surfaces, and the fourth (--accent-dim) is the hue conflict #112 removed.
  • An accent rail on the focused group's strip — works, but adds chrome, and needs a :has() gate so it doesn't sit there permanently on an unsplit dock.

Implementation notes

  • calc() on the alpha rather than new tokens: --wash-tab is a bare number, so one multiplier expresses "one tier down" and keeps tracking the base values if they change.
  • color-mix against --sess, not rgba(var(--idc-rgb), …)--idc-rgb is unset on a tab with no scope, where an invalid var() computes to currentColor. --sess is always defined, and is transparent in exactly that case.

Verification

Gates: typecheck 4898 files / 0 errors, lint clean, 121 files / 1202 tests.

Driven in a real split in a running instance (a two-group layout restored through nexus.dock.layout.v2, which is the genuine fromJSON path):

  • dockview's invariant confirmed live — 2 groups, exactly 1 active and 1 inactive.
  • Inactive group, current tab: wash 0.07, name rgb(138,138,138), underline --sess / 0.45, hue text --sess / 0.8.
  • Inactive group, other tab: wash 0.03, name --muted, no underline.
  • Active group: unchanged — wash 0.14, #e8e8e8, full-strength hue.
  • Tab heights 44px on both sides — no layout shift.
  • Single group: zero inactive groups, and every tab value identical to before the change.

Also checked: Svelte emits all three rules into the built CSS (none pruned); the .dockview-theme-vs border-top rules that read the zeroed vars do not apply to our abyss theme; and the overflow dropdown is portalled to .dv-popover-anchor at the dockview root, so it is outside .dv-inactive-group and unaffected.

Known trade

Once the name drops to --muted, "which tab is current over there" rests on the wash step and the underline alone. That is intended — you are not acting in that group — but it is the thing to look at in use.

Still open, separate

Zeroing the four vars in #112 also emptied .dv-tabs-overflow-container .dv-active-tab, dockview's own "+N more" list, which has no current-panel highlight. It renders dockview's rows rather than SessionTab, so the fill there is free to use — a one-liner, not folded into this PR.

Closes the known limitation shipped in #112 and #118: with all four `--dv-*-tab-background-color` set to `transparent` (so the session wash is a tab's only fill), nothing distinguished the focused dock group from an unfocused one. In a split there was no way to tell which half the next panel would open into. ## Approach Rather than add chrome, the group you are **not** acting in steps down one tier, using the three things the tab already paints: **wash**, **underline**, **name colour**. dockview keeps exactly one `.dv-active-group` at all times — it is last-interacted *selection*, not DOM focus (`BaseGrid.doSetGroupActive`), and it never clears when focus leaves the dock. So an inactive group exists if and only if there are two or more, and these rules are **inert until the dock is actually split**. No group counting, no JS, no new tokens. 24 lines of CSS in one file. ## The three levers are not equal Measured across all eight `SESSION_PALETTE` colours, worst case: | lever | measured | verdict | |---|---|---| | wash ×0.5 | **1.10 : 1** | `--surface` vs `--surface-2` is 1.04:1 — decoration, not signal | | icon + micro-caps @ 80% | **4.67 : 1** | AA | | name `--text` → `--muted` | **5.13 : 1** (from 14.4) | AA — the load-bearing lever | | underline @ 45% | **2.29 : 1** | decorative, no floor to meet | The ordering is the reverse of the intuitive one. **The name carries the signal**; the wash cannot, and is present only to keep the four states coherent. Two floors this respects: - **Hue text stops at 80%.** Below it the coral session's 9.5px micro-caps line drops under 4.5:1, and coral is the palette entry that governs. - **Non-current tabs stay at `--muted`.** Stepping them to `--muted-2` measures 3.27:1 — under AA, for very little gain. The underline is explicitly exempt: it is decorative, and the tab is already identified by its name's emphasis. ## Why not the obvious alternatives - **A darker fill on the inactive group's tabs** — dead on arrival. Our three neutral surfaces measure 1.04–1.05:1 apart; you cannot see the boundary between them. This is also why dockview's own group-focus signal was already invisible here: three of its four cells mapped onto exactly those surfaces, and the fourth (`--accent-dim`) is the hue conflict #112 removed. - **An accent rail on the focused group's strip** — works, but adds chrome, and needs a `:has()` gate so it doesn't sit there permanently on an unsplit dock. ## Implementation notes - `calc()` on the alpha rather than new tokens: `--wash-tab` is a bare number, so one multiplier expresses "one tier down" and keeps tracking the base values if they change. - `color-mix` against `--sess`, **not** `rgba(var(--idc-rgb), …)` — `--idc-rgb` is unset on a tab with no scope, where an invalid `var()` computes to `currentColor`. `--sess` is always defined, and is `transparent` in exactly that case. ## Verification Gates: typecheck 4898 files / 0 errors, lint clean, 121 files / 1202 tests. Driven in a **real split** in a running instance (a two-group layout restored through `nexus.dock.layout.v2`, which is the genuine `fromJSON` path): - dockview's invariant confirmed live — 2 groups, exactly 1 active and 1 inactive. - Inactive group, current tab: wash `0.07`, name `rgb(138,138,138)`, underline `--sess / 0.45`, hue text `--sess / 0.8`. - Inactive group, other tab: wash `0.03`, name `--muted`, no underline. - Active group: unchanged — wash `0.14`, `#e8e8e8`, full-strength hue. - Tab heights `44px` on both sides — no layout shift. - **Single group: zero inactive groups, and every tab value identical to before the change.** Also checked: Svelte emits all three rules into the built CSS (none pruned); the `.dockview-theme-vs` `border-top` rules that read the zeroed vars do not apply to our `abyss` theme; and the overflow dropdown is portalled to `.dv-popover-anchor` at the dockview root, so it is outside `.dv-inactive-group` and unaffected. ## Known trade Once the name drops to `--muted`, "which tab is current *over there*" rests on the wash step and the underline alone. That is intended — you are not acting in that group — but it is the thing to look at in use. ## Still open, separate Zeroing the four vars in #112 also emptied `.dv-tabs-overflow-container .dv-active-tab`, dockview's own "+N more" list, which has no current-panel highlight. It renders dockview's rows rather than `SessionTab`, so the fill there *is* free to use — a one-liner, not folded into this PR.
feat(dock): recede the inactive group's tabs in a split dock
Some checks failed
ci / nexus (pull_request) Successful in 10m58s
ci / images (pull_request) Successful in 13m8s
ci / nexus (push) Has been cancelled
ci / images (push) Has been cancelled
pr-image-cleanup / delete-pr-images (pull_request) Successful in 15s
fe4677e24b
With all four --dv-*-tab-background-color set to transparent (#112), nothing
distinguished the focused dock group from an unfocused one — so in a split
there was no way to tell which half the next panel would open into.

Rather than add chrome, the group you are NOT acting in steps down one tier
using the three things the tab already paints: wash, underline, name colour.
dockview keeps exactly one .dv-active-group at all times, so the rules are
inert until the dock is actually split — no group counting, no gate.

Measured across all eight SESSION_PALETTE colours, worst case:

  wash x0.5                1.10:1   (--surface vs --surface-2 is 1.04:1)
  icon + micro-caps @ 80%  4.67:1   AA
  name --text -> --muted   5.13:1   AA  (from 14.4:1)
  underline @ 45%          2.29:1   decorative, no floor to meet

The name carries the signal; the wash cannot, and is there only to keep the
four states coherent. 80% is the floor on hue text — below it coral's 9.5px
micro-caps line drops under 4.5:1. Non-current tabs stay at --muted rather
than --muted-2, which measures 3.27:1.

Verified in a live split: wash 0.07/0.03, name #8a8a8a, underline at 45%,
hue text at 80%, tab heights unchanged at 44px, and a single-group dock
identical to before the change.
lz merged commit fe4677e24b into main 2026-09-05 12:04:38 +02:00
lz deleted branch feat/inactive-group-recede 2026-09-05 12:04:40 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus!119
No description provided.