mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-16 17:48:34 +02:00
feat: migrate syntax highlighting from Prism.js to Shiki
Replace Prism.js with Shiki for syntax highlighting with several key improvements: - Lazy loading: Languages loaded on-demand via dynamic imports instead of bundling all upfront - Broader language support: 200+ TextMate grammars vs 11 statically imported - Singleton highlighter: Core languages (JS, TS, JSON, diff, bash) preloaded, others loaded on first use New files: - src/lib/shiki.ts: Shiki service with highlightCode(), normalizeLanguage(), language aliases - src/hooks/useHighlightedCode.ts: React hook for async highlighting with loading states - src/styles/shiki-theme.css: Grimoire dark theme matching previous minimalistic style Updated components: - SyntaxHighlight: Now uses Shiki with graceful loading/error states - CodeSnippetRenderer/DetailRenderer: Simplified, removed manual language mapping - MarkdownContent: Removed type casts, any language now supported Removed: - prismjs and @types/prismjs dependencies - src/styles/prism-theme.css
This commit is contained in:
733
package-lock.json
generated
733
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -70,7 +70,6 @@
|
||||
"js-yaml": "^4.1.1",
|
||||
"lucide-react": "latest",
|
||||
"media-chrome": "^4.17.2",
|
||||
"prismjs": "^1.30.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
@@ -82,6 +81,7 @@
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"shell-quote": "^1.8.3",
|
||||
"shiki": "^3.20.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tippy.js": "^6.3.7"
|
||||
@@ -93,7 +93,6 @@
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
|
||||
@@ -1,45 +1,24 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import Prism from "prismjs";
|
||||
|
||||
// Core languages
|
||||
import "prismjs/components/prism-diff";
|
||||
import "prismjs/components/prism-javascript";
|
||||
import "prismjs/components/prism-typescript";
|
||||
import "prismjs/components/prism-jsx";
|
||||
import "prismjs/components/prism-tsx";
|
||||
import "prismjs/components/prism-bash";
|
||||
import "prismjs/components/prism-json";
|
||||
import "prismjs/components/prism-markdown";
|
||||
import "prismjs/components/prism-css";
|
||||
import "prismjs/components/prism-python";
|
||||
import "prismjs/components/prism-yaml";
|
||||
import { useHighlightedCode } from "@/hooks/useHighlightedCode";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SyntaxHighlightProps {
|
||||
code: string;
|
||||
language:
|
||||
| "diff"
|
||||
| "javascript"
|
||||
| "typescript"
|
||||
| "jsx"
|
||||
| "tsx"
|
||||
| "bash"
|
||||
| "shell"
|
||||
| "json"
|
||||
| "markdown"
|
||||
| "css"
|
||||
| "python"
|
||||
| "yaml";
|
||||
language?: string | null;
|
||||
className?: string;
|
||||
showLineNumbers?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntax highlighting component using Prism.js
|
||||
* Matches Grimoire's dark theme using CSS custom properties
|
||||
* Syntax highlighting component using Shiki with lazy language loading
|
||||
*
|
||||
* Languages are loaded on-demand - the first render of a new language
|
||||
* will show a brief loading state while the grammar is fetched.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <SyntaxHighlight code={patchContent} language="diff" />
|
||||
* <SyntaxHighlight code={jsonStr} language="json" />
|
||||
* <SyntaxHighlight code={snippet} language="python" />
|
||||
* ```
|
||||
*/
|
||||
export function SyntaxHighlight({
|
||||
@@ -48,26 +27,45 @@ export function SyntaxHighlight({
|
||||
className = "",
|
||||
showLineNumbers = false,
|
||||
}: SyntaxHighlightProps) {
|
||||
const codeRef = useRef<HTMLElement>(null);
|
||||
const { html, loading, error } = useHighlightedCode(code, language);
|
||||
|
||||
// Normalize language aliases
|
||||
const normalizedLanguage = language === "shell" ? "bash" : language;
|
||||
// Loading state - show code without highlighting
|
||||
if (loading) {
|
||||
return (
|
||||
<pre
|
||||
className={cn(
|
||||
"shiki-loading overflow-x-auto max-w-full font-mono text-xs",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<code className="text-foreground/70">{code}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Check for browser environment (SSR safety)
|
||||
if (typeof window === "undefined" || !codeRef.current) return;
|
||||
|
||||
// Highlight the code element
|
||||
Prism.highlightElement(codeRef.current);
|
||||
}, [code, normalizedLanguage]);
|
||||
// Error state - fallback to plain code
|
||||
if (error || !html) {
|
||||
return (
|
||||
<pre
|
||||
className={cn(
|
||||
"overflow-x-auto max-w-full font-mono text-xs",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
// Render highlighted HTML
|
||||
return (
|
||||
<pre
|
||||
className={`language-${normalizedLanguage} ${showLineNumbers ? "line-numbers" : ""} overflow-x-auto max-w-full ${className}`.trim()}
|
||||
>
|
||||
<code ref={codeRef} className={`language-${normalizedLanguage}`}>
|
||||
{code}
|
||||
</code>
|
||||
</pre>
|
||||
<div
|
||||
className={cn(
|
||||
"shiki-container overflow-x-auto max-w-full [&_pre]:!bg-transparent [&_code]:text-xs [&_code]:font-mono",
|
||||
showLineNumbers && "line-numbers",
|
||||
className,
|
||||
)}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ function CodeBlock({
|
||||
return (
|
||||
<div className="relative my-4">
|
||||
{language ? (
|
||||
<SyntaxHighlight code={code} language={language as any} />
|
||||
<SyntaxHighlight code={code} language={language} />
|
||||
) : (
|
||||
<pre
|
||||
className={`bg-muted p-4 border border-border rounded overflow-x-auto max-w-full ${isSingleLine ? "" : "pr-12"}`}
|
||||
|
||||
@@ -80,41 +80,6 @@ export function Kind1337DetailRenderer({ event }: Kind1337DetailRendererProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// Normalize language to supported Prism languages
|
||||
const normalizedLanguage = useMemo(() => {
|
||||
if (!language) return null;
|
||||
const lang = language.toLowerCase();
|
||||
|
||||
// Map common language names to Prism identifiers
|
||||
const languageMap: Record<string, string> = {
|
||||
js: "javascript",
|
||||
ts: "typescript",
|
||||
py: "python",
|
||||
sh: "bash",
|
||||
shell: "bash",
|
||||
yml: "yaml",
|
||||
};
|
||||
|
||||
const mapped = languageMap[lang] || lang;
|
||||
|
||||
// Check if it's a supported language
|
||||
const supported = [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"jsx",
|
||||
"tsx",
|
||||
"bash",
|
||||
"json",
|
||||
"markdown",
|
||||
"css",
|
||||
"python",
|
||||
"yaml",
|
||||
"diff",
|
||||
];
|
||||
|
||||
return supported.includes(mapped) ? mapped : null;
|
||||
}, [language]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-6">
|
||||
{/* Header */}
|
||||
@@ -195,29 +160,16 @@ export function Kind1337DetailRenderer({ event }: Kind1337DetailRendererProps) {
|
||||
|
||||
{/* Code Section */}
|
||||
<div className="relative">
|
||||
{normalizedLanguage ? (
|
||||
<>
|
||||
<SyntaxHighlight
|
||||
code={event.content}
|
||||
language={normalizedLanguage as any}
|
||||
className="bg-muted p-4 pr-10 border border-border overflow-x-auto"
|
||||
/>
|
||||
<CodeCopyButton
|
||||
onCopy={handleCopyCode}
|
||||
copied={copied}
|
||||
label="Copy code"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<pre className="text-xs font-mono bg-muted p-4 pr-10 border border-border overflow-x-auto">
|
||||
<CodeCopyButton
|
||||
onCopy={handleCopyCode}
|
||||
copied={copied}
|
||||
label="Copy code"
|
||||
/>
|
||||
<code>{event.content}</code>
|
||||
</pre>
|
||||
)}
|
||||
<SyntaxHighlight
|
||||
code={event.content}
|
||||
language={language}
|
||||
className="bg-muted p-4 pr-10 border border-border overflow-x-auto"
|
||||
/>
|
||||
<CodeCopyButton
|
||||
onCopy={handleCopyCode}
|
||||
copied={copied}
|
||||
label="Copy code"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,58 +11,6 @@ import {
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { SyntaxHighlight } from "@/components/SyntaxHighlight";
|
||||
|
||||
// Map common language names to Prism-supported languages
|
||||
function mapLanguage(
|
||||
lang: string | null | undefined,
|
||||
):
|
||||
| "javascript"
|
||||
| "typescript"
|
||||
| "jsx"
|
||||
| "tsx"
|
||||
| "bash"
|
||||
| "json"
|
||||
| "markdown"
|
||||
| "css"
|
||||
| "python"
|
||||
| "yaml"
|
||||
| "diff" {
|
||||
if (!lang) return "javascript";
|
||||
|
||||
const normalized = lang.toLowerCase();
|
||||
|
||||
// Direct matches
|
||||
if (
|
||||
[
|
||||
"javascript",
|
||||
"typescript",
|
||||
"jsx",
|
||||
"tsx",
|
||||
"bash",
|
||||
"json",
|
||||
"markdown",
|
||||
"css",
|
||||
"python",
|
||||
"yaml",
|
||||
"diff",
|
||||
].includes(normalized)
|
||||
) {
|
||||
return normalized as any;
|
||||
}
|
||||
|
||||
// Common aliases
|
||||
const aliases: Record<string, string> = {
|
||||
js: "javascript",
|
||||
ts: "typescript",
|
||||
sh: "bash",
|
||||
shell: "bash",
|
||||
py: "python",
|
||||
md: "markdown",
|
||||
yml: "yaml",
|
||||
};
|
||||
|
||||
return (aliases[normalized] as any) || "javascript";
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderer for Kind 1337 - Code Snippet (NIP-C0)
|
||||
* Displays code snippet name, language, description, and preview in feed
|
||||
@@ -109,7 +57,7 @@ export function Kind1337Renderer({ event }: BaseEventProps) {
|
||||
<div className="relative">
|
||||
<SyntaxHighlight
|
||||
code={previewCode}
|
||||
language={mapLanguage(language)}
|
||||
language={language}
|
||||
className="overflow-x-auto bg-muted/30 p-3 border border-border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
51
src/hooks/useHighlightedCode.ts
Normal file
51
src/hooks/useHighlightedCode.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { highlightCode } from "@/lib/shiki";
|
||||
|
||||
interface UseHighlightedCodeResult {
|
||||
html: string | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to highlight code asynchronously with lazy language loading
|
||||
*
|
||||
* @example
|
||||
* const { html, loading } = useHighlightedCode(code, "typescript")
|
||||
* if (loading) return <pre>{code}</pre>
|
||||
* return <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
*/
|
||||
export function useHighlightedCode(
|
||||
code: string,
|
||||
language: string | null | undefined,
|
||||
): UseHighlightedCodeResult {
|
||||
const [html, setHtml] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
highlightCode(code, language)
|
||||
.then((result) => {
|
||||
if (!cancelled) {
|
||||
setHtml(result);
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) {
|
||||
setError(err instanceof Error ? err : new Error(String(err)));
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [code, language]);
|
||||
|
||||
return { html, loading, error };
|
||||
}
|
||||
631
src/index.css
631
src/index.css
@@ -1,8 +1,8 @@
|
||||
/* Tailwind CSS v4 - CSS-first configuration */
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Prism syntax highlighting theme */
|
||||
@import "./styles/prism-theme.css";
|
||||
/* Shiki syntax highlighting theme */
|
||||
@import "./styles/shiki-theme.css";
|
||||
|
||||
/* ==========================================================================
|
||||
Theme Configuration (@theme)
|
||||
@@ -45,562 +45,219 @@
|
||||
@keyframes skeleton-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Color Tokens
|
||||
These map runtime CSS variables (set by ThemeProvider) to Tailwind colors.
|
||||
The runtime variables use HSL values WITHOUT the hsl() wrapper.
|
||||
========================================================================== */
|
||||
|
||||
/* Core Colors */
|
||||
/* Colors - Map Tailwind utilities to CSS variables */
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
|
||||
/* Card */
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
|
||||
/* Popover */
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
|
||||
/* Primary */
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
|
||||
/* Secondary */
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
|
||||
/* Accent */
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
|
||||
/* Muted */
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
|
||||
/* Destructive */
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
|
||||
/* Form Elements */
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
|
||||
/* Status Colors */
|
||||
--color-success: hsl(var(--success));
|
||||
--color-warning: hsl(var(--warning));
|
||||
--color-info: hsl(var(--info));
|
||||
|
||||
/* Nostr-specific Colors */
|
||||
--color-zap: hsl(var(--zap));
|
||||
--color-live: hsl(var(--live));
|
||||
--color-highlight: hsl(var(--highlight));
|
||||
|
||||
/* Tooltip */
|
||||
--color-tooltip: hsl(var(--tooltip));
|
||||
--color-tooltip-foreground: hsl(var(--tooltip-foreground));
|
||||
--color-sidebar: hsl(var(--sidebar));
|
||||
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
|
||||
--color-sidebar-primary: hsl(var(--sidebar-primary));
|
||||
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
|
||||
--color-sidebar-accent: hsl(var(--sidebar-accent));
|
||||
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
|
||||
--color-sidebar-border: hsl(var(--sidebar-border));
|
||||
--color-sidebar-ring: hsl(var(--sidebar-ring));
|
||||
--color-chart-1: hsl(var(--chart-1));
|
||||
--color-chart-2: hsl(var(--chart-2));
|
||||
--color-chart-3: hsl(var(--chart-3));
|
||||
--color-chart-4: hsl(var(--chart-4));
|
||||
--color-chart-5: hsl(var(--chart-5));
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Runtime Theme Variables
|
||||
These are HSL values WITHOUT the hsl() wrapper, allowing alpha transparency.
|
||||
Set dynamically by ThemeProvider via applyTheme().
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
/* Core colors - light theme defaults (overridden by ThemeProvider) */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Status colors */
|
||||
--success: 142 76% 36%;
|
||||
--warning: 45 93% 47%;
|
||||
--info: 199 89% 48%;
|
||||
|
||||
/* Nostr-specific colors */
|
||||
--zap: 45 93% 40%;
|
||||
--live: 0 72% 45%;
|
||||
|
||||
/* UI highlight (active user, self-references) */
|
||||
--highlight: 25 90% 35%;
|
||||
|
||||
/* Tooltip colors */
|
||||
--tooltip: 222.2 47.4% 11.2%;
|
||||
--tooltip-foreground: 210 40% 98%;
|
||||
|
||||
/* 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%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 70%;
|
||||
--accent: 270 100% 70%;
|
||||
--accent-foreground: 222.2 84% 4.9%;
|
||||
--destructive: 0 75% 75%;
|
||||
--destructive-foreground: 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% 46%;
|
||||
--warning: 38 92% 60%;
|
||||
--info: 199 89% 58%;
|
||||
|
||||
/* Nostr-specific colors */
|
||||
--zap: 45 93% 58%;
|
||||
--live: 0 72% 51%;
|
||||
|
||||
/* UI highlight (active user, self-references) */
|
||||
--highlight: 27 96% 61%;
|
||||
|
||||
/* Tooltip colors */
|
||||
--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-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;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Custom Scrollbar Styling
|
||||
========================================================================== */
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--scrollbar-thumb)) hsl(var(--scrollbar-track));
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: hsl(var(--scrollbar-track));
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--scrollbar-thumb));
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsl(var(--scrollbar-thumb-hover));
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Base Layer - Global Styles
|
||||
Base Styles (@layer base)
|
||||
These are low-specificity foundational styles.
|
||||
========================================================================== */
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground font-mono;
|
||||
/* iOS PWA safe area insets for notch support */
|
||||
padding-top: env(safe-area-inset-top, 0);
|
||||
padding-right: env(safe-area-inset-right, 0);
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
padding-left: env(safe-area-inset-left, 0);
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Force hardware acceleration for smooth scrolling */
|
||||
.hardware-accelerated {
|
||||
transform: translateZ(0);
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Custom Utilities (v4 @utility syntax)
|
||||
Theme Variables
|
||||
Runtime CSS variables that power the design system.
|
||||
These are toggled by adding/removing the .dark class on <html>.
|
||||
========================================================================== */
|
||||
|
||||
@utility no-scrollbar {
|
||||
-ms-overflow-style: none !important;
|
||||
scrollbar-width: none !important;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--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%;
|
||||
--sidebar: 0 0% 98%;
|
||||
--sidebar-foreground: 0 0% 9%;
|
||||
--sidebar-primary: 0 0% 9%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 0 0% 94%;
|
||||
--sidebar-accent-foreground: 0 0% 9%;
|
||||
--sidebar-border: 0 0% 89%;
|
||||
--sidebar-ring: 0 0% 3.9%;
|
||||
}
|
||||
|
||||
@utility hide-scrollbar {
|
||||
-ms-overflow-style: none !important;
|
||||
scrollbar-width: none !important;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@utility text-grimoire-gradient {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgb(var(--gradient-1)),
|
||||
rgb(var(--gradient-2)),
|
||||
rgb(var(--gradient-3)),
|
||||
rgb(var(--gradient-4))
|
||||
);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--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%;
|
||||
--sidebar: 0 0% 5%;
|
||||
--sidebar-foreground: 0 0% 98%;
|
||||
--sidebar-primary: 0 0% 98%;
|
||||
--sidebar-primary-foreground: 0 0% 9%;
|
||||
--sidebar-accent: 0 0% 12%;
|
||||
--sidebar-accent-foreground: 0 0% 98%;
|
||||
--sidebar-border: 0 0% 14.9%;
|
||||
--sidebar-ring: 0 0% 83.1%;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Third-Party Component Overrides
|
||||
Component Styles
|
||||
========================================================================== */
|
||||
|
||||
/* react-medium-image-zoom theme customization */
|
||||
[data-rmiz-modal-overlay] {
|
||||
background-color: hsl(var(--background) / 0.92) !important;
|
||||
/* Custom scrollbar for dark theme */
|
||||
.dark ::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
[data-rmiz-modal-content] {
|
||||
box-shadow: 0 0 40px hsl(var(--foreground) / 0.2);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
React Mosaic Theme Customization
|
||||
========================================================================== */
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme {
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: hsl(var(--background));
|
||||
}
|
||||
|
||||
/* Smooth animations for window resizing and repositioning */
|
||||
/* Only animate during preset application, not manual resize/drag */
|
||||
body.animating-layout
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
|
||||
.mosaic-tile {
|
||||
transition:
|
||||
width 150ms cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
height 150ms cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
top 150ms cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
left 150ms cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
contain: layout; /* Isolate layout calculations for better performance */
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
|
||||
.mosaic-window
|
||||
.mosaic-window-toolbar {
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--muted));
|
||||
border: none;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
border-radius: 0;
|
||||
color: hsl(var(--foreground));
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
|
||||
.mosaic-window
|
||||
.mosaic-window-title {
|
||||
color: hsl(var(--foreground));
|
||||
font-family: inherit;
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(var(--muted-foreground) / 0.5);
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window {
|
||||
background: hsl(var(--background));
|
||||
outline: none;
|
||||
border-radius: 0 !important;
|
||||
/* Mosaic component overrides */
|
||||
.mosaic-root {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window * {
|
||||
border-radius: 0 !important;
|
||||
.mosaic-tile {
|
||||
margin: 2px !important;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window::before,
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window::after {
|
||||
display: none;
|
||||
.mosaic-window {
|
||||
border-radius: var(--radius) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme.bp4-dark .mosaic-window,
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme.bp4-dark .mosaic-preview {
|
||||
box-shadow: none;
|
||||
.mosaic-window-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
|
||||
.mosaic-window
|
||||
.mosaic-window-body {
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
.mosaic-window-body {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window-controls {
|
||||
color: hsl(var(--muted-foreground));
|
||||
.mosaic-split {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
|
||||
.mosaic-window-controls:hover {
|
||||
color: hsl(var(--foreground));
|
||||
.mosaic-split:hover {
|
||||
background: hsl(var(--primary) / 0.2) !important;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
|
||||
.mosaic-window-toolbar
|
||||
.separator {
|
||||
border-left: 1px solid hsl(var(--border));
|
||||
.mosaic-split.-row {
|
||||
margin: 0 -2px !important;
|
||||
width: 6px !important;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
|
||||
.mosaic-window-body-overlay {
|
||||
background: hsl(var(--background));
|
||||
.mosaic-split.-column {
|
||||
margin: -2px 0 !important;
|
||||
height: 6px !important;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-preview {
|
||||
background: hsl(var(--accent) / 0.3);
|
||||
border: 2px solid hsl(var(--primary));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-drop-target {
|
||||
border: 2px solid var(--border);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-split:hover {
|
||||
background: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-split.-row {
|
||||
width: 4px;
|
||||
margin: 0 -2px;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-split.-column {
|
||||
height: 4px;
|
||||
margin: -2px 0;
|
||||
}
|
||||
|
||||
/* Mobile: Wider split dividers for touch dragging */
|
||||
@media (max-width: 767px) {
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-split.-row {
|
||||
width: 12px;
|
||||
margin: 0 -6px;
|
||||
}
|
||||
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-split.-column {
|
||||
height: 12px;
|
||||
margin: -6px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Accessibility: Focus Indicators
|
||||
========================================================================== */
|
||||
|
||||
@layer base {
|
||||
/* Focus-visible for buttons and interactive elements */
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
[role="button"]:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Focus-visible for input elements */
|
||||
input:focus-visible,
|
||||
textarea:focus-visible,
|
||||
select:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
/* Focus-visible for command launcher items */
|
||||
[cmdk-item]:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Focus-visible for tab buttons */
|
||||
.tabbar-button:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
TipTap Editor Styles
|
||||
========================================================================== */
|
||||
|
||||
.ProseMirror {
|
||||
min-height: 1.25rem;
|
||||
line-height: 1.25rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror p {
|
||||
margin: 0;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
color: hsl(var(--muted-foreground));
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* Disable link navigation in editor - allow editing instead of clicking */
|
||||
.ProseMirror a {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Mention styles */
|
||||
.ProseMirror .mention {
|
||||
color: hsl(var(--primary));
|
||||
background-color: hsl(var(--primary) / 0.1);
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ProseMirror .mention:hover {
|
||||
background-color: hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
/* Emoji styles */
|
||||
.ProseMirror .emoji-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ProseMirror .emoji-image {
|
||||
height: 1.2em;
|
||||
width: auto;
|
||||
vertical-align: middle;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.ProseMirror .emoji-unicode {
|
||||
font-size: 1.1em;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Nostr event preview styles */
|
||||
.ProseMirror .nostr-event-preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background-color: hsl(var(--primary) / 0.1);
|
||||
border: 1px solid hsl(var(--primary) / 0.3);
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
vertical-align: middle;
|
||||
cursor: default;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.ProseMirror .nostr-event-preview:hover {
|
||||
background-color: hsl(var(--primary) / 0.15);
|
||||
}
|
||||
|
||||
/* Hide scrollbar in RichEditor */
|
||||
.rich-editor .ProseMirror {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
.rich-editor .ProseMirror::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari/Opera */
|
||||
/* Active split highlight */
|
||||
.mosaic-split:active {
|
||||
background: hsl(var(--primary) / 0.4) !important;
|
||||
}
|
||||
|
||||
375
src/lib/shiki.ts
Normal file
375
src/lib/shiki.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
import {
|
||||
createHighlighterCore,
|
||||
type HighlighterCore,
|
||||
type ThemeRegistration,
|
||||
} from "shiki/core";
|
||||
import { createOnigurumaEngine } from "shiki/engine/oniguruma";
|
||||
|
||||
// Singleton highlighter instance
|
||||
let highlighter: HighlighterCore | null = null;
|
||||
let highlighterPromise: Promise<HighlighterCore> | null = null;
|
||||
const loadedLanguages = new Set<string>();
|
||||
|
||||
/**
|
||||
* Grimoire dark theme matching current minimalistic Prism styles
|
||||
* Uses muted colors with primary accent for keywords/functions
|
||||
*/
|
||||
const grimoireTheme: ThemeRegistration = {
|
||||
name: "grimoire-dark",
|
||||
type: "dark",
|
||||
colors: {
|
||||
"editor.background": "#000000",
|
||||
"editor.foreground": "#e5e5e5",
|
||||
},
|
||||
tokenColors: [
|
||||
// Comments - muted
|
||||
{
|
||||
scope: ["comment", "punctuation.definition.comment"],
|
||||
settings: { foreground: "#6b7280" },
|
||||
},
|
||||
// Strings - muted but slightly emphasized
|
||||
{
|
||||
scope: ["string", "string.quoted"],
|
||||
settings: { foreground: "#9ca3af" },
|
||||
},
|
||||
// Keywords, operators - primary color
|
||||
{
|
||||
scope: [
|
||||
"keyword",
|
||||
"storage",
|
||||
"storage.type",
|
||||
"storage.modifier",
|
||||
"keyword.operator",
|
||||
"keyword.control",
|
||||
],
|
||||
settings: { foreground: "#a855f7" },
|
||||
},
|
||||
// Functions, methods - primary bold
|
||||
{
|
||||
scope: ["entity.name.function", "support.function", "meta.function-call"],
|
||||
settings: { foreground: "#a855f7", fontStyle: "bold" },
|
||||
},
|
||||
// Classes, types - primary bold
|
||||
{
|
||||
scope: [
|
||||
"entity.name.class",
|
||||
"entity.name.type",
|
||||
"support.class",
|
||||
"support.type",
|
||||
],
|
||||
settings: { foreground: "#a855f7", fontStyle: "bold" },
|
||||
},
|
||||
// Numbers, constants - primary
|
||||
{
|
||||
scope: [
|
||||
"constant",
|
||||
"constant.numeric",
|
||||
"constant.language",
|
||||
"constant.character",
|
||||
],
|
||||
settings: { foreground: "#a855f7" },
|
||||
},
|
||||
// Variables, parameters - foreground
|
||||
{
|
||||
scope: ["variable", "variable.parameter", "variable.other"],
|
||||
settings: { foreground: "#e5e5e5" },
|
||||
},
|
||||
// Punctuation - slightly muted
|
||||
{
|
||||
scope: ["punctuation", "meta.brace"],
|
||||
settings: { foreground: "#b3b3b3" },
|
||||
},
|
||||
// Properties, attributes
|
||||
{
|
||||
scope: [
|
||||
"variable.other.property",
|
||||
"entity.other.attribute-name",
|
||||
"support.type.property-name",
|
||||
],
|
||||
settings: { foreground: "#d4d4d4" },
|
||||
},
|
||||
// Tags (HTML/JSX)
|
||||
{
|
||||
scope: ["entity.name.tag", "support.class.component"],
|
||||
settings: { foreground: "#a855f7" },
|
||||
},
|
||||
// JSON keys
|
||||
{
|
||||
scope: ["support.type.property-name.json"],
|
||||
settings: { foreground: "#d4d4d4" },
|
||||
},
|
||||
// Diff - deleted (red)
|
||||
{
|
||||
scope: [
|
||||
"markup.deleted",
|
||||
"punctuation.definition.deleted",
|
||||
"meta.diff.header.from-file",
|
||||
],
|
||||
settings: { foreground: "#ff8787" },
|
||||
},
|
||||
// Diff - inserted (green)
|
||||
{
|
||||
scope: [
|
||||
"markup.inserted",
|
||||
"punctuation.definition.inserted",
|
||||
"meta.diff.header.to-file",
|
||||
],
|
||||
settings: { foreground: "#69db7c" },
|
||||
},
|
||||
// Diff - changed/range
|
||||
{
|
||||
scope: ["markup.changed", "meta.diff.range", "meta.diff.header"],
|
||||
settings: { foreground: "#66d9ef" },
|
||||
},
|
||||
// Markdown headings
|
||||
{
|
||||
scope: ["markup.heading", "entity.name.section"],
|
||||
settings: { foreground: "#a855f7", 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: "#a855f7" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Language alias mapping (file extensions and common names to Shiki IDs)
|
||||
*/
|
||||
const LANGUAGE_ALIASES: Record<string, string> = {
|
||||
// JavaScript family
|
||||
ts: "typescript",
|
||||
tsx: "tsx",
|
||||
js: "javascript",
|
||||
jsx: "jsx",
|
||||
mjs: "javascript",
|
||||
cjs: "javascript",
|
||||
// Python
|
||||
py: "python",
|
||||
pyw: "python",
|
||||
// Ruby
|
||||
rb: "ruby",
|
||||
// Rust
|
||||
rs: "rust",
|
||||
// Go
|
||||
go: "go",
|
||||
// Shell
|
||||
sh: "bash",
|
||||
bash: "bash",
|
||||
shell: "bash",
|
||||
zsh: "bash",
|
||||
fish: "fish",
|
||||
// Config/Data
|
||||
yml: "yaml",
|
||||
yaml: "yaml",
|
||||
toml: "toml",
|
||||
ini: "ini",
|
||||
// JSON
|
||||
json: "json",
|
||||
jsonc: "jsonc",
|
||||
json5: "json5",
|
||||
// Markdown
|
||||
md: "markdown",
|
||||
mdx: "mdx",
|
||||
// CSS
|
||||
css: "css",
|
||||
scss: "scss",
|
||||
sass: "sass",
|
||||
less: "less",
|
||||
// HTML/XML
|
||||
html: "html",
|
||||
htm: "html",
|
||||
xml: "xml",
|
||||
svg: "xml",
|
||||
// SQL
|
||||
sql: "sql",
|
||||
// C family
|
||||
c: "c",
|
||||
h: "c",
|
||||
cpp: "cpp",
|
||||
"c++": "cpp",
|
||||
cc: "cpp",
|
||||
cxx: "cpp",
|
||||
hpp: "cpp",
|
||||
hxx: "cpp",
|
||||
// C#
|
||||
cs: "csharp",
|
||||
csharp: "csharp",
|
||||
// Java/JVM
|
||||
java: "java",
|
||||
kt: "kotlin",
|
||||
kotlin: "kotlin",
|
||||
scala: "scala",
|
||||
groovy: "groovy",
|
||||
// Apple
|
||||
swift: "swift",
|
||||
objc: "objective-c",
|
||||
// PHP
|
||||
php: "php",
|
||||
// Lua
|
||||
lua: "lua",
|
||||
// Vim
|
||||
vim: "viml",
|
||||
// Docker
|
||||
dockerfile: "dockerfile",
|
||||
docker: "dockerfile",
|
||||
// Make
|
||||
makefile: "makefile",
|
||||
make: "makefile",
|
||||
// Diff/Patch
|
||||
diff: "diff",
|
||||
patch: "diff",
|
||||
// Blockchain
|
||||
sol: "solidity",
|
||||
solidity: "solidity",
|
||||
// Zig
|
||||
zig: "zig",
|
||||
// Functional
|
||||
ex: "elixir",
|
||||
exs: "elixir",
|
||||
erl: "erlang",
|
||||
hs: "haskell",
|
||||
ml: "ocaml",
|
||||
clj: "clojure",
|
||||
cljs: "clojure",
|
||||
// GraphQL
|
||||
graphql: "graphql",
|
||||
gql: "graphql",
|
||||
// Protocol Buffers
|
||||
proto: "protobuf",
|
||||
// Nix
|
||||
nix: "nix",
|
||||
// Terraform
|
||||
tf: "hcl",
|
||||
hcl: "hcl",
|
||||
// PowerShell
|
||||
ps1: "powershell",
|
||||
psm1: "powershell",
|
||||
// R
|
||||
r: "r",
|
||||
// Perl
|
||||
pl: "perl",
|
||||
pm: "perl",
|
||||
// LaTeX
|
||||
tex: "latex",
|
||||
latex: "latex",
|
||||
// WASM
|
||||
wat: "wasm",
|
||||
wasm: "wasm",
|
||||
};
|
||||
|
||||
/**
|
||||
* Core languages to preload (most commonly used in Grimoire)
|
||||
*/
|
||||
const CORE_LANGUAGES = [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"json",
|
||||
"diff",
|
||||
"bash",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Normalize language identifier to Shiki language ID
|
||||
*/
|
||||
export function normalizeLanguage(lang: string | null | undefined): string {
|
||||
if (!lang) return "text";
|
||||
const normalized = lang.toLowerCase().trim();
|
||||
return LANGUAGE_ALIASES[normalized] || normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create the singleton highlighter instance
|
||||
*/
|
||||
export async function getHighlighter(): Promise<HighlighterCore> {
|
||||
if (highlighter) return highlighter;
|
||||
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = createHighlighterCore({
|
||||
themes: [grimoireTheme],
|
||||
langs: [
|
||||
import("shiki/langs/javascript.mjs"),
|
||||
import("shiki/langs/typescript.mjs"),
|
||||
import("shiki/langs/json.mjs"),
|
||||
import("shiki/langs/diff.mjs"),
|
||||
import("shiki/langs/bash.mjs"),
|
||||
],
|
||||
engine: createOnigurumaEngine(import("shiki/wasm")),
|
||||
}).then((hl) => {
|
||||
highlighter = hl;
|
||||
CORE_LANGUAGES.forEach((l) => loadedLanguages.add(l));
|
||||
return hl;
|
||||
});
|
||||
}
|
||||
|
||||
return highlighterPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a language on demand
|
||||
*/
|
||||
async function loadLanguage(lang: string): Promise<boolean> {
|
||||
if (lang === "text" || loadedLanguages.has(lang)) return true;
|
||||
|
||||
const hl = await getHighlighter();
|
||||
|
||||
try {
|
||||
// Dynamic import for the language
|
||||
const langModule = await import(`shiki/langs/${lang}.mjs`);
|
||||
await hl.loadLanguage(langModule.default || langModule);
|
||||
loadedLanguages.add(lang);
|
||||
return true;
|
||||
} catch {
|
||||
// Language not available
|
||||
console.warn(
|
||||
`[shiki] Language "${lang}" not available, falling back to plaintext`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight code with lazy language loading
|
||||
* Returns HTML string
|
||||
*/
|
||||
export async function highlightCode(
|
||||
code: string,
|
||||
language: string | null | undefined,
|
||||
): Promise<string> {
|
||||
const lang = normalizeLanguage(language);
|
||||
const hl = await getHighlighter();
|
||||
|
||||
// Try to load the language if not already loaded
|
||||
const loaded = await loadLanguage(lang);
|
||||
const effectiveLang = loaded ? lang : "text";
|
||||
|
||||
return hl.codeToHtml(code, {
|
||||
lang: effectiveLang,
|
||||
theme: "grimoire-dark",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a language is loaded
|
||||
*/
|
||||
export function isLanguageLoaded(lang: string): boolean {
|
||||
return loadedLanguages.has(normalizeLanguage(lang));
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload languages (e.g., before rendering known content)
|
||||
*/
|
||||
export async function preloadLanguages(langs: string[]): Promise<void> {
|
||||
await getHighlighter();
|
||||
await Promise.all(langs.map((l) => loadLanguage(normalizeLanguage(l))));
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
/* Grimoire Prism Theme - Uses CSS theme variables */
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: hsl(var(--foreground));
|
||||
background: none;
|
||||
text-shadow: none;
|
||||
font-family: "Oxygen Mono", monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
tab-size: 4;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Diff-specific tokens */
|
||||
|
||||
/* Deleted lines (red) - subtle background, no strikethrough */
|
||||
.token.deleted {
|
||||
color: hsl(var(--diff-deleted));
|
||||
background: hsl(var(--diff-deleted-bg));
|
||||
display: block;
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Added lines (green) - subtle background */
|
||||
.token.inserted {
|
||||
color: hsl(var(--diff-inserted));
|
||||
background: hsl(var(--diff-inserted-bg));
|
||||
display: block;
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Hunk headers (@@ -1,5 +1,7 @@) - cyan/blue */
|
||||
.token.diff.coord,
|
||||
.token.coord {
|
||||
color: hsl(var(--diff-meta));
|
||||
background: hsl(var(--diff-meta-bg));
|
||||
display: block;
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* File headers (diff --git, ---, +++) */
|
||||
.token.diff.range,
|
||||
.token.prefix.unchanged,
|
||||
.language-diff .token.unchanged {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Prefix characters (+/-) */
|
||||
.language-diff .token.prefix {
|
||||
font-weight: 700;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* General syntax tokens */
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: hsl(var(--syntax-comment));
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: hsl(var(--syntax-punctuation));
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol {
|
||||
color: hsl(var(--syntax-property));
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin {
|
||||
color: hsl(var(--syntax-string));
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: hsl(var(--syntax-operator));
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: hsl(var(--syntax-keyword));
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: hsl(var(--syntax-function));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: hsl(var(--syntax-variable));
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Line highlighting */
|
||||
pre[class*="language-"] > code {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Optional: Line numbers support */
|
||||
.line-numbers .line-numbers-rows {
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.line-numbers-rows > span:before {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
81
src/styles/shiki-theme.css
Normal file
81
src/styles/shiki-theme.css
Normal file
@@ -0,0 +1,81 @@
|
||||
/* Shiki syntax highlighting - Grimoire dark theme overrides */
|
||||
|
||||
/* Base container styling */
|
||||
.shiki-container pre {
|
||||
background: transparent !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.shiki-container code {
|
||||
font-family: "Oxygen Mono", monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
/* Ensure shiki's inline styles don't conflict */
|
||||
.shiki-container .shiki {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.shiki-container .shiki code {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Loading state - subtle pulse animation */
|
||||
.shiki-loading {
|
||||
animation: shiki-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shiki-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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);
|
||||
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);
|
||||
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);
|
||||
display: block;
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Optional: Line numbers */
|
||||
.shiki-container.line-numbers .line::before {
|
||||
content: attr(data-line);
|
||||
display: inline-block;
|
||||
width: 2rem;
|
||||
margin-right: 1rem;
|
||||
text-align: right;
|
||||
color: hsl(var(--muted-foreground));
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
Reference in New Issue
Block a user