Groundwork for the continuous interface scale (a root font-size lever): every size the UI derives from must be root-relative, or it freezes while the rest grows. Gate for this commit: at the default 16px root the app renders pixel-identical to before — every conversion is exact (11px = 0.6875rem …). - The 8 panel-chrome tokens in styles.css: px -> rem, and the data-text compact/large tiers are DELETED — a continuous scale replaces discrete tiers. Same for --w7-fs (12px -> 0.75rem) and the win7 --radius (5px -> 0.3125rem, still landing rounded-md exactly on --w7-el-bdr's 3px). --radius-sm/md subtrahends: px -> rem, or corners would sharpen as the UI grew. - All 93 text-[11px]/text-[10px] arbitraries -> rem equivalents, plus the two inline padding:24px empty-state cells in the 7like file panel. - 7like.css gains a scale bridge in skinfix: 7.css is 370 px values with zero rem, and --w7-font reaches only 15 rules' TYPOGRAPHY — under a root lever win7 text would grow inside boxes frozen at 23px/14px/15px. The bridge re-expresses 7.css's size vars and its hardcoded box metrics (buttons via .w7-btn only, inputs, table rows/headers, title-bar controls, menu items, tabs, the 10pt check glyph) in rem at identical scale-1 pixels. 1px hairlines and focus rects stay px deliberately — crisp at any scale, and the tr border exists precisely to stop selection shifting rows. - Two skinfix traps found while writing it, one by reasoning and one by pixels: (a) NO blanket `.win7 button` restatement — skinfix outranks the skin-layer neutralizer that strips 7.css's 75x23 metrics off the ~84 icon buttons, so a blanket rule would re-inflate them all; (b) restating a base rule in skinfix also outranks 7.css's own VARIANTS of it by layer — the title-bar rule silently shrank the close button from 48px to 29px until the harness pixel diff caught it, and the active tab's padding-bottom needed the same treatment. Both variants are restated alongside their base rules. - TS px constants that mirror rem CSS are the latent-bug half of this work: both context menus' flip/clamp math converts through a new @core/metrics remPx() helper reading the LIVE root font-size (the shadlike menu's MENU_W=224 diverged from its w-56 box the moment scale != 1; the 7like menu's [style.width.px] pinned the box at 200px while text grew — it now binds width in rem, so math and box cannot disagree). The editors' cachedCharWidth is now keyed on the computed font string, self-invalidating under any font change, and both || px fallbacks derive from the element's font size instead of assuming a 16px root. Verified: tsc + build clean; modern and win7 live captures and the harness all diff to zero against pre-change baselines in their opaque regions (the harness to exactly zero end to end; modern's only delta is the native GTK titlebar focus state; win7's is the translucent Aero corner over the desktop). Co-Authored-By: Claude <noreply@anthropic.com>
150 lines
5.3 KiB
TypeScript
150 lines
5.3 KiB
TypeScript
import {
|
|
Component,
|
|
ElementRef,
|
|
afterNextRender,
|
|
computed,
|
|
inject,
|
|
input,
|
|
viewChild,
|
|
} from '@angular/core';
|
|
import { NgIcon } from '@ng-icons/core';
|
|
import { I18nService } from '@core/i18n.service';
|
|
import { DialogsService, type OverwriteChoice, type OverwriteRequest } from '@core/dialogs.service';
|
|
import { FocusService } from '@core/focus.service';
|
|
|
|
@Component({
|
|
selector: 'app-overwrite-dialog',
|
|
imports: [NgIcon],
|
|
template: `
|
|
<div
|
|
class="fixed inset-0 z-40 grid place-items-center bg-black/40"
|
|
(keydown.escape)="pick('cancel')"
|
|
>
|
|
<div
|
|
class="w-[26rem] rounded-lg border border-border bg-card p-4 text-card-foreground shadow-xl"
|
|
>
|
|
<div class="flex items-start justify-between gap-2">
|
|
<h2 class="min-w-0 text-sm font-semibold">
|
|
{{ isDir() ? i18n.t('dialogs.mergeFolder') : i18n.t('dialogs.replace') }}
|
|
<span class="font-mono">"{{ request().srcEntry.name }}"</span>?
|
|
</h2>
|
|
@if (request().total > 1) {
|
|
<span
|
|
class="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[0.625rem] font-medium text-muted-foreground"
|
|
>
|
|
{{ i18n.t('dialogs.ofTotal', { index: request().index, total: request().total }) }}
|
|
</span>
|
|
}
|
|
</div>
|
|
<p class="mt-1 text-xs text-muted-foreground">
|
|
@if (isDir()) {
|
|
{{ i18n.t('dialogs.overwriteDirBody') }}
|
|
} @else {
|
|
{{ i18n.t('dialogs.overwriteFileBody') }}
|
|
}
|
|
</p>
|
|
|
|
<!-- comparison -->
|
|
<div class="mt-3 grid grid-cols-2 gap-2">
|
|
@for (side of comparison(); track side.label) {
|
|
<div class="rounded-md border border-border p-2.5">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-[0.625rem] font-medium tracking-wide text-muted-foreground uppercase">
|
|
{{ i18n.t('dialogs.' + side.label) }}
|
|
</span>
|
|
@if (side.newer) {
|
|
<span class="rounded-full bg-success/15 px-1.5 text-[0.625rem] font-medium text-success">
|
|
{{ i18n.t('dialogs.newer') }}
|
|
</span>
|
|
}
|
|
</div>
|
|
<div class="mt-1.5 flex items-center gap-1.5 text-xs">
|
|
<ng-icon
|
|
[name]="isDir() ? 'mynaFolder' : 'mynaFile'"
|
|
size="0.9rem"
|
|
class="shrink-0 text-muted-foreground"
|
|
/>
|
|
<span class="font-mono tabular-nums">
|
|
{{ isDir() ? '—' : i18n.size(side.entry.size) }}
|
|
</span>
|
|
</div>
|
|
<div class="mt-1 font-mono text-[0.6875rem] tabular-nums text-muted-foreground">
|
|
{{ i18n.date(side.entry.mtime) }}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- actions -->
|
|
<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"
|
|
>
|
|
{{ i18n.t('common.cancel') }}
|
|
</button>
|
|
<div class="flex-1"></div>
|
|
@if (request().total > 1) {
|
|
<button
|
|
(click)="pick('skipAll')"
|
|
class="rounded-md px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
{{ i18n.t('dialogs.skipAll') }}
|
|
</button>
|
|
<button
|
|
(click)="pick('overwriteAll')"
|
|
class="rounded-md px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
{{ isDir() ? i18n.t('dialogs.mergeAll') : i18n.t('dialogs.replaceAll') }}
|
|
</button>
|
|
<div class="h-4 w-px bg-border"></div>
|
|
}
|
|
<button
|
|
(click)="pick('skip')"
|
|
class="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"
|
|
>
|
|
{{ isDir() ? i18n.t('dialogs.merge') : i18n.t('dialogs.replace') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
})
|
|
export class OverwriteDialog {
|
|
readonly i18n = inject(I18nService);
|
|
readonly request = input.required<OverwriteRequest>();
|
|
|
|
|
|
private readonly dialogs = inject(DialogsService);
|
|
private readonly focus = inject(FocusService);
|
|
private readonly primaryBtn = viewChild.required<ElementRef<HTMLButtonElement>>('primaryBtn');
|
|
|
|
constructor() {
|
|
afterNextRender(() => this.primaryBtn().nativeElement.focus());
|
|
}
|
|
|
|
readonly isDir = computed(() => this.request().srcEntry.type === 'dir');
|
|
|
|
readonly comparison = computed(() => {
|
|
const { srcEntry, dstEntry } = this.request();
|
|
return [
|
|
{ label: 'source', entry: srcEntry, newer: srcEntry.mtime > dstEntry.mtime },
|
|
{ label: 'destination', entry: dstEntry, newer: dstEntry.mtime > srcEntry.mtime },
|
|
];
|
|
});
|
|
|
|
pick(choice: OverwriteChoice): void {
|
|
const req = this.request();
|
|
this.dialogs.overwrite.set(null);
|
|
this.focus.bump();
|
|
req.resolve(choice);
|
|
}
|
|
}
|