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; export type ThemeName = "light" | "dark" | "zinc" | "midnight" | "claude" | "ghostty"; // 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 reads as one of // several muted signals in a row, so it uses statusMutedSuccess/Danger below rather than a // near-duplicate 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. Used by check // statuses, PR states, and review decisions. // // Normalized, not hand-picked. Every color below shares one lightness and one chroma within // its tier; 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. // // Two tiers. Full is for a surface whose job is to report the status — the PR pane, a usage // bar. Muted is for the same signal repeated on every row of a dense list, where full // saturation makes the list itself the loudest thing on screen; it sits at roughly // foregroundMuted's weight so a row built from it reads as one line of subtitle. // // Deliberately not on this scale: the workspace status dot. That one is supposed to shout, // so it keeps the raw palette — see getStatusDotColor. const lightStatusColors = { // L=0.53, chroma 90% of gamut max statusSuccess: "#1f8040", statusDanger: "#c32424", statusWarning: "#91601b", statusMerged: "#8728e5", // L=0.50, chroma 45% of gamut max statusMutedSuccess: "#496d50", statusMutedDanger: "#904d46", statusMutedWarning: "#755f45", statusMutedMerged: "#6e519d", }; const darkStatusColors = { // L=0.66, chroma 90% of gamut max statusSuccess: "#2ead58", statusDanger: "#f65048", statusWarning: "#c38328", statusMerged: "#a670f5", // L=0.70, chroma 40% of gamut max statusMutedSuccess: "#7cac85", statusMutedDanger: "#ca8c85", statusMutedWarning: "#b79875", statusMutedMerged: "#a594c6", }; // Semantic color tokens - Layer-based system const lightSemanticColors = { // 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) surfaceDiffEmpty: "#f6f6f6", // Empty side of split diff rows, between surface1 and surface2 and biased toward surface2 surfaceSidebar: "#f4f4f5", // Sidebar background (darker than main) surfaceSidebarHover: "#e9e9ec", // Sidebar hover (darker in light mode) surfaceWorkspace: "#ffffff", // Workspace main background // Text foreground: "#1a1a1e", foregroundMuted: "#71717a", foregroundExtraMuted: "#a1a1aa", // Controls scrollbarHandle: "#3f3f46", // zinc-700 // 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, 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; surfaceSidebarHover: string; foregroundMuted: string; foregroundExtraMuted: string; scrollbarHandle: string; border: string; borderAccent: string; accent: string; accentBright: string; accentForeground?: string; destructive: 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.surfaceSidebarHover, surfaceWorkspace: tint.surface1, foreground: "#fafafa", foregroundMuted: tint.foregroundMuted, foregroundExtraMuted: tint.foregroundExtraMuted, scrollbarHandle: tint.scrollbarHandle, 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, terminal: { background: tint.surface0, foreground: "#fafafa", cursor: "#fafafa", cursorAccent: tint.surface0, selectionBackground: "rgba(255, 255, 255, 0.2)", selectionForeground: "#fafafa", black: tint.surfaceSidebar, ...darkTerminalAnsi, brightBlack: tint.surface3, }, }; } // --------------------------------------------------------------------------- // 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", surfaceSidebarHover: "#1c1f1e", foregroundMuted: "#A1A5A4", foregroundExtraMuted: "#717574", scrollbarHandle: "#717574", border: "#252B2A", borderAccent: "#2F3534", accent: "#20744A", accentBright: "#7ccba0", destructive: "#c64f43", // warm red, hue ~7 — reads as red (not pink) against the green tint }); // Zinc — neutral gray, no tint const zincDarkColors = buildDarkSemanticColors({ surface0: "#18181b", surface1: "#1f1f22", surface2: "#27272a", surface3: "#3f3f46", surface4: "#52525b", surfaceDiffEmpty: "#242427", surfaceSidebar: "#131316", surfaceSidebarHover: "#1b1b1e", foregroundMuted: "#a1a1aa", foregroundExtraMuted: "#71717a", scrollbarHandle: "#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 }); // Midnight — subtle blue tint const midnightDarkColors = buildDarkSemanticColors({ surface0: "#161820", surface1: "#1c1e27", surface2: "#252731", surface3: "#3c3e4c", surface4: "#535564", surfaceDiffEmpty: "#222430", surfaceSidebar: "#121420", surfaceSidebarHover: "#1a1c28", foregroundMuted: "#9a9db0", foregroundExtraMuted: "#6b6e82", scrollbarHandle: "#6b6e82", border: "#242636", borderAccent: "#2e3040", accent: "#3b6fcf", accentBright: "#7eaaeb", destructive: "#c44a52", // red with a hint of cool lean against the blue tint }); // Claude — warm neutral with subtle orange undertone const claudeDarkColors = buildDarkSemanticColors({ surface0: "#1f1f1e", surface1: "#262523", surface2: "#2f2d2b", surface3: "#4a4745", surface4: "#605d5b", surfaceDiffEmpty: "#2a2826", surfaceSidebar: "#1a1918", surfaceSidebarHover: "#222120", foregroundMuted: "#ada9a5", foregroundExtraMuted: "#78746f", scrollbarHandle: "#78746f", border: "#2c2a27", borderAccent: "#36332f", accent: "#d97757", accentBright: "#e89a7f", destructive: "#cf513e", // warm orange-red, hue ~10 — sits with the Claude orange accent }); // 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", surfaceSidebarHover: "#292d36", foregroundMuted: "#c8ccd8", foregroundExtraMuted: "#a0a4b2", scrollbarHandle: "#a0a4b2", border: "#353a47", borderAccent: "#3f4454", accent: "#89b4fa", accentBright: "#b4d0fc", destructive: "#c44a55", // red with slight cool lean against the slate-blue surfaces }); export const SPACING = { 0: 0, 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 = { xs: 12, code: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 22, "3xl": 26, "4xl": 34, } 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; fontFamily: { ui: string; mono: string }; lineHeight: Record; 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) { 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); 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 a union type that works for both themes export type Theme = typeof darkTheme | typeof lightTheme; type UnistylesThemeKey = | "light" | "dark" | "darkZinc" | "darkMidnight" | "darkClaude" | "darkGhostty"; export const THEME_TO_UNISTYLES: Record = { light: "light", dark: "dark", zinc: "darkZinc", midnight: "darkMidnight", claude: "darkClaude", ghostty: "darkGhostty", }; export const THEME_SWATCHES: Record = { light: "#ffffff", dark: "#2D8B62", zinc: "#808080", midnight: "#4A6BA8", claude: "#D97757", ghostty: "#8caaee", };