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:
2026-08-16 04:50:53 +02:00
co-authored by Claude
parent 17bb1a14fb
commit 39e019af9f
21 changed files with 21 additions and 4 deletions
@@ -0,0 +1,59 @@
@if (transfers.expanded() && transfers.hasJobs()) {
<div class="flex items-center justify-between border-t border-border bg-chrome px-3 py-1 text-[11px] text-muted-foreground">
<span>
{{ i18n.t('transfer.summary', { active: transfers.activeJobs().length, finished: transfers.finishedJobs().length }) }}
</span>
<button
(click)="transfers.clearFinished()"
[disabled]="!transfers.finishedJobs().length"
class="flex items-center gap-1 rounded px-1.5 py-0.5 hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
>
<ng-icon name="mynaTrash" size="0.75rem" /> {{ i18n.t('transfer.clearFinished') }}
</button>
</div>
<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)" />
<span class="w-40 shrink-0 truncate font-medium" [title]="job.srcPath">{{ job.name }}</span>
<div class="min-w-0 flex-1">
<div class="truncate text-[11px] text-muted-foreground">{{ progressLabel(job) }}</div>
@if (isActive(job)) {
<!-- 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)"
></div>
</div>
}
</div>
@if (isActive(job)) {
<button
(click)="transfers.cancel(job.id)"
[title]="i18n.t('transfer.cancel')"
class="shrink-0 rounded p-1 text-muted-foreground hover:bg-accent hover:text-destructive"
>
<ng-icon name="mynaX" size="0.9rem" />
</button>
} @else {
<button
(click)="transfers.dismiss(job.id)"
[title]="i18n.t('transfer.dismiss')"
class="shrink-0 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
>
<ng-icon name="mynaX" size="0.9rem" />
</button>
}
</div>
}
</div>
}
@@ -0,0 +1,82 @@
import { Component, inject } from '@angular/core';
import { NgIcon } from '@ng-icons/core';
import type { TransferSnapshot } from '@shared/types';
import { formatEta } from '@core/format';
import { I18nService } from '@core/i18n.service';
import { SettingsService } from '@core/settings.service';
import { TransfersService } from '@core/transfers.service';
@Component({
selector: 'app-transfer-bar',
imports: [NgIcon],
templateUrl: './transfer-bar.html',
})
export class TransferBar {
readonly i18n = inject(I18nService);
readonly transfers = inject(TransfersService);
readonly settings = inject(SettingsService);
readonly formatSize = (bytes: number): string => this.i18n.size(bytes);
/** "14.2 MB/s · 0:42 left" while a job is moving bytes. */
speedLabel(job: TransferSnapshot): string {
if (job.state !== 'running') return '';
const speed = this.transfers.speeds().get(job.id);
if (!speed) return '';
const parts = [this.i18n.speed(speed, this.settings.speedUnit())];
const remaining = job.totalBytes - job.doneBytes;
if (remaining > 0 && speed > 0) {
parts.push(this.i18n.t('transfer.left', { eta: formatEta(remaining / speed) }));
}
return parts.join(' · ');
}
stateIcon(job: TransferSnapshot): string {
switch (job.state) {
case 'completed':
return 'mynaCheckCircle';
case 'failed':
case 'cancelled':
return 'mynaXCircle';
default:
return 'mynaSpinner';
}
}
stateClass(job: TransferSnapshot): string {
switch (job.state) {
case 'completed':
return 'shrink-0 text-success';
case 'failed':
return 'shrink-0 text-destructive';
case 'cancelled':
return 'shrink-0 text-muted-foreground';
default:
return 'shrink-0 animate-spin text-primary';
}
}
isActive(job: TransferSnapshot): boolean {
return job.state === 'queued' || job.state === 'scanning' || job.state === 'running';
}
jobPercent(job: TransferSnapshot): number {
if (!job.totalBytes) return job.state === 'completed' ? 100 : 0;
return Math.min(100, (job.doneBytes / job.totalBytes) * 100);
}
progressLabel(job: TransferSnapshot): string {
if (job.state === 'queued') return this.i18n.t('transfer.queuedState');
if (job.state === 'scanning') return this.i18n.t('transfer.scanning');
if (job.state === 'failed')
return `${job.currentFile ? `${job.currentFile} — ` : ''}${job.error ?? this.i18n.t('transfer.failedState')}`;
if (job.state === 'cancelled') return this.i18n.t('transfer.cancelled');
if (job.state === 'completed')
return this.i18n.t('transfer.completedFiles', {
count: job.doneFiles,
size: this.i18n.size(job.doneBytes),
});
const speed = this.speedLabel(job);
const base = `${job.currentFile} · ${this.i18n.size(job.doneBytes)} / ${this.i18n.size(job.totalBytes)}`;
return speed ? `${base} · ${speed}` : base;
}
}