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:
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Rem-to-pixel conversion at the CURRENT root font size.
|
||||||
|
*
|
||||||
|
* The interface scale (settings.uiScale) works by setting the root font-size,
|
||||||
|
* so any TypeScript that positions or measures against rem-sized CSS must
|
||||||
|
* convert through the LIVE root — never through an assumed 16. Both context
|
||||||
|
* menus' flip/clamp math is the canonical consumer: their widths and row
|
||||||
|
* heights are rem in CSS, and hardcoding their px mirrors is exactly the bug
|
||||||
|
* that made the shadlike menu flip at the wrong edge once scaling landed.
|
||||||
|
*
|
||||||
|
* Read per call, not cached: the scale can change mid-session (stepper,
|
||||||
|
* Ctrl+/-) and a menu opened after that must measure against the new root.
|
||||||
|
*/
|
||||||
|
export function remPx(rem: number): number {
|
||||||
|
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
|
}
|
||||||
@@ -74,12 +74,13 @@
|
|||||||
:root.win7 {
|
:root.win7 {
|
||||||
/* Real Segoe first (Windows), bundled substitute everywhere else. */
|
/* Real Segoe first (Windows), bundled substitute everywhere else. */
|
||||||
--w7-family: 'Segoe UI', 'SegoeUI', 'Noto Sans App', system-ui, sans-serif;
|
--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.
|
/* ~7.css's 9pt, in REM so it follows the interface scale (the root
|
||||||
Restating the `font` SHORTHAND (not just the family) is deliberate:
|
font-size lever) like every other size in the app — 0.75rem = 12px at the
|
||||||
7.css applies `font: var(--w7-font)` on 12 component rules, and the
|
16px root. Restating the `font` SHORTHAND (not just the family) is
|
||||||
shorthand also resets weight and line-height — pinning 400/1.4 here stops
|
deliberate: 7.css applies `font: var(--w7-font)` on 15 component rules,
|
||||||
those rules from flattening the app's `font-medium` / leading. */
|
and the shorthand also resets weight and line-height — pinning 400/1.4
|
||||||
--w7-fs: 12px;
|
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);
|
--w7-font: 400 var(--w7-fs) / 1.4 var(--w7-family);
|
||||||
|
|
||||||
/* Surface ramp, Win7 Explorer. This INVERTS the modern ramp's direction:
|
/* Surface ramp, Win7 Explorer. This INVERTS the modern ramp's direction:
|
||||||
@@ -104,21 +105,15 @@
|
|||||||
--destructive-foreground: #ffffff;
|
--destructive-foreground: #ffffff;
|
||||||
--success: #06b025; /* the Win7 progress-bar green */
|
--success: #06b025; /* the Win7 progress-bar green */
|
||||||
|
|
||||||
/* 5px, NOT 3px: `rounded-md` is 74 of the 112 radius utilities in the
|
/* 0.3125rem = 5px at the 16px root — NOT 3px: `rounded-md` is 74 of the 112
|
||||||
templates and compiles to calc(var(--radius) - 2px), so 5 lands it exactly
|
radius utilities in the templates and compiles to
|
||||||
on --w7-el-bdr (3px). --radius itself feeds `rounded-lg` (12 uses) at 5px,
|
calc(var(--radius) - 0.125rem), so 5 lands it exactly on --w7-el-bdr (3px).
|
||||||
near the 6px window radius. Bare `rounded` (26 uses) is a Tailwind literal
|
--radius itself feeds `rounded-lg` (12 uses) at 5px, near the 6px window
|
||||||
0.25rem and is unaffected. */
|
radius. Bare `rounded` (26 uses) is a Tailwind literal 0.25rem. Rem so the
|
||||||
--radius: 5px;
|
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
|
/* 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
|
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
|
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
|
/* Above `skin`, for rules that must state a value rather than defer to a
|
||||||
neighbouring layer. */
|
neighbouring layer. */
|
||||||
@layer skinfix {
|
@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
|
/* 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 {
|
.win7 .w7-btn {
|
||||||
min-width: 75px;
|
min-width: 4.6875rem;
|
||||||
min-height: 23px;
|
min-height: 1.4375rem;
|
||||||
padding: 0 12px;
|
padding: 0 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- ListView (file panels) ----
|
/* ---- ListView (file panels) ----
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
(click)="importWinScp()"
|
(click)="importWinScp()"
|
||||||
[disabled]="importing()"
|
[disabled]="importing()"
|
||||||
[title]="i18n.t('connMgr.importWinscpHint')"
|
[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()) {
|
@if (importing()) {
|
||||||
<ng-icon name="mynaSpinner" size="0.85rem" class="animate-spin" />
|
<ng-icon name="mynaSpinner" size="0.85rem" class="animate-spin" />
|
||||||
@@ -72,14 +72,14 @@
|
|||||||
size="0.8rem"
|
size="0.8rem"
|
||||||
/>
|
/>
|
||||||
</button>
|
</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 }}
|
{{ group.stack.name }}
|
||||||
</span>
|
</span>
|
||||||
<div class="min-w-0 flex-1"></div>
|
<div class="min-w-0 flex-1"></div>
|
||||||
<!-- count and actions swap on hover: they never share the space,
|
<!-- count and actions swap on hover: they never share the space,
|
||||||
so a long stack name can't collide with the buttons -->
|
so a long stack name can't collide with the buttons -->
|
||||||
<span
|
<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 }}
|
{{ group.connections.length }}
|
||||||
</span>
|
</span>
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
<span class="truncate">{{ connection.name }}</span>
|
<span class="truncate">{{ connection.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
} @empty {
|
} @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>
|
</div>
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
[class.ring-primary]="dropTarget() === 'loose'"
|
[class.ring-primary]="dropTarget() === 'loose'"
|
||||||
>
|
>
|
||||||
@if (store.grouped().stacks.length && store.grouped().loose.length) {
|
@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') }}
|
{{ i18n.t('connMgr.connectionsHeader') }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -163,10 +163,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!store.connections().length && !store.stacks().length) {
|
@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') }}
|
{{ i18n.t('connMgr.noneTitle') }}
|
||||||
</p>
|
</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') }}
|
{{ i18n.t('connMgr.noneBody') }}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
@@ -294,7 +294,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="pickKeyFile()"
|
(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') }}
|
{{ i18n.t('common.choose') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -321,7 +321,7 @@
|
|||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
class="h-7 rounded-md border border-input bg-transparent px-2 font-mono outline-none focus:ring-2 focus:ring-ring"
|
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') }}
|
{{ i18n.t('connMgr.localDirHint') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { Component, ElementRef, computed, effect, inject, signal, viewChild } from '@angular/core';
|
import { Component, ElementRef, computed, effect, inject, signal, viewChild } from '@angular/core';
|
||||||
import { ContextMenuService, type MenuItem } from '@core/context-menu.service';
|
import { ContextMenuService, type MenuItem } from '@core/context-menu.service';
|
||||||
import { FocusService } from '@core/focus.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. */
|
/** Rough per-item metrics in REM, used to size the menu and decide whether it
|
||||||
const ITEM_H = 24; // 7.css: 4px padding x2 + ~15px line
|
* must flip. Rem because the interface scale is a root font-size lever: the
|
||||||
const DIVIDER_H = 7; // .has-divider:after: 2px bevel + margins
|
* old `MENU_W = 200` was pinned into the DOM via [style.width.px], so the box
|
||||||
const MENU_W = 200;
|
* stayed 200px while the text inside grew with the scale and overflowed.
|
||||||
const EDGE_GAP = 6;
|
* 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. */
|
/** One rendered <li>: separators collapse onto the item BEFORE them. */
|
||||||
interface MenuRow {
|
interface MenuRow {
|
||||||
@@ -44,7 +50,7 @@ interface MenuRow {
|
|||||||
(keydown)="onKeydown($event)"
|
(keydown)="onKeydown($event)"
|
||||||
[style.left.px]="position().x"
|
[style.left.px]="position().x"
|
||||||
[style.top.px]="position().y"
|
[style.top.px]="position().y"
|
||||||
[style.width.px]="menuWidth"
|
[style.width.rem]="menuWidthRem"
|
||||||
class="fixed z-50 outline-none"
|
class="fixed z-50 outline-none"
|
||||||
>
|
>
|
||||||
@for (row of rows(); track $index) {
|
@for (row of rows(); track $index) {
|
||||||
@@ -80,7 +86,9 @@ export class ContextMenu {
|
|||||||
private readonly focus = inject(FocusService);
|
private readonly focus = inject(FocusService);
|
||||||
private readonly menuEl = viewChild<ElementRef<HTMLUListElement>>('menuEl');
|
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. */
|
/** Model items → rendered rows: a separator becomes has-divider on its predecessor. */
|
||||||
readonly rows = computed<MenuRow[]>(() => {
|
readonly rows = computed<MenuRow[]>(() => {
|
||||||
@@ -96,15 +104,20 @@ export class ContextMenu {
|
|||||||
return rows;
|
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(() => {
|
readonly position = computed(() => {
|
||||||
const req = this.menu.request();
|
const req = this.menu.request();
|
||||||
if (!req) return { x: 0, y: 0 };
|
if (!req) return { x: 0, y: 0 };
|
||||||
const rows = this.rows();
|
const rows = this.rows();
|
||||||
|
const menuW = remPx(MENU_W_REM);
|
||||||
const height =
|
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 =
|
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 =
|
const y =
|
||||||
req.y + height > window.innerHeight - EDGE_GAP ? Math.max(EDGE_GAP, req.y - height) : req.y;
|
req.y + height > window.innerHeight - EDGE_GAP ? Math.max(EDGE_GAP, req.y - height) : req.y;
|
||||||
return { x, y };
|
return { x, y };
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { FocusService } from '@core/focus.service';
|
|||||||
</h2>
|
</h2>
|
||||||
@if (request().total > 1) {
|
@if (request().total > 1) {
|
||||||
<span
|
<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 }) }}
|
{{ i18n.t('dialogs.ofTotal', { index: request().index, total: request().total }) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -49,11 +49,11 @@ import { FocusService } from '@core/focus.service';
|
|||||||
@for (side of comparison(); track side.label) {
|
@for (side of comparison(); track side.label) {
|
||||||
<div class="rounded-md border border-border p-2.5">
|
<div class="rounded-md border border-border p-2.5">
|
||||||
<div class="flex items-center justify-between">
|
<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) }}
|
{{ i18n.t('dialogs.' + side.label) }}
|
||||||
</span>
|
</span>
|
||||||
@if (side.newer) {
|
@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') }}
|
{{ i18n.t('dialogs.newer') }}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ import { FocusService } from '@core/focus.service';
|
|||||||
{{ isDir() ? '—' : i18n.size(side.entry.size) }}
|
{{ isDir() ? '—' : i18n.size(side.entry.size) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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) }}
|
{{ i18n.date(side.entry.mtime) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,12 +22,12 @@
|
|||||||
<h1 class="min-w-0 truncate font-mono text-sm font-semibold">
|
<h1 class="min-w-0 truncate font-mono text-sm font-semibold">
|
||||||
{{ ctx.name }}{{ dirty() ? ' •' : '' }}
|
{{ ctx.name }}{{ dirty() ? ' •' : '' }}
|
||||||
</h1>
|
</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>
|
<div class="flex-1"></div>
|
||||||
<button
|
<button
|
||||||
(click)="toggleWrap()"
|
(click)="toggleWrap()"
|
||||||
[title]="i18n.t('editor.wrapHint', { alt: altLabel })"
|
[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.bg-accent]="wrap()"
|
||||||
[class.text-foreground]="wrap()"
|
[class.text-foreground]="wrap()"
|
||||||
[class.text-muted-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"
|
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
|
<span
|
||||||
class="text-[11px] tabular-nums"
|
class="text-[0.6875rem] tabular-nums"
|
||||||
[class.text-destructive]="findQuery() && !matches().length"
|
[class.text-destructive]="findQuery() && !matches().length"
|
||||||
[class.text-muted-foreground]="!findQuery() || matches().length"
|
[class.text-muted-foreground]="!findQuery() || matches().length"
|
||||||
>
|
>
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
|
|
||||||
@if (sessionDead()) {
|
@if (sessionDead()) {
|
||||||
<div
|
<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" />
|
<ng-icon name="mynaXCircle" size="0.85rem" class="shrink-0" />
|
||||||
{{ i18n.t('editor.disconnected', { name: ctx.sessionTitle }) }}
|
{{ i18n.t('editor.disconnected', { name: ctx.sessionTitle }) }}
|
||||||
@@ -139,9 +139,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="flex shrink-0 items-center gap-2 border-t border-border bg-chrome px-4 py-2">
|
<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-[0.6875rem] text-muted-foreground">{{ i18n.t('editor.shortcuts', { mod: modLabel }) }}</span>
|
||||||
<span class="text-[11px] text-muted-foreground">·</span>
|
<span class="text-[0.6875rem] text-muted-foreground">·</span>
|
||||||
<span class="text-[11px] tabular-nums text-muted-foreground">
|
<span class="text-[0.6875rem] tabular-nums text-muted-foreground">
|
||||||
{{ i18n.t('editor.lines', { count: lineCount() }) }}
|
{{ i18n.t('editor.lines', { count: lineCount() }) }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export class EditorWindow {
|
|||||||
private readonly gutter = viewChild<ElementRef<HTMLElement>>('gutter');
|
private readonly gutter = viewChild<ElementRef<HTMLElement>>('gutter');
|
||||||
private readonly findBox = viewChild<ElementRef<HTMLInputElement>>('findBox');
|
private readonly findBox = viewChild<ElementRef<HTMLInputElement>>('findBox');
|
||||||
private cachedCharWidth = 0;
|
private cachedCharWidth = 0;
|
||||||
|
private cachedCharFont = '';
|
||||||
private readonly rpc = inject(RpcService);
|
private readonly rpc = inject(RpcService);
|
||||||
private readonly toast = inject(ToastService);
|
private readonly toast = inject(ToastService);
|
||||||
private readonly document = inject(DOCUMENT);
|
private readonly document = inject(DOCUMENT);
|
||||||
@@ -221,7 +222,11 @@ export class EditorWindow {
|
|||||||
el.setSelectionRange(start, start + this.findQuery().length);
|
el.setSelectionRange(start, start + this.findQuery().length);
|
||||||
|
|
||||||
const text = this.text();
|
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 line = 0;
|
||||||
let i = text.indexOf('\n');
|
let i = text.indexOf('\n');
|
||||||
while (i !== -1 && i < start) {
|
while (i !== -1 && i < start) {
|
||||||
@@ -236,12 +241,17 @@ export class EditorWindow {
|
|||||||
this.onScroll();
|
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 {
|
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')!;
|
const canvas = this.document.createElement('canvas').getContext('2d')!;
|
||||||
canvas.font = getComputedStyle(el).font;
|
canvas.font = font;
|
||||||
this.cachedCharWidth = canvas.measureText('0').width || 7;
|
this.cachedCharWidth = canvas.measureText('0').width || parseFloat(getComputedStyle(el).fontSize) * 0.6;
|
||||||
|
this.cachedCharFont = font;
|
||||||
}
|
}
|
||||||
return this.cachedCharWidth;
|
return this.cachedCharWidth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@for (group of connections.grouped().stacks; track group.stack.id) {
|
@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 }}
|
{{ group.stack.name }}
|
||||||
</div>
|
</div>
|
||||||
@for (connection of group.connections; track connection.id) {
|
@for (connection of group.connections; track connection.id) {
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
|
|
||||||
@if (connections.sshHosts().length) {
|
@if (connections.sshHosts().length) {
|
||||||
<div class="my-1 border-t border-border"></div>
|
<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
|
~/.ssh/config
|
||||||
</div>
|
</div>
|
||||||
@for (host of connections.sshHosts(); track host.name) {
|
@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">
|
<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" />
|
<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="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() }) }}
|
{{ i18n.t('panel.matches', { count: filterMatches() }) }}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -325,14 +325,14 @@
|
|||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<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') }}
|
{{ i18n.t('panel.emptyDir') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@if (panel().filter() && filterMatches() === 0) {
|
@if (panel().filter() && filterMatches() === 0) {
|
||||||
<tr>
|
<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') }}
|
{{ i18n.t('panel.noMatches') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { TransfersService } from '@core/transfers.service';
|
|||||||
imports: [NgIcon],
|
imports: [NgIcon],
|
||||||
template: `
|
template: `
|
||||||
<div
|
<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 -->
|
<!-- transfers summary; click toggles the detailed list -->
|
||||||
<!-- w7-flat: this is a text toggle, not a push button. Without it 7.css
|
<!-- 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()"
|
(click)="onUpdateClick()"
|
||||||
[disabled]="updater.state() === 'downloading'"
|
[disabled]="updater.state() === 'downloading'"
|
||||||
[title]="i18n.t('tabBar.updateTooltip', { version: updater.version() })"
|
[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') {
|
@if (updater.state() === 'downloading') {
|
||||||
<span
|
<span
|
||||||
@@ -136,7 +136,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
<button
|
<button
|
||||||
(click)="openManager()"
|
(click)="openManager()"
|
||||||
[title]="i18n.t('tabBar.manageConnections')"
|
[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" /> -->
|
<!-- <ng-icon name="mynaCog" size="0.9rem" /> -->
|
||||||
{{ i18n.t('tabBar.connections') }}
|
{{ 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>
|
<span class="text-xs text-muted-foreground">{{ i18n.t('options.language') }}</span>
|
||||||
<select
|
<select
|
||||||
(change)="settings.setLocale($any($event.target).value)"
|
(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'">
|
<option value="system" [selected]="settings.locale() === 'system'">
|
||||||
{{ i18n.t('options.system') }}
|
{{ i18n.t('options.system') }}
|
||||||
@@ -189,7 +189,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
<button
|
<button
|
||||||
(click)="settings.setSkin(s)"
|
(click)="settings.setSkin(s)"
|
||||||
[title]="i18n.t('options.skin.restartHint')"
|
[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.bg-accent]="settings.skin() === s"
|
||||||
[class.text-foreground]="settings.skin() === s"
|
[class.text-foreground]="settings.skin() === s"
|
||||||
[class.text-muted-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>
|
||||||
</div>
|
</div>
|
||||||
@if (skinPending()) {
|
@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') }}
|
{{ i18n.t('options.skin.restartHint') }}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
@for (theme of themes; track theme) {
|
@for (theme of themes; track theme) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setTheme(theme)"
|
(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.bg-accent]="settings.theme() === theme"
|
||||||
[class.text-foreground]="settings.theme() === theme"
|
[class.text-foreground]="settings.theme() === theme"
|
||||||
[class.text-muted-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) {
|
@for (tone of darkTones; track tone) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setDarkTone(tone)"
|
(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.bg-accent]="settings.darkTone() === tone"
|
||||||
[class.text-foreground]="settings.darkTone() === tone"
|
[class.text-foreground]="settings.darkTone() === tone"
|
||||||
[class.text-muted-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) {
|
@for (mode of iconColorModes; track mode) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setIconColors(mode)"
|
(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.bg-accent]="settings.iconColors() === mode"
|
||||||
[class.text-foreground]="settings.iconColors() === mode"
|
[class.text-foreground]="settings.iconColors() === mode"
|
||||||
[class.text-muted-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) {
|
@for (size of textSizes; track size) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setTextSize(size)"
|
(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.bg-accent]="settings.textSize() === size"
|
||||||
[class.text-foreground]="settings.textSize() === size"
|
[class.text-foreground]="settings.textSize() === size"
|
||||||
[class.text-muted-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) {
|
@for (unit of speedUnits; track unit.value) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setSpeedUnit(unit.value)"
|
(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.bg-accent]="settings.speedUnit() === unit.value"
|
||||||
[class.text-foreground]="settings.speedUnit() === unit.value"
|
[class.text-foreground]="settings.speedUnit() === unit.value"
|
||||||
[class.text-muted-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) {
|
@for (mode of transferModes; track mode) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setTransferMode(mode)"
|
(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.bg-accent]="settings.transferMode() === mode"
|
||||||
[class.text-foreground]="settings.transferMode() === mode"
|
[class.text-foreground]="settings.transferMode() === mode"
|
||||||
[class.text-muted-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) {
|
@for (count of concurrencyChoices; track count) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setTransferConcurrency(count)"
|
(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.bg-accent]="settings.transferConcurrency() === count"
|
||||||
[class.text-foreground]="settings.transferConcurrency() === count"
|
[class.text-foreground]="settings.transferConcurrency() === count"
|
||||||
[class.text-muted-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">
|
<div class="flex rounded-md border border-border p-0.5">
|
||||||
<button
|
<button
|
||||||
(click)="settings.setEditor('builtin')"
|
(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.bg-accent]="settings.editor() === 'builtin'"
|
||||||
[class.text-foreground]="settings.editor() === 'builtin'"
|
[class.text-foreground]="settings.editor() === 'builtin'"
|
||||||
[class.text-muted-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>
|
||||||
<button
|
<button
|
||||||
(click)="settings.setEditor('custom')"
|
(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.bg-accent]="settings.editor() === 'custom'"
|
||||||
[class.text-foreground]="settings.editor() === 'custom'"
|
[class.text-foreground]="settings.editor() === 'custom'"
|
||||||
[class.text-muted-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">
|
<div class="flex shrink-0 items-center gap-1">
|
||||||
<button
|
<button
|
||||||
(click)="pickEditorApp()"
|
(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') }}
|
{{ i18n.t('common.choose') }}
|
||||||
</button>
|
</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">
|
<div class="flex rounded-md border border-border p-0.5">
|
||||||
<button
|
<button
|
||||||
(click)="settings.setAutoClearCompleted(true)"
|
(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.bg-accent]="settings.autoClearCompleted()"
|
||||||
[class.text-foreground]="settings.autoClearCompleted()"
|
[class.text-foreground]="settings.autoClearCompleted()"
|
||||||
[class.text-muted-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>
|
||||||
<button
|
<button
|
||||||
(click)="settings.setAutoClearCompleted(false)"
|
(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.bg-accent]="!settings.autoClearCompleted()"
|
||||||
[class.text-foreground]="!settings.autoClearCompleted()"
|
[class.text-foreground]="!settings.autoClearCompleted()"
|
||||||
[class.text-muted-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">
|
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||||
<span class="text-xs text-muted-foreground">Auto-refresh</span>
|
<span class="text-xs text-muted-foreground">Auto-refresh</span>
|
||||||
<div class="flex rounded-md border border-border p-0.5">
|
<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.bg-accent]="settings.autoRefresh()"
|
||||||
[class.text-foreground]="settings.autoRefresh()"
|
[class.text-foreground]="settings.autoRefresh()"
|
||||||
[class.text-muted-foreground]="!settings.autoRefresh()">On</button>
|
[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.bg-accent]="!settings.autoRefresh()"
|
||||||
[class.text-foreground]="!settings.autoRefresh()"
|
[class.text-foreground]="!settings.autoRefresh()"
|
||||||
[class.text-muted-foreground]="settings.autoRefresh()">Off</button>
|
[class.text-muted-foreground]="settings.autoRefresh()">Off</button>
|
||||||
@@ -439,7 +439,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
<!-- about footer -->
|
<!-- about footer -->
|
||||||
<button
|
<button
|
||||||
(click)="showAbout()"
|
(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 }}
|
{{ appName }} · {{ buildLabel }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@if (transfers.expanded() && transfers.hasJobs()) {
|
@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>
|
<span>
|
||||||
{{ i18n.t('transfer.summary', { active: transfers.activeJobs().length, finished: transfers.finishedJobs().length }) }}
|
{{ i18n.t('transfer.summary', { active: transfers.activeJobs().length, finished: transfers.finishedJobs().length }) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<ng-icon [name]="stateIcon(job)" size="0.95rem" [class]="stateClass(job)" />
|
<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>
|
<span class="w-40 shrink-0 truncate font-medium" [title]="job.srcPath">{{ job.name }}</span>
|
||||||
<div class="min-w-0 flex-1">
|
<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)) {
|
@if (isActive(job)) {
|
||||||
<!-- role+aria are 7.css's hook for the Win7 progress bar
|
<!-- role+aria are 7.css's hook for the Win7 progress bar
|
||||||
([role=progressbar] > div); under the modern skin they are
|
([role=progressbar] > div); under the modern skin they are
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
(click)="importWinScp()"
|
(click)="importWinScp()"
|
||||||
[disabled]="importing()"
|
[disabled]="importing()"
|
||||||
[title]="i18n.t('connMgr.importWinscpHint')"
|
[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()) {
|
@if (importing()) {
|
||||||
<ng-icon name="mynaSpinner" size="0.85rem" class="animate-spin" />
|
<ng-icon name="mynaSpinner" size="0.85rem" class="animate-spin" />
|
||||||
@@ -71,14 +71,14 @@
|
|||||||
size="0.8rem"
|
size="0.8rem"
|
||||||
/>
|
/>
|
||||||
</button>
|
</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 }}
|
{{ group.stack.name }}
|
||||||
</span>
|
</span>
|
||||||
<div class="min-w-0 flex-1"></div>
|
<div class="min-w-0 flex-1"></div>
|
||||||
<!-- count and actions swap on hover: they never share the space,
|
<!-- count and actions swap on hover: they never share the space,
|
||||||
so a long stack name can't collide with the buttons -->
|
so a long stack name can't collide with the buttons -->
|
||||||
<span
|
<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 }}
|
{{ group.connections.length }}
|
||||||
</span>
|
</span>
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
<span class="truncate">{{ connection.name }}</span>
|
<span class="truncate">{{ connection.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
} @empty {
|
} @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>
|
</div>
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
[class.ring-primary]="dropTarget() === 'loose'"
|
[class.ring-primary]="dropTarget() === 'loose'"
|
||||||
>
|
>
|
||||||
@if (store.grouped().stacks.length && store.grouped().loose.length) {
|
@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') }}
|
{{ i18n.t('connMgr.connectionsHeader') }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -162,10 +162,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!store.connections().length && !store.stacks().length) {
|
@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') }}
|
{{ i18n.t('connMgr.noneTitle') }}
|
||||||
</p>
|
</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') }}
|
{{ i18n.t('connMgr.noneBody') }}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
@@ -293,7 +293,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="pickKeyFile()"
|
(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') }}
|
{{ i18n.t('common.choose') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -320,7 +320,7 @@
|
|||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
class="h-7 rounded-md border border-input bg-transparent px-2 font-mono outline-none focus:ring-2 focus:ring-ring"
|
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') }}
|
{{ i18n.t('connMgr.localDirHint') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -10,12 +10,17 @@ import {
|
|||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { ContextMenuService, type MenuItem } from '@core/context-menu.service';
|
import { ContextMenuService, type MenuItem } from '@core/context-menu.service';
|
||||||
import { FocusService } from '@core/focus.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. */
|
/** Rough per-item metrics in REM, used only to decide whether the menu must
|
||||||
const ITEM_H = 26;
|
* flip. Converted through remPx() at open time so the math tracks the same
|
||||||
const SEPARATOR_H = 7;
|
* root font-size the CSS scales with — the old px constants (`MENU_W = 224`)
|
||||||
const MENU_W = 224; // w-56
|
* were 16px-root mirrors of rem classes, so the box grew with the interface
|
||||||
const EDGE_GAP = 6;
|
* scale while the flip math still believed 224 and fired at the wrong edge. */
|
||||||
|
const ITEM_H_REM = 1.625; // px-3 py-1 row + --path-font line ≈ 26px @16
|
||||||
|
const SEPARATOR_H_REM = 0.4375; // my-1 + 1px border ≈ 7px @16
|
||||||
|
const MENU_W_REM = 14; // w-56
|
||||||
|
const EDGE_GAP = 6; // px on purpose: a viewport-edge margin, not a UI size
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The panel right-click menu. Replaces electrobun's native ContextMenu, which
|
* The panel right-click menu. Replaces electrobun's native ContextMenu, which
|
||||||
@@ -81,16 +86,20 @@ export class ContextMenu {
|
|||||||
|
|
||||||
readonly activeIndex = signal(-1);
|
readonly activeIndex = signal(-1);
|
||||||
|
|
||||||
/** Click point, flipped/clamped so the menu never overflows the viewport. */
|
/** Click point, flipped/clamped so the menu never overflows the viewport.
|
||||||
|
* All UI metrics convert through the live root font-size; window.inner* is
|
||||||
|
* viewport CSS px, so both sides of every comparison share units. */
|
||||||
readonly position = computed(() => {
|
readonly position = computed(() => {
|
||||||
const req = this.menu.request();
|
const req = this.menu.request();
|
||||||
if (!req) return { x: 0, y: 0 };
|
if (!req) return { x: 0, y: 0 };
|
||||||
|
const itemH = remPx(ITEM_H_REM);
|
||||||
|
const separatorH = remPx(SEPARATOR_H_REM);
|
||||||
|
const menuW = remPx(MENU_W_REM);
|
||||||
const height =
|
const height =
|
||||||
req.items.reduce((sum, i) => sum + (i.type === 'separator' ? SEPARATOR_H : ITEM_H), 0) + 8;
|
req.items.reduce((sum, i) => sum + (i.type === 'separator' ? separatorH : itemH), 0) +
|
||||||
|
remPx(0.5); // the menu's py-1 (0.25rem x2)
|
||||||
const x =
|
const x =
|
||||||
req.x + MENU_W > window.innerWidth - EDGE_GAP
|
req.x + menuW > window.innerWidth - EDGE_GAP ? Math.max(EDGE_GAP, req.x - menuW) : req.x;
|
||||||
? Math.max(EDGE_GAP, req.x - MENU_W)
|
|
||||||
: req.x;
|
|
||||||
const y =
|
const y =
|
||||||
req.y + height > window.innerHeight - EDGE_GAP
|
req.y + height > window.innerHeight - EDGE_GAP
|
||||||
? Math.max(EDGE_GAP, req.y - height)
|
? Math.max(EDGE_GAP, req.y - height)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { FocusService } from '@core/focus.service';
|
|||||||
</h2>
|
</h2>
|
||||||
@if (request().total > 1) {
|
@if (request().total > 1) {
|
||||||
<span
|
<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 }) }}
|
{{ i18n.t('dialogs.ofTotal', { index: request().index, total: request().total }) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -49,11 +49,11 @@ import { FocusService } from '@core/focus.service';
|
|||||||
@for (side of comparison(); track side.label) {
|
@for (side of comparison(); track side.label) {
|
||||||
<div class="rounded-md border border-border p-2.5">
|
<div class="rounded-md border border-border p-2.5">
|
||||||
<div class="flex items-center justify-between">
|
<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) }}
|
{{ i18n.t('dialogs.' + side.label) }}
|
||||||
</span>
|
</span>
|
||||||
@if (side.newer) {
|
@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') }}
|
{{ i18n.t('dialogs.newer') }}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ import { FocusService } from '@core/focus.service';
|
|||||||
{{ isDir() ? '—' : i18n.size(side.entry.size) }}
|
{{ isDir() ? '—' : i18n.size(side.entry.size) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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) }}
|
{{ i18n.date(side.entry.mtime) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,12 +24,12 @@
|
|||||||
<h1 class="min-w-0 truncate font-mono text-sm font-semibold">
|
<h1 class="min-w-0 truncate font-mono text-sm font-semibold">
|
||||||
{{ ctx.name }}{{ dirty() ? ' •' : '' }}
|
{{ ctx.name }}{{ dirty() ? ' •' : '' }}
|
||||||
</h1>
|
</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>
|
<div class="flex-1"></div>
|
||||||
<button
|
<button
|
||||||
(click)="toggleWrap()"
|
(click)="toggleWrap()"
|
||||||
[title]="i18n.t('editor.wrapHint', { alt: altLabel })"
|
[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.bg-accent]="wrap()"
|
||||||
[class.text-foreground]="wrap()"
|
[class.text-foreground]="wrap()"
|
||||||
[class.text-muted-foreground]="!wrap()"
|
[class.text-muted-foreground]="!wrap()"
|
||||||
@@ -58,7 +58,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"
|
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
|
<span
|
||||||
class="text-[11px] tabular-nums"
|
class="text-[0.6875rem] tabular-nums"
|
||||||
[class.text-destructive]="findQuery() && !matches().length"
|
[class.text-destructive]="findQuery() && !matches().length"
|
||||||
[class.text-muted-foreground]="!findQuery() || matches().length"
|
[class.text-muted-foreground]="!findQuery() || matches().length"
|
||||||
>
|
>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
|
|
||||||
@if (sessionDead()) {
|
@if (sessionDead()) {
|
||||||
<div
|
<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" />
|
<ng-icon name="mynaXCircle" size="0.85rem" class="shrink-0" />
|
||||||
{{ i18n.t('editor.disconnected', { name: ctx.sessionTitle }) }}
|
{{ i18n.t('editor.disconnected', { name: ctx.sessionTitle }) }}
|
||||||
@@ -141,9 +141,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="flex shrink-0 items-center gap-2 border-t border-border bg-chrome px-4 py-2">
|
<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-[0.6875rem] text-muted-foreground">{{ i18n.t('editor.shortcuts', { mod: modLabel }) }}</span>
|
||||||
<span class="text-[11px] text-muted-foreground">·</span>
|
<span class="text-[0.6875rem] text-muted-foreground">·</span>
|
||||||
<span class="text-[11px] tabular-nums text-muted-foreground">
|
<span class="text-[0.6875rem] tabular-nums text-muted-foreground">
|
||||||
{{ i18n.t('editor.lines', { count: lineCount() }) }}
|
{{ i18n.t('editor.lines', { count: lineCount() }) }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export class EditorWindow {
|
|||||||
private readonly gutter = viewChild<ElementRef<HTMLElement>>('gutter');
|
private readonly gutter = viewChild<ElementRef<HTMLElement>>('gutter');
|
||||||
private readonly findBox = viewChild<ElementRef<HTMLInputElement>>('findBox');
|
private readonly findBox = viewChild<ElementRef<HTMLInputElement>>('findBox');
|
||||||
private cachedCharWidth = 0;
|
private cachedCharWidth = 0;
|
||||||
|
private cachedCharFont = '';
|
||||||
private readonly rpc = inject(RpcService);
|
private readonly rpc = inject(RpcService);
|
||||||
private readonly toast = inject(ToastService);
|
private readonly toast = inject(ToastService);
|
||||||
private readonly document = inject(DOCUMENT);
|
private readonly document = inject(DOCUMENT);
|
||||||
@@ -214,7 +215,11 @@ export class EditorWindow {
|
|||||||
el.setSelectionRange(start, start + this.findQuery().length);
|
el.setSelectionRange(start, start + this.findQuery().length);
|
||||||
|
|
||||||
const text = this.text();
|
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 line = 0;
|
||||||
let i = text.indexOf('\n');
|
let i = text.indexOf('\n');
|
||||||
while (i !== -1 && i < start) {
|
while (i !== -1 && i < start) {
|
||||||
@@ -229,12 +234,17 @@ export class EditorWindow {
|
|||||||
this.onScroll();
|
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 {
|
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')!;
|
const canvas = this.document.createElement('canvas').getContext('2d')!;
|
||||||
canvas.font = getComputedStyle(el).font;
|
canvas.font = font;
|
||||||
this.cachedCharWidth = canvas.measureText('0').width || 7;
|
this.cachedCharWidth = canvas.measureText('0').width || parseFloat(getComputedStyle(el).fontSize) * 0.6;
|
||||||
|
this.cachedCharFont = font;
|
||||||
}
|
}
|
||||||
return this.cachedCharWidth;
|
return this.cachedCharWidth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@for (group of connections.grouped().stacks; track group.stack.id) {
|
@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 }}
|
{{ group.stack.name }}
|
||||||
</div>
|
</div>
|
||||||
@for (connection of group.connections; track connection.id) {
|
@for (connection of group.connections; track connection.id) {
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
|
|
||||||
@if (connections.sshHosts().length) {
|
@if (connections.sshHosts().length) {
|
||||||
<div class="my-1 border-t border-border"></div>
|
<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
|
~/.ssh/config
|
||||||
</div>
|
</div>
|
||||||
@for (host of connections.sshHosts(); track host.name) {
|
@for (host of connections.sshHosts(); track host.name) {
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
<div
|
<div
|
||||||
(contextmenu)="headerContextMenu($event)"
|
(contextmenu)="headerContextMenu($event)"
|
||||||
[style.grid-template-columns]="gridTemplate()"
|
[style.grid-template-columns]="gridTemplate()"
|
||||||
class="grid border-b border-border text-[11px] font-medium text-muted-foreground"
|
class="grid border-b border-border text-[0.6875rem] font-medium text-muted-foreground"
|
||||||
>
|
>
|
||||||
<button class="px-2 py-1 text-left hover:text-foreground" (click)="panel().toggleSort('name')">
|
<button class="px-2 py-1 text-left hover:text-foreground" (click)="panel().toggleSort('name')">
|
||||||
{{ i18n.t('columns.name') }} {{ sortIndicator('name') }}
|
{{ i18n.t('columns.name') }} {{ sortIndicator('name') }}
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
<div class="flex items-center gap-1.5 border-b border-border bg-accent/50 px-2 py-1">
|
<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" />
|
<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="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() }) }}
|
{{ i18n.t('panel.matches', { count: filterMatches() }) }}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { TransfersService } from '@core/transfers.service';
|
|||||||
imports: [NgIcon],
|
imports: [NgIcon],
|
||||||
template: `
|
template: `
|
||||||
<div
|
<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 -->
|
<!-- transfers summary; click toggles the detailed list -->
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
(click)="onUpdateClick()"
|
(click)="onUpdateClick()"
|
||||||
[disabled]="updater.state() === 'downloading'"
|
[disabled]="updater.state() === 'downloading'"
|
||||||
[title]="i18n.t('tabBar.updateTooltip', { version: updater.version() })"
|
[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') {
|
@if (updater.state() === 'downloading') {
|
||||||
<span
|
<span
|
||||||
@@ -142,7 +142,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
<button
|
<button
|
||||||
(click)="openManager()"
|
(click)="openManager()"
|
||||||
[title]="i18n.t('tabBar.manageConnections')"
|
[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" /> -->
|
<!-- <ng-icon name="mynaCog" size="0.9rem" /> -->
|
||||||
{{ i18n.t('tabBar.connections') }}
|
{{ i18n.t('tabBar.connections') }}
|
||||||
@@ -168,7 +168,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
<span class="text-xs text-muted-foreground">{{ i18n.t('options.language') }}</span>
|
<span class="text-xs text-muted-foreground">{{ i18n.t('options.language') }}</span>
|
||||||
<select
|
<select
|
||||||
(change)="settings.setLocale($any($event.target).value)"
|
(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'">
|
<option value="system" [selected]="settings.locale() === 'system'">
|
||||||
{{ i18n.t('options.system') }}
|
{{ i18n.t('options.system') }}
|
||||||
@@ -195,7 +195,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
<button
|
<button
|
||||||
(click)="settings.setSkin(s)"
|
(click)="settings.setSkin(s)"
|
||||||
[title]="i18n.t('options.skin.restartHint')"
|
[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.bg-accent]="settings.skin() === s"
|
||||||
[class.text-foreground]="settings.skin() === s"
|
[class.text-foreground]="settings.skin() === s"
|
||||||
[class.text-muted-foreground]="settings.skin() !== s"
|
[class.text-muted-foreground]="settings.skin() !== s"
|
||||||
@@ -206,7 +206,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (skinPending()) {
|
@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') }}
|
{{ i18n.t('options.skin.restartHint') }}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
@@ -217,7 +217,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
@for (theme of themes; track theme) {
|
@for (theme of themes; track theme) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setTheme(theme)"
|
(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.bg-accent]="settings.theme() === theme"
|
||||||
[class.text-foreground]="settings.theme() === theme"
|
[class.text-foreground]="settings.theme() === theme"
|
||||||
[class.text-muted-foreground]="settings.theme() !== theme"
|
[class.text-muted-foreground]="settings.theme() !== theme"
|
||||||
@@ -235,7 +235,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
@for (tone of darkTones; track tone) {
|
@for (tone of darkTones; track tone) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setDarkTone(tone)"
|
(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.bg-accent]="settings.darkTone() === tone"
|
||||||
[class.text-foreground]="settings.darkTone() === tone"
|
[class.text-foreground]="settings.darkTone() === tone"
|
||||||
[class.text-muted-foreground]="settings.darkTone() !== tone"
|
[class.text-muted-foreground]="settings.darkTone() !== tone"
|
||||||
@@ -271,7 +271,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
@for (mode of iconColorModes; track mode) {
|
@for (mode of iconColorModes; track mode) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setIconColors(mode)"
|
(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.bg-accent]="settings.iconColors() === mode"
|
||||||
[class.text-foreground]="settings.iconColors() === mode"
|
[class.text-foreground]="settings.iconColors() === mode"
|
||||||
[class.text-muted-foreground]="settings.iconColors() !== mode"
|
[class.text-muted-foreground]="settings.iconColors() !== mode"
|
||||||
@@ -288,7 +288,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
@for (size of textSizes; track size) {
|
@for (size of textSizes; track size) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setTextSize(size)"
|
(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.bg-accent]="settings.textSize() === size"
|
||||||
[class.text-foreground]="settings.textSize() === size"
|
[class.text-foreground]="settings.textSize() === size"
|
||||||
[class.text-muted-foreground]="settings.textSize() !== size"
|
[class.text-muted-foreground]="settings.textSize() !== size"
|
||||||
@@ -305,7 +305,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
@for (unit of speedUnits; track unit.value) {
|
@for (unit of speedUnits; track unit.value) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setSpeedUnit(unit.value)"
|
(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.bg-accent]="settings.speedUnit() === unit.value"
|
||||||
[class.text-foreground]="settings.speedUnit() === unit.value"
|
[class.text-foreground]="settings.speedUnit() === unit.value"
|
||||||
[class.text-muted-foreground]="settings.speedUnit() !== unit.value"
|
[class.text-muted-foreground]="settings.speedUnit() !== unit.value"
|
||||||
@@ -322,7 +322,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
@for (mode of transferModes; track mode) {
|
@for (mode of transferModes; track mode) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setTransferMode(mode)"
|
(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.bg-accent]="settings.transferMode() === mode"
|
||||||
[class.text-foreground]="settings.transferMode() === mode"
|
[class.text-foreground]="settings.transferMode() === mode"
|
||||||
[class.text-muted-foreground]="settings.transferMode() !== mode"
|
[class.text-muted-foreground]="settings.transferMode() !== mode"
|
||||||
@@ -340,7 +340,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
@for (count of concurrencyChoices; track count) {
|
@for (count of concurrencyChoices; track count) {
|
||||||
<button
|
<button
|
||||||
(click)="settings.setTransferConcurrency(count)"
|
(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.bg-accent]="settings.transferConcurrency() === count"
|
||||||
[class.text-foreground]="settings.transferConcurrency() === count"
|
[class.text-foreground]="settings.transferConcurrency() === count"
|
||||||
[class.text-muted-foreground]="settings.transferConcurrency() !== count"
|
[class.text-muted-foreground]="settings.transferConcurrency() !== count"
|
||||||
@@ -357,7 +357,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
<div class="flex rounded-md border border-border p-0.5">
|
<div class="flex rounded-md border border-border p-0.5">
|
||||||
<button
|
<button
|
||||||
(click)="settings.setEditor('builtin')"
|
(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.bg-accent]="settings.editor() === 'builtin'"
|
||||||
[class.text-foreground]="settings.editor() === 'builtin'"
|
[class.text-foreground]="settings.editor() === 'builtin'"
|
||||||
[class.text-muted-foreground]="settings.editor() !== 'builtin'"
|
[class.text-muted-foreground]="settings.editor() !== 'builtin'"
|
||||||
@@ -366,7 +366,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="settings.setEditor('custom')"
|
(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.bg-accent]="settings.editor() === 'custom'"
|
||||||
[class.text-foreground]="settings.editor() === 'custom'"
|
[class.text-foreground]="settings.editor() === 'custom'"
|
||||||
[class.text-muted-foreground]="settings.editor() !== 'custom'"
|
[class.text-muted-foreground]="settings.editor() !== 'custom'"
|
||||||
@@ -384,7 +384,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
<div class="flex shrink-0 items-center gap-1">
|
<div class="flex shrink-0 items-center gap-1">
|
||||||
<button
|
<button
|
||||||
(click)="pickEditorApp()"
|
(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') }}
|
{{ i18n.t('common.choose') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -406,7 +406,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
<div class="flex rounded-md border border-border p-0.5">
|
<div class="flex rounded-md border border-border p-0.5">
|
||||||
<button
|
<button
|
||||||
(click)="settings.setAutoClearCompleted(true)"
|
(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.bg-accent]="settings.autoClearCompleted()"
|
||||||
[class.text-foreground]="settings.autoClearCompleted()"
|
[class.text-foreground]="settings.autoClearCompleted()"
|
||||||
[class.text-muted-foreground]="!settings.autoClearCompleted()"
|
[class.text-muted-foreground]="!settings.autoClearCompleted()"
|
||||||
@@ -415,7 +415,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="settings.setAutoClearCompleted(false)"
|
(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.bg-accent]="!settings.autoClearCompleted()"
|
||||||
[class.text-foreground]="!settings.autoClearCompleted()"
|
[class.text-foreground]="!settings.autoClearCompleted()"
|
||||||
[class.text-muted-foreground]="settings.autoClearCompleted()"
|
[class.text-muted-foreground]="settings.autoClearCompleted()"
|
||||||
@@ -430,11 +430,11 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
<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">Auto-refresh</span>
|
<span class="text-xs text-muted-foreground">Auto-refresh</span>
|
||||||
<div class="flex rounded-md border border-border p-0.5">
|
<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.bg-accent]="settings.autoRefresh()"
|
||||||
[class.text-foreground]="settings.autoRefresh()"
|
[class.text-foreground]="settings.autoRefresh()"
|
||||||
[class.text-muted-foreground]="!settings.autoRefresh()">On</button>
|
[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.bg-accent]="!settings.autoRefresh()"
|
||||||
[class.text-foreground]="!settings.autoRefresh()"
|
[class.text-foreground]="!settings.autoRefresh()"
|
||||||
[class.text-muted-foreground]="settings.autoRefresh()">Off</button>
|
[class.text-muted-foreground]="settings.autoRefresh()">Off</button>
|
||||||
@@ -445,7 +445,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
<!-- about footer -->
|
<!-- about footer -->
|
||||||
<button
|
<button
|
||||||
(click)="showAbout()"
|
(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 }}
|
{{ appName }} · {{ buildLabel }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@if (transfers.expanded() && transfers.hasJobs()) {
|
@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>
|
<span>
|
||||||
{{ i18n.t('transfer.summary', { active: transfers.activeJobs().length, finished: transfers.finishedJobs().length }) }}
|
{{ i18n.t('transfer.summary', { active: transfers.activeJobs().length, finished: transfers.finishedJobs().length }) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<ng-icon [name]="stateIcon(job)" size="0.95rem" [class]="stateClass(job)" />
|
<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>
|
<span class="w-40 shrink-0 truncate font-medium" [title]="job.srcPath">{{ job.name }}</span>
|
||||||
<div class="min-w-0 flex-1">
|
<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)) {
|
@if (isActive(job)) {
|
||||||
<div class="mt-0.5 h-1 overflow-hidden rounded-full bg-muted">
|
<div class="mt-0.5 h-1 overflow-hidden rounded-full bg-muted">
|
||||||
<div
|
<div
|
||||||
|
|||||||
+21
-33
@@ -181,45 +181,33 @@
|
|||||||
--color-icon-media: var(--icon-media);
|
--color-icon-media: var(--icon-media);
|
||||||
--color-icon-archive: var(--icon-archive);
|
--color-icon-archive: var(--icon-archive);
|
||||||
--color-icon-code: var(--icon-code);
|
--color-icon-code: var(--icon-code);
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
/* rem subtrahends (4px/2px at the 16px root), NOT px: --radius is rem, and a
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
mixed-unit calc would stop the derived radii from following the interface
|
||||||
|
scale — corners would sharpen as the UI grew. */
|
||||||
|
--radius-sm: calc(var(--radius) - 0.25rem);
|
||||||
|
--radius-md: calc(var(--radius) - 0.125rem);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Interface-size setting (data-text set by SettingsService). Scales the whole
|
/* Panel-chrome metrics: file list, path bar, action buttons, source picker.
|
||||||
panel chrome: file list, path bar, action buttons, and the source picker
|
ALL REM, deliberately — the interface scale (settings.uiScale) works by
|
||||||
with its dropdown. The tab bar (including the options button) stays fixed.
|
setting the root font-size, so every rem value in the app scales from that
|
||||||
Default-tier values match the old hardcoded classes exactly. */
|
one lever. A px value here would freeze while everything around it grew,
|
||||||
|
which is exactly what happened to the win7 boxes before stage 1c. There is
|
||||||
|
only one tier now: the old data-text compact/large tiers are gone, replaced
|
||||||
|
by the continuous scale. Values are the old default tier verbatim
|
||||||
|
(13px -> 0.8125rem etc. at the 16px root), so scale 1.0 is pixel-identical
|
||||||
|
to the pre-scale app. */
|
||||||
:root {
|
:root {
|
||||||
--list-font: 13px;
|
--list-font: 0.8125rem;
|
||||||
--list-meta-font: 12px;
|
--list-meta-font: 0.75rem;
|
||||||
--path-font: 12px;
|
--path-font: 0.75rem;
|
||||||
--row-h: 28px;
|
--row-h: 1.75rem;
|
||||||
--path-ctl-h: 28px;
|
--path-ctl-h: 1.75rem;
|
||||||
--action-h: 24px;
|
--action-h: 1.5rem;
|
||||||
--action-font: 11px;
|
--action-font: 0.6875rem;
|
||||||
--action-icon: 0.9rem;
|
--action-icon: 0.9rem;
|
||||||
}
|
}
|
||||||
:root[data-text='compact'] {
|
|
||||||
--list-font: 12px;
|
|
||||||
--list-meta-font: 11px;
|
|
||||||
--path-font: 11px;
|
|
||||||
--row-h: 24px;
|
|
||||||
--path-ctl-h: 24px;
|
|
||||||
--action-h: 22px;
|
|
||||||
--action-font: 10px;
|
|
||||||
--action-icon: 0.85rem;
|
|
||||||
}
|
|
||||||
:root[data-text='large'] {
|
|
||||||
--list-font: 14px;
|
|
||||||
--list-meta-font: 13px;
|
|
||||||
--path-font: 13px;
|
|
||||||
--row-h: 32px;
|
|
||||||
--path-ctl-h: 32px;
|
|
||||||
--action-h: 28px;
|
|
||||||
--action-font: 12px;
|
|
||||||
--action-icon: 1.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Soft entrance for the panel busy states (connecting placeholder, load overlay). */
|
/* Soft entrance for the panel busy states (connecting placeholder, load overlay). */
|
||||||
@keyframes panel-fade-in {
|
@keyframes panel-fade-in {
|
||||||
|
|||||||
Reference in New Issue
Block a user