Stage 1: extract the skin-agnostic UI layer into app/core/
First step of splitting the UI into two skin trees. All 12 services plus platform.ts, format.ts, file-icons.ts, icons.ts and app.config.ts move to ui/src/app/core/ — the layer both skins will share. This is a pure move: git records all 17 as renames, and no file content changed beyond import paths. The premise holds because `git diff main...feat/7css-skin` touches none of these files — the service layer was already skin-agnostic, so there is nothing to untangle, only to relocate. Components now reach it through a new `@core/*` tsconfig alias rather than a relative path. That is deliberate and load-bearing for the next stage: skin components are about to move under app/skins/<skin>/ at varying depths, and an alias keeps every one of these 67 imports correct wherever its file ends up. `@shared/*` (the bun-side src/shared) already set the precedent. Intra-core imports needed no edits at all — every ./sibling reference among the moved files points at another moved file. Verified: tsc clean, `bun run build:ui` clean (AOT resolves every import and template symbol), and the running window's opaque interior is pixel-identical to a pre-refactor capture. Note on that comparison: the Aero frame and title bar do NOT compare equal between launches, and that is not a regression — .window.glass::before runs at opacity .6, so the chrome composites over whatever desktop is behind the window and shifts by a uniform ~8% brightness. The check therefore diffs the opaque interior (inset 30px sides, 80px top), where anything this refactor could break would show. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
const SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
/** Locale-aware number formatting (decimal separator); cached per locale. */
|
||||
const NUM_FORMATS = new Map<string, Intl.NumberFormat>();
|
||||
function num(value: number, locale: string | undefined): string {
|
||||
const key = locale ?? '';
|
||||
let fmt = NUM_FORMATS.get(key);
|
||||
if (!fmt) {
|
||||
fmt = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });
|
||||
NUM_FORMATS.set(key, fmt);
|
||||
}
|
||||
return fmt.format(value);
|
||||
}
|
||||
|
||||
export function formatSize(bytes: number, locale?: string): string {
|
||||
let value = bytes;
|
||||
let unit = 0;
|
||||
while (value >= 1024 && unit < SIZE_UNITS.length - 1) {
|
||||
value /= 1024;
|
||||
unit++;
|
||||
}
|
||||
return `${unit === 0 ? num(value, locale) : num(Number(value.toFixed(1)), locale)} ${SIZE_UNITS[unit]}`;
|
||||
}
|
||||
|
||||
const DATE_FORMATS = new Map<string, Intl.DateTimeFormat>();
|
||||
export function formatDate(epochMs: number, locale?: string): string {
|
||||
if (!epochMs) return '';
|
||||
const key = locale ?? '';
|
||||
let fmt = DATE_FORMATS.get(key);
|
||||
if (!fmt) {
|
||||
fmt = new Intl.DateTimeFormat(locale, {
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
DATE_FORMATS.set(key, fmt);
|
||||
}
|
||||
return fmt.format(new Date(epochMs));
|
||||
}
|
||||
|
||||
/** bytes/sec -> "14.2 MB/s" or "113.8 Mbps" depending on the configured unit. */
|
||||
export function formatSpeed(bytesPerSec: number, unit: 'MBps' | 'Mbps', locale?: string): string {
|
||||
if (unit === 'Mbps') {
|
||||
return `${num(Number(((bytesPerSec * 8) / 1_000_000).toFixed(1)), locale)} Mbps`;
|
||||
}
|
||||
return `${formatSize(bytesPerSec, locale)}/s`;
|
||||
}
|
||||
|
||||
/** Seconds -> "0:42" / "3:07" / "1:02:15". */
|
||||
export function formatEta(seconds: number): string {
|
||||
if (!Number.isFinite(seconds) || seconds < 0) return '';
|
||||
const s = Math.round(seconds);
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
const mmss = `${m}:${String(sec).padStart(2, '0')}`;
|
||||
return h ? `${h}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}` : mmss;
|
||||
}
|
||||
|
||||
/**
|
||||
* Separator-aware join for UI-composed paths: remote and POSIX-local dirs use
|
||||
* "/", a Windows-local dir ("C:\Users\x") keeps "\" so paths stay consistent.
|
||||
*/
|
||||
export function joinPath(dir: string, name: string): string {
|
||||
const sep = dir.includes('\\') && !dir.includes('/') ? '\\' : '/';
|
||||
return dir.endsWith(sep) ? dir + name : `${dir}${sep}${name}`;
|
||||
}
|
||||
|
||||
/** "/" (POSIX) or a drive root like "C:\" — nothing above to navigate to. */
|
||||
export function isRootPath(p: string): boolean {
|
||||
return p === '/' || /^[A-Za-z]:[\\/]?$/.test(p);
|
||||
}
|
||||
|
||||
/** 0o755 -> "rwxr-xr-x" */
|
||||
export function formatMode(mode: number): string {
|
||||
const flags = 'rwxrwxrwx';
|
||||
let out = '';
|
||||
for (let i = 0; i < 9; i++) {
|
||||
out += mode & (0o400 >> i) ? flags[i] : '-';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user