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:
2026-08-16 04:49:27 +02:00
co-authored by Claude
parent ab0bb4ef6f
commit 17bb1a14fb
33 changed files with 74 additions and 68 deletions
+11 -11
View File
@@ -1,29 +1,29 @@
import { Component, inject, signal } from '@angular/core'; import { Component, inject, signal } from '@angular/core';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { I18nService } from './i18n.service'; import { I18nService } from '@core/i18n.service';
import { BRANDING } from '@shared/branding'; import { BRANDING } from '@shared/branding';
import type { ColumnKey } from '@shared/types'; import type { ColumnKey } from '@shared/types';
import { ConnectionsService } from './connections.service'; import { ConnectionsService } from '@core/connections.service';
import { ContextMenu } from './context-menu'; import { ContextMenu } from './context-menu';
import { ChmodDialog } from './dialogs/chmod-dialog'; import { ChmodDialog } from './dialogs/chmod-dialog';
import { ConfirmDialog } from './dialogs/confirm-dialog'; import { ConfirmDialog } from './dialogs/confirm-dialog';
import { OverwriteDialog } from './dialogs/overwrite-dialog'; import { OverwriteDialog } from './dialogs/overwrite-dialog';
import { PromptDialog } from './dialogs/prompt-dialog'; import { PromptDialog } from './dialogs/prompt-dialog';
import { DialogsService } from './dialogs.service'; import { DialogsService } from '@core/dialogs.service';
import { FileActionsService } from './file-actions.service'; import { FileActionsService } from '@core/file-actions.service';
import { FilePanel } from './file-panel/file-panel'; import { FilePanel } from './file-panel/file-panel';
import { PanelsService } from './panels.service'; import { PanelsService } from '@core/panels.service';
import { blockNativeContextMenu } from './platform'; import { blockNativeContextMenu } from '@core/platform';
import { RpcService } from './rpc.service'; import { RpcService } from '@core/rpc.service';
import { SettingsService } from './settings.service'; import { SettingsService } from '@core/settings.service';
import { StatusBar } from './status-bar/status-bar'; import { StatusBar } from './status-bar/status-bar';
import { TabBar } from './tab-bar/tab-bar'; import { TabBar } from './tab-bar/tab-bar';
import { TitleBar } from './title-bar/title-bar'; import { TitleBar } from './title-bar/title-bar';
import { ToastService } from './toast.service'; import { ToastService } from '@core/toast.service';
import { Toasts } from './toasts/toasts'; import { Toasts } from './toasts/toasts';
import { TransferBar } from './transfer-bar/transfer-bar'; import { TransferBar } from './transfer-bar/transfer-bar';
import { TransfersService } from './transfers.service'; import { TransfersService } from '@core/transfers.service';
import { UpdaterService } from './updater.service'; import { UpdaterService } from '@core/updater.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -1,14 +1,14 @@
import { Component, computed, inject, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { I18nService } from '../i18n.service'; import { I18nService } from '@core/i18n.service';
import type { SavedConnection, Stack } from '@shared/types'; import type { SavedConnection, Stack } from '@shared/types';
import { ConnectionsService } from '../connections.service'; import { ConnectionsService } from '@core/connections.service';
import { ConfirmDialog } from '../dialogs/confirm-dialog'; import { ConfirmDialog } from '../dialogs/confirm-dialog';
import { PromptDialog } from '../dialogs/prompt-dialog'; import { PromptDialog } from '../dialogs/prompt-dialog';
import { DialogsService } from '../dialogs.service'; import { DialogsService } from '@core/dialogs.service';
import { blockNativeContextMenu } from '../platform'; import { blockNativeContextMenu } from '@core/platform';
import { RpcService } from '../rpc.service'; import { RpcService } from '@core/rpc.service';
import { SettingsService } from '../settings.service'; import { SettingsService } from '@core/settings.service';
import { TitleBar } from '../title-bar/title-bar'; import { TitleBar } from '../title-bar/title-bar';
import { Toasts } from '../toasts/toasts'; import { Toasts } from '../toasts/toasts';
+2 -2
View File
@@ -1,6 +1,6 @@
import { Component, ElementRef, computed, effect, inject, signal, viewChild } from '@angular/core'; import { Component, ElementRef, computed, effect, inject, signal, viewChild } from '@angular/core';
import { ContextMenuService, type MenuItem } from './context-menu.service'; import { ContextMenuService, type MenuItem } from '@core/context-menu.service';
import { FocusService } from './focus.service'; import { FocusService } from '@core/focus.service';
/** Rough per-item metrics, used only to decide whether the menu must flip. */ /** Rough per-item metrics, used only to decide whether the menu must flip. */
const ITEM_H = 24; // 7.css: 4px padding x2 + ~15px line const ITEM_H = 24; // 7.css: 4px padding x2 + ~15px line
+3 -3
View File
@@ -1,7 +1,7 @@
import { Component, computed, inject, input, signal } from '@angular/core'; import { Component, computed, inject, input, signal } from '@angular/core';
import { I18nService } from '../i18n.service'; import { I18nService } from '@core/i18n.service';
import { DialogsService, type ChmodRequest } from '../dialogs.service'; import { DialogsService, type ChmodRequest } from '@core/dialogs.service';
import { FocusService } from '../focus.service'; import { FocusService } from '@core/focus.service';
const ROLES = ['roleOwner', 'roleGroup', 'roleOthers'] as const; const ROLES = ['roleOwner', 'roleGroup', 'roleOthers'] as const;
const PERMS = ['r', 'w', 'x'] as const; const PERMS = ['r', 'w', 'x'] as const;
+3 -3
View File
@@ -1,7 +1,7 @@
import { Component, ElementRef, afterNextRender, inject, input, viewChild } from '@angular/core'; import { Component, ElementRef, afterNextRender, inject, input, viewChild } from '@angular/core';
import { I18nService } from '../i18n.service'; import { I18nService } from '@core/i18n.service';
import { DialogsService, type ConfirmRequest } from '../dialogs.service'; import { DialogsService, type ConfirmRequest } from '@core/dialogs.service';
import { FocusService } from '../focus.service'; import { FocusService } from '@core/focus.service';
@Component({ @Component({
selector: 'app-confirm-dialog', selector: 'app-confirm-dialog',
+3 -3
View File
@@ -8,9 +8,9 @@ import {
viewChild, viewChild,
} from '@angular/core'; } from '@angular/core';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { I18nService } from '../i18n.service'; import { I18nService } from '@core/i18n.service';
import { DialogsService, type OverwriteChoice, type OverwriteRequest } from '../dialogs.service'; import { DialogsService, type OverwriteChoice, type OverwriteRequest } from '@core/dialogs.service';
import { FocusService } from '../focus.service'; import { FocusService } from '@core/focus.service';
@Component({ @Component({
selector: 'app-overwrite-dialog', selector: 'app-overwrite-dialog',
+3 -3
View File
@@ -7,9 +7,9 @@ import {
signal, signal,
viewChild, viewChild,
} from '@angular/core'; } from '@angular/core';
import { I18nService } from '../i18n.service'; import { I18nService } from '@core/i18n.service';
import { DialogsService, type PromptRequest } from '../dialogs.service'; import { DialogsService, type PromptRequest } from '@core/dialogs.service';
import { FocusService } from '../focus.service'; import { FocusService } from '@core/focus.service';
@Component({ @Component({
selector: 'app-prompt-dialog', selector: 'app-prompt-dialog',
+5 -5
View File
@@ -10,11 +10,11 @@ import {
viewChild, viewChild,
} from '@angular/core'; } from '@angular/core';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { I18nService } from '../i18n.service'; import { I18nService } from '@core/i18n.service';
import { ALT_LABEL, blockNativeContextMenu, EDITOR_CONTEXT, MOD_LABEL } from '../platform'; import { ALT_LABEL, blockNativeContextMenu, EDITOR_CONTEXT, MOD_LABEL } from '@core/platform';
import { RpcService } from '../rpc.service'; import { RpcService } from '@core/rpc.service';
import { SettingsService } from '../settings.service'; import { SettingsService } from '@core/settings.service';
import { ToastService } from '../toast.service'; import { ToastService } from '@core/toast.service';
import { TitleBar } from '../title-bar/title-bar'; import { TitleBar } from '../title-bar/title-bar';
import { Toasts } from '../toasts/toasts'; import { Toasts } from '../toasts/toasts';
+12 -12
View File
@@ -9,19 +9,19 @@ import {
viewChild, viewChild,
} from '@angular/core'; } from '@angular/core';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { I18nService } from '../i18n.service'; import { I18nService } from '@core/i18n.service';
import type { ColumnKey, FileEntry, SavedConnection, SshConfigHost } from '@shared/types'; import type { ColumnKey, FileEntry, SavedConnection, SshConfigHost } from '@shared/types';
import { ConnectionsService } from '../connections.service'; import { ConnectionsService } from '@core/connections.service';
import { ContextMenuService } from '../context-menu.service'; import { ContextMenuService } from '@core/context-menu.service';
import { DialogsService } from '../dialogs.service'; import { DialogsService } from '@core/dialogs.service';
import { FileActionsService } from '../file-actions.service'; import { FileActionsService } from '@core/file-actions.service';
import { fileGlyph, fileIconClass, fileKindKey } from '../file-icons'; import { fileGlyph, fileIconClass, fileKindKey } from '@core/file-icons';
import { formatMode } from '../format'; import { formatMode } from '@core/format';
import { PanelsService, type Side, type SortKey } from '../panels.service'; import { PanelsService, type Side, type SortKey } from '@core/panels.service';
import { IS_WINDOWS } from '../platform'; import { IS_WINDOWS } from '@core/platform';
import { RpcService } from '../rpc.service'; import { RpcService } from '@core/rpc.service';
import { SettingsService } from '../settings.service'; import { SettingsService } from '@core/settings.service';
import { TransfersService } from '../transfers.service'; import { TransfersService } from '@core/transfers.service';
/** Optional columns in fixed display order, with their track widths. */ /** Optional columns in fixed display order, with their track widths. */
const COLUMN_DEFS: { key: ColumnKey; label: string; width: string }[] = [ const COLUMN_DEFS: { key: ColumnKey; label: string; width: string }[] = [
+5 -5
View File
@@ -1,11 +1,11 @@
import { Component, computed, inject } from '@angular/core'; import { Component, computed, inject } from '@angular/core';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { I18nService } from '../i18n.service'; import { I18nService } from '@core/i18n.service';
import type { TransferSnapshot } from '@shared/types'; import type { TransferSnapshot } from '@shared/types';
import { formatSpeed } from '../format'; import { formatSpeed } from '@core/format';
import { PanelsService } from '../panels.service'; import { PanelsService } from '@core/panels.service';
import { SettingsService } from '../settings.service'; import { SettingsService } from '@core/settings.service';
import { TransfersService } from '../transfers.service'; import { TransfersService } from '@core/transfers.service';
@Component({ @Component({
selector: 'app-status-bar', selector: 'app-status-bar',
+6 -6
View File
@@ -3,12 +3,12 @@ import { NgIcon } from '@ng-icons/core';
import { BRANDING } from '@shared/branding'; import { BRANDING } from '@shared/branding';
import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n'; import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n';
import type { AccentTone } from '@shared/types'; import type { AccentTone } from '@shared/types';
import { I18nService } from '../i18n.service'; import { I18nService } from '@core/i18n.service';
import { PanelsService } from '../panels.service'; import { PanelsService } from '@core/panels.service';
import { MOD_LABEL } from '../platform'; import { MOD_LABEL } from '@core/platform';
import { RpcService } from '../rpc.service'; import { RpcService } from '@core/rpc.service';
import { SettingsService } from '../settings.service'; import { SettingsService } from '@core/settings.service';
import { UpdaterService } from '../updater.service'; import { UpdaterService } from '@core/updater.service';
const THEMES = ['system', 'light', 'dark'] as const; const THEMES = ['system', 'light', 'dark'] as const;
const DARK_TONES = ['dark', 'darker'] as const; const DARK_TONES = ['dark', 'darker'] as const;
+1 -1
View File
@@ -1,5 +1,5 @@
import { Component, inject, input, output, signal } from '@angular/core'; import { Component, inject, input, output, signal } from '@angular/core';
import { RpcService } from '../rpc.service'; import { RpcService } from '@core/rpc.service';
/** /**
* The Aero title bar. Rendered by all three window kinds. The native frame is * The Aero title bar. Rendered by all three window kinds. The native frame is
+1 -1
View File
@@ -1,6 +1,6 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { ToastService } from '../toast.service'; import { ToastService } from '@core/toast.service';
/** Toast stack overlay; shared by the main window and editor windows. */ /** Toast stack overlay; shared by the main window and editor windows. */
@Component({ @Component({
+4 -4
View File
@@ -1,10 +1,10 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import type { TransferSnapshot } from '@shared/types'; import type { TransferSnapshot } from '@shared/types';
import { formatEta } from '../format'; import { formatEta } from '@core/format';
import { I18nService } from '../i18n.service'; import { I18nService } from '@core/i18n.service';
import { SettingsService } from '../settings.service'; import { SettingsService } from '@core/settings.service';
import { TransfersService } from '../transfers.service'; import { TransfersService } from '@core/transfers.service';
@Component({ @Component({
selector: 'app-transfer-bar', selector: 'app-transfer-bar',
+2 -2
View File
@@ -1,9 +1,9 @@
import { bootstrapApplication } from '@angular/platform-browser'; import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config'; import { appConfig } from '@core/app.config';
import { App } from './app/app'; import { App } from './app/app';
import { ConnectionManager } from './app/connection-manager/connection-manager'; import { ConnectionManager } from './app/connection-manager/connection-manager';
import { EditorWindow } from './app/editor-window/editor-window'; import { EditorWindow } from './app/editor-window/editor-window';
import { APP_MODE } from './app/platform'; import { APP_MODE } from '@core/platform';
// One bundle, three window kinds: the preload global picks the root component. // One bundle, three window kinds: the preload global picks the root component.
const root = APP_MODE === 'editor' ? EditorWindow : APP_MODE === 'manager' ? ConnectionManager : App; const root = APP_MODE === 'editor' ? EditorWindow : APP_MODE === 'manager' ? ConnectionManager : App;
+7 -1
View File
@@ -4,8 +4,14 @@
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
/* @shared -> the bun/UI shared code (src/shared). @core -> the skin-agnostic
UI layer (services, platform, formatters). @core is an ALIAS rather than a
relative path on purpose: skin components live at varying depths under
app/skins/<skin>/, and an alias keeps their imports identical wherever a
component is moved to. */
"paths": { "paths": {
"@shared/*": ["../src/shared/*"] "@shared/*": ["../src/shared/*"],
"@core/*": ["./src/app/core/*"]
}, },
"noImplicitOverride": true, "noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,