Scale stage 1: everything the UI sizes with is now rem

Groundwork for the continuous interface scale (a root font-size lever): every
size the UI derives from must be root-relative, or it freezes while the rest
grows. Gate for this commit: at the default 16px root the app renders
pixel-identical to before — every conversion is exact (11px = 0.6875rem …).

- The 8 panel-chrome tokens in styles.css: px -> rem, and the data-text
  compact/large tiers are DELETED — a continuous scale replaces discrete
  tiers. Same for --w7-fs (12px -> 0.75rem) and the win7 --radius (5px ->
  0.3125rem, still landing rounded-md exactly on --w7-el-bdr's 3px).
  --radius-sm/md subtrahends: px -> rem, or corners would sharpen as the UI
  grew.
- All 93 text-[11px]/text-[10px] arbitraries -> rem equivalents, plus the two
  inline padding:24px empty-state cells in the 7like file panel.
- 7like.css gains a scale bridge in skinfix: 7.css is 370 px values with zero
  rem, and --w7-font reaches only 15 rules' TYPOGRAPHY — under a root lever
  win7 text would grow inside boxes frozen at 23px/14px/15px. The bridge
  re-expresses 7.css's size vars and its hardcoded box metrics (buttons via
  .w7-btn only, inputs, table rows/headers, title-bar controls, menu items,
  tabs, the 10pt check glyph) in rem at identical scale-1 pixels. 1px
  hairlines and focus rects stay px deliberately — crisp at any scale, and
  the tr border exists precisely to stop selection shifting rows.
- Two skinfix traps found while writing it, one by reasoning and one by
  pixels: (a) NO blanket `.win7 button` restatement — skinfix outranks the
  skin-layer neutralizer that strips 7.css's 75x23 metrics off the ~84 icon
  buttons, so a blanket rule would re-inflate them all; (b) restating a base
  rule in skinfix also outranks 7.css's own VARIANTS of it by layer — the
  title-bar rule silently shrank the close button from 48px to 29px until the
  harness pixel diff caught it, and the active tab's padding-bottom needed
  the same treatment. Both variants are restated alongside their base rules.
- TS px constants that mirror rem CSS are the latent-bug half of this work:
  both context menus' flip/clamp math converts through a new @core/metrics
  remPx() helper reading the LIVE root font-size (the shadlike menu's
  MENU_W=224 diverged from its w-56 box the moment scale != 1; the 7like
  menu's [style.width.px] pinned the box at 200px while text grew — it now
  binds width in rem, so math and box cannot disagree). The editors'
  cachedCharWidth is now keyed on the computed font string, self-invalidating
  under any font change, and both || px fallbacks derive from the element's
  font size instead of assuming a 16px root.

