Right-click did nothing under the win7 skin, and then swallowed the next
click. 7.css declares `position: relative` on ul[role=menu] and the `skin`
layer outranks `utilities`, so Tailwind's `.fixed` lost: [style.left/top.px]
became flow offsets from the menu's static position — the last child of the
h-screen frame — and every menu rendered below the viewport, unreachable
behind overflow:hidden. Its backdrop is a plain <div>, unmatched by 7.css, so
that stayed genuinely fixed and ate the following click. Regressed in 0ae2a62,
when the menu adopted 7.css's real markup.
Restated `position: fixed` in skinfix, scoped to a new `w7-ctx` marker so
7.css's own menus keep theirs. scripts/menu-probe.ts is the regression test:
it renders the menu at declared coordinates, pixel-scans whether it landed
there, and carries a marker-less control menu (the bug verbatim) as its own
discriminator. A resting pixel diff cannot see this class of bug.
Selection band goes flat: --w7-sel-bg/-bd (#cbe8f6/#26a0da) is what Explorer
actually painted, where 7.css reused --w7-el-grad-h, its Aero element-hover
ramp. That var is left alone — buttons and tabs still want the gloss. The
unfocused pane gets the grey idle band; the band still never disappears with
focus, it only weakens. Harness diff confirms the change touches selection
rows and nothing else, in both themes.
New workspace-tab right-click menu in both skins: New Tab, Close Tab, Close
Other Tabs, Close Tabs to the Right, Disconnect. The target id travels inside
the action string because the right-clicked tab is not necessarily the active
one. Illegal entries are disabled rather than omitted, the way Win7 greyed
out — and since 7.css's [aria-disabled] handling is CSS-only, the keyboard
paths check the flag themselves.
Co-Authored-By: Claude <noreply@anthropic.com>
144 lines
6.0 KiB
TypeScript
144 lines
6.0 KiB
TypeScript
// Programmatic POSITIONING test for the win7 context menu.
|
|
//
|
|
// bun scripts/menu-probe.ts
|
|
//
|
|
// Why this exists: 7.css declares `position: relative` on ul[role=menu] (it
|
|
// needs it for the gutter stripe and submenus), and the `skin` layer outranks
|
|
// `utilities` — so Tailwind's `.fixed` on the app's own menu lost, its
|
|
// [style.left/top.px] became FLOW offsets, and every right-click menu rendered
|
|
// below the viewport of the h-screen frame. Nothing about the idle window
|
|
// changes when that breaks, so the screenshot harness (scripts/shot.ts) cannot
|
|
// see it, and hover-probe.ts tests a different axis. This script renders the
|
|
// menu markup at declared coordinates and pixel-scans whether it actually
|
|
// landed there.
|
|
//
|
|
// It carries its own control: a second menu WITHOUT the `w7-ctx` marker class,
|
|
// 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.
|
|
//
|
|
// 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.
|
|
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const ROOT = path.join(import.meta.dir, "..");
|
|
const DIST = path.join(ROOT, "ui", "dist", "ui", "browser");
|
|
const WORK = path.join(ROOT, ".shot");
|
|
|
|
function fail(message: string): never {
|
|
console.error(`[menu-probe] ERROR: ${message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!existsSync(DIST)) fail(`no build output at ${DIST} — run: bun run build:ui`);
|
|
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`);
|
|
|
|
// ---- the probe page ----
|
|
|
|
// Geometry (CSS px = image px; headless Firefox applies no DPR scaling).
|
|
const MENU_X = 200;
|
|
const MENU_Y = 100;
|
|
const CONTROL_X = 560;
|
|
const MENU_W = 200;
|
|
|
|
// Mirrors ui/src/app/skins/7like/context-menu.ts EXACTLY where it matters:
|
|
// ul[role=menu] > li[role=menuitem] > button, the `w7-ctx fixed` class pair,
|
|
// and left/top as inline pixel styles. `w7-ctx` is what the skinfix rule in
|
|
// 7like.css keys on; the control deliberately omits it.
|
|
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" 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>
|
|
</ul>`;
|
|
|
|
// The frame reproduces the app's: .window.w7-frame is a 100vh flex column and
|
|
// <app-context-menu> is one of its LAST children, so a menu that falls back to
|
|
// static flow lands past the bottom edge — which is the whole bug.
|
|
// The body gets an explicit white background (inline, so no layer can touch
|
|
// 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>
|
|
<body>
|
|
<div class="window glass w7-frame active" style="display: flex; flex-direction: column; height: 100vh">
|
|
<div class="title-bar">
|
|
<div class="title-bar-text">miniSCP</div>
|
|
<div class="title-bar-controls"><button aria-label="Close"></button></div>
|
|
</div>
|
|
<div class="window-body" style="flex: 1; min-height: 0; background: #fff"></div>
|
|
${menu(MENU_X, true)}
|
|
${menu(CONTROL_X, false)}
|
|
</div>
|
|
</body></html>`;
|
|
|
|
const pagePath = path.join(DIST, "_menu-probe.html");
|
|
await Bun.write(pagePath, page);
|
|
|
|
// ---- shoot ----
|
|
|
|
mkdirSync(WORK, { recursive: true });
|
|
const profile = path.join(WORK, "profile");
|
|
mkdirSync(profile, { recursive: true });
|
|
rmSync(path.join(profile, "parent.lock"), { force: true });
|
|
|
|
const out = path.join(WORK, "menu-probe.png");
|
|
const proc = Bun.spawnSync(
|
|
["firefox", "--headless", "--profile", profile, "--screenshot", out, "--window-size", "800,400", `file://${pagePath}`],
|
|
{ cwd: ROOT, stdout: "pipe", stderr: "pipe" },
|
|
);
|
|
if (!existsSync(out)) {
|
|
console.error(proc.stderr.toString().trim());
|
|
fail("firefox produced no image");
|
|
}
|
|
|
|
// ---- pixel scan ----
|
|
|
|
/**
|
|
* Count pixels that are visibly NOT the white window body: the menu's border
|
|
* (#0006 over white ≈ #999), its shadow and its black item text all qualify.
|
|
* A menu that rendered where it was told fills this with hundreds of them; an
|
|
* empty white region scores 0.
|
|
*/
|
|
async function inkCount(img: string, x: number, y: number, w: number, h: number): Promise<number> {
|
|
const p = Bun.spawnSync(["magick", img, "-crop", `${w}x${h}+${x}+${y}`, "+repage", "txt:"], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
let count = 0;
|
|
for (const line of p.stdout.toString().split("\n").slice(1)) {
|
|
const m = /\((\d+),(\d+),(\d+)/.exec(line);
|
|
if (!m) continue;
|
|
const [r, g, b] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
if (Math.max(r, g, b) < 200) count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// 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);
|
|
const control = await inkCount(out, CONTROL_X + 2, MENU_Y + 2, BOX.w, BOX.h);
|
|
|
|
console.log("[menu-probe] ink at the declared coordinates:\n");
|
|
console.log(" menu px verdict");
|
|
console.log(
|
|
` w7-ctx (position: fixed)${String(placed).padStart(6)} ${placed > 0 ? "placed" : "MISSING: menu did not render where it was told"}`,
|
|
);
|
|
console.log(
|
|
` control (no marker) ${String(control).padStart(6)} ${control > 0 ? "NOTE: 7.css no longer forces relative — premise stale" : "off-viewport, as expected"}`,
|
|
);
|
|
console.log(`\n[menu-probe] image: ${out}`);
|
|
|
|
if (placed === 0) {
|
|
console.log("[menu-probe] the context menu is positioned in FLOW again — see 7like.css .w7-ctx");
|
|
process.exit(2);
|
|
}
|
|
console.log("[menu-probe] clean");
|