From c2727bab2863a8d630a4aa1f13ad9ed90e90153b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Jan 2026 08:54:55 +0000 Subject: [PATCH] refactor: improve git tree and syntax highlighting - Remove shallow clone fallback from useGitTree, only use no-blobs fetch Servers without filter capability are skipped instead of downloading blobs - Add light theme support for Shiki syntax highlighting Theme is automatically selected based on current color scheme --- src/hooks/useGitTree.ts | 29 +- src/index.css | 627 +++++++++++++++++++++++++++++++--------- src/lib/shiki.ts | 150 +++++++++- 3 files changed, 648 insertions(+), 158 deletions(-) diff --git a/src/hooks/useGitTree.ts b/src/hooks/useGitTree.ts index 6ad5a60..fdad32b 100644 --- a/src/hooks/useGitTree.ts +++ b/src/hooks/useGitTree.ts @@ -2,7 +2,6 @@ import { useState, useEffect, useCallback } from "react"; import { getInfoRefs, getDirectoryTreeAt, - shallowCloneRepositoryAt, MissingCapability, } from "@fiatjaf/git-natural-api"; import type { DirectoryTree } from "@/lib/git-types"; @@ -33,8 +32,8 @@ interface UseGitTreeResult { * Hook to fetch a git repository tree from clone URLs * * Tries each clone URL in sequence until one succeeds. - * Uses the lightweight `getDirectoryTreeAt` if the server supports filtering, - * otherwise falls back to `shallowCloneRepositoryAt`. + * Uses the lightweight `getDirectoryTreeAt` which requires filter capability. + * Servers without filter support are skipped. * * @example * const { tree, loading, error } = useGitTree({ @@ -69,7 +68,18 @@ export function useGitTree({ try { // Get server info to check capabilities and resolve refs const info = await getInfoRefs(url); - const hasFilter = info.capabilities.includes("filter"); + + // Only use servers that support filter capability (lightweight fetch) + // Skip servers that would require downloading all blobs + if (!info.capabilities.includes("filter")) { + console.warn( + `[useGitTree] Server ${url} doesn't support filter capability, skipping`, + ); + errors.push( + new MissingCapability("filter", "Server doesn't support filter"), + ); + continue; + } // Resolve the ref to a commit hash let resolvedRef = ref; @@ -92,15 +102,8 @@ export function useGitTree({ } } - // Fetch the tree - let fetchedTree: DirectoryTree; - if (hasFilter) { - // Server supports filter - use lightweight fetch (tree only, no blobs) - fetchedTree = await getDirectoryTreeAt(url, resolvedRef); - } else { - // No filter support - need to do shallow clone - fetchedTree = await shallowCloneRepositoryAt(url, resolvedRef); - } + // Fetch the tree using lightweight filter (tree only, no blobs) + const fetchedTree = await getDirectoryTreeAt(url, resolvedRef); setTree(fetchedTree); setServerUrl(url); diff --git a/src/index.css b/src/index.css index cec4235..fc9891d 100644 --- a/src/index.css +++ b/src/index.css @@ -45,219 +45,562 @@ @keyframes skeleton-pulse { 0%, 100% { - opacity: 0.4; + opacity: 1; } 50% { - opacity: 0.7; + opacity: 0.5; } } - /* Colors - Map Tailwind utilities to CSS variables */ + /* ========================================================================== + 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 */ --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)); - --color-muted: hsl(var(--muted)); - --color-muted-foreground: hsl(var(--muted-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-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)); - --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)); + + /* 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)); } /* ========================================================================== - Base Styles (@layer base) - These are low-specificity foundational styles. + 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 ========================================================================== */ @layer base { * { @apply border-border; } - body { - @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; + @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); } } /* ========================================================================== - Theme Variables - Runtime CSS variables that power the design system. - These are toggled by adding/removing the .dark class on . + Custom Utilities (v4 @utility syntax) ========================================================================== */ -: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 no-scrollbar { + -ms-overflow-style: none !important; + scrollbar-width: none !important; + + &::-webkit-scrollbar { + display: none !important; + width: 0 !important; + height: 0 !important; + } } -.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%; +@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; } /* ========================================================================== - Component Styles + Third-Party Component Overrides ========================================================================== */ -/* Custom scrollbar for dark theme */ -.dark ::-webkit-scrollbar { - width: 8px; - height: 8px; +/* react-medium-image-zoom theme customization */ +[data-rmiz-modal-overlay] { + background-color: hsl(var(--background) / 0.92) !important; } -.dark ::-webkit-scrollbar-track { +[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 { background: hsl(var(--background)); } -.dark ::-webkit-scrollbar-thumb { +/* 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 { background: hsl(var(--muted)); - border-radius: 4px; + border: none; + border-bottom: 1px solid hsl(var(--border)); + border-radius: 0; + color: hsl(var(--foreground)); + height: 30px; } -.dark ::-webkit-scrollbar-thumb:hover { - background: hsl(var(--muted-foreground) / 0.5); +.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme + .mosaic-window + .mosaic-window-title { + color: hsl(var(--foreground)); + font-family: inherit; } -/* Mosaic component overrides */ -.mosaic-root { - background: transparent !important; +.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window { + background: hsl(var(--background)); + outline: none; + border-radius: 0 !important; } -.mosaic-tile { - margin: 2px !important; +.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window * { + border-radius: 0 !important; } -.mosaic-window { - border-radius: var(--radius) !important; - overflow: hidden; +.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window::before, +.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window::after { + display: none; } -.mosaic-window-toolbar { - display: none !important; +.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-body { - background: transparent !important; +.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme + .mosaic-window + .mosaic-window-body { + background: hsl(var(--background)); + color: hsl(var(--foreground)); } -.mosaic-split { - background: transparent !important; +.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window-controls { + color: hsl(var(--muted-foreground)); } -.mosaic-split:hover { - background: hsl(var(--primary) / 0.2) !important; +.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme + .mosaic-window-controls:hover { + color: hsl(var(--foreground)); } -.mosaic-split.-row { - margin: 0 -2px !important; - width: 6px !important; +.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme + .mosaic-window-toolbar + .separator { + border-left: 1px solid hsl(var(--border)); } -.mosaic-split.-column { - margin: -2px 0 !important; - height: 6px !important; +.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme + .mosaic-window-body-overlay { + background: hsl(var(--background)); } -/* Active split highlight */ -.mosaic-split:active { - background: hsl(var(--primary) / 0.4) !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 */ } diff --git a/src/lib/shiki.ts b/src/lib/shiki.ts index 78ac8ce..06b3b92 100644 --- a/src/lib/shiki.ts +++ b/src/lib/shiki.ts @@ -14,7 +14,7 @@ const loadedLanguages = new Set(); * Grimoire dark theme - minimalistic grayscale with semantic colors * Uses muted grays for syntax with color only for diff semantics */ -const grimoireTheme: ThemeRegistration = { +const grimoireDarkTheme: ThemeRegistration = { name: "grimoire-dark", type: "dark", colors: { @@ -143,6 +143,138 @@ const grimoireTheme: 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) */ @@ -298,7 +430,7 @@ export async function getHighlighter(): Promise { if (!highlighterPromise) { highlighterPromise = createHighlighterCore({ - themes: [grimoireTheme], + themes: [grimoireDarkTheme, grimoireLightTheme], langs: [ import("shiki/langs/javascript.mjs"), import("shiki/langs/typescript.mjs"), @@ -342,9 +474,18 @@ async function loadLanguage(lang: string): Promise { } } +/** + * Detect if dark mode is currently active + */ +function isDarkMode(): boolean { + if (typeof document === "undefined") return true; + return document.documentElement.classList.contains("dark"); +} + /** * Highlight code with lazy language loading * Returns HTML string + * Automatically uses the appropriate theme based on current color scheme */ export async function highlightCode( code: string, @@ -357,9 +498,12 @@ 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: "grimoire-dark", + theme, }); }