The status bar's transfers summary is a text toggle, but 7.css painted it as a full Aero push button — a stray bordered slab sitting in the status line. Marked it w7-flat, the per-button escape hatch that existed for exactly this and had never been used. It did not work, for a reason worth keeping: 7.css restates border-color at `.win7 button:not(:disabled):not(:hover)` = (0,3,1), which outranks `.win7 .w7-flat` = (0,2,0). The .w7-toolbar arm of the same rule only ever won that fight by tying on specificity and landing later in the same layer — an accident w7-flat did not share. Split the rule: `background: revert-layer` stays in `skin` (it must land on `utilities`), and the paint-suppression literals move to `skinfix`, where layer order settles it instead of a specificity arms race. Docs updated in the same change per the working agreement — architecture.md gains the tabs contract, the w7-flat/w7-toolbar split and the frame transparency invariant; gotchas.md gains a win7-skin section (unlayered beats every layer, the (0,3,1) border-color restatement, overflow-x clipping the y-axis, 7.css's hardcoded light literals, exact selectors) plus the two Angular traps this round hit: a duplicate `class` attribute is silently dropped, and a backtick inside a component template's HTML comment terminates the TS template literal. Also recorded the verification trap that cost three wrong readings today: `bun run start` leaves the old instance running and `import -window` grabs the OLDEST match, so screenshots silently come from a stale build — this is what made the w7-flat fix above look like a no-op when it was already correct. Kill and count instances before capturing. And `pkill -f 'electrobun dev'` matches the shell running it and kills your own command. Co-Authored-By: Claude <noreply@anthropic.com>
82 lines
3.4 KiB
TypeScript
82 lines
3.4 KiB
TypeScript
import { Component, computed, inject } from '@angular/core';
|
|
import { NgIcon } from '@ng-icons/core';
|
|
import { I18nService } from '../i18n.service';
|
|
import type { TransferSnapshot } from '@shared/types';
|
|
import { formatSpeed } from '../format';
|
|
import { PanelsService } from '../panels.service';
|
|
import { SettingsService } from '../settings.service';
|
|
import { TransfersService } from '../transfers.service';
|
|
|
|
@Component({
|
|
selector: 'app-status-bar',
|
|
imports: [NgIcon],
|
|
template: `
|
|
<div
|
|
class="flex h-6 items-center justify-between gap-4 border-t border-border bg-chrome px-3 text-[11px] text-muted-foreground select-none"
|
|
>
|
|
<!-- transfers summary; click toggles the detailed list -->
|
|
<!-- w7-flat: this is a text toggle, not a push button. Without it 7.css
|
|
paints the full Aero slab and the status line reads as a stray
|
|
bordered button sitting in the status bar. -->
|
|
<button
|
|
(click)="transfers.expanded.set(!transfers.expanded())"
|
|
class="w7-flat flex min-w-0 items-center gap-1.5 hover:text-foreground"
|
|
[title]="i18n.t('transfer.showDetails')"
|
|
>
|
|
@if (transfers.currentJob(); as job) {
|
|
<ng-icon name="mynaSpinner" size="0.8rem" class="shrink-0 animate-spin text-primary" />
|
|
<span class="truncate">
|
|
{{ i18n.t('transfer.copying', { name: job.name, percent: percent(job), speed: speedSuffix(job) }) }}
|
|
@if (transfers.queuedCount(); as queued) {
|
|
· {{ i18n.t('transfer.queued', { count: queued }) }}
|
|
}
|
|
</span>
|
|
} @else if (transfers.failedJobs().length; as failed) {
|
|
<ng-icon name="mynaXCircle" size="0.8rem" class="shrink-0 text-destructive" />
|
|
<span class="text-destructive">
|
|
{{ i18n.t('transfer.failed', { count: failed }) }}
|
|
</span>
|
|
} @else if (transfers.hasJobs()) {
|
|
<ng-icon name="mynaCheckCircle" size="0.8rem" class="shrink-0 text-success" />
|
|
<span>{{ i18n.t('transfer.finished') }}</span>
|
|
} @else {
|
|
<span>{{ i18n.t('transfer.ready') }}</span>
|
|
}
|
|
</button>
|
|
|
|
<!-- active panel info -->
|
|
<div class="flex shrink-0 items-center gap-1.5">
|
|
<ng-icon
|
|
[name]="activePanel().sessionId() === 'local' ? 'mynaHardDrive' : 'mynaServers'"
|
|
size="0.8rem"
|
|
[class.text-primary]="activePanel().sessionId() !== 'local'"
|
|
/>
|
|
<span class="max-w-56 truncate">
|
|
{{ activePanel().sessionId() === 'local' ? i18n.t('common.local') : activePanel().title() }}
|
|
</span>
|
|
<span>· {{ i18n.t('transfer.items', { count: activePanel().sorted().length }) }}</span>
|
|
@if (activePanel().selected().size; as count) {
|
|
<span>· {{ i18n.t('transfer.selected', { count }) }}</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
`,
|
|
})
|
|
export class StatusBar {
|
|
readonly i18n = inject(I18nService);
|
|
readonly transfers = inject(TransfersService);
|
|
readonly panels = inject(PanelsService);
|
|
readonly settings = inject(SettingsService);
|
|
readonly activePanel = computed(() => this.panels.get(this.panels.activeSide()));
|
|
|
|
percent(job: TransferSnapshot): number {
|
|
if (!job.totalBytes) return 0;
|
|
return Math.min(100, Math.round((job.doneBytes / job.totalBytes) * 100));
|
|
}
|
|
|
|
speedSuffix(job: TransferSnapshot): string {
|
|
const speed = this.transfers.speeds().get(job.id);
|
|
return speed ? ` · ${formatSpeed(speed, this.settings.speedUnit())}` : '';
|
|
}
|
|
}
|