refactor(ui): one base .btn:disabled rule, dedup the disabled affordance #78

Merged
lz merged 1 commit from fix/btn-disabled-affordance into main 2026-07-17 17:56:01 +02:00
Owner

Reworked to reduce LOC and repetition rather than add to them. Net −1 line, one fewer CSS rule, and the opacity: 0.4; cursor: not-allowed affordance now appears exactly once in the whole codebase (was two places).

 nexus/src/app.css                                  | 8 +++++---
 nexus/src/lib/components/SessionTerminal.svelte    | 6 ------
 .../config-explorer/InstallSkillModal.svelte       | 3 +++
 3 files changed, 8 insertions(+), 9 deletions(-)

What it consolidates

Before, the disabled-button affordance lived in three partly-duplicated places:

  • app.css.btn.primary:disabled { opacity: 0.4; cursor: not-allowed } (primary only)
  • SessionTerminal.svelte → a scoped .disconnected-overlay .btn:disabled { opacity: 0.4; cursor: not-allowed } (copy #2, added to work around the gap)
  • plain .btn everywhere else → no disabled affordance at all (the original bug: Reconnect, "Install all remaining" looked clickable while ignoring clicks)

After, one base rule covers every button:

.btn:hover:not(:disabled)         { background: var(--surface-2); }   /* was .btn:hover */
.btn:disabled                     { opacity: 0.4; cursor: not-allowed; }   /* new, the only copy */
.btn.primary:hover:not(:disabled) { opacity: 0.92; ... }   /* was .btn.primary:hover */
/* .btn.primary:disabled — DELETED */

The trick that lets .btn.primary:disabled be deleted rather than kept: guarding both :hover rules with :not(:disabled) means a disabled primary matches no hover rule, so the base .btn:disabled wins outright. That also removes the specificity-ordering fragility the earlier revision of this PR had to document with a "don't merge these rules" comment — now there's nothing to trip over.

Two consequences, both handled

  • .disconnected-overlay .btn:disabled (from #73) is now redundant — removed, along with its 5-line comment that argued against going global. The Reconnect button dims via the base rule.
  • .install-action chips carry .btn, so the base dim would fade the green "installed" / accent "installing" status badges to 40%. They already had a .install-action:disabled rule; adding opacity: 1 there (one line) opts all of them out uniformly — their colour is the status, not a dead control. Trade-off: an idle row disabled by installAllBusy no longer dims either; acceptable, it still shows cursor: not-allowed.

Verification

Browser-verified against the loaded and compiled CSS (suite is node-only, no DOM), measuring computed styles:

case result
plain disabled .btn 0.4 / not-allowedthe fix
enabled .btn on hover still lights (rgb(22,22,22))
disabled .btn on hover does not light (rgb(10,10,10))
hovered disabled PRIMARY holds 0.4 — proves deleting .btn.primary:disabled is safe
.install-action installing / installed stay opacity 1, colours intact
Reconnect button dims via the base rule (scoped rule gone)

728 tests pass, lint clean, typecheck 0 errors.


History: opened as an additive fix stacked on #71/#73 (both since merged to main); rebased onto main and reworked into this net-reducing consolidation.

Reworked to **reduce** LOC and repetition rather than add to them. **Net −1 line, one fewer CSS rule**, and the `opacity: 0.4; cursor: not-allowed` affordance now appears exactly **once** in the whole codebase (was two places). ``` nexus/src/app.css | 8 +++++--- nexus/src/lib/components/SessionTerminal.svelte | 6 ------ .../config-explorer/InstallSkillModal.svelte | 3 +++ 3 files changed, 8 insertions(+), 9 deletions(-) ``` ## What it consolidates Before, the disabled-button affordance lived in three partly-duplicated places: - `app.css` → `.btn.primary:disabled { opacity: 0.4; cursor: not-allowed }` (primary only) - `SessionTerminal.svelte` → a scoped `.disconnected-overlay .btn:disabled { opacity: 0.4; cursor: not-allowed }` (copy #2, added to work around the gap) - plain `.btn` everywhere else → **no disabled affordance at all** (the original bug: Reconnect, "Install all remaining" looked clickable while ignoring clicks) After, one base rule covers every button: ```css .btn:hover:not(:disabled) { background: var(--surface-2); } /* was .btn:hover */ .btn:disabled { opacity: 0.4; cursor: not-allowed; } /* new, the only copy */ .btn.primary:hover:not(:disabled) { opacity: 0.92; ... } /* was .btn.primary:hover */ /* .btn.primary:disabled — DELETED */ ``` The trick that lets `.btn.primary:disabled` be **deleted** rather than kept: guarding both `:hover` rules with `:not(:disabled)` means a disabled primary matches no hover rule, so the base `.btn:disabled` wins outright. That also removes the specificity-ordering fragility the earlier revision of this PR had to document with a "don't merge these rules" comment — now there's nothing to trip over. ## Two consequences, both handled - **`.disconnected-overlay .btn:disabled` (from #73) is now redundant — removed**, along with its 5-line comment that argued *against* going global. The Reconnect button dims via the base rule. - **`.install-action` chips carry `.btn`**, so the base dim would fade the green "installed" / accent "installing" status badges to 40%. They already had a `.install-action:disabled` rule; adding `opacity: 1` there (one line) opts all of them out uniformly — their colour is the status, not a dead control. **Trade-off:** an idle row disabled by `installAllBusy` no longer dims either; acceptable, it still shows `cursor: not-allowed`. ## Verification Browser-verified against the loaded and compiled CSS (suite is node-only, no DOM), measuring computed styles: | case | result | |---|---| | plain disabled `.btn` | `0.4` / `not-allowed` — **the fix** | | enabled `.btn` on hover | still lights (`rgb(22,22,22)`) | | disabled `.btn` on hover | does **not** light (`rgb(10,10,10)`) | | **hovered disabled PRIMARY** | holds `0.4` — proves deleting `.btn.primary:disabled` is safe | | `.install-action` installing / installed | stay `opacity 1`, colours intact | | Reconnect button | dims via the base rule (scoped rule gone) | 728 tests pass, lint clean, typecheck 0 errors. --- *History: opened as an additive fix stacked on #71/#73 (both since merged to main); rebased onto main and reworked into this net-reducing consolidation.*
fix(ui): base disabled affordance for plain .btn
All checks were successful
ci / nexus (pull_request) Successful in 6m16s
ci / images (pull_request) Successful in 8m50s
65a4e0dcb3
app.css styled only `.btn.primary:disabled`. Every plain disabled `.btn`
rendered identically to an enabled one — full opacity, and still lighting up
on hover — so a disabled control looked clickable and silently ignored the
click. The Reconnect button (PR #73) hit this and had to carry a scoped
`.btn:disabled` of its own; the "Install all remaining" button in the skill
installer hits it too, with no local workaround.

Promote the affordance to the base rule:

  .btn:disabled          { opacity: 0.4; cursor: not-allowed; }
  .btn:hover:not(:disabled) { ... }   // was .btn:hover

The `:not(:disabled)` guard matches what the component-level buttons already
do (`.ib:hover:not(:disabled)`, `.install-action:hover:not(:disabled)`), so a
disabled button no longer lights up under the cursor.

Two call sites needed attention, both verified in a real browser:

- `.disconnected-overlay .btn:disabled` (added in #73) is now redundant with
  the base rule — removed. The Reconnect button still dims via the global rule
  (measured: opacity 0.4, cursor not-allowed).

- `.install-action.installing` / `.installed` are `disabled` as STATUS, not as
  unavailable controls — colour carries the meaning. The new base dim would
  fade the green "installed" badge to 40%, so both opt back out with
  `opacity: 1`. An idle install row disabled by `installAllBusy` is NOT
  exempted — it genuinely is unavailable and should dim.

`.btn.primary:disabled` is kept despite now-identical declarations: it must be
declared AFTER `.btn.primary:hover` to win at equal specificity (0,3,0),
otherwise a hovered disabled primary renders at opacity 0.92. Proven
load-bearing in-browser (0.4 with the rule, 0.92 without) — a comment says so
to stop a future "dedupe" from merging it away.

Verified in a real browser (the suite is node-only, no DOM): plain disabled
.btn now 0.4/not-allowed; enabled still lights on hover; disabled does not;
primary-disabled holds 0.4 on hover; install-action status chips stay at
opacity 1 while a blocked idle row dims. 725 tests pass, lint clean,
typecheck 0 errors.
lz force-pushed fix/btn-disabled-affordance from 65a4e0dcb3
All checks were successful
ci / nexus (pull_request) Successful in 6m16s
ci / images (pull_request) Successful in 8m50s
to af541f5619
All checks were successful
ci / nexus (pull_request) Successful in 7m8s
pr-image-cleanup / delete-pr-images (pull_request) Successful in 9s
ci / images (pull_request) Successful in 6m55s
2026-07-17 17:50:29 +02:00
Compare
lz changed title from fix(ui): base disabled affordance for plain .btn to refactor(ui): one base .btn:disabled rule, dedup the disabled affordance 2026-07-17 17:51:01 +02:00
lz merged commit 556b305d75 into main 2026-07-17 17:56:01 +02:00
lz deleted branch fix/btn-disabled-affordance 2026-07-17 17:56:02 +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!78
No description provided.