diff --git a/src/index.css b/src/index.css index fc9891d..62a9fc7 100644 --- a/src/index.css +++ b/src/index.css @@ -164,6 +164,8 @@ --syntax-function: 222.2 47.4% 11.2%; --syntax-variable: 222.2 84% 4.9%; --syntax-operator: 222.2 84% 20%; + --syntax-constant: 199 89% 48%; + --syntax-tag: 142 76% 36%; /* Diff colors */ --diff-inserted: 142 60% 30%; @@ -223,15 +225,17 @@ --tooltip: 217.2 32.6% 30%; --tooltip-foreground: 210 40% 98%; - /* Syntax highlighting */ - --syntax-comment: 215 20.2% 70%; - --syntax-punctuation: 210 40% 70%; + /* Syntax highlighting - high contrast for dark backgrounds */ + --syntax-comment: 215 20% 55%; + --syntax-punctuation: 210 30% 75%; --syntax-property: 210 40% 98%; - --syntax-string: 215 20.2% 70%; - --syntax-keyword: 210 40% 98%; + --syntax-string: 140 70% 65%; + --syntax-keyword: 270 100% 80%; --syntax-function: 210 40% 98%; --syntax-variable: 210 40% 98%; --syntax-operator: 210 40% 98%; + --syntax-constant: 199 90% 70%; + --syntax-tag: 140 70% 65%; /* Diff colors */ --diff-inserted: 134 60% 76%; diff --git a/src/lib/shiki.ts b/src/lib/shiki.ts index 718dd93..a044b0b 100644 --- a/src/lib/shiki.ts +++ b/src/lib/shiki.ts @@ -1,7 +1,7 @@ import { createHighlighterCore, type HighlighterCore, - type ThemeRegistration, + type ShikiTransformer, } from "shiki/core"; import { createOnigurumaEngine } from "shiki/engine/oniguruma"; @@ -12,28 +12,83 @@ const loadedLanguages = new Set(); const failedLanguages = new Set(); /** - * Grimoire dark theme - minimalistic grayscale with high contrast - * Uses bright grays for readability with color only for diff semantics + * Transformer that adds CSS classes based on token scopes + * This allows us to style tokens with CSS variables instead of inline colors */ -const grimoireDarkTheme: ThemeRegistration = { - name: "grimoire-dark", - type: "dark", +const classTransformer: ShikiTransformer = { + name: "class-transformer", + span(node) { + // Map inline colors to semantic CSS classes + // This allows us to style tokens with CSS variables instead of hardcoded colors + const style = node.properties?.style as string | undefined; + if (!style) return; + + // Remove inline style and add class based on token type + delete node.properties.style; + + // Add base class + node.properties.className = node.properties.className || []; + const classes = node.properties.className as string[]; + classes.push("shiki-token"); + + // Detect token type from the original style color + // These colors come from our theme definitions + if (style.includes("#8b949e") || style.includes("comment")) { + classes.push("shiki-comment"); + } else if (style.includes("#a5d6ff") || style.includes("string")) { + classes.push("shiki-string"); + } else if (style.includes("#79c0ff") || style.includes("constant")) { + classes.push("shiki-constant"); + } else if (style.includes("#7ee787") || style.includes("tag")) { + classes.push("shiki-tag"); + } else if (style.includes("#ffa198") || style.includes("deleted")) { + classes.push("shiki-deleted"); + } else if ( + style.includes("#f0f0f0") || + style.includes("#e6edf3") || + style.includes("keyword") || + style.includes("function") + ) { + classes.push("shiki-keyword"); + } else if (style.includes("#c9d1d9") || style.includes("punctuation")) { + classes.push("shiki-punctuation"); + } + }, + pre(node) { + // Remove background color from pre, let CSS handle it + if (node.properties?.style) { + const style = node.properties.style as string; + node.properties.style = style.replace(/background-color:[^;]+;?/g, ""); + } + }, + code(node) { + // Remove color from code element + if (node.properties?.style) { + delete node.properties.style; + } + }, +}; + +/** + * Minimal theme - we'll override colors via CSS + * Using high-contrast colors as fallback if CSS fails + */ +const minimalTheme = { + name: "grimoire", + type: "dark" as const, colors: { - "editor.background": "#0d1117", + "editor.background": "transparent", "editor.foreground": "#e6edf3", }, tokenColors: [ - // Comments - readable gray { scope: ["comment", "punctuation.definition.comment"], settings: { foreground: "#8b949e" }, }, - // Strings - distinct but not colorful { scope: ["string", "string.quoted"], settings: { foreground: "#a5d6ff" }, }, - // Keywords, operators - bright { scope: [ "keyword", @@ -45,12 +100,10 @@ const grimoireDarkTheme: ThemeRegistration = { ], settings: { foreground: "#f0f0f0" }, }, - // Functions, methods - foreground bold { scope: ["entity.name.function", "support.function", "meta.function-call"], - settings: { foreground: "#e6edf3", fontStyle: "bold" }, + settings: { foreground: "#e6edf3" }, }, - // Classes, types - foreground bold { scope: [ "entity.name.class", @@ -58,9 +111,8 @@ const grimoireDarkTheme: ThemeRegistration = { "support.class", "support.type", ], - settings: { foreground: "#f0f0f0", fontStyle: "bold" }, + settings: { foreground: "#f0f0f0" }, }, - // Numbers, constants - bright { scope: [ "constant", @@ -70,17 +122,14 @@ const grimoireDarkTheme: ThemeRegistration = { ], settings: { foreground: "#79c0ff" }, }, - // Variables, parameters - foreground { scope: ["variable", "variable.parameter", "variable.other"], settings: { foreground: "#e6edf3" }, }, - // Punctuation - visible { scope: ["punctuation", "meta.brace"], settings: { foreground: "#c9d1d9" }, }, - // Properties, attributes { scope: [ "variable.other.property", @@ -89,17 +138,14 @@ const grimoireDarkTheme: ThemeRegistration = { ], settings: { foreground: "#e6edf3" }, }, - // Tags (HTML/JSX) { scope: ["entity.name.tag", "support.class.component"], settings: { foreground: "#7ee787" }, }, - // JSON keys { scope: ["support.type.property-name.json"], settings: { foreground: "#a5d6ff" }, }, - // Diff - deleted (red) { scope: [ "markup.deleted", @@ -108,7 +154,6 @@ const grimoireDarkTheme: ThemeRegistration = { ], settings: { foreground: "#ffa198" }, }, - // Diff - inserted (green) { scope: [ "markup.inserted", @@ -117,17 +162,14 @@ const grimoireDarkTheme: ThemeRegistration = { ], settings: { foreground: "#7ee787" }, }, - // Diff - changed/range { scope: ["markup.changed", "meta.diff.range", "meta.diff.header"], settings: { foreground: "#a5d6ff" }, }, - // Markdown headings { scope: ["markup.heading", "entity.name.section"], - settings: { foreground: "#f0f0f0", fontStyle: "bold" }, + settings: { foreground: "#f0f0f0" }, }, - // Markdown bold/italic { scope: ["markup.bold"], settings: { fontStyle: "bold" }, @@ -136,12 +178,10 @@ const grimoireDarkTheme: ThemeRegistration = { scope: ["markup.italic"], settings: { fontStyle: "italic" }, }, - // Markdown links { scope: ["markup.underline.link"], settings: { foreground: "#a5d6ff" }, }, - // Markdown code { scope: ["markup.inline.raw", "markup.raw"], settings: { foreground: "#a5d6ff" }, @@ -149,138 +189,6 @@ const grimoireDarkTheme: ThemeRegistration = { ], }; -/** - * Grimoire light theme - minimalistic grayscale for light backgrounds - */ -const grimoireLightTheme: ThemeRegistration = { - name: "grimoire-light", - type: "light", - colors: { - "editor.background": "#ffffff", - "editor.foreground": "#1a1a1a", - }, - tokenColors: [ - // Comments - muted - { - scope: ["comment", "punctuation.definition.comment"], - settings: { foreground: "#6b7280" }, - }, - // Strings - muted but slightly emphasized - { - scope: ["string", "string.quoted"], - settings: { foreground: "#4b5563" }, - }, - // Keywords, operators - emphasized dark gray - { - scope: [ - "keyword", - "storage", - "storage.type", - "storage.modifier", - "keyword.operator", - "keyword.control", - ], - settings: { foreground: "#374151" }, - }, - // Functions, methods - foreground bold - { - scope: ["entity.name.function", "support.function", "meta.function-call"], - settings: { foreground: "#1a1a1a", fontStyle: "bold" }, - }, - // Classes, types - foreground bold - { - scope: [ - "entity.name.class", - "entity.name.type", - "support.class", - "support.type", - ], - settings: { foreground: "#1a1a1a", fontStyle: "bold" }, - }, - // Numbers, constants - emphasized dark gray - { - scope: [ - "constant", - "constant.numeric", - "constant.language", - "constant.character", - ], - settings: { foreground: "#374151" }, - }, - // Variables, parameters - foreground - { - scope: ["variable", "variable.parameter", "variable.other"], - settings: { foreground: "#1a1a1a" }, - }, - // Punctuation - slightly muted - { - scope: ["punctuation", "meta.brace"], - settings: { foreground: "#4b5563" }, - }, - // Properties, attributes - { - scope: [ - "variable.other.property", - "entity.other.attribute-name", - "support.type.property-name", - ], - settings: { foreground: "#374151" }, - }, - // Tags (HTML/JSX) - { - scope: ["entity.name.tag", "support.class.component"], - settings: { foreground: "#374151" }, - }, - // JSON keys - { - scope: ["support.type.property-name.json"], - settings: { foreground: "#374151" }, - }, - // Diff - deleted (red) - { - scope: [ - "markup.deleted", - "punctuation.definition.deleted", - "meta.diff.header.from-file", - ], - settings: { foreground: "#dc2626" }, - }, - // Diff - inserted (green) - { - scope: [ - "markup.inserted", - "punctuation.definition.inserted", - "meta.diff.header.to-file", - ], - settings: { foreground: "#16a34a" }, - }, - // Diff - changed/range - { - scope: ["markup.changed", "meta.diff.range", "meta.diff.header"], - settings: { foreground: "#0891b2" }, - }, - // Markdown headings - { - scope: ["markup.heading", "entity.name.section"], - settings: { foreground: "#1a1a1a", fontStyle: "bold" }, - }, - // Markdown bold/italic - { - scope: ["markup.bold"], - settings: { fontStyle: "bold" }, - }, - { - scope: ["markup.italic"], - settings: { fontStyle: "italic" }, - }, - // Markdown links - { - scope: ["markup.underline.link"], - settings: { foreground: "#2563eb" }, - }, - ], -}; - /** * Language alias mapping (file extensions and common names to Shiki IDs) */ @@ -436,7 +344,7 @@ export async function getHighlighter(): Promise { if (!highlighterPromise) { highlighterPromise = createHighlighterCore({ - themes: [grimoireDarkTheme, grimoireLightTheme], + themes: [minimalTheme], langs: [ import("shiki/langs/javascript.mjs"), import("shiki/langs/typescript.mjs"), @@ -482,18 +390,9 @@ async function loadLanguage(lang: string): Promise { } } -/** - * Detect if dark mode is currently active - */ -function isDarkMode(): boolean { - if (typeof document === "undefined") return true; - return document.documentElement.classList.contains("dark"); -} - /** * Highlight code with lazy language loading - * Returns HTML string - * Automatically uses the appropriate theme based on current color scheme + * Returns HTML string with CSS classes for styling via CSS variables */ export async function highlightCode( code: string, @@ -506,12 +405,10 @@ export async function highlightCode( const loaded = await loadLanguage(lang); const effectiveLang = loaded ? lang : "text"; - // Select theme based on current color scheme - const theme = isDarkMode() ? "grimoire-dark" : "grimoire-light"; - return hl.codeToHtml(code, { lang: effectiveLang, - theme, + theme: "grimoire", + transformers: [classTransformer], }); } diff --git a/src/styles/shiki-theme.css b/src/styles/shiki-theme.css index 87a72da..bf67fe7 100644 --- a/src/styles/shiki-theme.css +++ b/src/styles/shiki-theme.css @@ -1,6 +1,15 @@ -/* Shiki syntax highlighting - Grimoire dark theme overrides */ +/* ========================================================================== + Shiki Syntax Highlighting - CSS Variable Based Theming + + Token colors are defined using CSS variables from the theme system, + allowing automatic adaptation to light/dark mode. + ========================================================================== */ /* Base container styling */ +.shiki-container { + color: hsl(var(--foreground)); +} + .shiki-container pre { background: transparent !important; margin: 0; @@ -15,6 +24,7 @@ word-spacing: normal; word-break: normal; tab-size: 4; + color: hsl(var(--foreground)); } /* Ensure shiki's inline styles don't conflict */ @@ -26,7 +36,59 @@ background: transparent !important; } -/* Loading state - subtle pulse animation */ +/* ========================================================================== + Token Classes - Styled with CSS Variables + ========================================================================== */ + +/* Base token - inherits foreground color */ +.shiki-token { + color: hsl(var(--syntax-variable)); +} + +/* Comments */ +.shiki-comment { + color: hsl(var(--syntax-comment)); +} + +/* Strings */ +.shiki-string { + color: hsl(var(--syntax-string)); +} + +/* Keywords (if, else, return, function, etc.) */ +.shiki-keyword { + color: hsl(var(--syntax-keyword)); +} + +/* Constants and numbers */ +.shiki-constant { + color: hsl(var(--syntax-constant)); +} + +/* Punctuation */ +.shiki-punctuation { + color: hsl(var(--syntax-punctuation)); +} + +/* Tags (HTML/JSX) */ +.shiki-tag { + color: hsl(var(--syntax-tag)); +} + +/* Diff - deleted lines */ +.shiki-deleted { + color: hsl(var(--diff-deleted)); +} + +/* Diff - inserted lines */ +.shiki-inserted { + color: hsl(var(--diff-inserted)); +} + +/* ========================================================================== + Loading State + ========================================================================== */ + .shiki-loading { animation: shiki-pulse 1.5s ease-in-out infinite; } @@ -41,34 +103,38 @@ } } +/* ========================================================================== + Diff Block Backgrounds + ========================================================================== */ + /* Diff-specific styling - block-level backgrounds for inserted/deleted */ -/* These target the line spans that Shiki generates for diff content */ -.shiki-container .line:has(.diff.add), -.shiki-container .line:has([style*="color:#69db7c"]) { - background: rgba(52, 199, 89, 0.1); +.shiki-container .line:has(.shiki-deleted) { + background: hsl(var(--diff-deleted-bg)); display: block; margin: 0 -1rem; padding: 0 1rem; } -.shiki-container .line:has(.diff.remove), -.shiki-container .line:has([style*="color:#ff8787"]) { - background: rgba(255, 59, 48, 0.1); +.shiki-container .line:has(.shiki-inserted) { + background: hsl(var(--diff-inserted-bg)); display: block; margin: 0 -1rem; padding: 0 1rem; } -/* Hunk headers (@@ lines) - cyan background */ -.shiki-container .line:has([style*="color:#66d9ef"]) { - background: rgba(102, 217, 239, 0.08); +/* Hunk headers (@@ lines) */ +.shiki-container .line:has([class*="meta"]) { + background: hsl(var(--diff-meta-bg)); display: block; margin: 0 -1rem; padding: 0 1rem; font-weight: 600; } -/* Optional: Line numbers */ +/* ========================================================================== + Line Numbers (optional) + ========================================================================== */ + .shiki-container.line-numbers .line::before { content: attr(data-line); display: inline-block;