Add 4 switchable themes (Anthropic / Instrument / Editorial / Retro CRT)

- @fontsource-variable/{fraunces,jetbrains-mono,newsreader} + @fontsource/{dm-sans,ibm-plex-mono} bundled offline.
- styles.css restructured: theme-agnostic base + 4 [data-theme="..."] overrides driving CSS custom props (--bg, --fg, --accent, --font-display, --font-body, --atmosphere).
- Each theme has its own typographic + chromatic personality:
  * Anthropic: warm cream-on-charcoal, Newsreader display + DM Sans body, sunset orange + claude purple.
  * Instrument: synth panel, JetBrains Mono throughout, chartreuse on slate, ring tick marks, faint scanlines, bracket corners on title bar.
  * Editorial: magazine artifact, Fraunces variable serif (opsz axis), saffron on warm charcoal, hairline rules.
  * Retro CRT: phosphor green on near-black, IBM Plex Mono, scanlines + vignette, blink-cursor in corner, [bracketed] header label.
- Settings panel: 4-up theme picker (each card renders a sample percentage in that theme's actual fonts/colors). Click = live preview; Cancel reverts; Save persists.
- BlockRing big % bumped to 38px logical with theme-specific font-variation-settings.
- TitleBar voice differs per theme without changing underlying string.
- Default theme: Anthropic (warmest first impression).
This commit is contained in:
megaproxy 2026-05-09 15:43:57 +01:00
parent 5200caf21f
commit 8a7ebd60b1
9 changed files with 575 additions and 39 deletions

View file

@ -11,6 +11,11 @@
"tauri": "tauri"
},
"dependencies": {
"@fontsource-variable/fraunces": "^5.0.0",
"@fontsource-variable/jetbrains-mono": "^5.0.0",
"@fontsource-variable/newsreader": "^5.0.0",
"@fontsource/dm-sans": "^5.0.0",
"@fontsource/ibm-plex-mono": "^5.0.0",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-autostart": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0",

View file

@ -42,6 +42,8 @@ pub struct Settings {
pub claude_command: Option<String>,
/// How often (seconds) to refetch `/usage`. Defaults to 300 (5 min).
pub cli_refresh_secs: u64,
/// One of: "anthropic" (default), "instrument", "editorial", "retro".
pub theme: String,
}
impl Default for Settings {
@ -54,6 +56,7 @@ impl Default for Settings {
autostart: false,
claude_command: None,
cli_refresh_secs: 300,
theme: "anthropic".to_string(),
}
}
}

View file

