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:
2026-08-16 18:09:59 +02:00
co-authored by Claude
parent 9f2a076e08
commit f101257f20
14 changed files with 131 additions and 55 deletions
+25 -3
View File
@@ -100,8 +100,13 @@ export interface AppSettings {
/** Visible optional file-list columns (both panels; Name always shows). */
columns: ColumnKey[];
speedUnit: "MBps" | "Mbps";
/** Size of the panel interface: file list, path bar, header controls. */
textSize: "compact" | "default" | "large";
/**
* Interface scale factor, 0.5–4 in 0.1 steps (shown as 50%–400%). Works by
* setting the root font-size — every size in the UI is rem, so one lever
* scales BOTH skins whole. Replaced the 3-tier `textSize`
* (compact|default|large); SettingsStore.load migrates old files.
*/
uiScale: number;
/** Run transfer jobs one at a time, or several in parallel. */
transferMode: "single" | "multi";
/** How many jobs run at once when transferMode is "multi". */
@@ -127,7 +132,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
iconColors: "multi",
columns: ["size", "mtime"],
speedUnit: "MBps",
textSize: "default",
uiScale: 1,
transferMode: "single",
transferConcurrency: 3,
autoClearCompleted: true,
@@ -136,6 +141,23 @@ export const DEFAULT_SETTINGS: AppSettings = {
// autoRefresh: true,
};
export const UI_SCALE_MIN = 0.5;
export const UI_SCALE_MAX = 4;
export const UI_SCALE_STEP = 0.1;
/**
* Clamp + snap a scale factor to the legal range and step grid. Shared by the
* store (a hand-edited settings.json must not be able to render the UI
* unusable), the settings service and the stepper — one definition of "valid",
* same as effectiveConcurrency below.
*/
export function clampUiScale(value: number): number {
if (!Number.isFinite(value)) return 1;
const clamped = Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, value));
// snap to the step grid; toFixed strips float noise (0.7000000000000001 -> 0.7)
return Number((Math.round(clamped / UI_SCALE_STEP) * UI_SCALE_STEP).toFixed(2));
}
export function effectiveConcurrency(settings: AppSettings): number {
if (settings.transferMode === "single") return 1;
return Math.max(1, Math.min(8, Math.round(settings.transferConcurrency) || 1));