Remove the Windows 7 (7.css) skin

The win7 skin is dropped. Everything else the branch built stays: Linux
support and the HTML context menus, the workspace-tab menu, the continuous
interface scale (rem everywhere, stepper, Ctrl/Cmd +/-/0) and the core/
service layer. The modern tree already carried all of them, and nothing
outside skins/7like/ imported the win7 tree except main.ts and styles.css.

Removed: the skins/7like/ tree (5.1k lines, fonts), the 7.css dependency,
the skin setting and its options row + strings in all 7 locales, the
frameless-window plumbing (window-control RPCs, windowState push,
trackMaximizeState, the synthesized resize grip and its postinstall
watchdog), the session-skin pinning in buildPreload/chromeOptions, the
skin/skinfix cascade layers, and the win7-only probes and screenshot
harness (the harness cannot render the modern tree: plain Firefox has no
RPC bridge). chromeOptions() stays as the one shared frame decision.

Fixed on the way out:
- index.html's per-theme pre-paint background is restored. The branch had
  replaced it with a win7-only transparent rule, so modern dark windows had
  silently lost one of the three flash guards.
- editor-windows.ts wrote its window-key separator as a literal NUL byte,
  which made git and grep treat the file as binary; it is `\0` now (same
  string at runtime).

No settings migration: a stale "skin" key is carried harmlessly by the
store's spread-merge and dropped on the next save.

UI bundle: 616.7 kB -> 405.4 kB, back under the 500 kB budget.

