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:
@@ -9,6 +9,7 @@
|
||||
"private": true,
|
||||
"packageManager": "bun@1.3.13",
|
||||
"dependencies": {
|
||||
"7.css": "^0.21.1",
|
||||
"@angular/common": "^22.0.0",
|
||||
"@angular/compiler": "^22.0.0",
|
||||
"@angular/core": "^22.0.0",
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- win7 skin only: the window is frameless, so this IS the chrome -->
|
||||
<app-title-bar [title]="appName" (closeRequested)="closeWindow()" />
|
||||
<app-tab-bar />
|
||||
|
||||
<main class="flex min-h-0 flex-1 divide-x divide-border">
|
||||
|
||||
@@ -17,6 +17,7 @@ import { RpcService } from './rpc.service';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { StatusBar } from './status-bar/status-bar';
|
||||
import { TabBar } from './tab-bar/tab-bar';
|
||||
import { TitleBar } from './title-bar/title-bar';
|
||||
import { ToastService } from './toast.service';
|
||||
import { Toasts } from './toasts/toasts';
|
||||
import { TransferBar } from './transfer-bar/transfer-bar';
|
||||
@@ -36,6 +37,7 @@ import { UpdaterService } from './updater.service';
|
||||
OverwriteDialog,
|
||||
Toasts,
|
||||
TransferBar,
|
||||
TitleBar,
|
||||
],
|
||||
templateUrl: './app.html',
|
||||
host: {
|
||||
@@ -58,6 +60,18 @@ export class App {
|
||||
/** The connection-manager window is open — this window is modal-blocked. */
|
||||
readonly managerOpen = signal(false);
|
||||
|
||||
/** Shown in the win7 title bar (no native titlebar to carry it). */
|
||||
readonly appName = BRANDING.appName;
|
||||
|
||||
/**
|
||||
* win7 title bar's X. Closing the main window quits, and the app-level
|
||||
* before-quit guard (dirty editors + running transfers) still runs — it is
|
||||
* the one close path electrobun lets us cancel.
|
||||
*/
|
||||
closeWindow(): void {
|
||||
void this.rpc.request.windowClose({});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
document.title = BRANDING.appName;
|
||||
if (this.rpc.available) {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<div class="flex h-screen flex-col bg-background text-foreground">
|
||||
<!-- win7 skin only: frameless window. Fixed-size modal, so Close only. -->
|
||||
<app-title-bar
|
||||
[title]="i18n.t('connMgr.title')"
|
||||
[resizable]="false"
|
||||
(closeRequested)="close()"
|
||||
/>
|
||||
<!-- header: on macOS this row IS the titlebar (hiddenInset + drag region),
|
||||
elsewhere the native titlebar sits above and this is a compact strip -->
|
||||
<header
|
||||
class="flex shrink-0 items-center gap-2 border-b border-border bg-chrome pr-2"
|
||||
class="w7-toolbar flex shrink-0 items-center gap-2 border-b border-border bg-chrome pr-2"
|
||||
[class.h-10]="isMacChrome"
|
||||
[class.pl-24]="isMacChrome"
|
||||
[class.electrobun-webkit-app-region-drag]="isMacChrome"
|
||||
@@ -47,7 +53,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto py-1">
|
||||
<div class="has-scrollbar flex-1 overflow-y-auto py-1">
|
||||
<!-- stacks -->
|
||||
@for (group of store.grouped().stacks; track group.stack.id) {
|
||||
<div
|
||||
@@ -175,7 +181,7 @@
|
||||
<!-- edit form -->
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
@if (draft(); as d) {
|
||||
<div class="grid flex-1 auto-rows-min grid-cols-2 gap-x-4 gap-y-3 overflow-y-auto p-5">
|
||||
<div class="has-scrollbar grid flex-1 auto-rows-min grid-cols-2 gap-x-4 gap-y-3 overflow-y-auto p-5">
|
||||
<label class="col-span-2 flex flex-col gap-1 text-xs">
|
||||
<span class="text-muted-foreground">{{ i18n.t('connMgr.name') }}</span>
|
||||
<input
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DialogsService } from '../dialogs.service';
|
||||
import { blockNativeContextMenu, IS_MAC_CHROME } from '../platform';
|
||||
import { RpcService } from '../rpc.service';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { TitleBar } from '../title-bar/title-bar';
|
||||
import { Toasts } from '../toasts/toasts';
|
||||
|
||||
interface Draft {
|
||||
@@ -72,7 +73,7 @@ function fromDraft(d: Draft): SavedConnection {
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [NgIcon, ConfirmDialog, PromptDialog, Toasts],
|
||||
imports: [NgIcon, ConfirmDialog, PromptDialog, Toasts, TitleBar],
|
||||
templateUrl: './connection-manager.html',
|
||||
host: {
|
||||
'(window:keydown.escape)': 'onEscape()',
|
||||
|
||||
@@ -60,13 +60,13 @@ const PERMS = ['r', 'w', 'x'] as const;
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
(click)="cancel()"
|
||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
class="w7-btn rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
>
|
||||
{{ i18n.t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
(click)="submit()"
|
||||
class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="w7-btn rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{{ i18n.t('common.apply') }}
|
||||
</button>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { FocusService } from '../focus.service';
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
(click)="cancel()"
|
||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
class="w7-btn rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
>
|
||||
{{ i18n.t('common.cancel') }}
|
||||
</button>
|
||||
@@ -26,8 +26,8 @@ import { FocusService } from '../focus.service';
|
||||
(click)="submit()"
|
||||
[class]="
|
||||
request().destructive
|
||||
? 'rounded-md bg-destructive px-3 py-1.5 text-xs font-medium text-destructive-foreground hover:bg-destructive/90'
|
||||
: 'rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90'
|
||||
? 'w7-btn rounded-md bg-destructive px-3 py-1.5 text-xs font-medium text-destructive-foreground hover:bg-destructive/90'
|
||||
: 'w7-btn rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90'
|
||||
"
|
||||
>
|
||||
{{ request().confirmLabel }}
|
||||
|
||||
@@ -79,7 +79,7 @@ import { FocusService } from '../focus.service';
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<button
|
||||
(click)="pick('cancel')"
|
||||
class="rounded-md px-3 py-1.5 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
class="w7-btn rounded-md px-3 py-1.5 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
{{ i18n.t('common.cancel') }}
|
||||
</button>
|
||||
@@ -101,14 +101,14 @@ import { FocusService } from '../focus.service';
|
||||
}
|
||||
<button
|
||||
(click)="pick('skip')"
|
||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
class="w7-btn rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
>
|
||||
{{ i18n.t('dialogs.skip') }}
|
||||
</button>
|
||||
<button
|
||||
#primaryBtn
|
||||
(click)="pick('overwrite')"
|
||||
class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="w7-btn rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{{ isDir() ? i18n.t('dialogs.merge') : i18n.t('dialogs.replace') }}
|
||||
</button>
|
||||
|
||||
@@ -33,14 +33,14 @@ import { FocusService } from '../focus.service';
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
(click)="cancel()"
|
||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
class="w7-btn rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
>
|
||||
{{ i18n.t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
(click)="submit()"
|
||||
[disabled]="!value().trim()"
|
||||
class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
class="w7-btn rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{{ request().confirmLabel }}
|
||||
</button>
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- win7 skin only: the window is frameless. The X runs the SAME dirty
|
||||
prompt as ⌘W and the Close button — never a raw window close. -->
|
||||
<app-title-bar [title]="ctx.name + (dirty() ? ' *' : '')" (closeRequested)="requestClose()" />
|
||||
<!-- header: on macOS this row IS the titlebar (hiddenInset + drag region),
|
||||
elsewhere the native titlebar sits above and this is a compact strip -->
|
||||
<header
|
||||
class="flex shrink-0 items-center gap-2 border-b border-border bg-chrome pr-2"
|
||||
class="w7-toolbar flex shrink-0 items-center gap-2 border-b border-border bg-chrome pr-2"
|
||||
[class.h-10]="isMacChrome"
|
||||
[class.pl-24]="isMacChrome"
|
||||
[class.electrobun-webkit-app-region-drag]="isMacChrome"
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ALT_LABEL, blockNativeContextMenu, EDITOR_CONTEXT, IS_MAC_CHROME, MOD_L
|
||||
import { RpcService } from '../rpc.service';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { ToastService } from '../toast.service';
|
||||
import { TitleBar } from '../title-bar/title-bar';
|
||||
import { Toasts } from '../toasts/toasts';
|
||||
|
||||
/** Debounce for mirroring the draft to bun (the traffic-light-close safety net). */
|
||||
@@ -30,7 +31,7 @@ const DRAFT_DEBOUNCE_MS = 400;
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [NgIcon, Toasts],
|
||||
imports: [NgIcon, Toasts, TitleBar],
|
||||
templateUrl: './editor-window.html',
|
||||
host: {
|
||||
'(window:keydown)': 'onKeydown($event)',
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
@if (menuOpen()) {
|
||||
<div class="fixed inset-0 z-20" (click)="menuOpen.set(false)"></div>
|
||||
<div
|
||||
class="absolute top-full left-2 z-30 mt-1 max-h-80 w-64 overflow-y-auto rounded-lg border border-border bg-card py-1 text-card-foreground shadow-xl"
|
||||
class="has-scrollbar absolute top-full left-2 z-30 mt-1 max-h-80 w-64 overflow-y-auto rounded-lg border border-border bg-card py-1 text-card-foreground shadow-xl"
|
||||
>
|
||||
<button
|
||||
(click)="pickLocal()"
|
||||
@@ -238,7 +238,7 @@
|
||||
(keydown)="onKeydown($event)"
|
||||
(mousedown)="activate()"
|
||||
(contextmenu)="listContextMenu($event)"
|
||||
class="h-full overflow-y-auto outline-none"
|
||||
class="has-scrollbar h-full overflow-y-auto outline-none"
|
||||
>
|
||||
@if (panel().connecting()) {
|
||||
<div class="panel-fade-in grid h-full place-items-center">
|
||||
|
||||
+26
-5
@@ -8,6 +8,9 @@ declare global {
|
||||
__APP_PLATFORM?: string;
|
||||
/** Which root component this window boots. */
|
||||
__APP_MODE?: 'main' | 'editor' | 'manager';
|
||||
/** Persisted skin — decides the window's native chrome, so it has to be
|
||||
* known before first paint (see chromeOptions in src/bun/windows.ts). */
|
||||
__APP_SKIN?: 'modern' | 'win7';
|
||||
/** Window-kind specific context (EditorOpenContext for editor windows). */
|
||||
__APP_CTX?: unknown;
|
||||
}
|
||||
@@ -15,14 +18,32 @@ declare global {
|
||||
|
||||
export const PLATFORM = typeof window !== 'undefined' ? (window.__APP_PLATFORM ?? '') : '';
|
||||
|
||||
/** True when the window uses macOS hiddenInset chrome (header fused with titlebar). */
|
||||
export const IS_MAC_CHROME = PLATFORM === 'darwin';
|
||||
const IS_MAC = PLATFORM === 'darwin';
|
||||
|
||||
/**
|
||||
* The win7 skin makes the window frameless on EVERY platform and draws its own
|
||||
* Aero title bar, so this is a window-chrome fact, not just a settings value —
|
||||
* it is baked into the preload rather than read from SettingsService, which
|
||||
* resolves too late for layout. Toggling the setting only affects windows
|
||||
* opened afterwards.
|
||||
*/
|
||||
export const IS_WIN7_CHROME =
|
||||
typeof window !== 'undefined' && window.__APP_SKIN === 'win7';
|
||||
|
||||
/**
|
||||
* True when macOS traffic lights overlay the app's header row (the fused
|
||||
* hiddenInset layout, which reserves pl-24 for them). False under the win7
|
||||
* skin even on macOS: titleBarStyle 'hidden' removes the traffic lights, so
|
||||
* reserving space for them would leave a gap.
|
||||
*/
|
||||
export const IS_MAC_CHROME = IS_MAC && !IS_WIN7_CHROME;
|
||||
|
||||
export const IS_WINDOWS = PLATFORM === 'win32';
|
||||
|
||||
/** Shortcut-hint labels: "⌘W" on macOS, "Ctrl+W" elsewhere. */
|
||||
export const MOD_LABEL = IS_MAC_CHROME ? '⌘' : 'Ctrl+';
|
||||
export const ALT_LABEL = IS_MAC_CHROME ? '⌥' : 'Alt+';
|
||||
/** Shortcut-hint labels: "⌘W" on macOS, "Ctrl+W" elsewhere. Keyed on the real
|
||||
* platform, NOT IS_MAC_CHROME — the keyboard does not change with the skin. */
|
||||
export const MOD_LABEL = IS_MAC ? '⌘' : 'Ctrl+';
|
||||
export const ALT_LABEL = IS_MAC ? '⌥' : 'Alt+';
|
||||
|
||||
export const APP_MODE: 'main' | 'editor' | 'manager' =
|
||||
typeof window !== 'undefined' ? (window.__APP_MODE ?? 'main') : 'main';
|
||||
|
||||
@@ -32,6 +32,7 @@ export class RpcService {
|
||||
private managerStateListeners = new Set<(open: boolean) => void>();
|
||||
private connectionsListeners = new Set<() => void>();
|
||||
private updateStatusListeners = new Set<(status: UpdateStatus) => void>();
|
||||
private windowStateListeners = new Set<(maximized: boolean) => void>();
|
||||
|
||||
private rpc: ReturnType<typeof Electroview.defineRPC<AppRPC>> | undefined;
|
||||
private ready: Promise<void> = Promise.resolve();
|
||||
@@ -71,6 +72,9 @@ export class RpcService {
|
||||
updateStatus: (status) => {
|
||||
for (const listener of this.updateStatusListeners) listener(status);
|
||||
},
|
||||
windowState: ({ maximized }) => {
|
||||
for (const listener of this.windowStateListeners) listener(maximized);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -123,6 +127,12 @@ export class RpcService {
|
||||
this.updateStatusListeners.add(listener);
|
||||
}
|
||||
|
||||
/** win7 skin: the frameless window's maximize state changed (derived from
|
||||
* the native resize event, so Aero Snap and Win+Up land here too). */
|
||||
onWindowState(listener: (maximized: boolean) => void): void {
|
||||
this.windowStateListeners.add(listener);
|
||||
}
|
||||
|
||||
/** Typed request proxy: `await rpc.request.listDir({...})`. Waits for the bridge. */
|
||||
get request() {
|
||||
const rpc = this.rpc;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { RpcService } from './rpc.service';
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SettingsService {
|
||||
readonly locale = signal<AppSettings['locale']>(DEFAULT_SETTINGS.locale);
|
||||
readonly skin = signal<AppSettings['skin']>(DEFAULT_SETTINGS.skin);
|
||||
readonly theme = signal<AppSettings['theme']>(DEFAULT_SETTINGS.theme);
|
||||
readonly darkTone = signal<AppSettings['darkTone']>(DEFAULT_SETTINGS.darkTone);
|
||||
readonly accent = signal<AppSettings['accent']>(DEFAULT_SETTINGS.accent);
|
||||
@@ -40,6 +41,7 @@ export class SettingsService {
|
||||
.addEventListener('change', (e) => this.systemDark.set(e.matches));
|
||||
// the default accent is not the CSS default — apply before first paint
|
||||
this.applyAccent();
|
||||
this.applySkin();
|
||||
if (!this.rpc.available) return;
|
||||
void this.rpc.request.getSettings({}).then((res) => {
|
||||
if (res.ok) this.applyAll(res.settings);
|
||||
@@ -50,6 +52,7 @@ export class SettingsService {
|
||||
|
||||
private applyAll(settings: AppSettings): void {
|
||||
this.locale.set(settings.locale);
|
||||
this.skin.set(settings.skin);
|
||||
this.theme.set(settings.theme);
|
||||
this.darkTone.set(settings.darkTone);
|
||||
this.accent.set(settings.accent);
|
||||
@@ -62,6 +65,7 @@ export class SettingsService {
|
||||
this.autoClearCompleted.set(settings.autoClearCompleted);
|
||||
this.editor.set(settings.editor);
|
||||
this.editorApp.set(settings.editorApp);
|
||||
this.applySkin();
|
||||
this.applyTheme();
|
||||
this.applyDarkTone();
|
||||
this.applyAccent();
|
||||
@@ -75,6 +79,12 @@ export class SettingsService {
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
setSkin(skin: AppSettings['skin']): void {
|
||||
this.skin.set(skin);
|
||||
this.applySkin();
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
setTheme(theme: AppSettings['theme']): void {
|
||||
this.theme.set(theme);
|
||||
this.applyTheme();
|
||||
@@ -142,6 +152,16 @@ export class SettingsService {
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
/**
|
||||
* The odd one out: a CLASS, not a data-* attribute. 7.scoped.css is
|
||||
* precompiled to require a literal `.win7` ancestor, so the class is not
|
||||
* ours to choose. Default ("modern") = class absent, matching the
|
||||
* delete-when-default rule the other axes follow.
|
||||
*/
|
||||
private applySkin(): void {
|
||||
document.documentElement.classList.toggle('win7', this.skin() === 'win7');
|
||||
}
|
||||
|
||||
private applyTheme(): void {
|
||||
const theme = this.theme();
|
||||
if (theme === 'system') {
|
||||
@@ -185,6 +205,7 @@ export class SettingsService {
|
||||
protected settings(): AppSettings {
|
||||
return {
|
||||
locale: this.locale(),
|
||||
skin: this.skin(),
|
||||
theme: this.theme(),
|
||||
darkTone: this.darkTone(),
|
||||
accent: this.accent(),
|
||||
|
||||
@@ -5,11 +5,12 @@ import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n';
|
||||
import type { AccentTone } from '@shared/types';
|
||||
import { I18nService } from '../i18n.service';
|
||||
import { PanelsService } from '../panels.service';
|
||||
import { IS_MAC_CHROME, MOD_LABEL } from '../platform';
|
||||
import { IS_MAC_CHROME, IS_WIN7_CHROME, MOD_LABEL } from '../platform';
|
||||
import { RpcService } from '../rpc.service';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { UpdaterService } from '../updater.service';
|
||||
|
||||
const SKINS = ['modern', 'win7'] as const;
|
||||
const THEMES = ['system', 'light', 'dark'] as const;
|
||||
const DARK_TONES = ['dark', 'darker'] as const;
|
||||
// Swatch colors are fixed mid-tones so every dot reads on both light and dark
|
||||
@@ -41,7 +42,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
<!-- On macOS this row IS the titlebar (hiddenInset): taller, padded past the
|
||||
traffic lights, and draggable. Elsewhere it's a normal compact row. -->
|
||||
<div
|
||||
class="relative flex items-center gap-0.5 border-b border-border bg-chrome pr-1.5 electrobun-webkit-app-region-drag"
|
||||
class="w7-toolbar relative flex items-center gap-0.5 border-b border-border bg-chrome pr-1.5 electrobun-webkit-app-region-drag"
|
||||
[class.h-10]="isMacChrome"
|
||||
[class.pl-24]="isMacChrome"
|
||||
[class.pl-1.5]="!isMacChrome"
|
||||
@@ -180,6 +181,30 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-muted-foreground">{{ i18n.t('options.skin.label') }}</span>
|
||||
<div class="flex rounded-md border border-border p-0.5">
|
||||
@for (option of skins; track option) {
|
||||
<button
|
||||
(click)="settings.setSkin(option)"
|
||||
class="rounded px-2 py-0.5 text-[11px]"
|
||||
[class.bg-accent]="settings.skin() === option"
|
||||
[class.text-foreground]="settings.skin() === option"
|
||||
[class.text-muted-foreground]="settings.skin() !== option"
|
||||
>
|
||||
{{ i18n.t('options.skin.' + option) }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- The CSS half of the skin flips live; the window frame does not
|
||||
(titleBarStyle is fixed at construction — see chromeOptions). -->
|
||||
@if (settings.skin() !== bootSkin) {
|
||||
<p class="mt-1 text-right text-[10px] text-muted-foreground">
|
||||
{{ i18n.t('options.skin.restartHint') }}
|
||||
</p>
|
||||
}
|
||||
|
||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-muted-foreground">{{ i18n.t('options.theme.label') }}</span>
|
||||
<div class="flex rounded-md border border-border p-0.5">
|
||||
@@ -436,6 +461,10 @@ export class TabBar {
|
||||
readonly modLabel = MOD_LABEL;
|
||||
readonly appName = BRANDING.appName;
|
||||
readonly buildLabel = BRANDING.buildLabel;
|
||||
readonly skins = SKINS;
|
||||
/** What the window was BUILT with, not the current setting — the frameless
|
||||
* chrome only changes on reopen, so the hint compares against this. */
|
||||
readonly bootSkin: (typeof SKINS)[number] = IS_WIN7_CHROME ? 'win7' : 'modern';
|
||||
readonly themes = THEMES;
|
||||
readonly darkTones = DARK_TONES;
|
||||
readonly accents = ACCENTS;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
<ng-icon name="mynaTrash" size="0.75rem" /> {{ i18n.t('transfer.clearFinished') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-40 overflow-y-auto border-t border-border bg-chrome">
|
||||
<div class="has-scrollbar max-h-40 overflow-y-auto border-t border-border bg-chrome">
|
||||
@for (job of transfers.jobs(); track job.id) {
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 text-xs">
|
||||
<ng-icon [name]="stateIcon(job)" size="0.95rem" [class]="stateClass(job)" />
|
||||
@@ -19,7 +19,16 @@
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-[11px] text-muted-foreground">{{ progressLabel(job) }}</div>
|
||||
@if (isActive(job)) {
|
||||
<div class="mt-0.5 h-1 overflow-hidden rounded-full bg-muted">
|
||||
<!-- role+aria are 7.css's hook for the Win7 progress bar
|
||||
([role=progressbar] > div); under the modern skin they are
|
||||
inert and the Tailwind classes below still draw the bar. -->
|
||||
<div
|
||||
role="progressbar"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
[attr.aria-valuenow]="jobPercent(job)"
|
||||
class="mt-0.5 h-1 overflow-hidden rounded-full bg-muted"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-full bg-primary transition-[width] duration-100"
|
||||
[style.width.%]="jobPercent(job)"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -20,6 +20,17 @@
|
||||
html:not([data-theme='light']) { background: oklch(0.2178 0.002 107); }
|
||||
html:not([data-theme='light'])[data-dark-tone='darker'] { background: oklch(0 0 0); }
|
||||
}
|
||||
/* win7 skin: same four states, mirroring the .win7 --background values
|
||||
in styles.css. The preload adds this class before any page script, so
|
||||
it is already on <html> here. Omitting this arm makes every win7
|
||||
window flash white before styles.css parses. */
|
||||
html.win7 { background: #ffffff; }
|
||||
html.win7[data-theme='dark'] { background: #1f1f1f; }
|
||||
html.win7[data-theme='dark'][data-dark-tone='darker'] { background: #121212; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html.win7:not([data-theme='light']) { background: #1f1f1f; }
|
||||
html.win7:not([data-theme='light'])[data-dark-tone='darker'] { background: #121212; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
/* Layer order — declared BEFORE the imports so it wins (a @layer statement is
|
||||
one of the only two rule types allowed above @import). `skin` sorts after
|
||||
`utilities`, so every 7.css rule outranks every Tailwind utility on the
|
||||
elements it targets; that inversion is what lets the win7 skin restyle
|
||||
~1900 inline utility classes without touching the templates. `skinfix` is
|
||||
above both, and uses `revert-layer` to hand individual properties back to
|
||||
`utilities` where 7.css's Win7-dialog metrics are wrong for this app.
|
||||
Tailwind re-declares the first four names, a no-op once ordered here.
|
||||
See the "Windows 7 skin" section at the bottom of this file. */
|
||||
@layer theme, base, components, utilities, skin, skinfix;
|
||||
@import "tailwindcss";
|
||||
/* The SCOPED 7.css build: postcss-prefix-selector rewrote every selector to
|
||||
`.win7` (root rules) or `.win7 <sel>` (the rest), so the whole framework is
|
||||
inert until SettingsService.applySkin puts that class on <html>. All 61
|
||||
icons are data URIs, so nothing needs copying into assets — and NOT the
|
||||
`7.inline.css` build, which flattens away the --w7-* vars the dark palette
|
||||
below depends on. The deep path is required: 7.css ships no `exports` map
|
||||
and Tailwind's resolver only reads `style`/`mainFields`, so a bare
|
||||
`@import "7.css"` finds no entry. */
|
||||
@import "7.css/dist/7.scoped.css" layer(skin);
|
||||
|
||||
/* shadcn-style theme tokens. Light stays the zinc family; the dark neutrals are a
|
||||
warm gray family (hue ~107) anchored on #1a1a19 — see the dark blocks below.
|
||||
@@ -276,3 +295,300 @@ body {
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Windows 7 skin — SettingsService.applySkin puts `.win7` on <html>
|
||||
==========================================================================
|
||||
|
||||
Three tiers, and the split matters:
|
||||
|
||||
- `@layer skin` (the 7.css import at the top of this file) owns the
|
||||
CONTROLS: buttons, inputs, tabs, progress bars, tree views, the Aero
|
||||
title bar. It sits ABOVE `utilities`, so a bare `button` rule in 7.css
|
||||
beats `.rounded-md` on that same button. That inversion is the whole
|
||||
trick — it restyles ~1900 inline utility classes with no template edits.
|
||||
- This unlayered section owns the BRIDGE: it re-points the app's own
|
||||
semantic tokens at Win7 values, so utilities Tailwind still controls
|
||||
(bg-card, border-border, text-muted-foreground) also come out
|
||||
Win7-correct. It adds NO new tokens on purpose — a new colour token
|
||||
would need a matching `--color-*` line in `@theme inline`.
|
||||
- `@layer skinfix` (last, above `skin`) hands individual properties BACK to
|
||||
the utilities layer with `revert-layer`, for the places where 7.css's
|
||||
Win7-dialog metrics are wrong for this app.
|
||||
|
||||
WARNING for future edits: because `skin` outranks `utilities`, any Tailwind
|
||||
utility added later to a <button>, <input>, <select>, <textarea> or <table>
|
||||
silently loses under this skin. `skinfix` is the only place to fix that.
|
||||
|
||||
Specificity notes — the blocks below must beat the token blocks above:
|
||||
:root.win7 (0,2,0) beats base :root (0,1,0)
|
||||
:root.win7[data-theme='dark'] (0,3,0) beats :root[data-theme=…] (0,2,0)
|
||||
and ties the darker-tone block (0,3,0),
|
||||
winning on source order (it is last).
|
||||
The accent formula blocks are deliberately NOT overridden: they only set
|
||||
--primary/--ring/--cursor-tint, and letting them through is what makes the
|
||||
accent picker recolour the Aero chrome — authentic, Win7 shipped exactly
|
||||
that ("Window Color" personalization). */
|
||||
|
||||
/* Segoe UI is not on macOS/Linux and is not redistributable. Noto Sans is the
|
||||
substitute 7.css itself names in its --w7-font fallback stack. Vendored
|
||||
(OFL-1.1) from @fontsource/noto-sans@5.3.0, files/noto-sans-latin-{400,700}
|
||||
-normal.woff2 -> ui/src/fonts/. Vendored rather than @imported from
|
||||
node_modules because Tailwind inlines imported CSS, which would leave
|
||||
@fontsource's relative url() rebased against the wrong directory. They live
|
||||
under src/ (not public/) so esbuild emits them as hashed assets and rewrites
|
||||
the url() — a public/ path is not resolvable at build time. */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans App';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url('./fonts/noto-sans-latin-400-normal.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Noto Sans App';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
src: url('./fonts/noto-sans-latin-700-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
:root.win7 {
|
||||
/* Real Segoe first (Windows), bundled substitute everywhere else. */
|
||||
--w7-family: 'Segoe UI', 'SegoeUI', 'Noto Sans App', system-ui, sans-serif;
|
||||
/* ~7.css's 9pt, but in px so it tracks the interface-size axis below.
|
||||
Restating the `font` SHORTHAND (not just the family) is deliberate:
|
||||
7.css applies `font: var(--w7-font)` on 12 component rules, and the
|
||||
shorthand also resets weight and line-height — pinning 400/1.4 here stops
|
||||
those rules from flattening the app's `font-medium` / leading. */
|
||||
--w7-fs: 12px;
|
||||
--w7-font: 400 var(--w7-fs) / 1.4 var(--w7-family);
|
||||
|
||||
/* Surface ramp, Win7 Explorer. This INVERTS the modern ramp's direction:
|
||||
there --chrome (frame strips) sits below --background (content panes);
|
||||
here the content pane is the brightest thing on screen and the frame is
|
||||
the #f0f0f0 window surface. Deliberate — that is what Explorer was. */
|
||||
--background: #ffffff;
|
||||
--chrome: #f0f0f0;
|
||||
--card: #f0f0f0;
|
||||
--foreground: #000000;
|
||||
--card-foreground: #000000;
|
||||
--muted-foreground: #6d6d6d;
|
||||
--secondary: #e8e8e8;
|
||||
--secondary-foreground: #000000;
|
||||
--muted: #e8e8e8;
|
||||
/* Win7's hover is a pale blue wash, not a grey — matches --w7-el-grad-h */
|
||||
--accent: #eaf6fd;
|
||||
--accent-foreground: #000000;
|
||||
--border: #d5d5d5;
|
||||
--input: #abadb3; /* 7.css's own text-input border */
|
||||
--destructive: #c62828;
|
||||
--destructive-foreground: #ffffff;
|
||||
--success: #06b025; /* the Win7 progress-bar green */
|
||||
|
||||
/* 5px, NOT 3px: `rounded-md` is 74 of the 112 radius utilities in the
|
||||
templates and compiles to calc(var(--radius) - 2px), so 5 lands it exactly
|
||||
on --w7-el-bdr (3px). --radius itself feeds `rounded-lg` (12 uses) at 5px,
|
||||
near the 6px window radius. Bare `rounded` (26 uses) is a Tailwind literal
|
||||
0.25rem and is unaffected. */
|
||||
--radius: 5px;
|
||||
}
|
||||
|
||||
/* Interface size: Win7 shipped this exact axis ("Smaller / Medium / Larger"),
|
||||
so scaling is more authentic than disabling it. One indirection covers all
|
||||
three tiers because var substitution is lazy. The --row-h / --list-font
|
||||
block further up scales the file list independently and is untouched. */
|
||||
:root.win7[data-text='compact'] { --w7-fs: 11px; }
|
||||
:root.win7[data-text='large'] { --w7-fs: 13px; }
|
||||
|
||||
/* Hand-authored dark Win7. Microsoft never shipped one — this exists so the
|
||||
theme and tone settings stay live under the skin. Only the colour-carrying
|
||||
--w7-* vars are listed: --w7-el-grad and --w7-w-grad are DERIVED from the
|
||||
values below and recolour for free, while --w7-el-grad-h/-a and
|
||||
--w7-li-bg-hl are literal hex in 7.css and so must be restated. The ~25
|
||||
geometry vars and ~20 base64 icon vars are theme-independent.
|
||||
DUPLICATED for forced-dark and system-dark, per the rule the token blocks
|
||||
above set: every dark value goes in BOTH. */
|
||||
:root.win7[data-theme='dark'] {
|
||||
--w7-surface: #2b2b2b;
|
||||
--w7-el-bg: #3a3a3a;
|
||||
--w7-el-bg-d: #333333;
|
||||
--w7-el-bg-s-1: #333333;
|
||||
--w7-el-bg-s-2: #262626;
|
||||
--w7-el-bd: #565656;
|
||||
--w7-el-bd-h: #4a8cbe;
|
||||
--w7-el-bd-a: #3f6c8c;
|
||||
--w7-el-bd-d: #3f3f3f;
|
||||
--w7-el-c: #f0f0f0;
|
||||
--w7-el-c-d: #6f6f6f;
|
||||
--w7-el-sd: inset 0 0 0 1px #ffffff14;
|
||||
--w7-el-grad-h: linear-gradient(#3c5a6b 45%, #315062 45%, #2b4557);
|
||||
--w7-el-grad-a: linear-gradient(#2c4657, #24384a 30% 50%, #1d2f3f 50%, #16242f);
|
||||
--w7-li-bd-hl: #3f6c8c;
|
||||
--w7-li-bg-hl: linear-gradient(#ffffff14, #4a6b8433 90%, #ffffff1a);
|
||||
--w7-w-bd: #000000d9;
|
||||
--w7-w-bg: #1f3a52;
|
||||
--w7-tab-bg: #2b2b2b;
|
||||
--w7-title-c: #8ab4e8;
|
||||
--w7-link-c: #6fb3ff;
|
||||
--w7-link-c-h: #9acbff;
|
||||
|
||||
--background: #1f1f1f;
|
||||
--chrome: #2b2b2b;
|
||||
--card: #2b2b2b;
|
||||
--foreground: #f0f0f0;
|
||||
--card-foreground: #f0f0f0;
|
||||
--muted-foreground: #a0a0a0;
|
||||
--secondary: #383838;
|
||||
--secondary-foreground: #f0f0f0;
|
||||
--muted: #383838;
|
||||
--accent: #2f4a5c;
|
||||
--accent-foreground: #f0f0f0;
|
||||
--border: #3c3c3c;
|
||||
--input: #55585c;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root.win7:not([data-theme='light']) {
|
||||
--w7-surface: #2b2b2b;
|
||||
--w7-el-bg: #3a3a3a;
|
||||
--w7-el-bg-d: #333333;
|
||||
--w7-el-bg-s-1: #333333;
|
||||
--w7-el-bg-s-2: #262626;
|
||||
--w7-el-bd: #565656;
|
||||
--w7-el-bd-h: #4a8cbe;
|
||||
--w7-el-bd-a: #3f6c8c;
|
||||
--w7-el-bd-d: #3f3f3f;
|
||||
--w7-el-c: #f0f0f0;
|
||||
--w7-el-c-d: #6f6f6f;
|
||||
--w7-el-sd: inset 0 0 0 1px #ffffff14;
|
||||
--w7-el-grad-h: linear-gradient(#3c5a6b 45%, #315062 45%, #2b4557);
|
||||
--w7-el-grad-a: linear-gradient(#2c4657, #24384a 30% 50%, #1d2f3f 50%, #16242f);
|
||||
--w7-li-bd-hl: #3f6c8c;
|
||||
--w7-li-bg-hl: linear-gradient(#ffffff14, #4a6b8433 90%, #ffffff1a);
|
||||
--w7-w-bd: #000000d9;
|
||||
--w7-w-bg: #1f3a52;
|
||||
--w7-tab-bg: #2b2b2b;
|
||||
--w7-title-c: #8ab4e8;
|
||||
--w7-link-c: #6fb3ff;
|
||||
--w7-link-c-h: #9acbff;
|
||||
|
||||
--background: #1f1f1f;
|
||||
--chrome: #2b2b2b;
|
||||
--card: #2b2b2b;
|
||||
--foreground: #f0f0f0;
|
||||
--card-foreground: #f0f0f0;
|
||||
--muted-foreground: #a0a0a0;
|
||||
--secondary: #383838;
|
||||
--secondary-foreground: #f0f0f0;
|
||||
--muted: #383838;
|
||||
--accent: #2f4a5c;
|
||||
--accent-foreground: #f0f0f0;
|
||||
--border: #3c3c3c;
|
||||
--input: #55585c;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark tone "darker": Aero has no meaningful true black, so rather than leave
|
||||
the Tone row a dead control the skin just goes a step deeper. Duplicated
|
||||
forced + system like everything else. */
|
||||
:root.win7[data-theme='dark'][data-dark-tone='darker'] {
|
||||
--w7-surface: #1a1a1a;
|
||||
--w7-el-bg: #262626;
|
||||
--w7-w-bg: #12202c;
|
||||
--background: #121212;
|
||||
--chrome: #1a1a1a;
|
||||
--card: #1a1a1a;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root.win7:not([data-theme='light'])[data-dark-tone='darker'] {
|
||||
--w7-surface: #1a1a1a;
|
||||
--w7-el-bg: #262626;
|
||||
--w7-w-bg: #12202c;
|
||||
--background: #121212;
|
||||
--chrome: #1a1a1a;
|
||||
--card: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accent -> Aero window colour, so the existing accent picker keeps meaning
|
||||
under the skin. --w7-w-bg is the one colour 7.css documents as settable. */
|
||||
:root.win7[data-accent] {
|
||||
--w7-w-bg: var(--primary);
|
||||
}
|
||||
|
||||
/* Unlayered on purpose: the html/body rule above sets font-family unlayered,
|
||||
so a layered override would lose. Family only — NOT font-size: the app's
|
||||
rem-based utilities are root-relative, and shifting the root would move
|
||||
every line box for no gain. */
|
||||
.win7 body {
|
||||
font-family: var(--w7-family);
|
||||
}
|
||||
|
||||
/* Neutralizers live in `skin`, NOT `skinfix`, and this is load-bearing:
|
||||
`revert-layer` rolls a property back to the previous LAYER, so from inside
|
||||
`skin` it lands on `utilities` (the Tailwind classes already in the
|
||||
templates) — which is the whole point. From `skinfix` it would land back on
|
||||
7.css and do nothing. Within `skin` these beat the imported rules on equal
|
||||
specificity by source order, since the import is at the top of this file. */
|
||||
@layer skin {
|
||||
/* 7.css sizes every <button> like a Win7 dialog push button
|
||||
(min-width 75px, min-height 23px, padding 0 12px). Most of this app's 84
|
||||
buttons are ~24px icon squares sized by utilities, so without this the tab
|
||||
bar wraps and the options popover overflows. Hand the BOX MODEL back to
|
||||
the utilities layer and keep 7.css's paint. The title bar's own controls
|
||||
are matched by a MORE specific 7.css selector (.title-bar-controls button)
|
||||
and so keep their Aero glyph metrics untouched. */
|
||||
.win7 button {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: revert-layer;
|
||||
text-align: revert-layer;
|
||||
color: revert-layer;
|
||||
}
|
||||
|
||||
/* 7.css hardcodes #fff / #abadb3 / #222 on form controls and tables, outside
|
||||
its own var system. Reverting routes them back through the bridged tokens
|
||||
above, which is what makes dark mode nearly free. */
|
||||
.win7 input[type='text'],
|
||||
.win7 input[type='password'],
|
||||
.win7 input[type='number'],
|
||||
.win7 input[type='search'],
|
||||
.win7 textarea {
|
||||
background-color: revert-layer;
|
||||
border-color: revert-layer;
|
||||
}
|
||||
.win7 table {
|
||||
background-color: revert-layer;
|
||||
color: revert-layer;
|
||||
}
|
||||
|
||||
/* Win7 toolbar buttons are FLAT with a hover glow, not Aero slabs —
|
||||
Explorer's command bar worked exactly that way. `w7-toolbar` marks the
|
||||
three header rows; a plain `.title-bar` sibling is deliberately NOT one of
|
||||
them, so its controls keep the real Aero treatment. `w7-flat` is the
|
||||
per-button escape hatch. Both classes are inert under the modern skin. */
|
||||
.win7 .w7-toolbar button,
|
||||
.win7 .w7-flat {
|
||||
background: revert-layer;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.win7 .w7-toolbar button::before,
|
||||
.win7 .w7-toolbar button::after,
|
||||
.win7 .w7-flat::before,
|
||||
.win7 .w7-flat::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Above `skin`, for rules that must state a value rather than defer to a
|
||||
neighbouring layer. */
|
||||
@layer skinfix {
|
||||
/* Opt back IN to real Win7 push-button metrics, for the dialog and options
|
||||
buttons where they are right. Inert under the modern skin. */
|
||||
.win7 .w7-btn {
|
||||
min-width: 75px;
|
||||
min-height: 23px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user