Verified: tsc + build clean; modern and win7 live captures and the harness
all diff to zero against pre-change baselines in their opaque regions (the
harness to exactly zero end to end; modern's only delta is the native GTK
titlebar focus state; win7's is the translucent Aero corner over the
desktop).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-16 18:02:41 +02:00
co-authored by Claude
parent c8ccc6b918
commit 9f2a076e08
21 changed files with 330 additions and 181 deletions
+20 -20
View File
@@ -108,7 +108,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
(click)="onUpdateClick()"
[disabled]="updater.state() === 'downloading'"
[title]="i18n.t('tabBar.updateTooltip', { version: updater.version() })"
class="relative mr-1 grid h-6 place-items-center overflow-hidden rounded-md px-2.5 text-[11px] font-medium bg-primary text-primary-foreground electrobun-webkit-app-region-no-drag hover:opacity-90 disabled:cursor-default"
class="relative mr-1 grid h-6 place-items-center overflow-hidden rounded-md px-2.5 text-[0.6875rem] font-medium bg-primary text-primary-foreground electrobun-webkit-app-region-no-drag hover:opacity-90 disabled:cursor-default"
>
@if (updater.state() === 'downloading') {
<span
@@ -136,7 +136,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
<button
(click)="openManager()"
[title]="i18n.t('tabBar.manageConnections')"
class="grid h-6 px-2 py-1 place-items-center rounded-md text-[11px] text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground"
class="grid h-6 px-2 py-1 place-items-center rounded-md text-[0.6875rem] text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground"
>
<!-- <ng-icon name="mynaCog" size="0.9rem" /> -->
{{ i18n.t('tabBar.connections') }}
@@ -162,7 +162,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
<span class="text-xs text-muted-foreground">{{ i18n.t('options.language') }}</span>
<select
(change)="settings.setLocale($any($event.target).value)"
class="h-7 rounded-md border border-input bg-background px-1.5 text-[11px] outline-none focus:ring-2 focus:ring-ring"
class="h-7 rounded-md border border-input bg-background px-1.5 text-[0.6875rem] outline-none focus:ring-2 focus:ring-ring"
>
<option value="system" [selected]="settings.locale() === 'system'">
{{ i18n.t('options.system') }}
@@ -189,7 +189,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
<button
(click)="settings.setSkin(s)"
[title]="i18n.t('options.skin.restartHint')"
class="rounded px-2 py-0.5 text-[11px]"
class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="settings.skin() === s"
[class.text-foreground]="settings.skin() === s"
[class.text-muted-foreground]="settings.skin() !== s"
@@ -200,7 +200,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
</div>
</div>
@if (skinPending()) {
<p class="mt-1 text-right text-[10px] text-muted-foreground">
<p class="mt-1 text-right text-[0.625rem] text-muted-foreground">
{{ i18n.t('options.skin.restartHint') }}
</p>
}
@@ -211,7 +211,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
@for (theme of themes; track theme) {
<button
(click)="settings.setTheme(theme)"
class="rounded px-2 py-0.5 text-[11px]"
class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="settings.theme() === theme"
[class.text-foreground]="settings.theme() === theme"
[class.text-muted-foreground]="settings.theme() !== theme"
@@ -229,7 +229,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
@for (tone of darkTones; track tone) {
<button
(click)="settings.setDarkTone(tone)"
class="rounded px-2 py-0.5 text-[11px]"
class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="settings.darkTone() === tone"
[class.text-foreground]="settings.darkTone() === tone"
[class.text-muted-foreground]="settings.darkTone() !== tone"
@@ -265,7 +265,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
@for (mode of iconColorModes; track mode) {
<button
(click)="settings.setIconColors(mode)"
class="rounded px-2 py-0.5 text-[11px]"
class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="settings.iconColors() === mode"
[class.text-foreground]="settings.iconColors() === mode"
[class.text-muted-foreground]="settings.iconColors() !== mode"
@@ -282,7 +282,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
@for (size of textSizes; track size) {
<button
(click)="settings.setTextSize(size)"
class="rounded px-2 py-0.5 text-[11px]"
class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="settings.textSize() === size"
[class.text-foreground]="settings.textSize() === size"
[class.text-muted-foreground]="settings.textSize() !== size"
@@ -299,7 +299,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
@for (unit of speedUnits; track unit.value) {
<button
(click)="settings.setSpeedUnit(unit.value)"
class="rounded px-2 py-0.5 text-[11px]"
class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="settings.speedUnit() === unit.value"
[class.text-foreground]="settings.speedUnit() === unit.value"
[class.text-muted-foreground]="settings.speedUnit() !== unit.value"
@@ -316,7 +316,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
@for (mode of transferModes; track mode) {
<button
(click)="settings.setTransferMode(mode)"
class="rounded px-2 py-0.5 text-[11px]"
class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="settings.transferMode() === mode"
[class.text-foreground]="settings.transferMode() === mode"
[class.text-muted-foreground]="settings.transferMode() !== mode"
@@ -334,7 +334,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
@for (count of concurrencyChoices; track count) {
<button
(click)="settings.setTransferConcurrency(count)"
class="rounded px-2 py-0.5 text-[11px] tabular-nums"
class="rounded px-2 py-0.5 text-[0.6875rem] tabular-nums"
[class.bg-accent]="settings.transferConcurrency() === count"
[class.text-foreground]="settings.transferConcurrency() === count"
[class.text-muted-foreground]="settings.transferConcurrency() !== count"
@@ -351,7 +351,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
<div class="flex rounded-md border border-border p-0.5">
<button
(click)="settings.setEditor('builtin')"
class="rounded px-2 py-0.5 text-[11px]"
class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="settings.editor() === 'builtin'"
[class.text-foreground]="settings.editor() === 'builtin'"
[class.text-muted-foreground]="settings.editor() !== 'builtin'"
@@ -360,7 +360,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
</button>
<button
(click)="settings.setEditor('custom')"
class="rounded px-2 py-0.5 text-[11px]"
class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="settings.editor() === 'custom'"
[class.text-foreground]="settings.editor() === 'custom'"
[class.text-muted-foreground]="settings.editor() !== 'custom'"
@@ -378,7 +378,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
<div class="flex shrink-0 items-center gap-1">
<button
(click)="pickEditorApp()"
class="rounded-md border border-border px-2 py-0.5 text-[11px] hover:bg-accent"
class="rounded-md border border-border px-2 py-0.5 text-[0.6875rem] hover:bg-accent"
>
{{ i18n.t('common.choose') }}
</button>
@@ -400,7 +400,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
<div class="flex rounded-md border border-border p-0.5">
<button
(click)="settings.setAutoClearCompleted(true)"
class="rounded px-2 py-0.5 text-[11px]"
class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="settings.autoClearCompleted()"
[class.text-foreground]="settings.autoClearCompleted()"
[class.text-muted-foreground]="!settings.autoClearCompleted()"
@@ -409,7 +409,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
</button>
<button
(click)="settings.setAutoClearCompleted(false)"
class="rounded px-2 py-0.5 text-[11px]"
class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="!settings.autoClearCompleted()"
[class.text-foreground]="!settings.autoClearCompleted()"
[class.text-muted-foreground]="settings.autoClearCompleted()"
@@ -424,11 +424,11 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
<div class="mt-2.5 flex items-center justify-between gap-2">
<span class="text-xs text-muted-foreground">Auto-refresh</span>
<div class="flex rounded-md border border-border p-0.5">
<button (click)="settings.setAutoRefresh(true)" class="rounded px-2 py-0.5 text-[11px]"
<button (click)="settings.setAutoRefresh(true)" class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="settings.autoRefresh()"
[class.text-foreground]="settings.autoRefresh()"
[class.text-muted-foreground]="!settings.autoRefresh()">On</button>
<button (click)="settings.setAutoRefresh(false)" class="rounded px-2 py-0.5 text-[11px]"
<button (click)="settings.setAutoRefresh(false)" class="rounded px-2 py-0.5 text-[0.6875rem]"
[class.bg-accent]="!settings.autoRefresh()"
[class.text-foreground]="!settings.autoRefresh()"
[class.text-muted-foreground]="settings.autoRefresh()">Off</button>
@@ -439,7 +439,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
<!-- about footer -->
<button
(click)="showAbout()"
class="mt-3 w-full border-t border-border pt-2 text-center text-[11px] text-muted-foreground hover:text-foreground"
class="mt-3 w-full border-t border-border pt-2 text-center text-[0.6875rem] text-muted-foreground hover:text-foreground"
>
{{ appName }} · {{ buildLabel }}
</button>