shell: menus readable under every theme; account rows no longer collapse in Windows 98

Owner, live 2026-10-02: the account switcher's rows overlapped (names and handles collided) and
"Not signed in yet" was unreadable. Causes: edges.css forced every menu button to 26px, which squashed the
two-line account rows; the status inks (#f0b14a signed out, #ff8a80 errors) and the hint ink (a 38% fade of
the secondary text) were Night Ink literals, 2.1 to 3.6:1 on other themes' menus. Now the forced height is
gone, status, hint and secondary inks are measured on the menu surface (the face in bevelled themes) at
4.5:1 (--wb-c-warn, --wb-c-danger-ink, --wb-c-faint, --wb-c-muted), every line of a selected bevelled row
takes the selection's ink, and wayBetter's own default hint ink is legible. Checked: every shell menu
(accounts, app menu, tab menu, workspaces, palette) under 14 themes with an overlap and contrast scan
(verification/design-pass/1.0.2/menus-shots.mjs); new frame test for the inks.
This commit is contained in:
CRTkafa 2026-10-02 01:43:36 +07:00
parent 16e6555013
commit d7903e80ce
6 changed files with 30 additions and 9 deletions

View file

@ -29,7 +29,7 @@
.bt-acct-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; } .bt-acct-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.bt-acct-name { display: flex; align-items: center; gap: 6px; min-width: 0; font: 600 13.5px/1.15 var(--bt-ui); color: var(--bt-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .bt-acct-name { display: flex; align-items: center; gap: 6px; min-width: 0; font: 600 13.5px/1.15 var(--bt-ui); color: var(--bt-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bt-acct-meta { font: 400 12px/1.2 var(--bt-ui); color: var(--bt-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; } .bt-acct-meta { font: 400 12px/1.2 var(--bt-ui); color: var(--bt-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
.bt-acct-meta.is-signed-out, .bt-acct-meta.is-new { color: #f0b14a; } .bt-acct-meta.is-signed-out, .bt-acct-meta.is-new { color: var(--bt-warn); }
.bt-acct-count { color: var(--bt-muted); } .bt-acct-count { color: var(--bt-muted); }
.bt-acct-badge { .bt-acct-badge {
flex: none; padding: 2px 6px; border-radius: calc(999px * var(--bt-rs, 1)); background: rgb(var(--bt-text-rgb) / .07); color: var(--bt-text-2); flex: none; padding: 2px 6px; border-radius: calc(999px * var(--bt-rs, 1)); background: rgb(var(--bt-text-rgb) / .07); color: var(--bt-text-2);
@ -61,8 +61,8 @@
.bt-acct-sub .bt-menu-keys { font-size: 11px; } .bt-acct-sub .bt-menu-keys { font-size: 11px; }
.bt-menu .bt-acct-sub button[aria-checked=true]:disabled { color: var(--bt-text-2); } .bt-menu .bt-acct-sub button[aria-checked=true]:disabled { color: var(--bt-text-2); }
.bt-menu .bt-acct-sub button[aria-checked=true]:disabled > .bt-glyph { color: var(--bt-accent); opacity: 1; } .bt-menu .bt-acct-sub button[aria-checked=true]:disabled > .bt-glyph { color: var(--bt-accent); opacity: 1; }
.bt-menu .bt-acct-danger { color: #ff8a80; } .bt-menu .bt-acct-danger { color: var(--bt-danger-ink); }
.bt-menu .bt-acct-danger > .bt-glyph { color: #ff8a80; } .bt-menu .bt-acct-danger > .bt-glyph { color: var(--bt-danger-ink); }
.bt-acct-confirm { .bt-acct-confirm {
display: flex; flex-direction: column; gap: 10px; margin: 2px 6px 6px; padding: 10px 12px; border-radius: calc(9px * var(--bt-rs, 1)); display: flex; flex-direction: column; gap: 10px; margin: 2px 6px 6px; padding: 10px 12px; border-radius: calc(9px * var(--bt-rs, 1));
@ -82,7 +82,7 @@
.bt-acct-add-icon .bt-glyph { width: 15px; height: 15px; } .bt-acct-add-icon .bt-glyph { width: 15px; height: 15px; }
.bt-menu .bt-acct-add:hover .bt-acct-add-icon { box-shadow: inset 0 0 0 1px rgb(var(--bt-accent-rgb) / .6); color: var(--bt-accent-ink); } .bt-menu .bt-acct-add:hover .bt-acct-add-icon { box-shadow: inset 0 0 0 1px rgb(var(--bt-accent-rgb) / .6); color: var(--bt-accent-ink); }
.bt-acct-error { padding: 6px 10px; color: #ff8a80; font: 12px/1.4 var(--bt-ui); } .bt-acct-error { padding: 6px 10px; color: var(--bt-danger-ink); font: 12px/1.4 var(--bt-ui); }
/* Each pane's account, left of its address bar (same color as the tab's account dot). */ /* Each pane's account, left of its address bar (same color as the tab's account dot). */
.bt-pane-account { .bt-pane-account {

View file

@ -78,9 +78,13 @@ export function chromeVars(theme: ChromeTheme, own = false): Record<string, stri
'--wb-c-ground': bg, '--wb-c-chrome': surface, '--wb-c-field': mix(surface, elevated, 0.45), '--wb-c-ground': bg, '--wb-c-chrome': surface, '--wb-c-field': mix(surface, elevated, 0.45),
'--wb-c-raised': elevated, '--wb-c-raised-2': mix(elevated, border, 0.45), '--wb-c-raised': elevated, '--wb-c-raised-2': mix(elevated, border, 0.45),
'--wb-c-line': border, '--wb-c-line-soft': mix(border, bg, 0.45), '--wb-c-line-strong': mix(border, text, 0.14), '--wb-c-line': border, '--wb-c-line-soft': mix(border, bg, 0.45), '--wb-c-line-strong': mix(border, text, 0.14),
'--wb-c-text': text, '--wb-c-text-2': mix(text, muted, 0.4), '--wb-c-muted': muted, '--wb-c-faint': mix(muted, bg, 0.38), '--wb-c-text': text, '--wb-c-text-2': mix(text, muted, 0.4), '--wb-c-muted': legible(muted, elevated, 4.5),
// Shortcut hints and section labels sit on the menus' raised surface: they keep 4.5:1 there (a 38% fade toward the ground read 2.1 to 3.6:1).
'--wb-c-faint': legible(mix(muted, bg, 0.38), elevated, 4.5),
'--wb-c-text-rgb': channels(text), '--wb-c-accent': accent, '--wb-c-accent-rgb': channels(accent), '--wb-c-accent-ink': ink, '--wb-c-text-rgb': channels(text), '--wb-c-accent': accent, '--wb-c-accent-rgb': channels(accent), '--wb-c-accent-ink': ink,
'--wb-c-scheme': dark ? 'dark' : 'light', '--wb-c-scheme': dark ? 'dark' : 'light',
// Status inks (a signed-out account, an error) readable on the menus' raised surface, whatever its shade.
'--wb-c-warn': legible(dark ? '#f0b14a' : '#9a5b00', elevated, 4.5), '--wb-c-danger-ink': legible(dark ? '#ff8a80' : '#b3261e', elevated, 4.5),
// The chrome's marks (active tab, palette edge) in the theme's accent instead of wayBetter's brush. // The chrome's marks (active tab, palette edge) in the theme's accent instead of wayBetter's brush.
'--wb-c-brush': `linear-gradient(90deg, ${accent}, ${accent})`, '--wb-c-brush': `linear-gradient(90deg, ${accent}, ${accent})`,
...(dark ? {} : { '--wb-c-shadow-pop': `0 18px 44px -14px rgba(${hexToRgb(mix(text, bg, 0.2)).join(', ')}, .28), 0 3px 10px rgba(0, 0, 0, .08)` }), ...(dark ? {} : { '--wb-c-shadow-pop': `0 18px 44px -14px rgba(${hexToRgb(mix(text, bg, 0.2)).join(', ')}, .28), 0 3px 10px rgba(0, 0, 0, .08)` }),
@ -114,6 +118,9 @@ function bevelVars(colors: NonNullable<ChromeTheme['colors']>): Record<string, s
'--wb-c-title-from': from, '--wb-c-title-to': colors.accent, '--wb-c-title-text': titleText, '--wb-c-title-from': from, '--wb-c-title-to': colors.accent, '--wb-c-title-text': titleText,
// Selected rows (menus, lists): the title bar's own color, as in a 98 menu. // Selected rows (menus, lists): the title bar's own color, as in a 98 menu.
'--wb-c-select': from, '--wb-c-select-text': titleText, '--wb-c-select': from, '--wb-c-select-text': titleText,
// Menus are the face: status inks are measured on it.
'--wb-c-muted': legible(colors.muted, face, 4.5), '--wb-c-faint': legible(colors.muted, face, 4.5),
'--wb-c-warn': legible('#806000', face, 4.5), '--wb-c-danger-ink': legible('#a00000', face, 4.5),
}; };
} }

View file

@ -97,12 +97,13 @@ html[data-wb-edges=bevel] :is(.bt-profile-switcher[aria-expanded=true], .bt-upda
* Menus, the palette, popovers: a window edge on the face; the row under the pointer is selected. */ * Menus, the palette, popovers: a window edge on the face; the row under the pointer is selected. */
html[data-wb-edges=bevel] :is(.bt-menu, .bt-palette) { border: 0; background: var(--bt-bv-face); box-shadow: var(--bt-bv-window); color: var(--bt-bv-text); } html[data-wb-edges=bevel] :is(.bt-menu, .bt-palette) { border: 0; background: var(--bt-bv-face); box-shadow: var(--bt-bv-window); color: var(--bt-bv-text); }
html[data-wb-edges=bevel] .bt-menu { padding: 3px; } html[data-wb-edges=bevel] .bt-menu { padding: 3px; }
html[data-wb-edges=bevel] .bt-menu button { height: 26px; }
html[data-wb-edges=bevel] .bt-menu button:hover:not(:disabled), html[data-wb-edges=bevel] .bt-menu button:focus-visible, html[data-wb-edges=bevel] .bt-menu button:hover:not(:disabled), html[data-wb-edges=bevel] .bt-menu button:focus-visible,
html[data-wb-edges=bevel] .bt-menu button[aria-expanded=true] { background: var(--bt-bv-select); color: var(--bt-bv-select-text); outline: 0; } html[data-wb-edges=bevel] .bt-menu button[aria-expanded=true] { background: var(--bt-bv-select); color: var(--bt-bv-select-text); outline: 0; }
html[data-wb-edges=bevel] .bt-menu button:hover:not(:disabled) :is(.bt-glyph, .bt-menu-keys, small, svg), html[data-wb-edges=bevel] .bt-menu button:hover:not(:disabled) :is(.bt-glyph, .bt-menu-keys, small, svg),
html[data-wb-edges=bevel] .bt-menu button:focus-visible :is(.bt-glyph, .bt-menu-keys, small, svg), html[data-wb-edges=bevel] .bt-menu button:focus-visible :is(.bt-glyph, .bt-menu-keys, small, svg),
html[data-wb-edges=bevel] .bt-menu button[aria-expanded=true] :is(.bt-glyph, .bt-menu-keys, small, svg) { color: var(--bt-bv-select-text); } html[data-wb-edges=bevel] .bt-menu button[aria-expanded=true] :is(.bt-glyph, .bt-menu-keys, small, svg) { color: var(--bt-bv-select-text); }
/* Every line of a selected row (a title and its subline, as in the workspaces and accounts menus) takes the selection's ink. */
html[data-wb-edges=bevel] .bt-menu button:is(:hover:not(:disabled), :focus-visible, [aria-expanded=true]) * { color: var(--bt-bv-select-text) !important; }
html[data-wb-edges=bevel] .bt-menu button > .bt-glyph { color: var(--bt-bv-text); } html[data-wb-edges=bevel] .bt-menu button > .bt-glyph { color: var(--bt-bv-text); }
html[data-wb-edges=bevel] .bt-menu button:disabled { color: var(--bt-bv-sh); text-shadow: 1px 1px var(--bt-bv-hi); } html[data-wb-edges=bevel] .bt-menu button:disabled { color: var(--bt-bv-sh); text-shadow: 1px 1px var(--bt-bv-hi); }
html[data-wb-edges=bevel] .bt-menu-keys { color: var(--bt-bv-text); } html[data-wb-edges=bevel] .bt-menu-keys { color: var(--bt-bv-text); }

View file

@ -12,9 +12,10 @@
:root, .wb-pane-theme { :root, .wb-pane-theme {
--bt-ground: var(--wb-c-ground, #07080c); --bt-chrome: var(--wb-c-chrome, #0d1017); --bt-field: var(--wb-c-field, #11141c); --bt-raised: var(--wb-c-raised, #141823); --bt-raised-2: var(--wb-c-raised-2, #191e2b); --bt-ground: var(--wb-c-ground, #07080c); --bt-chrome: var(--wb-c-chrome, #0d1017); --bt-field: var(--wb-c-field, #11141c); --bt-raised: var(--wb-c-raised, #141823); --bt-raised-2: var(--wb-c-raised-2, #191e2b);
--bt-line: var(--wb-c-line, #1e2333); --bt-line-soft: var(--wb-c-line-soft, #161a26); --bt-line-strong: var(--wb-c-line-strong, #2a3044); --bt-line: var(--wb-c-line, #1e2333); --bt-line-soft: var(--wb-c-line-soft, #161a26); --bt-line-strong: var(--wb-c-line-strong, #2a3044);
--bt-text: var(--wb-c-text, #eef0f7); --bt-text-2: var(--wb-c-text-2, #c3c7d4); --bt-muted: var(--wb-c-muted, #8a90a6); --bt-faint: var(--wb-c-faint, #5d6379); --bt-text: var(--wb-c-text, #eef0f7); --bt-text-2: var(--wb-c-text-2, #c3c7d4); --bt-muted: var(--wb-c-muted, #8a90a6); --bt-faint: var(--wb-c-faint, #8a90a6);
/* Space-separated channels for translucent tints: rgb(var(--bt-text-rgb) / .06). */ /* Space-separated channels for translucent tints: rgb(var(--bt-text-rgb) / .06). */
--bt-text-rgb: var(--wb-c-text-rgb, 238 240 247); --bt-accent-rgb: var(--wb-c-accent-rgb, 29 155 240); --bt-text-rgb: var(--wb-c-text-rgb, 238 240 247); --bt-accent-rgb: var(--wb-c-accent-rgb, 29 155 240);
--bt-warn: var(--wb-c-warn, #f0b14a); --bt-danger-ink: var(--wb-c-danger-ink, #ff8a80);
--bt-accent: var(--wb-c-accent, #1d9bf0); --bt-accent-ink: var(--wb-c-accent-ink, #cfe6ff); --bt-accent-soft: rgb(var(--bt-accent-rgb) / .14); --bt-accent: var(--wb-c-accent, #1d9bf0); --bt-accent-ink: var(--wb-c-accent-ink, #cfe6ff); --bt-accent-soft: rgb(var(--bt-accent-rgb) / .14);
--bt-hover: rgb(var(--bt-text-rgb) / .055); --bt-press: rgb(var(--bt-text-rgb) / .09); --bt-hover: rgb(var(--bt-text-rgb) / .055); --bt-press: rgb(var(--bt-text-rgb) / .09);
--bt-ring: 0 0 0 1px rgb(var(--bt-accent-rgb) / .55), 0 0 0 4px rgb(var(--bt-accent-rgb) / .16); --bt-ring: 0 0 0 1px rgb(var(--bt-accent-rgb) / .55), 0 0 0 4px rgb(var(--bt-accent-rgb) / .16);
@ -460,7 +461,7 @@ html.bt-raised .bt-deck :is(.bt-col-head, .bt-compose-head) { background-color:
.bt-menu .bt-profile-danger { background: var(--bt-danger); color: #fff; } .bt-menu .bt-profile-danger { background: var(--bt-danger); color: #fff; }
.bt-menu .bt-profile-danger:hover:not(:disabled) { background: #d93d42; } .bt-menu .bt-profile-danger:hover:not(:disabled) { background: #d93d42; }
.bt-menu .bt-profile-cancel { background: var(--bt-hover); } .bt-menu .bt-profile-cancel { background: var(--bt-hover); }
.bt-profile-error { padding: 6px 10px; color: #ff8a80; font: 12px/1.4 var(--bt-ui); } .bt-profile-error { padding: 6px 10px; color: var(--bt-danger-ink); font: 12px/1.4 var(--bt-ui); }
/* ------------------------------------------------------------------------------------------------ /* ------------------------------------------------------------------------------------------------
* Command palette (Ctrl+K) and the composer drafts picker, which reuses it. */ * Command palette (Ctrl+K) and the composer drafts picker, which reuses it. */

View file

@ -63,7 +63,7 @@
.bt-menu .bt-ws-danger { background: var(--bt-danger); color: #fff; } .bt-menu .bt-ws-danger { background: var(--bt-danger); color: #fff; }
.bt-menu .bt-ws-danger:hover:not(:disabled) { background: #d93d42; } .bt-menu .bt-ws-danger:hover:not(:disabled) { background: #d93d42; }
.bt-menu .bt-ws-cancel { background: var(--bt-hover); } .bt-menu .bt-ws-cancel { background: var(--bt-hover); }
.bt-ws-error { padding: 6px 10px; color: #ff8a80; font: 12px/1.4 var(--bt-ui); } .bt-ws-error { padding: 6px 10px; color: var(--bt-danger-ink); font: 12px/1.4 var(--bt-ui); }
/* Quiet chips: only while there is something to say. */ /* Quiet chips: only while there is something to say. */
.bt-tb-chip { .bt-tb-chip {

View file

@ -111,6 +111,18 @@ test('chrome variables: none for the own style, fonts only without colors, every
for (const name of Object.keys(vars)) assert.ok(CHROME_VAR_NAMES.includes(name), name); for (const name of Object.keys(vars)) assert.ok(CHROME_VAR_NAMES.includes(name), name);
}); });
test('chrome variables: hints and status inks keep 4.5:1 on the menus of light, dark and bevelled themes', async () => {
const { contrast } = await import('../src/frame/chromeTheme.ts');
const light = { bg: '#fdf6e3', surface: '#f5efdc', elevated: '#eee8d5', border: '#d6cfb8', text: '#073642', muted: '#657b83', accent: '#268bd2' };
const dark = { bg: '#1a1b26', surface: '#1f2130', elevated: '#24283b', border: '#2f3549', text: '#c0caf5', muted: '#9099c4', accent: '#7aa2f7' };
for (const colors of [light, dark]) {
const vars = chromeVars({ colors });
for (const name of ['--wb-c-faint', '--wb-c-muted', '--wb-c-warn', '--wb-c-danger-ink']) assert.ok(contrast(vars[name]!, colors.elevated) >= 4.5, `${name} on ${colors.elevated}`);
}
const bevel = chromeVars({ colors: { ...light, page: '#c0c0c0', elevated: '#dfdfdf' }, edges: 'bevel' });
for (const name of ['--wb-c-faint', '--wb-c-muted', '--wb-c-warn', '--wb-c-danger-ink']) assert.ok(contrast(bevel[name]!, '#c0c0c0') >= 4.5, `${name} on the face`);
});
test('chrome variables: radii scale with the theme, a bevelled theme gets 98-style edges from its own face', () => { test('chrome variables: radii scale with the theme, a bevelled theme gets 98-style edges from its own face', () => {
const colors = { bg: '#ffffff', surface: '#f4f4f4', elevated: '#dfdfdf', border: '#808080', text: '#000000', muted: '#3c3c3c', accent: '#0000a8', page: '#c0c0c0', inverse: '#000080', onInverse: '#ffffff' }; const colors = { bg: '#ffffff', surface: '#f4f4f4', elevated: '#dfdfdf', border: '#808080', text: '#000000', muted: '#3c3c3c', accent: '#0000a8', page: '#c0c0c0', inverse: '#000080', onInverse: '#ffffff' };
assert.equal(chromeVars({ colors, radius: 0 })['--wb-c-rs'], '0'); assert.equal(chromeVars({ colors, radius: 0 })['--wb-c-rs'], '0');