Dark tone setting: grey Dark (default) vs near-black Darker
Tone row appears under Theme when the effective theme is dark. Grey palette uses macOS-like #26262a background. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,8 @@ export type AccentTone =
|
|||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
theme: "system" | "light" | "dark";
|
theme: "system" | "light" | "dark";
|
||||||
|
/** Dark appearance: grey ("dark") or near-black AMOLED ("darker"). */
|
||||||
|
darkTone: "dark" | "darker";
|
||||||
/** Accent tone for chrome and selection tint; graphite = neutral zinc. */
|
/** Accent tone for chrome and selection tint; graphite = neutral zinc. */
|
||||||
accent: AccentTone;
|
accent: AccentTone;
|
||||||
/** File-list icons: plain neutral glyphs, or per-category glyphs and colors. */
|
/** File-list icons: plain neutral glyphs, or per-category glyphs and colors. */
|
||||||
@@ -85,6 +87,7 @@ export interface AppSettings {
|
|||||||
|
|
||||||
export const DEFAULT_SETTINGS: AppSettings = {
|
export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
theme: "system",
|
theme: "system",
|
||||||
|
darkTone: "dark",
|
||||||
accent: "blue",
|
accent: "blue",
|
||||||
iconColors: "multi",
|
iconColors: "multi",
|
||||||
speedUnit: "MBps",
|
speedUnit: "MBps",
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Injectable, inject, signal } from '@angular/core';
|
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||||
import { DEFAULT_SETTINGS, type AppSettings } from '@shared/types';
|
import { DEFAULT_SETTINGS, type AppSettings } from '@shared/types';
|
||||||
import { RpcService } from './rpc.service';
|
import { RpcService } from './rpc.service';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class SettingsService {
|
export class SettingsService {
|
||||||
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 accent = signal<AppSettings['accent']>(DEFAULT_SETTINGS.accent);
|
readonly accent = signal<AppSettings['accent']>(DEFAULT_SETTINGS.accent);
|
||||||
readonly iconColors = signal<AppSettings['iconColors']>(DEFAULT_SETTINGS.iconColors);
|
readonly iconColors = signal<AppSettings['iconColors']>(DEFAULT_SETTINGS.iconColors);
|
||||||
readonly speedUnit = signal<AppSettings['speedUnit']>(DEFAULT_SETTINGS.speedUnit);
|
readonly speedUnit = signal<AppSettings['speedUnit']>(DEFAULT_SETTINGS.speedUnit);
|
||||||
@@ -17,13 +18,23 @@ export class SettingsService {
|
|||||||
|
|
||||||
private readonly rpc = inject(RpcService);
|
private readonly rpc = inject(RpcService);
|
||||||
|
|
||||||
|
/** Whether the app is actually rendering dark right now (drives the Tone row). */
|
||||||
|
readonly effectiveDark = computed(
|
||||||
|
() => this.theme() === 'dark' || (this.theme() === 'system' && this.systemDark()),
|
||||||
|
);
|
||||||
|
private readonly systemDark = signal(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
window
|
||||||
|
.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
.addEventListener('change', (e) => this.systemDark.set(e.matches));
|
||||||
// the default accent is not the CSS default — apply before first paint
|
// the default accent is not the CSS default — apply before first paint
|
||||||
this.applyAccent();
|
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) return;
|
if (!res.ok) return;
|
||||||
this.theme.set(res.settings.theme);
|
this.theme.set(res.settings.theme);
|
||||||
|
this.darkTone.set(res.settings.darkTone);
|
||||||
this.accent.set(res.settings.accent);
|
this.accent.set(res.settings.accent);
|
||||||
this.iconColors.set(res.settings.iconColors);
|
this.iconColors.set(res.settings.iconColors);
|
||||||
this.speedUnit.set(res.settings.speedUnit);
|
this.speedUnit.set(res.settings.speedUnit);
|
||||||
@@ -34,6 +45,7 @@ export class SettingsService {
|
|||||||
this.editor.set(res.settings.editor);
|
this.editor.set(res.settings.editor);
|
||||||
this.editorApp.set(res.settings.editorApp);
|
this.editorApp.set(res.settings.editorApp);
|
||||||
this.applyTheme();
|
this.applyTheme();
|
||||||
|
this.applyDarkTone();
|
||||||
this.applyAccent();
|
this.applyAccent();
|
||||||
this.applyTextSize();
|
this.applyTextSize();
|
||||||
});
|
});
|
||||||
@@ -45,6 +57,12 @@ export class SettingsService {
|
|||||||
void this.persist();
|
void this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDarkTone(tone: AppSettings['darkTone']): void {
|
||||||
|
this.darkTone.set(tone);
|
||||||
|
this.applyDarkTone();
|
||||||
|
void this.persist();
|
||||||
|
}
|
||||||
|
|
||||||
setAccent(tone: AppSettings['accent']): void {
|
setAccent(tone: AppSettings['accent']): void {
|
||||||
this.accent.set(tone);
|
this.accent.set(tone);
|
||||||
this.applyAccent();
|
this.applyAccent();
|
||||||
@@ -102,6 +120,15 @@ export class SettingsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyDarkTone(): void {
|
||||||
|
const tone = this.darkTone();
|
||||||
|
if (tone === 'dark') {
|
||||||
|
delete document.documentElement.dataset['darkTone'];
|
||||||
|
} else {
|
||||||
|
document.documentElement.dataset['darkTone'] = tone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private applyAccent(): void {
|
private applyAccent(): void {
|
||||||
const tone = this.accent();
|
const tone = this.accent();
|
||||||
if (tone === 'graphite') {
|
if (tone === 'graphite') {
|
||||||
@@ -123,6 +150,7 @@ export class SettingsService {
|
|||||||
protected settings(): AppSettings {
|
protected settings(): AppSettings {
|
||||||
return {
|
return {
|
||||||
theme: this.theme(),
|
theme: this.theme(),
|
||||||
|
darkTone: this.darkTone(),
|
||||||
accent: this.accent(),
|
accent: this.accent(),
|
||||||
iconColors: this.iconColors(),
|
iconColors: this.iconColors(),
|
||||||
speedUnit: this.speedUnit(),
|
speedUnit: this.speedUnit(),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { RpcService } from '../rpc.service';
|
|||||||
import { SettingsService } from '../settings.service';
|
import { SettingsService } from '../settings.service';
|
||||||
|
|
||||||
const THEMES = ['system', 'light', 'dark'] as const;
|
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
|
// Swatch colors are fixed mid-tones so every dot reads on both light and dark
|
||||||
// cards (the live --primary values would vanish against one of them).
|
// cards (the live --primary values would vanish against one of them).
|
||||||
const ACCENTS: readonly { value: AccentTone; swatch: string; label: string }[] = [
|
const ACCENTS: readonly { value: AccentTone; swatch: string; label: string }[] = [
|
||||||
@@ -133,6 +134,25 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (settings.effectiveDark()) {
|
||||||
|
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||||
|
<span class="text-xs text-muted-foreground">Tone</span>
|
||||||
|
<div class="flex rounded-md border border-border p-0.5">
|
||||||
|
@for (tone of darkTones; track tone) {
|
||||||
|
<button
|
||||||
|
(click)="settings.setDarkTone(tone)"
|
||||||
|
class="rounded px-2 py-0.5 text-[11px] capitalize"
|
||||||
|
[class.bg-accent]="settings.darkTone() === tone"
|
||||||
|
[class.text-foreground]="settings.darkTone() === tone"
|
||||||
|
[class.text-muted-foreground]="settings.darkTone() !== tone"
|
||||||
|
>
|
||||||
|
{{ tone }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||||
<span class="text-xs text-muted-foreground">Accent</span>
|
<span class="text-xs text-muted-foreground">Accent</span>
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
@@ -322,6 +342,7 @@ export class TabBar {
|
|||||||
readonly settingsOpen = signal(false);
|
readonly settingsOpen = signal(false);
|
||||||
readonly isMacChrome = IS_MAC_CHROME;
|
readonly isMacChrome = IS_MAC_CHROME;
|
||||||
readonly themes = THEMES;
|
readonly themes = THEMES;
|
||||||
|
readonly darkTones = DARK_TONES;
|
||||||
readonly accents = ACCENTS;
|
readonly accents = ACCENTS;
|
||||||
readonly iconColorModes = ICON_COLOR_MODES;
|
readonly iconColorModes = ICON_COLOR_MODES;
|
||||||
readonly speedUnits = SPEED_UNITS;
|
readonly speedUnits = SPEED_UNITS;
|
||||||
|
|||||||
+42
-16
@@ -41,23 +41,23 @@
|
|||||||
|
|
||||||
:root[data-theme='dark'] {
|
:root[data-theme='dark'] {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--background: oklch(0.141 0.005 285.823);
|
--background: oklch(0.27 0.006 286.03);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.21 0.006 285.885);
|
--card: oklch(0.315 0.007 286.05);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.92 0.004 286.32);
|
--primary: oklch(0.92 0.004 286.32);
|
||||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||||
--secondary: oklch(0.274 0.006 286.033);
|
--secondary: oklch(0.37 0.013 285.805);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.274 0.006 286.033);
|
--muted: oklch(0.37 0.013 285.805);
|
||||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
--muted-foreground: oklch(0.74 0.015 286.07);
|
||||||
--accent: oklch(0.274 0.006 286.033);
|
--accent: oklch(0.37 0.013 285.805);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--destructive-foreground: oklch(0.985 0 0);
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
--success: oklch(0.696 0.17 149.2);
|
--success: oklch(0.696 0.17 149.2);
|
||||||
--border: oklch(1 0 0 / 12%);
|
--border: oklch(1 0 0 / 14%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 18%);
|
||||||
--ring: oklch(0.552 0.016 285.938);
|
--ring: oklch(0.552 0.016 285.938);
|
||||||
--icon-folder: oklch(0.75 0.11 250);
|
--icon-folder: oklch(0.75 0.11 250);
|
||||||
--icon-link: oklch(0.74 0.09 190);
|
--icon-link: oklch(0.74 0.09 190);
|
||||||
@@ -69,23 +69,23 @@
|
|||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root:not([data-theme='light']) {
|
:root:not([data-theme='light']) {
|
||||||
--background: oklch(0.141 0.005 285.823);
|
--background: oklch(0.27 0.006 286.03);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.21 0.006 285.885);
|
--card: oklch(0.315 0.007 286.05);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.92 0.004 286.32);
|
--primary: oklch(0.92 0.004 286.32);
|
||||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||||
--secondary: oklch(0.274 0.006 286.033);
|
--secondary: oklch(0.37 0.013 285.805);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.274 0.006 286.033);
|
--muted: oklch(0.37 0.013 285.805);
|
||||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
--muted-foreground: oklch(0.74 0.015 286.07);
|
||||||
--accent: oklch(0.274 0.006 286.033);
|
--accent: oklch(0.37 0.013 285.805);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--destructive-foreground: oklch(0.985 0 0);
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
--success: oklch(0.696 0.17 149.2);
|
--success: oklch(0.696 0.17 149.2);
|
||||||
--border: oklch(1 0 0 / 12%);
|
--border: oklch(1 0 0 / 14%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 18%);
|
||||||
--ring: oklch(0.552 0.016 285.938);
|
--ring: oklch(0.552 0.016 285.938);
|
||||||
--icon-folder: oklch(0.75 0.11 250);
|
--icon-folder: oklch(0.75 0.11 250);
|
||||||
--icon-link: oklch(0.74 0.09 190);
|
--icon-link: oklch(0.74 0.09 190);
|
||||||
@@ -96,6 +96,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark tone "darker" (data-dark-tone set by SettingsService; absent = grey "dark").
|
||||||
|
Near-black AMOLED palette. Duplicated for forced-dark and system-dark like the
|
||||||
|
token blocks above — the extra attribute outranks them. */
|
||||||
|
:root[data-theme='dark'][data-dark-tone='darker'] {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--border: oklch(1 0 0 / 12%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme='light'])[data-dark-tone='darker'] {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--border: oklch(1 0 0 / 12%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
|
|||||||
Reference in New Issue
Block a user