ee845e15b3
Add background themes and custom images Add shell command logging toggle Add UTF-8 locale guarantee for PTY Add Claude hook settings injection Add hotkey system for GUI Add glass panel styling Add search disabled state for agent panels Add zoom toggle command Add device report filtering Add entitlements for notarization Update version to 0.1.27
195 lines
8.6 KiB
TypeScript
195 lines
8.6 KiB
TypeScript
import type { SurfaceState } from "./layoutTypes";
|
|
|
|
/** Design tokens — mirror of DOCS/space-sh.pen variables. Single source for the UI. */
|
|
export const COLORS = {
|
|
accent: "var(--c-accent)",
|
|
bgApp: "var(--c-bg-app)",
|
|
bgElevated: "var(--c-bg-elevated)",
|
|
bgHover: "var(--c-bg-hover)",
|
|
bgPanel: "var(--c-bg-panel)",
|
|
panelGlass: "var(--c-panel-glass, var(--c-bg-panel))",
|
|
panelBlur: "var(--c-panel-blur, none)",
|
|
appBg: "var(--app-bg, var(--c-bg-app))",
|
|
elevatedGlass: "var(--c-elevated-glass, var(--c-bg-elevated))",
|
|
sidebarGlass: "var(--c-sidebar-glass, var(--c-bg-sidebar))",
|
|
bgSidebar: "var(--c-bg-sidebar)",
|
|
borderStrong: "var(--c-border-strong)",
|
|
borderSubtle: "var(--c-border-subtle)",
|
|
textPrimary: "var(--c-text-primary)",
|
|
textSecondary: "var(--c-text-secondary)",
|
|
textMuted: "var(--c-text-muted)",
|
|
stWork: "var(--c-st-work)",
|
|
stWait: "var(--c-st-wait)",
|
|
stDone: "var(--c-st-done)",
|
|
stError: "var(--c-st-error)",
|
|
stIdle: "var(--c-st-idle)",
|
|
searchMatch: "var(--c-search-match)",
|
|
} as const;
|
|
|
|
export const FONT = {
|
|
ui: "Inter, system-ui, sans-serif",
|
|
mono: "'JetBrains Mono Variable', 'JetBrains Mono', monospace",
|
|
} as const;
|
|
|
|
/** Status color by surface state, plus the stopped pseudo-state. */
|
|
export const STATE_COLOR: Record<SurfaceState | "stopped", string> = {
|
|
work: COLORS.stWork,
|
|
wait: COLORS.stWait,
|
|
done: COLORS.stDone,
|
|
error: COLORS.stError,
|
|
idle: COLORS.stIdle,
|
|
stopped: COLORS.stIdle,
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Palettes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type Palette = Record<string, string>;
|
|
|
|
/** Dark palette — hex values identical to what COLORS contained before. */
|
|
const DARK: Palette = {
|
|
"bg-app": "#0E1116",
|
|
"bg-elevated": "#1A2029",
|
|
"bg-hover": "#222A35",
|
|
"bg-panel": "#0A0D12",
|
|
"bg-sidebar": "#13171F",
|
|
"border-strong": "#323C49",
|
|
"border-subtle": "#232A33",
|
|
"text-primary": "#E6EDF3",
|
|
"text-secondary": "#8B97A6",
|
|
"text-muted": "#5A6573",
|
|
"st-work": "#4C8DFF",
|
|
"st-wait": "#F2B84B",
|
|
"st-done": "#3FB950",
|
|
"st-error": "#F4544E",
|
|
"st-idle": "#5A6573",
|
|
"search-match": "#5A4A1F",
|
|
};
|
|
|
|
/** Light palette — a readable counterpart for every token. */
|
|
const LIGHT: Palette = {
|
|
"bg-app": "#F5F7FA",
|
|
"bg-elevated": "#FFFFFF",
|
|
"bg-hover": "#E8EDF3",
|
|
"bg-panel": "#EBEEF3",
|
|
"bg-sidebar": "#DDE3EC",
|
|
"border-strong": "#B0BAC7",
|
|
"border-subtle": "#CDD4DE",
|
|
"text-primary": "#0D1117",
|
|
"text-secondary": "#4A5568",
|
|
"text-muted": "#8898AA",
|
|
"st-work": "#2266CC",
|
|
"st-wait": "#C07800",
|
|
"st-done": "#1A7A30",
|
|
"st-error": "#C4231F",
|
|
"st-idle": "#8898AA",
|
|
"search-match": "#FDE68A",
|
|
};
|
|
|
|
export const ACCENTS: Record<string, string> = {
|
|
blue: "#4C8DFF",
|
|
teal: "#34D3C2",
|
|
purple: "#9B7BFF",
|
|
green: "#3FB950",
|
|
orange: "#F2934B",
|
|
};
|
|
|
|
export type ThemeName = "dark" | "light";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Background themes (Warp-style full-window fills)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* A background theme paints the WHOLE window behind every panel, instead of each
|
|
* terminal owning an opaque tile. Panels become semi-transparent glass (rgba over
|
|
* a backdrop blur) so the shared fill shows through uniformly across the grid.
|
|
*
|
|
* `css` is the CSS `background` value for the app root: a gradient, or `""` for
|
|
* the classic solid look ("none"), or the sentinel "custom" handled at apply time
|
|
* via a user-supplied image. `panelAlpha`/`blur` tune the glass over the fill.
|
|
*/
|
|
export interface BackgroundTheme {
|
|
label: string;
|
|
css: string; // app-root `background` value ("" = solid bg-app)
|
|
swatch: string; // gallery preview (gradient/color)
|
|
panelAlpha: number; // 0..1 — panel glass opacity over the fill
|
|
blur: number; // px — panel backdrop blur
|
|
}
|
|
|
|
export const CUSTOM_BACKGROUND = "custom";
|
|
|
|
export const BACKGROUNDS: Record<string, BackgroundTheme> = {
|
|
none: { label: "None", css: "", swatch: DARK["bg-panel"], panelAlpha: 1, blur: 0 },
|
|
cyberwave: { label: "Cyber Wave", css: "linear-gradient(135deg,#06121f 0%,#0a2a3f 45%,#10183a 100%)", swatch: "linear-gradient(135deg,#06121f,#0a2a3f,#10183a)", panelAlpha: 0.46, blur: 9 },
|
|
phenomenon: { label: "Phenomenon", css: "radial-gradient(120% 120% at 80% 0%,#241a2e 0%,#15121d 45%,#0a0910 100%)", swatch: "radial-gradient(120% 120% at 80% 0%,#241a2e,#15121d,#0a0910)", panelAlpha: 0.5, blur: 8 },
|
|
dracula: { label: "Dracula", css: "linear-gradient(160deg,#282a36 0%,#21222c 60%,#1a1b23 100%)", swatch: "linear-gradient(160deg,#282a36,#21222c,#1a1b23)", panelAlpha: 0.58, blur: 6 },
|
|
aurora: { label: "Aurora", css: "linear-gradient(135deg,#0b3d2e 0%,#0a2c3a 40%,#241147 100%)", swatch: "linear-gradient(135deg,#0b3d2e,#0a2c3a,#241147)", panelAlpha: 0.44, blur: 10 },
|
|
ember: { label: "Ember", css: "linear-gradient(135deg,#2a0f12 0%,#3a1410 45%,#160a14 100%)", swatch: "linear-gradient(135deg,#2a0f12,#3a1410,#160a14)", panelAlpha: 0.5, blur: 8 },
|
|
referred: { label: "Referred", css: "linear-gradient(120deg,#b9c6ff 0%,#cdb6ff 40%,#ffd6e7 100%)", swatch: "linear-gradient(120deg,#b9c6ff,#cdb6ff,#ffd6e7)", panelAlpha: 0.34, blur: 12 },
|
|
};
|
|
|
|
/** Resolve a background name to its theme, falling back to "none". */
|
|
export function backgroundFor(name: string): BackgroundTheme {
|
|
return BACKGROUNDS[name] ?? BACKGROUNDS.none;
|
|
}
|
|
|
|
/** Hex (#rrggbb) → rgba() string at the given alpha. */
|
|
function hexToRgba(hex: string, alpha: number): string {
|
|
const h = hex.replace("#", "");
|
|
const r = parseInt(h.slice(0, 2), 16);
|
|
const g = parseInt(h.slice(2, 4), 16);
|
|
const b = parseInt(h.slice(4, 6), 16);
|
|
return `rgba(${r},${g},${b},${alpha})`;
|
|
}
|
|
|
|
/**
|
|
* Real color values for consumers that can't use var() (xterm). Keys are the
|
|
* kebab tokens plus "accent" and "term-bg". When a background theme is active the
|
|
* terminal renders on transparent glass, so "term-bg" is fully transparent and
|
|
* the panel container supplies the visible tint.
|
|
*/
|
|
export function resolvePalette(theme: ThemeName, accent: string, background: string = "none"): Record<string, string> {
|
|
const base = theme === "light" ? LIGHT : DARK;
|
|
const active = background !== "none";
|
|
return {
|
|
...base,
|
|
accent: ACCENTS[accent] ?? ACCENTS.blue,
|
|
"term-bg": active ? "rgba(0,0,0,0)" : base["bg-panel"],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Write the active palette + background fill to :root as --c-* custom properties.
|
|
* `imageDataUrl` is only consulted when `background === "custom"`.
|
|
*/
|
|
export function applyTheme(theme: ThemeName, accent: string, background: string = "none", imageDataUrl: string | null = null): void {
|
|
const p = resolvePalette(theme, accent, background);
|
|
const root = document.documentElement.style;
|
|
for (const [k, v] of Object.entries(p)) root.setProperty(`--c-${k}`, v);
|
|
|
|
const bg = backgroundFor(background);
|
|
const base = theme === "light" ? LIGHT : DARK;
|
|
const active = background !== "none";
|
|
|
|
// App-root fill: custom image (cover) > gradient > solid bg-app.
|
|
const appBg = background === CUSTOM_BACKGROUND && imageDataUrl
|
|
? `center / cover no-repeat url("${imageDataUrl}")`
|
|
: bg.css || base["bg-app"];
|
|
root.setProperty("--app-bg", appBg);
|
|
|
|
// Panel glass: rgba(bg-panel, alpha) over the fill, plus optional backdrop blur.
|
|
// When inactive this is the solid bg-panel so the classic look is byte-identical.
|
|
const alpha = active ? (background === CUSTOM_BACKGROUND ? 0.5 : bg.panelAlpha) : 1;
|
|
const blur = active ? (background === CUSTOM_BACKGROUND ? 8 : bg.blur) : 0;
|
|
root.setProperty("--c-panel-glass", alpha < 1 ? hexToRgba(base["bg-panel"], alpha) : base["bg-panel"]);
|
|
root.setProperty("--c-panel-blur", blur > 0 ? `blur(${blur}px)` : "none");
|
|
|
|
// Chrome glass (TopBar / toolbar / sidebar / panel headers) — a touch more
|
|
// opaque than the panels so labels and controls stay legible over the fill.
|
|
const chromeAlpha = active ? Math.min(alpha + 0.22, 0.92) : 1;
|
|
root.setProperty("--c-elevated-glass", chromeAlpha < 1 ? hexToRgba(base["bg-elevated"], chromeAlpha) : base["bg-elevated"]);
|
|
root.setProperty("--c-sidebar-glass", chromeAlpha < 1 ? hexToRgba(base["bg-sidebar"], chromeAlpha) : base["bg-sidebar"]);
|
|
}
|