Docs: architecture, gotchas and platform-notes lose the win7 material but
keep what stays true (construction-time preloads/frames, drag regions
ignoring preventDefault, electrobun's missing resize/maximize primitives);
flash-prevention, dev-URL and Bun-panic notes move back under Electrobun;
stale context-menu/tab-bar links fixed; README documents the scale keys.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-23 16:17:38 +02:00
co-authored by Claude
parent 0a0c708140
commit 2bac7678ca
64 changed files with 144 additions and 7194 deletions
+10 -89
View File
@@ -1,31 +1,9 @@
import { BrowserWindow } from "electrobun/bun";
import { DEFAULT_SETTINGS, type AppSettings } from "../shared/types";
/**
* The skin THIS PROCESS booted with, frozen at startup by `initSessionSkin`.
*
* Every window must be built from this, never from the live settings snapshot.
* The skin picks a whole component tree (bootstrapped from the preload-baked
* __APP_SKIN) and the native frame (constructor-only), so it cannot change
* without a restart — but the persisted setting CAN change mid-session. Reading
* the live value would boot a newly opened editor or manager into the pending
* skin while the main window still runs the old one: two windows, two trees,
* two frames.
*
* Pinning it in one place makes that impossible rather than making each call
* site remember. Everything else in the preload keeps using the live snapshot,
* because theme/tone/accent/text-size genuinely do apply live.
*/
let sessionSkin: AppSettings["skin"] = DEFAULT_SETTINGS.skin;
/** Call once, at startup, before any window is constructed. */
export function initSessionSkin(skin: AppSettings["skin"]): void {
sessionSkin = skin;
}
import type { AppSettings } from "../shared/types";
/**
* The preload globals every window gets. Single composition point: the UI
* reads them in ui/src/app/platform.ts before first paint (window chrome and
* reads them in ui/src/app/core/platform.ts before first paint (window chrome and
* root-component selection both depend on them).
*
* The persisted theme attributes are baked in here — mirroring the
@@ -39,22 +17,11 @@ export function buildPreload(
settings: AppSettings,
ctx?: unknown,
): string {
// sessionSkin, NOT settings.skin — a pending skin change must not leak into
// a window opened before the restart. See initSessionSkin.
let script =
`window.__APP_PLATFORM=${JSON.stringify(process.platform)};` +
`window.__APP_MODE=${JSON.stringify(mode)};` +
`window.__APP_SKIN=${JSON.stringify(sessionSkin)};`;
`window.__APP_MODE=${JSON.stringify(mode)};`;
if (ctx !== undefined) script += `window.__APP_CTX=${JSON.stringify(ctx)};`;
// A class, not a data-* attribute: 7.scoped.css is precompiled to require a
// literal `.win7` ancestor, so the name is not ours to choose. This is the
// ONLY place it is ever written — the UI must never toggle it live, because
// each skin's CSS is written against its own tree's markup.
if (sessionSkin === "win7") {
script += `document.documentElement.classList.add("win7");`;
}
const dataset: Record<string, string> = {};
if (settings.theme !== "system") dataset["theme"] = settings.theme;
if (settings.darkTone !== "dark") dataset["darkTone"] = settings.darkTone;
@@ -72,32 +39,14 @@ export function buildPreload(
}
/**
* Native chrome options for a window. The ONE place the frame decision is made —
* both the main window (src/bun/index.ts) and openSecondaryWindow spread this.
*
* Both values are BrowserWindow CONSTRUCTOR options — there is no API to
* re-decorate a live window — which is half of why the skin is a launch-time
* choice. Takes no argument on purpose: it reads the pinned sessionSkin, so no
* call site can accidentally frame a window from a pending setting.
* Native chrome for every window — the ONE place the frame is decided; the main
* window (src/bun/index.ts) and openSecondaryWindow both spread this. macOS
* fuses the titlebar with the app's own header row (hiddenInset: the traffic
* lights overlay it, which is what IS_MAC_CHROME reserves pl-24 for); other
* platforms keep the native titlebar. Constructor-only: there is no API to
* re-decorate a live window.
*/
export function chromeOptions() {
const skin = sessionSkin;
// win7 draws its own Aero title bar (drag region + our minimize/maximize/
// close), so the window is frameless on EVERY platform. `titleBarStyle:
// "hidden"` clears `Titled` from the Cocoa style mask, which is also what
// removes the macOS traffic lights — there is no separate
// hide-traffic-lights API. Linux honours it too: the GTK wrapper calls
// gtk_window_set_decorated(false).
//
// `transparent` is what lets the Aero frame be a real frame: .window's 6px
// radius is drawn by the webview OUTSIDE the window's own rectangle, so on
// an opaque surface the corners are simply clipped square.
// PORTABILITY: on Linux this needs a compositor granting an RGBA visual.
if (skin === "win7") return { titleBarStyle: "hidden" as const, transparent: true };
// modern: the native frame stays. On macOS it fuses with the app's own
// header row (hiddenInset), and the traffic lights overlay it — which is
// what IS_MAC_CHROME reserves pl-24 for in the shadlike tree.
return process.platform === "darwin"
? { titleBarStyle: "hiddenInset" as const, trafficLightOffset: { x: 12, y: 13 } }
: {};
@@ -123,31 +72,6 @@ export function showWhenReady(win: BrowserWindow): void {
});
}
/**
* Push maximize-state changes to a window's webview so the win7 title bar can
* swap its maximize/restore glyph.
*
* electrobun emits NO maximize/unmaximize event (only close/resize/move/focus/
* blur/keyDown/keyUp), so the state is derived from `resize` — which is also
* what makes OS-initiated changes work: double-click zoom, Win+Up, Aero Snap
* all resize the window. `isMaximized()` is a synchronous FFI read, so polling
* it per resize is cheap; the last-value guard keeps the wire quiet during a
* drag-resize. The emitter has no off(), so this closure must not capture
* anything that needs to be garbage collected (see WindowRegistry.track).
*/
export function trackMaximizeState(
win: BrowserWindow,
push: (maximized: boolean) => void,
): void {
let last = win.isMaximized();
win.on("resize", () => {
const now = win.isMaximized();
if (now === last) return;
last = now;
push(now);
});
}
export interface SecondaryWindowOptions {
title: string;
/** The once-resolved dev/prod url (see getMainViewUrl in index.ts). */
@@ -162,10 +86,7 @@ export interface SecondaryWindowOptions {
rpc: NonNullable<ConstructorParameters<typeof BrowserWindow>[0]>["rpc"];
}
/**
* App-chromed secondary window. The frame decision is delegated to
* chromeOptions (the single frame decision).
*/
/** App-chromed secondary window; its frame comes from chromeOptions. */
export function openSecondaryWindow(opts: SecondaryWindowOptions): BrowserWindow {
const win = new BrowserWindow({
title: opts.title,