Scale stage 2: continuous uiScale setting + the (-) 100% (+) stepper
AppSettings.textSize (compact|default|large) becomes uiScale: number, 0.5-4 in 0.1 steps, default 1. One shared clampUiScale (next to the effectiveConcurrency precedent) defines "valid" for the store, the service and the stepper: clamp to range, snap to the step grid, strip float noise, and map NaN/Infinity to 1. - SettingsStore.load migrates legacy files (compact/default/large -> 0.9/1/1.2, never over an explicit uiScale) and clamps ALWAYS, not just on migration — buildPreload writes this value into every window's root font-size, so a hand-edited settings.json (uiScale: 100) must not be able to render the UI unusable. Verified against 100, 0.001, -3, NaN, Infinity and float noise; migration verified against a real legacy file. - SettingsService: applyUiScale sets the root font-size (16 * scale, removed entirely at 1 per the delete-when-default rule). Rides the existing settingsChanged broadcast, so open editor/manager windows follow live. - buildPreload bakes the same font-size so the first paint is already scaled — without it every window opens at 100% and snaps. - Both tab-bars: the 3-button Interface row becomes the stepper. Clicking the percentage resets to 100% (tooltip says so). i18n: compact/default/large dropped, smaller/larger/reset added, all 7 locales. Verified live, win7 + modern: scale 1 is pixel-identical to the pre-work baseline (opaque interior diff exactly 0); 0.5x is a real dense mode (~2x rows); 2x scales title bar, tabs, rows, icons and controls together with text contained in its boxes — which is what stage 1c's 7.css bridge bought; 4x is enormous but coherent, and the win7 title-bar controls stay on-screen at the default window size. The known caveat stands: no minimum window size exists, so 4x in a small window is legal but cramped. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||
import { resolveLocale } from '@shared/i18n';
|
||||
import { DEFAULT_SETTINGS, type AppSettings, type ColumnKey } from '@shared/types';
|
||||
import { clampUiScale, DEFAULT_SETTINGS, type AppSettings, type ColumnKey } from '@shared/types';
|
||||
import { I18nService } from './i18n.service';
|
||||
import { RpcService } from './rpc.service';
|
||||
|
||||
@@ -14,7 +14,7 @@ export class SettingsService {
|
||||
readonly iconColors = signal<AppSettings['iconColors']>(DEFAULT_SETTINGS.iconColors);
|
||||
readonly columns = signal<ColumnKey[]>(DEFAULT_SETTINGS.columns);
|
||||
readonly speedUnit = signal<AppSettings['speedUnit']>(DEFAULT_SETTINGS.speedUnit);
|
||||
readonly textSize = signal<AppSettings['textSize']>(DEFAULT_SETTINGS.textSize);
|
||||
readonly uiScale = signal<number>(DEFAULT_SETTINGS.uiScale);
|
||||
readonly transferMode = signal<AppSettings['transferMode']>(DEFAULT_SETTINGS.transferMode);
|
||||
readonly transferConcurrency = signal(DEFAULT_SETTINGS.transferConcurrency);
|
||||
readonly autoClearCompleted = signal(DEFAULT_SETTINGS.autoClearCompleted);
|
||||
@@ -58,7 +58,7 @@ export class SettingsService {
|
||||
this.iconColors.set(settings.iconColors);
|
||||
this.columns.set(settings.columns);
|
||||
this.speedUnit.set(settings.speedUnit);
|
||||
this.textSize.set(settings.textSize);
|
||||
this.uiScale.set(settings.uiScale);
|
||||
this.transferMode.set(settings.transferMode);
|
||||
this.transferConcurrency.set(settings.transferConcurrency);
|
||||
this.autoClearCompleted.set(settings.autoClearCompleted);
|
||||
@@ -70,7 +70,7 @@ export class SettingsService {
|
||||
this.applyTheme();
|
||||
this.applyDarkTone();
|
||||
this.applyAccent();
|
||||
this.applyTextSize();
|
||||
this.applyUiScale();
|
||||
this.applyLocale();
|
||||
}
|
||||
|
||||
@@ -140,9 +140,9 @@ export class SettingsService {
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
setTextSize(size: AppSettings['textSize']): void {
|
||||
this.textSize.set(size);
|
||||
this.applyTextSize();
|
||||
setUiScale(scale: number): void {
|
||||
this.uiScale.set(clampUiScale(scale));
|
||||
this.applyUiScale();
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
@@ -204,12 +204,17 @@ export class SettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
private applyTextSize(): void {
|
||||
const size = this.textSize();
|
||||
if (size === 'default') {
|
||||
delete document.documentElement.dataset['text'];
|
||||
/** The whole interface scale is ONE declaration: every size in the app is
|
||||
* rem, so moving the root font-size moves both skins in lockstep. 1 = the
|
||||
* browser-default 16px root; delete-when-default keeps the pristine state
|
||||
* attribute-free like the other axes. buildPreload mirrors this for a
|
||||
* flash-free first paint. */
|
||||
private applyUiScale(): void {
|
||||
const scale = this.uiScale();
|
||||
if (scale === 1) {
|
||||
document.documentElement.style.removeProperty('font-size');
|
||||
} else {
|
||||
document.documentElement.dataset['text'] = size;
|
||||
document.documentElement.style.fontSize = `${16 * scale}px`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +232,7 @@ export class SettingsService {
|
||||
iconColors: this.iconColors(),
|
||||
columns: this.columns(),
|
||||
speedUnit: this.speedUnit(),
|
||||
textSize: this.textSize(),
|
||||
uiScale: this.uiScale(),
|
||||
transferMode: this.transferMode(),
|
||||
transferConcurrency: this.transferConcurrency(),
|
||||
autoClearCompleted: this.autoClearCompleted(),
|
||||
|
||||
Reference in New Issue
Block a user