diff --git a/src/index.css b/src/index.css index d6c47f4..bec41fe 100644 --- a/src/index.css +++ b/src/index.css @@ -5,10 +5,10 @@ @tailwind components; @tailwind utilities; -/* Custom scrollbar styling */ +/* Custom scrollbar styling - uses theme variables */ * { scrollbar-width: thin; - scrollbar-color: rgba(255, 255, 255, 0.2) transparent; + scrollbar-color: hsl(var(--scrollbar-thumb)) hsl(var(--scrollbar-track)); } *::-webkit-scrollbar { @@ -17,20 +17,21 @@ } *::-webkit-scrollbar-track { - background: transparent; + background: hsl(var(--scrollbar-track)); } *::-webkit-scrollbar-thumb { - background-color: rgba(255, 255, 255, 0.2); + background-color: hsl(var(--scrollbar-thumb)); border-radius: 4px; } *::-webkit-scrollbar-thumb:hover { - background-color: rgba(255, 255, 255, 0.3); + background-color: hsl(var(--scrollbar-thumb-hover)); } @layer base { :root { + /* Core colors - light theme defaults (overridden by ThemeProvider) */ --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; @@ -51,12 +52,43 @@ --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; --radius: 0.5rem; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; + + /* Status colors */ + --success: 142 76% 36%; + --warning: 45 93% 47%; + --info: 199 89% 48%; + + /* Syntax highlighting */ + --syntax-comment: 215.4 16.3% 46.9%; + --syntax-punctuation: 222.2 84% 30%; + --syntax-property: 222.2 47.4% 11.2%; + --syntax-string: 142 60% 30%; + --syntax-keyword: 270 80% 50%; + --syntax-function: 222.2 47.4% 11.2%; + --syntax-variable: 222.2 84% 4.9%; + --syntax-operator: 222.2 84% 20%; + + /* Diff colors */ + --diff-inserted: 142 60% 30%; + --diff-inserted-bg: 142 60% 50% / 0.15; + --diff-deleted: 0 70% 45%; + --diff-deleted-bg: 0 70% 50% / 0.15; + --diff-meta: 199 80% 40%; + --diff-meta-bg: 199 80% 50% / 0.1; + + /* Scrollbar */ + --scrollbar-thumb: 222.2 84% 4.9% / 0.2; + --scrollbar-thumb-hover: 222.2 84% 4.9% / 0.3; + --scrollbar-track: 0 0% 0% / 0; + + /* Gradient colors (RGB values) */ + --gradient-1: 234 179 8; + --gradient-2: 249 115 22; + --gradient-3: 147 51 234; + --gradient-4: 6 182 212; } + + /* Dark theme - applied via .dark class or ThemeProvider */ .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; @@ -77,11 +109,40 @@ --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; + + /* Status colors */ + --success: 142 76% 36%; + --warning: 45 93% 47%; + --info: 199 89% 48%; + + /* Syntax highlighting */ + --syntax-comment: 215 20.2% 70%; + --syntax-punctuation: 210 40% 70%; + --syntax-property: 210 40% 98%; + --syntax-string: 215 20.2% 70%; + --syntax-keyword: 210 40% 98%; + --syntax-function: 210 40% 98%; + --syntax-variable: 210 40% 98%; + --syntax-operator: 210 40% 98%; + + /* Diff colors */ + --diff-inserted: 134 60% 76%; + --diff-inserted-bg: 145 63% 42% / 0.1; + --diff-deleted: 0 100% 76%; + --diff-deleted-bg: 0 100% 60% / 0.1; + --diff-meta: 190 77% 70%; + --diff-meta-bg: 190 77% 70% / 0.08; + + /* Scrollbar */ + --scrollbar-thumb: 0 0% 100% / 0.2; + --scrollbar-thumb-hover: 0 0% 100% / 0.3; + --scrollbar-track: 0 0% 0% / 0; + + /* Gradient colors (RGB values) */ + --gradient-1: 250 204 21; + --gradient-2: 251 146 60; + --gradient-3: 168 85 247; + --gradient-4: 34 211 238; } } @@ -106,10 +167,10 @@ .text-grimoire-gradient { background: linear-gradient( to bottom, - rgb(250 204 21), - /* yellow-400 */ rgb(251 146 60), - /* orange-400 */ rgb(168 85 247), - /* purple-500 */ rgb(34 211 238) /* cyan-400 */ + rgb(var(--gradient-1)), + rgb(var(--gradient-2)), + rgb(var(--gradient-3)), + rgb(var(--gradient-4)) ); background-clip: text; -webkit-background-clip: text; @@ -117,16 +178,16 @@ } } -/* react-medium-image-zoom dark theme customization */ +/* react-medium-image-zoom theme customization - uses background with opacity */ [data-rmiz-modal-overlay] { - background-color: rgba(12, 12, 18, 0.92) !important; + background-color: hsl(var(--background) / 0.92) !important; } [data-rmiz-modal-content] { - box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); + box-shadow: 0 0 40px hsl(var(--foreground) / 0.2); } -/* React Mosaic Dark Theme Customization */ +/* React Mosaic Theme Customization */ .mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme { background: hsl(var(--background)); } diff --git a/src/lib/themes/apply.ts b/src/lib/themes/apply.ts new file mode 100644 index 0000000..a64ba4a --- /dev/null +++ b/src/lib/themes/apply.ts @@ -0,0 +1,150 @@ +import type { Theme } from "./types"; + +/** + * Apply a theme by setting CSS custom properties on the document root. + * This updates all theme variables at runtime without requiring a page reload. + */ +export function applyTheme(theme: Theme): void { + const root = document.documentElement; + + // Apply core colors + root.style.setProperty("--background", theme.colors.background); + root.style.setProperty("--foreground", theme.colors.foreground); + + root.style.setProperty("--card", theme.colors.card); + root.style.setProperty("--card-foreground", theme.colors.cardForeground); + + root.style.setProperty("--popover", theme.colors.popover); + root.style.setProperty( + "--popover-foreground", + theme.colors.popoverForeground, + ); + + root.style.setProperty("--primary", theme.colors.primary); + root.style.setProperty( + "--primary-foreground", + theme.colors.primaryForeground, + ); + + root.style.setProperty("--secondary", theme.colors.secondary); + root.style.setProperty( + "--secondary-foreground", + theme.colors.secondaryForeground, + ); + + root.style.setProperty("--accent", theme.colors.accent); + root.style.setProperty("--accent-foreground", theme.colors.accentForeground); + + root.style.setProperty("--muted", theme.colors.muted); + root.style.setProperty("--muted-foreground", theme.colors.mutedForeground); + + root.style.setProperty("--destructive", theme.colors.destructive); + root.style.setProperty( + "--destructive-foreground", + theme.colors.destructiveForeground, + ); + + root.style.setProperty("--border", theme.colors.border); + root.style.setProperty("--input", theme.colors.input); + root.style.setProperty("--ring", theme.colors.ring); + + // Status colors + root.style.setProperty("--success", theme.colors.success); + root.style.setProperty("--warning", theme.colors.warning); + root.style.setProperty("--info", theme.colors.info); + + // Syntax highlighting + root.style.setProperty("--syntax-comment", theme.syntax.comment); + root.style.setProperty("--syntax-punctuation", theme.syntax.punctuation); + root.style.setProperty("--syntax-property", theme.syntax.property); + root.style.setProperty("--syntax-string", theme.syntax.string); + root.style.setProperty("--syntax-keyword", theme.syntax.keyword); + root.style.setProperty("--syntax-function", theme.syntax.function); + root.style.setProperty("--syntax-variable", theme.syntax.variable); + root.style.setProperty("--syntax-operator", theme.syntax.operator); + + // Diff colors + root.style.setProperty("--diff-inserted", theme.syntax.diffInserted); + root.style.setProperty("--diff-inserted-bg", theme.syntax.diffInsertedBg); + root.style.setProperty("--diff-deleted", theme.syntax.diffDeleted); + root.style.setProperty("--diff-deleted-bg", theme.syntax.diffDeletedBg); + root.style.setProperty("--diff-meta", theme.syntax.diffMeta); + root.style.setProperty("--diff-meta-bg", theme.syntax.diffMetaBg); + + // Scrollbar + root.style.setProperty("--scrollbar-thumb", theme.scrollbar.thumb); + root.style.setProperty("--scrollbar-thumb-hover", theme.scrollbar.thumbHover); + root.style.setProperty("--scrollbar-track", theme.scrollbar.track); + + // Gradient + root.style.setProperty("--gradient-1", theme.gradient.color1); + root.style.setProperty("--gradient-2", theme.gradient.color2); + root.style.setProperty("--gradient-3", theme.gradient.color3); + root.style.setProperty("--gradient-4", theme.gradient.color4); + + // Layout + root.style.setProperty("--radius", theme.radius); + + // Remove the dark class management - we now handle this via CSS variables directly + // The dark class is no longer needed as themes apply their own color values +} + +/** + * Get all CSS variable names used by the theme system. + * Useful for debugging or documentation. + */ +export function getThemeVariables(): string[] { + return [ + // Core colors + "--background", + "--foreground", + "--card", + "--card-foreground", + "--popover", + "--popover-foreground", + "--primary", + "--primary-foreground", + "--secondary", + "--secondary-foreground", + "--accent", + "--accent-foreground", + "--muted", + "--muted-foreground", + "--destructive", + "--destructive-foreground", + "--border", + "--input", + "--ring", + // Status + "--success", + "--warning", + "--info", + // Syntax + "--syntax-comment", + "--syntax-punctuation", + "--syntax-property", + "--syntax-string", + "--syntax-keyword", + "--syntax-function", + "--syntax-variable", + "--syntax-operator", + // Diff + "--diff-inserted", + "--diff-inserted-bg", + "--diff-deleted", + "--diff-deleted-bg", + "--diff-meta", + "--diff-meta-bg", + // Scrollbar + "--scrollbar-thumb", + "--scrollbar-thumb-hover", + "--scrollbar-track", + // Gradient + "--gradient-1", + "--gradient-2", + "--gradient-3", + "--gradient-4", + // Layout + "--radius", + ]; +} diff --git a/src/lib/themes/builtin/dark.ts b/src/lib/themes/builtin/dark.ts new file mode 100644 index 0000000..290ff47 --- /dev/null +++ b/src/lib/themes/builtin/dark.ts @@ -0,0 +1,80 @@ +import type { Theme } from "../types"; + +/** + * Dark theme - the original Grimoire theme + * Deep blue-black background with bright purple accent + */ +export const darkTheme: Theme = { + id: "dark", + name: "Dark", + description: "The original Grimoire dark theme", + + colors: { + background: "222.2 84% 4.9%", + foreground: "210 40% 98%", + + card: "222.2 84% 4.9%", + cardForeground: "210 40% 98%", + + popover: "222.2 84% 4.9%", + popoverForeground: "210 40% 98%", + + primary: "210 40% 98%", + primaryForeground: "222.2 47.4% 11.2%", + + secondary: "217.2 32.6% 17.5%", + secondaryForeground: "210 40% 98%", + + accent: "270 100% 70%", + accentForeground: "222.2 84% 4.9%", + + muted: "217.2 32.6% 17.5%", + mutedForeground: "215 20.2% 70%", + + destructive: "0 62.8% 30.6%", + destructiveForeground: "210 40% 98%", + + border: "217.2 32.6% 17.5%", + input: "217.2 32.6% 17.5%", + ring: "212.7 26.8% 83.9%", + + // Status colors + success: "142 76% 36%", + warning: "45 93% 47%", + info: "199 89% 48%", + }, + + syntax: { + comment: "215 20.2% 70%", + punctuation: "210 40% 70%", + property: "210 40% 98%", + string: "215 20.2% 70%", + keyword: "210 40% 98%", + function: "210 40% 98%", + variable: "210 40% 98%", + operator: "210 40% 98%", + + // Diff colors (converted from hardcoded RGB) + diffInserted: "134 60% 76%", + diffInsertedBg: "145 63% 42% / 0.1", + diffDeleted: "0 100% 76%", + diffDeletedBg: "0 100% 60% / 0.1", + diffMeta: "190 77% 70%", + diffMetaBg: "190 77% 70% / 0.08", + }, + + scrollbar: { + thumb: "0 0% 100% / 0.2", + thumbHover: "0 0% 100% / 0.3", + track: "0 0% 0% / 0", + }, + + gradient: { + color1: "250 204 21", // yellow-400 + color2: "251 146 60", // orange-400 + color3: "168 85 247", // purple-500 + color4: "34 211 238", // cyan-400 + }, + + radius: "0.5rem", +}; diff --git a/src/lib/themes/builtin/index.ts b/src/lib/themes/builtin/index.ts new file mode 100644 index 0000000..cd06c79 --- /dev/null +++ b/src/lib/themes/builtin/index.ts @@ -0,0 +1,23 @@ +export { darkTheme } from "./dark"; +export { lightTheme } from "./light"; +export { plan9Theme } from "./plan9"; + +import { darkTheme } from "./dark"; +import { lightTheme } from "./light"; +import { plan9Theme } from "./plan9"; +import type { Theme, BuiltinThemeId } from "../types"; + +/** Map of all built-in themes by ID */ +export const builtinThemes: Record = { + dark: darkTheme, + light: lightTheme, + plan9: plan9Theme, +}; + +/** Array of all built-in themes for iteration */ +export const builtinThemeList: Theme[] = [darkTheme, lightTheme, plan9Theme]; + +/** Get a built-in theme by ID */ +export function getBuiltinTheme(id: BuiltinThemeId): Theme { + return builtinThemes[id]; +} diff --git a/src/lib/themes/builtin/light.ts b/src/lib/themes/builtin/light.ts new file mode 100644 index 0000000..942aa8f --- /dev/null +++ b/src/lib/themes/builtin/light.ts @@ -0,0 +1,80 @@ +import type { Theme } from "../types"; + +/** + * Light theme - clean white background + * Based on the original shadcn/ui light mode values + */ +export const lightTheme: Theme = { + id: "light", + name: "Light", + description: "Clean light theme for daytime use", + + colors: { + background: "0 0% 100%", + foreground: "222.2 84% 4.9%", + + card: "0 0% 100%", + cardForeground: "222.2 84% 4.9%", + + popover: "0 0% 100%", + popoverForeground: "222.2 84% 4.9%", + + primary: "222.2 47.4% 11.2%", + primaryForeground: "210 40% 98%", + + secondary: "210 40% 96.1%", + secondaryForeground: "222.2 47.4% 11.2%", + + accent: "270 80% 60%", + accentForeground: "0 0% 100%", + + muted: "210 40% 96.1%", + mutedForeground: "215.4 16.3% 46.9%", + + destructive: "0 84.2% 60.2%", + destructiveForeground: "210 40% 98%", + + border: "214.3 31.8% 91.4%", + input: "214.3 31.8% 91.4%", + ring: "222.2 84% 4.9%", + + // Status colors + success: "142 76% 36%", + warning: "45 93% 47%", + info: "199 89% 48%", + }, + + syntax: { + comment: "215.4 16.3% 46.9%", + punctuation: "222.2 84% 30%", + property: "222.2 47.4% 11.2%", + string: "142 60% 30%", + keyword: "270 80% 50%", + function: "222.2 47.4% 11.2%", + variable: "222.2 84% 4.9%", + operator: "222.2 84% 20%", + + // Diff colors + diffInserted: "142 60% 30%", + diffInsertedBg: "142 60% 50% / 0.15", + diffDeleted: "0 70% 45%", + diffDeletedBg: "0 70% 50% / 0.15", + diffMeta: "199 80% 40%", + diffMetaBg: "199 80% 50% / 0.1", + }, + + scrollbar: { + thumb: "222.2 84% 4.9% / 0.2", + thumbHover: "222.2 84% 4.9% / 0.3", + track: "0 0% 0% / 0", + }, + + gradient: { + color1: "234 179 8", // yellow-500 (darker for light bg) + color2: "249 115 22", // orange-500 + color3: "147 51 234", // purple-600 + color4: "6 182 212", // cyan-500 + }, + + radius: "0.5rem", +}; diff --git a/src/lib/themes/builtin/plan9.ts b/src/lib/themes/builtin/plan9.ts new file mode 100644 index 0000000..1302e57 --- /dev/null +++ b/src/lib/themes/builtin/plan9.ts @@ -0,0 +1,98 @@ +import type { Theme } from "../types"; + +/** + * Plan 9 theme - inspired by the Plan 9 from Bell Labs operating system + * + * Characteristics: + * - Pale yellow/cream backgrounds (#ffffe0) + * - Light blue-green window chrome (#eaffea) + * - Black text for high contrast + * - Bright yellow selections + * - Dark blue accents + * - No rounded corners (squared aesthetic) + */ +export const plan9Theme: Theme = { + id: "plan9", + name: "Plan 9", + description: "Inspired by Plan 9 from Bell Labs", + + colors: { + // Characteristic pale yellow background + background: "60 100% 94%", // #ffffe0 + foreground: "0 0% 0%", // Pure black + + // Window chrome - pale green (acme editor style) + card: "120 100% 95%", // #eaffea + cardForeground: "0 0% 0%", + + popover: "60 100% 97%", + popoverForeground: "0 0% 0%", + + // Dark blue for interactive elements + primary: "220 100% 25%", + primaryForeground: "60 100% 94%", + + // Muted green secondary + secondary: "120 30% 88%", + secondaryForeground: "0 0% 0%", + + // Bright yellow accent (Plan9 signature selection color) + accent: "60 100% 50%", + accentForeground: "0 0% 0%", + + // Muted yellow for subdued elements + muted: "60 30% 88%", + mutedForeground: "0 0% 35%", + + // Red for destructive + destructive: "0 70% 45%", + destructiveForeground: "0 0% 100%", + + // Dark blue borders + border: "220 40% 50%", + input: "60 30% 92%", + ring: "220 100% 25%", + + // Status colors + success: "120 60% 30%", + warning: "45 90% 45%", + info: "200 80% 40%", + }, + + syntax: { + // Acme-inspired syntax colors + comment: "0 0% 45%", // Gray + punctuation: "0 0% 25%", // Dark gray + property: "220 100% 25%", // Dark blue + string: "120 60% 28%", // Forest green + keyword: "280 60% 35%", // Purple + function: "220 100% 25%", // Dark blue + variable: "0 0% 0%", // Black + operator: "0 0% 15%", // Near black + + // Diff colors - subtle on yellow background + diffInserted: "120 60% 25%", + diffInsertedBg: "120 50% 85%", + diffDeleted: "0 65% 40%", + diffDeletedBg: "0 50% 90%", + diffMeta: "200 70% 35%", + diffMetaBg: "200 50% 88%", + }, + + scrollbar: { + thumb: "220 30% 55%", + thumbHover: "220 40% 45%", + track: "60 30% 90%", + }, + + gradient: { + // Muted gradient for Plan9 aesthetic + color1: "180 140 20", // Olive/mustard + color2: "200 120 50", // Burnt orange + color3: "100 60 180", // Muted purple + color4: "40 160 180", // Teal + }, + + // Plan9 has no rounded corners - everything is squared + radius: "0", +}; diff --git a/src/lib/themes/context.tsx b/src/lib/themes/context.tsx new file mode 100644 index 0000000..745ebae --- /dev/null +++ b/src/lib/themes/context.tsx @@ -0,0 +1,193 @@ +import * as React from "react"; +import type { Theme, BuiltinThemeId } from "./types"; +import { isBuiltinTheme } from "./types"; +import { builtinThemes, builtinThemeList } from "./builtin"; +import { applyTheme } from "./apply"; + +const STORAGE_KEY = "grimoire-theme"; +const DEFAULT_THEME_ID: BuiltinThemeId = "dark"; + +interface ThemeContextValue { + /** Current active theme */ + theme: Theme; + /** Current theme ID */ + themeId: string; + /** Set theme by ID */ + setTheme: (id: string) => void; + /** List of all available themes (builtin + custom) */ + availableThemes: Theme[]; + /** Custom themes added by user */ + customThemes: Theme[]; + /** Add a custom theme */ + addCustomTheme: (theme: Theme) => void; + /** Remove a custom theme by ID */ + removeCustomTheme: (id: string) => void; +} + +const ThemeContext = React.createContext(null); + +/** + * Hook to access theme context + * Must be used within a ThemeProvider + */ +export function useTheme(): ThemeContextValue { + const context = React.useContext(ThemeContext); + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +} + +/** + * Get the saved theme ID from localStorage + */ +function getSavedThemeId(): string { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + const data = JSON.parse(saved); + return data.themeId || DEFAULT_THEME_ID; + } + } catch { + // Ignore parse errors + } + return DEFAULT_THEME_ID; +} + +/** + * Get saved custom themes from localStorage + */ +function getSavedCustomThemes(): Theme[] { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + const data = JSON.parse(saved); + return data.customThemes || []; + } + } catch { + // Ignore parse errors + } + return []; +} + +/** + * Save theme data to localStorage + */ +function saveThemeData(themeId: string, customThemes: Theme[]): void { + try { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ themeId, customThemes }), + ); + } catch { + // Ignore storage errors (quota exceeded, etc.) + } +} + +/** + * Find a theme by ID from builtin and custom themes + */ +function findTheme(id: string, customThemes: Theme[]): Theme | undefined { + if (isBuiltinTheme(id)) { + return builtinThemes[id]; + } + return customThemes.find((t) => t.id === id); +} + +interface ThemeProviderProps { + children: React.ReactNode; + /** Default theme ID (overrides localStorage on first render) */ + defaultTheme?: string; +} + +/** + * Theme provider component + * Manages theme state, persistence, and CSS variable application + */ +export function ThemeProvider({ + children, + defaultTheme, +}: ThemeProviderProps): React.ReactElement { + // Initialize from localStorage or default + const [themeId, setThemeIdState] = React.useState(() => { + return defaultTheme || getSavedThemeId(); + }); + + const [customThemes, setCustomThemes] = React.useState(() => { + return getSavedCustomThemes(); + }); + + // Resolve current theme + const theme = React.useMemo(() => { + return findTheme(themeId, customThemes) || builtinThemes[DEFAULT_THEME_ID]; + }, [themeId, customThemes]); + + // Apply theme on mount and when theme changes + React.useEffect(() => { + applyTheme(theme); + }, [theme]); + + // Save to localStorage when theme changes + React.useEffect(() => { + saveThemeData(themeId, customThemes); + }, [themeId, customThemes]); + + const setTheme = React.useCallback((id: string) => { + setThemeIdState(id); + }, []); + + const addCustomTheme = React.useCallback((newTheme: Theme) => { + setCustomThemes((prev) => { + // Replace if exists, otherwise add + const existing = prev.findIndex((t) => t.id === newTheme.id); + if (existing >= 0) { + const updated = [...prev]; + updated[existing] = newTheme; + return updated; + } + return [...prev, newTheme]; + }); + }, []); + + const removeCustomTheme = React.useCallback( + (id: string) => { + setCustomThemes((prev) => prev.filter((t) => t.id !== id)); + // If removing current theme, switch to default + if (themeId === id) { + setThemeIdState(DEFAULT_THEME_ID); + } + }, + [themeId], + ); + + const availableThemes = React.useMemo(() => { + return [...builtinThemeList, ...customThemes]; + }, [customThemes]); + + const contextValue = React.useMemo( + () => ({ + theme, + themeId, + setTheme, + availableThemes, + customThemes, + addCustomTheme, + removeCustomTheme, + }), + [ + theme, + themeId, + setTheme, + availableThemes, + customThemes, + addCustomTheme, + removeCustomTheme, + ], + ); + + return ( + + {children} + + ); +} diff --git a/src/lib/themes/index.ts b/src/lib/themes/index.ts new file mode 100644 index 0000000..1de0865 --- /dev/null +++ b/src/lib/themes/index.ts @@ -0,0 +1,29 @@ +// Types +export type { + Theme, + ThemeColors, + ThemeSyntax, + ThemeScrollbar, + ThemeGradient, + ThemeMeta, + HSLValue, + RGBValue, + BuiltinThemeId, +} from "./types"; +export { isBuiltinTheme } from "./types"; + +// Built-in themes +export { + darkTheme, + lightTheme, + plan9Theme, + builtinThemes, + builtinThemeList, + getBuiltinTheme, +} from "./builtin"; + +// Theme application +export { applyTheme, getThemeVariables } from "./apply"; + +// Context and hooks +export { ThemeProvider, useTheme } from "./context"; diff --git a/src/lib/themes/types.ts b/src/lib/themes/types.ts new file mode 100644 index 0000000..6fc1746 --- /dev/null +++ b/src/lib/themes/types.ts @@ -0,0 +1,149 @@ +/** + * Theme System Types + * + * All color values are HSL without the hsl() wrapper. + * Format: "hue saturation% lightness%" (e.g., "220 70% 50%") + * + * For Nostr publishing: + * - kind: 30078 (NIP-78 arbitrary app data) + * - d tag: "grimoire-theme" + * - name tag: theme display name + */ + +/** HSL color value without wrapper (e.g., "220 70% 50%") */ +export type HSLValue = string; + +/** RGB color for gradients (e.g., "250 204 21") */ +export type RGBValue = string; + +/** Core semantic colors for UI components */ +export interface ThemeColors { + // Core backgrounds and text + background: HSLValue; + foreground: HSLValue; + + // Card/panel surfaces + card: HSLValue; + cardForeground: HSLValue; + + // Popover/dropdown surfaces + popover: HSLValue; + popoverForeground: HSLValue; + + // Primary interactive elements + primary: HSLValue; + primaryForeground: HSLValue; + + // Secondary interactive elements + secondary: HSLValue; + secondaryForeground: HSLValue; + + // Accent/highlight color + accent: HSLValue; + accentForeground: HSLValue; + + // Subdued/muted elements + muted: HSLValue; + mutedForeground: HSLValue; + + // Destructive/error states + destructive: HSLValue; + destructiveForeground: HSLValue; + + // Form elements + border: HSLValue; + input: HSLValue; + ring: HSLValue; + + // Status indicators (replacing hardcoded Tailwind colors) + success: HSLValue; + warning: HSLValue; + info: HSLValue; +} + +/** Syntax highlighting colors for code blocks */ +export interface ThemeSyntax { + // General tokens + comment: HSLValue; + punctuation: HSLValue; + property: HSLValue; + string: HSLValue; + keyword: HSLValue; + function: HSLValue; + variable: HSLValue; + operator: HSLValue; + + // Diff-specific tokens + diffInserted: HSLValue; + diffInsertedBg: HSLValue; + diffDeleted: HSLValue; + diffDeletedBg: HSLValue; + diffMeta: HSLValue; + diffMetaBg: HSLValue; +} + +/** Scrollbar styling */ +export interface ThemeScrollbar { + thumb: HSLValue; + thumbHover: HSLValue; + track: HSLValue; +} + +/** Gradient colors (RGB values for CSS rgb() function) */ +export interface ThemeGradient { + // Grimoire brand gradient (4 color stops) + color1: RGBValue; // Top - yellow + color2: RGBValue; // Upper-middle - orange + color3: RGBValue; // Lower-middle - purple + color4: RGBValue; // Bottom - cyan +} + +/** Complete theme definition */ +export interface Theme { + /** Unique identifier (e.g., "plan9", "dark", "light") */ + id: string; + + /** Display name shown in UI */ + name: string; + + /** Theme author (npub or display name) */ + author?: string; + + /** Semantic version */ + version?: string; + + /** Theme description */ + description?: string; + + /** Core UI colors */ + colors: ThemeColors; + + /** Syntax highlighting colors */ + syntax: ThemeSyntax; + + /** Scrollbar colors */ + scrollbar: ThemeScrollbar; + + /** Gradient colors */ + gradient: ThemeGradient; + + /** Border radius base value (e.g., "0.5rem", "0") */ + radius: string; +} + +/** Theme metadata for listings (without full color data) */ +export interface ThemeMeta { + id: string; + name: string; + author?: string; + version?: string; + description?: string; +} + +/** Built-in theme IDs */ +export type BuiltinThemeId = "dark" | "light" | "plan9"; + +/** Check if a theme ID is a built-in theme */ +export function isBuiltinTheme(id: string): id is BuiltinThemeId { + return id === "dark" || id === "light" || id === "plan9"; +} diff --git a/src/main.tsx b/src/main.tsx index 0ea432d..dd08235 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,31 +8,30 @@ import { Toaster } from "sonner"; import { TooltipProvider } from "./components/ui/tooltip"; import { ErrorBoundary } from "./components/ErrorBoundary"; import { initializeErrorHandling } from "./lib/error-handler"; - -// Add dark class to html element for default dark theme -document.documentElement.classList.add("dark"); +import { ThemeProvider } from "./lib/themes"; // Initialize global error handling initializeErrorHandling(); createRoot(document.getElementById("root")!).render( - - - - - - + + + + + + + + , ); diff --git a/src/styles/prism-theme.css b/src/styles/prism-theme.css index 168517f..a3f9dfe 100644 --- a/src/styles/prism-theme.css +++ b/src/styles/prism-theme.css @@ -1,4 +1,4 @@ -/* Grimoire Prism Theme - Matches dark theme using CSS variables */ +/* Grimoire Prism Theme - Uses CSS theme variables */ code[class*="language-"], pre[class*="language-"] { @@ -19,8 +19,8 @@ pre[class*="language-"] { /* Deleted lines (red) - subtle background, no strikethrough */ .token.deleted { - color: #ff8787; - background: rgba(255, 59, 48, 0.1); + color: hsl(var(--diff-deleted)); + background: hsl(var(--diff-deleted-bg)); display: block; margin: 0 -1rem; padding: 0 1rem; @@ -28,8 +28,8 @@ pre[class*="language-"] { /* Added lines (green) - subtle background */ .token.inserted { - color: #69db7c; - background: rgba(52, 199, 89, 0.1); + color: hsl(var(--diff-inserted)); + background: hsl(var(--diff-inserted-bg)); display: block; margin: 0 -1rem; padding: 0 1rem; @@ -38,8 +38,8 @@ pre[class*="language-"] { /* Hunk headers (@@ -1,5 +1,7 @@) - cyan/blue */ .token.diff.coord, .token.coord { - color: #66d9ef; - background: rgba(102, 217, 239, 0.08); + color: hsl(var(--diff-meta)); + background: hsl(var(--diff-meta-bg)); display: block; margin: 0 -1rem; padding: 0 1rem; @@ -66,11 +66,11 @@ pre[class*="language-"] { .token.prolog, .token.doctype, .token.cdata { - color: hsl(var(--muted-foreground)); + color: hsl(var(--syntax-comment)); } .token.punctuation { - color: hsl(var(--foreground) / 0.7); + color: hsl(var(--syntax-punctuation)); } .token.property, @@ -79,7 +79,7 @@ pre[class*="language-"] { .token.number, .token.constant, .token.symbol { - color: hsl(var(--primary)); + color: hsl(var(--syntax-property)); } .token.selector, @@ -87,8 +87,7 @@ pre[class*="language-"] { .token.string, .token.char, .token.builtin { - color: hsl(var(--muted-foreground)); - font-weight: 500; + color: hsl(var(--syntax-string)); } .token.operator, @@ -96,25 +95,25 @@ pre[class*="language-"] { .token.url, .language-css .token.string, .style .token.string { - color: hsl(var(--foreground)); + color: hsl(var(--syntax-operator)); } .token.atrule, .token.attr-value, .token.keyword { - color: hsl(var(--primary)); + color: hsl(var(--syntax-keyword)); } .token.function, .token.class-name { - color: hsl(var(--primary)); + color: hsl(var(--syntax-function)); font-weight: bold; } .token.regex, .token.important, .token.variable { - color: hsl(var(--primary)); + color: hsl(var(--syntax-variable)); } .token.important, diff --git a/tailwind.config.js b/tailwind.config.js index 23c5e58..df78150 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -65,13 +65,10 @@ export default { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", - chart: { - 1: "hsl(var(--chart-1))", - 2: "hsl(var(--chart-2))", - 3: "hsl(var(--chart-3))", - 4: "hsl(var(--chart-4))", - 5: "hsl(var(--chart-5))", - }, + // Status colors + success: "hsl(var(--success))", + warning: "hsl(var(--warning))", + info: "hsl(var(--info))", }, }, },