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 fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
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";
|
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). */
|
/** App settings persisted bun-side (WKWebView localStorage on views:// is unreliable). */
|
||||||
export class SettingsStore {
|
export class SettingsStore {
|
||||||
private readonly file = path.join(appDataDir(), "settings.json");
|
private readonly file = path.join(appDataDir(), "settings.json");
|
||||||
@@ -10,7 +13,18 @@ export class SettingsStore {
|
|||||||
async load(): Promise<AppSettings> {
|
async load(): Promise<AppSettings> {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(await fs.readFile(this.file, "utf8"));
|
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 {
|
} catch {
|
||||||
return { ...DEFAULT_SETTINGS };
|
return { ...DEFAULT_SETTINGS };
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -59,7 +59,12 @@ export function buildPreload(
|
|||||||
if (settings.theme !== "system") dataset["theme"] = settings.theme;
|
if (settings.theme !== "system") dataset["theme"] = settings.theme;
|
||||||
if (settings.darkTone !== "dark") dataset["darkTone"] = settings.darkTone;
|
if (settings.darkTone !== "dark") dataset["darkTone"] = settings.darkTone;
|
||||||
if (settings.accent !== "graphite") dataset["accent"] = settings.accent;
|
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)) {
|
for (const [key, value] of Object.entries(dataset)) {
|
||||||
script += `document.documentElement.dataset[${JSON.stringify(key)}]=${JSON.stringify(value)};`;
|
script += `document.documentElement.dataset[${JSON.stringify(key)}]=${JSON.stringify(value)};`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const de = {
|
|||||||
teal: "Petrol",
|
teal: "Petrol",
|
||||||
},
|
},
|
||||||
fileIcons: { label: "Datei-Symbole", plain: "Einfach", multi: "Farbig" },
|
fileIcons: { label: "Datei-Symbole", plain: "Einfach", multi: "Farbig" },
|
||||||
interface: { label: "Oberfläche", compact: "Kompakt", default: "Normal", large: "Groß" },
|
interface: { label: "Oberfläche", smaller: "Kleiner", larger: "Größer", reset: "Auf 100% zurücksetzen" },
|
||||||
speedUnit: { label: "Geschwindigkeitseinheit" },
|
speedUnit: { label: "Geschwindigkeitseinheit" },
|
||||||
transfers: { label: "Übertragungen", single: "Einzeln", multi: "Mehrere" },
|
transfers: { label: "Übertragungen", single: "Einzeln", multi: "Mehrere" },
|
||||||
atOnce: "Gleichzeitig",
|
atOnce: "Gleichzeitig",
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const en = {
|
|||||||
teal: "Teal",
|
teal: "Teal",
|
||||||
},
|
},
|
||||||
fileIcons: { label: "File icons", plain: "Plain", multi: "Multi" },
|
fileIcons: { label: "File icons", plain: "Plain", multi: "Multi" },
|
||||||
interface: { label: "Interface", compact: "Compact", default: "Default", large: "Large" },
|
interface: { label: "Interface", smaller: "Smaller", larger: "Larger", reset: "Reset to 100%" },
|
||||||
speedUnit: { label: "Speed unit" },
|
speedUnit: { label: "Speed unit" },
|
||||||
transfers: { label: "Transfers", single: "Single", multi: "Multi" },
|
transfers: { label: "Transfers", single: "Single", multi: "Multi" },
|
||||||
atOnce: "At once",
|
atOnce: "At once",
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const es = {
|
|||||||
teal: "Turquesa",
|
teal: "Turquesa",
|
||||||
},
|
},
|
||||||
fileIcons: { label: "Iconos", plain: "Simple", multi: "Color" },
|
fileIcons: { label: "Iconos", plain: "Simple", multi: "Color" },
|
||||||
interface: { label: "Interfaz", compact: "Compacta", default: "Normal", large: "Grande" },
|
interface: { label: "Interfaz", smaller: "Más pequeña", larger: "Más grande", reset: "Restablecer al 100%" },
|
||||||
speedUnit: { label: "Unidad de velocidad" },
|
speedUnit: { label: "Unidad de velocidad" },
|
||||||
transfers: { label: "Transferencias", single: "Una", multi: "Varias" },
|
transfers: { label: "Transferencias", single: "Una", multi: "Varias" },
|
||||||
atOnce: "A la vez",
|
atOnce: "A la vez",
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const eu = {
|
|||||||
teal: "Turkesa",
|
teal: "Turkesa",
|
||||||
},
|
},
|
||||||
fileIcons: { label: "Fitxategi-ikonoak", plain: "Soila", multi: "Koloretsua" },
|
fileIcons: { label: "Fitxategi-ikonoak", plain: "Soila", multi: "Koloretsua" },
|
||||||
interface: { label: "Interfazea", compact: "Trinkoa", default: "Lehenetsia", large: "Handia" },
|
interface: { label: "Interfazea", smaller: "Txikiagoa", larger: "Handiagoa", reset: "Berrezarri %100era" },
|
||||||
speedUnit: { label: "Abiadura-unitatea" },
|
speedUnit: { label: "Abiadura-unitatea" },
|
||||||
transfers: { label: "Transferentziak", single: "Banaka", multi: "Anitz" },
|
transfers: { label: "Transferentziak", single: "Banaka", multi: "Anitz" },
|
||||||
atOnce: "Aldi berean",
|
atOnce: "Aldi berean",
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const fr = {
|
|||||||
teal: "Sarcelle",
|
teal: "Sarcelle",
|
||||||
},
|
},
|
||||||
fileIcons: { label: "Icônes", plain: "Simple", multi: "Couleur" },
|
fileIcons: { label: "Icônes", plain: "Simple", multi: "Couleur" },
|
||||||
interface: { label: "Interface", compact: "Compacte", default: "Normale", large: "Grande" },
|
interface: { label: "Interface", smaller: "Plus petite", larger: "Plus grande", reset: "Réinitialiser à 100%" },
|
||||||
speedUnit: { label: "Unité de vitesse" },
|
speedUnit: { label: "Unité de vitesse" },
|
||||||
transfers: { label: "Transferts", single: "Un", multi: "Plusieurs" },
|
transfers: { label: "Transferts", single: "Un", multi: "Plusieurs" },
|
||||||
atOnce: "Simultanés",
|
atOnce: "Simultanés",
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const ja = {
|
|||||||
teal: "ティール",
|
teal: "ティール",
|
||||||
},
|
},
|
||||||
fileIcons: { label: "ファイルアイコン", plain: "シンプル", multi: "カラー" },
|
fileIcons: { label: "ファイルアイコン", plain: "シンプル", multi: "カラー" },
|
||||||
interface: { label: "インターフェース", compact: "コンパクト", default: "標準", large: "大" },
|
interface: { label: "インターフェース", smaller: "縮小", larger: "拡大", reset: "100%に戻す" },
|
||||||
speedUnit: { label: "速度の単位" },
|
speedUnit: { label: "速度の単位" },
|
||||||
transfers: { label: "転送", single: "1件ずつ", multi: "複数" },
|
transfers: { label: "転送", single: "1件ずつ", multi: "複数" },
|
||||||
atOnce: "同時数",
|
atOnce: "同時数",
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const zh = {
|
|||||||
teal: "青色",
|
teal: "青色",
|
||||||
},
|
},
|
||||||
fileIcons: { label: "文件图标", plain: "简约", multi: "彩色" },
|
fileIcons: { label: "文件图标", plain: "简约", multi: "彩色" },
|
||||||
interface: { label: "界面", compact: "紧凑", default: "默认", large: "大" },
|
interface: { label: "界面", smaller: "缩小", larger: "放大", reset: "重置为100%" },
|
||||||
speedUnit: { label: "速度单位" },
|
speedUnit: { label: "速度单位" },
|
||||||
transfers: { label: "传输", single: "单个", multi: "多个" },
|
transfers: { label: "传输", single: "单个", multi: "多个" },
|
||||||
atOnce: "同时",
|
atOnce: "同时",
|
||||||
|
|||||||
+25
-3
@@ -100,8 +100,13 @@ export interface AppSettings {
|
|||||||
/** Visible optional file-list columns (both panels; Name always shows). */
|
/** Visible optional file-list columns (both panels; Name always shows). */
|
||||||
columns: ColumnKey[];
|
columns: ColumnKey[];
|
||||||
speedUnit: "MBps" | "Mbps";
|
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. */
|
/** Run transfer jobs one at a time, or several in parallel. */
|
||||||
transferMode: "single" | "multi";
|
transferMode: "single" | "multi";
|
||||||
/** How many jobs run at once when transferMode is "multi". */
|
/** How many jobs run at once when transferMode is "multi". */
|
||||||
@@ -127,7 +132,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
iconColors: "multi",
|
iconColors: "multi",
|
||||||
columns: ["size", "mtime"],
|
columns: ["size", "mtime"],
|
||||||
speedUnit: "MBps",
|
speedUnit: "MBps",
|
||||||
textSize: "default",
|
uiScale: 1,
|
||||||
transferMode: "single",
|
transferMode: "single",
|
||||||
transferConcurrency: 3,
|
transferConcurrency: 3,
|
||||||
autoClearCompleted: true,
|
autoClearCompleted: true,
|
||||||
@@ -136,6 +141,23 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
// autoRefresh: true,
|
// 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 {
|
export function effectiveConcurrency(settings: AppSettings): number {
|
||||||
if (settings.transferMode === "single") return 1;
|
if (settings.transferMode === "single") return 1;
|
||||||
return Math.max(1, Math.min(8, Math.round(settings.transferConcurrency) || 1));
|
return Math.max(1, Math.min(8, Math.round(settings.transferConcurrency) || 1));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable, computed, inject, signal } from '@angular/core';
|
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||||
import { resolveLocale } from '@shared/i18n';
|
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 { I18nService } from './i18n.service';
|
||||||
import { RpcService } from './rpc.service';
|
import { RpcService } from './rpc.service';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export class SettingsService {
|
|||||||
readonly iconColors = signal<AppSettings['iconColors']>(DEFAULT_SETTINGS.iconColors);
|
readonly iconColors = signal<AppSettings['iconColors']>(DEFAULT_SETTINGS.iconColors);
|
||||||
readonly columns = signal<ColumnKey[]>(DEFAULT_SETTINGS.columns);
|
readonly columns = signal<ColumnKey[]>(DEFAULT_SETTINGS.columns);
|
||||||
readonly speedUnit = signal<AppSettings['speedUnit']>(DEFAULT_SETTINGS.speedUnit);
|
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 transferMode = signal<AppSettings['transferMode']>(DEFAULT_SETTINGS.transferMode);
|
||||||
readonly transferConcurrency = signal(DEFAULT_SETTINGS.transferConcurrency);
|
readonly transferConcurrency = signal(DEFAULT_SETTINGS.transferConcurrency);
|
||||||
readonly autoClearCompleted = signal(DEFAULT_SETTINGS.autoClearCompleted);
|
readonly autoClearCompleted = signal(DEFAULT_SETTINGS.autoClearCompleted);
|
||||||
@@ -58,7 +58,7 @@ export class SettingsService {
|
|||||||
this.iconColors.set(settings.iconColors);
|
this.iconColors.set(settings.iconColors);
|
||||||
this.columns.set(settings.columns);
|
this.columns.set(settings.columns);
|
||||||
this.speedUnit.set(settings.speedUnit);
|
this.speedUnit.set(settings.speedUnit);
|
||||||
this.textSize.set(settings.textSize);
|
this.uiScale.set(settings.uiScale);
|
||||||
this.transferMode.set(settings.transferMode);
|
this.transferMode.set(settings.transferMode);
|
||||||
this.transferConcurrency.set(settings.transferConcurrency);
|
this.transferConcurrency.set(settings.transferConcurrency);
|
||||||
this.autoClearCompleted.set(settings.autoClearCompleted);
|
this.autoClearCompleted.set(settings.autoClearCompleted);
|
||||||
@@ -70,7 +70,7 @@ export class SettingsService {
|
|||||||
this.applyTheme();
|
this.applyTheme();
|
||||||
this.applyDarkTone();
|
this.applyDarkTone();
|
||||||
this.applyAccent();
|
this.applyAccent();
|
||||||
this.applyTextSize();
|
this.applyUiScale();
|
||||||
this.applyLocale();
|
this.applyLocale();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,9 +140,9 @@ export class SettingsService {
|
|||||||
void this.persist();
|
void this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
setTextSize(size: AppSettings['textSize']): void {
|
setUiScale(scale: number): void {
|
||||||
this.textSize.set(size);
|
this.uiScale.set(clampUiScale(scale));
|
||||||
this.applyTextSize();
|
this.applyUiScale();
|
||||||
void this.persist();
|
void this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,12 +204,17 @@ export class SettingsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyTextSize(): void {
|
/** The whole interface scale is ONE declaration: every size in the app is
|
||||||
const size = this.textSize();
|
* rem, so moving the root font-size moves both skins in lockstep. 1 = the
|
||||||
if (size === 'default') {
|
* browser-default 16px root; delete-when-default keeps the pristine state
|
||||||
delete document.documentElement.dataset['text'];
|
* 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 {
|
} else {
|
||||||
document.documentElement.dataset['text'] = size;
|
document.documentElement.style.fontSize = `${16 * scale}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +232,7 @@ export class SettingsService {
|
|||||||
iconColors: this.iconColors(),
|
iconColors: this.iconColors(),
|
||||||
columns: this.columns(),
|
columns: this.columns(),
|
||||||
speedUnit: this.speedUnit(),
|
speedUnit: this.speedUnit(),
|
||||||
textSize: this.textSize(),
|
uiScale: this.uiScale(),
|
||||||
transferMode: this.transferMode(),
|
transferMode: this.transferMode(),
|
||||||
transferConcurrency: this.transferConcurrency(),
|
transferConcurrency: this.transferConcurrency(),
|
||||||
autoClearCompleted: this.autoClearCompleted(),
|
autoClearCompleted: this.autoClearCompleted(),
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const SPEED_UNITS = [
|
|||||||
{ value: 'MBps', label: 'MB/s' },
|
{ value: 'MBps', label: 'MB/s' },
|
||||||
{ value: 'Mbps', label: 'Mbps' },
|
{ value: 'Mbps', label: 'Mbps' },
|
||||||
] as const;
|
] as const;
|
||||||
const TEXT_SIZES = ['compact', 'default', 'large'] as const;
|
|
||||||
const ICON_COLOR_MODES = ['plain', 'multi'] as const;
|
const ICON_COLOR_MODES = ['plain', 'multi'] as const;
|
||||||
const TRANSFER_MODES = ['single', 'multi'] as const;
|
const TRANSFER_MODES = ['single', 'multi'] as const;
|
||||||
const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||||
@@ -276,20 +275,35 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Interface scale: a continuous stepper, 50%..400% in 10-point
|
||||||
|
steps. One lever moves the whole UI (root font-size; everything
|
||||||
|
is rem), which is also why this popover itself grows with it. -->
|
||||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||||
<span class="text-xs text-muted-foreground">{{ i18n.t('options.interface.label') }}</span>
|
<span class="text-xs text-muted-foreground">{{ i18n.t('options.interface.label') }}</span>
|
||||||
<div class="flex rounded-md border border-border p-0.5">
|
<div class="flex items-center rounded-md border border-border p-0.5">
|
||||||
@for (size of textSizes; track size) {
|
|
||||||
<button
|
<button
|
||||||
(click)="settings.setTextSize(size)"
|
(click)="settings.setUiScale(settings.uiScale() - 0.1)"
|
||||||
class="rounded px-2 py-0.5 text-[0.6875rem]"
|
[disabled]="settings.uiScale() <= 0.5"
|
||||||
[class.bg-accent]="settings.textSize() === size"
|
[title]="i18n.t('options.interface.smaller')"
|
||||||
[class.text-foreground]="settings.textSize() === size"
|
class="grid size-5 place-items-center rounded text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
||||||
[class.text-muted-foreground]="settings.textSize() !== size"
|
|
||||||
>
|
>
|
||||||
{{ i18n.t('options.interface.' + size) }}
|
−
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
(click)="settings.setUiScale(1)"
|
||||||
|
[title]="i18n.t('options.interface.reset')"
|
||||||
|
class="w-11 rounded px-1 py-0.5 text-center text-[0.6875rem] tabular-nums hover:bg-accent"
|
||||||
|
>
|
||||||
|
{{ scalePercent() }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
(click)="settings.setUiScale(settings.uiScale() + 0.1)"
|
||||||
|
[disabled]="settings.uiScale() >= 4"
|
||||||
|
[title]="i18n.t('options.interface.larger')"
|
||||||
|
class="grid size-5 place-items-center rounded text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
||||||
|
>
|
||||||
|
+
|
||||||
</button>
|
</button>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -474,7 +488,8 @@ export class TabBar {
|
|||||||
readonly accents = ACCENTS;
|
readonly accents = ACCENTS;
|
||||||
readonly iconColorModes = ICON_COLOR_MODES;
|
readonly iconColorModes = ICON_COLOR_MODES;
|
||||||
readonly speedUnits = SPEED_UNITS;
|
readonly speedUnits = SPEED_UNITS;
|
||||||
readonly textSizes = TEXT_SIZES;
|
/** "100%" for the stepper readout; the value itself lives in settings. */
|
||||||
|
readonly scalePercent = computed(() => `${Math.round(this.settings.uiScale() * 100)}%`);
|
||||||
readonly transferModes = TRANSFER_MODES;
|
readonly transferModes = TRANSFER_MODES;
|
||||||
readonly concurrencyChoices = CONCURRENCY_CHOICES;
|
readonly concurrencyChoices = CONCURRENCY_CHOICES;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Intentionally near-empty, and that is not an oversight.
|
Intentionally near-empty, and that is not an oversight.
|
||||||
|
|
||||||
This skin IS the shared foundation: the OKLCH token system, `@theme inline`,
|
This skin IS the shared foundation: the OKLCH token system, `@theme inline`,
|
||||||
the data-text interface sizes and the accent axis all live in ui/src/styles.css
|
the panel-chrome metrics and the accent axis all live in ui/src/styles.css
|
||||||
because both skins consume them, and everything else the modern look needs is
|
because both skins consume them, and everything else the modern look needs is
|
||||||
a Tailwind utility written inline in its templates. There is no third-party
|
a Tailwind utility written inline in its templates. There is no third-party
|
||||||
framework to bridge and no cascade inversion to undo, so there is simply
|
framework to bridge and no cascade inversion to undo, so there is simply
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const SPEED_UNITS = [
|
|||||||
{ value: 'MBps', label: 'MB/s' },
|
{ value: 'MBps', label: 'MB/s' },
|
||||||
{ value: 'Mbps', label: 'Mbps' },
|
{ value: 'Mbps', label: 'Mbps' },
|
||||||
] as const;
|
] as const;
|
||||||
const TEXT_SIZES = ['compact', 'default', 'large'] as const;
|
|
||||||
const ICON_COLOR_MODES = ['plain', 'multi'] as const;
|
const ICON_COLOR_MODES = ['plain', 'multi'] as const;
|
||||||
const TRANSFER_MODES = ['single', 'multi'] as const;
|
const TRANSFER_MODES = ['single', 'multi'] as const;
|
||||||
const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||||
@@ -282,20 +281,35 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Interface scale: a continuous stepper, 50%..400% in 10-point
|
||||||
|
steps. One lever moves the whole UI (root font-size; everything
|
||||||
|
is rem), which is also why this popover itself grows with it. -->
|
||||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||||
<span class="text-xs text-muted-foreground">{{ i18n.t('options.interface.label') }}</span>
|
<span class="text-xs text-muted-foreground">{{ i18n.t('options.interface.label') }}</span>
|
||||||
<div class="flex rounded-md border border-border p-0.5">
|
<div class="flex items-center rounded-md border border-border p-0.5">
|
||||||
@for (size of textSizes; track size) {
|
|
||||||
<button
|
<button
|
||||||
(click)="settings.setTextSize(size)"
|
(click)="settings.setUiScale(settings.uiScale() - 0.1)"
|
||||||
class="rounded px-2 py-0.5 text-[0.6875rem]"
|
[disabled]="settings.uiScale() <= 0.5"
|
||||||
[class.bg-accent]="settings.textSize() === size"
|
[title]="i18n.t('options.interface.smaller')"
|
||||||
[class.text-foreground]="settings.textSize() === size"
|
class="grid size-5 place-items-center rounded text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
||||||
[class.text-muted-foreground]="settings.textSize() !== size"
|
|
||||||
>
|
>
|
||||||
{{ i18n.t('options.interface.' + size) }}
|
−
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
(click)="settings.setUiScale(1)"
|
||||||
|
[title]="i18n.t('options.interface.reset')"
|
||||||
|
class="w-11 rounded px-1 py-0.5 text-center text-[0.6875rem] tabular-nums hover:bg-accent"
|
||||||
|
>
|
||||||
|
{{ scalePercent() }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
(click)="settings.setUiScale(settings.uiScale() + 0.1)"
|
||||||
|
[disabled]="settings.uiScale() >= 4"
|
||||||
|
[title]="i18n.t('options.interface.larger')"
|
||||||
|
class="grid size-5 place-items-center rounded text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
||||||
|
>
|
||||||
|
+
|
||||||
</button>
|
</button>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -479,7 +493,8 @@ export class TabBar {
|
|||||||
readonly accents = ACCENTS;
|
readonly accents = ACCENTS;
|
||||||
readonly iconColorModes = ICON_COLOR_MODES;
|
readonly iconColorModes = ICON_COLOR_MODES;
|
||||||
readonly speedUnits = SPEED_UNITS;
|
readonly speedUnits = SPEED_UNITS;
|
||||||
readonly textSizes = TEXT_SIZES;
|
/** "100%" for the stepper readout; the value itself lives in settings. */
|
||||||
|
readonly scalePercent = computed(() => `${Math.round(this.settings.uiScale() * 100)}%`);
|
||||||
readonly transferModes = TRANSFER_MODES;
|
readonly transferModes = TRANSFER_MODES;
|
||||||
readonly concurrencyChoices = CONCURRENCY_CHOICES;
|
readonly concurrencyChoices = CONCURRENCY_CHOICES;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user