Skip to content

refactor(client): share the modal scrim and the destructive-button reds (UI/UX v3 G11 follow-up) - #70

Merged
mizu-jun merged 1 commit into
masterfrom
refactor/g11-followup-dialog-colours
Aug 21, 2026
Merged

refactor(client): share the modal scrim and the destructive-button reds (UI/UX v3 G11 follow-up)#70
mizu-jun merged 1 commit into
masterfrom
refactor/g11-followup-dialog-colours

Conversation

@mizu-jun

Copy link
Copy Markdown
Owner

What

Second half of the #62 follow-up (first half: #69). This takes the dialog surfaces.

The scrim — five surfaces, one of them token-aware

Five places veil the screen behind a modal. Only the settings panel derived its veil from the active scheme; the other four carried the same hard-coded [0.0, 0.0, 0.0, 0.55]:

Site Was
settings/mod.rs (panel) surface_0 + α 0.72 ✅
dialog.rs (consent) hard-coded black
dialog.rs (close-window) hard-coded black
settings/ssh_tab.rs (delete) hard-coded black
settings/keybindings_tab.rs (delete) hard-coded black

So on a light scheme a black veil sat behind a light panel, while the settings panel's own veil matched it. All five now call util::scrim_color, keeping surface_0 as the hue for the reason the settings panel already documented in-place — it is the deepest background in the scheme, so the veil stays in the panel's colour family in either theme. Alpha stays a parameter: the panel fades its scrim in with the open animation, the modals snap to SCRIM_ALPHA_FLOOR.

The destructive-confirmation button — the drift the plan predicted

The plan noted these reds "have already drifted apart", and they had:

Ssh Keybindings
Focused fill [0.498, 0.196, 0.196] [0.486, 0.180, 0.180]
Resting fill [0.235, 0.118, 0.118] tokens.surface_1
Label constant pale pink pale pink / dimmed pink

Both now call row::danger_button_colors, which blends semantic_error into surface_1 instead of using the raw ANSI red — the same darkening both sites were doing by hand, for the same reason: a saturated red leaves no headroom for a readable label.

The label split is measured, not aesthetic. On the focused fill the scheme's own foreground only reaches ~3.0:1, so the label comes from color_util::on_surface_text (added in #69), which picks whichever extreme the fill actually contrasts with:

Scheme State Label Contrast
Tokyo Night focused white 4.65:1
Gruvbox Light focused dark 5.00:1
both resting text_primary 6.55:1

Both dialog titles likewise move onto semantic_error through ensure_readable.

Test plan

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings — clean through every crate
  • cargo test --workspace — all pass, 0 failures
  • Four new tests: the scrim keeps surface_0's hue at the requested alpha, and a light scheme's veil is measurably lighter than a dark one's (a returning black literal fails this); the danger button clears MIN_TEXT_CONTRAST in both states across a dark and a light scheme, and its focused fill is measurably redder than its resting one
  • On-device visual check — not run. Joins the accepted-unverified backlog in docs/plans/ui-ux-modernization-v3.md. The light-scheme scrim is the item most worth a look: it is the one place this PR changes what an existing surface looks like rather than only where its value comes from.

Still out of scope

dialog.rs's close-window kill/cancel button colours and its two [0.10, 0.10, 0.10, 1.0] on-accent labels — a separate concern (button palette for a non-settings modal), left for a third PR. picker.rs's purple/green branding still needs a product decision, and color_util::resolve_color's no-palette fallbacks remain unreachable outside tests (explained in #69).

🤖 Generated with Claude Code

…ds (UI/UX v3 G11 follow-up)

Second half of the #62 follow-up, taking the dialog surfaces.

**The scrim.** Five surfaces veil the screen behind a modal, and only one
of them — the settings panel — derived its colour from the active scheme.
The consent dialog, the close-window dialog and the two settings-tab
delete confirmations each carried the same hard-coded `[0.0, 0.0, 0.0,
0.55]`, so on a light scheme a black veil sat behind a light panel while
the settings panel's own veil matched it. All five now call
`util::scrim_color`, which keeps `surface_0` as the hue for the reason the
settings panel already documented: it is the deepest background in the
scheme, so the veil stays in the panel's colour family in both themes.
Alpha stays a parameter — the settings panel fades its scrim in with the
open animation, the modals snap to `SCRIM_ALPHA_FLOOR`.

**The destructive-confirmation button.** The Ssh and Keybindings delete
dialogs had hand-mixed their own reds and had already drifted: `[0.498,
0.196, 0.196]` against `[0.486, 0.180, 0.180]` for the focused fill, plus
two different resting treatments (a dark red in one, `surface_1` in the
other) and two different label colours. Both now call
`row::danger_button_colors`, which blends `semantic_error` into
`surface_1` rather than using the raw ANSI red — the same darkening both
call sites had been doing by hand, and for the same reason: a saturated
red leaves no headroom for a readable label.

