From 17bb1a14fb0c16913e693ebea79e8b535026c990 Mon Sep 17 00:00:00 2001 From: Jokin Date: Sun, 16 Aug 2026 04:49:27 +0200 Subject: [PATCH] Stage 1: extract the skin-agnostic UI layer into app/core/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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// 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 --- ui/src/app/app.ts | 22 ++++++++--------- .../connection-manager/connection-manager.ts | 12 +++++----- ui/src/app/context-menu.ts | 4 ++-- ui/src/app/{ => core}/app.config.ts | 0 ui/src/app/{ => core}/connections.service.ts | 0 ui/src/app/{ => core}/context-menu.service.ts | 0 ui/src/app/{ => core}/dialogs.service.ts | 0 ui/src/app/{ => core}/file-actions.service.ts | 0 ui/src/app/{ => core}/file-icons.ts | 0 ui/src/app/{ => core}/focus.service.ts | 0 ui/src/app/{ => core}/format.ts | 0 ui/src/app/{ => core}/i18n.service.ts | 0 ui/src/app/{ => core}/icons.ts | 0 ui/src/app/{ => core}/panels.service.ts | 0 ui/src/app/{ => core}/platform.ts | 0 ui/src/app/{ => core}/rpc.service.ts | 0 ui/src/app/{ => core}/settings.service.ts | 0 ui/src/app/{ => core}/toast.service.ts | 0 ui/src/app/{ => core}/transfers.service.ts | 0 ui/src/app/{ => core}/updater.service.ts | 0 ui/src/app/dialogs/chmod-dialog.ts | 6 ++--- ui/src/app/dialogs/confirm-dialog.ts | 6 ++--- ui/src/app/dialogs/overwrite-dialog.ts | 6 ++--- ui/src/app/dialogs/prompt-dialog.ts | 6 ++--- ui/src/app/editor-window/editor-window.ts | 10 ++++---- ui/src/app/file-panel/file-panel.ts | 24 +++++++++---------- ui/src/app/status-bar/status-bar.ts | 10 ++++---- ui/src/app/tab-bar/tab-bar.ts | 12 +++++----- ui/src/app/title-bar/title-bar.ts | 2 +- ui/src/app/toasts/toasts.ts | 2 +- ui/src/app/transfer-bar/transfer-bar.ts | 8 +++---- ui/src/main.ts | 4 ++-- ui/tsconfig.json | 8 ++++++- 33 files changed, 74 insertions(+), 68 deletions(-) rename ui/src/app/{ => core}/app.config.ts (100%) rename ui/src/app/{ => core}/connections.service.ts (100%) rename ui/src/app/{ => core}/context-menu.service.ts (100%) rename ui/src/app/{ => core}/dialogs.service.ts (100%) rename ui/src/app/{ => core}/file-actions.service.ts (100%) rename ui/src/app/{ => core}/file-icons.ts (100%) rename ui/src/app/{ => core}/focus.service.ts (100%) rename ui/src/app/{ => core}/format.ts (100%) rename ui/src/app/{ => core}/i18n.service.ts (100%) rename ui/src/app/{ => core}/icons.ts (100%) rename ui/src/app/{ => core}/panels.service.ts (100%) rename ui/src/app/{ => core}/platform.ts (100%) rename ui/src/app/{ => core}/rpc.service.ts (100%) rename ui/src/app/{ => core}/settings.service.ts (100%) rename ui/src/app/{ => core}/toast.service.ts (100%) rename ui/src/app/{ => core}/transfers.service.ts (100%) rename ui/src/app/{ => core}/updater.service.ts (100%) diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 62a237d..2445ff7 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -1,29 +1,29 @@ import { Component, inject, signal } from '@angular/core'; import { NgIcon } from '@ng-icons/core'; -import { I18nService } from './i18n.service'; +import { I18nService } from '@core/i18n.service'; import { BRANDING } from '@shared/branding'; import type { ColumnKey } from '@shared/types'; -import { ConnectionsService } from './connections.service'; +import { ConnectionsService } from '@core/connections.service'; import { ContextMenu } from './context-menu'; import { ChmodDialog } from './dialogs/chmod-dialog'; import { ConfirmDialog } from './dialogs/confirm-dialog'; import { OverwriteDialog } from './dialogs/overwrite-dialog'; import { PromptDialog } from './dialogs/prompt-dialog'; -import { DialogsService } from './dialogs.service'; -import { FileActionsService } from './file-actions.service'; +import { DialogsService } from '@core/dialogs.service'; +import { FileActionsService } from '@core/file-actions.service'; import { FilePanel } from './file-panel/file-panel'; -import { PanelsService } from './panels.service'; -import { blockNativeContextMenu } from './platform'; -import { RpcService } from './rpc.service'; -import { SettingsService } from './settings.service'; +import { PanelsService } from '@core/panels.service'; +import { blockNativeContextMenu } from '@core/platform'; +import { RpcService } from '@core/rpc.service'; +import { SettingsService } from '@core/settings.service'; import { StatusBar } from './status-bar/status-bar'; import { TabBar } from './tab-bar/tab-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 { TransferBar } from './transfer-bar/transfer-bar'; -import { TransfersService } from './transfers.service'; -import { UpdaterService } from './updater.service'; +import { TransfersService } from '@core/transfers.service'; +import { UpdaterService } from '@core/updater.service'; @Component({ selector: 'app-root', diff --git a/ui/src/app/connection-manager/connection-manager.ts b/ui/src/app/connection-manager/connection-manager.ts index 5d86d8e..e14eecc 100644 --- a/ui/src/app/connection-manager/connection-manager.ts +++ b/ui/src/app/connection-manager/connection-manager.ts @@ -1,14 +1,14 @@ import { Component, computed, inject, signal } from '@angular/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 { ConnectionsService } from '../connections.service'; +import { ConnectionsService } from '@core/connections.service'; import { ConfirmDialog } from '../dialogs/confirm-dialog'; import { PromptDialog } from '../dialogs/prompt-dialog'; -import { DialogsService } from '../dialogs.service'; -import { blockNativeContextMenu } from '../platform'; -import { RpcService } from '../rpc.service'; -import { SettingsService } from '../settings.service'; +import { DialogsService } from '@core/dialogs.service'; +import { blockNativeContextMenu } from '@core/platform'; +import { RpcService } from '@core/rpc.service'; +import { SettingsService } from '@core/settings.service'; import { TitleBar } from '../title-bar/title-bar'; import { Toasts } from '../toasts/toasts'; diff --git a/ui/src/app/context-menu.ts b/ui/src/app/context-menu.ts index 7a8ade5..b7cfea9 100644 --- a/ui/src/app/context-menu.ts +++ b/ui/src/app/context-menu.ts @@ -1,6 +1,6 @@ import { Component, ElementRef, computed, effect, inject, signal, viewChild } from '@angular/core'; -import { ContextMenuService, type MenuItem } from './context-menu.service'; -import { FocusService } from './focus.service'; +import { ContextMenuService, type MenuItem } from '@core/context-menu.service'; +import { FocusService } from '@core/focus.service'; /** Rough per-item metrics, used only to decide whether the menu must flip. */ const ITEM_H = 24; // 7.css: 4px padding x2 + ~15px line diff --git a/ui/src/app/app.config.ts b/ui/src/app/core/app.config.ts similarity index 100% rename from ui/src/app/app.config.ts rename to ui/src/app/core/app.config.ts diff --git a/ui/src/app/connections.service.ts b/ui/src/app/core/connections.service.ts similarity index 100% rename from ui/src/app/connections.service.ts rename to ui/src/app/core/connections.service.ts diff --git a/ui/src/app/context-menu.service.ts b/ui/src/app/core/context-menu.service.ts similarity index 100% rename from ui/src/app/context-menu.service.ts rename to ui/src/app/core/context-menu.service.ts diff --git a/ui/src/app/dialogs.service.ts b/ui/src/app/core/dialogs.service.ts similarity index 100% rename from ui/src/app/dialogs.service.ts rename to ui/src/app/core/dialogs.service.ts diff --git a/ui/src/app/file-actions.service.ts b/ui/src/app/core/file-actions.service.ts similarity index 100% rename from ui/src/app/file-actions.service.ts rename to ui/src/app/core/file-actions.service.ts diff --git a/ui/src/app/file-icons.ts b/ui/src/app/core/file-icons.ts similarity index 100% rename from ui/src/app/file-icons.ts rename to ui/src/app/core/file-icons.ts diff --git a/ui/src/app/focus.service.ts b/ui/src/app/core/focus.service.ts similarity index 100% rename from ui/src/app/focus.service.ts rename to ui/src/app/core/focus.service.ts diff --git a/ui/src/app/format.ts b/ui/src/app/core/format.ts similarity index 100% rename from ui/src/app/format.ts rename to ui/src/app/core/format.ts diff --git a/ui/src/app/i18n.service.ts b/ui/src/app/core/i18n.service.ts similarity index 100% rename from ui/src/app/i18n.service.ts rename to ui/src/app/core/i18n.service.ts diff --git a/ui/src/app/icons.ts b/ui/src/app/core/icons.ts similarity index 100% rename from ui/src/app/icons.ts rename to ui/src/app/core/icons.ts diff --git a/ui/src/app/panels.service.ts b/ui/src/app/core/panels.service.ts similarity index 100% rename from ui/src/app/panels.service.ts rename to ui/src/app/core/panels.service.ts diff --git a/ui/src/app/platform.ts b/ui/src/app/core/platform.ts similarity index 100% rename from ui/src/app/platform.ts rename to ui/src/app/core/platform.ts diff --git a/ui/src/app/rpc.service.ts b/ui/src/app/core/rpc.service.ts similarity index 100% rename from ui/src/app/rpc.service.ts rename to ui/src/app/core/rpc.service.ts diff --git a/ui/src/app/settings.service.ts b/ui/src/app/core/settings.service.ts similarity index 100% rename from ui/src/app/settings.service.ts rename to ui/src/app/core/settings.service.ts diff --git a/ui/src/app/toast.service.ts b/ui/src/app/core/toast.service.ts similarity index 100% rename from ui/src/app/toast.service.ts rename to ui/src/app/core/toast.service.ts diff --git a/ui/src/app/transfers.service.ts b/ui/src/app/core/transfers.service.ts similarity index 100% rename from ui/src/app/transfers.service.ts rename to ui/src/app/core/transfers.service.ts diff --git a/ui/src/app/updater.service.ts b/ui/src/app/core/updater.service.ts similarity index 100% rename from ui/src/app/updater.service.ts rename to ui/src/app/core/updater.service.ts diff --git a/ui/src/app/dialogs/chmod-dialog.ts b/ui/src/app/dialogs/chmod-dialog.ts index be49784..01082ae 100644 --- a/ui/src/app/dialogs/chmod-dialog.ts +++ b/ui/src/app/dialogs/chmod-dialog.ts @@ -1,7 +1,7 @@ import { Component, computed, inject, input, signal } from '@angular/core'; -import { I18nService } from '../i18n.service'; -import { DialogsService, type ChmodRequest } from '../dialogs.service'; -import { FocusService } from '../focus.service'; +import { I18nService } from '@core/i18n.service'; +import { DialogsService, type ChmodRequest } from '@core/dialogs.service'; +import { FocusService } from '@core/focus.service'; const ROLES = ['roleOwner', 'roleGroup', 'roleOthers'] as const; const PERMS = ['r', 'w', 'x'] as const; diff --git a/ui/src/app/dialogs/confirm-dialog.ts b/ui/src/app/dialogs/confirm-dialog.ts index 1356c6e..ba3bce7 100644 --- a/ui/src/app/dialogs/confirm-dialog.ts +++ b/ui/src/app/dialogs/confirm-dialog.ts @@ -1,7 +1,7 @@ import { Component, ElementRef, afterNextRender, inject, input, viewChild } from '@angular/core'; -import { I18nService } from '../i18n.service'; -import { DialogsService, type ConfirmRequest } from '../dialogs.service'; -import { FocusService } from '../focus.service'; +import { I18nService } from '@core/i18n.service'; +import { DialogsService, type ConfirmRequest } from '@core/dialogs.service'; +import { FocusService } from '@core/focus.service'; @Component({ selector: 'app-confirm-dialog', diff --git a/ui/src/app/dialogs/overwrite-dialog.ts b/ui/src/app/dialogs/overwrite-dialog.ts index 62d9af3..a1e35a4 100644 --- a/ui/src/app/dialogs/overwrite-dialog.ts +++ b/ui/src/app/dialogs/overwrite-dialog.ts @@ -8,9 +8,9 @@ import { viewChild, } from '@angular/core'; import { NgIcon } from '@ng-icons/core'; -import { I18nService } from '../i18n.service'; -import { DialogsService, type OverwriteChoice, type OverwriteRequest } from '../dialogs.service'; -import { FocusService } from '../focus.service'; +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', diff --git a/ui/src/app/dialogs/prompt-dialog.ts b/ui/src/app/dialogs/prompt-dialog.ts index fe964ba..067bf47 100644 --- a/ui/src/app/dialogs/prompt-dialog.ts +++ b/ui/src/app/dialogs/prompt-dialog.ts @@ -7,9 +7,9 @@ import { signal, viewChild, } from '@angular/core'; -import { I18nService } from '../i18n.service'; -import { DialogsService, type PromptRequest } from '../dialogs.service'; -import { FocusService } from '../focus.service'; +import { I18nService } from '@core/i18n.service'; +import { DialogsService, type PromptRequest } from '@core/dialogs.service'; +import { FocusService } from '@core/focus.service'; @Component({ selector: 'app-prompt-dialog', diff --git a/ui/src/app/editor-window/editor-window.ts b/ui/src/app/editor-window/editor-window.ts index 4b98cfa..62491c0 100644 --- a/ui/src/app/editor-window/editor-window.ts +++ b/ui/src/app/editor-window/editor-window.ts @@ -10,11 +10,11 @@ import { viewChild, } from '@angular/core'; import { NgIcon } from '@ng-icons/core'; -import { I18nService } from '../i18n.service'; -import { ALT_LABEL, blockNativeContextMenu, EDITOR_CONTEXT, MOD_LABEL } from '../platform'; -import { RpcService } from '../rpc.service'; -import { SettingsService } from '../settings.service'; -import { ToastService } from '../toast.service'; +import { I18nService } from '@core/i18n.service'; +import { ALT_LABEL, blockNativeContextMenu, EDITOR_CONTEXT, MOD_LABEL } from '@core/platform'; +import { RpcService } from '@core/rpc.service'; +import { SettingsService } from '@core/settings.service'; +import { ToastService } from '@core/toast.service'; import { TitleBar } from '../title-bar/title-bar'; import { Toasts } from '../toasts/toasts'; diff --git a/ui/src/app/file-panel/file-panel.ts b/ui/src/app/file-panel/file-panel.ts index e29a93d..8739410 100644 --- a/ui/src/app/file-panel/file-panel.ts +++ b/ui/src/app/file-panel/file-panel.ts @@ -9,19 +9,19 @@ import { viewChild, } from '@angular/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 { ConnectionsService } from '../connections.service'; -import { ContextMenuService } from '../context-menu.service'; -import { DialogsService } from '../dialogs.service'; -import { FileActionsService } from '../file-actions.service'; -import { fileGlyph, fileIconClass, fileKindKey } from '../file-icons'; -import { formatMode } from '../format'; -import { PanelsService, type Side, type SortKey } from '../panels.service'; -import { IS_WINDOWS } from '../platform'; -import { RpcService } from '../rpc.service'; -import { SettingsService } from '../settings.service'; -import { TransfersService } from '../transfers.service'; +import { ConnectionsService } from '@core/connections.service'; +import { ContextMenuService } from '@core/context-menu.service'; +import { DialogsService } from '@core/dialogs.service'; +import { FileActionsService } from '@core/file-actions.service'; +import { fileGlyph, fileIconClass, fileKindKey } from '@core/file-icons'; +import { formatMode } from '@core/format'; +import { PanelsService, type Side, type SortKey } from '@core/panels.service'; +import { IS_WINDOWS } from '@core/platform'; +import { RpcService } from '@core/rpc.service'; +import { SettingsService } from '@core/settings.service'; +import { TransfersService } from '@core/transfers.service'; /** Optional columns in fixed display order, with their track widths. */ const COLUMN_DEFS: { key: ColumnKey; label: string; width: string }[] = [ diff --git a/ui/src/app/status-bar/status-bar.ts b/ui/src/app/status-bar/status-bar.ts index 868bf19..66634d5 100644 --- a/ui/src/app/status-bar/status-bar.ts +++ b/ui/src/app/status-bar/status-bar.ts @@ -1,11 +1,11 @@ import { Component, computed, inject } from '@angular/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 { formatSpeed } from '../format'; -import { PanelsService } from '../panels.service'; -import { SettingsService } from '../settings.service'; -import { TransfersService } from '../transfers.service'; +import { formatSpeed } from '@core/format'; +import { PanelsService } from '@core/panels.service'; +import { SettingsService } from '@core/settings.service'; +import { TransfersService } from '@core/transfers.service'; @Component({ selector: 'app-status-bar', diff --git a/ui/src/app/tab-bar/tab-bar.ts b/ui/src/app/tab-bar/tab-bar.ts index 7a060e4..ffa6c78 100644 --- a/ui/src/app/tab-bar/tab-bar.ts +++ b/ui/src/app/tab-bar/tab-bar.ts @@ -3,12 +3,12 @@ import { NgIcon } from '@ng-icons/core'; import { BRANDING } from '@shared/branding'; import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n'; import type { AccentTone } from '@shared/types'; -import { I18nService } from '../i18n.service'; -import { PanelsService } from '../panels.service'; -import { MOD_LABEL } from '../platform'; -import { RpcService } from '../rpc.service'; -import { SettingsService } from '../settings.service'; -import { UpdaterService } from '../updater.service'; +import { I18nService } from '@core/i18n.service'; +import { PanelsService } from '@core/panels.service'; +import { MOD_LABEL } from '@core/platform'; +import { RpcService } from '@core/rpc.service'; +import { SettingsService } from '@core/settings.service'; +import { UpdaterService } from '@core/updater.service'; const THEMES = ['system', 'light', 'dark'] as const; const DARK_TONES = ['dark', 'darker'] as const; diff --git a/ui/src/app/title-bar/title-bar.ts b/ui/src/app/title-bar/title-bar.ts index b2277d7..5ddbeb9 100644 --- a/ui/src/app/title-bar/title-bar.ts +++ b/ui/src/app/title-bar/title-bar.ts @@ -1,5 +1,5 @@ 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 diff --git a/ui/src/app/toasts/toasts.ts b/ui/src/app/toasts/toasts.ts index 2459eca..a09a13d 100644 --- a/ui/src/app/toasts/toasts.ts +++ b/ui/src/app/toasts/toasts.ts @@ -1,6 +1,6 @@ import { Component, inject } from '@angular/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. */ @Component({ diff --git a/ui/src/app/transfer-bar/transfer-bar.ts b/ui/src/app/transfer-bar/transfer-bar.ts index 50951e8..a23ed12 100644 --- a/ui/src/app/transfer-bar/transfer-bar.ts +++ b/ui/src/app/transfer-bar/transfer-bar.ts @@ -1,10 +1,10 @@ import { Component, inject } from '@angular/core'; import { NgIcon } from '@ng-icons/core'; import type { TransferSnapshot } from '@shared/types'; -import { formatEta } from '../format'; -import { I18nService } from '../i18n.service'; -import { SettingsService } from '../settings.service'; -import { TransfersService } from '../transfers.service'; +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', diff --git a/ui/src/main.ts b/ui/src/main.ts index 147cdf0..aa52a98 100644 --- a/ui/src/main.ts +++ b/ui/src/main.ts @@ -1,9 +1,9 @@ import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; +import { appConfig } from '@core/app.config'; import { App } from './app/app'; import { ConnectionManager } from './app/connection-manager/connection-manager'; 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. const root = APP_MODE === 'editor' ? EditorWindow : APP_MODE === 'manager' ? ConnectionManager : App; diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 3f78267..31103d2 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -4,8 +4,14 @@ "compileOnSave": false, "compilerOptions": { "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//, and an alias keeps their imports identical wherever a + component is moved to. */ "paths": { - "@shared/*": ["../src/shared/*"] + "@shared/*": ["../src/shared/*"], + "@core/*": ["./src/app/core/*"] }, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true,