Adopt Oxide's color scheme and a square design
The palette now follows Oxide's design system (docs.oxide.computer): a cool neutral scale at hue 260 with almost no chroma, opaque strokes, and Oxide's green as the only accent. The accent-color option is gone: selection, the cursor row, focus rings and the active-panel line all read green. The file panel's header strip now steps toward the list, one shade per row (session strip, path bar, column headers), derived by color-mix so every theme, AMOLED included, follows on its own. Everything is square: every rounded utility and the radius tokens are gone, and checkboxes, radios, selects, number spinners and scrollbars are restyled to match (square scrollbars are always visible on macOS, the cost of styling them). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -55,7 +55,7 @@ Dense reference material lives in [docs/](docs/): [architecture](docs/architectu
|
|||||||
| Runtime | Bun (main process, all SSH/SFTP + file IO) |
|
| Runtime | Bun (main process, all SSH/SFTP + file IO) |
|
||||||
| Desktop | Electrobun 1.18 (native webview, typed RPC bridge) |
|
| Desktop | Electrobun 1.18 (native webview, typed RPC bridge) |
|
||||||
| Frontend | Angular 22 (standalone, signals, zoneless) in `ui/` |
|
| Frontend | Angular 22 (standalone, signals, zoneless) in `ui/` |
|
||||||
| Styling | Tailwind CSS v4, shadcn-style tokens (no component library) |
|
| Styling | Tailwind CSS v4, Oxide-derived tokens (no component library) |
|
||||||
| Icons | @ng-icons/tdesign-icons |
|
| Icons | @ng-icons/tdesign-icons |
|
||||||
| SSH/SFTP | `ssh2` (pure-JS under Bun) |
|
| SSH/SFTP | `ssh2` (pure-JS under Bun) |
|
||||||
|
|
||||||
|
|||||||
+38
-32
@@ -49,8 +49,7 @@ before any page script, plus the persisted theme attributes and interface
|
|||||||
scale so the first paint is already right. `ui/src/main.ts` picks the root
|
scale so the first paint is already right. `ui/src/main.ts` picks the root
|
||||||
component from `APP_MODE`; all four roots use selector `app-root` (index.html
|
component from `APP_MODE`; all four roots use selector `app-root` (index.html
|
||||||
hosts only that tag). `chromeOptions()` is the ONLY place the native frame is
|
hosts only that tag). `chromeOptions()` is the ONLY place the native frame is
|
||||||
decided, spread by
|
decided, spread by the main window and `openSecondaryWindow`: macOS `hiddenInset` +
|
||||||
the main window and `openSecondaryWindow`: macOS `hiddenInset` +
|
|
||||||
`trafficLightOffset {x:12,y:13}` (the UI renders an `h-10 pl-24` drag-region
|
`trafficLightOffset {x:12,y:13}` (the UI renders an `h-10 pl-24` drag-region
|
||||||
header via `IS_MAC_CHROME`); other platforms keep the native titlebar and a
|
header via `IS_MAC_CHROME`); other platforms keep the native titlebar and a
|
||||||
compact `h-9` non-drag header.
|
compact `h-9` non-drag header.
|
||||||
@@ -233,7 +232,7 @@ component; tokens → `ui/src/styles.css`.
|
|||||||
enqueue and runs the collision flow (same-path auto-skip, type-mismatch
|
enqueue and runs the collision flow (same-path auto-skip, type-mismatch
|
||||||
skip, overwrite dialog with all/none batch modes).
|
skip, overwrite dialog with all/none batch modes).
|
||||||
- `settings.service.ts` — one signal per setting; **imperative apply pattern**
|
- `settings.service.ts` — one signal per setting; **imperative apply pattern**
|
||||||
(`applyTheme/DarkTone/Accent` set `data-*` attributes on `<html>`;
|
(`applyTheme/DarkTone` set `data-*` attributes on `<html>`;
|
||||||
`applyUiScale` sets its inline `font-size`); `applyAll` also serves the
|
`applyUiScale` sets its inline `font-size`); `applyAll` also serves the
|
||||||
`settingsChanged` broadcast, so every window follows a save made in any
|
`settingsChanged` broadcast, so every window follows a save made in any
|
||||||
other. Every window RPC serves `saveSettings` (editor and manager windows
|
other. Every window RPC serves `saveSettings` (editor and manager windows
|
||||||
@@ -281,7 +280,7 @@ component; tokens → `ui/src/styles.css`.
|
|||||||
pointer; the keyboard path never consults it — so `pick()` and the arrow-key
|
pointer; the keyboard path never consults it — so `pick()` and the arrow-key
|
||||||
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, never accent-tinted) 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`), passed to
|
- `icons.ts` — the one icon registry (TDesign, `@ng-icons/tdesign-icons`), passed to
|
||||||
`provideIcons` in `app.config.ts`. Icon names are plain strings in templates and
|
`provideIcons` in `app.config.ts`. Icon names are plain strings in templates and
|
||||||
the build never checks them: an unregistered name renders nothing (console error
|
the build never checks them: an unregistered name renders nothing (console error
|
||||||
@@ -301,35 +300,42 @@ component; tokens → `ui/src/styles.css`.
|
|||||||
|
|
||||||
## Theming
|
## Theming
|
||||||
|
|
||||||
`ui/src/styles.css`: shadcn-style OKLCH tokens; dark values are duplicated in
|
`ui/src/styles.css`: OKLCH tokens adapted from **Oxide's design system**
|
||||||
`:root[data-theme='dark']` AND a `prefers-color-scheme` media block (system
|
(docs.oxide.computer: a cool neutral scale at hue 260 with almost no chroma,
|
||||||
mode) — every dark token change goes in BOTH. Light is the zinc family; the
|
one green accent, opaque strokes; the comments carry Oxide's step names
|
||||||
dark neutrals are a **warm gray family (hue 107)** anchored on `#1a1a19`
|
n0…n1300 / green-800). Dark values are duplicated in `:root[data-theme='dark']`
|
||||||
(`--background`) — retoning one dark neutral means retoning them all, or warm
|
AND a `prefers-color-scheme` media block (system mode) — every dark token
|
||||||
and cool surfaces clash. **Surface layers**, each its own shade: `--chrome`
|
change goes in BOTH; retone neutrals along the one scale, or warm and cool
|
||||||
(frame — tab/transfer/status bars, editor/manager header+footer strips, the
|
surfaces clash. **There is no accent setting**: `--primary` is Oxide green per
|
||||||
manager's connection sidebar), `--toolbar` (each file panel's header strip —
|
theme (green-800 dark, green-500 light), so selection (`bg-primary/15`), the
|
||||||
session strip, path bar, column headers — and the editor's search bar; both
|
cursor row (`--cursor-tint`, a `color-mix` of `--primary`), focus rings
|
||||||
panels share one neutral strip, and the ACTIVE panel is marked by a 0.125rem
|
(green-600) and `--success` all read green. **Surface layers**, each its own
|
||||||
`--primary` line along its top edge, an inset box-shadow on the session strip so
|
shade: `--chrome` (frame — tab/transfer/status bars, the editor/manager/settings
|
||||||
switching panels never shifts layout), `--background` (file lists, editor
|
header+footer strips and sidebars), the file panel's header strip **stepping
|
||||||
text, forms; `--row-alt` stripes odd file rows, skipped on the cursor/selected
|
toward the list** — session strip `--toolbar`, path bar `--toolbar-2`, column
|
||||||
row so two `bg-*` utilities never fight) and `--card` (popovers, dialogs,
|
headers `--toolbar-3` (`color-mix` steps declared once in `:root`, so every
|
||||||
toasts). Dark climbs frame 0.165 < list 0.218 < header 0.265 < card 0.285;
|
theme follows) — then `--background` (file lists, editor text, forms;
|
||||||
light steps greys out from white content: frame 0.935 < header 0.965 < list =
|
`--row-alt` stripes odd file rows, skipped on the cursor/selected row so two
|
||||||
card 1. **`--accent` is translucent** (6% zinc-900 light, 9% white dark):
|
`bg-*` utilities never fight) and `--card` (popovers, dialogs, toasts). The
|
||||||
hover/selected states sit on every layer, and an opaque hover vanishes on a
|
ACTIVE panel is marked by a 0.125rem `--primary` line along its top edge (an
|
||||||
surface of its own lightness. The "darker" tone is true-black AMOLED: chrome,
|
inset box-shadow on the session strip, so switching panels never shifts
|
||||||
background, toolbar and row-alt are all `#000` (the layers collapse
|
layout). Dark: frame 0.162 < list 0.195 < header 0.275 → 0.248 → 0.221 < card
|
||||||
on purpose), elevation survives only via `--card`/`--accent` (opaque there),
|
0.29; light steps greys out from near-white content: frame 0.925 < header 0.95
|
||||||
and `--border`/`--input` go to 16%/18% white because those lines then carry the
|
→ 0.965 → 0.98 < list = card 0.995. **`--accent` is translucent** (n0 at 5%
|
||||||
whole layout. Accent tones: one
|
light, white 7% dark): hover/selected states sit on every layer, and an opaque
|
||||||
`--tone-h/--tone-c` line per tone + three formula blocks derive `--primary`,
|
hover vanishes on a surface of its own lightness. Strokes are opaque
|
||||||
`--ring`, `--cursor-tint`; graphite = attribute removed = pristine zinc.
|
(`--border` n300 dark / n900 light, `--input` one step stronger). The
|
||||||
|
"darker" tone is true-black AMOLED: chrome, background, toolbar (+ its steps)
|
||||||
|
and row-alt are all `#000` (the layers collapse on purpose); elevation
|
||||||
|
survives only via `--card`/`--muted`/`--accent` (opaque there). **Square
|
||||||
|
design**: no `rounded-*` utilities and no radius tokens; an `@layer base`
|
||||||
|
block restyles native controls to match — checkboxes/radios (`appearance:
|
||||||
|
none`, a drawn check / square dot), selects (a `currentColor` caret; use
|
||||||
|
`pl-*`, never `px-*`, on a select), hidden number spinners, and square
|
||||||
|
`::-webkit-scrollbar`s (which make macOS scrollbars always visible).
|
||||||
`@theme inline` maps tokens to Tailwind utilities (`bg-primary`,
|
`@theme inline` maps tokens to Tailwind utilities (`bg-primary`,
|
||||||
`text-icon-folder`, `bg-cursor-tint`). Selected rows are `bg-primary/15`
|
`text-icon-folder`, `bg-toolbar-2`, `bg-cursor-tint`). File-type icon colors
|
||||||
(follow the accent); the cursor row uses `--cursor-tint` so graphite stays
|
come from Oxide's hue scales (800 steps dark, 600 light). **Interface scale** (`settings.uiScale`,
|
||||||
pixel-identical to the old neutral hover. **Interface scale** (`settings.uiScale`,
|
|
||||||
0.5–4 in 0.1 steps): one root font-size lever — `applyUiScale` sets
|
0.5–4 in 0.1 steps): one root font-size lever — `applyUiScale` sets
|
||||||
`html { font-size: 16px * scale }`, and since EVERY size in the app is rem
|
`html { font-size: 16px * scale }`, and since EVERY size in the app is rem
|
||||||
(panel tokens like `--row-h`, all Tailwind utilities, ng-icon sizes), the
|
(panel tokens like `--row-h`, all Tailwind utilities, ng-icon sizes), the
|
||||||
|
|||||||
+13
-8
@@ -60,17 +60,22 @@ touching the adjacent code.
|
|||||||
|
|
||||||
- **A new surface token needs an AMOLED line, or AMOLED stops being
|
- **A new surface token needs an AMOLED line, or AMOLED stops being
|
||||||
all-black.** The two `[data-dark-tone='darker']` blocks re-declare every
|
all-black.** The two `[data-dark-tone='darker']` blocks re-declare every
|
||||||
surface (`--chrome`, `--toolbar`, `--row-alt`, …) as `#000`; a token added
|
surface (`--chrome`, `--toolbar`, `--background`, `--row-alt`, …) as `#000`;
|
||||||
only to the dark blocks shows through in AMOLED.
|
a token added only to the dark blocks shows through in AMOLED. Tokens derived
|
||||||
- **Never declare one token in both the AMOLED blocks and the accent dark
|
in `:root` by `color-mix` (`--toolbar-2/-3`) follow on their own.
|
||||||
formula blocks.** Both are (0,3,0), so source order decides and the later
|
|
||||||
accent block wins. A token that must follow the accent belongs in the base
|
|
||||||
theme blocks instead — a `var(--primary)` there still resolves per accent on
|
|
||||||
`:root`.
|
|
||||||
- **Hover/selected color is a translucent overlay (`--accent`).** An opaque
|
- **Hover/selected color is a translucent overlay (`--accent`).** An opaque
|
||||||
hover vanishes on a surface of its own lightness — light's old 0.967 hover
|
hover vanishes on a surface of its own lightness — light's old 0.967 hover
|
||||||
on the 0.965 header strip did exactly that. Keep `--accent` alpha-based when
|
on the 0.965 header strip did exactly that. Keep `--accent` alpha-based when
|
||||||
retoning surfaces.
|
retoning surfaces.
|
||||||
|
- **A native control ignores `border-radius` and background until
|
||||||
|
`appearance: none`.** WebKit draws checkboxes, radios and selects itself;
|
||||||
|
the square restyle in the `@layer base` block depends on it. Put `pl-*`,
|
||||||
|
never `px-*`, on a `<select>`: utilities outrank the base layer, so a `px`
|
||||||
|
utility would erase the padding-right the drawn caret lives in.
|
||||||
|
- **A custom `::-webkit-scrollbar` turns off macOS overlay scrollbars.** The
|
||||||
|
moment the pseudo-element is styled, WKWebView draws classic always-visible
|
||||||
|
scrollbars that take layout width. Deliberate here (square design); don't
|
||||||
|
reach for it to "tint" scrollbars without accepting that.
|
||||||
|
|
||||||
## The interface scale (root font-size lever)
|
## The interface scale (root font-size lever)
|
||||||
|
|
||||||
@@ -144,7 +149,7 @@ touching the adjacent code.
|
|||||||
- **Window-open flash prevention is three-layer** — remove any one and the
|
- **Window-open flash prevention is three-layer** — remove any one and the
|
||||||
white/inverted blink returns: (1) windows are created `hidden: true` and
|
white/inverted blink returns: (1) windows are created `hidden: true` and
|
||||||
revealed by `showWhenReady` on the webview's first `dom-ready` (800 ms
|
revealed by `showWhenReady` on the webview's first `dom-ready` (800 ms
|
||||||
fallback), (2) the bun preload bakes the persisted `data-theme`/tone/accent
|
fallback), (2) the bun preload bakes the persisted `data-theme`/tone
|
||||||
attributes and the interface scale before first paint (`buildPreload`),
|
attributes and the interface scale before first paint (`buildPreload`),
|
||||||
(3) `ui/src/index.html` carries an inline `<style>` with the html background
|
(3) `ui/src/index.html` carries an inline `<style>` with the html background
|
||||||
per theme — its oklch values MUST stay in sync with the `--background` tokens
|
per theme — its oklch values MUST stay in sync with the `--background` tokens
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ macOS is the primary target; Windows 11+ x64 and Linux x64 are **beta** targets
|
|||||||
| Concern | File | mac | win | linux |
|
| Concern | File | mac | win | linux |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| Window chrome | `src/bun/windows.ts chromeOptions` | hiddenInset + traffic-light offset, UI drag header | native titlebar, compact header | same as win |
|
| Window chrome | `src/bun/windows.ts chromeOptions` | hiddenInset + traffic-light offset, UI drag header | native titlebar, compact header | same as win |
|
||||||
|
| Scrollbars | `ui/src/styles.css` (`::-webkit-scrollbar`) | custom square, always visible (styling them turns off overlay scrollbars) | custom square | custom square |
|
||||||
| External editor launch | `src/bun/edits.ts launchEditor` | `open [-a app]` | `cmd /c start "" [app] file` | app directly / `xdg-open` |
|
| External editor launch | `src/bun/edits.ts launchEditor` | `open [-a app]` | `cmd /c start "" [app] file` | app directly / `xdg-open` |
|
||||||
| Editor app picker | `rpc-handlers.ts pickEditorApp` | /Applications, `.app` | %ProgramFiles%, `.exe` | home, any |
|
| Editor app picker | `rpc-handlers.ts pickEditorApp` | /Applications, `.app` | %ProgramFiles%, `.exe` | home, any |
|
||||||
| Owner/group names | `fs/local.ts resolveId` | `id -nu` + `dscacheutil` | undefined → "—" | `id -nu` + `getent` |
|
| Owner/group names | `fs/local.ts resolveId` | `id -nu` + `dscacheutil` | undefined → "—" | `id -nu` + `getent` |
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export function buildPreload(
|
|||||||
const dataset: Record<string, string> = {};
|
const dataset: Record<string, string> = {};
|
||||||
if (settings.theme !== "system") dataset["theme"] = settings.theme;
|
if (settings.theme !== "system") dataset["theme"] = settings.theme;
|
||||||
if (settings.darkTone !== "dark") dataset["darkTone"] = settings.darkTone;
|
if (settings.darkTone !== "dark") dataset["darkTone"] = settings.darkTone;
|
||||||
if (settings.accent !== "graphite") dataset["accent"] = settings.accent;
|
|
||||||
// interface scale: mirrors SettingsService.applyUiScale so the FIRST paint
|
// interface scale: mirrors SettingsService.applyUiScale so the FIRST paint
|
||||||
// is already scaled — without this every window opens at 100% and snaps.
|
// is already scaled — without this every window opens at 100% and snaps.
|
||||||
// Written as an inline style, not a data attribute: the scale is continuous.
|
// Written as an inline style, not a data attribute: the scale is continuous.
|
||||||
|
|||||||
@@ -43,17 +43,6 @@ export const de = {
|
|||||||
system: "System",
|
system: "System",
|
||||||
theme: { label: "Design", system: "System", light: "Hell", dark: "Dunkel" },
|
theme: { label: "Design", system: "System", light: "Hell", dark: "Dunkel" },
|
||||||
tone: { label: "Ton", dark: "Dunkel", darker: "AMOLED" },
|
tone: { label: "Ton", dark: "Dunkel", darker: "AMOLED" },
|
||||||
accent: { label: "Akzent" },
|
|
||||||
accentNames: {
|
|
||||||
graphite: "Graphit",
|
|
||||||
blue: "Blau",
|
|
||||||
indigo: "Indigo",
|
|
||||||
purple: "Lila",
|
|
||||||
rose: "Rosa",
|
|
||||||
orange: "Orange",
|
|
||||||
green: "Grün",
|
|
||||||
teal: "Petrol",
|
|
||||||
},
|
|
||||||
interface: { label: "Oberfläche", smaller: "Kleiner", larger: "Größer", reset: "Auf 100% zurücksetzen" },
|
interface: { label: "Oberfläche", smaller: "Kleiner", larger: "Größer", reset: "Auf 100% zurücksetzen" },
|
||||||
speedUnit: { label: "Geschwindigkeitseinheit" },
|
speedUnit: { label: "Geschwindigkeitseinheit" },
|
||||||
transfers: { label: "Übertragungen", single: "Einzeln", multi: "Mehrere" },
|
transfers: { label: "Übertragungen", single: "Einzeln", multi: "Mehrere" },
|
||||||
|
|||||||
@@ -47,17 +47,6 @@ export const en = {
|
|||||||
system: "System",
|
system: "System",
|
||||||
theme: { label: "Theme", system: "System", light: "Light", dark: "Dark" },
|
theme: { label: "Theme", system: "System", light: "Light", dark: "Dark" },
|
||||||
tone: { label: "Tone", dark: "Dark", darker: "AMOLED" },
|
tone: { label: "Tone", dark: "Dark", darker: "AMOLED" },
|
||||||
accent: { label: "Accent" },
|
|
||||||
accentNames: {
|
|
||||||
graphite: "Graphite",
|
|
||||||
blue: "Blue",
|
|
||||||
indigo: "Indigo",
|
|
||||||
purple: "Purple",
|
|
||||||
rose: "Rose",
|
|
||||||
orange: "Orange",
|
|
||||||
green: "Green",
|
|
||||||
teal: "Teal",
|
|
||||||
},
|
|
||||||
interface: { label: "Interface", smaller: "Smaller", larger: "Larger", reset: "Reset to 100%" },
|
interface: { label: "Interface", smaller: "Smaller", larger: "Larger", reset: "Reset to 100%" },
|
||||||
speedUnit: { label: "Speed unit" },
|
speedUnit: { label: "Speed unit" },
|
||||||
transfers: { label: "Transfers", single: "Single", multi: "Multi" },
|
transfers: { label: "Transfers", single: "Single", multi: "Multi" },
|
||||||
|
|||||||
@@ -43,17 +43,6 @@ export const es = {
|
|||||||
system: "Sistema",
|
system: "Sistema",
|
||||||
theme: { label: "Tema", system: "Sistema", light: "Claro", dark: "Oscuro" },
|
theme: { label: "Tema", system: "Sistema", light: "Claro", dark: "Oscuro" },
|
||||||
tone: { label: "Tono", dark: "Oscuro", darker: "AMOLED" },
|
tone: { label: "Tono", dark: "Oscuro", darker: "AMOLED" },
|
||||||
accent: { label: "Acento" },
|
|
||||||
accentNames: {
|
|
||||||
graphite: "Grafito",
|
|
||||||
blue: "Azul",
|
|
||||||
indigo: "Índigo",
|
|
||||||
purple: "Morado",
|
|
||||||
rose: "Rosa",
|
|
||||||
orange: "Naranja",
|
|
||||||
green: "Verde",
|
|
||||||
teal: "Turquesa",
|
|
||||||
},
|
|
||||||
interface: { label: "Interfaz", smaller: "Más pequeña", larger: "Más grande", reset: "Restablecer al 100%" },
|
interface: { label: "Interfaz", smaller: "Más pequeña", larger: "Más grande", reset: "Restablecer al 100%" },
|
||||||
speedUnit: { label: "Unidad de velocidad" },
|
speedUnit: { label: "Unidad de velocidad" },
|
||||||
transfers: { label: "Transferencias", single: "Una", multi: "Varias" },
|
transfers: { label: "Transferencias", single: "Una", multi: "Varias" },
|
||||||
|
|||||||
@@ -45,17 +45,6 @@ export const eu = {
|
|||||||
system: "Sistema",
|
system: "Sistema",
|
||||||
theme: { label: "Gaia", system: "Sistema", light: "Argia", dark: "Iluna" },
|
theme: { label: "Gaia", system: "Sistema", light: "Argia", dark: "Iluna" },
|
||||||
tone: { label: "Tonua", dark: "Iluna", darker: "AMOLED" },
|
tone: { label: "Tonua", dark: "Iluna", darker: "AMOLED" },
|
||||||
accent: { label: "Azentua" },
|
|
||||||
accentNames: {
|
|
||||||
graphite: "Grafitoa",
|
|
||||||
blue: "Urdina",
|
|
||||||
indigo: "Indigoa",
|
|
||||||
purple: "Morea",
|
|
||||||
rose: "Arrosa",
|
|
||||||
orange: "Laranja",
|
|
||||||
green: "Berdea",
|
|
||||||
teal: "Turkesa",
|
|
||||||
},
|
|
||||||
interface: { label: "Interfazea", smaller: "Txikiagoa", larger: "Handiagoa", reset: "Berrezarri %100era" },
|
interface: { label: "Interfazea", smaller: "Txikiagoa", larger: "Handiagoa", reset: "Berrezarri %100era" },
|
||||||
speedUnit: { label: "Abiadura-unitatea" },
|
speedUnit: { label: "Abiadura-unitatea" },
|
||||||
transfers: { label: "Transferentziak", single: "Banaka", multi: "Anitz" },
|
transfers: { label: "Transferentziak", single: "Banaka", multi: "Anitz" },
|
||||||
|
|||||||
@@ -43,17 +43,6 @@ export const fr = {
|
|||||||
system: "Système",
|
system: "Système",
|
||||||
theme: { label: "Thème", system: "Système", light: "Clair", dark: "Sombre" },
|
theme: { label: "Thème", system: "Système", light: "Clair", dark: "Sombre" },
|
||||||
tone: { label: "Ton", dark: "Sombre", darker: "AMOLED" },
|
tone: { label: "Ton", dark: "Sombre", darker: "AMOLED" },
|
||||||
accent: { label: "Accent" },
|
|
||||||
accentNames: {
|
|
||||||
graphite: "Graphite",
|
|
||||||
blue: "Bleu",
|
|
||||||
indigo: "Indigo",
|
|
||||||
purple: "Violet",
|
|
||||||
rose: "Rose",
|
|
||||||
orange: "Orange",
|
|
||||||
green: "Vert",
|
|
||||||
teal: "Sarcelle",
|
|
||||||
},
|
|
||||||
interface: { label: "Interface", smaller: "Plus petite", larger: "Plus grande", reset: "Réinitialiser à 100%" },
|
interface: { label: "Interface", smaller: "Plus petite", larger: "Plus grande", reset: "Réinitialiser à 100%" },
|
||||||
speedUnit: { label: "Unité de vitesse" },
|
speedUnit: { label: "Unité de vitesse" },
|
||||||
transfers: { label: "Transferts", single: "Un", multi: "Plusieurs" },
|
transfers: { label: "Transferts", single: "Un", multi: "Plusieurs" },
|
||||||
|
|||||||
@@ -44,17 +44,6 @@ export const ja = {
|
|||||||
system: "システム",
|
system: "システム",
|
||||||
theme: { label: "テーマ", system: "システム", light: "ライト", dark: "ダーク" },
|
theme: { label: "テーマ", system: "システム", light: "ライト", dark: "ダーク" },
|
||||||
tone: { label: "トーン", dark: "ダーク", darker: "AMOLED" },
|
tone: { label: "トーン", dark: "ダーク", darker: "AMOLED" },
|
||||||
accent: { label: "アクセント" },
|
|
||||||
accentNames: {
|
|
||||||
graphite: "グラファイト",
|
|
||||||
blue: "ブルー",
|
|
||||||
indigo: "インディゴ",
|
|
||||||
purple: "パープル",
|
|
||||||
rose: "ローズ",
|
|
||||||
orange: "オレンジ",
|
|
||||||
green: "グリーン",
|
|
||||||
teal: "ティール",
|
|
||||||
},
|
|
||||||
interface: { label: "インターフェース", smaller: "縮小", larger: "拡大", reset: "100%に戻す" },
|
interface: { label: "インターフェース", smaller: "縮小", larger: "拡大", reset: "100%に戻す" },
|
||||||
speedUnit: { label: "速度の単位" },
|
speedUnit: { label: "速度の単位" },
|
||||||
transfers: { label: "転送", single: "1件ずつ", multi: "複数" },
|
transfers: { label: "転送", single: "1件ずつ", multi: "複数" },
|
||||||
|
|||||||
@@ -44,17 +44,6 @@ export const zh = {
|
|||||||
system: "系统",
|
system: "系统",
|
||||||
theme: { label: "主题", system: "系统", light: "浅色", dark: "深色" },
|
theme: { label: "主题", system: "系统", light: "浅色", dark: "深色" },
|
||||||
tone: { label: "色调", dark: "深色", darker: "AMOLED" },
|
tone: { label: "色调", dark: "深色", darker: "AMOLED" },
|
||||||
accent: { label: "强调色" },
|
|
||||||
accentNames: {
|
|
||||||
graphite: "石墨",
|
|
||||||
blue: "蓝色",
|
|
||||||
indigo: "靛蓝",
|
|
||||||
purple: "紫色",
|
|
||||||
rose: "玫红",
|
|
||||||
orange: "橙色",
|
|
||||||
green: "绿色",
|
|
||||||
teal: "青色",
|
|
||||||
},
|
|
||||||
interface: { label: "界面", smaller: "缩小", larger: "放大", reset: "重置为100%" },
|
interface: { label: "界面", smaller: "缩小", larger: "放大", reset: "重置为100%" },
|
||||||
speedUnit: { label: "速度单位" },
|
speedUnit: { label: "速度单位" },
|
||||||
transfers: { label: "传输", single: "单个", multi: "多个" },
|
transfers: { label: "传输", single: "单个", multi: "多个" },
|
||||||
|
|||||||
@@ -65,24 +65,12 @@ export interface ConnectionsFile {
|
|||||||
/** UI languages the app ships translations for. */
|
/** UI languages the app ships translations for. */
|
||||||
export type SupportedLocale = "en" | "es" | "eu" | "fr" | "de" | "zh" | "ja";
|
export type SupportedLocale = "en" | "es" | "eu" | "fr" | "de" | "zh" | "ja";
|
||||||
|
|
||||||
export type AccentTone =
|
|
||||||
| "graphite"
|
|
||||||
| "blue"
|
|
||||||
| "indigo"
|
|
||||||
| "purple"
|
|
||||||
| "rose"
|
|
||||||
| "orange"
|
|
||||||
| "green"
|
|
||||||
| "teal";
|
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
/** UI language; "system" resolves to the OS locale (fallback English). */
|
/** UI language; "system" resolves to the OS locale (fallback English). */
|
||||||
locale: "system" | SupportedLocale;
|
locale: "system" | SupportedLocale;
|
||||||
theme: "system" | "light" | "dark";
|
theme: "system" | "light" | "dark";
|
||||||
/** Dark appearance: grey ("dark") or near-black AMOLED ("darker"). */
|
/** Dark appearance: grey ("dark") or near-black AMOLED ("darker"). */
|
||||||
darkTone: "dark" | "darker";
|
darkTone: "dark" | "darker";
|
||||||
/** Accent tone for chrome and selection tint; graphite = neutral zinc. */
|
|
||||||
accent: AccentTone;
|
|
||||||
/** Visible optional file-list columns (both panels; Name always shows). */
|
/** Visible optional file-list columns (both panels; Name always shows). */
|
||||||
columns: ColumnKey[];
|
columns: ColumnKey[];
|
||||||
speedUnit: "MBps" | "Mbps";
|
speedUnit: "MBps" | "Mbps";
|
||||||
@@ -113,7 +101,6 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
locale: "system",
|
locale: "system",
|
||||||
theme: "system",
|
theme: "system",
|
||||||
darkTone: "dark",
|
darkTone: "dark",
|
||||||
accent: "blue",
|
|
||||||
columns: ["size", "mtime"],
|
columns: ["size", "mtime"],
|
||||||
speedUnit: "MBps",
|
speedUnit: "MBps",
|
||||||
uiScale: 1,
|
uiScale: 1,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export class SettingsService {
|
|||||||
readonly locale = signal<AppSettings['locale']>(DEFAULT_SETTINGS.locale);
|
readonly locale = signal<AppSettings['locale']>(DEFAULT_SETTINGS.locale);
|
||||||
readonly theme = signal<AppSettings['theme']>(DEFAULT_SETTINGS.theme);
|
readonly theme = signal<AppSettings['theme']>(DEFAULT_SETTINGS.theme);
|
||||||
readonly darkTone = signal<AppSettings['darkTone']>(DEFAULT_SETTINGS.darkTone);
|
readonly darkTone = signal<AppSettings['darkTone']>(DEFAULT_SETTINGS.darkTone);
|
||||||
readonly accent = signal<AppSettings['accent']>(DEFAULT_SETTINGS.accent);
|
|
||||||
readonly columns = signal<ColumnKey[]>(DEFAULT_SETTINGS.columns);
|
readonly columns = signal<ColumnKey[]>(DEFAULT_SETTINGS.columns);
|
||||||
readonly speedUnit = signal<AppSettings['speedUnit']>(DEFAULT_SETTINGS.speedUnit);
|
readonly speedUnit = signal<AppSettings['speedUnit']>(DEFAULT_SETTINGS.speedUnit);
|
||||||
readonly uiScale = signal<number>(DEFAULT_SETTINGS.uiScale);
|
readonly uiScale = signal<number>(DEFAULT_SETTINGS.uiScale);
|
||||||
@@ -37,8 +36,6 @@ export class SettingsService {
|
|||||||
window
|
window
|
||||||
.matchMedia('(prefers-color-scheme: dark)')
|
.matchMedia('(prefers-color-scheme: dark)')
|
||||||
.addEventListener('change', (e) => this.systemDark.set(e.matches));
|
.addEventListener('change', (e) => this.systemDark.set(e.matches));
|
||||||
// the default accent is not the CSS default — apply before first paint
|
|
||||||
this.applyAccent();
|
|
||||||
if (!this.rpc.available) return;
|
if (!this.rpc.available) return;
|
||||||
void this.rpc.request.getSettings({}).then((res) => {
|
void this.rpc.request.getSettings({}).then((res) => {
|
||||||
if (res.ok) this.applyAll(res.settings);
|
if (res.ok) this.applyAll(res.settings);
|
||||||
@@ -51,7 +48,6 @@ export class SettingsService {
|
|||||||
this.locale.set(settings.locale);
|
this.locale.set(settings.locale);
|
||||||
this.theme.set(settings.theme);
|
this.theme.set(settings.theme);
|
||||||
this.darkTone.set(settings.darkTone);
|
this.darkTone.set(settings.darkTone);
|
||||||
this.accent.set(settings.accent);
|
|
||||||
this.columns.set(settings.columns);
|
this.columns.set(settings.columns);
|
||||||
this.speedUnit.set(settings.speedUnit);
|
this.speedUnit.set(settings.speedUnit);
|
||||||
this.uiScale.set(settings.uiScale);
|
this.uiScale.set(settings.uiScale);
|
||||||
@@ -62,7 +58,6 @@ export class SettingsService {
|
|||||||
this.editorApp.set(settings.editorApp);
|
this.editorApp.set(settings.editorApp);
|
||||||
this.applyTheme();
|
this.applyTheme();
|
||||||
this.applyDarkTone();
|
this.applyDarkTone();
|
||||||
this.applyAccent();
|
|
||||||
this.applyUiScale();
|
this.applyUiScale();
|
||||||
this.applyLocale();
|
this.applyLocale();
|
||||||
}
|
}
|
||||||
@@ -85,12 +80,6 @@ export class SettingsService {
|
|||||||
void this.persist();
|
void this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
setAccent(tone: AppSettings['accent']): void {
|
|
||||||
this.accent.set(tone);
|
|
||||||
this.applyAccent();
|
|
||||||
void this.persist();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleColumn(key: ColumnKey): void {
|
toggleColumn(key: ColumnKey): void {
|
||||||
this.columns.update((list) =>
|
this.columns.update((list) =>
|
||||||
list.includes(key) ? list.filter((c) => c !== key) : [...list, key],
|
list.includes(key) ? list.filter((c) => c !== key) : [...list, key],
|
||||||
@@ -152,15 +141,6 @@ export class SettingsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyAccent(): void {
|
|
||||||
const tone = this.accent();
|
|
||||||
if (tone === 'graphite') {
|
|
||||||
delete document.documentElement.dataset['accent'];
|
|
||||||
} else {
|
|
||||||
document.documentElement.dataset['accent'] = tone;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The whole interface scale is ONE declaration: every size in the app is
|
/** The whole interface scale is ONE declaration: every size in the app is
|
||||||
* rem, so moving the root font-size moves the whole UI. 1 = the
|
* rem, so moving the root font-size moves the whole UI. 1 = the
|
||||||
* browser-default 16px root; delete-when-default keeps the pristine state
|
* browser-default 16px root; delete-when-default keeps the pristine state
|
||||||
@@ -184,7 +164,6 @@ export class SettingsService {
|
|||||||
locale: this.locale(),
|
locale: this.locale(),
|
||||||
theme: this.theme(),
|
theme: this.theme(),
|
||||||
darkTone: this.darkTone(),
|
darkTone: this.darkTone(),
|
||||||
accent: this.accent(),
|
|
||||||
columns: this.columns(),
|
columns: this.columns(),
|
||||||
speedUnit: this.speedUnit(),
|
speedUnit: this.speedUnit(),
|
||||||
uiScale: this.uiScale(),
|
uiScale: this.uiScale(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="flex h-screen flex-col bg-background text-foreground">
|
<div class="flex h-screen flex-col bg-background text-foreground">
|
||||||
@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 rounded-lg 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="1.5rem" class="text-destructive" />
|
<ng-icon name="tdesignCloseCircle" size="1.5rem" 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">
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
class="fixed inset-0 z-60 flex items-center justify-center bg-black/40"
|
class="fixed inset-0 z-60 flex items-center justify-center bg-black/40"
|
||||||
(mousedown)="focusManager()"
|
(mousedown)="focusManager()"
|
||||||
>
|
>
|
||||||
<div class="rounded-lg 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="1.25rem" class="text-muted-foreground" />
|
<ng-icon name="tdesignServer" size="1.25rem" 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
|
||||||
class="mt-3 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
class="mt-3 bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
{{ i18n.t('app.showConnections') }}
|
{{ i18n.t('app.showConnections') }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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-[0.6875rem] 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 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="tdesignLoading" size="0.85rem" class="animate-spin" />
|
<ng-icon name="tdesignLoading" size="0.85rem" class="animate-spin" />
|
||||||
@@ -34,14 +34,14 @@
|
|||||||
<button
|
<button
|
||||||
(click)="newConnection()"
|
(click)="newConnection()"
|
||||||
[title]="i18n.t('connMgr.newConnectionHint')"
|
[title]="i18n.t('connMgr.newConnectionHint')"
|
||||||
class="flex items-center gap-1 rounded-md 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="tdesignAdd" size="0.85rem" /> {{ i18n.t('connMgr.newConnection') }}
|
<ng-icon name="tdesignAdd" size="0.85rem" /> {{ i18n.t('connMgr.newConnection') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="newStack()"
|
(click)="newStack()"
|
||||||
[title]="i18n.t('connMgr.newStackHint')"
|
[title]="i18n.t('connMgr.newStackHint')"
|
||||||
class="flex items-center gap-1 rounded-md 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="0.85rem" /> {{ i18n.t('connMgr.newStack') }}
|
<ng-icon name="tdesignFolderAdd" size="0.85rem" /> {{ i18n.t('connMgr.newStack') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -54,7 +54,6 @@
|
|||||||
(dragover)="onDragOver($event, group.stack.id)"
|
(dragover)="onDragOver($event, group.stack.id)"
|
||||||
(dragleave)="dropTarget() === group.stack.id && dropTarget.set(null)"
|
(dragleave)="dropTarget() === group.stack.id && dropTarget.set(null)"
|
||||||
(drop)="onDrop($event, group.stack.id)"
|
(drop)="onDrop($event, group.stack.id)"
|
||||||
class="rounded-md"
|
|
||||||
[class.bg-primary/10]="dropTarget() === group.stack.id"
|
[class.bg-primary/10]="dropTarget() === group.stack.id"
|
||||||
[class.ring-1]="dropTarget() === group.stack.id"
|
[class.ring-1]="dropTarget() === group.stack.id"
|
||||||
[class.ring-inset]="dropTarget() === group.stack.id"
|
[class.ring-inset]="dropTarget() === group.stack.id"
|
||||||
@@ -63,7 +62,7 @@
|
|||||||
<div class="group flex items-center gap-1 px-2 pt-2 pb-0.5">
|
<div class="group flex items-center gap-1 px-2 pt-2 pb-0.5">
|
||||||
<button
|
<button
|
||||||
(click)="toggleStack(group.stack)"
|
(click)="toggleStack(group.stack)"
|
||||||
class="grid size-4 shrink-0 place-items-center rounded text-muted-foreground hover:text-foreground"
|
class="grid size-4 shrink-0 place-items-center text-muted-foreground hover:text-foreground"
|
||||||
[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
|
||||||
@@ -86,14 +85,14 @@
|
|||||||
<button
|
<button
|
||||||
(click)="renameStack(group.stack)"
|
(click)="renameStack(group.stack)"
|
||||||
[title]="i18n.t('connMgr.renameStack')"
|
[title]="i18n.t('connMgr.renameStack')"
|
||||||
class="grid size-4 place-items-center rounded text-muted-foreground hover:text-foreground"
|
class="grid size-4 place-items-center text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<ng-icon name="tdesignEdit" size="0.75rem" />
|
<ng-icon name="tdesignEdit" size="0.75rem" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="deleteStack(group.stack)"
|
(click)="deleteStack(group.stack)"
|
||||||
[title]="i18n.t('connMgr.deleteStack')"
|
[title]="i18n.t('connMgr.deleteStack')"
|
||||||
class="grid size-4 place-items-center rounded text-muted-foreground hover:text-destructive"
|
class="grid size-4 place-items-center text-muted-foreground hover:text-destructive"
|
||||||
>
|
>
|
||||||
<ng-icon name="tdesignDelete" size="0.75rem" />
|
<ng-icon name="tdesignDelete" size="0.75rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -101,7 +100,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="openStack(group.stack.id)"
|
(click)="openStack(group.stack.id)"
|
||||||
[title]="i18n.t('connMgr.openStack')"
|
[title]="i18n.t('connMgr.openStack')"
|
||||||
class="grid size-4 place-items-center rounded text-muted-foreground hover:text-primary"
|
class="grid size-4 place-items-center text-muted-foreground hover:text-primary"
|
||||||
>
|
>
|
||||||
<ng-icon name="tdesignArrowRight" size="0.8rem" />
|
<ng-icon name="tdesignArrowRight" size="0.8rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -134,7 +133,7 @@
|
|||||||
(dragover)="onDragOver($event, 'loose')"
|
(dragover)="onDragOver($event, 'loose')"
|
||||||
(dragleave)="dropTarget() === 'loose' && dropTarget.set(null)"
|
(dragleave)="dropTarget() === 'loose' && dropTarget.set(null)"
|
||||||
(drop)="onDrop($event, null)"
|
(drop)="onDrop($event, null)"
|
||||||
class="min-h-8 rounded-md pb-1"
|
class="min-h-8 pb-1"
|
||||||
[class.bg-primary/10]="dropTarget() === 'loose'"
|
[class.bg-primary/10]="dropTarget() === 'loose'"
|
||||||
[class.ring-1]="dropTarget() === 'loose'"
|
[class.ring-1]="dropTarget() === 'loose'"
|
||||||
[class.ring-inset]="dropTarget() === 'loose'"
|
[class.ring-inset]="dropTarget() === 'loose'"
|
||||||
@@ -183,7 +182,7 @@
|
|||||||
(input)="patch('name', $any($event.target).value)"
|
(input)="patch('name', $any($event.target).value)"
|
||||||
[placeholder]="i18n.t('connMgr.namePlaceholder')"
|
[placeholder]="i18n.t('connMgr.namePlaceholder')"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
class="h-7 rounded-md border border-input bg-transparent px-2 outline-none focus:ring-2 focus:ring-ring"
|
class="h-7 border border-input bg-transparent px-2 outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -194,7 +193,7 @@
|
|||||||
(input)="patch('host', $any($event.target).value)"
|
(input)="patch('host', $any($event.target).value)"
|
||||||
placeholder="192.168.1.10"
|
placeholder="192.168.1.10"
|
||||||
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 border border-input bg-transparent px-2 font-mono outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
@@ -204,14 +203,14 @@
|
|||||||
type="number"
|
type="number"
|
||||||
[value]="d.port"
|
[value]="d.port"
|
||||||
(input)="patch('port', +$any($event.target).value)"
|
(input)="patch('port', +$any($event.target).value)"
|
||||||
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 border border-input bg-transparent px-2 font-mono outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1 text-xs">
|
<label class="flex flex-col gap-1 text-xs">
|
||||||
<span class="text-muted-foreground">{{ i18n.t('connMgr.stack') }}</span>
|
<span class="text-muted-foreground">{{ i18n.t('connMgr.stack') }}</span>
|
||||||
<select
|
<select
|
||||||
(change)="patch('stackId', $any($event.target).value || null)"
|
(change)="patch('stackId', $any($event.target).value || null)"
|
||||||
class="h-7 rounded-md border border-input bg-background px-1 outline-none focus:ring-2 focus:ring-ring"
|
class="h-7 border border-input bg-background pl-1 outline-none focus:ring-2 focus:ring-ring"
|
||||||
>
|
>
|
||||||
<!-- [selected] per option: binding [value] on the <select> runs
|
<!-- [selected] per option: binding [value] on the <select> runs
|
||||||
before the options render and silently shows "(none)" -->
|
before the options render and silently shows "(none)" -->
|
||||||
@@ -231,7 +230,7 @@
|
|||||||
[value]="d.username"
|
[value]="d.username"
|
||||||
(input)="patch('username', $any($event.target).value)"
|
(input)="patch('username', $any($event.target).value)"
|
||||||
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 border border-input bg-transparent px-2 font-mono outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1 text-xs">
|
<label class="flex flex-col gap-1 text-xs">
|
||||||
@@ -241,7 +240,7 @@
|
|||||||
(input)="patch('initialPath', $any($event.target).value)"
|
(input)="patch('initialPath', $any($event.target).value)"
|
||||||
[placeholder]="i18n.t('connMgr.remotePathPlaceholder')"
|
[placeholder]="i18n.t('connMgr.remotePathPlaceholder')"
|
||||||
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 border border-input bg-transparent px-2 font-mono outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -253,7 +252,6 @@
|
|||||||
name="authKind"
|
name="authKind"
|
||||||
[checked]="d.authKind === 'password'"
|
[checked]="d.authKind === 'password'"
|
||||||
(change)="patch('authKind', 'password')"
|
(change)="patch('authKind', 'password')"
|
||||||
class="accent-primary"
|
|
||||||
/>
|
/>
|
||||||
{{ i18n.t('connMgr.authPassword') }}
|
{{ i18n.t('connMgr.authPassword') }}
|
||||||
</label>
|
</label>
|
||||||
@@ -263,7 +261,6 @@
|
|||||||
name="authKind"
|
name="authKind"
|
||||||
[checked]="d.authKind === 'key'"
|
[checked]="d.authKind === 'key'"
|
||||||
(change)="patch('authKind', 'key')"
|
(change)="patch('authKind', 'key')"
|
||||||
class="accent-primary"
|
|
||||||
/>
|
/>
|
||||||
{{ i18n.t('connMgr.authKey') }}
|
{{ i18n.t('connMgr.authKey') }}
|
||||||
</label>
|
</label>
|
||||||
@@ -276,7 +273,7 @@
|
|||||||
type="password"
|
type="password"
|
||||||
[value]="d.password"
|
[value]="d.password"
|
||||||
(input)="patch('password', $any($event.target).value)"
|
(input)="patch('password', $any($event.target).value)"
|
||||||
class="h-7 rounded-md border border-input bg-transparent px-2 outline-none focus:ring-2 focus:ring-ring"
|
class="h-7 border border-input bg-transparent px-2 outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -288,12 +285,12 @@
|
|||||||
(input)="patch('privateKeyPath', $any($event.target).value)"
|
(input)="patch('privateKeyPath', $any($event.target).value)"
|
||||||
placeholder="~/.ssh/id_ed25519"
|
placeholder="~/.ssh/id_ed25519"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
class="h-7 min-w-0 flex-1 rounded-md border border-input bg-transparent px-2 font-mono outline-none focus:ring-2 focus:ring-ring"
|
class="h-7 min-w-0 flex-1 border border-input bg-transparent px-2 font-mono outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="pickKeyFile()"
|
(click)="pickKeyFile()"
|
||||||
class="h-7 shrink-0 rounded-md border border-border px-2 text-[0.6875rem] hover:bg-accent"
|
class="h-7 shrink-0 border border-border px-2 text-[0.6875rem] hover:bg-accent"
|
||||||
>
|
>
|
||||||
{{ i18n.t('common.choose') }}
|
{{ i18n.t('common.choose') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -305,7 +302,7 @@
|
|||||||
type="password"
|
type="password"
|
||||||
[value]="d.passphrase"
|
[value]="d.passphrase"
|
||||||
(input)="patch('passphrase', $any($event.target).value)"
|
(input)="patch('passphrase', $any($event.target).value)"
|
||||||
class="h-7 rounded-md border border-input bg-transparent px-2 outline-none focus:ring-2 focus:ring-ring"
|
class="h-7 border border-input bg-transparent px-2 outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
@@ -318,7 +315,7 @@
|
|||||||
(input)="patch('localDir', $any($event.target).value)"
|
(input)="patch('localDir', $any($event.target).value)"
|
||||||
[placeholder]="i18n.t('connMgr.localDirPlaceholder')"
|
[placeholder]="i18n.t('connMgr.localDirPlaceholder')"
|
||||||
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 border border-input bg-transparent px-2 font-mono outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
<span class="text-[0.625rem] text-muted-foreground/80">
|
<span class="text-[0.625rem] text-muted-foreground/80">
|
||||||
{{ i18n.t('connMgr.localDirHint') }}
|
{{ i18n.t('connMgr.localDirHint') }}
|
||||||
@@ -330,13 +327,13 @@
|
|||||||
<div class="flex items-center gap-2 border-t border-border bg-chrome px-4 py-2.5">
|
<div class="flex items-center gap-2 border-t border-border bg-chrome px-4 py-2.5">
|
||||||
<button
|
<button
|
||||||
(click)="deleteCurrent()"
|
(click)="deleteCurrent()"
|
||||||
class="flex items-center gap-1.5 rounded-md 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="0.85rem" /> {{ i18n.t('common.delete') }}
|
<ng-icon name="tdesignDelete" size="0.85rem" /> {{ i18n.t('common.delete') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="moreOpen.set(!moreOpen())"
|
(click)="moreOpen.set(!moreOpen())"
|
||||||
class="rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
class="border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
{{ moreOpen() ? i18n.t('connMgr.less') : i18n.t('connMgr.more') }}
|
{{ moreOpen() ? i18n.t('connMgr.less') : i18n.t('connMgr.more') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -344,14 +341,14 @@
|
|||||||
<button
|
<button
|
||||||
(click)="save()"
|
(click)="save()"
|
||||||
[disabled]="!valid()"
|
[disabled]="!valid()"
|
||||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent disabled:opacity-50"
|
class="px-3 py-1.5 text-xs font-medium hover:bg-accent disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{{ i18n.t('common.save') }}
|
{{ i18n.t('common.save') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="saveAndConnect()"
|
(click)="saveAndConnect()"
|
||||||
[disabled]="!valid() || busy()"
|
[disabled]="!valid() || busy()"
|
||||||
class="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
class="flex items-center gap-1.5 bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
@if (busy()) {
|
@if (busy()) {
|
||||||
<ng-icon name="tdesignLoading" size="0.85rem" class="animate-spin" />
|
<ng-icon name="tdesignLoading" size="0.85rem" class="animate-spin" />
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const EDGE_GAP = 6; // px on purpose: a viewport-edge margin, not a UI size
|
|||||||
(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"
|
||||||
class="fixed z-50 w-56 rounded-lg border border-border bg-card py-1 text-card-foreground shadow-xl outline-none"
|
class="fixed z-50 w-56 border border-border bg-card py-1 text-card-foreground shadow-xl outline-none"
|
||||||
>
|
>
|
||||||
@for (item of req.items; track $index) {
|
@for (item of req.items; track $index) {
|
||||||
@if (item.type === 'separator') {
|
@if (item.type === 'separator') {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const PERMS = ['r', 'w', 'x'] as const;
|
|||||||
template: `
|
template: `
|
||||||
<div class="fixed inset-0 z-40 grid place-items-center bg-black/40" (mousedown)="cancel()">
|
<div class="fixed inset-0 z-40 grid place-items-center bg-black/40" (mousedown)="cancel()">
|
||||||
<div
|
<div
|
||||||
class="w-80 rounded-lg border border-border bg-card p-4 text-card-foreground shadow-xl"
|
class="w-80 border border-border bg-card p-4 text-card-foreground shadow-xl"
|
||||||
(mousedown)="$event.stopPropagation()"
|
(mousedown)="$event.stopPropagation()"
|
||||||
(keydown.escape)="cancel()"
|
(keydown.escape)="cancel()"
|
||||||
>
|
>
|
||||||
@@ -37,7 +37,7 @@ const PERMS = ['r', 'w', 'x'] as const;
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
[checked]="hasBit(r, p)"
|
[checked]="hasBit(r, p)"
|
||||||
(change)="toggleBit(r, p)"
|
(change)="toggleBit(r, p)"
|
||||||
class="size-3.5 accent-primary"
|
class="size-3.5"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
@@ -53,20 +53,20 @@ const PERMS = ['r', 'w', 'x'] as const;
|
|||||||
(input)="setOctal($any($event.target).value)"
|
(input)="setOctal($any($event.target).value)"
|
||||||
maxlength="4"
|
maxlength="4"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
class="h-7 w-20 rounded-md border border-input bg-transparent px-2 font-mono text-xs outline-none focus:ring-2 focus:ring-ring"
|
class="h-7 w-20 border border-input bg-transparent px-2 font-mono text-xs outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex justify-end gap-2">
|
<div class="mt-4 flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
(click)="cancel()"
|
(click)="cancel()"
|
||||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
class="px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||||
>
|
>
|
||||||
{{ i18n.t('common.cancel') }}
|
{{ i18n.t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="submit()"
|
(click)="submit()"
|
||||||
class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
class="bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
{{ i18n.t('common.apply') }}
|
{{ i18n.t('common.apply') }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { FocusService } from '@core/focus.service';
|
|||||||
template: `
|
template: `
|
||||||
<div class="fixed inset-0 z-40 grid place-items-center bg-black/40" (mousedown)="cancel()">
|
<div class="fixed inset-0 z-40 grid place-items-center bg-black/40" (mousedown)="cancel()">
|
||||||
<div
|
<div
|
||||||
class="w-96 rounded-lg border border-border bg-card p-4 text-card-foreground shadow-xl"
|
class="w-96 border border-border bg-card p-4 text-card-foreground shadow-xl"
|
||||||
(mousedown)="$event.stopPropagation()"
|
(mousedown)="$event.stopPropagation()"
|
||||||
(keydown.escape)="cancel()"
|
(keydown.escape)="cancel()"
|
||||||
>
|
>
|
||||||
@@ -17,7 +17,7 @@ import { FocusService } from '@core/focus.service';
|
|||||||
<div class="mt-4 flex justify-end gap-2">
|
<div class="mt-4 flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
(click)="cancel()"
|
(click)="cancel()"
|
||||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
class="px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||||
>
|
>
|
||||||
{{ i18n.t('common.cancel') }}
|
{{ i18n.t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -26,8 +26,8 @@ import { FocusService } from '@core/focus.service';
|
|||||||
(click)="submit()"
|
(click)="submit()"
|
||||||
[class]="
|
[class]="
|
||||||
request().destructive
|
request().destructive
|
||||||
? 'rounded-md bg-destructive px-3 py-1.5 text-xs font-medium text-destructive-foreground hover:bg-destructive/90'
|
? 'bg-destructive px-3 py-1.5 text-xs font-medium text-destructive-foreground hover:bg-destructive/90'
|
||||||
: 'rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90'
|
: 'bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ request().confirmLabel }}
|
{{ request().confirmLabel }}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { FocusService } from '@core/focus.service';
|
|||||||
(keydown.escape)="pick('cancel')"
|
(keydown.escape)="pick('cancel')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-[26rem] rounded-lg border border-border bg-card p-4 text-card-foreground shadow-xl"
|
class="w-[26rem] border border-border bg-card p-4 text-card-foreground shadow-xl"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<h2 class="min-w-0 text-sm font-semibold">
|
<h2 class="min-w-0 text-sm font-semibold">
|
||||||
@@ -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-[0.625rem] font-medium text-muted-foreground"
|
class="shrink-0 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>
|
||||||
@@ -47,13 +47,13 @@ import { FocusService } from '@core/focus.service';
|
|||||||
<!-- comparison -->
|
<!-- comparison -->
|
||||||
<div class="mt-3 grid grid-cols-2 gap-2">
|
<div class="mt-3 grid grid-cols-2 gap-2">
|
||||||
@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="border border-border p-2.5">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-[0.625rem] 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-[0.625rem] font-medium text-success">
|
<span class="bg-success/15 px-1.5 text-[0.625rem] font-medium text-success">
|
||||||
{{ i18n.t('dialogs.newer') }}
|
{{ i18n.t('dialogs.newer') }}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ import { FocusService } from '@core/focus.service';
|
|||||||
<div class="mt-4 flex items-center gap-2">
|
<div class="mt-4 flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
(click)="pick('cancel')"
|
(click)="pick('cancel')"
|
||||||
class="rounded-md px-3 py-1.5 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-foreground"
|
class="px-3 py-1.5 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
{{ i18n.t('common.cancel') }}
|
{{ i18n.t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -87,13 +87,13 @@ import { FocusService } from '@core/focus.service';
|
|||||||
@if (request().total > 1) {
|
@if (request().total > 1) {
|
||||||
<button
|
<button
|
||||||
(click)="pick('skipAll')"
|
(click)="pick('skipAll')"
|
||||||
class="rounded-md px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
class="px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
{{ i18n.t('dialogs.skipAll') }}
|
{{ i18n.t('dialogs.skipAll') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="pick('overwriteAll')"
|
(click)="pick('overwriteAll')"
|
||||||
class="rounded-md px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
class="px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
{{ isDir() ? i18n.t('dialogs.mergeAll') : i18n.t('dialogs.replaceAll') }}
|
{{ isDir() ? i18n.t('dialogs.mergeAll') : i18n.t('dialogs.replaceAll') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -101,14 +101,14 @@ import { FocusService } from '@core/focus.service';
|
|||||||
}
|
}
|
||||||
<button
|
<button
|
||||||
(click)="pick('skip')"
|
(click)="pick('skip')"
|
||||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
class="px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||||
>
|
>
|
||||||
{{ i18n.t('dialogs.skip') }}
|
{{ i18n.t('dialogs.skip') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
#primaryBtn
|
#primaryBtn
|
||||||
(click)="pick('overwrite')"
|
(click)="pick('overwrite')"
|
||||||
class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
class="bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
{{ isDir() ? i18n.t('dialogs.merge') : i18n.t('dialogs.replace') }}
|
{{ isDir() ? i18n.t('dialogs.merge') : i18n.t('dialogs.replace') }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { FocusService } from '@core/focus.service';
|
|||||||
template: `
|
template: `
|
||||||
<div class="fixed inset-0 z-40 grid place-items-center bg-black/40" (mousedown)="cancel()">
|
<div class="fixed inset-0 z-40 grid place-items-center bg-black/40" (mousedown)="cancel()">
|
||||||
<div
|
<div
|
||||||
class="w-96 rounded-lg border border-border bg-card p-4 text-card-foreground shadow-xl"
|
class="w-96 border border-border bg-card p-4 text-card-foreground shadow-xl"
|
||||||
(mousedown)="$event.stopPropagation()"
|
(mousedown)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<h2 class="text-sm font-semibold">{{ request().title }}</h2>
|
<h2 class="text-sm font-semibold">{{ request().title }}</h2>
|
||||||
@@ -28,19 +28,19 @@ import { FocusService } from '@core/focus.service';
|
|||||||
(keydown.escape)="cancel()"
|
(keydown.escape)="cancel()"
|
||||||
[placeholder]="request().placeholder ?? ''"
|
[placeholder]="request().placeholder ?? ''"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
class="mt-3 h-8 w-full rounded-md border border-input bg-transparent px-2 text-sm outline-none focus:ring-2 focus:ring-ring"
|
class="mt-3 h-8 w-full border border-input bg-transparent px-2 text-sm outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
<div class="mt-4 flex justify-end gap-2">
|
<div class="mt-4 flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
(click)="cancel()"
|
(click)="cancel()"
|
||||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
class="px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||||
>
|
>
|
||||||
{{ i18n.t('common.cancel') }}
|
{{ i18n.t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="submit()"
|
(click)="submit()"
|
||||||
[disabled]="!value().trim()"
|
[disabled]="!value().trim()"
|
||||||
class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
class="bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{{ request().confirmLabel }}
|
{{ request().confirmLabel }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="flex h-screen flex-col bg-background text-foreground">
|
<div class="flex h-screen flex-col bg-background text-foreground">
|
||||||
@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 rounded-lg 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="1.5rem" class="text-destructive" />
|
<ng-icon name="tdesignCloseCircle" size="1.5rem" 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">
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<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-[0.6875rem] electrobun-webkit-app-region-no-drag hover:bg-accent"
|
class="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()"
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="openFind()"
|
(click)="openFind()"
|
||||||
[title]="i18n.t('editor.findHint', { mod: modLabel })"
|
[title]="i18n.t('editor.findHint', { mod: modLabel })"
|
||||||
class="grid size-6 place-items-center rounded-md 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="0.85rem" />
|
<ng-icon name="tdesignSearch" size="0.85rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
(keydown)="onFindKeydown($event)"
|
(keydown)="onFindKeydown($event)"
|
||||||
[placeholder]="i18n.t('editor.findPlaceholder')"
|
[placeholder]="i18n.t('editor.findPlaceholder')"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
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 border border-input bg-transparent px-2 text-xs outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="text-[0.6875rem] tabular-nums"
|
class="text-[0.6875rem] tabular-nums"
|
||||||
@@ -67,14 +67,14 @@
|
|||||||
<button
|
<button
|
||||||
(click)="findStep(-1)"
|
(click)="findStep(-1)"
|
||||||
[title]="i18n.t('editor.prevMatch')"
|
[title]="i18n.t('editor.prevMatch')"
|
||||||
class="grid size-5 place-items-center rounded 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="0.8rem" />
|
<ng-icon name="tdesignChevronUp" size="0.8rem" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="findStep(1)"
|
(click)="findStep(1)"
|
||||||
[title]="i18n.t('editor.nextMatch')"
|
[title]="i18n.t('editor.nextMatch')"
|
||||||
class="grid size-5 place-items-center rounded 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="0.8rem" />
|
<ng-icon name="tdesignChevronDown" size="0.8rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="closeFind()"
|
(click)="closeFind()"
|
||||||
[title]="i18n.t('editor.closeHint')"
|
[title]="i18n.t('editor.closeHint')"
|
||||||
class="grid size-5 place-items-center rounded 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="0.8rem" />
|
<ng-icon name="tdesignClose" size="0.8rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -104,13 +104,13 @@
|
|||||||
</div>
|
</div>
|
||||||
} @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 rounded-lg 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="1.5rem" class="text-destructive" />
|
<ng-icon name="tdesignCloseCircle" size="1.5rem" 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
|
||||||
(click)="discardAndClose()"
|
(click)="discardAndClose()"
|
||||||
class="mt-4 rounded-md border border-border px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
class="mt-4 border border-border px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||||
>
|
>
|
||||||
{{ i18n.t('editor.closeWindow') }}
|
{{ i18n.t('editor.closeWindow') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -149,14 +149,14 @@
|
|||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
<button
|
<button
|
||||||
(click)="requestClose()"
|
(click)="requestClose()"
|
||||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
class="px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||||
>
|
>
|
||||||
{{ i18n.t('editor.close') }}
|
{{ i18n.t('editor.close') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="save()"
|
(click)="save()"
|
||||||
[disabled]="saving() || !dirty()"
|
[disabled]="saving() || !dirty()"
|
||||||
class="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
class="flex items-center gap-1.5 bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
@if (saving()) {
|
@if (saving()) {
|
||||||
<ng-icon name="tdesignLoading" size="0.85rem" class="animate-spin" />
|
<ng-icon name="tdesignLoading" size="0.85rem" class="animate-spin" />
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
@if (closePrompt()) {
|
@if (closePrompt()) {
|
||||||
<div class="fixed inset-0 z-40 grid place-items-center bg-black/40" (mousedown)="cancelClose()">
|
<div class="fixed inset-0 z-40 grid place-items-center bg-black/40" (mousedown)="cancelClose()">
|
||||||
<div
|
<div
|
||||||
class="w-96 rounded-lg border border-border bg-card p-4 text-card-foreground shadow-xl"
|
class="w-96 border border-border bg-card p-4 text-card-foreground shadow-xl"
|
||||||
(mousedown)="$event.stopPropagation()"
|
(mousedown)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<h2 class="text-sm font-semibold">{{ i18n.t('editor.saveChangesTitle', { name: ctx.name }) }}</h2>
|
<h2 class="text-sm font-semibold">{{ i18n.t('editor.saveChangesTitle', { name: ctx.name }) }}</h2>
|
||||||
@@ -181,21 +181,21 @@
|
|||||||
<div class="mt-4 flex items-center gap-2">
|
<div class="mt-4 flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
(click)="discardAndClose()"
|
(click)="discardAndClose()"
|
||||||
class="rounded-md px-3 py-1.5 text-xs font-medium text-destructive hover:bg-accent"
|
class="px-3 py-1.5 text-xs font-medium text-destructive hover:bg-accent"
|
||||||
>
|
>
|
||||||
{{ i18n.t('editor.dontSave') }}
|
{{ i18n.t('editor.dontSave') }}
|
||||||
</button>
|
</button>
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
<button
|
<button
|
||||||
(click)="cancelClose()"
|
(click)="cancelClose()"
|
||||||
class="rounded-md px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
class="px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||||
>
|
>
|
||||||
{{ i18n.t('common.cancel') }}
|
{{ i18n.t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="saveAndClose()"
|
(click)="saveAndClose()"
|
||||||
[disabled]="saving()"
|
[disabled]="saving()"
|
||||||
class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
class="bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{{ i18n.t('common.save') }}
|
{{ i18n.t('common.save') }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
(click)="toggleMenu()"
|
(click)="toggleMenu()"
|
||||||
class="flex min-w-0 items-center gap-1.5 rounded-md px-2 py-1 text-(length:--path-font) font-medium hover:bg-accent"
|
class="flex min-w-0 items-center gap-1.5 px-2 py-1 text-(length:--path-font) font-medium hover:bg-accent"
|
||||||
[title]="i18n.t('panel.choosePanel')"
|
[title]="i18n.t('panel.choosePanel')"
|
||||||
>
|
>
|
||||||
@if (panel().connecting()) {
|
@if (panel().connecting()) {
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="copyToOther()"
|
(click)="copyToOther()"
|
||||||
[title]="side() === 'left' ? i18n.t('panel.copyToRight') : i18n.t('panel.copyToLeft')"
|
[title]="side() === 'left' ? i18n.t('panel.copyToRight') : i18n.t('panel.copyToLeft')"
|
||||||
class="flex h-(--action-h) shrink-0 items-center rounded-md px-1 text-muted-foreground hover:bg-accent hover:text-primary"
|
class="flex h-(--action-h) shrink-0 items-center px-1 text-muted-foreground hover:bg-accent hover:text-primary"
|
||||||
>
|
>
|
||||||
<ng-icon [name]="copyIcon()" size="var(--action-icon)" />
|
<ng-icon [name]="copyIcon()" size="var(--action-icon)" />
|
||||||
<span class="ml-1 hidden text-(length:--action-font) @[30rem]:inline">{{ i18n.t('panel.copy') }}</span>
|
<span class="ml-1 hidden text-(length:--action-font) @[30rem]:inline">{{ i18n.t('panel.copy') }}</span>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="run('edit')"
|
(click)="run('edit')"
|
||||||
[title]="i18n.t('panel.editFile')"
|
[title]="i18n.t('panel.editFile')"
|
||||||
class="flex h-(--action-h) shrink-0 items-center rounded-md 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(--action-icon)" />
|
<ng-icon name="tdesignFile1" size="var(--action-icon)" />
|
||||||
<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>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="run('rename')"
|
(click)="run('rename')"
|
||||||
[title]="i18n.t('panel.rename')"
|
[title]="i18n.t('panel.rename')"
|
||||||
class="flex h-(--action-h) shrink-0 items-center rounded-md 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(--action-icon)" />
|
<ng-icon name="tdesignEdit" size="var(--action-icon)" />
|
||||||
<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>
|
||||||
@@ -55,14 +55,14 @@
|
|||||||
<button
|
<button
|
||||||
(click)="run('mkdir')"
|
(click)="run('mkdir')"
|
||||||
[title]="i18n.t('panel.newFolder')"
|
[title]="i18n.t('panel.newFolder')"
|
||||||
class="grid size-(--action-h) shrink-0 place-items-center rounded-md 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(--action-icon)" />
|
<ng-icon name="tdesignFolderAdd" size="var(--action-icon)" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="run('createFile')"
|
(click)="run('createFile')"
|
||||||
[title]="i18n.t('panel.newFile')"
|
[title]="i18n.t('panel.newFile')"
|
||||||
class="grid size-(--action-h) shrink-0 place-items-center rounded-md 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(--action-icon)" />
|
<ng-icon name="tdesignFileAdd" size="var(--action-icon)" />
|
||||||
</button>
|
</button>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="run('chmod')"
|
(click)="run('chmod')"
|
||||||
[title]="i18n.t('panel.permissions')"
|
[title]="i18n.t('panel.permissions')"
|
||||||
class="grid size-(--action-h) shrink-0 place-items-center rounded-md 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(--action-icon)" />
|
<ng-icon name="tdesignLockOn" size="var(--action-icon)" />
|
||||||
</button>
|
</button>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="run('delete')"
|
(click)="run('delete')"
|
||||||
[title]="i18n.t('panel.deleteHint')"
|
[title]="i18n.t('panel.deleteHint')"
|
||||||
class="flex h-(--action-h) shrink-0 items-center rounded-md 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(--action-icon)" />
|
<ng-icon name="tdesignDelete" size="var(--action-icon)" />
|
||||||
<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>
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
@if (menuOpen()) {
|
@if (menuOpen()) {
|
||||||
<div class="fixed inset-0 z-20" (click)="menuOpen.set(false)"></div>
|
<div class="fixed inset-0 z-20" (click)="menuOpen.set(false)"></div>
|
||||||
<div
|
<div
|
||||||
class="absolute top-full left-2 z-30 mt-1 max-h-80 w-64 overflow-y-auto rounded-lg border border-border bg-card py-1 text-card-foreground shadow-xl"
|
class="absolute top-full left-2 z-30 mt-1 max-h-80 w-64 overflow-y-auto border border-border bg-card py-1 text-card-foreground shadow-xl"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
(click)="pickLocal()"
|
(click)="pickLocal()"
|
||||||
@@ -161,12 +161,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- path bar -->
|
<!-- path bar -->
|
||||||
<div class="flex items-center gap-1 border-b border-border bg-toolbar px-2 py-1.5">
|
<div class="flex items-center gap-1 border-b border-border bg-toolbar-2 px-2 py-1.5">
|
||||||
<button
|
<button
|
||||||
(click)="panel().up()"
|
(click)="panel().up()"
|
||||||
[disabled]="blocked()"
|
[disabled]="blocked()"
|
||||||
[title]="i18n.t('panel.parentDir')"
|
[title]="i18n.t('panel.parentDir')"
|
||||||
class="grid size-(--path-ctl-h) shrink-0 place-items-center rounded-md 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="1rem" />
|
<ng-icon name="tdesignArrowUp" size="1rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -178,13 +178,13 @@
|
|||||||
(keydown)="$event.stopPropagation()"
|
(keydown)="$event.stopPropagation()"
|
||||||
[disabled]="blocked()"
|
[disabled]="blocked()"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
class="h-(--path-ctl-h) min-w-0 flex-1 rounded-md border border-input bg-transparent px-2 font-mono text-(length:--path-font) outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
|
class="h-(--path-ctl-h) min-w-0 flex-1 border border-input bg-transparent px-2 font-mono text-(length:--path-font) outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
(click)="panel().refresh()"
|
(click)="panel().refresh()"
|
||||||
[disabled]="blocked()"
|
[disabled]="blocked()"
|
||||||
[title]="i18n.t('panel.refresh')"
|
[title]="i18n.t('panel.refresh')"
|
||||||
class="grid size-(--path-ctl-h) shrink-0 place-items-center rounded-md 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"
|
||||||
>
|
>
|
||||||
@if (panel().loading()) {
|
@if (panel().loading()) {
|
||||||
<ng-icon name="tdesignLoading" size="1rem" class="animate-spin" />
|
<ng-icon name="tdesignLoading" size="1rem" class="animate-spin" />
|
||||||
@@ -198,7 +198,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 bg-toolbar text-[0.6875rem] font-medium text-muted-foreground"
|
class="grid border-b border-border bg-toolbar-3 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') }}
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="clearFilter()"
|
(click)="clearFilter()"
|
||||||
[title]="i18n.t('panel.clearFilter')"
|
[title]="i18n.t('panel.clearFilter')"
|
||||||
class="grid size-5 shrink-0 place-items-center rounded 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="0.8rem" />
|
<ng-icon name="tdesignClose" size="0.8rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
(click)="cancelConnect()"
|
(click)="cancelConnect()"
|
||||||
class="rounded-md border border-border px-3 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
class="border border-border px-3 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
{{ i18n.t('common.cancel') }}
|
{{ i18n.t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
@for (s of sections; track s) {
|
@for (s of sections; track s) {
|
||||||
<button
|
<button
|
||||||
(click)="section.set(s)"
|
(click)="section.set(s)"
|
||||||
class="rounded-md px-2.5 py-1.5 text-left text-xs hover:bg-accent hover:text-foreground"
|
class="px-2.5 py-1.5 text-left text-xs hover:bg-accent hover:text-foreground"
|
||||||
[class.bg-accent]="section() === s"
|
[class.bg-accent]="section() === s"
|
||||||
[class.font-medium]="section() === s"
|
[class.font-medium]="section() === s"
|
||||||
[class.text-foreground]="section() === s"
|
[class.text-foreground]="section() === s"
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<span class="text-xs">{{ i18n.t('options.language') }}</span>
|
<span class="text-xs">{{ 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-[0.6875rem] outline-none focus:ring-2 focus:ring-ring"
|
class="h-7 border border-input bg-background pl-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') }}
|
||||||
@@ -55,11 +55,11 @@
|
|||||||
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-xs">{{ i18n.t('options.theme.label') }}</span>
|
<span class="text-xs">{{ i18n.t('options.theme.label') }}</span>
|
||||||
<div class="flex rounded-md border border-border p-0.5">
|
<div class="flex border border-border p-0.5">
|
||||||
@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-[0.6875rem]"
|
class="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"
|
||||||
@@ -73,11 +73,11 @@
|
|||||||
@if (settings.effectiveDark()) {
|
@if (settings.effectiveDark()) {
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-xs">{{ i18n.t('options.tone.label') }}</span>
|
<span class="text-xs">{{ i18n.t('options.tone.label') }}</span>
|
||||||
<div class="flex rounded-md border border-border p-0.5">
|
<div class="flex border border-border p-0.5">
|
||||||
@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-[0.6875rem]"
|
class="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"
|
||||||
@@ -89,42 +89,24 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<span class="text-xs">{{ i18n.t('options.accent.label') }}</span>
|
|
||||||
<div class="flex items-center gap-1.5">
|
|
||||||
@for (tone of accents; track tone.value) {
|
|
||||||
<button
|
|
||||||
(click)="settings.setAccent(tone.value)"
|
|
||||||
[title]="i18n.t('options.accentNames.' + tone.value)"
|
|
||||||
class="size-3 rounded-full border border-foreground/15"
|
|
||||||
[style.background-color]="tone.swatch"
|
|
||||||
[class.ring-2]="settings.accent() === tone.value"
|
|
||||||
[class.ring-foreground]="settings.accent() === tone.value"
|
|
||||||
[class.ring-offset-1]="settings.accent() === tone.value"
|
|
||||||
[class.ring-offset-background]="settings.accent() === tone.value"
|
|
||||||
></button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Interface scale: a continuous stepper, 50%..400% in 10-point
|
<!-- Interface scale: a continuous stepper, 50%..400% in 10-point
|
||||||
steps. One lever moves the whole UI (root font-size; everything
|
steps. One lever moves the whole UI (root font-size; everything
|
||||||
is rem), this window included. -->
|
is rem), this window included. -->
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-xs">{{ i18n.t('options.interface.label') }}</span>
|
<span class="text-xs">{{ i18n.t('options.interface.label') }}</span>
|
||||||
<div class="flex items-center rounded-md border border-border p-0.5">
|
<div class="flex items-center border border-border p-0.5">
|
||||||
<button
|
<button
|
||||||
(click)="settings.setUiScale(settings.uiScale() - 0.1)"
|
(click)="settings.setUiScale(settings.uiScale() - 0.1)"
|
||||||
[disabled]="settings.uiScale() <= 0.5"
|
[disabled]="settings.uiScale() <= 0.5"
|
||||||
[title]="i18n.t('options.interface.smaller')"
|
[title]="i18n.t('options.interface.smaller')"
|
||||||
class="grid size-5 place-items-center rounded text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
class="grid size-5 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
||||||
>
|
>
|
||||||
−
|
−
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="settings.setUiScale(1)"
|
(click)="settings.setUiScale(1)"
|
||||||
[title]="i18n.t('options.interface.reset')"
|
[title]="i18n.t('options.interface.reset')"
|
||||||
class="w-11 rounded px-1 py-0.5 text-center text-[0.6875rem] tabular-nums hover:bg-accent"
|
class="w-11 px-1 py-0.5 text-center text-[0.6875rem] tabular-nums hover:bg-accent"
|
||||||
>
|
>
|
||||||
{{ scalePercent() }}
|
{{ scalePercent() }}
|
||||||
</button>
|
</button>
|
||||||
@@ -132,7 +114,7 @@
|
|||||||
(click)="settings.setUiScale(settings.uiScale() + 0.1)"
|
(click)="settings.setUiScale(settings.uiScale() + 0.1)"
|
||||||
[disabled]="settings.uiScale() >= 4"
|
[disabled]="settings.uiScale() >= 4"
|
||||||
[title]="i18n.t('options.interface.larger')"
|
[title]="i18n.t('options.interface.larger')"
|
||||||
class="grid size-5 place-items-center rounded text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
class="grid size-5 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
@@ -143,11 +125,11 @@
|
|||||||
@case ('transfers') {
|
@case ('transfers') {
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-xs">{{ i18n.t('options.speedUnit.label') }}</span>
|
<span class="text-xs">{{ i18n.t('options.speedUnit.label') }}</span>
|
||||||
<div class="flex rounded-md border border-border p-0.5">
|
<div class="flex border border-border p-0.5">
|
||||||
@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-[0.6875rem]"
|
class="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"
|
||||||
@@ -160,11 +142,11 @@
|
|||||||
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-xs">{{ i18n.t('options.transfers.label') }}</span>
|
<span class="text-xs">{{ i18n.t('options.transfers.label') }}</span>
|
||||||
<div class="flex rounded-md border border-border p-0.5">
|
<div class="flex border border-border p-0.5">
|
||||||
@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-[0.6875rem]"
|
class="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"
|
||||||
@@ -178,11 +160,11 @@
|
|||||||
@if (settings.transferMode() === 'multi') {
|
@if (settings.transferMode() === 'multi') {
|
||||||
<div class="flex items-center justify-between gap-2 pl-3">
|
<div class="flex items-center justify-between gap-2 pl-3">
|
||||||
<span class="text-xs text-muted-foreground">{{ i18n.t('options.atOnce') }}</span>
|
<span class="text-xs text-muted-foreground">{{ i18n.t('options.atOnce') }}</span>
|
||||||
<div class="flex rounded-md border border-border p-0.5">
|
<div class="flex border border-border p-0.5">
|
||||||
@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-[0.6875rem] tabular-nums"
|
class="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"
|
||||||
@@ -196,10 +178,10 @@
|
|||||||
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-xs">{{ i18n.t('options.finishedTransfers.label') }}</span>
|
<span class="text-xs">{{ i18n.t('options.finishedTransfers.label') }}</span>
|
||||||
<div class="flex rounded-md border border-border p-0.5">
|
<div class="flex border border-border p-0.5">
|
||||||
<button
|
<button
|
||||||
(click)="settings.setAutoClearCompleted(true)"
|
(click)="settings.setAutoClearCompleted(true)"
|
||||||
class="rounded px-2 py-0.5 text-[0.6875rem]"
|
class="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()"
|
||||||
@@ -208,7 +190,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="settings.setAutoClearCompleted(false)"
|
(click)="settings.setAutoClearCompleted(false)"
|
||||||
class="rounded px-2 py-0.5 text-[0.6875rem]"
|
class="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()"
|
||||||
@@ -222,10 +204,10 @@
|
|||||||
@case ('editor') {
|
@case ('editor') {
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-xs">{{ i18n.t('options.editor.label') }}</span>
|
<span class="text-xs">{{ i18n.t('options.editor.label') }}</span>
|
||||||
<div class="flex rounded-md border border-border p-0.5">
|
<div class="flex border border-border p-0.5">
|
||||||
<button
|
<button
|
||||||
(click)="settings.setEditor('builtin')"
|
(click)="settings.setEditor('builtin')"
|
||||||
class="rounded px-2 py-0.5 text-[0.6875rem]"
|
class="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'"
|
||||||
@@ -234,7 +216,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="settings.setEditor('custom')"
|
(click)="settings.setEditor('custom')"
|
||||||
class="rounded px-2 py-0.5 text-[0.6875rem]"
|
class="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'"
|
||||||
@@ -252,7 +234,7 @@
|
|||||||
<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-[0.6875rem] hover:bg-accent"
|
class="border border-border px-2 py-0.5 text-[0.6875rem] hover:bg-accent"
|
||||||
>
|
>
|
||||||
{{ i18n.t('common.choose') }}
|
{{ i18n.t('common.choose') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -260,7 +242,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="settings.setEditorApp('')"
|
(click)="settings.setEditorApp('')"
|
||||||
[title]="i18n.t('options.resetDefault')"
|
[title]="i18n.t('options.resetDefault')"
|
||||||
class="grid size-5 place-items-center rounded 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="0.7rem" />
|
<ng-icon name="tdesignClose" size="0.7rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -277,7 +259,6 @@
|
|||||||
<label class="flex items-center gap-2 text-xs">
|
<label class="flex items-center gap-2 text-xs">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="accent-primary"
|
|
||||||
[checked]="settings.columns().includes(key)"
|
[checked]="settings.columns().includes(key)"
|
||||||
(change)="settings.toggleColumn(key)"
|
(change)="settings.toggleColumn(key)"
|
||||||
/>
|
/>
|
||||||
@@ -291,7 +272,7 @@
|
|||||||
<!-- the old popover footer: opens the native About box -->
|
<!-- the old popover footer: opens the native About box -->
|
||||||
<button
|
<button
|
||||||
(click)="showAbout()"
|
(click)="showAbout()"
|
||||||
class="self-start rounded-md border border-border px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
class="self-start border border-border px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
{{ appName }} · {{ buildLabel }}
|
{{ appName }} · {{ buildLabel }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Component, computed, inject, signal } from '@angular/core';
|
|||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { BRANDING } from '@shared/branding';
|
import { BRANDING } from '@shared/branding';
|
||||||
import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n';
|
import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n';
|
||||||
import type { AccentTone } from '@shared/types';
|
|
||||||
import { COLUMN_KEYS } from '@core/context-menu.service';
|
import { COLUMN_KEYS } from '@core/context-menu.service';
|
||||||
import { I18nService } from '@core/i18n.service';
|
import { I18nService } from '@core/i18n.service';
|
||||||
import { blockNativeContextMenu, IS_MAC_CHROME } from '@core/platform';
|
import { blockNativeContextMenu, IS_MAC_CHROME } from '@core/platform';
|
||||||
@@ -15,19 +14,6 @@ type Section = (typeof SECTIONS)[number];
|
|||||||
|
|
||||||
const THEMES = ['system', 'light', 'dark'] as const;
|
const THEMES = ['system', 'light', 'dark'] as const;
|
||||||
const DARK_TONES = ['dark', 'darker'] as const;
|
const DARK_TONES = ['dark', 'darker'] as const;
|
||||||
// Swatch colors are fixed mid-tones so every dot reads on both light and dark
|
|
||||||
// pages (the live --primary values would vanish against one of them). Names are
|
|
||||||
// translated via options.accentNames.<value>.
|
|
||||||
const ACCENTS: readonly { value: AccentTone; swatch: string }[] = [
|
|
||||||
{ value: 'graphite', swatch: 'oklch(0.552 0.016 285.9)' },
|
|
||||||
{ value: 'blue', swatch: 'oklch(0.65 0.17 255)' },
|
|
||||||
{ value: 'indigo', swatch: 'oklch(0.65 0.19 277)' },
|
|
||||||
{ value: 'purple', swatch: 'oklch(0.65 0.17 305)' },
|
|
||||||
{ value: 'rose', swatch: 'oklch(0.65 0.17 8)' },
|
|
||||||
{ value: 'orange', swatch: 'oklch(0.68 0.17 50)' },
|
|
||||||
{ value: 'green', swatch: 'oklch(0.65 0.15 150)' },
|
|
||||||
{ value: 'teal', swatch: 'oklch(0.65 0.12 185)' },
|
|
||||||
];
|
|
||||||
const SPEED_UNITS = [
|
const SPEED_UNITS = [
|
||||||
{ value: 'MBps', label: 'MB/s' },
|
{ value: 'MBps', label: 'MB/s' },
|
||||||
{ value: 'Mbps', label: 'Mbps' },
|
{ value: 'Mbps', label: 'Mbps' },
|
||||||
@@ -62,7 +48,6 @@ export class SettingsWindow {
|
|||||||
readonly localeLabels = LOCALE_LABELS;
|
readonly localeLabels = LOCALE_LABELS;
|
||||||
readonly themes = THEMES;
|
readonly themes = THEMES;
|
||||||
readonly darkTones = DARK_TONES;
|
readonly darkTones = DARK_TONES;
|
||||||
readonly accents = ACCENTS;
|
|
||||||
readonly speedUnits = SPEED_UNITS;
|
readonly speedUnits = SPEED_UNITS;
|
||||||
readonly transferModes = TRANSFER_MODES;
|
readonly transferModes = TRANSFER_MODES;
|
||||||
readonly concurrencyChoices = CONCURRENCY_CHOICES;
|
readonly concurrencyChoices = CONCURRENCY_CHOICES;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { UpdaterService } from '@core/updater.service';
|
|||||||
@for (ws of panels.workspaces(); track ws.id; let i = $index) {
|
@for (ws of panels.workspaces(); track ws.id; let i = $index) {
|
||||||
<div
|
<div
|
||||||
[attr.data-tab-id]="ws.id"
|
[attr.data-tab-id]="ws.id"
|
||||||
class="group relative flex max-w-52 shrink-0 items-center rounded-md electrobun-webkit-app-region-no-drag"
|
class="group relative flex max-w-52 shrink-0 items-center electrobun-webkit-app-region-no-drag"
|
||||||
[class.bg-accent]="ws.id === panels.activeWorkspaceId()"
|
[class.bg-accent]="ws.id === panels.activeWorkspaceId()"
|
||||||
[class.opacity-50]="draggedId() === ws.id"
|
[class.opacity-50]="draggedId() === ws.id"
|
||||||
[style.box-shadow]="dropIndicatorShadow(i)"
|
[style.box-shadow]="dropIndicatorShadow(i)"
|
||||||
@@ -64,7 +64,7 @@ import { UpdaterService } from '@core/updater.service';
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
[title]="i18n.t('tabBar.closeTab', { mod: modLabel })"
|
[title]="i18n.t('tabBar.closeTab', { mod: modLabel })"
|
||||||
class="mr-1 grid size-4 shrink-0 place-items-center rounded 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="0.7rem" />
|
<ng-icon name="tdesignClose" size="0.7rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -72,7 +72,7 @@ import { UpdaterService } from '@core/updater.service';
|
|||||||
<span class="mr-1 size-4 shrink-0"></span>
|
<span class="mr-1 size-4 shrink-0"></span>
|
||||||
}
|
}
|
||||||
@if (ws.id === panels.activeWorkspaceId()) {
|
@if (ws.id === panels.activeWorkspaceId()) {
|
||||||
<div class="absolute inset-x-0 -bottom-px h-0.5 rounded-full bg-primary"></div>
|
<div class="absolute inset-x-0 -bottom-px h-0.5 bg-primary"></div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ import { UpdaterService } from '@core/updater.service';
|
|||||||
<button
|
<button
|
||||||
(click)="panels.newWorkspace()"
|
(click)="panels.newWorkspace()"
|
||||||
[title]="i18n.t('tabBar.newTab', { mod: modLabel })"
|
[title]="i18n.t('tabBar.newTab', { mod: modLabel })"
|
||||||
class="ml-0.5 grid size-6 place-items-center rounded-md 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="tdesignAdd" size=".8rem" />
|
<ng-icon name="tdesignAdd" size=".8rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -94,7 +94,7 @@ import { UpdaterService } from '@core/updater.service';
|
|||||||
(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-[0.6875rem] 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 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
|
||||||
@@ -122,7 +122,7 @@ import { UpdaterService } from '@core/updater.service';
|
|||||||
<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-[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="0.9rem" /> -->
|
<!-- <ng-icon name="tdesignSetting" size="0.9rem" /> -->
|
||||||
{{ i18n.t('tabBar.connections') }}
|
{{ i18n.t('tabBar.connections') }}
|
||||||
@@ -132,7 +132,7 @@ import { UpdaterService } from '@core/updater.service';
|
|||||||
<button
|
<button
|
||||||
(click)="openSettings()"
|
(click)="openSettings()"
|
||||||
[title]="i18n.t('tabBar.options')"
|
[title]="i18n.t('tabBar.options')"
|
||||||
class="grid size-6 place-items-center rounded-md 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="0.9rem" />
|
<ng-icon name="tdesignSetting" size="0.9rem" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { ToastService } from '@core/toast.service';
|
|||||||
<div
|
<div
|
||||||
(mouseenter)="toast.pause(t.id)"
|
(mouseenter)="toast.pause(t.id)"
|
||||||
(mouseleave)="toast.resume(t.id)"
|
(mouseleave)="toast.resume(t.id)"
|
||||||
class="group pointer-events-auto relative flex items-start gap-2 overflow-hidden rounded-lg border border-border bg-card p-3 text-xs text-card-foreground shadow-lg"
|
class="group pointer-events-auto relative flex items-start gap-2 overflow-hidden border border-border bg-card p-3 text-xs text-card-foreground shadow-lg"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
[name]="toastIcon(t.kind)"
|
[name]="toastIcon(t.kind)"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="transfers.clearFinished()"
|
(click)="transfers.clearFinished()"
|
||||||
[disabled]="!transfers.finishedJobs().length"
|
[disabled]="!transfers.finishedJobs().length"
|
||||||
class="flex items-center gap-1 rounded 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="0.75rem" /> {{ i18n.t('transfer.clearFinished') }}
|
<ng-icon name="tdesignDelete" size="0.75rem" /> {{ i18n.t('transfer.clearFinished') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -25,10 +25,10 @@
|
|||||||
aria-valuemax="100"
|
aria-valuemax="100"
|
||||||
[attr.aria-valuenow]="jobPercent(job)"
|
[attr.aria-valuenow]="jobPercent(job)"
|
||||||
[attr.aria-label]="job.name"
|
[attr.aria-label]="job.name"
|
||||||
class="mt-0.5 h-1 overflow-hidden rounded-full bg-muted"
|
class="mt-0.5 h-1 overflow-hidden bg-muted"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="h-full rounded-full bg-primary transition-[width] duration-100"
|
class="h-full bg-primary transition-[width] duration-100"
|
||||||
[style.width.%]="jobPercent(job)"
|
[style.width.%]="jobPercent(job)"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="transfers.cancel(job.id)"
|
(click)="transfers.cancel(job.id)"
|
||||||
[title]="i18n.t('transfer.cancel')"
|
[title]="i18n.t('transfer.cancel')"
|
||||||
class="shrink-0 rounded 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="0.9rem" />
|
<ng-icon name="tdesignClose" size="0.9rem" />
|
||||||
</button>
|
</button>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="transfers.dismiss(job.id)"
|
(click)="transfers.dismiss(job.id)"
|
||||||
[title]="i18n.t('transfer.dismiss')"
|
[title]="i18n.t('transfer.dismiss')"
|
||||||
class="shrink-0 rounded 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="0.9rem" />
|
<ng-icon name="tdesignClose" size="0.9rem" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+3
-3
@@ -13,11 +13,11 @@
|
|||||||
attributes are baked in by the bun preload (buildPreload in
|
attributes are baked in by the bun preload (buildPreload in
|
||||||
src/bun/windows.ts) so forced themes are correct at first paint. -->
|
src/bun/windows.ts) so forced themes are correct at first paint. -->
|
||||||
<style>
|
<style>
|
||||||
html { background: oklch(1 0 0); }
|
html { background: oklch(0.995 0.0001 260); }
|
||||||
html[data-theme='dark'] { background: oklch(0.2178 0.002 107); }
|
html[data-theme='dark'] { background: oklch(0.195 0.009 260); }
|
||||||
html[data-theme='dark'][data-dark-tone='darker'] { background: oklch(0 0 0); }
|
html[data-theme='dark'][data-dark-tone='darker'] { background: oklch(0 0 0); }
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
html:not([data-theme='light']) { background: oklch(0.2178 0.002 107); }
|
html:not([data-theme='light']) { background: oklch(0.195 0.009 260); }
|
||||||
html:not([data-theme='light'])[data-dark-tone='darker'] { background: oklch(0 0 0); }
|
html:not([data-theme='light'])[data-dark-tone='darker'] { background: oklch(0 0 0); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+198
-164
@@ -1,50 +1,54 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
/* shadcn-style theme tokens. Light stays the zinc family; the dark neutrals are a
|
/* Theme tokens, adapted from Oxide's design system (docs.oxide.computer): a cool
|
||||||
warm gray family (hue ~107) anchored on #1a1a19 — see the dark blocks below.
|
neutral scale at hue 260 with almost no chroma, one green accent, opaque
|
||||||
The app follows the OS light/dark preference automatically.
|
strokes. Step names in the comments (n0…n1300, green-800, …) are Oxide's.
|
||||||
|
The app follows the OS light/dark preference unless a theme is forced.
|
||||||
|
|
||||||
Surface layers, each its own shade so the window reads as parts: --chrome
|
Surface layers, each its own shade so the window reads as parts: --chrome
|
||||||
(frame strips: tab/transfer/status bars, window header/footer strips),
|
(frame strips: tab/transfer/status bars, window header/footer strips and
|
||||||
--toolbar (the file-panel header strip — toolbar, path bar, column header —
|
sidebars), --toolbar (the top row of a file panel's header strip, and
|
||||||
and secondary toolbars), --background (content: file lists, editor text,
|
secondary toolbars) stepping through --toolbar-2/-3 toward --background
|
||||||
forms; --row-alt stripes every other file row) and --card (popovers,
|
(content: file lists, editor text, forms; --row-alt stripes every other file
|
||||||
dialogs). Dark climbs frame < list < header < card; light steps greys out
|
row), and --card (popovers, dialogs). Dark climbs frame < list < header <
|
||||||
from white content: frame < header < list = card. --accent is a translucent
|
card; light steps greys out from near-white content: frame < header < list
|
||||||
overlay, not a surface: hover/selected states must read on every layer, and
|
= card. --accent is a translucent overlay, not a surface: hover/selected
|
||||||
an opaque hover vanishes on a surface of its own lightness. */
|
states must read on every layer, and an opaque hover vanishes on a surface
|
||||||
|
of its own lightness. The design is square: there are no radius tokens. */
|
||||||
:root {
|
:root {
|
||||||
/* Native UI (scrollbars, form controls) follows the OS unless a theme is forced. */
|
/* Native UI (form controls) follows the OS unless a theme is forced. */
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(0.995 0.0001 260); /* n1300 */
|
||||||
--chrome: oklch(0.935 0.002 286.375);
|
--chrome: oklch(0.925 0.0001 260); /* n1000 */
|
||||||
--toolbar: oklch(0.965 0.001 286.375);
|
--toolbar: oklch(0.95 0.0001 260); /* n1100 */
|
||||||
--row-alt: oklch(0.978 0.001 286.375);
|
--row-alt: oklch(0.975 0.0001 260); /* n1200 */
|
||||||
--foreground: oklch(0.141 0.005 285.823);
|
--foreground: oklch(0.162 0.01 260); /* n0 */
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(0.995 0.0001 260);
|
||||||
--card-foreground: oklch(0.141 0.005 285.823);
|
--card-foreground: oklch(0.162 0.01 260);
|
||||||
--primary: oklch(0.21 0.006 285.885);
|
--primary: oklch(0.474 0.1481 173.7); /* green-500 */
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.995 0.0001 260);
|
||||||
--secondary: oklch(0.967 0.001 286.375);
|
--muted: oklch(0.95 0.0001 260); /* n1100 */
|
||||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
--muted-foreground: oklch(0.483 0.0041 260); /* n500 */
|
||||||
--muted: oklch(0.967 0.001 286.375);
|
--accent: oklch(0.162 0.01 260 / 5%);
|
||||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
--destructive: oklch(0.474 0.1285 21.3); /* red-500 */
|
||||||
--accent: oklch(0.21 0.006 285.885 / 6%);
|
--destructive-foreground: oklch(0.995 0.0001 260);
|
||||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
--success: oklch(0.474 0.1481 173.7); /* green-500 */
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--border: oklch(0.9 0.0001 260); /* n900 */
|
||||||
--destructive-foreground: oklch(0.985 0 0);
|
--input: oklch(0.79 0.0011 260); /* n800 */
|
||||||
--success: oklch(0.627 0.17 149.2);
|
--ring: oklch(0.563 0.1714 170.4); /* green-600 */
|
||||||
--border: oklch(0.92 0.004 286.32);
|
--cursor-tint: color-mix(in oklab, var(--primary) 8%, transparent);
|
||||||
--input: oklch(0.92 0.004 286.32);
|
/* The file panel's header strip steps toward the list, a third per row.
|
||||||
--ring: oklch(0.705 0.015 286.067);
|
Declared once: the var()s resolve on :root, so dark and AMOLED (where both
|
||||||
--radius: 0.5rem;
|
ends are black) follow without their own lines. */
|
||||||
/* file-type icon colors — fixed category hues, muted chroma */
|
--toolbar-2: color-mix(in oklab, var(--toolbar) 66%, var(--background));
|
||||||
--icon-folder: oklch(0.58 0.13 250);
|
--toolbar-3: color-mix(in oklab, var(--toolbar) 33%, var(--background));
|
||||||
--icon-link: oklch(0.56 0.1 190);
|
/* file-type icon colors — fixed category hues from Oxide's scales (600 steps) */
|
||||||
--icon-image: oklch(0.58 0.13 295);
|
--icon-folder: oklch(0.563 0.1139 278.7); /* blue-600 */
|
||||||
--icon-media: oklch(0.6 0.13 10);
|
--icon-link: oklch(0.67 0.1216 275.3); /* blue-700 */
|
||||||
--icon-archive: oklch(0.62 0.13 60);
|
--icon-image: oklch(0.563 0.1328 312.1); /* purple-600 */
|
||||||
--icon-code: oklch(0.56 0.12 155);
|
--icon-media: oklch(0.563 0.1487 18); /* red-600 */
|
||||||
|
--icon-archive: oklch(0.563 0.125 81.7); /* yellow-600 */
|
||||||
|
--icon-code: oklch(0.563 0.1714 170.4); /* green-600 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark tokens apply when the user forces dark, or follows a dark system theme.
|
/* Dark tokens apply when the user forces dark, or follows a dark system theme.
|
||||||
@@ -55,91 +59,80 @@
|
|||||||
|
|
||||||
:root[data-theme='dark'] {
|
:root[data-theme='dark'] {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--background: oklch(0.2178 0.002 107);
|
--background: oklch(0.195 0.009 260); /* n50 */
|
||||||
--chrome: oklch(0.165 0.002 107);
|
--chrome: oklch(0.162 0.01 260); /* n0 */
|
||||||
--toolbar: oklch(0.265 0.002 107);
|
--toolbar: oklch(0.275 0.0065 260); /* between n200 and n300 */
|
||||||
--row-alt: oklch(0.232 0.002 107);
|
--row-alt: oklch(0.205 0.0085 260); /* between n50 and n100 */
|
||||||
--foreground: oklch(0.982 0.003 107);
|
--foreground: oklch(0.9 0.0001 260); /* n900 */
|
||||||
/* above --toolbar, so a popover never reads darker than the strip it covers */
|
/* above --toolbar, so a popover never reads darker than the strip it covers */
|
||||||
--card: oklch(0.285 0.002 107);
|
--card: oklch(0.29 0.0065 260);
|
||||||
--card-foreground: oklch(0.982 0.003 107);
|
--card-foreground: oklch(0.9 0.0001 260);
|
||||||
--primary: oklch(0.92 0.004 107);
|
--primary: oklch(0.77 0.1919 163.7); /* green-800 */
|
||||||
--primary-foreground: oklch(0.21 0.002 107);
|
--primary-foreground: oklch(0.162 0.01 260);
|
||||||
--secondary: oklch(0.302 0.003 107);
|
--muted: oklch(0.316 0.006 260); /* n300 */
|
||||||
--secondary-foreground: oklch(0.982 0.003 107);
|
--muted-foreground: oklch(0.717 0.0021 260); /* n700 */
|
||||||
--muted: oklch(0.335 0.003 107);
|
--accent: oklch(1 0 0 / 7%);
|
||||||
--muted-foreground: oklch(0.715 0.012 107);
|
--destructive: oklch(0.712 0.185 11.3); /* red-800 */
|
||||||
--accent: oklch(1 0 0 / 9%);
|
--destructive-foreground: oklch(0.162 0.01 260);
|
||||||
--accent-foreground: oklch(0.982 0.003 107);
|
--success: oklch(0.77 0.1919 163.7); /* green-800 */
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--border: oklch(0.316 0.006 260); /* n300 */
|
||||||
--destructive-foreground: oklch(0.982 0.003 107);
|
--input: oklch(0.389 0.005 260); /* n400 */
|
||||||
--success: oklch(0.696 0.17 149.2);
|
--ring: oklch(0.563 0.1714 170.4); /* green-600 */
|
||||||
--border: oklch(1 0 0 / 14%);
|
--cursor-tint: color-mix(in oklab, var(--primary) 12%, transparent);
|
||||||
--input: oklch(1 0 0 / 18%);
|
--icon-folder: oklch(0.71 0.15 272); /* blue-800 */
|
||||||
--ring: oklch(0.552 0.01 107);
|
--icon-link: oklch(0.67 0.1216 275.3); /* blue-700 */
|
||||||
--icon-folder: oklch(0.75 0.11 250);
|
--icon-image: oklch(0.74 0.175 305.4); /* purple-800 */
|
||||||
--icon-link: oklch(0.74 0.09 190);
|
--icon-media: oklch(0.712 0.185 11.3); /* red-800 */
|
||||||
--icon-image: oklch(0.76 0.11 295);
|
--icon-archive: oklch(0.837 0.14 75); /* yellow-800 */
|
||||||
--icon-media: oklch(0.76 0.11 10);
|
--icon-code: oklch(0.77 0.1919 163.7); /* green-800 */
|
||||||
--icon-archive: oklch(0.77 0.11 65);
|
|
||||||
--icon-code: oklch(0.75 0.1 155);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root:not([data-theme='light']) {
|
:root:not([data-theme='light']) {
|
||||||
--background: oklch(0.2178 0.002 107);
|
--background: oklch(0.195 0.009 260);
|
||||||
--chrome: oklch(0.165 0.002 107);
|
--chrome: oklch(0.162 0.01 260);
|
||||||
--toolbar: oklch(0.265 0.002 107);
|
--toolbar: oklch(0.275 0.0065 260);
|
||||||
--row-alt: oklch(0.232 0.002 107);
|
--row-alt: oklch(0.205 0.0085 260);
|
||||||
--foreground: oklch(0.982 0.003 107);
|
--foreground: oklch(0.9 0.0001 260);
|
||||||
--card: oklch(0.285 0.002 107);
|
--card: oklch(0.29 0.0065 260);
|
||||||
--card-foreground: oklch(0.982 0.003 107);
|
--card-foreground: oklch(0.9 0.0001 260);
|
||||||
--primary: oklch(0.92 0.004 107);
|
--primary: oklch(0.77 0.1919 163.7);
|
||||||
--primary-foreground: oklch(0.21 0.002 107);
|
--primary-foreground: oklch(0.162 0.01 260);
|
||||||
--secondary: oklch(0.302 0.003 107);
|
--muted: oklch(0.316 0.006 260);
|
||||||
--secondary-foreground: oklch(0.982 0.003 107);
|
--muted-foreground: oklch(0.717 0.0021 260);
|
||||||
--muted: oklch(0.335 0.003 107);
|
--accent: oklch(1 0 0 / 7%);
|
||||||
--muted-foreground: oklch(0.715 0.012 107);
|
--destructive: oklch(0.712 0.185 11.3);
|
||||||
--accent: oklch(1 0 0 / 9%);
|
--destructive-foreground: oklch(0.162 0.01 260);
|
||||||
--accent-foreground: oklch(0.982 0.003 107);
|
--success: oklch(0.77 0.1919 163.7);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--border: oklch(0.316 0.006 260);
|
||||||
--destructive-foreground: oklch(0.982 0.003 107);
|
--input: oklch(0.389 0.005 260);
|
||||||
--success: oklch(0.696 0.17 149.2);
|
--ring: oklch(0.563 0.1714 170.4);
|
||||||
--border: oklch(1 0 0 / 14%);
|
--cursor-tint: color-mix(in oklab, var(--primary) 12%, transparent);
|
||||||
--input: oklch(1 0 0 / 18%);
|
--icon-folder: oklch(0.71 0.15 272);
|
||||||
--ring: oklch(0.552 0.01 107);
|
--icon-link: oklch(0.67 0.1216 275.3);
|
||||||
--icon-folder: oklch(0.75 0.11 250);
|
--icon-image: oklch(0.74 0.175 305.4);
|
||||||
--icon-link: oklch(0.74 0.09 190);
|
--icon-media: oklch(0.712 0.185 11.3);
|
||||||
--icon-image: oklch(0.76 0.11 295);
|
--icon-archive: oklch(0.837 0.14 75);
|
||||||
--icon-media: oklch(0.76 0.11 10);
|
--icon-code: oklch(0.77 0.1919 163.7);
|
||||||
--icon-archive: oklch(0.77 0.11 65);
|
|
||||||
--icon-code: oklch(0.75 0.1 155);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark tone "darker" (data-dark-tone set by SettingsService; absent = grey "dark").
|
/* Dark tone "darker" (data-dark-tone set by SettingsService; absent = grey "dark").
|
||||||
True-black AMOLED palette: --chrome collapses into --background on purpose, so
|
True-black AMOLED palette: every surface layer (--chrome, --toolbar and the
|
||||||
the frame/content step of the default tone disappears and every large surface is
|
steps derived from it, --background, --row-alt) collapses to #000 on purpose
|
||||||
#000 (OLED pixels actually off). Elevation survives only through --card and
|
— OLED pixels actually off — so every NEW surface token needs a line here.
|
||||||
--accent; --border/--input are raised to 16%/18% because on pure black those
|
Elevation survives only through --card, --muted and --accent (opaque here:
|
||||||
lines are the only thing defining panes, bars and rows. The surface layers
|
an overlay on black is too faint); the opaque strokes already read on black.
|
||||||
(--toolbar, --row-alt) collapse to black too: AMOLED stays one flat black on
|
|
||||||
purpose, so every NEW surface token needs a line here.
|
|
||||||
Duplicated for forced-dark and system-dark like the token blocks above — the
|
Duplicated for forced-dark and system-dark like the token blocks above — the
|
||||||
extra attribute outranks them. Never declare a token both here and in the
|
extra attribute outranks them. */
|
||||||
accent dark formula blocks below: same (0,3,0) specificity, the later wins. */
|
|
||||||
:root[data-theme='dark'][data-dark-tone='darker'] {
|
:root[data-theme='dark'][data-dark-tone='darker'] {
|
||||||
--background: oklch(0 0 0);
|
--background: oklch(0 0 0);
|
||||||
--chrome: oklch(0 0 0);
|
--chrome: oklch(0 0 0);
|
||||||
--toolbar: oklch(0 0 0);
|
--toolbar: oklch(0 0 0);
|
||||||
--row-alt: oklch(0 0 0);
|
--row-alt: oklch(0 0 0);
|
||||||
--card: oklch(0.21 0.002 107);
|
--card: oklch(0.216 0.008 260); /* n100 */
|
||||||
--secondary: oklch(0.274 0.003 107);
|
--muted: oklch(0.247 0.007 260); /* n200 */
|
||||||
--muted: oklch(0.274 0.003 107);
|
--accent: oklch(0.247 0.007 260); /* n200 */
|
||||||
--muted-foreground: oklch(0.705 0.012 107);
|
|
||||||
--accent: oklch(0.274 0.003 107);
|
|
||||||
--border: oklch(1 0 0 / 16%);
|
|
||||||
--input: oklch(1 0 0 / 18%);
|
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root:not([data-theme='light'])[data-dark-tone='darker'] {
|
:root:not([data-theme='light'])[data-dark-tone='darker'] {
|
||||||
@@ -147,13 +140,9 @@
|
|||||||
--chrome: oklch(0 0 0);
|
--chrome: oklch(0 0 0);
|
||||||
--toolbar: oklch(0 0 0);
|
--toolbar: oklch(0 0 0);
|
||||||
--row-alt: oklch(0 0 0);
|
--row-alt: oklch(0 0 0);
|
||||||
--card: oklch(0.21 0.002 107);
|
--card: oklch(0.216 0.008 260);
|
||||||
--secondary: oklch(0.274 0.003 107);
|
--muted: oklch(0.247 0.007 260);
|
||||||
--muted: oklch(0.274 0.003 107);
|
--accent: oklch(0.247 0.007 260);
|
||||||
--muted-foreground: oklch(0.705 0.012 107);
|
|
||||||
--accent: oklch(0.274 0.003 107);
|
|
||||||
--border: oklch(1 0 0 / 16%);
|
|
||||||
--input: oklch(1 0 0 / 18%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,18 +150,17 @@
|
|||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-chrome: var(--chrome);
|
--color-chrome: var(--chrome);
|
||||||
--color-toolbar: var(--toolbar);
|
--color-toolbar: var(--toolbar);
|
||||||
|
--color-toolbar-2: var(--toolbar-2);
|
||||||
|
--color-toolbar-3: var(--toolbar-3);
|
||||||
--color-row-alt: var(--row-alt);
|
--color-row-alt: var(--row-alt);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
--color-primary: var(--primary);
|
--color-primary: var(--primary);
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-muted: var(--muted);
|
--color-muted: var(--muted);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
--color-accent: var(--accent);
|
--color-accent: var(--accent);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-destructive: var(--destructive);
|
--color-destructive: var(--destructive);
|
||||||
--color-destructive-foreground: var(--destructive-foreground);
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
--color-success: var(--success);
|
--color-success: var(--success);
|
||||||
@@ -186,12 +174,6 @@
|
|||||||
--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);
|
||||||
/* rem subtrahends (4px/2px at the 16px root), NOT px: --radius is rem, and a
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Panel-chrome metrics: file list, path bar, action buttons, source picker.
|
/* Panel-chrome metrics: file list, path bar, action buttons, source picker.
|
||||||
@@ -238,43 +220,6 @@
|
|||||||
animation-play-state: paused;
|
animation-play-state: paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Accent tone (data-accent set by SettingsService; absent = graphite/zinc).
|
|
||||||
Each tone is one hue+chroma line; the formula blocks below derive --primary,
|
|
||||||
--ring and the cursor-row tint for light and both dark states. The dark
|
|
||||||
formula blocks (0,3,0) outrank the base dark token blocks and the light
|
|
||||||
formula block (0,2,0) in every dark state; graphite matches no [data-accent]
|
|
||||||
selector and keeps the zinc tokens untouched. */
|
|
||||||
:root {
|
|
||||||
/* graphite: cursor row identical to the old bg-accent highlight */
|
|
||||||
--cursor-tint: var(--accent);
|
|
||||||
}
|
|
||||||
:root[data-accent='blue'] { --tone-h: 255; --tone-c: 0.17; }
|
|
||||||
:root[data-accent='indigo'] { --tone-h: 277; --tone-c: 0.19; }
|
|
||||||
:root[data-accent='purple'] { --tone-h: 305; --tone-c: 0.17; }
|
|
||||||
:root[data-accent='rose'] { --tone-h: 8; --tone-c: 0.17; }
|
|
||||||
/* darkest orange that still reads orange; white button text lands ≈ 4.3:1 */
|
|
||||||
:root[data-accent='orange'] { --tone-h: 50; --tone-c: 0.18; --tone-l: 0.58; }
|
|
||||||
:root[data-accent='green'] { --tone-h: 150; --tone-c: 0.15; }
|
|
||||||
:root[data-accent='teal'] { --tone-h: 185; --tone-c: 0.12; }
|
|
||||||
|
|
||||||
:root[data-accent] {
|
|
||||||
--primary: oklch(var(--tone-l, 0.55) var(--tone-c) var(--tone-h));
|
|
||||||
--ring: oklch(0.62 calc(var(--tone-c) * 0.55) var(--tone-h));
|
|
||||||
--cursor-tint: color-mix(in oklab, var(--primary) 8%, transparent);
|
|
||||||
}
|
|
||||||
:root[data-theme='dark'][data-accent] {
|
|
||||||
--primary: oklch(0.72 calc(var(--tone-c) * 0.85) var(--tone-h));
|
|
||||||
--ring: oklch(0.62 calc(var(--tone-c) * 0.55) var(--tone-h));
|
|
||||||
--cursor-tint: color-mix(in oklab, var(--primary) 12%, transparent);
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root[data-accent]:not([data-theme='light']) {
|
|
||||||
--primary: oklch(0.72 calc(var(--tone-c) * 0.85) var(--tone-h));
|
|
||||||
--ring: oklch(0.62 calc(var(--tone-c) * 0.55) var(--tone-h));
|
|
||||||
--cursor-tint: color-mix(in oklab, var(--primary) 12%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -286,3 +231,92 @@ body {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Native controls, restyled square to match the rest. WebKit draws checkboxes,
|
||||||
|
radios and selects natively and ignores border-radius/background until
|
||||||
|
appearance: none. In @layer base so utilities (size-3.5, pl-1 …) still win —
|
||||||
|
which is also why the selects use pl-*, never px-*: a px utility would
|
||||||
|
override the padding-right the caret needs. */
|
||||||
|
@layer base {
|
||||||
|
input[type='checkbox'],
|
||||||
|
input[type='radio'] {
|
||||||
|
appearance: none;
|
||||||
|
display: inline-grid;
|
||||||
|
place-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 0.875rem;
|
||||||
|
height: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid var(--input);
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
||||||
|
input[type='checkbox']:checked,
|
||||||
|
input[type='radio']:checked {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background-color: var(--primary);
|
||||||
|
}
|
||||||
|
/* the check: an L of two borders, rotated */
|
||||||
|
input[type='checkbox']:checked::after {
|
||||||
|
content: '';
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.28rem;
|
||||||
|
border: solid var(--primary-foreground);
|
||||||
|
border-width: 0 0 0.125rem 0.125rem;
|
||||||
|
transform: translateY(-0.06rem) rotate(-45deg);
|
||||||
|
}
|
||||||
|
/* radios are square too; a square dot tells them apart from checkboxes */
|
||||||
|
input[type='radio']:checked::after {
|
||||||
|
content: '';
|
||||||
|
width: 0.375rem;
|
||||||
|
height: 0.375rem;
|
||||||
|
background-color: var(--primary-foreground);
|
||||||
|
}
|
||||||
|
input[type='checkbox']:focus-visible,
|
||||||
|
input[type='radio']:focus-visible {
|
||||||
|
outline: 2px solid var(--ring);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
input[type='checkbox']:disabled,
|
||||||
|
input[type='radio']:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No native arrow: a caret drawn from two currentColor triangles follows the
|
||||||
|
theme (a data-URI svg can't). The opened option list stays native — macOS
|
||||||
|
draws that popup itself. */
|
||||||
|
select {
|
||||||
|
appearance: none;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, currentColor 50%),
|
||||||
|
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
||||||
|
background-position:
|
||||||
|
calc(100% - 0.8rem) 55%,
|
||||||
|
calc(100% - 0.5rem) 55%;
|
||||||
|
background-size: 0.3rem 0.3rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* number inputs (the manager's port field): no native spin buttons */
|
||||||
|
input[type='number']::-webkit-inner-spin-button,
|
||||||
|
input[type='number']::-webkit-outer-spin-button {
|
||||||
|
appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Square scrollbars. Styling them replaces macOS's auto-hiding overlay
|
||||||
|
scrollbars with always-visible ones — the inherent cost. */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track,
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--border);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user