- 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>
78 lines
3.4 KiB
TypeScript
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();
|
|
});
|