diff --git a/README.md b/README.md index fa7ede5..aa449e8 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Dense reference material lives in [docs/](docs/): [architecture](docs/architectu | Runtime | Bun (main process, all SSH/SFTP + file IO) | | Desktop | Electrobun 1.18 (native webview, typed RPC bridge) | | 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 | | SSH/SFTP | `ssh2` (pure-JS under Bun) | diff --git a/docs/architecture.md b/docs/architecture.md index 5e0641f..2aa99e3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 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 -decided, spread by -the main window and `openSecondaryWindow`: macOS `hiddenInset` + +decided, spread by the main window and `openSecondaryWindow`: macOS `hiddenInset` + `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 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 skip, overwrite dialog with all/none batch modes). - `settings.service.ts` — one signal per setting; **imperative apply pattern** - (`applyTheme/DarkTone/Accent` set `data-*` attributes on ``; + (`applyTheme/DarkTone` set `data-*` attributes on ``; `applyUiScale` sets its inline `font-size`); `applyAll` also serves the `settingsChanged` broadcast, so every window follows a save made in any 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 walker check the flag themselves. - `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 `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 @@ -301,35 +300,42 @@ component; tokens → `ui/src/styles.css`. ## Theming -`ui/src/styles.css`: shadcn-style OKLCH tokens; dark values are duplicated in -`:root[data-theme='dark']` AND a `prefers-color-scheme` media block (system -mode) — every dark token change goes in BOTH. Light is the zinc family; the -dark neutrals are a **warm gray family (hue 107)** anchored on `#1a1a19` -(`--background`) — retoning one dark neutral means retoning them all, or warm -and cool surfaces clash. **Surface layers**, each its own shade: `--chrome` -(frame — tab/transfer/status bars, editor/manager header+footer strips, the -manager's connection sidebar), `--toolbar` (each file panel's header strip — -session strip, path bar, column headers — and the editor's search bar; both -panels share one neutral strip, and the ACTIVE panel is marked by a 0.125rem -`--primary` line along its top edge, an inset box-shadow on the session strip so -switching panels never shifts layout), `--background` (file lists, editor -text, forms; `--row-alt` stripes odd file rows, skipped on the cursor/selected -row so two `bg-*` utilities never fight) and `--card` (popovers, dialogs, -toasts). Dark climbs frame 0.165 < list 0.218 < header 0.265 < card 0.285; -light steps greys out from white content: frame 0.935 < header 0.965 < list = -card 1. **`--accent` is translucent** (6% zinc-900 light, 9% white dark): -hover/selected states sit on every layer, and an opaque hover vanishes on a -surface of its own lightness. The "darker" tone is true-black AMOLED: chrome, -background, toolbar and row-alt are all `#000` (the layers collapse -on purpose), elevation survives only via `--card`/`--accent` (opaque there), -and `--border`/`--input` go to 16%/18% white because those lines then carry the -whole layout. Accent tones: one -`--tone-h/--tone-c` line per tone + three formula blocks derive `--primary`, -`--ring`, `--cursor-tint`; graphite = attribute removed = pristine zinc. +`ui/src/styles.css`: OKLCH tokens adapted from **Oxide's design system** +(docs.oxide.computer: a cool neutral scale at hue 260 with almost no chroma, +one green accent, opaque strokes; the comments carry Oxide's step names +n0…n1300 / green-800). Dark values are duplicated in `:root[data-theme='dark']` +AND a `prefers-color-scheme` media block (system mode) — every dark token +change goes in BOTH; retone neutrals along the one scale, or warm and cool +surfaces clash. **There is no accent setting**: `--primary` is Oxide green per +theme (green-800 dark, green-500 light), so selection (`bg-primary/15`), the +cursor row (`--cursor-tint`, a `color-mix` of `--primary`), focus rings +(green-600) and `--success` all read green. **Surface layers**, each its own +shade: `--chrome` (frame — tab/transfer/status bars, the editor/manager/settings +header+footer strips and sidebars), the file panel's header strip **stepping +toward the list** — session strip `--toolbar`, path bar `--toolbar-2`, column +headers `--toolbar-3` (`color-mix` steps declared once in `:root`, so every +theme follows) — then `--background` (file lists, editor text, forms; +`--row-alt` stripes odd file rows, skipped on the cursor/selected row so two +`bg-*` utilities never fight) and `--card` (popovers, dialogs, toasts). The +ACTIVE panel is marked by a 0.125rem `--primary` line along its top edge (an +inset box-shadow on the session strip, so switching panels never shifts +layout). Dark: frame 0.162 < list 0.195 < header 0.275 → 0.248 → 0.221 < card +0.29; light steps greys out from near-white content: frame 0.925 < header 0.95 +→ 0.965 → 0.98 < list = card 0.995. **`--accent` is translucent** (n0 at 5% +light, white 7% dark): hover/selected states sit on every layer, and an opaque +hover vanishes on a surface of its own lightness. Strokes are opaque +(`--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`, -`text-icon-folder`, `bg-cursor-tint`). Selected rows are `bg-primary/15` -(follow the accent); the cursor row uses `--cursor-tint` so graphite stays -pixel-identical to the old neutral hover. **Interface scale** (`settings.uiScale`, +`text-icon-folder`, `bg-toolbar-2`, `bg-cursor-tint`). File-type icon colors +come from Oxide's hue scales (800 steps dark, 600 light). **Interface scale** (`settings.uiScale`, 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 (panel tokens like `--row-h`, all Tailwind utilities, ng-icon sizes), the diff --git a/docs/gotchas.md b/docs/gotchas.md index f21a4c2..c40e1a9 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -60,17 +60,22 @@ touching the adjacent code. - **A new surface token needs an AMOLED line, or AMOLED stops being all-black.** The two `[data-dark-tone='darker']` blocks re-declare every - surface (`--chrome`, `--toolbar`, `--row-alt`, …) as `#000`; a token added - only to the dark blocks shows through in AMOLED. -- **Never declare one token in both the AMOLED blocks and the accent dark - 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`. + surface (`--chrome`, `--toolbar`, `--background`, `--row-alt`, …) as `#000`; + a token added only to the dark blocks shows through in AMOLED. Tokens derived + in `:root` by `color-mix` (`--toolbar-2/-3`) follow on their own. - **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 on the 0.965 header strip did exactly that. Keep `--accent` alpha-based when 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 ` @@ -231,7 +230,7 @@ [value]="d.username" (input)="patch('username', $any($event.target).value)" 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" /> @@ -253,7 +252,6 @@ name="authKind" [checked]="d.authKind === 'password'" (change)="patch('authKind', 'password')" - class="accent-primary" /> {{ i18n.t('connMgr.authPassword') }} @@ -263,7 +261,6 @@ name="authKind" [checked]="d.authKind === 'key'" (change)="patch('authKind', 'key')" - class="accent-primary" /> {{ i18n.t('connMgr.authKey') }} @@ -276,7 +273,7 @@ type="password" [value]="d.password" (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" /> } @else { @@ -288,12 +285,12 @@ (input)="patch('privateKeyPath', $any($event.target).value)" placeholder="~/.ssh/id_ed25519" 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" /> @@ -305,7 +302,7 @@ type="password" [value]="d.passphrase" (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" /> } @@ -318,7 +315,7 @@ (input)="patch('localDir', $any($event.target).value)" [placeholder]="i18n.t('connMgr.localDirPlaceholder')" 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" /> {{ i18n.t('connMgr.localDirHint') }} @@ -330,13 +327,13 @@
@@ -344,14 +341,14 @@ diff --git a/ui/src/app/skins/shadlike/dialogs/confirm-dialog.ts b/ui/src/app/skins/shadlike/dialogs/confirm-dialog.ts index 84a6f6f..bb3e389 100644 --- a/ui/src/app/skins/shadlike/dialogs/confirm-dialog.ts +++ b/ui/src/app/skins/shadlike/dialogs/confirm-dialog.ts @@ -8,7 +8,7 @@ import { FocusService } from '@core/focus.service'; template: `
@@ -17,7 +17,7 @@ import { FocusService } from '@core/focus.service';
@@ -26,8 +26,8 @@ import { FocusService } from '@core/focus.service'; (click)="submit()" [class]=" request().destructive - ? 'rounded-md 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-destructive px-3 py-1.5 text-xs font-medium text-destructive-foreground hover:bg-destructive/90' + : 'bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90' " > {{ request().confirmLabel }} diff --git a/ui/src/app/skins/shadlike/dialogs/overwrite-dialog.ts b/ui/src/app/skins/shadlike/dialogs/overwrite-dialog.ts index df797bc..f6ee2cf 100644 --- a/ui/src/app/skins/shadlike/dialogs/overwrite-dialog.ts +++ b/ui/src/app/skins/shadlike/dialogs/overwrite-dialog.ts @@ -21,7 +21,7 @@ import { FocusService } from '@core/focus.service'; (keydown.escape)="pick('cancel')" >

@@ -30,7 +30,7 @@ import { FocusService } from '@core/focus.service';

@if (request().total > 1) { {{ i18n.t('dialogs.ofTotal', { index: request().index, total: request().total }) }} @@ -47,13 +47,13 @@ import { FocusService } from '@core/focus.service';
@for (side of comparison(); track side.label) { -
+
{{ i18n.t('dialogs.' + side.label) }} @if (side.newer) { - + {{ i18n.t('dialogs.newer') }} } @@ -79,7 +79,7 @@ import { FocusService } from '@core/focus.service';
@@ -87,13 +87,13 @@ import { FocusService } from '@core/focus.service'; @if (request().total > 1) { @@ -101,14 +101,14 @@ import { FocusService } from '@core/focus.service'; } diff --git a/ui/src/app/skins/shadlike/dialogs/prompt-dialog.ts b/ui/src/app/skins/shadlike/dialogs/prompt-dialog.ts index 8f35683..5d27ee6 100644 --- a/ui/src/app/skins/shadlike/dialogs/prompt-dialog.ts +++ b/ui/src/app/skins/shadlike/dialogs/prompt-dialog.ts @@ -16,7 +16,7 @@ import { FocusService } from '@core/focus.service'; template: `

{{ request().title }}

@@ -28,19 +28,19 @@ import { FocusService } from '@core/focus.service'; (keydown.escape)="cancel()" [placeholder]="request().placeholder ?? ''" 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" />
diff --git a/ui/src/app/skins/shadlike/editor-window/editor-window.html b/ui/src/app/skins/shadlike/editor-window/editor-window.html index 80c794a..db440cb 100644 --- a/ui/src/app/skins/shadlike/editor-window/editor-window.html +++ b/ui/src/app/skins/shadlike/editor-window/editor-window.html @@ -1,7 +1,7 @@
@if (!ctx) {
-
+

{{ i18n.t('editor.noFileTitle') }}

@@ -29,7 +29,7 @@ @@ -55,7 +55,7 @@ (keydown)="onFindKeydown($event)" [placeholder]="i18n.t('editor.findPlaceholder')" 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" /> @@ -82,7 +82,7 @@ @@ -104,13 +104,13 @@

} @else if (loadError(); as error) {
-
+

{{ i18n.t('editor.cantOpen', { name: ctx.name }) }}

{{ error }}

@@ -149,14 +149,14 @@
diff --git a/ui/src/app/skins/shadlike/file-panel/file-panel.html b/ui/src/app/skins/shadlike/file-panel/file-panel.html index cb11a46..1507485 100644 --- a/ui/src/app/skins/shadlike/file-panel/file-panel.html +++ b/ui/src/app/skins/shadlike/file-panel/file-panel.html @@ -5,7 +5,7 @@ > @@ -70,7 +70,7 @@ @@ -78,7 +78,7 @@ @@ -178,13 +178,13 @@ (keydown)="$event.stopPropagation()" [disabled]="blocked()" 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" /> @@ -252,7 +252,7 @@ diff --git a/ui/src/app/skins/shadlike/settings-window/settings-window.html b/ui/src/app/skins/shadlike/settings-window/settings-window.html index 0fa6078..04be898 100644 --- a/ui/src/app/skins/shadlike/settings-window/settings-window.html +++ b/ui/src/app/skins/shadlike/settings-window/settings-window.html @@ -19,7 +19,7 @@ @for (s of sections; track s) { diff --git a/ui/src/app/skins/shadlike/settings-window/settings-window.ts b/ui/src/app/skins/shadlike/settings-window/settings-window.ts index a8c183f..cd34cb7 100644 --- a/ui/src/app/skins/shadlike/settings-window/settings-window.ts +++ b/ui/src/app/skins/shadlike/settings-window/settings-window.ts @@ -2,7 +2,6 @@ import { Component, computed, inject, signal } from '@angular/core'; import { NgIcon } from '@ng-icons/core'; import { BRANDING } from '@shared/branding'; import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n'; -import type { AccentTone } from '@shared/types'; import { COLUMN_KEYS } from '@core/context-menu.service'; import { I18nService } from '@core/i18n.service'; 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 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.. -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 = [ { value: 'MBps', label: 'MB/s' }, { value: 'Mbps', label: 'Mbps' }, @@ -62,7 +48,6 @@ export class SettingsWindow { readonly localeLabels = LOCALE_LABELS; readonly themes = THEMES; readonly darkTones = DARK_TONES; - readonly accents = ACCENTS; readonly speedUnits = SPEED_UNITS; readonly transferModes = TRANSFER_MODES; readonly concurrencyChoices = CONCURRENCY_CHOICES; diff --git a/ui/src/app/skins/shadlike/tab-bar/tab-bar.ts b/ui/src/app/skins/shadlike/tab-bar/tab-bar.ts index 4eacace..9d527d0 100644 --- a/ui/src/app/skins/shadlike/tab-bar/tab-bar.ts +++ b/ui/src/app/skins/shadlike/tab-bar/tab-bar.ts @@ -28,7 +28,7 @@ import { UpdaterService } from '@core/updater.service'; @for (ws of panels.workspaces(); track ws.id; let i = $index) { } @@ -80,7 +80,7 @@ import { UpdaterService } from '@core/updater.service'; @@ -94,7 +94,7 @@ import { UpdaterService } from '@core/updater.service'; (click)="onUpdateClick()" [disabled]="updater.state() === 'downloading'" [title]="i18n.t('tabBar.updateTooltip', { version: updater.version() })" - class="relative mr-1 grid h-6 place-items-center overflow-hidden rounded-md px-2.5 text-[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') { {{ i18n.t('tabBar.connections') }} @@ -132,7 +132,7 @@ import { UpdaterService } from '@core/updater.service'; diff --git a/ui/src/app/skins/shadlike/toasts/toasts.ts b/ui/src/app/skins/shadlike/toasts/toasts.ts index 6bfbab6..a87d6fb 100644 --- a/ui/src/app/skins/shadlike/toasts/toasts.ts +++ b/ui/src/app/skins/shadlike/toasts/toasts.ts @@ -12,7 +12,7 @@ import { ToastService } from '@core/toast.service';
{{ i18n.t('transfer.clearFinished') }} @@ -25,10 +25,10 @@ aria-valuemax="100" [attr.aria-valuenow]="jobPercent(job)" [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" >
@@ -38,7 +38,7 @@ @@ -46,7 +46,7 @@ diff --git a/ui/src/index.html b/ui/src/index.html index d1b26f0..06a4408 100644 --- a/ui/src/index.html +++ b/ui/src/index.html @@ -13,11 +13,11 @@ attributes are baked in by the bun preload (buildPreload in src/bun/windows.ts) so forced themes are correct at first paint. --> diff --git a/ui/src/styles.css b/ui/src/styles.css index da585a2..13788a4 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -1,50 +1,54 @@ @import "tailwindcss"; -/* shadcn-style theme tokens. Light stays the zinc family; the dark neutrals are a - warm gray family (hue ~107) anchored on #1a1a19 — see the dark blocks below. - The app follows the OS light/dark preference automatically. +/* Theme tokens, adapted from Oxide's design system (docs.oxide.computer): a cool + neutral scale at hue 260 with almost no chroma, one green accent, opaque + 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 - (frame strips: tab/transfer/status bars, window header/footer strips), - --toolbar (the file-panel header strip — toolbar, path bar, column header — - and secondary toolbars), --background (content: file lists, editor text, - forms; --row-alt stripes every other file row) and --card (popovers, - dialogs). Dark climbs frame < list < header < card; light steps greys out - from white content: frame < header < list = card. --accent is a translucent - overlay, not a surface: hover/selected states must read on every layer, and - an opaque hover vanishes on a surface of its own lightness. */ + (frame strips: tab/transfer/status bars, window header/footer strips and + sidebars), --toolbar (the top row of a file panel's header strip, and + secondary toolbars) stepping through --toolbar-2/-3 toward --background + (content: file lists, editor text, forms; --row-alt stripes every other file + row), and --card (popovers, dialogs). Dark climbs frame < list < header < + card; light steps greys out from near-white content: frame < header < list + = card. --accent is a translucent overlay, not a surface: hover/selected + 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 { - /* 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; - --background: oklch(1 0 0); - --chrome: oklch(0.935 0.002 286.375); - --toolbar: oklch(0.965 0.001 286.375); - --row-alt: oklch(0.978 0.001 286.375); - --foreground: oklch(0.141 0.005 285.823); - --card: oklch(1 0 0); - --card-foreground: oklch(0.141 0.005 285.823); - --primary: oklch(0.21 0.006 285.885); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.967 0.001 286.375); - --secondary-foreground: oklch(0.21 0.006 285.885); - --muted: oklch(0.967 0.001 286.375); - --muted-foreground: oklch(0.552 0.016 285.938); - --accent: oklch(0.21 0.006 285.885 / 6%); - --accent-foreground: oklch(0.21 0.006 285.885); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.985 0 0); - --success: oklch(0.627 0.17 149.2); - --border: oklch(0.92 0.004 286.32); - --input: oklch(0.92 0.004 286.32); - --ring: oklch(0.705 0.015 286.067); - --radius: 0.5rem; - /* file-type icon colors — fixed category hues, muted chroma */ - --icon-folder: oklch(0.58 0.13 250); - --icon-link: oklch(0.56 0.1 190); - --icon-image: oklch(0.58 0.13 295); - --icon-media: oklch(0.6 0.13 10); - --icon-archive: oklch(0.62 0.13 60); - --icon-code: oklch(0.56 0.12 155); + --background: oklch(0.995 0.0001 260); /* n1300 */ + --chrome: oklch(0.925 0.0001 260); /* n1000 */ + --toolbar: oklch(0.95 0.0001 260); /* n1100 */ + --row-alt: oklch(0.975 0.0001 260); /* n1200 */ + --foreground: oklch(0.162 0.01 260); /* n0 */ + --card: oklch(0.995 0.0001 260); + --card-foreground: oklch(0.162 0.01 260); + --primary: oklch(0.474 0.1481 173.7); /* green-500 */ + --primary-foreground: oklch(0.995 0.0001 260); + --muted: oklch(0.95 0.0001 260); /* n1100 */ + --muted-foreground: oklch(0.483 0.0041 260); /* n500 */ + --accent: oklch(0.162 0.01 260 / 5%); + --destructive: oklch(0.474 0.1285 21.3); /* red-500 */ + --destructive-foreground: oklch(0.995 0.0001 260); + --success: oklch(0.474 0.1481 173.7); /* green-500 */ + --border: oklch(0.9 0.0001 260); /* n900 */ + --input: oklch(0.79 0.0011 260); /* n800 */ + --ring: oklch(0.563 0.1714 170.4); /* green-600 */ + --cursor-tint: color-mix(in oklab, var(--primary) 8%, transparent); + /* The file panel's header strip steps toward the list, a third per row. + Declared once: the var()s resolve on :root, so dark and AMOLED (where both + ends are black) follow without their own lines. */ + --toolbar-2: color-mix(in oklab, var(--toolbar) 66%, var(--background)); + --toolbar-3: color-mix(in oklab, var(--toolbar) 33%, var(--background)); + /* file-type icon colors — fixed category hues from Oxide's scales (600 steps) */ + --icon-folder: oklch(0.563 0.1139 278.7); /* blue-600 */ + --icon-link: oklch(0.67 0.1216 275.3); /* blue-700 */ + --icon-image: oklch(0.563 0.1328 312.1); /* purple-600 */ + --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. @@ -55,91 +59,80 @@ :root[data-theme='dark'] { color-scheme: dark; - --background: oklch(0.2178 0.002 107); - --chrome: oklch(0.165 0.002 107); - --toolbar: oklch(0.265 0.002 107); - --row-alt: oklch(0.232 0.002 107); - --foreground: oklch(0.982 0.003 107); + --background: oklch(0.195 0.009 260); /* n50 */ + --chrome: oklch(0.162 0.01 260); /* n0 */ + --toolbar: oklch(0.275 0.0065 260); /* between n200 and n300 */ + --row-alt: oklch(0.205 0.0085 260); /* between n50 and n100 */ + --foreground: oklch(0.9 0.0001 260); /* n900 */ /* above --toolbar, so a popover never reads darker than the strip it covers */ - --card: oklch(0.285 0.002 107); - --card-foreground: oklch(0.982 0.003 107); - --primary: oklch(0.92 0.004 107); - --primary-foreground: oklch(0.21 0.002 107); - --secondary: oklch(0.302 0.003 107); - --secondary-foreground: oklch(0.982 0.003 107); - --muted: oklch(0.335 0.003 107); - --muted-foreground: oklch(0.715 0.012 107); - --accent: oklch(1 0 0 / 9%); - --accent-foreground: oklch(0.982 0.003 107); - --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(0.982 0.003 107); - --success: oklch(0.696 0.17 149.2); - --border: oklch(1 0 0 / 14%); - --input: oklch(1 0 0 / 18%); - --ring: oklch(0.552 0.01 107); - --icon-folder: oklch(0.75 0.11 250); - --icon-link: oklch(0.74 0.09 190); - --icon-image: oklch(0.76 0.11 295); - --icon-media: oklch(0.76 0.11 10); - --icon-archive: oklch(0.77 0.11 65); - --icon-code: oklch(0.75 0.1 155); + --card: oklch(0.29 0.0065 260); + --card-foreground: oklch(0.9 0.0001 260); + --primary: oklch(0.77 0.1919 163.7); /* green-800 */ + --primary-foreground: oklch(0.162 0.01 260); + --muted: oklch(0.316 0.006 260); /* n300 */ + --muted-foreground: oklch(0.717 0.0021 260); /* n700 */ + --accent: oklch(1 0 0 / 7%); + --destructive: oklch(0.712 0.185 11.3); /* red-800 */ + --destructive-foreground: oklch(0.162 0.01 260); + --success: oklch(0.77 0.1919 163.7); /* green-800 */ + --border: oklch(0.316 0.006 260); /* n300 */ + --input: oklch(0.389 0.005 260); /* n400 */ + --ring: oklch(0.563 0.1714 170.4); /* green-600 */ + --cursor-tint: color-mix(in oklab, var(--primary) 12%, transparent); + --icon-folder: oklch(0.71 0.15 272); /* blue-800 */ + --icon-link: oklch(0.67 0.1216 275.3); /* blue-700 */ + --icon-image: oklch(0.74 0.175 305.4); /* purple-800 */ + --icon-media: oklch(0.712 0.185 11.3); /* red-800 */ + --icon-archive: oklch(0.837 0.14 75); /* yellow-800 */ + --icon-code: oklch(0.77 0.1919 163.7); /* green-800 */ } @media (prefers-color-scheme: dark) { :root:not([data-theme='light']) { - --background: oklch(0.2178 0.002 107); - --chrome: oklch(0.165 0.002 107); - --toolbar: oklch(0.265 0.002 107); - --row-alt: oklch(0.232 0.002 107); - --foreground: oklch(0.982 0.003 107); - --card: oklch(0.285 0.002 107); - --card-foreground: oklch(0.982 0.003 107); - --primary: oklch(0.92 0.004 107); - --primary-foreground: oklch(0.21 0.002 107); - --secondary: oklch(0.302 0.003 107); - --secondary-foreground: oklch(0.982 0.003 107); - --muted: oklch(0.335 0.003 107); - --muted-foreground: oklch(0.715 0.012 107); - --accent: oklch(1 0 0 / 9%); - --accent-foreground: oklch(0.982 0.003 107); - --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(0.982 0.003 107); - --success: oklch(0.696 0.17 149.2); - --border: oklch(1 0 0 / 14%); - --input: oklch(1 0 0 / 18%); - --ring: oklch(0.552 0.01 107); - --icon-folder: oklch(0.75 0.11 250); - --icon-link: oklch(0.74 0.09 190); - --icon-image: oklch(0.76 0.11 295); - --icon-media: oklch(0.76 0.11 10); - --icon-archive: oklch(0.77 0.11 65); - --icon-code: oklch(0.75 0.1 155); + --background: oklch(0.195 0.009 260); + --chrome: oklch(0.162 0.01 260); + --toolbar: oklch(0.275 0.0065 260); + --row-alt: oklch(0.205 0.0085 260); + --foreground: oklch(0.9 0.0001 260); + --card: oklch(0.29 0.0065 260); + --card-foreground: oklch(0.9 0.0001 260); + --primary: oklch(0.77 0.1919 163.7); + --primary-foreground: oklch(0.162 0.01 260); + --muted: oklch(0.316 0.006 260); + --muted-foreground: oklch(0.717 0.0021 260); + --accent: oklch(1 0 0 / 7%); + --destructive: oklch(0.712 0.185 11.3); + --destructive-foreground: oklch(0.162 0.01 260); + --success: oklch(0.77 0.1919 163.7); + --border: oklch(0.316 0.006 260); + --input: oklch(0.389 0.005 260); + --ring: oklch(0.563 0.1714 170.4); + --cursor-tint: color-mix(in oklab, var(--primary) 12%, transparent); + --icon-folder: oklch(0.71 0.15 272); + --icon-link: oklch(0.67 0.1216 275.3); + --icon-image: oklch(0.74 0.175 305.4); + --icon-media: oklch(0.712 0.185 11.3); + --icon-archive: oklch(0.837 0.14 75); + --icon-code: oklch(0.77 0.1919 163.7); } } /* Dark tone "darker" (data-dark-tone set by SettingsService; absent = grey "dark"). - True-black AMOLED palette: --chrome collapses into --background on purpose, so - the frame/content step of the default tone disappears and every large surface is - #000 (OLED pixels actually off). Elevation survives only through --card and - --accent; --border/--input are raised to 16%/18% because on pure black those - lines are the only thing defining panes, bars and rows. The surface layers - (--toolbar, --row-alt) collapse to black too: AMOLED stays one flat black on - purpose, so every NEW surface token needs a line here. + True-black AMOLED palette: every surface layer (--chrome, --toolbar and the + steps derived from it, --background, --row-alt) collapses to #000 on purpose + — OLED pixels actually off — so every NEW surface token needs a line here. + Elevation survives only through --card, --muted and --accent (opaque here: + an overlay on black is too faint); the opaque strokes already read on black. 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 - accent dark formula blocks below: same (0,3,0) specificity, the later wins. */ + extra attribute outranks them. */ :root[data-theme='dark'][data-dark-tone='darker'] { --background: oklch(0 0 0); --chrome: oklch(0 0 0); --toolbar: oklch(0 0 0); --row-alt: oklch(0 0 0); - --card: oklch(0.21 0.002 107); - --secondary: oklch(0.274 0.003 107); - --muted: oklch(0.274 0.003 107); - --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%); + --card: oklch(0.216 0.008 260); /* n100 */ + --muted: oklch(0.247 0.007 260); /* n200 */ + --accent: oklch(0.247 0.007 260); /* n200 */ } @media (prefers-color-scheme: dark) { :root:not([data-theme='light'])[data-dark-tone='darker'] { @@ -147,13 +140,9 @@ --chrome: oklch(0 0 0); --toolbar: oklch(0 0 0); --row-alt: oklch(0 0 0); - --card: oklch(0.21 0.002 107); - --secondary: oklch(0.274 0.003 107); - --muted: oklch(0.274 0.003 107); - --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%); + --card: oklch(0.216 0.008 260); + --muted: oklch(0.247 0.007 260); + --accent: oklch(0.247 0.007 260); } } @@ -161,18 +150,17 @@ --color-background: var(--background); --color-chrome: var(--chrome); --color-toolbar: var(--toolbar); + --color-toolbar-2: var(--toolbar-2); + --color-toolbar-3: var(--toolbar-3); --color-row-alt: var(--row-alt); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); --color-success: var(--success); @@ -186,12 +174,6 @@ --color-icon-media: var(--icon-media); --color-icon-archive: var(--icon-archive); --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. @@ -238,43 +220,6 @@ 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, body { height: 100%; @@ -286,3 +231,92 @@ body { 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); + } +}