Name every icon by the pixel grid it is drawn for

Groundwork for redrawing the icons on their pixel grids. A name is now
<icon><px> (server14, server16, server24) and must match the size token
where it renders: 14 at --icon-size-sm, 16 at -md, 24 at -xl.
tdesignLoading is the one exception, since it spins. Every name not yet
redrawn aliases its TDesign glyph, so nothing changes on screen except
the manager-open overlay icon, which moves from lg to xl ("a status
mark on a card").

- pixel-icons.ts holds the redrawn glyphs, so far add14 (the + from
  3f7145d, renamed from pixelAdd).
- TS code that picks icons takes Icon14 / Icon16 (menu items, file
  glyphs, the copy arrow, toasts, transfer states), so build:ui
  rejects a wrong name.
- icons.spec.ts checks every <ng-icon> in the skins: it has a size
  token, and each literal name, both sides of a [name] ternary
  included, is registered and drawn for that token's grid.
- TDesign's file1 is now fileText, since file1 plus a suffix would
  read file114.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-10-09 13:24:38 +02:00
co-authored by Claude
parent 3f7145d69a
commit 5d23809d48
21 changed files with 208 additions and 138 deletions
+9 -5
View File
@@ -372,11 +372,15 @@ component; tokens → `ui/src/styles.css`.
walker check the flag themselves. walker check the flag themselves.
- `file-icons.ts` — extension → category map driving glyphs, colors - `file-icons.ts` — extension → category map driving glyphs, colors
(fixed `--icon-*` tokens from Oxide's hue scales) and the Kind column. (fixed `--icon-*` tokens from Oxide's hue scales) and the Kind column.
- `icons.ts` — the one icon registry (TDesign, `@ng-icons/tdesign-icons`, plus one - `icons.ts` — the one icon registry, passed to `provideIcons` in `app.config.ts`.
redrawn glyph: `pixelAdd`, TDesign's + on the 14px pixel grid so it stays sharp A name is `<icon><px>`: the pixel grid its glyph is drawn for, which must match
on 1x screens), passed to `provideIcons` in `app.config.ts`. Icon names are plain strings in templates and the size token where it renders (`server14` at `--icon-size-sm`, `server16` at
the build never checks them: an unregistered name renders nothing (console error `-md`, `server24` at `-xl`). The one exception is `tdesignLoading`, which spins.
only), so every name used anywhere must be listed here. `pixel-icons.ts` holds the glyphs redrawn on their grids, and names not yet
redrawn alias TDesign's (`@ng-icons/tdesign-icons`). Templates can't type-check
icon names, and an unregistered one renders nothing (a console warning only), so
`icons.spec.ts` checks every `<ng-icon>` in the skins. TS code that picks an icon
takes the `Icon14` / `Icon16` types.
- `platform.ts` — preload-global readers (`PLATFORM`, `APP_MODE`, - `platform.ts` — preload-global readers (`PLATFORM`, `APP_MODE`,
`EDITOR_CONTEXT`, `IS_MAC`/`IS_MAC_CHROME`, `IS_WINDOWS`, `IS_LINUX`) + `EDITOR_CONTEXT`, `IS_MAC`/`IS_MAC_CHROME`, `IS_WINDOWS`, `IS_LINUX`) +
`blockNativeContextMenu` (kills WebKit's Reload/Inspect menu outside text `blockNativeContextMenu` (kills WebKit's Reload/Inspect menu outside text
+1 -1
View File
@@ -86,7 +86,7 @@ touching the adjacent code.
small size.** At 12 or 14px the centre is a pixel boundary, so a ~1px bar small size.** At 12 or 14px the centre is a pixel boundary, so a ~1px bar
covers two pixels at half ink. A 1px stroke alone doesn't help, because the covers two pixels at half ink. A 1px stroke alone doesn't help, because the
bar stays centred on the boundary. This is geometry, not Electrobun: bar stays centred on the boundary. This is geometry, not Electrobun:
headless Chrome renders the same. So the + is `pixelAdd` (`icons.ts`), headless Chrome renders the same. So the + is `add14` (`pixel-icons.ts`),
redrawn on the 14px grid with 1px bars at 7.5, which fill whole pixels at 1x redrawn on the 14px grid with 1px bars at 7.5, which fill whole pixels at 1x
and at 2x. It's tuned for `--icon-size-sm` only. Arrow shafts (↑ →) have and at 2x. It's tuned for `--icon-size-sm` only. Arrow shafts (↑ →) have
the same issue if it ever shows. the same issue if it ever shows.
+10 -9
View File
@@ -1,6 +1,7 @@
import { Injectable, inject, signal } from '@angular/core'; import { Injectable, inject, signal } from '@angular/core';
import type { ColumnKey, EntryType } from '@shared/types'; import type { ColumnKey, EntryType } from '@shared/types';
import type { FileCommand } from './file-actions.service'; import type { FileCommand } from './file-actions.service';
import type { Icon14 } from './icons';
import { I18nService } from './i18n.service'; import { I18nService } from './i18n.service';
import { stepIndex } from './menu-nav'; import { stepIndex } from './menu-nav';
import type { Side } from './panels.service'; import type { Side } from './panels.service';
@@ -23,8 +24,8 @@ export interface MenuItem {
/** The keymap command the item runs: its first key shows as the soft hint, /** The keymap command the item runs: its first key shows as the soft hint,
* and pressing that key while the menu is open picks the item. */ * and pressing that key while the menu is open picks the item. */
shortcut?: ShortcutId; shortcut?: ShortcutId;
/** A registered icon (core/icons.ts), drawn in the check column. */ /** A 14px icon (core/icons.ts), drawn in the check column. */
icon?: string; icon?: Icon14;
/** Greyed and unclickable. Menus disable rather than hide, so the set of /** Greyed and unclickable. Menus disable rather than hide, so the set of
* commands stays in the same place from one right-click to the next. */ * commands stays in the same place from one right-click to the next. */
disabled?: boolean; disabled?: boolean;
@@ -123,7 +124,7 @@ export class ContextMenuService {
if (!ctx.hasEntry || ctx.name === '..') { if (!ctx.hasEntry || ctx.name === '..') {
// empty space (or the parent row): folder-level actions // empty space (or the parent row): folder-level actions
items.push(cmd('new-folder', t('ctx.newFolder'), { icon: 'tdesignFolderAdd' })); items.push(cmd('new-folder', t('ctx.newFolder'), { icon: 'folderAdd14' }));
items.push(cmd('new-file', t('ctx.newFile'))); items.push(cmd('new-file', t('ctx.newFile')));
items.push({ type: 'separator' }); items.push({ type: 'separator' });
items.push(cmd('refresh', t('ctx.refresh'))); items.push(cmd('refresh', t('ctx.refresh')));
@@ -135,7 +136,7 @@ export class ContextMenuService {
} }
// the arrow points at the other panel, as on the toolbar's Send button // the arrow points at the other panel, as on the toolbar's Send button
const sendIcon = ctx.side === 'left' ? 'tdesignArrowRight' : 'tdesignArrowLeft'; const sendIcon = ctx.side === 'left' ? 'arrowRight14' : 'arrowLeft14';
items.push(cmd('send', t('ctx.copyToOther'), { icon: sendIcon })); items.push(cmd('send', t('ctx.copyToOther'), { icon: sendIcon }));
if (ctx.type === 'dir') { if (ctx.type === 'dir') {
items.push(cmd('open', t('ctx.open'))); items.push(cmd('open', t('ctx.open')));
@@ -143,9 +144,9 @@ export class ContextMenuService {
if (ctx.isLocal) { if (ctx.isLocal) {
items.push(cmd('open-default', t('ctx.open'), { tooltip: t('ctx.openDefault') })); items.push(cmd('open-default', t('ctx.open'), { tooltip: t('ctx.openDefault') }));
} }
items.push(cmd('edit', t('ctx.edit'), { icon: 'tdesignFile1' })); items.push(cmd('edit', t('ctx.edit'), { icon: 'fileText14' }));
} }
items.push(cmd('rename', t('ctx.rename'), { icon: 'tdesignEdit' })); items.push(cmd('rename', t('ctx.rename'), { icon: 'edit14' }));
// chmod on Windows local files only flips the read-only bit — hide the lie // chmod on Windows local files only flips the read-only bit — hide the lie
if (!(ctx.isLocal && IS_WINDOWS)) { if (!(ctx.isLocal && IS_WINDOWS)) {
items.push(cmd('chmod', t('ctx.permissions'))); items.push(cmd('chmod', t('ctx.permissions')));
@@ -159,7 +160,7 @@ export class ContextMenuService {
if (ctx.isLocal) { if (ctx.isLocal) {
items.push(cmd('trash', t(trashKey()), IS_MAC ? { tooltip: t('ctx.trashTooltip') } : {})); items.push(cmd('trash', t(trashKey()), IS_MAC ? { tooltip: t('ctx.trashTooltip') } : {}));
} }
items.push(cmd('delete', t('ctx.delete'), { icon: 'tdesignDelete' })); items.push(cmd('delete', t('ctx.delete'), { icon: 'delete14' }));
return items; return items;
} }
@@ -180,13 +181,13 @@ export class ContextMenuService {
const t = (key: string) => this.i18n.t(key); const t = (key: string) => this.i18n.t(key);
const last = ctx.tabCount <= 1; const last = ctx.tabCount <= 1;
return [ return [
{ label: t('ctx.tabNew'), action: 'tab:new', shortcut: 'new-tab', icon: 'pixelAdd' }, { label: t('ctx.tabNew'), action: 'tab:new', shortcut: 'new-tab', icon: 'add14' },
{ type: 'separator' }, { type: 'separator' },
{ {
label: t('ctx.tabClose'), label: t('ctx.tabClose'),
action: `tab:close:${ctx.id}`, action: `tab:close:${ctx.id}`,
shortcut: 'close-tab', shortcut: 'close-tab',
icon: 'tdesignClose', icon: 'close14',
disabled: last, disabled: last,
}, },
{ {
+13 -12
View File
@@ -3,6 +3,7 @@
// Only the icon glyph is colored; row text stays neutral. Docs and unknown // Only the icon glyph is colored; row text stays neutral. Docs and unknown
// files stay muted on purpose — sparing color is what keeps it scannable. // files stay muted on purpose — sparing color is what keeps it scannable.
import type { FileEntry } from '@shared/types'; import type { FileEntry } from '@shared/types';
import type { Icon16 } from './icons';
type Category = 'image' | 'audio' | 'video' | 'archive' | 'code' | 'doc'; type Category = 'image' | 'audio' | 'video' | 'archive' | 'code' | 'doc';
@@ -11,29 +12,29 @@ const MUTED = 'shrink-0 text-muted-foreground';
const FOLDER = 'shrink-0 text-icon-folder'; const FOLDER = 'shrink-0 text-icon-folder';
const LINK = 'shrink-0 text-icon-link'; const LINK = 'shrink-0 text-icon-link';
const CATEGORIES: Record<Category, { glyph: string; class: string; extensions: string[] }> = { const CATEGORIES: Record<Category, { glyph: Icon16; class: string; extensions: string[] }> = {
image: { image: {
glyph: 'tdesignImage', glyph: 'image16',
class: 'shrink-0 text-icon-image', class: 'shrink-0 text-icon-image',
extensions: 'png jpg jpeg gif webp avif svg ico icns bmp tiff tif heic heif raw'.split(' '), extensions: 'png jpg jpeg gif webp avif svg ico icns bmp tiff tif heic heif raw'.split(' '),
}, },
audio: { audio: {
glyph: 'tdesignMusic', glyph: 'music16',
class: 'shrink-0 text-icon-media', class: 'shrink-0 text-icon-media',
extensions: 'mp3 wav flac aac ogg oga m4a opus wma aiff aif mid midi'.split(' '), extensions: 'mp3 wav flac aac ogg oga m4a opus wma aiff aif mid midi'.split(' '),
}, },
video: { video: {
glyph: 'tdesignVideo', glyph: 'video16',
class: 'shrink-0 text-icon-media', class: 'shrink-0 text-icon-media',
extensions: 'mp4 mkv mov avi webm m4v mpg mpeg wmv flv 3gp'.split(' '), extensions: 'mp4 mkv mov avi webm m4v mpg mpeg wmv flv 3gp'.split(' '),
}, },
archive: { archive: {
glyph: 'tdesignFileZip', glyph: 'fileZip16',
class: 'shrink-0 text-icon-archive', class: 'shrink-0 text-icon-archive',
extensions: 'zip tar gz tgz bz2 xz zst 7z rar dmg iso jar deb rpm pkg apk'.split(' '), extensions: 'zip tar gz tgz bz2 xz zst 7z rar dmg iso jar deb rpm pkg apk'.split(' '),
}, },
code: { code: {
glyph: 'tdesignCode', glyph: 'code16',
class: 'shrink-0 text-icon-code', class: 'shrink-0 text-icon-code',
// ".ts" is TypeScript here, not an MPEG transport stream (deliberate) // ".ts" is TypeScript here, not an MPEG transport stream (deliberate)
extensions: ( extensions: (
@@ -45,7 +46,7 @@ const CATEGORIES: Record<Category, { glyph: string; class: string; extensions: s
).split(' '), ).split(' '),
}, },
doc: { doc: {
glyph: 'tdesignFile1', glyph: 'fileText16',
class: MUTED, // docs keep the neutral color; only the glyph changes class: MUTED, // docs keep the neutral color; only the glyph changes
extensions: 'md markdown txt rtf pdf doc docx xls xlsx ppt pptx csv tsv log odt ods odp epub'.split(' '), extensions: 'md markdown txt rtf pdf doc docx xls xlsx ppt pptx csv tsv log odt ods odp epub'.split(' '),
}, },
@@ -84,12 +85,12 @@ export function fileKindKey(entry: FileEntry): string {
return category ? KIND_KEYS[category] : 'file'; return category ? KIND_KEYS[category] : 'file';
} }
export function fileGlyph(entry: FileEntry): string { export function fileGlyph(entry: FileEntry): Icon16 {
if (entry.name === '..') return 'tdesignArrowUp'; if (entry.name === '..') return 'arrowUp16';
if (entry.type === 'dir') return 'tdesignFolder'; if (entry.type === 'dir') return 'folder16';
if (entry.type === 'symlink') return 'tdesignLink'; if (entry.type === 'symlink') return 'link16';
const category = categoryOf(entry.name); const category = categoryOf(entry.name);
return category ? CATEGORIES[category].glyph : 'tdesignFile'; return category ? CATEGORIES[category].glyph : 'file16';
} }
export function fileIconClass(entry: FileEntry): string { export function fileIconClass(entry: FileEntry): string {
+33
View File
@@ -0,0 +1,33 @@
import { expect, test } from 'bun:test';
import { APP_ICONS } from './icons';
// The px grid of each size token (styles.css); lg only carries the spinner.
const GRID: Record<string, string> = { sm: '14', md: '16', lg: '20', xl: '24' };
const SKINS = `${import.meta.dir}/../skins`;
// Templates can't type-check icon names, so read them: every <ng-icon> has a
// size token, and every literal name (both sides of a [name] ternary too) is
// registered and drawn for that token's grid. Names computed in TS are typed.
test('every template icon is registered and drawn for the size it renders at', async () => {
const problems: string[] = [];
for (const file of new Bun.Glob('**/*.{html,ts}').scanSync(SKINS)) {
const src = (await Bun.file(`${SKINS}/${file}`).text()).replace(/<!--[\s\S]*?-->/g, '');
for (const [tag] of src.matchAll(/<ng-icon\b[^>]*>/g)) {
const token = tag.match(/size="var\(--icon-size-(\w+)\)"/)?.[1];
if (!token || !GRID[token]) {
problems.push(`${file}: no size token in ${tag}`);
continue;
}
const bound = tag.match(/\[name\]="([^"]*)"/)?.[1] ?? '';
// a ternary's results, not its condition ('local' in `id === 'local' ? …`)
const names = [...tag.matchAll(/\bname="(\w+)"/g), ...bound.matchAll(/[?:]\s*'(\w+)'/g)].map((m) => m[1]);
for (const name of names) {
if (!Object.hasOwn(APP_ICONS, name)) problems.push(`${file}: ${name} is not registered`);
else if (name !== 'tdesignLoading' && !name.endsWith(GRID[token])) {
problems.push(`${file}: ${name} renders at ${token} (${GRID[token]}px)`);
}
}
}
}
expect(problems).toEqual([]);
});
+51 -41
View File
@@ -1,7 +1,8 @@
// Single icon registry for the whole app, registered once in app.config.ts. // Single icon registry for the whole app, registered once in app.config.ts.
// TDesign's package root exports every icon; named imports tree-shake. Names are // A name is <icon><px>: the pixel grid the glyph is drawn for, which must match
// plain strings in templates — the build never checks them, so every name used // the size token where it renders (pixel-icons.ts). Templates can't type-check
// anywhere must be listed here (an unregistered one renders nothing). // names and an unregistered one renders nothing (a console warning only), so
// icons.spec.ts checks every template name; TS code takes the Icon14/Icon16 types.
import { import {
tdesignArrowLeft, tdesignArrowLeft,
tdesignArrowRight, tdesignArrowRight,
@@ -35,45 +36,54 @@ import {
tdesignSetting, tdesignSetting,
tdesignVideo, tdesignVideo,
} from "@ng-icons/tdesign-icons"; } from "@ng-icons/tdesign-icons";
import { PIXEL_ICONS } from "./pixel-icons";
// TDesign's + redrawn on the pixel grid of --icon-size-sm (14px). Its own glyph
// centres 24-grid strokes on the box's middle, a pixel boundary at 14px, so on a
// 1x screen both bars smear over two pixels at half ink. These bars are 1px wide
// on a half pixel (7.5): whole pixels at 1x and 2x, half a pixel off-centre.
const pixelAdd =
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14"><path stroke="currentColor" d="M7.5 3v9M3 7.5h9"/></svg>';
export const APP_ICONS = { export const APP_ICONS = {
pixelAdd, ...PIXEL_ICONS,
tdesignArrowLeft, // TDesign's glyphs under their grid names, until each is redrawn in pixel-icons.ts
tdesignArrowRight, arrowLeft14: tdesignArrowLeft,
tdesignArrowUp, arrowRight14: tdesignArrowRight,
tdesignCheck, check14: tdesignCheck,
tdesignCheckCircle, checkCircle14: tdesignCheckCircle,
tdesignChevronDown, chevronDown14: tdesignChevronDown,
tdesignChevronRight, chevronRight14: tdesignChevronRight,
tdesignChevronUp, chevronUp14: tdesignChevronUp,
tdesignClose, close14: tdesignClose,
tdesignCloseCircle, closeCircle14: tdesignCloseCircle,
tdesignCode, delete14: tdesignDelete,
tdesignDelete, download14: tdesignDownload,
tdesignDownload, edit14: tdesignEdit,
tdesignEdit, file14: tdesignFile,
tdesignFile, fileAdd14: tdesignFileAdd,
tdesignFile1, fileText14: tdesignFile1,
tdesignFileAdd, folder14: tdesignFolder,
tdesignFileZip, folderAdd14: tdesignFolderAdd,
tdesignFolder, hardDrive14: tdesignHardDrive,
tdesignFolderAdd, lockOn14: tdesignLockOn,
tdesignHardDrive, refresh14: tdesignRefresh,
tdesignImage, search14: tdesignSearch,
tdesignLink, server14: tdesignServer,
setting14: tdesignSetting,
arrowUp16: tdesignArrowUp,
code16: tdesignCode,
file16: tdesignFile,
fileText16: tdesignFile1,
fileZip16: tdesignFileZip,
folder16: tdesignFolder,
image16: tdesignImage,
link16: tdesignLink,
music16: tdesignMusic,
refresh16: tdesignRefresh,
server16: tdesignServer,
setting16: tdesignSetting,
video16: tdesignVideo,
server24: tdesignServer,
// TDesign's own 24px grid: as sharp as a circle and its diagonals can be
closeCircle24: tdesignCloseCircle,
// spins, so no pixel grid holds; any size
tdesignLoading, tdesignLoading,
tdesignLockOn,
tdesignMusic,
tdesignRefresh,
tdesignSearch,
tdesignServer,
tdesignSetting,
tdesignVideo,
}; };
export type IconName = keyof typeof APP_ICONS;
export type Icon14 = Extract<IconName, `${string}14`>;
export type Icon16 = Extract<IconName, `${string}16`>;
+16
View File
@@ -0,0 +1,16 @@
// Icons drawn on the pixel grid they render at. A TDesign glyph is a 24-unit
// SVG with 2-unit strokes on whole units, so at 14 or 16px every straight line
// lands between pixels and smears over two at half ink on a 1x screen. Each
// glyph here is drawn on its own grid (viewBox 0 0 N N for an N px icon, one
// unit = one CSS pixel at 100% scale) with 1px strokes on half pixels, so
// straight lines fill whole pixels at 1x and 2x. The name's suffix is that
// grid and must match the size token where it renders: 14 = --icon-size-sm,
// 16 = -md, 24 = -xl (icons.spec.ts checks). Drawing rules: docs/gotchas.md.
/** One glyph on an n-px grid: 1px square-capped strokes in the text colour. */
const svg = (n: number, body: string) =>
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n} ${n}" fill="none" stroke="currentColor" stroke-linecap="square">${body}</svg>`;
export const PIXEL_ICONS = {
add14: svg(14, '<path d="M7.5 3.5v8M3.5 7.5h8"/>'),
};
+2 -2
View File
@@ -2,7 +2,7 @@
@if (!rpc.available) { @if (!rpc.available) {
<div class="flex flex-1 items-center justify-center"> <div class="flex flex-1 items-center justify-center">
<div class="max-w-sm border border-border bg-card p-6 text-center shadow-sm"> <div class="max-w-sm border border-border bg-card p-6 text-center shadow-sm">
<ng-icon name="tdesignCloseCircle" size="var(--icon-size-xl)" class="text-destructive" /> <ng-icon name="closeCircle24" size="var(--icon-size-xl)" class="text-destructive" />
<p class="mt-2 text-sm font-medium">{{ i18n.t('app.notConnectedTitle') }}</p> <p class="mt-2 text-sm font-medium">{{ i18n.t('app.notConnectedTitle') }}</p>
<p class="mt-1 text-xs text-muted-foreground"> <p class="mt-1 text-xs text-muted-foreground">
{{ i18n.t('app.notConnectedBody') }} {{ i18n.t('app.notConnectedBody') }}
@@ -28,7 +28,7 @@
(mousedown)="focusManager()" (mousedown)="focusManager()"
> >
<div class="border border-border bg-card px-5 py-4 text-center shadow-xl"> <div class="border border-border bg-card px-5 py-4 text-center shadow-xl">
<ng-icon name="tdesignServer" size="var(--icon-size-lg)" class="text-muted-foreground" /> <ng-icon name="server24" size="var(--icon-size-xl)" class="text-muted-foreground" />
<p class="mt-1.5 text-xs text-muted-foreground">{{ i18n.t('app.managerOpen') }}</p> <p class="mt-1.5 text-xs text-muted-foreground">{{ i18n.t('app.managerOpen') }}</p>
<button <button
(click)="focusManager()" (click)="focusManager()"
@@ -9,7 +9,7 @@
[class.h-9]="!isMacChrome" [class.h-9]="!isMacChrome"
[class.pl-3]="!isMacChrome" [class.pl-3]="!isMacChrome"
> >
<ng-icon name="tdesignServer" size="var(--icon-size-md)" class="shrink-0 text-muted-foreground" /> <ng-icon name="server16" size="var(--icon-size-md)" class="shrink-0 text-muted-foreground" />
<h1 class="text-sm font-semibold">{{ i18n.t('connMgr.title') }}</h1> <h1 class="text-sm font-semibold">{{ i18n.t('connMgr.title') }}</h1>
<div class="flex-1"></div> <div class="flex-1"></div>
<button <button
@@ -36,14 +36,14 @@
[title]="shortcuts.tip(i18n.t('connMgr.newConnectionHint'), 'conn-new')" [title]="shortcuts.tip(i18n.t('connMgr.newConnectionHint'), 'conn-new')"
class="flex items-center gap-1 px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground" class="flex items-center gap-1 px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="pixelAdd" size="var(--icon-size-sm)" /> {{ i18n.t('connMgr.newConnection') }} <ng-icon name="add14" size="var(--icon-size-sm)" /> {{ i18n.t('connMgr.newConnection') }}
</button> </button>
<button <button
(click)="newStack()" (click)="newStack()"
[title]="shortcuts.tip(i18n.t('connMgr.newStackHint'), 'conn-new-stack')" [title]="shortcuts.tip(i18n.t('connMgr.newStackHint'), 'conn-new-stack')"
class="flex items-center gap-1 px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground" class="flex items-center gap-1 px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignFolderAdd" size="var(--icon-size-sm)" /> {{ i18n.t('connMgr.newStack') }} <ng-icon name="folderAdd14" size="var(--icon-size-sm)" /> {{ i18n.t('connMgr.newStack') }}
</button> </button>
</div> </div>
@@ -68,7 +68,7 @@
[title]="group.stack.collapsed ? i18n.t('connMgr.expand') : i18n.t('connMgr.collapse')" [title]="group.stack.collapsed ? i18n.t('connMgr.expand') : i18n.t('connMgr.collapse')"
> >
<ng-icon <ng-icon
[name]="group.stack.collapsed ? 'tdesignChevronRight' : 'tdesignChevronDown'" [name]="group.stack.collapsed ? 'chevronRight14' : 'chevronDown14'"
size="var(--icon-size-sm)" size="var(--icon-size-sm)"
/> />
</button> </button>
@@ -90,14 +90,14 @@
[title]="shortcuts.tip(i18n.t('connMgr.renameStack'), 'conn-rename-stack')" [title]="shortcuts.tip(i18n.t('connMgr.renameStack'), 'conn-rename-stack')"
class="grid size-4 place-items-center text-muted-foreground hover:text-foreground" class="grid size-4 place-items-center text-muted-foreground hover:text-foreground"
> >
<ng-icon name="tdesignEdit" size="var(--icon-size-sm)" /> <ng-icon name="edit14" size="var(--icon-size-sm)" />
</button> </button>
<button <button
(click)="deleteStack(group.stack)" (click)="deleteStack(group.stack)"
[title]="shortcuts.tip(i18n.t('connMgr.deleteStack'), 'conn-delete')" [title]="shortcuts.tip(i18n.t('connMgr.deleteStack'), 'conn-delete')"
class="grid size-4 place-items-center text-muted-foreground hover:text-destructive" class="grid size-4 place-items-center text-muted-foreground hover:text-destructive"
> >
<ng-icon name="tdesignDelete" size="var(--icon-size-sm)" /> <ng-icon name="delete14" size="var(--icon-size-sm)" />
</button> </button>
@if (group.connections.length) { @if (group.connections.length) {
<button <button
@@ -105,7 +105,7 @@
[title]="shortcuts.tip(i18n.t('connMgr.openStack'), 'conn-connect')" [title]="shortcuts.tip(i18n.t('connMgr.openStack'), 'conn-connect')"
class="grid size-4 place-items-center text-muted-foreground hover:text-primary" class="grid size-4 place-items-center text-muted-foreground hover:text-primary"
> >
<ng-icon name="tdesignArrowRight" size="var(--icon-size-sm)" /> <ng-icon name="arrowRight14" size="var(--icon-size-sm)" />
</button> </button>
} }
</div> </div>
@@ -123,7 +123,7 @@
[class.bg-accent]="draft()?.id === connection.id" [class.bg-accent]="draft()?.id === connection.id"
[class.opacity-50]="draggedId() === connection.id" [class.opacity-50]="draggedId() === connection.id"
> >
<ng-icon name="tdesignServer" size="var(--icon-size-sm)" class="shrink-0 text-muted-foreground" /> <ng-icon name="server14" size="var(--icon-size-sm)" class="shrink-0 text-muted-foreground" />
<span class="truncate">{{ connection.name }}</span> <span class="truncate">{{ connection.name }}</span>
</button> </button>
} @empty { } @empty {
@@ -161,7 +161,7 @@
[class.bg-accent]="draft()?.id === connection.id" [class.bg-accent]="draft()?.id === connection.id"
[class.opacity-50]="draggedId() === connection.id" [class.opacity-50]="draggedId() === connection.id"
> >
<ng-icon name="tdesignServer" size="var(--icon-size-sm)" class="shrink-0 text-muted-foreground" /> <ng-icon name="server14" size="var(--icon-size-sm)" class="shrink-0 text-muted-foreground" />
<span class="truncate">{{ connection.name }}</span> <span class="truncate">{{ connection.name }}</span>
</button> </button>
} }
@@ -342,7 +342,7 @@
[title]="shortcuts.tip(i18n.t('common.delete'), 'conn-delete')" [title]="shortcuts.tip(i18n.t('common.delete'), 'conn-delete')"
class="flex items-center gap-1.5 px-2 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-destructive" class="flex items-center gap-1.5 px-2 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-destructive"
> >
<ng-icon name="tdesignDelete" size="var(--icon-size-sm)" /> {{ i18n.t('common.delete') }} <ng-icon name="delete14" size="var(--icon-size-sm)" /> {{ i18n.t('common.delete') }}
</button> </button>
<button <button
(click)="moreOpen.set(!moreOpen())" (click)="moreOpen.set(!moreOpen())"
@@ -374,7 +374,7 @@
} @else { } @else {
<div class="grid flex-1 place-items-center"> <div class="grid flex-1 place-items-center">
<div class="text-center"> <div class="text-center">
<ng-icon name="tdesignServer" size="var(--icon-size-xl)" class="text-muted-foreground" /> <ng-icon name="server24" size="var(--icon-size-xl)" class="text-muted-foreground" />
<p class="mt-2 text-xs text-muted-foreground"> <p class="mt-2 text-xs text-muted-foreground">
{{ i18n.t('connMgr.selectPrompt') }} {{ i18n.t('connMgr.selectPrompt') }}
</p> </p>
+1 -1
View File
@@ -77,7 +77,7 @@ const EDGE_GAP = 6; // px on purpose: a viewport-edge margin, not a UI size
<!-- the check column: a checkmark, else the item's icon --> <!-- the check column: a checkmark, else the item's icon -->
<span class="grid w-4 shrink-0 place-items-center"> <span class="grid w-4 shrink-0 place-items-center">
@if (item.checked) { @if (item.checked) {
<ng-icon name="tdesignCheck" size="var(--icon-size-sm)" class="text-primary" /> <ng-icon name="check14" size="var(--icon-size-sm)" class="text-primary" />
} @else if (item.icon) { } @else if (item.icon) {
<ng-icon <ng-icon
[name]="item.icon" [name]="item.icon"
@@ -63,7 +63,7 @@ import { FocusService } from '@core/focus.service';
</div> </div>
<div class="mt-1.5 flex items-center gap-1.5 text-xs"> <div class="mt-1.5 flex items-center gap-1.5 text-xs">
<ng-icon <ng-icon
[name]="isDir() ? 'tdesignFolder' : 'tdesignFile'" [name]="isDir() ? 'folder14' : 'file14'"
size="var(--icon-size-sm)" size="var(--icon-size-sm)"
class="shrink-0 text-muted-foreground" class="shrink-0 text-muted-foreground"
/> />
@@ -2,7 +2,7 @@
@if (!ctx) { @if (!ctx) {
<div class="flex flex-1 items-center justify-center"> <div class="flex flex-1 items-center justify-center">
<div class="max-w-sm border border-border bg-card p-6 text-center shadow-sm"> <div class="max-w-sm border border-border bg-card p-6 text-center shadow-sm">
<ng-icon name="tdesignCloseCircle" size="var(--icon-size-xl)" class="text-destructive" /> <ng-icon name="closeCircle24" size="var(--icon-size-xl)" class="text-destructive" />
<p class="mt-2 text-sm font-medium">{{ i18n.t('editor.noFileTitle') }}</p> <p class="mt-2 text-sm font-medium">{{ i18n.t('editor.noFileTitle') }}</p>
<p class="mt-1 text-xs text-muted-foreground"> <p class="mt-1 text-xs text-muted-foreground">
{{ i18n.t('editor.noFileBody') }} {{ i18n.t('editor.noFileBody') }}
@@ -20,7 +20,7 @@
[class.h-9]="!isMacChrome" [class.h-9]="!isMacChrome"
[class.pl-3]="!isMacChrome" [class.pl-3]="!isMacChrome"
> >
<ng-icon name="tdesignFile1" size="var(--icon-size-md)" class="shrink-0 text-muted-foreground" /> <ng-icon name="fileText16" size="var(--icon-size-md)" class="shrink-0 text-muted-foreground" />
<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>
@@ -41,13 +41,13 @@
[title]="shortcuts.tip(i18n.t('editor.findHint'), 'find')" [title]="shortcuts.tip(i18n.t('editor.findHint'), 'find')"
class="grid size-6 place-items-center text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground" class="grid size-6 place-items-center text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignSearch" size="var(--icon-size-sm)" /> <ng-icon name="search14" size="var(--icon-size-sm)" />
</button> </button>
</header> </header>
@if (findOpen()) { @if (findOpen()) {
<div class="flex shrink-0 items-center gap-2 border-b border-border bg-toolbar px-3 py-1.5"> <div class="flex shrink-0 items-center gap-2 border-b border-border bg-toolbar px-3 py-1.5">
<ng-icon name="tdesignSearch" size="var(--icon-size-sm)" class="shrink-0 text-muted-foreground" /> <ng-icon name="search14" size="var(--icon-size-sm)" class="shrink-0 text-muted-foreground" />
<input <input
#findBox #findBox
[value]="findQuery()" [value]="findQuery()"
@@ -69,14 +69,14 @@
[title]="shortcuts.tipFixed(i18n.t('editor.prevMatch'), 'Shift+Enter')" [title]="shortcuts.tipFixed(i18n.t('editor.prevMatch'), 'Shift+Enter')"
class="grid size-5 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground" class="grid size-5 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignChevronUp" size="var(--icon-size-sm)" /> <ng-icon name="chevronUp14" size="var(--icon-size-sm)" />
</button> </button>
<button <button
(click)="findStep(1)" (click)="findStep(1)"
[title]="shortcuts.tipFixed(i18n.t('editor.nextMatch'), 'Enter')" [title]="shortcuts.tipFixed(i18n.t('editor.nextMatch'), 'Enter')"
class="grid size-5 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground" class="grid size-5 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignChevronDown" size="var(--icon-size-sm)" /> <ng-icon name="chevronDown14" size="var(--icon-size-sm)" />
</button> </button>
<div class="flex-1"></div> <div class="flex-1"></div>
<button <button
@@ -84,7 +84,7 @@
[title]="i18n.t('editor.closeHint')" [title]="i18n.t('editor.closeHint')"
class="grid size-5 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground" class="grid size-5 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignClose" size="var(--icon-size-sm)" /> <ng-icon name="close14" size="var(--icon-size-sm)" />
</button> </button>
</div> </div>
} }
@@ -93,7 +93,7 @@
<div <div
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" 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="tdesignCloseCircle" size="var(--icon-size-sm)" class="shrink-0" /> <ng-icon name="closeCircle14" size="var(--icon-size-sm)" class="shrink-0" />
{{ i18n.t('editor.disconnected', { name: ctx.sessionTitle }) }} {{ i18n.t('editor.disconnected', { name: ctx.sessionTitle }) }}
</div> </div>
} }
@@ -105,7 +105,7 @@
} @else if (loadError(); as error) { } @else if (loadError(); as error) {
<div class="flex flex-1 items-center justify-center"> <div class="flex flex-1 items-center justify-center">
<div class="max-w-md border border-border bg-card p-6 text-center shadow-sm"> <div class="max-w-md border border-border bg-card p-6 text-center shadow-sm">
<ng-icon name="tdesignCloseCircle" size="var(--icon-size-xl)" class="text-destructive" /> <ng-icon name="closeCircle24" size="var(--icon-size-xl)" class="text-destructive" />
<p class="mt-2 text-sm font-medium">{{ i18n.t('editor.cantOpen', { name: ctx.name }) }}</p> <p class="mt-2 text-sm font-medium">{{ i18n.t('editor.cantOpen', { name: ctx.name }) }}</p>
<p class="mt-1 text-xs text-muted-foreground">{{ error }}</p> <p class="mt-1 text-xs text-muted-foreground">{{ error }}</p>
<button <button
@@ -13,14 +13,14 @@
<ng-icon name="tdesignLoading" class="shrink-0 animate-spin text-primary" size="var(--icon-size-sm)" /> <ng-icon name="tdesignLoading" class="shrink-0 animate-spin text-primary" size="var(--icon-size-sm)" />
} @else { } @else {
<ng-icon <ng-icon
[name]="isRemote() ? 'tdesignServer' : 'tdesignHardDrive'" [name]="isRemote() ? 'server14' : 'hardDrive14'"
[class]="isRemote() ? 'shrink-0 text-primary' : 'shrink-0 text-muted-foreground'" [class]="isRemote() ? 'shrink-0 text-primary' : 'shrink-0 text-muted-foreground'"
size="var(--icon-size-sm)" size="var(--icon-size-sm)"
/> />
} }
<span class="truncate">{{ isRemote() ? panel().title() : i18n.t('common.local') }}</span> <span class="truncate">{{ isRemote() ? panel().title() : i18n.t('common.local') }}</span>
<ng-icon <ng-icon
name="tdesignChevronDown" name="chevronDown14"
class="shrink-0 text-muted-foreground" class="shrink-0 text-muted-foreground"
size="var(--icon-size-sm)" size="var(--icon-size-sm)"
/> />
@@ -42,7 +42,7 @@
[title]="shortcuts.tip(i18n.t('panel.editFile'), 'edit')" [title]="shortcuts.tip(i18n.t('panel.editFile'), 'edit')"
class="flex h-(--action-h) shrink-0 items-center px-1 text-muted-foreground hover:bg-accent hover:text-foreground" class="flex h-(--action-h) shrink-0 items-center px-1 text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignFile1" size="var(--icon-size-sm)" /> <ng-icon name="fileText14" size="var(--icon-size-sm)" />
<span class="ml-1 hidden text-(length:--action-font) @[30rem]:inline">{{ i18n.t('panel.edit') }}</span> <span class="ml-1 hidden text-(length:--action-font) @[30rem]:inline">{{ i18n.t('panel.edit') }}</span>
</button> </button>
<button <button
@@ -50,7 +50,7 @@
[title]="shortcuts.tip(i18n.t('panel.rename'), 'rename')" [title]="shortcuts.tip(i18n.t('panel.rename'), 'rename')"
class="flex h-(--action-h) shrink-0 items-center px-1 text-muted-foreground hover:bg-accent hover:text-foreground" class="flex h-(--action-h) shrink-0 items-center px-1 text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignEdit" size="var(--icon-size-sm)" /> <ng-icon name="edit14" size="var(--icon-size-sm)" />
<span class="ml-1 hidden text-(length:--action-font) @[30rem]:inline">{{ i18n.t('common.rename') }}</span> <span class="ml-1 hidden text-(length:--action-font) @[30rem]:inline">{{ i18n.t('common.rename') }}</span>
</button> </button>
<button <button
@@ -58,14 +58,14 @@
[title]="shortcuts.tip(i18n.t('panel.newFolder'), 'new-folder')" [title]="shortcuts.tip(i18n.t('panel.newFolder'), 'new-folder')"
class="grid size-(--action-h) shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground" class="grid size-(--action-h) shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignFolderAdd" size="var(--icon-size-sm)" /> <ng-icon name="folderAdd14" size="var(--icon-size-sm)" />
</button> </button>
<button <button
(click)="run('new-file')" (click)="run('new-file')"
[title]="shortcuts.tip(i18n.t('panel.newFile'), 'new-file')" [title]="shortcuts.tip(i18n.t('panel.newFile'), 'new-file')"
class="grid size-(--action-h) shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground" class="grid size-(--action-h) shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignFileAdd" size="var(--icon-size-sm)" /> <ng-icon name="fileAdd14" size="var(--icon-size-sm)" />
</button> </button>
@if (chmodAvailable()) { @if (chmodAvailable()) {
<button <button
@@ -73,7 +73,7 @@
[title]="shortcuts.tip(i18n.t('panel.permissions'), 'chmod')" [title]="shortcuts.tip(i18n.t('panel.permissions'), 'chmod')"
class="grid size-(--action-h) shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground" class="grid size-(--action-h) shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignLockOn" size="var(--icon-size-sm)" /> <ng-icon name="lockOn14" size="var(--icon-size-sm)" />
</button> </button>
} }
<button <button
@@ -81,7 +81,7 @@
[title]="shortcuts.tip(i18n.t('common.delete'), 'delete')" [title]="shortcuts.tip(i18n.t('common.delete'), 'delete')"
class="flex h-(--action-h) shrink-0 items-center px-1 text-muted-foreground hover:bg-accent hover:text-destructive" class="flex h-(--action-h) shrink-0 items-center px-1 text-muted-foreground hover:bg-accent hover:text-destructive"
> >
<ng-icon name="tdesignDelete" size="var(--icon-size-sm)" /> <ng-icon name="delete14" size="var(--icon-size-sm)" />
<span class="ml-1 hidden text-(length:--action-font) @[30rem]:inline">{{ i18n.t('common.delete') }}</span> <span class="ml-1 hidden text-(length:--action-font) @[30rem]:inline">{{ i18n.t('common.delete') }}</span>
</button> </button>
@@ -95,7 +95,7 @@
[title]="shortcuts.tip(i18n.t('panel.parentDir'), 'parent')" [title]="shortcuts.tip(i18n.t('panel.parentDir'), 'parent')"
class="grid size-(--path-ctl-h) shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 disabled:hover:bg-transparent" class="grid size-(--path-ctl-h) shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 disabled:hover:bg-transparent"
> >
<ng-icon name="tdesignArrowUp" size="var(--icon-size-md)" /> <ng-icon name="arrowUp16" size="var(--icon-size-md)" />
</button> </button>
<!-- no blanket stopPropagation: the window handler lets only ⌘/Ctrl chords <!-- no blanket stopPropagation: the window handler lets only ⌘/Ctrl chords
through from a text field, and its WebView2 reload guard must see F5 --> through from a text field, and its WebView2 reload guard must see F5 -->
@@ -119,7 +119,7 @@
@if (panel().loading()) { @if (panel().loading()) {
<ng-icon name="tdesignLoading" size="var(--icon-size-md)" class="animate-spin" /> <ng-icon name="tdesignLoading" size="var(--icon-size-md)" class="animate-spin" />
} @else { } @else {
<ng-icon name="tdesignRefresh" size="var(--icon-size-md)" /> <ng-icon name="refresh16" size="var(--icon-size-md)" />
} }
</button> </button>
</div> </div>
@@ -153,7 +153,7 @@
<!-- type-to-filter bar (appears while a query is active) --> <!-- type-to-filter bar (appears while a query is active) -->
@if (panel().filter(); as query) { @if (panel().filter(); as query) {
<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="tdesignSearch" size="var(--icon-size-sm)" class="shrink-0 text-muted-foreground" /> <ng-icon name="search14" size="var(--icon-size-sm)" 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-[0.6875rem] 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() }) }}
@@ -163,7 +163,7 @@
[title]="i18n.t('panel.clearFilter')" [title]="i18n.t('panel.clearFilter')"
class="grid size-5 shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground" class="grid size-5 shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignClose" size="var(--icon-size-sm)" /> <ng-icon name="close14" size="var(--icon-size-sm)" />
</button> </button>
</div> </div>
} }
@@ -18,6 +18,7 @@ import { DialogsService } from '@core/dialogs.service';
import { FileActionsService, type FileCommand, type FileTarget } from '@core/file-actions.service'; import { FileActionsService, type FileCommand, type FileTarget } from '@core/file-actions.service';
import { fileGlyph, fileIconClass, fileKindKey } from '@core/file-icons'; import { fileGlyph, fileIconClass, fileKindKey } from '@core/file-icons';
import { formatMode } from '@core/format'; import { formatMode } from '@core/format';
import type { Icon14, Icon16 } from '@core/icons';
import { remPx } from '@core/metrics'; import { remPx } from '@core/metrics';
import { PanelsService, type Side, type SortKey } from '@core/panels.service'; import { PanelsService, type Side, type SortKey } from '@core/panels.service';
import { IS_WINDOWS } from '@core/platform'; import { IS_WINDOWS } from '@core/platform';
@@ -91,7 +92,7 @@ export class FilePanel {
/** Input is ignored while the panel is connecting or a listing is in flight. */ /** Input is ignored while the panel is connecting or a listing is in flight. */
readonly blocked = computed(() => this.panel().busy()); readonly blocked = computed(() => this.panel().busy());
/** The copy button points toward the other panel. */ /** The copy button points toward the other panel. */
readonly copyIcon = computed(() => (this.side() === 'left' ? 'tdesignArrowRight' : 'tdesignArrowLeft')); readonly copyIcon = computed<Icon14>(() => (this.side() === 'left' ? 'arrowRight14' : 'arrowLeft14'));
readonly pathDraft = signal(''); readonly pathDraft = signal('');
/** Windows drive roots for the source picker; always empty elsewhere. */ /** Windows drive roots for the source picker; always empty elsewhere. */
@@ -192,20 +193,20 @@ export class FilePanel {
{ {
label: this.i18n.t('common.local'), label: this.i18n.t('common.local'),
action: 'local', action: 'local',
icon: 'tdesignHardDrive', icon: 'hardDrive14',
checked: !this.isRemote(), checked: !this.isRemote(),
}, },
]; ];
// Windows drive roots (empty list elsewhere) // Windows drive roots (empty list elsewhere)
for (const drive of this.drives()) { for (const drive of this.drives()) {
items.push({ label: drive, action: `drive:${drive}`, icon: 'tdesignHardDrive' }); items.push({ label: drive, action: `drive:${drive}`, icon: 'hardDrive14' });
} }
const { loose, stacks } = this.connections.grouped(); const { loose, stacks } = this.connections.grouped();
if (loose.length || stacks.length) items.push({ type: 'separator' }); if (loose.length || stacks.length) items.push({ type: 'separator' });
const server = (connection: { id: string; name: string }): MenuItem => ({ const server = (connection: { id: string; name: string }): MenuItem => ({
label: connection.name, label: connection.name,
action: `conn:${connection.id}`, action: `conn:${connection.id}`,
icon: 'tdesignServer', icon: 'server14',
}); });
items.push(...loose.map(server)); items.push(...loose.map(server));
for (const group of stacks) { for (const group of stacks) {
@@ -218,7 +219,7 @@ export class FilePanel {
items.push({ items.push({
label: host.name, label: host.name,
action: `ssh:${host.name}`, action: `ssh:${host.name}`,
icon: 'tdesignServer', icon: 'server14',
disabled: !!host.unsupported, disabled: !!host.unsupported,
tooltip: this.sshHostTitle(host), tooltip: this.sshHostTitle(host),
}); });
@@ -267,7 +268,7 @@ export class FilePanel {
return `${host.username}@${host.hostname}:${host.port}`; return `${host.username}@${host.hostname}:${host.port}`;
} }
iconFor(entry: FileEntry): string { iconFor(entry: FileEntry): Icon16 {
return fileGlyph(entry); return fileGlyph(entry);
} }
@@ -9,7 +9,7 @@
[class.h-9]="!isMacChrome" [class.h-9]="!isMacChrome"
[class.pl-3]="!isMacChrome" [class.pl-3]="!isMacChrome"
> >
<ng-icon name="tdesignSetting" size="var(--icon-size-md)" class="shrink-0 text-muted-foreground" /> <ng-icon name="setting16" size="var(--icon-size-md)" class="shrink-0 text-muted-foreground" />
<h1 class="text-sm font-semibold">{{ i18n.t('options.title') }}</h1> <h1 class="text-sm font-semibold">{{ i18n.t('options.title') }}</h1>
</header> </header>
@@ -250,7 +250,7 @@
[title]="i18n.t('options.resetDefault')" [title]="i18n.t('options.resetDefault')"
class="grid size-5 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground" class="grid size-5 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignClose" size="var(--icon-size-sm)" /> <ng-icon name="close14" size="var(--icon-size-sm)" />
</button> </button>
} }
</div> </div>
@@ -305,7 +305,7 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="relative min-w-0 flex-1"> <div class="relative min-w-0 flex-1">
<ng-icon <ng-icon
name="tdesignSearch" name="search14"
size="var(--icon-size-sm)" size="var(--icon-size-sm)"
class="pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-muted-foreground" class="pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-muted-foreground"
/> />
@@ -367,7 +367,7 @@
[title]="i18n.t('keys.ui.remove', { key: shortcuts.format(combo) })" [title]="i18n.t('keys.ui.remove', { key: shortcuts.format(combo) })"
class="grid size-4 place-items-center text-muted-foreground hover:text-foreground" class="grid size-4 place-items-center text-muted-foreground hover:text-foreground"
> >
<ng-icon name="tdesignClose" size="var(--icon-size-sm)" /> <ng-icon name="close14" size="var(--icon-size-sm)" />
</button> </button>
</span> </span>
} }
@@ -387,7 +387,7 @@
class="grid size-5 shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground" class="grid size-5 shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground"
[class.text-primary]="capturing() === binding.id" [class.text-primary]="capturing() === binding.id"
> >
<ng-icon name="pixelAdd" size="var(--icon-size-sm)" /> <ng-icon name="add14" size="var(--icon-size-sm)" />
</button> </button>
<button <button
(click)="resetRow(binding)" (click)="resetRow(binding)"
@@ -396,7 +396,7 @@
[title]="i18n.t('keys.ui.reset')" [title]="i18n.t('keys.ui.reset')"
class="grid size-5 shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground disabled:invisible" class="grid size-5 shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground disabled:invisible"
> >
<ng-icon name="tdesignRefresh" size="var(--icon-size-sm)" /> <ng-icon name="refresh14" size="var(--icon-size-sm)" />
</button> </button>
</div> </div>
@if (note()?.id === binding.id) { @if (note()?.id === binding.id) {
@@ -30,12 +30,12 @@ import { TransfersService } from '@core/transfers.service';
} }
</span> </span>
} @else if (transfers.failedJobs().length; as failed) { } @else if (transfers.failedJobs().length; as failed) {
<ng-icon name="tdesignCloseCircle" size="var(--icon-size-sm)" class="shrink-0 text-destructive" /> <ng-icon name="closeCircle14" size="var(--icon-size-sm)" class="shrink-0 text-destructive" />
<span class="text-destructive"> <span class="text-destructive">
{{ i18n.t('transfer.failed', { count: failed }) }} {{ i18n.t('transfer.failed', { count: failed }) }}
</span> </span>
} @else if (transfers.hasJobs()) { } @else if (transfers.hasJobs()) {
<ng-icon name="tdesignCheckCircle" size="var(--icon-size-sm)" class="shrink-0 text-success" /> <ng-icon name="checkCircle14" size="var(--icon-size-sm)" class="shrink-0 text-success" />
<span>{{ i18n.t('transfer.finished') }}</span> <span>{{ i18n.t('transfer.finished') }}</span>
} @else { } @else {
<span>{{ i18n.t('transfer.ready') }}</span> <span>{{ i18n.t('transfer.ready') }}</span>
@@ -45,7 +45,7 @@ import { TransfersService } from '@core/transfers.service';
<!-- active panel info --> <!-- active panel info -->
<div class="flex shrink-0 items-center gap-1.5"> <div class="flex shrink-0 items-center gap-1.5">
<ng-icon <ng-icon
[name]="activePanel().sessionId() === 'local' ? 'tdesignHardDrive' : 'tdesignServer'" [name]="activePanel().sessionId() === 'local' ? 'hardDrive14' : 'server14'"
size="var(--icon-size-sm)" size="var(--icon-size-sm)"
[class.text-primary]="activePanel().sessionId() !== 'local'" [class.text-primary]="activePanel().sessionId() !== 'local'"
/> />
+7 -7
View File
@@ -52,7 +52,7 @@ import { UpdaterService } from '@core/updater.service';
[title]="panels.workspaceName(ws)" [title]="panels.workspaceName(ws)"
> >
<ng-icon <ng-icon
[name]="panels.workspaceIsRemote(ws) ? 'tdesignServer' : 'tdesignHardDrive'" [name]="panels.workspaceIsRemote(ws) ? 'server14' : 'hardDrive14'"
size="var(--icon-size-sm)" size="var(--icon-size-sm)"
class="shrink-0" class="shrink-0"
[class.text-primary]="panels.workspaceIsRemote(ws)" [class.text-primary]="panels.workspaceIsRemote(ws)"
@@ -67,7 +67,7 @@ import { UpdaterService } from '@core/updater.service';
[title]="i18n.t('tabBar.closeTab')" [title]="i18n.t('tabBar.closeTab')"
class="mr-1 grid size-4 shrink-0 place-items-center text-muted-foreground opacity-0 group-hover:opacity-100 hover:bg-border hover:text-foreground" class="mr-1 grid size-4 shrink-0 place-items-center text-muted-foreground opacity-0 group-hover:opacity-100 hover:bg-border hover:text-foreground"
> >
<ng-icon name="tdesignClose" size="var(--icon-size-sm)" /> <ng-icon name="close14" size="var(--icon-size-sm)" />
</button> </button>
} @else { } @else {
<span class="mr-1 size-4 shrink-0"></span> <span class="mr-1 size-4 shrink-0"></span>
@@ -83,7 +83,7 @@ import { UpdaterService } from '@core/updater.service';
[title]="shortcuts.tip(i18n.t('tabBar.newTab'), 'new-tab')" [title]="shortcuts.tip(i18n.t('tabBar.newTab'), 'new-tab')"
class="ml-0.5 grid size-6 place-items-center text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground" class="ml-0.5 grid size-6 place-items-center text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground"
> >
<ng-icon name="pixelAdd" size="var(--icon-size-sm)" /> <ng-icon name="add14" size="var(--icon-size-sm)" />
</button> </button>
<div class="flex-1"></div> <div class="flex-1"></div>
@@ -106,13 +106,13 @@ import { UpdaterService } from '@core/updater.service';
<span class="relative flex items-center gap-1.5"> <span class="relative flex items-center gap-1.5">
@switch (updater.state()) { @switch (updater.state()) {
@case ('available') { @case ('available') {
<ng-icon name="tdesignDownload" size="var(--icon-size-sm)" />{{ i18n.t('tabBar.update') }} <ng-icon name="download14" size="var(--icon-size-sm)" />{{ i18n.t('tabBar.update') }}
} }
@case ('downloading') { @case ('downloading') {
<ng-icon name="tdesignLoading" size="var(--icon-size-sm)" class="animate-spin" />{{ i18n.t('tabBar.updating') }} {{ updater.progress() }}% <ng-icon name="tdesignLoading" size="var(--icon-size-sm)" class="animate-spin" />{{ i18n.t('tabBar.updating') }} {{ updater.progress() }}%
} }
@case ('ready') { @case ('ready') {
<ng-icon name="tdesignRefresh" size="var(--icon-size-sm)" />{{ i18n.t('tabBar.restartToUpdate') }} <ng-icon name="refresh14" size="var(--icon-size-sm)" />{{ i18n.t('tabBar.restartToUpdate') }}
} }
} }
</span> </span>
@@ -125,7 +125,7 @@ import { UpdaterService } from '@core/updater.service';
[title]="shortcuts.tip(i18n.t('tabBar.manageConnections'), 'connection-manager')" [title]="shortcuts.tip(i18n.t('tabBar.manageConnections'), 'connection-manager')"
class="grid h-6 px-2 py-1 place-items-center text-[0.6875rem] 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 text-[0.6875rem] text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground"
> >
<!-- <ng-icon name="tdesignSetting" size="var(--icon-size-sm)" /> --> <!-- <ng-icon name="setting14" size="var(--icon-size-sm)" /> -->
{{ i18n.t('tabBar.connections') }} {{ i18n.t('tabBar.connections') }}
</button> </button>
@@ -135,7 +135,7 @@ import { UpdaterService } from '@core/updater.service';
[title]="shortcuts.tip(i18n.t('tabBar.options'), 'open-settings')" [title]="shortcuts.tip(i18n.t('tabBar.options'), 'open-settings')"
class="grid size-6 place-items-center text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground" class="grid size-6 place-items-center text-muted-foreground electrobun-webkit-app-region-no-drag hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignSetting" size="var(--icon-size-sm)" /> <ng-icon name="setting14" size="var(--icon-size-sm)" />
</button> </button>
</div> </div>
`, `,
+6 -5
View File
@@ -1,6 +1,7 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { I18nService } from '@core/i18n.service'; import { I18nService } from '@core/i18n.service';
import type { Icon14 } from '@core/icons';
import { ToastService } from '@core/toast.service'; import { ToastService } from '@core/toast.service';
/** Toast stack overlay; shared by the main window and editor windows. */ /** Toast stack overlay; shared by the main window and editor windows. */
@@ -26,7 +27,7 @@ import { ToastService } from '@core/toast.service';
[attr.aria-label]="i18n.t('transfer.dismiss')" [attr.aria-label]="i18n.t('transfer.dismiss')"
class="shrink-0 text-muted-foreground hover:text-foreground" class="shrink-0 text-muted-foreground hover:text-foreground"
> >
<ng-icon name="tdesignClose" size="var(--icon-size-sm)" /> <ng-icon name="close14" size="var(--icon-size-sm)" />
</button> </button>
<!-- auto-dismiss countdown; freezes with the timer while hovered --> <!-- auto-dismiss countdown; freezes with the timer while hovered -->
<div <div
@@ -45,9 +46,9 @@ export class Toasts {
readonly toast = inject(ToastService); readonly toast = inject(ToastService);
readonly i18n = inject(I18nService); readonly i18n = inject(I18nService);
toastIcon(kind: 'error' | 'info' | 'success'): string { toastIcon(kind: 'error' | 'info' | 'success'): Icon14 {
if (kind === 'error') return 'tdesignCloseCircle'; if (kind === 'error') return 'closeCircle14';
if (kind === 'success') return 'tdesignCheckCircle'; if (kind === 'success') return 'checkCircle14';
return 'tdesignCheck'; return 'check14';
} }
} }
@@ -8,7 +8,7 @@
[disabled]="!transfers.finishedJobs().length" [disabled]="!transfers.finishedJobs().length"
class="flex items-center gap-1 px-1.5 py-0.5 hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent" class="flex items-center gap-1 px-1.5 py-0.5 hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
> >
<ng-icon name="tdesignDelete" size="var(--icon-size-sm)" /> {{ i18n.t('transfer.clearFinished') }} <ng-icon name="delete14" size="var(--icon-size-sm)" /> {{ i18n.t('transfer.clearFinished') }}
</button> </button>
</div> </div>
<div class="max-h-40 overflow-y-auto border-t border-border bg-chrome"> <div class="max-h-40 overflow-y-auto border-t border-border bg-chrome">
@@ -40,7 +40,7 @@
[title]="i18n.t('transfer.cancel')" [title]="i18n.t('transfer.cancel')"
class="shrink-0 p-1 text-muted-foreground hover:bg-accent hover:text-destructive" class="shrink-0 p-1 text-muted-foreground hover:bg-accent hover:text-destructive"
> >
<ng-icon name="tdesignClose" size="var(--icon-size-sm)" /> <ng-icon name="close14" size="var(--icon-size-sm)" />
</button> </button>
} @else { } @else {
<button <button
@@ -48,7 +48,7 @@
[title]="i18n.t('transfer.dismiss')" [title]="i18n.t('transfer.dismiss')"
class="shrink-0 p-1 text-muted-foreground hover:bg-accent hover:text-foreground" class="shrink-0 p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
> >
<ng-icon name="tdesignClose" size="var(--icon-size-sm)" /> <ng-icon name="close14" size="var(--icon-size-sm)" />
</button> </button>
} }
</div> </div>
@@ -4,6 +4,7 @@ import type { TransferSnapshot } from '@shared/types';
import { cycleFocus } from '@core/focus-cycle'; import { cycleFocus } from '@core/focus-cycle';
import { formatEta } from '@core/format'; import { formatEta } from '@core/format';
import { I18nService } from '@core/i18n.service'; import { I18nService } from '@core/i18n.service';
import type { Icon14 } from '@core/icons';
import { PanelsService } from '@core/panels.service'; import { PanelsService } from '@core/panels.service';
import { SettingsService } from '@core/settings.service'; import { SettingsService } from '@core/settings.service';
import { TransfersService } from '@core/transfers.service'; import { TransfersService } from '@core/transfers.service';
@@ -56,13 +57,13 @@ export class TransferBar {
return parts.join(' · '); return parts.join(' · ');
} }
stateIcon(job: TransferSnapshot): string { stateIcon(job: TransferSnapshot): Icon14 | 'tdesignLoading' {
switch (job.state) { switch (job.state) {
case 'completed': case 'completed':
return 'tdesignCheckCircle'; return 'checkCircle14';
case 'failed': case 'failed':
case 'cancelled': case 'cancelled':
return 'tdesignCloseCircle'; return 'closeCircle14';
default: default:
return 'tdesignLoading'; return 'tdesignLoading';
} }
+3 -1
View File
@@ -204,7 +204,9 @@
the text beside it, in whole pixels (small button: 12px text, 14px icon). the text beside it, in whole pixels (small button: 12px text, 14px icon).
Every <ng-icon> takes one of these four: a one-off size like 0.9rem (14.4px) Every <ng-icon> takes one of these four: a one-off size like 0.9rem (14.4px)
puts every stroke between pixels, and one-offs drift until neighbours never puts every stroke between pixels, and one-offs drift until neighbours never
match. Rem like the rest, so the interface scale moves them too. */ match. Rem like the rest, so the interface scale moves them too.
sm, md and xl are also the pixel grids the glyphs in core/pixel-icons.ts
are drawn on (14, 16, 24): change one and those glyphs blur until redrawn. */
:root { :root {
--icon-size-sm: 0.875rem; /* 14px: beside 11-12px text, and on small icon buttons */ --icon-size-sm: 0.875rem; /* 14px: beside 11-12px text, and on small icon buttons */
--icon-size-md: 1rem; /* 16px: beside 13-14px text (file rows, window titles), path-bar buttons */ --icon-size-md: 1rem; /* 16px: beside 13-14px text (file rows, window titles), path-bar buttons */