Stage 2: move the win7 component tree into app/skins/7like/
All 14 components (including title-bar/, which has no modern counterpart)
move under app/skins/7like/. git records every one as a rename — no content
changed except main.ts.
The @core alias from stage 1 paid off exactly as intended: not one of the 67
service imports needed touching despite every file changing depth. The only
relative imports left inside the tree are skin-internal (app -> its dialogs,
connection-manager -> title-bar), and those survived because the move
preserved their relative structure.
Added skins/7like/index.ts exporting SEVEN_ROOTS { main, manager, editor }.
Every skin will export that same shape, so main.ts can pick a tree by skin
and then a root by APP_MODE without knowing either skin's internals. main.ts
selects from that barrel now, with the second axis still hardcoded until the
shadlike tree lands.
Verified: tsc clean, build:ui clean, opaque interior pixel-identical to the
pre-refactor baseline. That last check also proves Tailwind still scans the
templates from their new skins/** paths — a silent utility purge would have
collapsed the layout rather than reproducing it exactly.
The three structural 7.css contracts that a careless move breaks silently are
intact by construction, since no markup changed: app-title-bar is still a
direct child of .window.w7-frame, nothing new nests inside menu[role=tablist],
and the context menu still emits ul[role=menu] > li[role=menuitem] > button.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
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-[10px] 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-[10px] 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-[10px] 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-[11px] 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="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>
|
||||
<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="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="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>
|
||||
</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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user