Flatten the selection and the menu highlight; kill the Aero afterglow
Three things looked wrong, three different causes. The selected row carried a border. 7.css outlines tr.highlighted (#aaddfa) on top of its fill, and we had been restating that outline in the new blue — so the band had an edge, and on the cursor row the dotted focus rect made three concentric outlines around a single row. The reserved 1px border stays (it is what stops selection from shifting rows) but is transparent in every state; the fill paints under it, since background-clip is border-box. --w7-sel-bd and --w7-sel-bd-idle are gone with it. The context menu highlighted items with 7.css's translucent gradient wash — the same gradient the ListView selection just dropped. It now uses --w7-sel-bg, so selection reads identically wherever it appears. The hover-off animation was button.css bleeding into menu items, which ARE buttons: button::before is a glow overlay that fades out over a full second, and menu.css suppresses it with content:none on the :hover arm ONLY — so leaving an item brought the pseudo back with opacity animating 1 -> 0, a ghost trailing the pointer. Killing content in every state removes it. Same family, also fixed: button:focus runs pulse-anim infinitely, and this menu moves real focus between items for keyboard nav. menu-probe.ts now checks the highlight too, using hover-probe's :hover -> .simhover rewrite: it measures colour spread down a text-free strip of a force-hovered item, so "is it flat?" is a measurement. Verified the check can fail by injecting the old wash into the built stylesheet — spread 9, exit 2; the flat fill reads spread 0 at exactly #cbe8f6. The harness lost its selection outlines and nothing else, in both themes. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+18
-9
@@ -429,12 +429,17 @@ zeroed); the frame reads the state off its own child with
|
||||
existing `maximized` signal rather than subscribing to `windowState` twice.
|
||||
|
||||
**Selection band**: the file list's selected row is a FLAT Win7-Explorer blue
|
||||
(`--w7-sel-bg/-bd` = `#cbe8f6`/`#26a0da`), not 7.css's `--w7-el-grad-h` — that
|
||||
var is its Aero element-hover ramp and stays reserved for buttons and tabs,
|
||||
which still want the gloss. The unfocused pane gets `--w7-sel-bg-idle/-bd-idle`,
|
||||
Explorer's grey inactive selection; the band never disappears with focus, only
|
||||
weakens (the commander convention above). All four are dark-overridden in BOTH
|
||||
dark blocks.
|
||||
(`--w7-sel-bg`, `#cbe8f6`) with **no border** — not 7.css's `--w7-el-grad-h`,
|
||||
which is its Aero element-hover ramp and stays reserved for buttons and tabs
|
||||
that still want the gloss, and not its `#aaddfa` row outline, which stacked a
|
||||
second edge inside the fill and, with the cursor row's dotted rect, drew three
|
||||
concentric outlines on one row. Every row still reserves `border: 1px solid
|
||||
transparent` for geometry; the fill paints under it (`background-clip`
|
||||
defaults to `border-box`). The unfocused pane gets `--w7-sel-bg-idle`,
|
||||
Explorer's grey inactive selection — the band never disappears with focus,
|
||||
only weakens (the commander convention above). Both are dark-overridden in
|
||||
BOTH dark blocks. The context menu's item highlight uses the same
|
||||
`--w7-sel-bg`, so selection reads identically wherever it appears.
|
||||
|
||||
**Context menu**: pure 7.css via its real menu contract —
|
||||
`ul[role=menu] > li[role=menuitem] > button` for actions,
|
||||
@@ -446,9 +451,13 @@ one (`dist/gui/menu.css`), including the gutter stripe, disabled state and
|
||||
submenu arrows. Keyboard moves REAL focus between items (7.css styles
|
||||
`:focus-visible` like `:hover`); Enter/Space activate buttons natively. Only
|
||||
menu.css's hardcoded light literals are restated for dark, in `skinfix` — plus
|
||||
one structural override: `.w7-ctx` restores `position: fixed`, which 7.css's own
|
||||
`position: relative` on `ul[role=menu]` had been silently winning (see
|
||||
gotchas.md; `scripts/menu-probe.ts` is the regression test).
|
||||
three overrides keyed on the app's own `w7-ctx` marker: `position: fixed`
|
||||
(7.css's `position: relative` on `ul[role=menu]` had been silently winning),
|
||||
the flat `--w7-sel-bg` highlight in place of its gradient wash, and
|
||||
`content: none` on the item buttons' pseudo-elements, which kills both the
|
||||
1s Aero afterglow and the infinite `pulse-anim` on a focused item (see
|
||||
gotchas.md). `scripts/menu-probe.ts` regression-tests placement AND highlight
|
||||
flatness, each with a control that proves the check can fail.
|
||||
|
||||
**Font**: Segoe UI is absent off Windows and not redistributable. Noto Sans —
|
||||
the substitute 7.css itself names — is vendored (OFL-1.1, from
|
||||
|
||||
@@ -176,6 +176,20 @@ touching the adjacent code.
|
||||
bug**: nothing about the idle window changes. `scripts/menu-probe.ts` renders
|
||||
the menu at declared coordinates and scans whether it landed there, with a
|
||||
marker-less control menu as its own discriminator.
|
||||
- **7.css's button effects reach anything that IS a button — including menu
|
||||
items — and one of them fades out over a FULL SECOND.** `button::before` is
|
||||
an Aero glow overlay with `opacity: 0`, raised to 1 on hover, and
|
||||
`button:not(:hover):before` carries `transition: opacity 1s linear`.
|
||||
menu.css suppresses it with `content: none` on the **`:hover` arm only**, so
|
||||
while an item is hovered nothing paints, but the moment the pointer leaves,
|
||||
the pseudo returns with opacity animating 1 → 0: a blue ghost trailing the
|
||||
cursor down the menu. Killing `content` in *every* state is the fix —
|
||||
suppressing the transition alone leaves a glow that simply snaps. Two more
|
||||
from the same family: `button:focus` runs `pulse-anim` *infinitely*
|
||||
(`1s ease infinite alternate`), which matters because the context menu moves
|
||||
REAL focus between items for keyboard nav, and the hover paint is 7.css's
|
||||
translucent gradient wash — the same one the ListView selection dropped.
|
||||
All three are overridden together, scoped to `w7-ctx`.
|
||||
- **Its selectors are exact, and a near-miss looks exactly like "7.css doesn't
|
||||
support this".** `menu[role=tablist] button` is a DESCENDANT selector, so
|
||||
every button inside the strip becomes a tab — the `+` and the right-hand
|
||||
|
||||
+86
-4
@@ -1,4 +1,4 @@
|
||||
// Programmatic POSITIONING test for the win7 context menu.
|
||||
// Programmatic POSITIONING + HIGHLIGHT test for the win7 context menu.
|
||||
//
|
||||
// bun scripts/menu-probe.ts
|
||||
//
|
||||
@@ -16,6 +16,16 @@
|
||||
// i.e. the bug verbatim. That one must stay invisible — if it ever shows up,
|
||||
// 7.css stopped forcing `relative` and this probe's premise is stale.
|
||||
//
|
||||
// It also checks the item HIGHLIGHT, using hover-probe.ts's trick (`:hover`
|
||||
// rewritten to a `.simhover` class, since headless Firefox has no pointer):
|
||||
// 7.css paints hovered items with a translucent gradient wash, and this app
|
||||
// wants one flat colour. A gradient is detectable as vertical colour spread
|
||||
// down a text-free strip of the item, so "is it flat?" is a measurement, not
|
||||
// an opinion. What canNOT be measured here is the 1s afterglow fade — a
|
||||
// temporal effect — but its cause is structural (`button::before` still has
|
||||
// `content`), and this probe would show the glow's colour if the pseudo ever
|
||||
// came back.
|
||||
//
|
||||
// Inherits shot.ts's constraints: dedicated Firefox profile (a running Firefox
|
||||
// holds the default profile's lock), and the page must be written INTO the
|
||||
// dist folder or strict_origin_policy silently unstyles it.
|
||||
@@ -35,6 +45,12 @@ if (!existsSync(DIST)) fail(`no build output at ${DIST} — run: bun run build:u
|
||||
const styles = readdirSync(DIST).find((f) => f.startsWith("styles-") && f.endsWith(".css"));
|
||||
if (!styles) fail(`no styles-*.css in ${DIST} — run: bun run build:ui`);
|
||||
|
||||
// Hover as a CLASS: same rewrite hover-probe.ts uses, for the same reason —
|
||||
// `--screenshot` has no pointer. `:hover` has no partial-token hazard.
|
||||
const css = await Bun.file(path.join(DIST, styles)).text();
|
||||
const PROBE_CSS = "_menu-probe.css";
|
||||
await Bun.write(path.join(DIST, PROBE_CSS), css.replaceAll(":hover", ".simhover"));
|
||||
|
||||
// ---- the probe page ----
|
||||
|
||||
// Geometry (CSS px = image px; headless Firefox applies no DPR scaling).
|
||||
@@ -51,7 +67,7 @@ const menu = (x: number, marker: boolean) => `
|
||||
<ul role="menu" tabindex="-1"
|
||||
class="${marker ? "w7-ctx " : ""}fixed z-50 outline-none"
|
||||
style="left: ${x}px; top: ${MENU_Y}px; width: ${MENU_W}px">
|
||||
<li role="menuitem"><button type="button">Send to Other Panel</button></li>
|
||||
<li role="menuitem"><button type="button"${marker ? ' class="simhover"' : ""}>Send to Other Panel</button></li>
|
||||
<li role="menuitem" class="has-divider"><button type="button">Rename</button></li>
|
||||
<li role="menuitem"><button type="button">Copy Path</button></li>
|
||||
<li role="menuitem" aria-disabled="true"><button type="button">Delete</button></li>
|
||||
@@ -64,7 +80,7 @@ const menu = (x: number, marker: boolean) => `
|
||||
// it) purely so "is anything drawn here?" is an unambiguous question.
|
||||
const page = `<!doctype html>
|
||||
<html class="win7" lang="en">
|
||||
<head><meta charset="utf-8"><link rel="stylesheet" href="${styles}">
|
||||
<head><meta charset="utf-8"><link rel="stylesheet" href="${PROBE_CSS}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="window glass w7-frame active" style="display: flex; flex-direction: column; height: 100vh">
|
||||
@@ -121,6 +137,46 @@ async function inkCount(img: string, x: number, y: number, w: number, h: number)
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Colour spread down a text-free strip of the first (force-hovered) item: the
|
||||
* ul's own 2px padding + the item's 1px border put the item's background at
|
||||
* x >= menu+3, and its 32px left padding keeps label text out to menu+32. So a
|
||||
* column at menu+5..menu+26 is pure fill — except for the gutter stripe, which
|
||||
* 7.css puts at left: 28px. A flat colour reads 0 spread; a gradient does not.
|
||||
*/
|
||||
async function fillSpread(
|
||||
img: string,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
): Promise<{ spread: number; mean: [number, number, number] }> {
|
||||
const p = Bun.spawnSync(["magick", img, "-crop", `${w}x${h}+${x}+${y}`, "+repage", "txt:"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const lo: number[] = [255, 255, 255];
|
||||
const hi: number[] = [0, 0, 0];
|
||||
const sum = [0, 0, 0];
|
||||
let n = 0;
|
||||
for (const line of p.stdout.toString().split("\n").slice(1)) {
|
||||
const m = /\((\d+),(\d+),(\d+)/.exec(line);
|
||||
if (!m) continue;
|
||||
for (let c = 0; c < 3; c++) {
|
||||
const v = Number(m[c + 1]);
|
||||
lo[c] = Math.min(lo[c], v);
|
||||
hi[c] = Math.max(hi[c], v);
|
||||
sum[c] += v;
|
||||
}
|
||||
n++;
|
||||
}
|
||||
if (!n) return { spread: -1, mean: [0, 0, 0] };
|
||||
return {
|
||||
spread: Math.max(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]),
|
||||
mean: [Math.round(sum[0] / n), Math.round(sum[1] / n), Math.round(sum[2] / n)],
|
||||
};
|
||||
}
|
||||
|
||||
// Inset by 2px so the scan cannot straddle the region's own edge.
|
||||
const BOX = { w: MENU_W - 4, h: 60 };
|
||||
const placed = await inkCount(out, MENU_X + 2, MENU_Y + 2, BOX.w, BOX.h);
|
||||
@@ -134,10 +190,36 @@ console.log(
|
||||
console.log(
|
||||
` control (no marker) ${String(control).padStart(6)} ${control > 0 ? "NOTE: 7.css no longer forces relative — premise stale" : "off-viewport, as expected"}`,
|
||||
);
|
||||
// The force-hovered first item: fill strip, and the same strip on the item
|
||||
// below it as a not-hovered reference (which must stay the menu surface).
|
||||
const FILL = { x: MENU_X + 5, w: 21, h: 14 };
|
||||
const hovered = await fillSpread(out, FILL.x, MENU_Y + 7, FILL.w, FILL.h);
|
||||
const resting = await fillSpread(out, FILL.x, MENU_Y + 34, FILL.w, FILL.h);
|
||||
const FLAT_MAX = 6; // antialiasing tolerance; 7.css's wash spreads far wider
|
||||
|
||||
console.log("\n[menu-probe] item highlight (force-hovered vs resting):\n");
|
||||
console.log(" item mean rgb spread verdict");
|
||||
console.log(
|
||||
` hovered (.simhover) ${hovered.mean.join(",").padEnd(15)} ${String(hovered.spread).padStart(6)} ${
|
||||
hovered.spread <= FLAT_MAX ? "flat" : "GRADIENT: highlight is not a flat colour"
|
||||
}`,
|
||||
);
|
||||
console.log(` resting ${resting.mean.join(",").padEnd(15)} ${String(resting.spread).padStart(6)} reference`);
|
||||
|
||||
console.log(`\n[menu-probe] image: ${out}`);
|
||||
|
||||
let failed = false;
|
||||
if (placed === 0) {
|
||||
console.log("[menu-probe] the context menu is positioned in FLOW again — see 7like.css .w7-ctx");
|
||||
process.exit(2);
|
||||
failed = true;
|
||||
}
|
||||
if (hovered.spread > FLAT_MAX) {
|
||||
console.log("[menu-probe] the item highlight regained a gradient — see the menu block in 7like.css");
|
||||
failed = true;
|
||||
}
|
||||
if (hovered.mean.join(",") === resting.mean.join(",")) {
|
||||
console.log("[menu-probe] hovered and resting items are identical — the highlight is not painting at all");
|
||||
failed = true;
|
||||
}
|
||||
if (failed) process.exit(2);
|
||||
console.log("[menu-probe] clean");
|
||||
|
||||
+4
-1
@@ -200,7 +200,10 @@
|
||||
</figure>
|
||||
<figure class="cell" style="margin:0">
|
||||
<figcaption>ours — context-menu.ts output (buttons, divider, accelerator, checkables)</figcaption>
|
||||
<ul role="menu" style="width:200px">
|
||||
<!-- w7-ctx is the app's marker class (flat highlight, no Aero afterglow).
|
||||
It also carries a position:fixed restatement, which the app wants and a
|
||||
review page in normal flow does not — hence the inline static. -->
|
||||
<ul role="menu" class="w7-ctx" style="width:200px;position:static">
|
||||
<li role="menuitem"><button type="button">Send to Other Panel</button></li>
|
||||
<li role="menuitem"><button type="button">Open</button></li>
|
||||
<li role="menuitem"><button type="button">Edit <span>F4</span></button></li>
|
||||
|
||||
@@ -137,9 +137,7 @@
|
||||
commander convention this app already follows (see the pane-focus model
|
||||
below): the band never disappears, only its strength changes. */
|
||||
--w7-sel-bg: #cbe8f6;
|
||||
--w7-sel-bd: #26a0da;
|
||||
--w7-sel-bg-idle: #dedede;
|
||||
--w7-sel-bd-idle: #c9c9c9;
|
||||
/* Tabs (tabs.css). 7.css hardcodes `color: #222` on every tablist button,
|
||||
outside its own var system, which is unreadable on the dark surface. */
|
||||
--w7-tab-c: #222;
|
||||
@@ -201,9 +199,7 @@
|
||||
--w7-lv-head-bd: var(--w7-el-bd-d);
|
||||
--w7-lv-cell-bd: var(--w7-el-bd-d);
|
||||
--w7-sel-bg: #2a4a63;
|
||||
--w7-sel-bd: #4a8cbe;
|
||||
--w7-sel-bg-idle: #3a3a3a;
|
||||
--w7-sel-bd-idle: #4c4c4c;
|
||||
--w7-tab-c: var(--w7-el-c);
|
||||
--w7-mn-c: var(--w7-el-c);
|
||||
--w7-mn-bd: var(--w7-el-bd-d);
|
||||
@@ -262,9 +258,7 @@
|
||||
--w7-lv-head-bd: var(--w7-el-bd-d);
|
||||
--w7-lv-cell-bd: var(--w7-el-bd-d);
|
||||
--w7-sel-bg: #2a4a63;
|
||||
--w7-sel-bd: #4a8cbe;
|
||||
--w7-sel-bg-idle: #3a3a3a;
|
||||
--w7-sel-bd-idle: #4c4c4c;
|
||||
--w7-tab-c: var(--w7-el-c);
|
||||
--w7-mn-c: var(--w7-el-c);
|
||||
--w7-mn-bd: var(--w7-el-bd-d);
|
||||
@@ -582,7 +576,7 @@ html.win7 body {
|
||||
}
|
||||
.win7 table > tbody > tr.highlighted {
|
||||
background: var(--w7-sel-bg-idle);
|
||||
border-color: var(--w7-sel-bd-idle);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* ---- Window frame ----
|
||||
@@ -658,15 +652,15 @@ html.win7 body {
|
||||
1px at top and bottom and shoved every row below it down by 2px as the
|
||||
cursor moved. Every row reserves that border instead, transparent when not
|
||||
selected, so selecting changes colour and nothing else.
|
||||
NOTE: this sits in `skinfix`, which outranks `skin` by LAYER regardless of
|
||||
specificity, so it would also blank 7.css's highlight border — both
|
||||
selected states therefore restate their border-color below. */
|
||||
The border stays TRANSPARENT even when selected: it exists purely to
|
||||
reserve space. 7.css outlines the selected row (#aaddfa) on top of its
|
||||
fill, which stacked a second edge inside the band and, with the cursor
|
||||
row's dotted rect, drew three concentric outlines around one row. The fill
|
||||
paints under the transparent border (background-clip defaults to
|
||||
border-box), so the band still covers the full row. */
|
||||
.win7 table > tbody > tr {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.win7 table > tbody > tr.highlighted {
|
||||
border-color: var(--w7-sel-bd-idle);
|
||||
}
|
||||
|
||||
/* Pane-focus model (commander convention, matching Win7 Explorer):
|
||||
the CURSOR ROW always carries the selection band in BOTH panels —
|
||||
@@ -679,13 +673,13 @@ html.win7 body {
|
||||
The dotted focus rectangle is NOT the focus signal; it is layered on top
|
||||
of the band in the focused pane only, the way Win7 drew the keyboard
|
||||
focus rect over the selected item.
|
||||
FLAT, not a gradient: 7.css bands the row with --w7-el-grad-h, its Aero
|
||||
FLAT, and only a fill: 7.css bands the row with --w7-el-grad-h, its Aero
|
||||
element-hover ramp, which reads as a glossy button rather than a file
|
||||
selection. Explorer used one solid fill and a solid border. Keep the
|
||||
1px border and 7.css's 3px radius — those ARE Explorer's selection box. */
|
||||
selection, and outlines it as well. One solid colour, no edge — the
|
||||
dotted cursor rect is then the only line on the row, which is what makes
|
||||
it legible as the cursor. */
|
||||
.win7 table.is-active > tbody > tr.highlighted {
|
||||
background: var(--w7-sel-bg);
|
||||
border-color: var(--w7-sel-bd);
|
||||
}
|
||||
|
||||
/* One dotted rectangle around the ROW; per cell (tr > *) draws a dotted box
|
||||
@@ -739,6 +733,40 @@ html.win7 body {
|
||||
color: var(--w7-mn-check-c);
|
||||
}
|
||||
|
||||
/* Menu items: flat highlight, and no Aero afterglow trailing the pointer.
|
||||
Menu items ARE <button>s, so button.css reaches them with three effects
|
||||
that do not belong on a menu:
|
||||
(1) the highlight is 7.css's translucent wash
|
||||
`linear-gradient(#fff9, #e6ecf5cc 90%, #fffc)` + an #aaddfa edge — the
|
||||
same gradient the ListView selection above just dropped;
|
||||
(2) `button::before` is a blue glow overlay whose opacity fades out over
|
||||
a FULL SECOND (`button:not(:hover):before { transition: opacity 1s
|
||||
linear }`). menu.css sets `content: none` on the :HOVER arm only, so
|
||||
the pseudo reappears the instant you leave an item and then fades 1→0
|
||||
— a ghost dragging behind the pointer. Killing `content` in EVERY
|
||||
state is what removes it; suppressing the transition alone would not,
|
||||
because the glow itself is legitimate on real push buttons;
|
||||
(3) `button:focus` runs `pulse-anim` infinitely, and this menu moves REAL
|
||||
focus between items for keyboard nav — so a focused item would breathe.
|
||||
Scoped to the app's own menu (w7-ctx): 7.css's demo menus keep stock Aero. */
|
||||
.win7 ul[role='menu'].w7-ctx > li[role='menuitem'] > button,
|
||||
.win7 ul[role='menu'].w7-ctx > li[role='menuitem'] > label {
|
||||
transition: none;
|
||||
animation: none;
|
||||
}
|
||||
.win7 ul[role='menu'].w7-ctx > li[role='menuitem'] > button::before,
|
||||
.win7 ul[role='menu'].w7-ctx > li[role='menuitem'] > button::after {
|
||||
content: none;
|
||||
}
|
||||
.win7 ul[role='menu'].w7-ctx > li[role='menuitem'] > button:hover,
|
||||
.win7 ul[role='menu'].w7-ctx > li[role='menuitem'] > button:focus,
|
||||
.win7 ul[role='menu'].w7-ctx > li[role='menuitem'] > button:focus-visible,
|
||||
.win7 ul[role='menu'].w7-ctx > li[role='menuitem'] > label:hover,
|
||||
.win7 ul[role='menu'].w7-ctx > li[role='menuitem'] > label:focus-visible {
|
||||
background: var(--w7-sel-bg);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* ---- Flat buttons (toolbars + the w7-flat escape hatch) ----
|
||||
The paint-suppression half of the rule in `skin` (see there for the split).
|
||||
It lives in `skinfix` because 7.css restates border-color at (0,3,1) via
|
||||
|
||||
Reference in New Issue
Block a user