Stage 5: wire the skin switch — both UIs now reachable
The reunion is complete: one build contains both UIs and picks between them at bootstrap. - src/shared/types.ts regains `skin: "modern" | "win7"` on AppSettings. - SettingsService regains the skin signal, setSkin() and applySkin(), which toggles the `.win7` class on <html>. - buildPreload emits __APP_SKIN and adds `.win7` conditionally again, instead of unconditionally as it had sincefe028a9. - chromeOptions() regains its parameter: win7 gets frameless + transparent, modern gets the macOS hiddenInset fusion or the plain native frame. - core/platform.ts regains APP_SKIN / IS_WIN7_CHROME (added in stage 4). - main.ts now selects on both axes — skin picks the tree, APP_MODE picks the root within it. - Both tab-bars get a skin row, with the reopen caveat as its tooltip. DEFAULT_SETTINGS.skin is "modern", matching the original design and `main`. Worth flagging: this branch has booted win7 unconditionally sincefe028a9, so after this commit it starts in the modern UI unless the setting says otherwise. One word in types.ts flips that if the win7 look should be the default. i18n: en.ts is the source of truth and everything else falls back to it, so only en was strictly required — but es/eu/fr/de/zh/ja got real translations too, each matching the terminology that locale already uses for the Theme row. Verified both skins on live single-instance captures. Modern boots and renders fully styled, which also proves Tailwind scans skins/shadlike/** — a purge would have produced an unstyled page rather than this one. Win7, selected by setting skin in the persisted settings, is still pixel-identical to the pre-refactor baseline across all five stages. The persisted settings file was backed up and restored, so it still carries no skin key and resolves to the default through the backend's merge. Not verified: clicking the new skin row itself, which needs an interaction pass — no window automation is available here. Its bindings compile and the built bundle contains options.skin, setSkin and the translated labels. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { RpcService } from './rpc.service';
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SettingsService {
|
||||
readonly locale = signal<AppSettings['locale']>(DEFAULT_SETTINGS.locale);
|
||||
readonly skin = signal<AppSettings['skin']>(DEFAULT_SETTINGS.skin);
|
||||
readonly theme = signal<AppSettings['theme']>(DEFAULT_SETTINGS.theme);
|
||||
readonly darkTone = signal<AppSettings['darkTone']>(DEFAULT_SETTINGS.darkTone);
|
||||
readonly accent = signal<AppSettings['accent']>(DEFAULT_SETTINGS.accent);
|
||||
@@ -50,6 +51,7 @@ export class SettingsService {
|
||||
|
||||
private applyAll(settings: AppSettings): void {
|
||||
this.locale.set(settings.locale);
|
||||
this.skin.set(settings.skin);
|
||||
this.theme.set(settings.theme);
|
||||
this.darkTone.set(settings.darkTone);
|
||||
this.accent.set(settings.accent);
|
||||
@@ -62,6 +64,7 @@ export class SettingsService {
|
||||
this.autoClearCompleted.set(settings.autoClearCompleted);
|
||||
this.editor.set(settings.editor);
|
||||
this.editorApp.set(settings.editorApp);
|
||||
this.applySkin();
|
||||
this.applyTheme();
|
||||
this.applyDarkTone();
|
||||
this.applyAccent();
|
||||
@@ -75,6 +78,19 @@ export class SettingsService {
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the CSS half of a skin change lands here. The component TREE and the
|
||||
* native frame are both fixed at window construction (main.ts reads the
|
||||
* preload-baked __APP_SKIN; titleBarStyle/transparent are BrowserWindow
|
||||
* constructor options), so the switch is not complete until the window is
|
||||
* reopened. The options row says so.
|
||||
*/
|
||||
setSkin(skin: AppSettings['skin']): void {
|
||||
this.skin.set(skin);
|
||||
this.applySkin();
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
setTheme(theme: AppSettings['theme']): void {
|
||||
this.theme.set(theme);
|
||||
this.applyTheme();
|
||||
@@ -142,6 +158,13 @@ export class SettingsService {
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
/** A CLASS, not a data-* attribute: 7.scoped.css is precompiled to require a
|
||||
* literal `.win7` ancestor, so the name is not ours to choose. Mirrors what
|
||||
* buildPreload writes for the first paint. */
|
||||
private applySkin(): void {
|
||||
document.documentElement.classList.toggle('win7', this.skin() === 'win7');
|
||||
}
|
||||
|
||||
private applyTheme(): void {
|
||||
const theme = this.theme();
|
||||
if (theme === 'system') {
|
||||
@@ -185,6 +208,7 @@ export class SettingsService {
|
||||
protected settings(): AppSettings {
|
||||
return {
|
||||
locale: this.locale(),
|
||||
skin: this.skin(),
|
||||
theme: this.theme(),
|
||||
darkTone: this.darkTone(),
|
||||
accent: this.accent(),
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { UpdaterService } from '@core/updater.service';
|
||||
|
||||
const SKINS = ['modern', 'win7'] as const;
|
||||
const THEMES = ['system', 'light', 'dark'] as const;
|
||||
const DARK_TONES = ['dark', 'darker'] as const;
|
||||
// Swatch colors are fixed mid-tones so every dot reads on both light and dark
|
||||
@@ -174,6 +175,28 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Skin: swaps the WHOLE component tree (ui/src/app/skins/) plus the
|
||||
native frame. Only the CSS half applies live — titleBarStyle and
|
||||
transparent are BrowserWindow constructor options and main.ts
|
||||
reads the preload-baked __APP_SKIN — so the hint is not optional. -->
|
||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-muted-foreground">{{ i18n.t('options.skin.label') }}</span>
|
||||
<div class="flex rounded-md border border-border p-0.5">
|
||||
@for (s of skins; track s) {
|
||||
<button
|
||||
(click)="settings.setSkin(s)"
|
||||
[title]="i18n.t('options.skin.restartHint')"
|
||||
class="rounded px-2 py-0.5 text-[11px]"
|
||||
[class.bg-accent]="settings.skin() === s"
|
||||
[class.text-foreground]="settings.skin() === s"
|
||||
[class.text-muted-foreground]="settings.skin() !== s"
|
||||
>
|
||||
{{ i18n.t('options.skin.' + s) }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-muted-foreground">{{ i18n.t('options.theme.label') }}</span>
|
||||
<div class="flex rounded-md border border-border p-0.5">
|
||||
@@ -431,6 +454,7 @@ export class TabBar {
|
||||
readonly buildLabel = BRANDING.buildLabel;
|
||||
/** What the window was BUILT with, not the current setting — the frameless
|
||||
* chrome only changes on reopen, so the hint compares against this. */
|
||||
readonly skins = SKINS;
|
||||
readonly themes = THEMES;
|
||||
readonly darkTones = DARK_TONES;
|
||||
readonly accents = ACCENTS;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { UpdaterService } from '@core/updater.service';
|
||||
|
||||
const SKINS = ['modern', 'win7'] as const;
|
||||
const THEMES = ['system', 'light', 'dark'] as const;
|
||||
const DARK_TONES = ['dark', 'darker'] as const;
|
||||
// Swatch colors are fixed mid-tones so every dot reads on both light and dark
|
||||
@@ -180,6 +181,28 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Skin: swaps the WHOLE component tree (ui/src/app/skins/) plus the
|
||||
native frame. Only the CSS half applies live — titleBarStyle and
|
||||
transparent are BrowserWindow constructor options and main.ts
|
||||
reads the preload-baked __APP_SKIN — so the hint is not optional. -->
|
||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-muted-foreground">{{ i18n.t('options.skin.label') }}</span>
|
||||
<div class="flex rounded-md border border-border p-0.5">
|
||||
@for (s of skins; track s) {
|
||||
<button
|
||||
(click)="settings.setSkin(s)"
|
||||
[title]="i18n.t('options.skin.restartHint')"
|
||||
class="rounded px-2 py-0.5 text-[11px]"
|
||||
[class.bg-accent]="settings.skin() === s"
|
||||
[class.text-foreground]="settings.skin() === s"
|
||||
[class.text-muted-foreground]="settings.skin() !== s"
|
||||
>
|
||||
{{ i18n.t('options.skin.' + s) }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-muted-foreground">{{ i18n.t('options.theme.label') }}</span>
|
||||
<div class="flex rounded-md border border-border p-0.5">
|
||||
@@ -436,6 +459,7 @@ export class TabBar {
|
||||
readonly modLabel = MOD_LABEL;
|
||||
readonly appName = BRANDING.appName;
|
||||
readonly buildLabel = BRANDING.buildLabel;
|
||||
readonly skins = SKINS;
|
||||
readonly themes = THEMES;
|
||||
readonly darkTones = DARK_TONES;
|
||||
readonly accents = ACCENTS;
|
||||
|
||||
+13
-4
@@ -1,10 +1,19 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from '@core/app.config';
|
||||
import { APP_MODE } from '@core/platform';
|
||||
import { APP_MODE, APP_SKIN } from '@core/platform';
|
||||
import { SEVEN_ROOTS } from './app/skins/7like';
|
||||
import { SHAD_ROOTS } from './app/skins/shadlike';
|
||||
|
||||
// One bundle, three window kinds: the preload global picks the root component.
|
||||
// A second axis (skin) joins APP_MODE here once the shadlike tree lands.
|
||||
const tree = SEVEN_ROOTS;
|
||||
// One bundle, two skins x three window kinds. Both globals are baked into the
|
||||
// preload (src/bun/windows.ts buildPreload) because both are needed before the
|
||||
// first paint: settings arrive over an async RPC, far too late to choose a
|
||||
// component tree without flashing the wrong UI.
|
||||
//
|
||||
// Every skin's barrel exports the same { main, manager, editor } shape, so this
|
||||
// file never learns anything about either tree's internals. Both are imported
|
||||
// statically and ship in one bundle — the inactive skin costs bundle size but
|
||||
// no runtime work, which is the right trade for a local desktop app and keeps
|
||||
// bootstrap synchronous.
|
||||
const tree = APP_SKIN === 'win7' ? SEVEN_ROOTS : SHAD_ROOTS;
|
||||
const root = APP_MODE === 'editor' ? tree.editor : APP_MODE === 'manager' ? tree.manager : tree.main;
|
||||
bootstrapApplication(root, appConfig).catch((err) => console.error(err));
|
||||
|
||||
Reference in New Issue
Block a user