mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 23:47:12 +02:00
fix: improve dark theme contrast for syntax highlighting
- Add CSS variables for syntax highlighting instead of hardcoded colors - Add --syntax-constant and --syntax-tag variables to light and dark themes - Use high contrast colors for dark mode (bright green strings, purple keywords) - Simplify Shiki transformer to output CSS classes instead of inline styles - Remove unused parameters from transformer callback
This commit is contained in:
@@ -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%;
|
||||
|
||||
247
src/lib/shiki.ts
247
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<string>();
|
||||
const failedLanguages = new Set<string>();
|
||||
|
||||
/**
|
||||
* 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<HighlighterCore> {
|
||||
|
||||
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<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user