{language ? (
-
+
) : (
{
- if (!language) return null;
- const lang = language.toLowerCase();
-
- // Map common language names to Prism identifiers
- const languageMap: Record = {
- 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 (
{/* Header */}
@@ -195,29 +160,16 @@ export function Kind1337DetailRenderer({ event }: Kind1337DetailRendererProps) {
{/* Code Section */}
- {normalizedLanguage ? (
- <>
-
-
- >
- ) : (
-
-
- {event.content}
-
- )}
+
+
);
diff --git a/src/components/nostr/kinds/CodeSnippetRenderer.tsx b/src/components/nostr/kinds/CodeSnippetRenderer.tsx
index c542d59..d9ca145 100644
--- a/src/components/nostr/kinds/CodeSnippetRenderer.tsx
+++ b/src/components/nostr/kinds/CodeSnippetRenderer.tsx
@@ -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 = {
- 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) {
diff --git a/src/hooks/useHighlightedCode.ts b/src/hooks/useHighlightedCode.ts
new file mode 100644
index 0000000..85ad38f
--- /dev/null
+++ b/src/hooks/useHighlightedCode.ts
@@ -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 {code}
+ * return
+ */
+export function useHighlightedCode(
+ code: string,
+ language: string | null | undefined,
+): UseHighlightedCodeResult {
+ const [html, setHtml] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(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 };
+}
diff --git a/src/index.css b/src/index.css
index 0bbb5a2..cec4235 100644
--- a/src/index.css
+++ b/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 .
========================================================================== */
-@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;
}
diff --git a/src/lib/shiki.ts b/src/lib/shiki.ts
new file mode 100644
index 0000000..bc6c955
--- /dev/null
+++ b/src/lib/shiki.ts
@@ -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 | null = null;
+const loadedLanguages = new Set();
+
+/**
+ * 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 = {
+ // 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 {
+ 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 {
+ 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 {
+ 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 {
+ await getHighlighter();
+ await Promise.all(langs.map((l) => loadLanguage(normalizeLanguage(l))));
+}
diff --git a/src/styles/prism-theme.css b/src/styles/prism-theme.css
deleted file mode 100644
index a3f9dfe..0000000
--- a/src/styles/prism-theme.css
+++ /dev/null
@@ -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));
-}
diff --git a/src/styles/shiki-theme.css b/src/styles/shiki-theme.css
new file mode 100644
index 0000000..87a72da
--- /dev/null
+++ b/src/styles/shiki-theme.css
@@ -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;
+}