Add a toggleable Windows 7 (7.css) skin behind a new skin setting

Evaluation branch: a full Aero skin, including frameless windows with our own
title bar, switchable against the existing modern look.

The app already had a four-axis runtime skinning mechanism and zero component
stylesheets, so the skin is one new setting plus one stylesheet section rather
than a parallel UI. The key move is cascade order: 7.css is imported into a
`skin` layer ABOVE Tailwind's `utilities`, so its bare element rules outrank
the ~1900 inline utility classes already in the templates. Template changes are
limited to additive, skin-inert hooks (ARIA roles, `has-scrollbar`,
`w7-toolbar`, `w7-btn`).

- styles.css: explicit layer order, `7.css/dist/7.scoped.css` via `layer(skin)`,
  a token bridge re-pointing the existing semantic tokens at Win7 values (no new
  tokens, so `@theme inline` is untouched), a hand-authored dark Win7 palette,
  and neutralizers that `revert-layer` 7.css's dialog-button metrics back to the
  utility layer. Neutralizers must live inside `skin`, not `skinfix` —
  `revert-layer` only reaches `utilities` from one layer up.
- Frameless chrome: `chromeOptions(skin)` is now the single place either skin's
  frame is decided. `titleBarStyle: "hidden"` on every platform, which is also
  the only way to drop the macOS traffic lights. New `title-bar` component
  mounted by all three window roots.
- Window controls over RPC (minimize/toggle-maximize/close/is-maximized), served
  per window kind so the X reuses each one's existing close path — editors still
  get their dirty prompt. electrobun has no maximize event, so the state is
  derived from `resize`; double-click-to-maximize is implemented here too.
- Font: Noto Sans vendored (OFL-1.1) under ui/src/fonts, since Segoe UI is
  absent off Windows and not redistributable.

Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean (486 kB initial,
under the 500 kB budget), app launches with the skin active and no console
errors. The rendered result has not been eyeballed — screen capture and
accessibility are both denied to the shell on this machine.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-14 15:02:27 +02:00
co-authored by Claude
parent fc03fa6216
commit 07825c1bd1
33 changed files with 828 additions and 48 deletions
+60 -10
View File
@@ -21,9 +21,16 @@ export function buildPreload(
): string {
let script =
`window.__APP_PLATFORM=${JSON.stringify(process.platform)};` +
`window.__APP_MODE=${JSON.stringify(mode)};`;
`window.__APP_MODE=${JSON.stringify(mode)};` +
`window.__APP_SKIN=${JSON.stringify(settings.skin)};`;
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 (mirrors SettingsService.applySkin).
if (settings.skin === "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;
@@ -35,6 +42,30 @@ export function buildPreload(
return script;
}
/**
* Native chrome options for a window, per skin. The ONE place either skin's
* frame decision is made — both the main window (src/bun/index.ts) and
* openSecondaryWindow spread this.
*
* - "win7" draws its own Aero title bar in the webview (drag region + our own
* minimize/maximize/close), so the window must be 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.
* - "modern" keeps today's split: macOS fuses the tab bar into an inset
* titlebar (traffic lights overlay the UI's h-10 tab row, offset centers
* them); other platforms keep the native titlebar.
*
* Skin changes therefore only reach the frame on the NEXT window open — the
* CSS half applies live. The settings UI says so.
*/
export function chromeOptions(skin: AppSettings["skin"]) {
if (skin === "win7") return { titleBarStyle: "hidden" as const };
return isMac
? { titleBarStyle: "hiddenInset" as const, trafficLightOffset: { x: 12, y: 13 } }
: {};
}
/**
* Windows are created hidden and revealed on the webview's first dom-ready —
* showing them earlier paints the native (white) surface before the themed
@@ -55,6 +86,31 @@ 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). */
@@ -70,21 +126,15 @@ export interface SecondaryWindowOptions {
}
/**
* App-chromed secondary window.
*
* PORTABILITY: the only spot that branches on platform for secondary-window
* chrome. macOS fuses the titlebar (traffic lights overlay the UI's h-10
* header row, same treatment as the main window); Windows/Linux keep the
* native titlebar and the UI renders a compact non-drag header instead.
* App-chromed secondary window. The frame decision is delegated to
* chromeOptions (which owns both the per-skin and the per-platform branch).
*/
export function openSecondaryWindow(opts: SecondaryWindowOptions): BrowserWindow {
const win = new BrowserWindow({
title: opts.title,
url: opts.url,
frame: opts.frame,
...(isMac
? { titleBarStyle: "hiddenInset" as const, trafficLightOffset: { x: 12, y: 13 } }
: {}),
...chromeOptions(opts.settings.skin),
preload: buildPreload(opts.mode, opts.settings, opts.ctx),
rpc: opts.rpc,
hidden: true,