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:
@@ -74,12 +74,13 @@
|
||||
:root.win7 {
|
||||
/* Real Segoe first (Windows), bundled substitute everywhere else. */
|
||||
--w7-family: 'Segoe UI', 'SegoeUI', 'Noto Sans App', system-ui, sans-serif;
|
||||
/* ~7.css's 9pt, but in px so it tracks the interface-size axis below.
|
||||
Restating the `font` SHORTHAND (not just the family) is deliberate:
|
||||
7.css applies `font: var(--w7-font)` on 12 component rules, and the
|
||||
shorthand also resets weight and line-height — pinning 400/1.4 here stops
|
||||
those rules from flattening the app's `font-medium` / leading. */
|
||||
--w7-fs: 12px;
|
||||
/* ~7.css's 9pt, in REM so it follows the interface scale (the root
|
||||
font-size lever) like every other size in the app — 0.75rem = 12px at the
|
||||
16px root. Restating the `font` SHORTHAND (not just the family) is
|
||||
deliberate: 7.css applies `font: var(--w7-font)` on 15 component rules,
|
||||
and the shorthand also resets weight and line-height — pinning 400/1.4
|
||||
here stops those rules from flattening the app's `font-medium` / leading. */
|
||||
--w7-fs: 0.75rem;
|
||||
--w7-font: 400 var(--w7-fs) / 1.4 var(--w7-family);
|
||||
|
||||
/* Surface ramp, Win7 Explorer. This INVERTS the modern ramp's direction:
|
||||
@@ -104,21 +105,15 @@
|
||||
--destructive-foreground: #ffffff;
|
||||
--success: #06b025; /* the Win7 progress-bar green */
|
||||
|
||||
/* 5px, NOT 3px: `rounded-md` is 74 of the 112 radius utilities in the
|
||||
templates and compiles to calc(var(--radius) - 2px), so 5 lands it exactly
|
||||
on --w7-el-bdr (3px). --radius itself feeds `rounded-lg` (12 uses) at 5px,
|
||||
near the 6px window radius. Bare `rounded` (26 uses) is a Tailwind literal
|
||||
0.25rem and is unaffected. */
|
||||
--radius: 5px;
|
||||
/* 0.3125rem = 5px at the 16px root — NOT 3px: `rounded-md` is 74 of the 112
|
||||
radius utilities in the templates and compiles to
|
||||
calc(var(--radius) - 0.125rem), so 5 lands it exactly on --w7-el-bdr (3px).
|
||||
--radius itself feeds `rounded-lg` (12 uses) at 5px, near the 6px window
|
||||
radius. Bare `rounded` (26 uses) is a Tailwind literal 0.25rem. Rem so the
|
||||
corners follow the interface scale with everything else. */
|
||||
--radius: 0.3125rem;
|
||||
}
|
||||
|
||||
/* Interface size: Win7 shipped this exact axis ("Smaller / Medium / Larger"),
|
||||
so scaling is more authentic than disabling it. One indirection covers all
|
||||
three tiers because var substitution is lazy. The --row-h / --list-font
|
||||
block further up scales the file list independently and is untouched. */
|
||||
:root.win7[data-text='compact'] { --w7-fs: 11px; }
|
||||
:root.win7[data-text='large'] { --w7-fs: 13px; }
|
||||
|
||||
/* Light values for every 7.css literal the skinfix rules below restate through
|
||||
vars (7.css hardcodes these outside its var system). The values are 7.css's
|
||||
own, verbatim — light must render exactly as stock. Dark overrides live in
|
||||
@@ -420,12 +415,120 @@ html.win7 body {
|
||||
/* Above `skin`, for rules that must state a value rather than defer to a
|
||||
neighbouring layer. */
|
||||
@layer skinfix {
|
||||
/* ---- Scale bridge: re-express 7.css's px metrics in rem ----
|
||||
7.css sizes everything in px (370 of them, zero rem) and its --w7-font
|
||||
shorthand reaches only 15 rules' TYPOGRAPHY — the boxes stay frozen. Under
|
||||
the interface scale (root font-size lever) win7 text would grow inside
|
||||
23px buttons, 14px table rows and a 15px-line-height caption. Every value
|
||||
below is 7.css's own, converted at the 16px root (23px -> 1.4375rem …),
|
||||
so scale 1.0 renders pixel-identical to stock and every other scale keeps
|
||||
the boxes proportional to the text.
|
||||
Deliberately NOT converted: 1px hairlines and focus rects (the tr border
|
||||
that stops selection shifting rows, .is-cursor's 1px dotted outline, menu
|
||||
bevels, the frame's inset glass ring) — those stay crisp at any scale. */
|
||||
|
||||
/* 7.css's own size vars — defined on .win7 in px; rem lands on the same
|
||||
pixels at scale 1. Covers checkbox/radio/slider/tooltip/treeview/menu
|
||||
gutters and both window metrics in one shot. */
|
||||
.win7 {
|
||||
--w7-cb-size: 0.875rem; /* 14px */
|
||||
--w7-rd-size: 0.875rem;
|
||||
--w7-rdd-size: 0.5rem; /* 8px */
|
||||
--w7-rdl-space: 0.375rem; /* 6px */
|
||||
--w7-sdt-w: 0.625rem; /* 10px */
|
||||
--w7-sdt-h: 1.125rem; /* 18px */
|
||||
--w7-blt-size: 1.125rem;
|
||||
--w7-tvb-size: 0.5rem;
|
||||
--w7-tv-left: 1.25rem; /* 20px */
|
||||
--w7-clc-size: 0.3125rem; /* 5px */
|
||||
--w7-lvc-size: 0.3125rem;
|
||||
--w7-mn-left: 1.75rem; /* 28px */
|
||||
--w7-el-bdr: 0.1875rem; /* 3px */
|
||||
--w7-w-space: 0.375rem; /* 6px */
|
||||
--w7-w-bdr: 0.375rem;
|
||||
}
|
||||
|
||||
/* The hardcoded box metrics the vars do not reach, restated in rem; skinfix
|
||||
outranks skin by layer, so these win over 7.css.
|
||||
NO blanket `.win7 button` rule here, deliberately: the `skin`-layer
|
||||
neutralizer strips 7.css's 75x23 metrics off every button (most are icon
|
||||
squares sized by utilities), and a skinfix restatement would outrank that
|
||||
neutralizer by LAYER and re-inflate all of them. The only buttons that
|
||||
legitimately wear Win7 push-button metrics are .w7-btn (opted in below)
|
||||
and the title-bar controls (which beat the neutralizer on specificity
|
||||
inside `skin` and are restated here). */
|
||||
.win7 input[type='email'],
|
||||
.win7 input[type='number'],
|
||||
.win7 input[type='password'],
|
||||
.win7 input[type='search'],
|
||||
.win7 input[type='text'],
|
||||
.win7 input[type='url'] {
|
||||
height: 1.4375rem;
|
||||
}
|
||||
.win7 input[type='email'],
|
||||
.win7 input[type='number'],
|
||||
.win7 input[type='password'],
|
||||
.win7 input[type='search'],
|
||||
.win7 input[type='text'],
|
||||
.win7 input[type='url'],
|
||||
.win7 textarea {
|
||||
padding: 0.1875rem 0.25rem 0.3125rem; /* 3px 4px 5px */
|
||||
}
|
||||
.win7 table > thead > tr > * {
|
||||
height: 1.375rem; /* 22px */
|
||||
padding: 0 0.5rem; /* 0 8px */
|
||||
}
|
||||
.win7 table > tbody > tr > * {
|
||||
height: 0.875rem; /* 14px */
|
||||
padding: 0.125rem 0.5rem; /* 2px 8px */
|
||||
}
|
||||
.win7 .title-bar-text {
|
||||
line-height: 0.9375rem; /* 15px */
|
||||
}
|
||||
.win7 .title-bar-controls button {
|
||||
min-height: 1.1875rem; /* 19px */
|
||||
min-width: 1.8125rem; /* 29px */
|
||||
padding: 0;
|
||||
}
|
||||
/* BOTH of 7.css's wide-close selectors, not just .is-close: the app's title
|
||||
bars key the buttons on aria-label, and the base rule above outranks the
|
||||
`skin`-layer variant BY LAYER — restating only one arm shrank the close
|
||||
button from 48px to 29px (caught by the harness pixel diff). */
|
||||
.win7 .title-bar-controls button.is-close,
|
||||
.win7 .title-bar-controls button[aria-label='Close'] {
|
||||
min-width: 3rem; /* 48px */
|
||||
}
|
||||
.win7 ul[role='menu'] {
|
||||
min-width: 9.375rem; /* 150px */
|
||||
padding: 0.125rem; /* 2px */
|
||||
}
|
||||
.win7 ul[role='menu'] > [role='menuitem'] > a,
|
||||
.win7 ul[role='menu'] > [role='menuitem'] > button,
|
||||
.win7 ul[role='menu'] > [role='menuitem'] > label {
|
||||
padding: 0.25rem 0.625rem 0.25rem 2rem; /* 4px 10px 4px 32px */
|
||||
}
|
||||
.win7 menu[role='tablist'] button {
|
||||
padding: 0.125rem 0.375rem; /* 2px 6px */
|
||||
}
|
||||
/* The raised active tab: 7.css gives it padding-bottom 4px at (0,2,2) inside
|
||||
`skin`, but the shorthand above outranks that BY LAYER and would flatten
|
||||
the raise — so the variant is restated here too. */
|
||||
.win7 menu[role='tablist'] button[aria-selected='true'] {
|
||||
padding-bottom: 0.25rem; /* 4px */
|
||||
}
|
||||
/* The 10pt checkmark glyph in menu checkables — one of five pt sizes in
|
||||
7.css that bypass --w7-font entirely. 10pt = 13.333px ≈ 0.8333rem. */
|
||||
.win7 ul [role='menuitem'] > input[type='checkbox']:checked + label:after {
|
||||
font-size: 0.8333rem;
|
||||
}
|
||||
|
||||
/* Opt back IN to real Win7 push-button metrics, for the dialog and options
|
||||
buttons where they are right. */
|
||||
buttons where they are right. Same rem values as the button rule above —
|
||||
this class exists to undo the `skin`-layer neutralizer, not to differ. */
|
||||
.win7 .w7-btn {
|
||||
min-width: 75px;
|
||||
min-height: 23px;
|
||||
padding: 0 12px;
|
||||
min-width: 4.6875rem;
|
||||
min-height: 1.4375rem;
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
/* ---- ListView (file panels) ----
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
(click)="importWinScp()"
|
||||
[disabled]="importing()"
|
||||
[title]="i18n.t('connMgr.importWinscpHint')"
|
||||
class="flex items-center gap-1.5 rounded-md px-2 py-1 text-[11px] text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground disabled:opacity-50"
|
||||
class="flex items-center gap-1.5 rounded-md px-2 py-1 text-[0.6875rem] text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground disabled:opacity-50"
|
||||
>
|
||||
@if (importing()) {
|
||||
<ng-icon name="mynaSpinner" size="0.85rem" class="animate-spin" />
|
||||
@@ -72,14 +72,14 @@
|
||||
size="0.8rem"
|
||||
/>
|
||||
</button>
|
||||
<span class="truncate text-[11px] font-medium text-muted-foreground uppercase">
|
||||
<span class="truncate text-[0.6875rem] font-medium text-muted-foreground uppercase">
|
||||
{{ group.stack.name }}
|
||||
</span>
|
||||
<div class="min-w-0 flex-1"></div>
|
||||
<!-- count and actions swap on hover: they never share the space,
|
||||
so a long stack name can't collide with the buttons -->
|
||||
<span
|
||||
class="shrink-0 text-[10px] text-muted-foreground/70 tabular-nums group-hover:hidden"
|
||||
class="shrink-0 text-[0.625rem] text-muted-foreground/70 tabular-nums group-hover:hidden"
|
||||
>
|
||||
{{ group.connections.length }}
|
||||
</span>
|
||||
@@ -124,7 +124,7 @@
|
||||
<span class="truncate">{{ connection.name }}</span>
|
||||
</button>
|
||||
} @empty {
|
||||
<div class="py-1 pl-6 text-[11px] text-muted-foreground italic">{{ i18n.t('connMgr.empty') }}</div>
|
||||
<div class="py-1 pl-6 text-[0.6875rem] text-muted-foreground italic">{{ i18n.t('connMgr.empty') }}</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -142,7 +142,7 @@
|
||||
[class.ring-primary]="dropTarget() === 'loose'"
|
||||
>
|
||||
@if (store.grouped().stacks.length && store.grouped().loose.length) {
|
||||
<div class="px-2 pt-2 pb-0.5 text-[11px] font-medium text-muted-foreground uppercase">
|
||||
<div class="px-2 pt-2 pb-0.5 text-[0.6875rem] font-medium text-muted-foreground uppercase">
|
||||
{{ i18n.t('connMgr.connectionsHeader') }}
|
||||
</div>
|
||||
}
|
||||
@@ -163,10 +163,10 @@
|
||||
</div>
|
||||
|
||||
@if (!store.connections().length && !store.stacks().length) {
|
||||
<p class="px-3 py-2 text-[11px] text-muted-foreground">
|
||||
<p class="px-3 py-2 text-[0.6875rem] text-muted-foreground">
|
||||
{{ i18n.t('connMgr.noneTitle') }}
|
||||
</p>
|
||||
<p class="px-3 py-2 text-[11px] text-muted-foreground">
|
||||
<p class="px-3 py-2 text-[0.6875rem] text-muted-foreground">
|
||||
{{ i18n.t('connMgr.noneBody') }}
|
||||
</p>
|
||||
}
|
||||
@@ -294,7 +294,7 @@
|
||||
<button
|
||||
type="button"
|
||||
(click)="pickKeyFile()"
|
||||
class="h-7 shrink-0 rounded-md border border-border px-2 text-[11px] hover:bg-accent"
|
||||
class="h-7 shrink-0 rounded-md border border-border px-2 text-[0.6875rem] hover:bg-accent"
|
||||
>
|
||||
{{ i18n.t('common.choose') }}
|
||||
</button>
|
||||
@@ -321,7 +321,7 @@
|
||||
spellcheck="false"
|
||||
class="h-7 rounded-md border border-input bg-transparent px-2 font-mono outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<span class="text-[10px] text-muted-foreground/80">
|
||||
<span class="text-[0.625rem] text-muted-foreground/80">
|
||||
{{ i18n.t('connMgr.localDirHint') }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { Component, ElementRef, computed, effect, inject, signal, viewChild } from '@angular/core';
|
||||
import { ContextMenuService, type MenuItem } from '@core/context-menu.service';
|
||||
import { FocusService } from '@core/focus.service';
|
||||
import { remPx } from '@core/metrics';
|
||||
|
||||
/** Rough per-item metrics, used only to decide whether the menu must flip. */
|
||||
const ITEM_H = 24; // 7.css: 4px padding x2 + ~15px line
|
||||
const DIVIDER_H = 7; // .has-divider:after: 2px bevel + margins
|
||||
const MENU_W = 200;
|
||||
const EDGE_GAP = 6;
|
||||
/** Rough per-item metrics in REM, used to size the menu and decide whether it
|
||||
* must flip. Rem because the interface scale is a root font-size lever: the
|
||||
* old `MENU_W = 200` was pinned into the DOM via [style.width.px], so the box
|
||||
* stayed 200px while the text inside grew with the scale and overflowed.
|
||||
* Values mirror the skinfix restatement of 7.css's menu metrics in 7like.css
|
||||
* (item padding 0.25rem x2 + the ~0.9375rem line). */
|
||||
const ITEM_H_REM = 1.5; // 24px @16
|
||||
const DIVIDER_H_REM = 0.4375; // 7px @16: 2px bevel + 3px/2px margins
|
||||
const MENU_W_REM = 12.5; // 200px @16
|
||||
const EDGE_GAP = 6; // px on purpose: a viewport-edge margin, not a UI size
|
||||
|
||||
/** One rendered <li>: separators collapse onto the item BEFORE them. */
|
||||
interface MenuRow {
|
||||
@@ -44,7 +50,7 @@ interface MenuRow {
|
||||
(keydown)="onKeydown($event)"
|
||||
[style.left.px]="position().x"
|
||||
[style.top.px]="position().y"
|
||||
[style.width.px]="menuWidth"
|
||||
[style.width.rem]="menuWidthRem"
|
||||
class="fixed z-50 outline-none"
|
||||
>
|
||||
@for (row of rows(); track $index) {
|
||||
@@ -80,7 +86,9 @@ export class ContextMenu {
|
||||
private readonly focus = inject(FocusService);
|
||||
private readonly menuEl = viewChild<ElementRef<HTMLUListElement>>('menuEl');
|
||||
|
||||
protected readonly menuWidth = MENU_W;
|
||||
/** Rendered width AND math input are the same rem value, so the flip point
|
||||
* and the box can never disagree — the shadlike menu's old bug. */
|
||||
protected readonly menuWidthRem = MENU_W_REM;
|
||||
|
||||
/** Model items → rendered rows: a separator becomes has-divider on its predecessor. */
|
||||
readonly rows = computed<MenuRow[]>(() => {
|
||||
@@ -96,15 +104,20 @@ export class ContextMenu {
|
||||
return rows;
|
||||
});
|
||||
|
||||
/** Click point, flipped/clamped so the menu never overflows the viewport. */
|
||||
/** Click point, flipped/clamped so the menu never overflows the viewport.
|
||||
* UI metrics convert through the live root font-size (remPx); the click
|
||||
* point and window.inner* are both viewport CSS px already. */
|
||||
readonly position = computed(() => {
|
||||
const req = this.menu.request();
|
||||
if (!req) return { x: 0, y: 0 };
|
||||
const rows = this.rows();
|
||||
const menuW = remPx(MENU_W_REM);
|
||||
const height =
|
||||
rows.length * ITEM_H + rows.filter((r) => r.hasDivider).length * DIVIDER_H + 6;
|
||||
rows.length * remPx(ITEM_H_REM) +
|
||||
rows.filter((r) => r.hasDivider).length * remPx(DIVIDER_H_REM) +
|
||||
remPx(0.25); // ul padding 0.125rem x2
|
||||
const x =
|
||||
req.x + MENU_W > window.innerWidth - EDGE_GAP ? Math.max(EDGE_GAP, req.x - MENU_W) : req.x;
|
||||
req.x + menuW > window.innerWidth - EDGE_GAP ? Math.max(EDGE_GAP, req.x - menuW) : req.x;
|
||||
const y =
|
||||
req.y + height > window.innerHeight - EDGE_GAP ? Math.max(EDGE_GAP, req.y - height) : req.y;
|
||||
return { x, y };
|
||||
|
||||
@@ -30,7 +30,7 @@ import { FocusService } from '@core/focus.service';
|
||||
</h2>
|
||||
@if (request().total > 1) {
|
||||
<span
|
||||
class="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground"
|
||||
class="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[0.625rem] font-medium text-muted-foreground"
|
||||
>
|
||||
{{ i18n.t('dialogs.ofTotal', { index: request().index, total: request().total }) }}
|
||||
</span>
|
||||
@@ -49,11 +49,11 @@ import { FocusService } from '@core/focus.service';
|
||||
@for (side of comparison(); track side.label) {
|
||||
<div class="rounded-md border border-border p-2.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[10px] font-medium tracking-wide text-muted-foreground uppercase">
|
||||
<span class="text-[0.625rem] font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{{ i18n.t('dialogs.' + side.label) }}
|
||||
</span>
|
||||
@if (side.newer) {
|
||||
<span class="rounded-full bg-success/15 px-1.5 text-[10px] font-medium text-success">
|
||||
<span class="rounded-full bg-success/15 px-1.5 text-[0.625rem] font-medium text-success">
|
||||
{{ i18n.t('dialogs.newer') }}
|
||||
</span>
|
||||
}
|
||||
@@ -68,7 +68,7 @@ import { FocusService } from '@core/focus.service';
|
||||
{{ isDir() ? '—' : i18n.size(side.entry.size) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 font-mono text-[11px] tabular-nums text-muted-foreground">
|
||||
<div class="mt-1 font-mono text-[0.6875rem] tabular-nums text-muted-foreground">
|
||||
{{ i18n.date(side.entry.mtime) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
<h1 class="min-w-0 truncate font-mono text-sm font-semibold">
|
||||
{{ ctx.name }}{{ dirty() ? ' •' : '' }}
|
||||
</h1>
|
||||
<span class="shrink-0 text-[11px] text-muted-foreground">{{ ctx.sessionTitle }}</span>
|
||||
<span class="shrink-0 text-[0.6875rem] text-muted-foreground">{{ ctx.sessionTitle }}</span>
|
||||
<div class="flex-1"></div>
|
||||
<button
|
||||
(click)="toggleWrap()"
|
||||
[title]="i18n.t('editor.wrapHint', { alt: altLabel })"
|
||||
class="rounded-md px-2 py-1 text-[11px] electrobun-webkit-app-region-no-drag hover:bg-accent"
|
||||
class="rounded-md px-2 py-1 text-[0.6875rem] electrobun-webkit-app-region-no-drag hover:bg-accent"
|
||||
[class.bg-accent]="wrap()"
|
||||
[class.text-foreground]="wrap()"
|
||||
[class.text-muted-foreground]="!wrap()"
|
||||
@@ -56,7 +56,7 @@
|
||||
class="h-6 w-56 rounded-md border border-input bg-transparent px-2 text-xs outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<span
|
||||
class="text-[11px] tabular-nums"
|
||||
class="text-[0.6875rem] tabular-nums"
|
||||
[class.text-destructive]="findQuery() && !matches().length"
|
||||
[class.text-muted-foreground]="!findQuery() || matches().length"
|
||||
>
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
@if (sessionDead()) {
|
||||
<div
|
||||
class="flex shrink-0 items-center gap-2 border-b border-border bg-destructive/10 px-3 py-1.5 text-[11px] text-destructive"
|
||||
class="flex shrink-0 items-center gap-2 border-b border-border bg-destructive/10 px-3 py-1.5 text-[0.6875rem] text-destructive"
|
||||
>
|
||||
<ng-icon name="mynaXCircle" size="0.85rem" class="shrink-0" />
|
||||
{{ i18n.t('editor.disconnected', { name: ctx.sessionTitle }) }}
|
||||
@@ -139,9 +139,9 @@
|
||||
</div>
|
||||
|
||||
<footer class="flex shrink-0 items-center gap-2 border-t border-border bg-chrome px-4 py-2">
|
||||
<span class="text-[11px] text-muted-foreground">{{ i18n.t('editor.shortcuts', { mod: modLabel }) }}</span>
|
||||
<span class="text-[11px] text-muted-foreground">·</span>
|
||||
<span class="text-[11px] tabular-nums text-muted-foreground">
|
||||
<span class="text-[0.6875rem] text-muted-foreground">{{ i18n.t('editor.shortcuts', { mod: modLabel }) }}</span>
|
||||
<span class="text-[0.6875rem] text-muted-foreground">·</span>
|
||||
<span class="text-[0.6875rem] tabular-nums text-muted-foreground">
|
||||
{{ i18n.t('editor.lines', { count: lineCount() }) }}
|
||||
</span>
|
||||
<div class="flex-1"></div>
|
||||
|
||||
@@ -101,6 +101,7 @@ export class EditorWindow {
|
||||
private readonly gutter = viewChild<ElementRef<HTMLElement>>('gutter');
|
||||
private readonly findBox = viewChild<ElementRef<HTMLInputElement>>('findBox');
|
||||
private cachedCharWidth = 0;
|
||||
private cachedCharFont = '';
|
||||
private readonly rpc = inject(RpcService);
|
||||
private readonly toast = inject(ToastService);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
@@ -221,7 +222,11 @@ export class EditorWindow {
|
||||
el.setSelectionRange(start, start + this.findQuery().length);
|
||||
|
||||
const text = this.text();
|
||||
const lineHeight = parseFloat(getComputedStyle(el).lineHeight) || 20;
|
||||
// Fallback derives from the element's own font size, not a hardcoded 20 —
|
||||
// the interface scale moves the root font-size, so any px literal here
|
||||
// would be a 16px-root assumption that miscentres at every other scale.
|
||||
const style = getComputedStyle(el);
|
||||
const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.625;
|
||||
let line = 0;
|
||||
let i = text.indexOf('\n');
|
||||
while (i !== -1 && i < start) {
|
||||
@@ -236,12 +241,17 @@ export class EditorWindow {
|
||||
this.onScroll();
|
||||
}
|
||||
|
||||
/** Monospace advance width, measured once per window. */
|
||||
/** Monospace advance width, cached per computed FONT rather than per window:
|
||||
* the interface scale changes the root font-size live, and a cache measured
|
||||
* once would keep positioning finds with the old advance width. Keying on
|
||||
* the font string makes it self-invalidating under any font change. */
|
||||
private charWidth(el: HTMLTextAreaElement): number {
|
||||
if (!this.cachedCharWidth) {
|
||||
const font = getComputedStyle(el).font;
|
||||
if (font !== this.cachedCharFont) {
|
||||
const canvas = this.document.createElement('canvas').getContext('2d')!;
|
||||
canvas.font = getComputedStyle(el).font;
|
||||
this.cachedCharWidth = canvas.measureText('0').width || 7;
|
||||
canvas.font = font;
|
||||
this.cachedCharWidth = canvas.measureText('0').width || parseFloat(getComputedStyle(el).fontSize) * 0.6;
|
||||
this.cachedCharFont = font;
|
||||
}
|
||||
return this.cachedCharWidth;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
</button>
|
||||
}
|
||||
@for (group of connections.grouped().stacks; track group.stack.id) {
|
||||
<div class="px-3 pt-1.5 pb-0.5 text-[10px] font-medium tracking-wide text-muted-foreground uppercase">
|
||||
<div class="px-3 pt-1.5 pb-0.5 text-[0.625rem] font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{{ group.stack.name }}
|
||||
</div>
|
||||
@for (connection of group.connections; track connection.id) {
|
||||
@@ -141,7 +141,7 @@
|
||||
|
||||
@if (connections.sshHosts().length) {
|
||||
<div class="my-1 border-t border-border"></div>
|
||||
<div class="px-3 pt-1.5 pb-0.5 text-[10px] font-medium tracking-wide text-muted-foreground uppercase">
|
||||
<div class="px-3 pt-1.5 pb-0.5 text-[0.625rem] font-medium tracking-wide text-muted-foreground uppercase">
|
||||
~/.ssh/config
|
||||
</div>
|
||||
@for (host of connections.sshHosts(); track host.name) {
|
||||
@@ -205,7 +205,7 @@
|
||||
<div class="flex items-center gap-1.5 border-b border-border bg-accent/50 px-2 py-1">
|
||||
<ng-icon name="mynaSearch" size="0.85rem" class="shrink-0 text-muted-foreground" />
|
||||
<span class="min-w-0 flex-1 truncate font-mono text-xs">{{ query }}</span>
|
||||
<span class="shrink-0 text-[11px] text-muted-foreground">
|
||||
<span class="shrink-0 text-[0.6875rem] text-muted-foreground">
|
||||
{{ i18n.t('panel.matches', { count: filterMatches() }) }}
|
||||
</span>
|
||||
<button
|
||||
@@ -325,14 +325,14 @@
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td [attr.colspan]="visibleColumns().length + 1" style="text-align: center; padding: 24px">
|
||||
<td [attr.colspan]="visibleColumns().length + 1" style="text-align: center; padding: 1.5rem">
|
||||
{{ i18n.t('panel.emptyDir') }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (panel().filter() && filterMatches() === 0) {
|
||||
<tr>
|
||||
<td [attr.colspan]="visibleColumns().length + 1" style="text-align: center; padding: 24px">
|
||||
<td [attr.colspan]="visibleColumns().length + 1" style="text-align: center; padding: 1.5rem">
|
||||
{{ i18n.t('panel.noMatches') }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { TransfersService } from '@core/transfers.service';
|
||||
imports: [NgIcon],
|
||||
template: `
|
||||
<div
|
||||
class="flex h-6 items-center justify-between gap-4 border-t border-border bg-chrome px-3 text-[11px] text-muted-foreground select-none"
|
||||
class="flex h-6 items-center justify-between gap-4 border-t border-border bg-chrome px-3 text-[0.6875rem] text-muted-foreground select-none"
|
||||
>
|
||||
<!-- transfers summary; click toggles the detailed list -->
|
||||
<!-- w7-flat: this is a text toggle, not a push button. Without it 7.css
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@if (transfers.expanded() && transfers.hasJobs()) {
|
||||
<div class="flex items-center justify-between border-t border-border bg-chrome px-3 py-1 text-[11px] text-muted-foreground">
|
||||
<div class="flex items-center justify-between border-t border-border bg-chrome px-3 py-1 text-[0.6875rem] text-muted-foreground">
|
||||
<span>
|
||||
{{ i18n.t('transfer.summary', { active: transfers.activeJobs().length, finished: transfers.finishedJobs().length }) }}
|
||||
</span>
|
||||
@@ -17,7 +17,7 @@
|
||||
<ng-icon [name]="stateIcon(job)" size="0.95rem" [class]="stateClass(job)" />
|
||||
<span class="w-40 shrink-0 truncate font-medium" [title]="job.srcPath">{{ job.name }}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-[11px] text-muted-foreground">{{ progressLabel(job) }}</div>
|
||||
<div class="truncate text-[0.6875rem] text-muted-foreground">{{ progressLabel(job) }}</div>
|
||||
@if (isActive(job)) {
|
||||
<!-- role+aria are 7.css's hook for the Win7 progress bar
|
||||
([role=progressbar] > div); under the modern skin they are
|
||||
|
||||
Reference in New Issue
Block a user