Files
codecraft/web/src/theme.ts
T
Richard ZinkandClaude Opus 5.5 5b569b6835 Add security hardening, Ghidra, recreate skill, checkpoints, shells, web search and themes
- Sandbox (bubblewrap) for non-admin shell/git, MCP per-user rights, hard budgets and quotas,
  login rate limiting, audit log and backups
- Ghidra headless decompilation (ARM64 natives built from source), binary_info tool,
  clean room / deep workflow per session
- Recreate skill and commands for analysing and re-building old software
- File checkpoints with rewind/restore, background shells (bash_output, kill_shell)
- web_search tool (DuckDuckGo, SearXNG, Brave, Tavily)
- In-app dialogs and toasts instead of browser dialogs
- 15 base themes (trilium-themes palettes plus Nord, VS Code, GitHub, Catppuccin, One,
  Dracula, Rose Pine, Everforest, Kanagawa) with light/dark/auto switcher and license notes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 19:12:30 +01:00

78 lines
3.4 KiB
TypeScript

import "./themes";
const KEY = "codecraft.theme";
const MODE_KEY = "codecraft.themeMode";
/**
* Base themes. Each has a dark and a light variant (CSS files
* `themes/<id>-dark.css` / `<id>-light.css`); the mode picks the variant.
*/
export const THEMES = [
{ id: "gruvbox", name: "Gruvbox", source: "morhetz/gruvbox", license: "MIT" },
{ id: "ash", name: "Ash", source: "zink/trilium-themes", license: "MIT" },
{ id: "harbor", name: "Harbor", source: "HANCORE-linux/omarchy-harbor-theme", license: "MIT" },
{ id: "coffee", name: "Coffee", source: "megabyte0x/omarchy-coffee-theme", license: "MIT" },
{ id: "solarized", name: "Solarized", source: "Ethan Schoonover", license: "MIT" },
{ id: "tokyonight", name: "Tokyo Night", source: "folke/tokyonight.nvim", license: "Apache-2.0" },
{ id: "nord", name: "Nord", source: "nordtheme/nord", license: "MIT" },
{ id: "vscode", name: "VS Code", source: "microsoft/vscode", license: "MIT" },
{ id: "github", name: "GitHub", source: "primer/github-vscode-theme", license: "MIT" },
{ id: "catppuccin", name: "Catppuccin", source: "catppuccin/catppuccin", license: "MIT" },
{ id: "one", name: "One Dark / Light", source: "atom/atom", license: "MIT" },
{ id: "dracula", name: "Dracula / Alucard", source: "dracula/dracula-theme", license: "MIT" },
{ id: "rosepine", name: "Rosé Pine", source: "rose-pine/rose-pine-theme", license: "MIT" },
{ id: "everforest", name: "Everforest", source: "sainnhe/everforest", license: "MIT" },
{ id: "kanagawa", name: "Kanagawa", source: "rebelot/kanagawa.nvim", license: "MIT" },
] as const;
export type Theme = (typeof THEMES)[number]["id"];
export type ThemeMode = "auto" | "light" | "dark";
export const THEME_MODES: ThemeMode[] = ["auto", "light", "dark"];
export function getTheme(): Theme {
const saved = localStorage.getItem(KEY) ?? "";
// Migrate old values like "gruvbox-dark", "dark", "system".
const base = saved.replace(/-(dark|light)$/, "");
return THEMES.some((t) => t.id === base) ? (base as Theme) : "gruvbox";
}
export function getThemeMode(): ThemeMode {
const saved = localStorage.getItem(MODE_KEY);
if (saved === "light" || saved === "dark" || saved === "auto") return saved;
// Migrate the old combined setting.
const old = localStorage.getItem(KEY) ?? "";
if (old.endsWith("dark")) return "dark";
if (old.endsWith("light")) return "light";
return "auto";
}
const media = matchMedia("(prefers-color-scheme: dark)");
const listeners = new Set<() => void>();
export function onThemeChange(fn: () => void) {
listeners.add(fn);
return () => void listeners.delete(fn);
}
export function applyTheme(theme: Theme = getTheme(), mode: ThemeMode = getThemeMode()) {
localStorage.setItem(KEY, theme);
localStorage.setItem(MODE_KEY, mode);
const dark = mode === "auto" ? media.matches : mode === "dark";
const root = document.documentElement;
root.dataset.theme = `${theme}-${dark ? "dark" : "light"}`;
root.dataset.mode = dark ? "dark" : "light";
const color = getComputedStyle(root).getPropertyValue("--theme-color").trim();
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", color || "#282828");
listeners.forEach((fn) => fn());
}
/** Cycle auto → light → dark (quick switcher). */
export function cycleThemeMode() {
const modes = THEME_MODES;
applyTheme(getTheme(), modes[(modes.indexOf(getThemeMode()) + 1) % modes.length]);
}
media.addEventListener("change", () => {
if (getThemeMode() === "auto") applyTheme();
});