@ -22,10 +22,18 @@
let showSettings = $state(false);
/** True when Claude Code can't be found anywhere on this machine. */
let claudeMissing = $state(false);
/** Active theme; applied as data-theme on the document root. */
let theme = $state<string>("anthropic");
let unlisten1: (() => void) | null = null;
let unlisten2: (() => void) | null = null;
// Whenever theme changes, propagate it to the document root so CSS
// [data-theme="..."] selectors take effect.
$effect(() => {
document.documentElement.setAttribute("data-theme", theme);
});
onMount(async () => {
try {
snap = await getSnapshot();
@ -44,9 +52,10 @@
console.error("listen failed", e);
}
// Probe whether claude is reachable at all.
// Probe whether claude is reachable at all + load theme preference.
try {
const settings = await getSettings();
theme = settings.theme || "anthropic";
const hasOverride = !!(settings.claude_command && settings.claude_command.trim());
const auto = await autodetectClaudeCommand();
claudeMissing = !hasOverride && !auto;

View file

@ -52,6 +52,23 @@
transform={`rotate(-90 ${SIZE / 2} ${SIZE / 2})`}
class:pulse
/>
<!-- Quarter-position tick marks; only the instrument theme shows them. -->
<g class="ticks" aria-hidden="true">
{#each [0, 90, 180, 270] as deg (deg)}
{@const a = (deg - 90) * Math.PI / 180}
{@const r1 = R - STROKE / 2 - 2}
{@const r2 = R + STROKE / 2 + 2}
<line
x1={SIZE / 2 + r1 * Math.cos(a)}
y1={SIZE / 2 + r1 * Math.sin(a)}
x2={SIZE / 2 + r2 * Math.cos(a)}
y2={SIZE / 2 + r2 * Math.sin(a)}
stroke="currentColor"
stroke-width="1"
/>
{/each}
</g>
<text x={SIZE / 2} y={SIZE / 2 - 6} text-anchor="middle" class="big">
{bar ? `${bar.percent}%` : fallbackText}
</text>
@ -83,13 +100,53 @@
max-width: 220px; /* don't get absurdly large in a wide window */
max-height: 220px;
}
text { fill: var(--fg); font-family: inherit; }
text.big { font-size: 26px; font-weight: 600; }
text.small { font-size: 11px; fill: var(--fg-dim); }
text.resets { font-size: 10px; fill: var(--fg-dim); }
text { fill: var(--fg); }
text.big {
font-size: 38px;
font-weight: 600;
font-family: var(--font-display);
font-variant-numeric: tabular-nums;
}
text.small {
font-size: 11px;
fill: var(--fg-dim);
font-family: var(--font-body);
text-transform: lowercase;
letter-spacing: 0.5px;
}
text.resets {
font-size: 10px;
fill: var(--fg-dim);
font-family: var(--font-body);
}
.pulse { animation: pulse 1.6s ease-in-out infinite; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.55; }
}
/* Per-theme touches on the ring text */
:global([data-theme="editorial"]) text.big {
font-variation-settings: "opsz" 144, "SOFT" 50;
font-weight: 400;
}
:global([data-theme="editorial"]) text.small {
font-style: italic;
}
:global([data-theme="retro"]) text.big {
font-weight: 700;
letter-spacing: -1px;
}
:global([data-theme="instrument"]) text.big {
font-weight: 700;
letter-spacing: -1px;
}
:global([data-theme="anthropic"]) text.big {
font-variation-settings: "opsz" 36;
font-weight: 500;
}
/* Tick marks: hidden everywhere except instrument theme */
.ticks { color: var(--fg-dim); display: none; }
:global([data-theme="instrument"]) .ticks { display: block; }
</style>

View file

@ -24,6 +24,19 @@
let busy = $state(false);
let testingCli = $state(false);
let err = $state<string | null>(null);
/** Theme captured on mount so Cancel can revert a live preview. */
let originalTheme = "anthropic";
const THEMES = [
{ id: "anthropic", label: "Anthropic", blurb: "Warm cream-on-charcoal · serif" },
{ id: "instrument", label: "Instrument", blurb: "Synth panel · mono · chartreuse" },
{ id: "editorial", label: "Editorial", blurb: "Magazine artifact · saffron" },
{ id: "retro", label: "Retro CRT", blurb: "Phosphor green · scanlines" },
] as const;
function applyThemeLive(id: string) {
document.documentElement.setAttribute("data-theme", id);
}
onMount(async () => {
try {
@ -31,6 +44,7 @@
distros = await listDistros();
roots = await getRoots();
cli = await getCliUsage();
originalTheme = settings.theme || "anthropic";
const enabled = await isAutostartEnabled();
if (settings && settings.autostart !== enabled) {
settings = { ...settings, autostart: enabled };
@ -56,6 +70,8 @@
try {
await setSettings(settings);
// Successful save: lock in the live-previewed theme.
originalTheme = settings.theme;
onClose();
} catch (e) {
err = `${e}`;
@ -64,6 +80,18 @@
}
}
function cancel() {
// Revert any live theme preview the user was sampling.
applyThemeLive(originalTheme);
onClose();
}
function pickTheme(id: string) {
if (!settings) return;
settings = { ...settings, theme: id as any };
applyThemeLive(id);
}
async function testCli() {
if (!settings) return;
testingCli = true;
@ -84,12 +112,32 @@
<div class="panel">
<div class="row spread">
<strong>Settings</strong>
<button class="icon" onclick={onClose} aria-label="Close settings">×</button>
<button class="icon" onclick={cancel} aria-label="Close settings">×</button>
</div>
{#if err}<div class="error">{err}</div>{/if}
{#if settings}
<div class="field">
<div class="label">theme</div>
<div class="theme-grid">
{#each THEMES as t (t.id)}
<button
type="button"
class="theme-card"
class:selected={settings.theme === t.id}
data-theme-preview={t.id}
onclick={() => pickTheme(t.id)}
aria-pressed={settings.theme === t.id}
>
<span class="tp-name">{t.label}</span>
<span class="tp-sample">42%</span>
<span class="tp-blurb">{t.blurb}</span>
</button>
{/each}
</div>
</div>
<div class="field">
<div class="label">claude command</div>
<input
@ -166,7 +214,7 @@
{/if}
<div class="row spread actions">
<button onclick={onClose}>Cancel</button>
<button onclick={cancel}>Cancel</button>
<button onclick={save} disabled={busy}>{busy ? "Saving…" : "Save"}</button>
</div>
{:else}
@ -199,6 +247,116 @@
.roots { margin: 0; padding-left: 14px; max-height: 60px; overflow: auto; }
.roots code { font-size: 10px; word-break: break-all; }
.hint { font-size: 10px; line-height: 1.3; }
/* ---- Theme picker ---- */
.theme-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.theme-card {
display: grid;
grid-template-rows: auto 1fr auto;
gap: 2px;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid var(--border);
cursor: pointer;
text-align: left;
overflow: hidden;
position: relative;
min-height: 64px;
}
.theme-card .tp-name {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
opacity: 0.65;
}
.theme-card .tp-sample {
font-size: 22px;
line-height: 1;
font-weight: 600;
margin-top: 2px;
}
.theme-card .tp-blurb {
font-size: 9px;
opacity: 0.55;
line-height: 1.2;
}
.theme-card.selected {
outline: 1.5px solid var(--accent);
outline-offset: -1.5px;
}
.theme-card.selected::after {
content: "✓";
position: absolute;
top: 4px;
right: 6px;
font-size: 11px;
color: var(--accent);
}
/* Per-theme card visuals (preview in their own typography & palette) */
.theme-card[data-theme-preview="anthropic"] {
background: linear-gradient(180deg, #1a1815 0%, #1a1815 100%);
color: #faf9f5;
font-family: "DM Sans", sans-serif;
}
.theme-card[data-theme-preview="anthropic"] .tp-sample {
font-family: "Newsreader Variable", "Newsreader", Georgia, serif;
font-weight: 500;
color: #cc785c;
}
.theme-card[data-theme-preview="instrument"] {
background:
repeating-linear-gradient(180deg, transparent 0 2px, rgba(212,255,61,0.04) 2px 3px),
#0e1014;
color: #d6e2d8;
font-family: "JetBrains Mono Variable", monospace;
border-color: rgba(212, 255, 61, 0.25);
}
.theme-card[data-theme-preview="instrument"] .tp-sample {
color: #d4ff3d;
font-family: "JetBrains Mono Variable", monospace;
font-weight: 700;
}
.theme-card[data-theme-preview="editorial"] {
background: #1a1813;
color: #f5f1e8;
font-family: "Fraunces Variable", Georgia, serif;
border-radius: 4px;
}
.theme-card[data-theme-preview="editorial"] .tp-name {
font-style: italic;
text-transform: none;
letter-spacing: 0;
font-size: 11px;
}
.theme-card[data-theme-preview="editorial"] .tp-sample {
color: #e8a02f;
font-family: "Fraunces Variable", serif;
font-weight: 500;
font-variation-settings: "opsz" 144;
}
.theme-card[data-theme-preview="retro"] {
background:
repeating-linear-gradient(180deg, transparent 0 1px, rgba(0,0,0,0.4) 1px 2px),
#0a0e0a;
color: #b8f0b8;
font-family: "IBM Plex Mono", monospace;
border-color: rgba(122, 240, 122, 0.3);
border-radius: 3px;
text-shadow: 0 0 4px rgba(122,240,122,0.4);
}
.theme-card[data-theme-preview="retro"] .tp-sample {
color: #7af07a;
font-family: "IBM Plex Mono", monospace;
font-weight: 700;
}
pre.raw {
margin: 4px 0 0;
padding: 6px;

View file

@ -60,6 +60,7 @@
letter-spacing: 0.8px;
color: var(--fg-dim);
cursor: grab;
font-family: var(--font-body);
}
.actions { display: inline-flex; gap: 2px; }
.icon.spin { animation: spin 1.2s linear infinite; }
@ -67,4 +68,48 @@
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Per-theme title voice ------------------------------------------------ */
/* Anthropic: serif italic, soft and warm */
:global([data-theme="anthropic"]) .title {
font-family: var(--font-display);
font-style: italic;
font-weight: 400;
text-transform: none;
letter-spacing: 0;
font-size: 13px;
color: var(--fg);
opacity: 0.85;
}
/* Instrument: tracked monospace caps, accent colored */
:global([data-theme="instrument"]) .title {
color: var(--accent);
font-weight: 700;
letter-spacing: 1.6px;
font-size: 10px;
}
/* Editorial: italic display serif */
:global([data-theme="editorial"]) .title {
font-family: var(--font-display);
font-style: italic;
text-transform: none;
letter-spacing: 0.2px;
font-size: 14px;
font-variation-settings: "opsz" 18, "SOFT" 50;
color: var(--fg);
opacity: 0.85;
}
/* Retro: bracketed CLI-style label */
:global([data-theme="retro"]) .title {
font-weight: 700;
color: var(--accent);
letter-spacing: 1px;
font-size: 11px;
}
:global([data-theme="retro"]) .title::before { content: "[ "; opacity: 0.6; }
:global([data-theme="retro"]) .title::after { content: " ]"; opacity: 0.6; }
</style>

View file

@ -1,5 +1,19 @@
import { mount } from "svelte";
import App from "./components/App.svelte";
// Theme fonts — bundled offline so the widget renders identically without a
// network connection. Variable-axis fonts where available so we can tune
// weight/optical size from CSS.
import "@fontsource-variable/jetbrains-mono";
import "@fontsource-variable/fraunces";
import "@fontsource-variable/newsreader";
import "@fontsource/dm-sans/400.css";
import "@fontsource/dm-sans/500.css";
import "@fontsource/dm-sans/600.css";
import "@fontsource/ibm-plex-mono/400.css";
import "@fontsource/ibm-plex-mono/500.css";
import "@fontsource/ibm-plex-mono/700.css";
import "./styles.css";
const app = mount(App, { target: document.getElementById("app")! });

View file

@ -1,25 +1,11 @@
/* Glass / always-on-top widget look. */
/* ============================================================
Base structure (theme-agnostic).
Theme tokens live in the [data-theme="..."] blocks below.
============================================================ */
:root {
--bg: rgba(18, 20, 26, 0.93);
--bg-card: rgba(255, 255, 255, 0.04);
--border: rgba(255, 255, 255, 0.08);
--fg: #e8eaf0;
--fg-dim: #9aa0aa;
--accent: #b08bff; /* Anthropic-ish purple */
--opus: #b08bff;
--sonnet: #6ec1ff;
--haiku: #7ee0a3;
--other: #d8d8d8;
--warn: #ffb454;
--danger: #ff6b6b;
--radius: 10px;
--pad: 10px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 13px;
color: var(--fg);
}
*, *::before, *::after { box-sizing: border-box; }
@ -29,10 +15,16 @@ html, body {
padding: 0;
width: 100%;
height: 100%;
background: transparent; /* honor Tauri transparent:true */
overflow: hidden; /* viewport never scrolls; sections handle their own */
background: transparent;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
font-family: var(--font-body);
font-size: 13px;
color: var(--fg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
#app {
@ -41,18 +33,27 @@ html, body {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden; /* the window itself never scrolls */
overflow: hidden;
display: flex;
flex-direction: column;
backdrop-filter: blur(14px);
position: relative;
}
/* Each child of #app gets a sensible flex behavior so resizing reflows
instead of overflowing. TitleBar and section panels are flex:0 (size
to content); the BlockRing wrap is flex:1 (claims remaining space). */
/* Optional decorative atmosphere layer per theme set --atmosphere
to a `background` value (e.g. a gradient + noise) to enable. */
#app::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: var(--atmosphere, none);
opacity: var(--atmosphere-opacity, 0);
mix-blend-mode: var(--atmosphere-blend, normal);
z-index: 0;
}
#app > * { position: relative; z-index: 1; }
/* Hide scrollbars unless content really overflows; when they do appear,
make them subtle. */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); border-radius: 3px; }
::-webkit-scrollbar-track { background: transparent; }
@ -65,8 +66,9 @@ button {
border-radius: 6px;
padding: 4px 8px;
cursor: pointer;
font-family: var(--font-body);
}
button:hover { background: rgba(255, 255, 255, 0.06); }
button:hover { background: var(--hover); }
button.icon {
border: none;
padding: 4px;
@ -82,12 +84,12 @@ button.icon:hover { color: var(--fg); }
input[type="number"], input[type="text"], select {
font: inherit;
color: inherit;
background: rgba(0, 0, 0, 0.25);
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 6px;
width: 100%;
box-sizing: border-box;
font-family: var(--font-body);
}
.section {
@ -98,9 +100,9 @@ input[type="number"], input[type="text"], select {
.section:first-child { border-top: none; }
.label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.6px;
font: var(--label-font);
text-transform: var(--label-transform, uppercase);
letter-spacing: var(--label-tracking, 0.6px);
color: var(--fg-dim);
margin-bottom: 4px;
}
@ -108,3 +110,241 @@ input[type="number"], input[type="text"], select {
.row { display: flex; align-items: center; gap: 8px; }
.row.spread { justify-content: space-between; }
.muted { color: var(--fg-dim); font-size: 11px; }
/* Tabular numerals everywhere a number can change */
.num, .big, text.big, .muted, .label, .bar-row .num {
font-variant-numeric: tabular-nums;
}
/* ============================================================
Theme: ANTHROPIC (default)
Warm charcoal + cream + sunset orange + claude purple.
Newsreader serif for display, DM Sans for body.
============================================================ */
[data-theme="anthropic"] {
--font-display: "Newsreader Variable", "Newsreader", Georgia, serif;
--font-body: "DM Sans", -apple-system, "Segoe UI", sans-serif;
--font-mono: "DM Sans", monospace;
--bg: #1a1815;
--bg-card: rgba(250, 249, 245, 0.04);
--border: rgba(250, 249, 245, 0.10);
--hover: rgba(250, 249, 245, 0.06);
--input-bg: rgba(0, 0, 0, 0.20);
--fg: #faf9f5;
--fg-dim: #b8ad9e;
--accent: #cc785c; /* sunset orange */
--accent-2: #c084fc; /* claude purple */
--opus: #c084fc;
--sonnet: #6ec1ff;
--haiku: #a3d9a5;
--other: #d8d8d8;
--warn: #e8a02f;
--danger: #d97559;
--label-font: 500 10px/1 var(--font-body);
--atmosphere: radial-gradient(60% 50% at 50% 0%, rgba(204, 120, 92, 0.18), transparent 60%);
--atmosphere-opacity: 1;
}
/* ============================================================
Theme: INSTRUMENT
Modular synth panel. JetBrains Mono. Chartreuse on slate.
Faint scanlines, tick marks on the ring.
============================================================ */
[data-theme="instrument"] {
--font-display: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, monospace;
--font-body: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, monospace;
--font-mono: "JetBrains Mono Variable", monospace;
--bg: #0e1014;
--bg-card: rgba(255, 255, 255, 0.03);
--border: rgba(212, 255, 61, 0.18);
--hover: rgba(212, 255, 61, 0.06);
--input-bg: rgba(0, 0, 0, 0.45);
--fg: #d6e2d8;
--fg-dim: #6f8478;
--accent: #d4ff3d; /* chartreuse */
--accent-2: #5dd8ff; /* cyan secondary */
--opus: #d4ff3d;
--sonnet: #5dd8ff;
--haiku: #ff6fb5;
--other: #9aa0aa;
--warn: #ffb454;
--danger: #ff3b88;
--label-font: 600 9px/1 var(--font-mono);
--label-tracking: 1.4px;
/* Faint horizontal scanlines + a subtle grid */
--atmosphere:
repeating-linear-gradient(
180deg,
transparent 0px,
transparent 2px,
rgba(212, 255, 61, 0.025) 2px,
rgba(212, 255, 61, 0.025) 3px
),
radial-gradient(80% 60% at 50% 0%, rgba(212, 255, 61, 0.05), transparent 70%);
--atmosphere-opacity: 1;
}
[data-theme="instrument"] #app {
border-color: rgba(212, 255, 61, 0.22);
}
/* Bracket-corners on the title bar in instrument mode */
[data-theme="instrument"] header::before,
[data-theme="instrument"] header::after {
content: "";
position: absolute;
width: 6px; height: 6px;
border: 1px solid var(--accent);
opacity: 0.5;
}
[data-theme="instrument"] header { position: relative; }
[data-theme="instrument"] header::before {
top: 4px; left: 4px;
border-right: 0; border-bottom: 0;
}
[data-theme="instrument"] header::after {
top: 4px; right: 4px;
border-left: 0; border-bottom: 0;
}
/* ============================================================
Theme: EDITORIAL
Magazine artifact. Fraunces serif. Saffron on warm charcoal.
Hairline rules, generous spacing.
============================================================ */
[data-theme="editorial"] {
--font-display: "Fraunces Variable", "Fraunces", Georgia, serif;
--font-body: "Fraunces Variable", "Fraunces", Georgia, serif;
--font-mono: "Fraunces Variable", monospace;
--bg: #1a1813;
--bg-card: rgba(245, 241, 232, 0.03);
--border: rgba(245, 241, 232, 0.12);
--hover: rgba(245, 241, 232, 0.05);
--input-bg: rgba(0, 0, 0, 0.25);
--fg: #f5f1e8;
--fg-dim: #9b9183;
--accent: #e8a02f; /* saffron */
--accent-2: #d97559;
--opus: #e8a02f;
--sonnet: #c8987a;
--haiku: #a3a87a;
--other: #9b9183;
--warn: #d97559;
--danger: #b54a3c;
--label-font: italic 400 11px/1 var(--font-display);
--label-transform: none;
--label-tracking: 0.2px;
}
/* Editorial uses small-caps italic for labels — let the variable axis breathe */
[data-theme="editorial"] .label {
font-variation-settings: "opsz" 14, "SOFT" 50;
}
[data-theme="editorial"] #app {
border-radius: 6px;
}
[data-theme="editorial"] .section {
border-top-style: solid;
border-top-width: 0.5px;
}
/* ============================================================
Theme: RETRO CRT
1980s home computer. IBM Plex Mono. Phosphor green.
Scanlines, bracket header, blink cursor.
============================================================ */
[data-theme="retro"] {
--font-display: "IBM Plex Mono", ui-monospace, monospace;
--font-body: "IBM Plex Mono", ui-monospace, monospace;
--font-mono: "IBM Plex Mono", monospace;
--bg: #0a0e0a;
--bg-card: rgba(122, 240, 122, 0.03);
--border: rgba(122, 240, 122, 0.20);
--hover: rgba(122, 240, 122, 0.08);
--input-bg: rgba(0, 0, 0, 0.55);
--fg: #b8f0b8;
--fg-dim: #5a8a5a;
--accent: #7af07a; /* phosphor green */
--accent-2: #ffb454; /* amber for warnings */
--opus: #7af07a;
--sonnet: #5dd8ff;
--haiku: #ffe066;
--other: #b8f0b8;
--warn: #ffb454;
--danger: #ff5a5a;
--label-font: 700 9px/1 var(--font-mono);
--label-tracking: 1.5px;
/* Strong scanlines + vignette + faint phosphor glow */
--atmosphere:
repeating-linear-gradient(
180deg,
transparent 0px,
transparent 1px,
rgba(0, 0, 0, 0.35) 1px,
rgba(0, 0, 0, 0.35) 2px
),
radial-gradient(120% 90% at 50% 50%, transparent 50%, rgba(0, 0, 0, 0.55) 100%),
radial-gradient(60% 40% at 50% 30%, rgba(122, 240, 122, 0.10), transparent 70%);
--atmosphere-opacity: 1;
}
[data-theme="retro"] #app {
border-radius: 4px;
border-color: rgba(122, 240, 122, 0.30);
text-shadow: 0 0 6px rgba(122, 240, 122, 0.35);
}
[data-theme="retro"] header { border-bottom-style: dashed; }
/* Blink cursor in the bottom-right of the widget — pure decoration */
[data-theme="retro"] #app::after {
content: "█";
position: absolute;
bottom: 6px;
right: 10px;
color: var(--accent);
opacity: 0.8;
animation: retro-blink 1.1s steps(2, end) infinite;
font-family: var(--font-mono);
font-size: 12px;
pointer-events: none;
z-index: 2;
}
@keyframes retro-blink {
50% { opacity: 0; }
}
/* ============================================================
Reduced-motion accommodation: kill all the breathing effects.
============================================================ */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}

View file

@ -45,12 +45,17 @@ export interface UsageSnapshot {
generated_at: string;
}
export type Theme = "anthropic" | "instrument" | "editorial" | "retro";
export interface Settings {
caps: Caps;
wsl_distro_override: string | null;
include_native: boolean;
window_pos: [number, number] | null;
autostart: boolean;
claude_command: string | null;
cli_refresh_secs: number;
theme: Theme;
}
export interface ResolvedRoots {