The two states take their label from different places, and the split is
measured rather than aesthetic. On the focused fill the scheme's own
foreground only reaches ~3.0:1, so the label comes from
`color_util::on_surface_text`, which picks whichever extreme the fill
actually contrasts with: 4.65:1 (Tokyo Night, white label) and 5.00:1
(Gruvbox Light, dark label). The resting fill is barely tinted, so
`text_primary` reads at 6.55:1 and keeps the button in the panel's type
colour. Both dialog titles likewise move onto `semantic_error` via
`ensure_readable`.

Four new tests: the scrim keeps `surface_0`'s hue at the requested alpha
and a light scheme's veil is measurably lighter than a dark one's (a
returning black literal fails this); the danger button clears
`MIN_TEXT_CONTRAST` in both states across a dark and a light scheme, and
its focused fill is measurably redder than its resting one.

On-device verification: not run. Joins the accepted-unverified backlog.
The light-scheme scrim is the item most worth a look — it is the one place
where this PR changes what an existing surface looks like rather than
where its value comes from.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@github-actions

Copy link
Copy Markdown

Coverage report


Generated by cargo llvm-cov (workspace minus nexterm-client-gpu and nexterm-i18n).

@mizu-jun
mizu-jun merged commit 7e71284 into master Aug 21, 2026
12 checks passed
@mizu-jun
mizu-jun deleted the refactor/g11-followup-dialog-colours branch August 21, 2026 13:33
mizu-jun added a commit that referenced this pull request Aug 21, 2026
…I/UX v3 G11 follow-up) (#71)

Third and last code half of the #62 follow-up: the close-window dialog's
Kill/Cancel buttons, and the consent dialog's selected-button fill.

The literals were `[0.75, 0.25, 0.25]` / `[0.95, 0.40, 0.40]` for Kill at
rest and selected, `[0.95, 0.85, 0.40]` for a selected Cancel, and
`[0.10, 0.10, 0.10]` for the label on any selected button (in two
dialogs). All now derive from `semantic_error` / `semantic_warning` via
two new helpers, `util::danger_fill` and `util::caution_fill`.

Kill deliberately does *not* reuse #70's `danger_button_colors`. That
helper answers "is this button focused", stepping from a barely-tinted
rest state to a mid blend. Kill has to read as the destructive choice
*before* it is selected — it sits next to Cancel — so it steps from the
mid blend to a strong one instead. Sharing the blend and not the
focused/unfocused rule is what keeps both readings intact.

The labels are the substance of this change. Selected-button labels were
a fixed near-black, which is wrong on any dark fill; they now come from
`on_surface_text`, chosen against the fill. Measuring that across the
nine built-in schemes turned up something a fixed blend strength cannot
fix: at 0.85 the error hue lands at a middling luminance on Nord
(4.42:1) and the warning hue does the same on Solarized (4.37:1) —
luminances where *neither* extreme has anything to contrast with. Used
raw, `semantic_warning` fails the same way, which is why `caution_fill`
blends rather than passing the token through.

`semantic_fill` therefore walks the blend back toward `surface_1` until
the label clears `MIN_TEXT_CONTRAST`, the same shape as
`row::ensure_readable`. It terminates because `surface_1` derives from
the scheme background — the end of the range a label can always be read
against. Some schemes get a slightly quieter fill than asked for; an
unreadable label on a destructive button is the worse trade.

The consent dialog's 3 px top stripe and its title keep the raw warning
hue: a line and text, not a fill.

Two new tests. One pins every fill/label pair the dialogs paint across
all nine schemes at 4.5:1 — it is the test that forced `caution_fill` to
exist, and it fails on `Nord` if the adaptive step is removed. The other
pins that a stronger blend always reads as redder, since that ordering
is what carries "dangerous" and "selected".

On-device verification: not run. Joins the accepted-unverified backlog.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
mizu-jun added a commit that referenced this pull request Aug 21, 2026
)

The follow-up entry has been an open checkbox holding a list of sites
since #62. It now records what was actually done in #69 / #70 / #71,
split by surface, plus the two sites that stay out and why they are
decisions rather than leftovers: `picker.rs`'s per-feature brand hues
need a product call, and `resolve_color`'s no-palette fallbacks sit on a
branch `render_frame` cannot reach outside tests.

Three findings are worth keeping in the plan rather than only in commit
messages, because each contradicts what the entry previously assumed:

- The delete-dialog reds had drifted further than recorded — not only the
  focused fill but the resting treatment and the label rule.
- The scrim was never "one literal in four files". It was an asymmetry:
  five modal surfaces, only one of them scheme-derived, so a light scheme
  put a black veil behind a light panel.
- No fixed blend strength can carry a semantic fill across all nine
  schemes. Nord (error, 4.42:1) and Solarized (warning used raw, 4.37:1)
  land at luminances where neither a near-black nor a near-white label
  has anything to contrast with, which is why `semantic_fill` steps the
  blend back until the label clears the floor.

The on-device backlog gains #69 / #70 / #71, with #70 flagged as the
highest-value item in the list: the scrim is the one place this work
changed what an existing surface *looks like* rather than where its
value comes from, and that was reasoned about, never seen. The same
section now also states what the colour work *did* get machine-verified,
so the distinction between measured readability and unverified
appearance stays explicit.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant