Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1bbc6f8ba | |||
| 160e08d4a8 | |||
| 6dd8200802 | |||
| 4d5bce244a | |||
| 14d9ca0b41 | |||
| 3a4b1a4d7a | |||
| 8a7ebd60b1 | |||
| 5200caf21f | |||
| dbf77a0cec |
22 changed files with 586 additions and 167 deletions
42
.gitattributes
vendored
Normal file
42
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Always check out source files with LF endings, regardless of the user's
|
||||
# core.autocrlf setting. Without this, Windows clones with core.autocrlf=true
|
||||
# (the Git-for-Windows default) treat every text file as "modified" because
|
||||
# Git's stored version is LF but the working copy is CRLF.
|
||||
|
||||
* text=auto eol=lf
|
||||
|
||||
# Explicit text files (belt-and-suspenders).
|
||||
*.rs text eol=lf
|
||||
*.toml text eol=lf
|
||||
*.json text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.js text eol=lf
|
||||
*.svelte text eol=lf
|
||||
*.css text eol=lf
|
||||
*.html text eol=lf
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
# Shell + Windows scripts keep their native endings.
|
||||
*.sh text eol=lf
|
||||
*.ps1 text eol=crlf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# Binaries — never touch these.
|
||||
*.png binary
|
||||
*.ico binary
|
||||
*.icns binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.exe binary
|
||||
*.dll binary
|
||||
*.so binary
|
||||
*.dylib binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -47,3 +47,17 @@ src-tauri/target/
|
|||
src-tauri/Cargo.lock
|
||||
*.tsbuildinfo
|
||||
.vite/
|
||||
|
||||
# Extra outputs from `pnpm tauri icon` — we ship only the canonical set
|
||||
# (32x32.png, 128x128.png, 128x128@2x.png, icon.ico, icon.icns).
|
||||
src-tauri/icons/64x64.png
|
||||
src-tauri/icons/icon.png
|
||||
src-tauri/icons/Square*.png
|
||||
src-tauri/icons/StoreLogo.png
|
||||
src-tauri/icons/android/
|
||||
src-tauri/icons/ios/
|
||||
|
||||
# Agent working files — meaningful to local Claude Code sessions, noise for
|
||||
# anyone else. Kept out of the public repo (they still live on disk).
|
||||
CLAUDE.md
|
||||
memory.md
|
||||
|
|
|
|||
30
CLAUDE.md
30
CLAUDE.md
|
|
@ -1,30 +0,0 @@
|
|||
# Project: claude-usage-widget
|
||||
|
||||
A small always-on-top Windows desktop widget that visualizes local Claude Code usage — current 5-hour session block, 7-day rolling weekly window, and per-model token breakdown. Reads `~/.claude/projects/**/*.jsonl` directly (via `\\wsl$\<distro>\…` from Windows). No Anthropic API. No auth.
|
||||
|
||||
## Working agreement
|
||||
|
||||
- This is a git repo. Commit after each logical change with a one-line imperative message.
|
||||
- Read `memory.md` at session start. Update it before ending the session.
|
||||
- Never commit secrets — see `.gitignore` and the rules in `~/claude/CLAUDE.md`.
|
||||
|
||||
## Project-specific notes
|
||||
|
||||
- **Stack:** Tauri 2 (Rust + Svelte 5 + Vite + TS). Inline SVG for charts, no chart library.
|
||||
- **Build target:** Windows `.exe` only. Develop the Rust + frontend code in WSL; do `pnpm tauri dev` / `pnpm tauri build` on the Windows host (it needs MSVC toolchain + WebView2).
|
||||
- **Data source:** `~/.claude/projects/**/*.jsonl` — assistant lines have `message.usage.{input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens}` and a `requestId` / `uuid` for dedupe (subagent transcripts in `<sessionId>/subagents/<id>.jsonl` overlap the parent).
|
||||
- **Aggregation algorithm:** ccusage-equivalent. 5-hour blocks are `floor_to_hour(first_ts) → +5h`; new block on ≥5h gap or when previous block ends. Weekly is rolling 7 days.
|
||||
- **Plan reference:** `~/.claude/plans/snug-mapping-milner.md` (the approved plan that drove this scaffold).
|
||||
|
||||
## Run
|
||||
|
||||
```powershell
|
||||
# On the Windows host, from this directory:
|
||||
pnpm install
|
||||
pnpm tauri dev # iterate
|
||||
pnpm tauri build # NSIS installer in src-tauri/target/release/bundle/nsis/
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
See `memory.md` and the plan file for the 10-step verification checklist (parse correctness, block boundary, dedupe, live tail, watcher fallback, autostart, etc).
|
||||
31
README.md
31
README.md
|
|
@ -4,24 +4,16 @@ A small always-on-top Windows desktop widget that shows your live Claude
|
|||
subscription usage — the same percentages Claude Code's `/usage` command
|
||||
displays, refreshed every 5 minutes.
|
||||
|
||||
```
|
||||
┌─────── Claude Usage ────────────╳ ─┐
|
||||
│ │
|
||||
│ ╭───────╮ │
|
||||
│ ╱ 72% ╲ │
|
||||
│ │ session │ │
|
||||
│ ╲ resets ╱ │
|
||||
│ ╰ 2:50am ╯ │
|
||||
│ │
|
||||
│ Models (current block) │
|
||||
│ ▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱ │
|
||||
│ ● Opus 42M ● Haiku 3M │
|
||||
│ │
|
||||
│ Weekly limits resets May 9 │
|
||||
│ All models ▆░░░░░ 8% │
|
||||
│ Sonnet ▃░░░░░ 5% │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
## Themes
|
||||
|
||||
Pick one in Settings; it applies live. Each is its own typographic + chromatic system.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Anthropic** — warm cream-on-charcoal, Newsreader serif + DM Sans, sunset-orange ring, soft radial halo. The default. | **Instrument** — modular-synth panel, JetBrains Mono, chartreuse on slate, tick marks at 12/3/6/9, faint scanlines, bracket corners on the title bar. |
|
||||
|  |  |
|
||||
| **Editorial** — magazine artifact, Fraunces variable serif (optical-size axis), saffron on warm charcoal, italic labels, hairline rules. | **Retro CRT** — 1980s home computer, IBM Plex Mono, phosphor green on near-black, scanlines + vignette, bracketed `[ Claude Usage ]` header, blink cursor in the corner. |
|
||||
|  |  |
|
||||
|
||||
## Install
|
||||
|
||||
|
|
@ -104,5 +96,4 @@ pnpm tauri dev # iterate
|
|||
pnpm tauri build # produces NSIS installer in src-tauri\target\release\bundle\nsis\
|
||||
```
|
||||
|
||||
Project layout, architecture decisions, and known follow-ups live in
|
||||
[`memory.md`](./memory.md).
|
||||
Filing issues / pull requests on the Forgejo repo is welcome.
|
||||
|
|
|
|||
BIN
docs/screenshots/anthropic.png
Executable file
BIN
docs/screenshots/anthropic.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/screenshots/editorial.png
Executable file
BIN
docs/screenshots/editorial.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/screenshots/instrument.png
Executable file
BIN
docs/screenshots/instrument.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/screenshots/retro.png
Executable file
BIN
docs/screenshots/retro.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
66
memory.md
66
memory.md
|
|
@ -1,66 +0,0 @@
|
|||
# memory — claude-usage-widget
|
||||
|
||||
Durable memory for this project. Read at session start, update before session end. Date format: `YYYY-MM-DD`.
|
||||
|
||||
## Decisions & rationale
|
||||
|
||||
- **Tauri 2 (Rust + Svelte 5 + Vite + TS)** over Electron — smaller binary (~10 MB vs ~150 MB), native Windows transparency, real always-on-top z-order. Chose Svelte over React because the widget has only three SVG primitives; React boilerplate isn't worth it.
|
||||
- **Inline SVG, no chart library** — `BlockRing` is one ring, `WeeklyBar` is two stacked progress bars, `ModelStack` is a single segmented bar. Adding Chart.js / ECharts / uPlot for ~80 lines of SVG would balloon the bundle for nothing.
|
||||
- **Subscription %s come from PTY-driving `claude /usage`** (NOT JSONL estimates, NOT the Anthropic API). The widget spawns `claude` via `portable-pty`, sends `/usage`, parses the three rendered bars (Current session / Current week all / Current week Sonnet), and shows those exact numbers. This is the same data Anthropic shows you in the CLI — no API key, no admin scope, no reverse-engineering of their backend. The trade-off is ~3-5 s per refresh and brittleness if Anthropic changes the rendered output format. Refresh every 5 min by default; manual refresh button on the title bar.
|
||||
- **Per-model breakdown still comes from local JSONL** — the CLI's `/usage` doesn't break out Opus/Sonnet/Haiku, but our token-summing does. ModelStack remains.
|
||||
- **Widget runs on Windows host, not in WSLg** — needs to pin to the Windows desktop, autostart on login, and share the always-on-top z-order with native Windows apps. WSLg windows can't do that. JSONL transcripts read via `\\wsl$\<distro>\home\<user>\.claude\projects\` UNC mount; the PTY-driven `claude` is invoked via `wsl.exe -d Ubuntu bash -lc claude` (default on Windows when wsl.exe is on PATH).
|
||||
- **`notify` watcher + 60s tokio poll fallback** — `ReadDirectoryChangesW` on the WSL 9P mount is unreliable; the poll backstops it.
|
||||
- **All filesystem reads happen Rust-side** — the JS `capabilities/default.json` does NOT grant `tauri-plugin-fs`. Keeps the webview sandbox tight.
|
||||
- **Block algorithm (still in code, used for ModelStack only)** — `block_start = floor_to_hour(first_ts_of_block)`, `block_end = block_start + 5h`, new block on ≥5h gap OR when previous block ends. ccusage-equivalent.
|
||||
- **DROPPED: caps + tier-detection UI.** Replaced by real CLI percentages. Caps struct still exists in code as a deprecated fallback but the Settings panel no longer exposes it.
|
||||
|
||||
## Open questions / TODOs
|
||||
|
||||
- [ ] **Watcher does not re-bind on settings change.** If user changes WSL distro override in Settings, `set_settings` calls `refresh_and_emit` and updates `state.roots`, but the `notify` watcher is still pinned to the *old* roots. v0 workaround: restart the widget after changing distro.
|
||||
- [ ] **`/usage` parser is fragile to output format changes.** If Anthropic changes the rendered text (relabels sections, adds new ones, changes "X% used" pattern), the bars stop parsing silently. Settings panel exposes the raw output for debugging when this happens.
|
||||
- [ ] **`/usage` spawn cost is ~3-5s on Windows.** That's per refresh; default refresh is 300s so net overhead is fine. Title-bar refresh button gives user control. Consider caching to disk so cold start has *something* before the first PTY drive completes.
|
||||
- [ ] **Autostart toggle fails in dev builds** (target\debug\ exe path is unstable). Currently swallowed as a warning; needs proper testing once we ship the NSIS bundle.
|
||||
- [ ] **The default WSL-on-Windows command assumes Ubuntu.** Auto-detect could iterate `wsl.exe -l -q` for any distro that has `claude` on its login PATH, instead of hardcoding `-d Ubuntu`.
|
||||
- [ ] Decide whether to expose a tray icon for relaunch after `quit_app` (currently the widget can only be reopened via Start Menu / autostart).
|
||||
- [ ] Window background is too transparent — files behind it bleed through visibly. Bump `--bg` opacity from 0.78 to ~0.92.
|
||||
- [ ] Replace placeholder Tauri icons in `src-tauri/icons/` before release (`pnpm tauri icon source.png`).
|
||||
- [ ] Caps struct + Caps::default() are dead code now — delete after a few releases of stability.
|
||||
|
||||
## Session log
|
||||
|
||||
### 2026-05-08 / 2026-05-09
|
||||
- Planned the project (approved plan at `~/.claude/plans/snug-mapping-milner.md`) and built the full scaffold in one session that crossed midnight UTC.
|
||||
- Authored Tauri config (frameless, transparent, alwaysOnTop, skipTaskbar, 280×360), Cargo.toml, capabilities/default.json.
|
||||
- Authored Rust modules: `state.rs`, `settings.rs`, `paths.rs`, `jsonl.rs`, `usage.rs` (with unit tests for block boundaries), `watch.rs`, `commands.rs`, `lib.rs`, `main.rs`.
|
||||
- Authored Svelte 5 frontend: `App`, `TitleBar`, `BlockRing`, `WeeklyBar`, `ModelStack`, `Settings` components plus `ipc.ts`, `types.ts`, `format.ts`, `styles.css`.
|
||||
- Wrote `scripts/seed-fake-jsonl.ps1` verification helper and `README.md` build instructions.
|
||||
- Pushed to Forgejo at `https://git.rdx4.com/megaproxy/claude-usage-widget`. (Note: had to rename local `master` → `main` after the fact — `git init` ran before the global `init.defaultBranch=main` was set.)
|
||||
- **First successful run on Windows host (Doug's machine).** Took 4 incremental fixes to get there:
|
||||
1. pnpm 11 default-deny on postinstall scripts → declared `pnpm.onlyBuiltDependencies: ["esbuild"]` in `package.json`.
|
||||
2. Missing icons → user generated placeholder via PowerShell `System.Drawing` + `pnpm tauri icon`.
|
||||
3. `notify::Watcher` trait not in scope (E0599) and `tokio::JoinHandle` ≠ `tauri::async_runtime::JoinHandle` (E0308 ×2) in `watch.rs` — fixed in commit `ab75ca9`.
|
||||
4. `tauri-plugin-autostart` doesn't accept a `{"args":[…]}` block in `tauri.conf.json` — args go through the Rust `init()` call only. Removed the JSON entry in commit `8c25b01`.
|
||||
- **Pivoted from cap estimation to real `/usage` data.** First version showed 999% red ring because the placeholder caps (200k/2M) were wildly under what a Max user actually does. Tier-detection from `.claude.json` improved defaults but still wasn't right; user pointed out they'd previously had an app showing real subscription %. Investigated and found:
|
||||
- `/usage` slash command output isn't reachable via `claude --print` (LLM intercepts the literal string).
|
||||
- The data isn't cached anywhere on disk between invocations.
|
||||
- The OAuth credentials at `~/.claude/.credentials.json` work, but reusing them to call an undocumented endpoint felt fragile.
|
||||
- **Solution: PTY-drive `claude` itself.** New `src-tauri/src/cli_usage.rs` spawns claude via `portable-pty`, sends `/usage`, parses the three rendered bars (Current session / Current week all / Current week Sonnet). 5-min refresh + manual button. ~3-5s per fetch. (commit `db9a10a`)
|
||||
- **Bring-up gotchas while wiring the PTY drive on Windows:**
|
||||
1. Settings UI was non-interactive earlier because of two compounding bugs — a) Save was silently failing because the autostart plugin threw "OS error 2" in dev builds (target\debug exe path is unstable) and we let it abort the save; b) my PowerShell `mouse_event` clicks weren't reaching WebView2 (legacy API on a transparent borderless host), making me think the user's clicks were broken too. Real-mouse clicks worked once Save stopped getting blocked. Fix: best-effort autostart toggle (commit `9786437`).
|
||||
2. `paths::resolve_roots` was canonicalizing UNC paths to `\\?\UNC\…` form, which broke `Path::parent()` and made tier detection silently fail. Stopped canonicalizing (commit `c5c38d1`).
|
||||
3. Default `wsl.exe -- claude` invokes a non-interactive non-login shell with no PATH; resolved by defaulting to `wsl.exe -d Ubuntu bash -lc claude` on Windows when wsl.exe is detected (commit `7504990`). The `-d Ubuntu` matters because user's default WSL distro was `docker-desktop` (Alpine; no claude, no bash).
|
||||
4. Title bar buttons were inside `data-tauri-drag-region`; needed explicit `data-tauri-drag-region="false"` per button so clicks don't get interpreted as drag-start.
|
||||
- **Final UX polish (this session):** widget made resizable (220×240 min, 300×320 default), inline-SVG ring scales via viewBox, background opacity bumped to 93% so files behind don't bleed through, scrollbar bug from `border + width: 100vw` overflow killed via `box-sizing: border-box` reset + `body { overflow: hidden }` (commits `f90bb3b`, `c38d895`).
|
||||
- **Status:** widget is live on Windows showing real subscription percentages (72% session at end of session). 18 commits on `main`, all pushed. User is happy.
|
||||
- Toolchain (rust/node/pnpm) NOT installed in this WSL environment — that's expected; the build runs on the Windows host. `cargo check` / `pnpm install` not run from here.
|
||||
|
||||
## External references
|
||||
|
||||
- Forgejo repo: https://git.rdx4.com/megaproxy/claude-usage-widget
|
||||
- Approved plan: `/home/megaproxy/.claude/plans/snug-mapping-milner.md`
|
||||
- Tauri 2 prerequisites: https://v2.tauri.app/start/prerequisites/
|
||||
- Tauri 2 window customization: https://v2.tauri.app/learn/window-customization/
|
||||
- Tauri 2 autostart plugin: https://v2.tauri.app/plugin/autostart/
|
||||
- Tauri 2 capabilities: https://v2.tauri.app/security/capabilities/
|
||||
- ccusage (algorithm reference): https://github.com/ryoppippi/ccusage
|
||||
- Claude Max weekly reset issues (context for "rolling 7d" choice): https://github.com/anthropics/claude-code/issues/54974 · https://github.com/anthropics/claude-code/issues/52921
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "claude-usage-widget",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "claude-usage-widget"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
description = "Always-on-top Windows widget visualizing local Claude Code usage"
|
||||
authors = ["megaproxy"]
|
||||
edition = "2021"
|
||||
|
|
|
|||
|
|
@ -114,9 +114,8 @@ pub fn autodetect_command() -> Option<Vec<String>> {
|
|||
}
|
||||
|
||||
fn which_exists(name: &str) -> bool {
|
||||
use std::process::Command;
|
||||
let probe = if cfg!(windows) { "where" } else { "which" };
|
||||
Command::new(probe)
|
||||
crate::paths::quiet_command(probe)
|
||||
.arg(name)
|
||||
.output()
|
||||
.map(|o| o.status.success() && !o.stdout.is_empty())
|
||||
|
|
@ -124,8 +123,7 @@ fn which_exists(name: &str) -> bool {
|
|||
}
|
||||
|
||||
fn list_wsl_distros() -> Vec<String> {
|
||||
use std::process::Command;
|
||||
let Ok(out) = Command::new("wsl.exe").args(["-l", "-q"]).output() else {
|
||||
let Ok(out) = crate::paths::quiet_command("wsl.exe").args(["-l", "-q"]).output() else {
|
||||
return Vec::new();
|
||||
};
|
||||
if !out.status.success() {
|
||||
|
|
@ -145,8 +143,7 @@ fn list_wsl_distros() -> Vec<String> {
|
|||
}
|
||||
|
||||
fn probe_claude_in_wsl(distro: &str) -> bool {
|
||||
use std::process::Command;
|
||||
Command::new("wsl.exe")
|
||||
crate::paths::quiet_command("wsl.exe")
|
||||
.args(["-d", distro, "bash", "-lc", "command -v claude"])
|
||||
.output()
|
||||
.map(|o| o.status.success() && !o.stdout.is_empty())
|
||||
|
|
|
|||
|
|
@ -10,6 +10,23 @@
|
|||
use serde::Serialize;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Construct a `Command` that won't flash a console window on Windows.
|
||||
///
|
||||
/// `Command::new(...).output()` allocates a console for the child process
|
||||
/// before it exits, which appears as a black flash for short-lived processes
|
||||
/// like `wsl.exe -l -q` or `where claude`. The CREATE_NO_WINDOW flag (Win32
|
||||
/// process-creation flag 0x08000000) suppresses that.
|
||||
pub fn quiet_command(program: &str) -> std::process::Command {
|
||||
let mut c = std::process::Command::new(program);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
c.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ResolvedRoots {
|
||||
pub roots: Vec<PathBuf>,
|
||||
|
|
@ -24,7 +41,7 @@ pub fn list_wsl_distros() -> anyhow::Result<Vec<String>> {
|
|||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let out = match std::process::Command::new("wsl.exe")
|
||||
let out = match quiet_command("wsl.exe")
|
||||
.args(["-l", "-q"])
|
||||
.output()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Claude Usage Widget",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"identifier": "com.megaproxy.claude-usage-widget",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,7 +112,7 @@
|
|||
<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}
|
||||
|
|
@ -165,8 +193,25 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="field">
|
||||
<div class="label">theme</div>
|
||||
<div class="seg" role="radiogroup" aria-label="Theme">
|
||||
{#each THEMES as t (t.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="seg-item"
|
||||
class:selected={settings.theme === t.id}
|
||||
role="radio"
|
||||
aria-checked={settings.theme === t.id}
|
||||
onclick={() => pickTheme(t.id)}
|
||||
title={t.blurb}
|
||||
>{t.label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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}
|
||||
|
|
@ -179,8 +224,7 @@
|
|||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
z-index: 10;
|
||||
|
|
@ -199,6 +243,36 @@
|
|||
.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 (segmented control) ---- */
|
||||
.seg {
|
||||
display: flex;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--input-bg);
|
||||
}
|
||||
.seg-item {
|
||||
flex: 1 1 0;
|
||||
border: 0;
|
||||
border-right: 1px solid var(--border);
|
||||
border-radius: 0;
|
||||
padding: 6px 4px;
|
||||
background: transparent;
|
||||
color: var(--fg-dim);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, color 120ms ease;
|
||||
}
|
||||
.seg-item:last-child { border-right: 0; }
|
||||
.seg-item:hover { background: var(--hover); color: var(--fg); }
|
||||
.seg-item.selected {
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
font-weight: 600;
|
||||
}
|
||||
pre.raw {
|
||||
margin: 4px 0 0;
|
||||
padding: 6px;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
14
src/main.ts
14
src/main.ts
|
|
@ -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")! });
|
||||
|
|
|
|||
306
src/styles.css
306
src/styles.css
|
|
@ -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,29 @@ 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.
|
||||
z-index:-1 keeps it below all in-flow content WITHOUT having to
|
||||
promote children to a stacking context (which would clobber the
|
||||
Settings overlay's position: absolute). */
|
||||
#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: -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 +68,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 +86,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 +102,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 +112,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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue