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
+2
View File
@@ -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">
+14
View File
@@ -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()',
+2 -2
View File
@@ -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>
+3 -3
View File
@@ -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 }}
+3 -3
View File
@@ -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>
+2 -2
View File
@@ -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>
+4 -1
View File
@@ -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"
+2 -1
View File
@@ -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)',
+2 -2
View File
@@ -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
View File
@@ -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';
+10
View File
@@ -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;
+21
View File
@@ -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(),
+31 -2
View File
@@ -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;
+72
View File
@@ -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 -2
View File
@@ -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)"