Treat the appearance setting as the rendered base size so the type ramp scales predictably. Native starts one pixel larger while web and desktop preserve the authored 14px base, with legacy values migrated on load.
795 lines
22 KiB
TypeScript
795 lines
22 KiB
TypeScript
import { Platform } from "react-native";
|
||
import { darkHighlightColors, lightHighlightColors } from "@getpaseo/highlight";
|
||
|
||
export const baseColors = {
|
||
// Base colors
|
||
white: "#ffffff",
|
||
black: "#000000",
|
||
|
||
// Zinc scale (primary gray palette)
|
||
zinc: {
|
||
50: "#fafafa",
|
||
100: "#f4f4f5",
|
||
200: "#e4e4e7",
|
||
300: "#d4d4d8",
|
||
400: "#a1a1aa",
|
||
500: "#71717a",
|
||
600: "#52525b",
|
||
700: "#3f3f46",
|
||
800: "#27272a",
|
||
850: "#1a1a1d",
|
||
900: "#18181b",
|
||
950: "#121214",
|
||
},
|
||
|
||
// Gray scale
|
||
gray: {
|
||
50: "#f9fafb",
|
||
100: "#f3f4f6",
|
||
200: "#e5e7eb",
|
||
300: "#d1d5db",
|
||
400: "#9ca3af",
|
||
500: "#6b7280",
|
||
600: "#4b5563",
|
||
700: "#374151",
|
||
800: "#1f2937",
|
||
900: "#111827",
|
||
},
|
||
|
||
// Slate scale
|
||
slate: {
|
||
200: "#e2e8f0",
|
||
},
|
||
|
||
// Blue scale
|
||
blue: {
|
||
50: "#eff6ff",
|
||
100: "#dbeafe",
|
||
200: "#bfdbfe",
|
||
300: "#93c5fd",
|
||
400: "#60a5fa",
|
||
500: "#3b82f6",
|
||
600: "#2563eb",
|
||
700: "#1d4ed8",
|
||
800: "#1e40af",
|
||
900: "#1e3a8a",
|
||
950: "#172554",
|
||
},
|
||
|
||
// Green scale
|
||
green: {
|
||
100: "#dcfce7",
|
||
200: "#bbf7d0",
|
||
400: "#4ade80",
|
||
500: "#22c55e",
|
||
600: "#16a34a",
|
||
800: "#166534",
|
||
900: "#14532d",
|
||
},
|
||
|
||
// Red scale
|
||
red: {
|
||
100: "#fee2e2",
|
||
200: "#fecaca",
|
||
300: "#fca5a5",
|
||
500: "#ef4444",
|
||
600: "#dc2626",
|
||
800: "#991b1b",
|
||
900: "#7f1d1d",
|
||
},
|
||
|
||
// Teal scale
|
||
teal: {
|
||
200: "#99f6e4",
|
||
},
|
||
|
||
// Amber scale
|
||
amber: {
|
||
500: "#f59e0b",
|
||
700: "#b45309",
|
||
},
|
||
|
||
// Yellow scale
|
||
yellow: {
|
||
400: "#fbbf24",
|
||
},
|
||
|
||
// Purple scale
|
||
purple: {
|
||
500: "#a855f7",
|
||
600: "#9333ea",
|
||
},
|
||
|
||
// Orange scale
|
||
orange: {
|
||
500: "#f97316",
|
||
600: "#ea580c",
|
||
},
|
||
} as const;
|
||
|
||
// Diff colors — the +/- inside a diff view, where the color *is* the signal and has to
|
||
// survive being scanned line by line, so it stays saturated. Light uses muted tones, dark
|
||
// uses the brighter palette values.
|
||
//
|
||
// A diff *stat* — the "+12 −3" footnote next to a title — is not this. It is a status
|
||
// signal, so it uses statusSuccess/statusDanger below rather than a tier of its own.
|
||
const lightDiffColors = {
|
||
diffAddition: "#15803d", // green-700 — readable on white without screaming
|
||
diffDeletion: "#b91c1c", // red-700
|
||
};
|
||
|
||
const darkDiffColors = {
|
||
diffAddition: "#4ade80", // green-400
|
||
diffDeletion: "#ef4444", // red-500
|
||
};
|
||
|
||
// Status colors — semantic signals for success/danger/warning/merged. There is exactly one
|
||
// token per signal, and every status surface uses it: PR state icons, CI check icons and
|
||
// pies, diff stats, file-change icons, status badges, usage bars. Status *dots* are the
|
||
// exception and have their own band below. A surface does not otherwise get a quieter or
|
||
// louder variant of a status color because of where it sits — if a dense list feels loud,
|
||
// that is a density or weight problem, not a color problem.
|
||
//
|
||
// The level is set by the densest consumer, the sidebar workspace list: quiet enough that a
|
||
// column of green checks reads as one line of subtitle and the single red row still stands
|
||
// out, saturated enough to name the state on its own. Every other surface follows it.
|
||
//
|
||
// Normalized, not hand-picked. Every color below shares one lightness and one chroma; only
|
||
// the hue changes, and each hue is the one that family already had. Chroma is a fixed
|
||
// fraction of what sRGB allows at that lightness and hue, because the gamut is lopsided —
|
||
// amber runs out of room long before red does, so a literal equal-chroma set leaves amber
|
||
// flat and red screaming. Equal fractions is what makes four hues read as one family.
|
||
// Regenerate with the same rule rather than nudging one value.
|
||
//
|
||
// Hues are fixed per family across both themes: success 150, danger 27, warning 70.5,
|
||
// merged 300.
|
||
const lightStatusColors = {
|
||
// L=0.50, chroma 60% of gamut max
|
||
statusSuccess: "#3e704a",
|
||
statusDanger: "#9d433b",
|
||
statusWarning: "#7b5d39",
|
||
statusMerged: "#7347af",
|
||
};
|
||
|
||
const darkStatusColors = {
|
||
// L=0.70, chroma 55% of gamut max
|
||
statusSuccess: "#6cb17b",
|
||
statusDanger: "#d8847b",
|
||
statusWarning: "#c09664",
|
||
statusMerged: "#a890d5",
|
||
};
|
||
|
||
// Status *dot* colors — the small filled discs on a sidebar row, and the glyphs that stand in
|
||
// for them. Same four hues and the same generation rule as the status colors above, but its
|
||
// own band, because a dot is doing a different job than a check icon or a host badge.
|
||
//
|
||
// A dot is 6pt of solid color with no shape to read and no label attached. At the status
|
||
// band's lightness the dots read dimmer than the static text and icons beside them on the same
|
||
// row, which is backwards — the dot is the row's state. The loudness comes from chroma: 90% of
|
||
// gamut max against the status family's 55-60%.
|
||
//
|
||
// Lightness is set by hue separation, not by distance from the surface. A dark dot on a light
|
||
// surface has plenty of contrast but the four hues collapse into each other at 6pt — dark green,
|
||
// dark red, dark amber and dark blue all read as "dark blob", and the point of the dot is telling
|
||
// them apart at a glance. So the light band runs as bright as the contrast floor allows: L=0.62
|
||
// is the last step where all four clear 3:1 against the sidebar's surface2 (success is the
|
||
// binding one at 3.10, and drops under 3 by L=0.64), which is WCAG's non-text minimum for a
|
||
// control that carries state.
|
||
//
|
||
// All four move together. A dot matching its siblings in lightness and chroma says only which
|
||
// state the row is in; one that does not says "this row matters more", which is a claim the
|
||
// color has no business making. Regenerate the set, never one hue.
|
||
//
|
||
// 90% and not 100%: at the gamut edge the lopsidedness is worst — green reaches C=0.215 while
|
||
// blue manages 0.116 — so the set stops reading as one family and green wins. Red running out
|
||
// of chroma as lightness climbs is what caps the dark band at L=0.72; higher turns the failed
|
||
// dot pink, and the light band pastels out the same way just above its own cap. Running is blue
|
||
// at hue 250, clear of
|
||
// identity-colors' blue at 256.6 so a blue host badge and a working dot on the same row do not
|
||
// read as related.
|
||
const lightStatusDotColors = {
|
||
// L=0.62, chroma 90% of gamut max
|
||
statusDotSuccess: "#299f51",
|
||
statusDotDanger: "#f12e2f",
|
||
statusDotWarning: "#b37824",
|
||
statusDotRunning: "#268ae0",
|
||
};
|
||
|
||
const darkStatusDotColors = {
|
||
// L=0.72, chroma 90% of gamut max
|
||
statusDotSuccess: "#35c264",
|
||
statusDotDanger: "#f7796d",
|
||
statusDotWarning: "#db932e",
|
||
statusDotRunning: "#5caaf6",
|
||
};
|
||
|
||
// Semantic color tokens - Layer-based system
|
||
const lightSurfaces = {
|
||
// Surfaces (layers) - shifted one step lighter
|
||
surface0: "#ffffff", // App background
|
||
surface1: "#fafafa", // Subtle hover (was zinc-100, now zinc-50)
|
||
surface2: "#f4f4f5", // Elevated: badges, inputs, sheets (was zinc-200, now zinc-100)
|
||
surface3: "#e4e4e7", // Highest elevation (was zinc-300, now zinc-200)
|
||
surface4: "#d4d4d8", // Extra emphasis (was zinc-400, now zinc-300)
|
||
} as const;
|
||
|
||
const lightSemanticColors = {
|
||
...lightSurfaces,
|
||
surfaceDiffEmpty: "#f6f6f6", // Empty side of split diff rows, between surface1 and surface2 and biased toward surface2
|
||
surfaceSidebar: lightSurfaces.surface2, // Sidebar background (darker than main)
|
||
surfaceSidebarHover: lightSurfaces.surface1,
|
||
surfaceSidebarSelected: lightSurfaces.surface2,
|
||
surfaceWorkspace: lightSurfaces.surface0, // Workspace main background
|
||
|
||
// Text
|
||
foreground: "#1a1a1e",
|
||
foregroundMuted: "#71717a",
|
||
foregroundExtraMuted: "#a1a1aa",
|
||
|
||
// Borders - shifted one step lighter
|
||
border: "#e4e4e7", // (was zinc-200, now zinc-200 - keep for contrast)
|
||
borderAccent: "#ececf1", // Softer accent border for low-emphasis outlines
|
||
|
||
// Brand
|
||
accent: "#20744A",
|
||
accentBright: "#239956",
|
||
accentForeground: "#ffffff",
|
||
|
||
// Semantic
|
||
destructive: "#b04138", // dark warm red on white — calm but unambiguously red
|
||
destructiveForeground: "#ffffff",
|
||
success: "#20744A",
|
||
successForeground: "#ffffff",
|
||
|
||
// Legacy aliases (for gradual migration)
|
||
background: "#ffffff",
|
||
popover: "#ffffff",
|
||
popoverForeground: "#1a1a1e",
|
||
primary: "#18181b",
|
||
primaryForeground: "#fafafa",
|
||
secondary: "#f4f4f5",
|
||
secondaryForeground: "#1a1a1e",
|
||
muted: "#f4f4f5",
|
||
mutedForeground: "#71717a",
|
||
accentBorder: "#ececf1",
|
||
input: "#f4f4f5",
|
||
ring: "#18181b",
|
||
|
||
...lightDiffColors,
|
||
...lightStatusColors,
|
||
...lightStatusDotColors,
|
||
|
||
terminal: {
|
||
background: "#ffffff",
|
||
foreground: "#1a1a1e",
|
||
cursor: "#1a1a1e",
|
||
cursorAccent: "#ffffff",
|
||
selectionBackground: "rgba(0, 0, 0, 0.15)",
|
||
selectionForeground: "#1a1a1e",
|
||
|
||
black: "#1a1a1e",
|
||
red: "#dc2626",
|
||
green: "#16a34a",
|
||
yellow: "#ca8a04",
|
||
blue: "#2563eb",
|
||
magenta: "#9333ea",
|
||
cyan: "#0891b2",
|
||
white: "#ffffff",
|
||
|
||
brightBlack: "#3f3f46",
|
||
brightRed: "#ef4444",
|
||
brightGreen: "#22c55e",
|
||
brightYellow: "#f59e0b",
|
||
brightBlue: "#3b82f6",
|
||
brightMagenta: "#a855f7",
|
||
brightCyan: "#06b6d4",
|
||
brightWhite: "#fafafa",
|
||
},
|
||
} as const;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Dark theme variant builder
|
||
// ---------------------------------------------------------------------------
|
||
|
||
interface DarkThemeConfig {
|
||
surface0: string;
|
||
surface1: string;
|
||
surface2: string;
|
||
surface3: string;
|
||
surface4: string;
|
||
surfaceDiffEmpty: string;
|
||
surfaceSidebar: string;
|
||
foregroundMuted: string;
|
||
foregroundExtraMuted: string;
|
||
border: string;
|
||
borderAccent: string;
|
||
accent: string;
|
||
accentBright: string;
|
||
accentForeground?: string;
|
||
destructive: string;
|
||
terminalBlack: string;
|
||
terminalBrightBlack: string;
|
||
}
|
||
|
||
const darkTerminalAnsi = {
|
||
red: "#e07070",
|
||
green: "#5dba80",
|
||
yellow: "#d4a44a",
|
||
blue: "#6a9de0",
|
||
magenta: "#b07ad0",
|
||
cyan: "#4aabb8",
|
||
white: "#d4d4d8",
|
||
brightRed: "#e89090",
|
||
brightGreen: "#7ecf9a",
|
||
brightYellow: "#e0be6e",
|
||
brightBlue: "#8ab4e8",
|
||
brightMagenta: "#c49ae0",
|
||
brightCyan: "#6ec2cc",
|
||
brightWhite: "#f0f0f2",
|
||
} as const;
|
||
|
||
function buildDarkSemanticColors(tint: DarkThemeConfig) {
|
||
return {
|
||
surface0: tint.surface0,
|
||
surface1: tint.surface1,
|
||
surface2: tint.surface2,
|
||
surface3: tint.surface3,
|
||
surface4: tint.surface4,
|
||
surfaceDiffEmpty: tint.surfaceDiffEmpty,
|
||
surfaceSidebar: tint.surfaceSidebar,
|
||
surfaceSidebarHover: tint.surface1,
|
||
surfaceSidebarSelected: tint.surface2,
|
||
surfaceWorkspace: tint.surface1,
|
||
|
||
foreground: "#fafafa",
|
||
foregroundMuted: tint.foregroundMuted,
|
||
foregroundExtraMuted: tint.foregroundExtraMuted,
|
||
|
||
border: tint.border,
|
||
borderAccent: tint.borderAccent,
|
||
|
||
accent: tint.accent,
|
||
accentBright: tint.accentBright,
|
||
accentForeground: tint.accentForeground ?? "#ffffff",
|
||
|
||
destructive: tint.destructive,
|
||
destructiveForeground: "#ffffff",
|
||
success: tint.accent,
|
||
successForeground: "#ffffff",
|
||
|
||
// Legacy aliases (for gradual migration)
|
||
background: tint.surface0,
|
||
popover: tint.surface2,
|
||
popoverForeground: "#fafafa",
|
||
primary: "#fafafa",
|
||
primaryForeground: tint.surface0,
|
||
secondary: tint.surface2,
|
||
secondaryForeground: "#fafafa",
|
||
muted: tint.surface2,
|
||
mutedForeground: tint.foregroundMuted,
|
||
accentBorder: tint.borderAccent,
|
||
input: tint.surface2,
|
||
ring: "#d4d4d8",
|
||
|
||
...darkDiffColors,
|
||
...darkStatusColors,
|
||
...darkStatusDotColors,
|
||
|
||
terminal: {
|
||
background: tint.surface0,
|
||
foreground: "#fafafa",
|
||
cursor: "#fafafa",
|
||
cursorAccent: tint.surface0,
|
||
selectionBackground: "rgba(255, 255, 255, 0.2)",
|
||
selectionForeground: "#fafafa",
|
||
black: tint.terminalBlack,
|
||
...darkTerminalAnsi,
|
||
brightBlack: tint.terminalBrightBlack,
|
||
},
|
||
};
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Dark tint definitions
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Paseo — subtle teal-green tint (default)
|
||
const paseoDarkColors = buildDarkSemanticColors({
|
||
surface0: "#181B1A",
|
||
surface1: "#1E2120",
|
||
surface2: "#272A29",
|
||
surface3: "#434645",
|
||
surface4: "#595B5B",
|
||
surfaceDiffEmpty: "#252827",
|
||
surfaceSidebar: "#141716",
|
||
foregroundMuted: "#A1A5A4",
|
||
foregroundExtraMuted: "#717574",
|
||
border: "#252B2A",
|
||
borderAccent: "#2F3534",
|
||
accent: "#20744A",
|
||
accentBright: "#7ccba0",
|
||
destructive: "#c64f43", // warm red, hue ~7 — reads as red (not pink) against the green tint
|
||
terminalBlack: "#141716",
|
||
terminalBrightBlack: "#434645",
|
||
});
|
||
|
||
// Zinc — neutral gray, no tint
|
||
const zincDarkColors = buildDarkSemanticColors({
|
||
surface0: "#18181b",
|
||
surface1: "#1f1f22",
|
||
surface2: "#27272a",
|
||
surface3: "#3f3f46",
|
||
surface4: "#52525b",
|
||
surfaceDiffEmpty: "#242427",
|
||
surfaceSidebar: "#131316",
|
||
foregroundMuted: "#a1a1aa",
|
||
foregroundExtraMuted: "#71717a",
|
||
border: "#27272a",
|
||
borderAccent: "#303036",
|
||
accent: "#e4e4e7",
|
||
accentBright: "#fafafa",
|
||
accentForeground: "#18181b", // monochrome zinc accent is near-white — needs dark text
|
||
destructive: "#c44a4a", // neutral red, hue 0 — clearly red without screaming
|
||
terminalBlack: "#131316",
|
||
terminalBrightBlack: "#3f3f46",
|
||
});
|
||
|
||
// Midnight — subtle blue tint
|
||
const midnightDarkColors = buildDarkSemanticColors({
|
||
surface0: "#161820",
|
||
surface1: "#1c1e27",
|
||
surface2: "#252731",
|
||
surface3: "#3c3e4c",
|
||
surface4: "#535564",
|
||
surfaceDiffEmpty: "#222430",
|
||
surfaceSidebar: "#121420",
|
||
foregroundMuted: "#9a9db0",
|
||
foregroundExtraMuted: "#6b6e82",
|
||
border: "#242636",
|
||
borderAccent: "#2e3040",
|
||
accent: "#3b6fcf",
|
||
accentBright: "#7eaaeb",
|
||
destructive: "#c44a52", // red with a hint of cool lean against the blue tint
|
||
terminalBlack: "#121420",
|
||
terminalBrightBlack: "#3c3e4c",
|
||
});
|
||
|
||
// Claude — warm neutral with subtle orange undertone
|
||
const claudeDarkColors = buildDarkSemanticColors({
|
||
surface0: "#1f1f1e",
|
||
surface1: "#262523",
|
||
surface2: "#2f2d2b",
|
||
surface3: "#4a4745",
|
||
surface4: "#605d5b",
|
||
surfaceDiffEmpty: "#2a2826",
|
||
surfaceSidebar: "#1a1918",
|
||
foregroundMuted: "#ada9a5",
|
||
foregroundExtraMuted: "#78746f",
|
||
border: "#2c2a27",
|
||
borderAccent: "#36332f",
|
||
accent: "#d97757",
|
||
accentBright: "#e89a7f",
|
||
destructive: "#cf513e", // warm orange-red, hue ~10 — sits with the Claude orange accent
|
||
terminalBlack: "#1a1918",
|
||
terminalBrightBlack: "#4a4745",
|
||
});
|
||
|
||
// Ghostty — blue-tinted dark based on Ghostty default background
|
||
const ghosttyDarkColors = buildDarkSemanticColors({
|
||
surface0: "#282c34",
|
||
surface1: "#2f333d",
|
||
surface2: "#383c48",
|
||
surface3: "#4a4f5e",
|
||
surface4: "#5b6175",
|
||
surfaceDiffEmpty: "#323643",
|
||
surfaceSidebar: "#21252d",
|
||
foregroundMuted: "#c8ccd8",
|
||
foregroundExtraMuted: "#a0a4b2",
|
||
border: "#353a47",
|
||
borderAccent: "#3f4454",
|
||
accent: "#89b4fa",
|
||
accentBright: "#b4d0fc",
|
||
destructive: "#c44a55", // red with slight cool lean against the slate-blue surfaces
|
||
terminalBlack: "#21252d",
|
||
terminalBrightBlack: "#4a4f5e",
|
||
});
|
||
|
||
export const SPACING = {
|
||
0: 0,
|
||
0.5: 2,
|
||
1: 4,
|
||
1.5: 6,
|
||
2: 8,
|
||
3: 12,
|
||
4: 16,
|
||
6: 24,
|
||
8: 32,
|
||
12: 48,
|
||
16: 64,
|
||
20: 80,
|
||
24: 96,
|
||
32: 128,
|
||
} as const;
|
||
|
||
export const FONT_SIZE = {
|
||
code: 12,
|
||
sm: 12,
|
||
base: 14,
|
||
lg: 16,
|
||
xl: 18,
|
||
"2xl": 20,
|
||
"3xl": 22,
|
||
"4xl": 26,
|
||
} as const;
|
||
|
||
export const LINE_HEIGHT = {
|
||
diff: 22,
|
||
} as const;
|
||
|
||
export const ICON_SIZE = {
|
||
xs: 12,
|
||
sm: 14,
|
||
md: 16,
|
||
lg: 20,
|
||
} as const;
|
||
|
||
export const FONT_WEIGHT = {
|
||
normal: "normal" as const,
|
||
medium: "500" as const,
|
||
semibold: "600" as const,
|
||
bold: "bold" as const,
|
||
} as const;
|
||
|
||
export const BORDER_RADIUS = {
|
||
none: 0,
|
||
sm: 2,
|
||
base: 4,
|
||
md: 6,
|
||
lg: 8,
|
||
xl: 12,
|
||
"2xl": 16,
|
||
full: 9999,
|
||
} as const;
|
||
|
||
export const BORDER_WIDTH = {
|
||
0: 0,
|
||
1: 1,
|
||
2: 2,
|
||
} as const;
|
||
|
||
export const OPACITY = {
|
||
0: 0,
|
||
50: 0.5,
|
||
100: 1,
|
||
} as const;
|
||
|
||
// Platform default font stacks — copied verbatim from constants/theme.ts `Fonts`
|
||
// (sans -> ui, mono -> mono). These seed the dynamic `fontFamily` theme token and
|
||
// are the fallback an empty user-supplied family resolves to at apply time.
|
||
export const DEFAULT_UI_FONT_STACK: string = Platform.select({
|
||
ios: "system-ui",
|
||
default: "normal",
|
||
web: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
||
});
|
||
|
||
export const DEFAULT_MONO_FONT_STACK: string = Platform.select({
|
||
ios: "ui-monospace",
|
||
default: "monospace",
|
||
web: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||
});
|
||
|
||
// `fontSize`, `fontFamily`, and `lineHeight` are deliberately widened to plain
|
||
// `number`/`string` (not narrowed by `as const`) so the appearance updater can patch
|
||
// them at runtime via `UnistylesRuntime.updateTheme`. The remaining tokens keep their
|
||
// literal types.
|
||
interface CommonTheme {
|
||
spacing: typeof SPACING;
|
||
fontSize: Record<keyof typeof FONT_SIZE, number>;
|
||
fontFamily: { ui: string; mono: string };
|
||
lineHeight: Record<keyof typeof LINE_HEIGHT, number>;
|
||
iconSize: typeof ICON_SIZE;
|
||
fontWeight: typeof FONT_WEIGHT;
|
||
borderRadius: typeof BORDER_RADIUS;
|
||
borderWidth: typeof BORDER_WIDTH;
|
||
opacity: typeof OPACITY;
|
||
}
|
||
|
||
const commonTheme: CommonTheme = {
|
||
spacing: SPACING,
|
||
fontSize: FONT_SIZE,
|
||
fontFamily: { ui: DEFAULT_UI_FONT_STACK, mono: DEFAULT_MONO_FONT_STACK },
|
||
lineHeight: LINE_HEIGHT,
|
||
iconSize: ICON_SIZE,
|
||
fontWeight: FONT_WEIGHT,
|
||
borderRadius: BORDER_RADIUS,
|
||
borderWidth: BORDER_WIDTH,
|
||
opacity: OPACITY,
|
||
};
|
||
|
||
const darkShadow = {
|
||
sm: {
|
||
shadowColor: "rgba(0, 0, 0, 0.25)",
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowRadius: 4,
|
||
elevation: 2,
|
||
},
|
||
md: {
|
||
shadowColor: "rgba(0, 0, 0, 0.20)",
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowRadius: 8,
|
||
elevation: 8,
|
||
},
|
||
lg: {
|
||
shadowColor: "rgba(0, 0, 0, 0.40)",
|
||
shadowOffset: { width: 0, height: 12 },
|
||
shadowRadius: 24,
|
||
elevation: 8,
|
||
},
|
||
} as const;
|
||
|
||
function buildDarkTheme(semanticColors: ReturnType<typeof buildDarkSemanticColors>) {
|
||
return {
|
||
colorScheme: "dark" as const,
|
||
colors: {
|
||
...semanticColors,
|
||
palette: baseColors,
|
||
syntax: darkHighlightColors,
|
||
},
|
||
shadow: darkShadow,
|
||
...commonTheme,
|
||
} as const;
|
||
}
|
||
|
||
export const darkTheme = buildDarkTheme(paseoDarkColors);
|
||
export const darkZincTheme = buildDarkTheme(zincDarkColors);
|
||
export const darkMidnightTheme = buildDarkTheme(midnightDarkColors);
|
||
export const darkClaudeTheme = buildDarkTheme(claudeDarkColors);
|
||
export const darkGhosttyTheme = buildDarkTheme(ghosttyDarkColors);
|
||
|
||
// Pure black — zero-luminance background with high-contrast surfaces.
|
||
const pureBlackDarkColors = buildDarkSemanticColors({
|
||
surface0: "#000000",
|
||
surface1: "#0a0a0a",
|
||
surface2: "#111111",
|
||
surface3: "#202020",
|
||
surface4: "#2d2d2d",
|
||
surfaceDiffEmpty: "#0c0c0c",
|
||
surfaceSidebar: "#000000",
|
||
foregroundMuted: "#a1a1aa",
|
||
foregroundExtraMuted: "#71717a",
|
||
border: "#1c1c1c",
|
||
borderAccent: "#242424",
|
||
accent: "#20744A",
|
||
accentBright: "#7ccba0",
|
||
destructive: "#c44a4a",
|
||
terminalBlack: "#595959",
|
||
terminalBrightBlack: "#8a8a8a",
|
||
});
|
||
|
||
export const darkPureBlackTheme = buildDarkTheme(pureBlackDarkColors);
|
||
|
||
export const lightTheme = {
|
||
colorScheme: "light" as const,
|
||
colors: {
|
||
...lightSemanticColors,
|
||
palette: baseColors,
|
||
syntax: lightHighlightColors,
|
||
},
|
||
shadow: {
|
||
sm: {
|
||
shadowColor: "rgba(0, 0, 0, 0.02)",
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowRadius: 8,
|
||
elevation: 2,
|
||
},
|
||
md: {
|
||
shadowColor: "rgba(0, 0, 0, 0.04)",
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowRadius: 16,
|
||
elevation: 4,
|
||
},
|
||
lg: {
|
||
shadowColor: "rgba(0, 0, 0, 0.08)",
|
||
shadowOffset: { width: 0, height: 8 },
|
||
shadowRadius: 24,
|
||
elevation: 8,
|
||
},
|
||
},
|
||
...commonTheme,
|
||
} as const;
|
||
|
||
// Keep compatibility with existing code
|
||
export const theme = darkTheme;
|
||
|
||
export const THEME_OPTIONS = [
|
||
{
|
||
name: "light",
|
||
group: "primary",
|
||
unistylesName: "light",
|
||
theme: lightTheme,
|
||
swatch: "#ffffff",
|
||
},
|
||
{
|
||
name: "dark",
|
||
group: "primary",
|
||
unistylesName: "dark",
|
||
theme: darkTheme,
|
||
swatch: "#2D8B62",
|
||
},
|
||
{ name: "auto", group: "primary" },
|
||
{
|
||
name: "zinc",
|
||
group: "variant",
|
||
unistylesName: "darkZinc",
|
||
theme: darkZincTheme,
|
||
swatch: "#808080",
|
||
},
|
||
{
|
||
name: "midnight",
|
||
group: "variant",
|
||
unistylesName: "darkMidnight",
|
||
theme: darkMidnightTheme,
|
||
swatch: "#4A6BA8",
|
||
},
|
||
{
|
||
name: "claude",
|
||
group: "variant",
|
||
unistylesName: "darkClaude",
|
||
theme: darkClaudeTheme,
|
||
swatch: "#D97757",
|
||
},
|
||
{
|
||
name: "ghostty",
|
||
group: "variant",
|
||
unistylesName: "darkGhostty",
|
||
theme: darkGhosttyTheme,
|
||
swatch: "#8caaee",
|
||
},
|
||
{
|
||
name: "pureBlack",
|
||
group: "variant",
|
||
unistylesName: "darkPureBlack",
|
||
theme: darkPureBlackTheme,
|
||
swatch: "#000000",
|
||
},
|
||
] as const;
|
||
|
||
export type ThemePreference = (typeof THEME_OPTIONS)[number]["name"];
|
||
export type ThemeName = Exclude<ThemePreference, "auto">;
|
||
type ConcreteThemeOption = Exclude<(typeof THEME_OPTIONS)[number], { name: "auto" }>;
|
||
export type Theme = ConcreteThemeOption["theme"];
|
||
|
||
const CONCRETE_THEME_OPTIONS = THEME_OPTIONS.filter(
|
||
(option): option is ConcreteThemeOption => option.name !== "auto",
|
||
);
|
||
|
||
type ThemeToUnistyles = {
|
||
[Name in ThemeName]: Extract<ConcreteThemeOption, { name: Name }>["unistylesName"];
|
||
};
|
||
|
||
type ThemeSwatches = {
|
||
[Name in ThemeName]: Extract<ConcreteThemeOption, { name: Name }>["swatch"];
|
||
};
|
||
|
||
type RegisteredThemes = {
|
||
[Option in ConcreteThemeOption as Option["unistylesName"]]: Option["theme"];
|
||
};
|
||
|
||
export const THEME_TO_UNISTYLES = Object.fromEntries(
|
||
CONCRETE_THEME_OPTIONS.map((option) => [option.name, option.unistylesName]),
|
||
) as ThemeToUnistyles;
|
||
|
||
export const THEME_SWATCHES = Object.fromEntries(
|
||
CONCRETE_THEME_OPTIONS.map((option) => [option.name, option.swatch]),
|
||
) as ThemeSwatches;
|
||
|
||
export const REGISTERED_THEMES = Object.fromEntries(
|
||
CONCRETE_THEME_OPTIONS.map((option) => [option.unistylesName, option.theme]),
|
||
) as RegisteredThemes;
|
||
|
||
export function getNextThemePreference(current: ThemePreference): ThemePreference {
|
||
const currentIndex = THEME_OPTIONS.findIndex((option) => option.name === current);
|
||
const nextIndex = (currentIndex + 1) % THEME_OPTIONS.length;
|
||
return THEME_OPTIONS[nextIndex]?.name ?? THEME_OPTIONS[0].name;
|
||
}
|