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:
@@ -0,0 +1,72 @@
|
||||
import { Component, inject, input, output, signal } from '@angular/core';
|
||||
import { IS_WIN7_CHROME } from '../platform';
|
||||
import { RpcService } from '../rpc.service';
|
||||
|
||||
/**
|
||||
* The win7 skin's Aero title bar. Rendered by all three window kinds, and only
|
||||
* when `IS_WIN7_CHROME` — under that skin the native frame is gone
|
||||
* (titleBarStyle 'hidden'), so this row IS the window's chrome.
|
||||
*
|
||||
* The markup is 7.css's contract, not ours: `.title-bar` > `.title-bar-text` +
|
||||
* `.title-bar-controls` > buttons keyed by `aria-label` (Minimize / Maximize /
|
||||
* Restore / Close each get their own glyph). Nothing here is styled with
|
||||
* Tailwind on purpose — the whole row comes from @layer skin.
|
||||
*
|
||||
* Dragging rides on electrobun's preload drag-region classes. Two things
|
||||
* electrobun does NOT provide and this component implements:
|
||||
* - double-click to maximize (it only wires mousedown/mouseup)
|
||||
* - the maximize/restore glyph state, which has no event; bun derives it
|
||||
* from the native resize and pushes `windowState` (see trackMaximizeState).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-title-bar',
|
||||
template: `
|
||||
@if (enabled) {
|
||||
<div class="title-bar electrobun-webkit-app-region-drag" (dblclick)="toggle()">
|
||||
<div class="title-bar-text">{{ title() }}</div>
|
||||
<div class="title-bar-controls electrobun-webkit-app-region-no-drag">
|
||||
@if (resizable()) {
|
||||
<button aria-label="Minimize" (click)="minimize()"></button>
|
||||
<button
|
||||
[attr.aria-label]="maximized() ? 'Restore' : 'Maximize'"
|
||||
(click)="toggle()"
|
||||
></button>
|
||||
}
|
||||
<button aria-label="Close" (click)="closeRequested.emit()"></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class TitleBar {
|
||||
readonly title = input('');
|
||||
/** The manager window is a fixed-size modal — it gets Close only. */
|
||||
readonly resizable = input(true);
|
||||
/** Hosts decide what closing means (quit / dirty prompt / managerClose). */
|
||||
readonly closeRequested = output<void>();
|
||||
|
||||
protected readonly enabled = IS_WIN7_CHROME;
|
||||
protected readonly maximized = signal(false);
|
||||
|
||||
private readonly rpc = inject(RpcService);
|
||||
|
||||
constructor() {
|
||||
if (!this.enabled || !this.rpc.available) return;
|
||||
// No maximize event exists, so seed from a one-shot read and then follow
|
||||
// the resize-derived push (which also covers Aero Snap and Win+Up).
|
||||
void this.rpc.request.windowIsMaximized({}).then((res) => {
|
||||
if (res.ok) this.maximized.set(res.maximized);
|
||||
});
|
||||
this.rpc.onWindowState((maximized) => this.maximized.set(maximized));
|
||||
}
|
||||
|
||||
protected minimize(): void {
|
||||
void this.rpc.request.windowMinimize({});
|
||||
}
|
||||
|
||||
protected toggle(): void {
|
||||
void this.rpc.request.windowToggleMaximize({}).then((res) => {
|
||||
if (res.ok) this.maximized.set(res.maximized);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user