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,8 +1,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { DEFAULT_SETTINGS, type AppSettings } from "../../shared/types";
|
||||
import { clampUiScale, DEFAULT_SETTINGS, type AppSettings } from "../../shared/types";
|
||||
import { appDataDir } from "./paths";
|
||||
|
||||
/** The retired 3-tier axis, mapped onto the scale that replaced it. */
|
||||
const LEGACY_TEXT_SIZE: Record<string, number> = { compact: 0.9, default: 1, large: 1.2 };
|
||||
|
||||
/** App settings persisted bun-side (WKWebView localStorage on views:// is unreliable). */
|
||||
export class SettingsStore {
|
||||
private readonly file = path.join(appDataDir(), "settings.json");
|
||||
@@ -10,7 +13,18 @@ export class SettingsStore {
|
||||
async load(): Promise<AppSettings> {
|
||||
try {
|
||||
const data = JSON.parse(await fs.readFile(this.file, "utf8"));
|
||||
return { ...DEFAULT_SETTINGS, ...data };
|
||||
const settings: AppSettings = { ...DEFAULT_SETTINGS, ...data };
|
||||
// migrate the retired textSize tier into the scale that replaced it —
|
||||
// but never over an explicit uiScale (a file carrying both has already
|
||||
// been migrated once and then adjusted)
|
||||
if (data.uiScale === undefined && typeof data.textSize === "string") {
|
||||
settings.uiScale = LEGACY_TEXT_SIZE[data.textSize] ?? 1;
|
||||
}
|
||||
// clamp ALWAYS, not just on migration: buildPreload writes this value
|
||||
// into every window's root font-size, so a hand-edited settings.json
|
||||
// (uiScale: 100, "2", NaN) must not be able to render the UI unusable
|
||||
settings.uiScale = clampUiScale(Number(settings.uiScale));
|
||||
return settings;
|
||||
} catch {
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
|
||||
+6
-1
@@ -59,7 +59,12 @@ export function buildPreload(
|
||||
if (settings.theme !== "system") dataset["theme"] = settings.theme;
|
||||
if (settings.darkTone !== "dark") dataset["darkTone"] = settings.darkTone;
|
||||
if (settings.accent !== "graphite") dataset["accent"] = settings.accent;
|
||||
if (settings.textSize !== "default") dataset["text"] = settings.textSize;
|
||||
// interface scale: mirrors SettingsService.applyUiScale so the FIRST paint
|
||||
// is already scaled — without this every window opens at 100% and snaps.
|
||||
// Written as an inline style, not a data attribute: the scale is continuous.
|
||||
if (settings.uiScale !== 1) {
|
||||
script += `document.documentElement.style.fontSize=${JSON.stringify(`${16 * settings.uiScale}px`)};`;
|
||||
}
|
||||
for (const [key, value] of Object.entries(dataset)) {
|
||||
script += `document.documentElement.dataset[${JSON.stringify(key)}]=${JSON.stringify(value)};`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user