diff --git a/package-lock.json b/package-lock.json index 4934145..cf33d68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-shell": "^2.3.3", "@tiptap/extension-bubble-menu": "^3.14.0", - "@tiptap/extension-code-block-lowlight": "^3.14.0", + "@tiptap/extension-code-block": "^3.18.0", "@tiptap/extension-floating-menu": "^3.14.0", "@tiptap/extension-horizontal-rule": "^3.14.0", "@tiptap/extension-image": "^3.14.0", @@ -57,6 +57,7 @@ "cors": "^2.8.5", "express": "^5.0.1", "fs-extra": "^11.3.2", + "highlight.js": "^11.11.1", "i18next": "^25.7.2", "i18next-browser-languagedetector": "^8.2.0", "lowlight": "^3.3.0", @@ -6072,16 +6073,16 @@ } }, "node_modules/@tiptap/core": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.17.1.tgz", - "integrity": "sha512-f8hB9MzXqsuXoF9qXEDEH5Fb3VgwhEFMBMfk9EKN88l5adri6oM8mt2XOWVxVVssjpEW0177zXSLPKWzoS/vrw==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.18.0.tgz", + "integrity": "sha512-Gczd4GbK1DNgy/QUPElMVozoa0GW9mW8E31VIi7Q4a9PHHz8PcrxPmuWwtJ2q0PF8MWpOSLuBXoQTWaXZRPRnQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^3.17.1" + "@tiptap/pm": "^3.18.0" } }, "node_modules/@tiptap/extension-blockquote": { @@ -6154,34 +6155,17 @@ } }, "node_modules/@tiptap/extension-code-block": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.17.1.tgz", - "integrity": "sha512-h4i+Y/cN7nMi0Tmlp6V1w4dI7NTqrUFSr1W/vMqnq4vn+c6jvm35KubKU5ry/1qQp8KfndDA02BtVQiMx6DmpA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.17.1", - "@tiptap/pm": "^3.17.1" - } - }, - "node_modules/@tiptap/extension-code-block-lowlight": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.17.1.tgz", - "integrity": "sha512-qdFDob6efjYp5aWD19Ac1fTN14l3WQbrHbopGbNhruSkXVj5LccwIHS2dgStSLHVHQcoot3YMEWEnauzprN51w==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.18.0.tgz", + "integrity": "sha512-fCx1oT95ikGfoizw+XCjeglQxlLK4lWgUcB4Dcn5TdaCoFBQMEaZs7Q0jVajxxxULnyArkg60uarc1ac/IF2Hw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.17.1", - "@tiptap/extension-code-block": "^3.17.1", - "@tiptap/pm": "^3.17.1", - "highlight.js": "^11", - "lowlight": "^2 || ^3" + "@tiptap/core": "^3.18.0", + "@tiptap/pm": "^3.18.0" } }, "node_modules/@tiptap/extension-document": { @@ -6563,9 +6547,9 @@ } }, "node_modules/@tiptap/pm": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.17.1.tgz", - "integrity": "sha512-UyVLkN8axV/zop6Se2DCBJRu5DM21X0XEQvwEC5P/vk8eC9OcQZ3FLtxeYy2ZjpAZUzBGLw0/BGsmEip/n7olw==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.18.0.tgz", + "integrity": "sha512-8RoI5gW0xBVCsuxahpK8vx7onAw6k2/uR3hbGBBnH+HocDMaAZKot3nTyY546ij8ospIC1mnQ7k4BhVUZesZDQ==", "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.3.0", diff --git a/package.json b/package.json index edc741f..e0847f3 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-shell": "^2.3.3", "@tiptap/extension-bubble-menu": "^3.14.0", - "@tiptap/extension-code-block-lowlight": "^3.14.0", + "@tiptap/extension-code-block": "^3.18.0", "@tiptap/extension-floating-menu": "^3.14.0", "@tiptap/extension-horizontal-rule": "^3.14.0", "@tiptap/extension-image": "^3.14.0", @@ -108,6 +108,7 @@ "cors": "^2.8.5", "express": "^5.0.1", "fs-extra": "^11.3.2", + "highlight.js": "^11.11.1", "i18next": "^25.7.2", "i18next-browser-languagedetector": "^8.2.0", "lowlight": "^3.3.0", diff --git a/src/ui/src/components/TiptapMarkdown.jsx b/src/ui/src/components/TiptapMarkdown.jsx index c6e78e2..18c5c43 100644 --- a/src/ui/src/components/TiptapMarkdown.jsx +++ b/src/ui/src/components/TiptapMarkdown.jsx @@ -13,7 +13,7 @@ import { useEffect, useRef, useState, useCallback, memo, forwardRef, useImperativeHandle } from 'react'; import { useTranslation } from 'react-i18next'; -import { useEditor, EditorContent } from '@tiptap/react'; +import { useEditor, EditorContent, ReactNodeViewRenderer } from '@tiptap/react'; import { Extension, InputRule } from '@tiptap/core'; import StarterKit from '@tiptap/starter-kit'; import Link from '@tiptap/extension-link'; @@ -21,12 +21,11 @@ import Placeholder from '@tiptap/extension-placeholder'; import { Table, TableRow, TableCell, TableHeader } from '@tiptap/extension-table'; import TaskList from '@tiptap/extension-task-list'; import TaskItem from '@tiptap/extension-task-item'; -import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; +import CodeBlock from '@tiptap/extension-code-block'; import Typography from '@tiptap/extension-typography'; import TableOfContents from '@tiptap/extension-table-of-contents'; import Image from '@tiptap/extension-image'; import { Markdown } from 'tiptap-markdown'; -import { all, createLowlight } from 'lowlight'; import * as api from '../api'; // Custom extension to handle exiting lists on Enter in empty list items @@ -102,6 +101,33 @@ const ListExitExtension = Extension.create({ }, }); +// Handle Tab indentation inside code blocks +const CodeBlockTabExtension = Extension.create({ + name: 'codeBlockTab', + addKeyboardShortcuts() { + return { + Tab: ({ editor }) => { + if (!editor.isActive('codeBlock')) return false; + editor.commands.insertContent(' '); + return true; + }, + 'Shift-Tab': ({ editor }) => { + if (!editor.isActive('codeBlock')) return false; + const { state } = editor; + const { selection } = state; + if (!selection.empty) return true; + const pos = selection.from; + if (pos < 2) return true; + const textBefore = state.doc.textBetween(pos - 2, pos, '\0', '\0'); + if (textBefore === ' ') { + editor.commands.deleteRange({ from: pos - 2, to: pos }); + } + return true; + }, + }; + }, +}); + // Helpers for mixed nested lists (bullet under ordered, or ordered under bullet) const NestedListHelpers = Extension.create({ name: 'nestedListHelpers', @@ -292,15 +318,49 @@ import TiptapSlashMenu from '../editor/tiptap/SlashMenu'; import TiptapFloatingToolbar from '../editor/tiptap/FloatingToolbar'; import TiptapTableToolbar from '../editor/tiptap/TableToolbar'; import PageRefPicker from '../editor/tiptap/PageRefPicker'; +import TiptapCodeBlockComponent from '../editor/tiptap/CodeBlockComponent'; +import HighlightJsExtension from '../editor/tiptap/HighlightJsExtension'; import { buildIdeaRefUrl, parseIdeaRefUrl } from '../utils/ideaRef'; import IdeaRefBlock from '../editor/tiptap/IdeaRefBlock'; -// Create lowlight instance for syntax highlighting -const lowlight = createLowlight(all); - -// Use official CodeBlockLowlight with syntax highlighting (simplified) -const SimpleCodeBlock = CodeBlockLowlight.configure({ - lowlight, +// Use CodeBlock with custom NodeView for highlight.js rendering +const CustomCodeBlock = CodeBlock.extend({ + addNodeView() { + return ReactNodeViewRenderer(TiptapCodeBlockComponent); + }, + addKeyboardShortcuts() { + return { + Tab: () => { + if (!this.editor.isActive('codeBlock')) return false; + return this.editor.commands.command(({ tr, state }) => { + const { selection } = state; + tr.insertText(' ', selection.from, selection.to); + return true; + }); + }, + 'Shift-Tab': () => { + if (!this.editor.isActive('codeBlock')) return false; + return this.editor.commands.command(({ tr, state }) => { + const { selection } = state; + const { $from } = selection; + // Get the text before cursor in current line + const parent = $from.parent; + const offset = $from.parentOffset; + const textBefore = parent.textBetween(0, offset, '\n', '\n'); + const lastNewline = textBefore.lastIndexOf('\n'); + const lineStart = lastNewline === -1 ? 0 : lastNewline + 1; + const maybeSpaces = parent.textBetween(lineStart, lineStart + 2, '\n', '\n'); + if (maybeSpaces === ' ') { + const from = $from.start() + lineStart; + tr.delete(from, from + 2); + return true; + } + return true; // Handled but nothing to delete + }); + }, + }; + }, +}).configure({ HTMLAttributes: { class: 'tiptap-code-block', }, @@ -390,14 +450,16 @@ export const TiptapMarkdownEditor = forwardRef(function TiptapMarkdownEditor({ const editor = useEditor({ extensions: [ StarterKit.configure({ - codeBlock: false, // Use CodeBlockLowlight instead + codeBlock: false, // Use CodeBlock instead heading: { levels: [1, 2, 3, 4, 5, 6] }, link: false, // prevent duplicate link extension (we add CustomLink) }), MixedListSwitch, // allow typing "- " / "1. " to switch list type in-place NestedListHelpers, // allow mixed nested lists (bullet under ordered, etc.) ListExitExtension, // Handle exiting lists on Enter/Backspace in empty items - SimpleCodeBlock, + CodeBlockTabExtension, // Allow Tab indentation in code blocks + CustomCodeBlock, + HighlightJsExtension, IdeaRefBlock, CustomLink, Placeholder.configure({ @@ -456,6 +518,28 @@ export const TiptapMarkdownEditor = forwardRef(function TiptapMarkdownEditor({ attributes: { class: `tiptap-editor prose prose-sm max-w-none focus:outline-none ${className || ''}`, }, + // When copying from inside a code block, output plain code (no Markdown fences) + clipboardTextSerializer: (slice) => { + // Check if the slice contains only content from a single code block + let isOnlyCodeBlock = true; + let codeContent = ''; + slice.content.forEach((node) => { + if (node.type.name === 'codeBlock') { + codeContent += node.textContent; + } else if (node.type.name === 'text') { + // Text node directly in slice (selected part of code block) + codeContent += node.text || ''; + } else { + isOnlyCodeBlock = false; + } + }); + // If selection is only code block content, return plain text + if (isOnlyCodeBlock && codeContent) { + return codeContent; + } + // Otherwise, let default behavior handle it (Markdown serialization) + return null; + }, handleClick: (view, pos, event) => { // Handle idea reference block clicks const target = event.target; @@ -788,6 +872,8 @@ export const TiptapMarkdownEditor = forwardRef(function TiptapMarkdownEditor({ {/* Table Toolbar (using official BubbleMenu - auto shows when in table) */} {!readOnly && } + + {/* Page Reference Picker Modal */} {isPageRefOpen && ( { + acc[opt.value] = opt.label; + return acc; +}, {}); + +const normalizeLanguage = (value) => { + const raw = String(value || '').trim(); + if (!raw) return ''; + const key = raw.toLowerCase(); + return LANGUAGE_ALIASES[key] || key; +}; + /** - * React NodeView for code blocks (alternative to vanilla JS NodeView) + * React NodeView for code blocks. */ -function TiptapCodeBlockComponent({ node, updateAttributes, editor, deleteNode }) { - const { t } = useTranslation(); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [isCopied, setIsCopied] = useState(false); +function TiptapCodeBlockComponent({ node, updateAttributes, editor }) { + const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); const buttonRef = useRef(null); - const lang = node.attrs.language || ''; - const currentLangLabel = LANG_OPTIONS.find((o) => o.value === lang)?.label || lang || 'Plain Text'; + const rawLang = String(node.attrs.language || '').trim(); + const normalizedLang = normalizeLanguage(rawLang); + const langLabel = LANGUAGE_LABELS[normalizedLang] || rawLang || 'Plain Text'; + const activeLang = LANGUAGE_LABELS[normalizedLang] ? normalizedLang : rawLang; + + useEffect(() => { + if (!editor?.isEditable) return; + if (!rawLang) return; + if (normalizedLang && rawLang !== normalizedLang) { + updateAttributes({ language: normalizedLang }); + } + }, [editor?.isEditable, normalizedLang, rawLang, updateAttributes]); - // Close dropdown on outside click useEffect(() => { - if (!isDropdownOpen) return; + if (!isOpen) return; const handleClickOutside = (event) => { if ( dropdownRef.current && @@ -52,118 +102,73 @@ function TiptapCodeBlockComponent({ node, updateAttributes, editor, deleteNode } buttonRef.current && !buttonRef.current.contains(event.target) ) { - setIsDropdownOpen(false); + setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); - }, [isDropdownOpen]); + }, [isOpen]); const setLanguage = useCallback((value) => { - updateAttributes({ language: value || null }); - setIsDropdownOpen(false); + const next = normalizeLanguage(value); + updateAttributes({ language: next || null }); + setIsOpen(false); }, [updateAttributes]); - const copyToClipboard = useCallback(async () => { - try { - const text = node.textContent; - await writeClipboardText(text); - setIsCopied(true); - setTimeout(() => setIsCopied(false), 2000); - } catch (err) { - console.error('Copy failed:', err); - } - }, [node]); - - const handleDelete = useCallback(() => { - deleteNode(); - }, [deleteNode]); - return ( - - {/* Header */} +
{ + e.preventDefault(); + }} > - {/* Language Dropdown */}
- - {isDropdownOpen && ( + {isOpen && (
e.stopPropagation()} + className="code-block-lang-menu" + onMouseDown={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} > - {LANG_OPTIONS.map((o) => ( + {LANG_OPTIONS.map((opt) => ( ))}
)}
- - {/* Actions */} -
- - - -
- - {/* Code Content */} -
-        
-      
+
+
+          
+        
+
); } diff --git a/src/ui/src/editor/tiptap/HighlightJsExtension.js b/src/ui/src/editor/tiptap/HighlightJsExtension.js new file mode 100644 index 0000000..9c6ea05 --- /dev/null +++ b/src/ui/src/editor/tiptap/HighlightJsExtension.js @@ -0,0 +1,109 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; +import { Decoration, DecorationSet } from '@tiptap/pm/view'; +import hljs from 'highlight.js/lib/common'; + +const LANGUAGE_ALIASES = { + js: 'javascript', + jsx: 'jsx', + ts: 'typescript', + tsx: 'tsx', + py: 'python', + sh: 'bash', + shell: 'bash', + zsh: 'bash', + yml: 'yaml', + md: 'markdown', + html: 'html', + json: 'json', + c: 'c', + 'c++': 'cpp', + cpp: 'cpp', + 'c#': 'csharp', + cs: 'csharp', + docker: 'dockerfile', +}; + +const normalizeLanguage = (value) => { + const raw = String(value || '').trim(); + if (!raw) return ''; + const key = raw.toLowerCase(); + return LANGUAGE_ALIASES[key] || key; +}; + +const buildDecorations = (doc) => { + const decorations = []; + + doc.descendants((node, pos) => { + if (node.type.name !== 'codeBlock') return; + const text = node.textContent || ''; + if (!text) return; + + const lang = normalizeLanguage(node.attrs.language); + let html = ''; + try { + if (lang && hljs.getLanguage(lang)) { + html = hljs.highlight(text, { language: lang, ignoreIllegals: true }).value; + } else { + html = hljs.highlightAuto(text).value; + } + } catch { + html = ''; + } + + if (!html) return; + + const container = document.createElement('div'); + container.innerHTML = html; + + let offset = 0; + const walk = (el, activeClasses) => { + if (el.nodeType === 3) { + const value = el.nodeValue || ''; + const len = value.length; + if (len > 0 && activeClasses.length) { + decorations.push( + Decoration.inline(pos + 1 + offset, pos + 1 + offset + len, { + class: activeClasses.join(' '), + }) + ); + } + offset += len; + return; + } + if (el.nodeType !== 1) return; + const classList = String(el.className || '') + .split(/\s+/) + .filter(Boolean); + const nextClasses = classList.length ? activeClasses.concat(classList) : activeClasses; + el.childNodes.forEach((child) => walk(child, nextClasses)); + }; + + container.childNodes.forEach((child) => walk(child, [])); + }); + + return DecorationSet.create(doc, decorations); +}; + +const HighlightJsExtension = Extension.create({ + name: 'highlightjs', + + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey('highlightjs'), + state: { + init: (_, { doc }) => buildDecorations(doc), + apply: (tr, old) => (tr.docChanged ? buildDecorations(tr.doc) : old), + }, + props: { + decorations(state) { + return this.getState(state); + }, + }, + }), + ]; + }, +}); + +export default HighlightJsExtension; diff --git a/src/ui/src/index.css b/src/ui/src/index.css index b340d50..0c966b5 100644 --- a/src/ui/src/index.css +++ b/src/ui/src/index.css @@ -115,6 +115,87 @@ body { .hljs-variable { color: #e36209; } +.hljs-function, +.hljs-title.function_ { + color: #6f42c1; +} +.hljs-built_in, +.hljs-class .hljs-title { + color: #6f42c1; +} +.hljs-type, +.hljs-params { + color: #005cc5; +} +.hljs-regexp { + color: #032f62; +} +.hljs-tag { + color: #22863a; +} +.hljs-attr { + color: #6f42c1; +} +.hljs-punctuation { + color: #24292e; +} + +/* --- Code block syntax highlighting (Dark mode - GitHub Dark theme inspired) --- */ +.dark .hljs-comment, +.dark .hljs-quote { + color: #8b949e; + font-style: italic; +} +.dark .hljs-keyword, +.dark .hljs-selector-tag, +.dark .hljs-literal, +.dark .hljs-name { + color: #ff7b72; +} +.dark .hljs-string, +.dark .hljs-title, +.dark .hljs-section, +.dark .hljs-attribute, +.dark .hljs-symbol, +.dark .hljs-bullet, +.dark .hljs-addition { + color: #a5d6ff; +} +.dark .hljs-number, +.dark .hljs-meta, +.dark .hljs-link { + color: #79c0ff; +} +.dark .hljs-deletion { + color: #ffa198; +} +.dark .hljs-variable { + color: #ffa657; +} +.dark .hljs-function, +.dark .hljs-title.function_ { + color: #d2a8ff; +} +.dark .hljs-built_in, +.dark .hljs-class .hljs-title { + color: #d2a8ff; +} +.dark .hljs-type, +.dark .hljs-params { + color: #79c0ff; +} +.dark .hljs-regexp { + color: #a5d6ff; +} +.dark .hljs-tag { + color: #7ee787; +} +.dark .hljs-attr { + color: #d2a8ff; +} +.dark .hljs-punctuation { + color: #c9d1d9; +} /* --- Search Modal Animations --- */ @keyframes fade-in { @@ -404,7 +485,7 @@ body { margin-left: 1.1rem; } -/* Tiptap code block styles (using official CodeBlockLowlight) */ +/* Tiptap code block styles */ .tiptap-editor pre, .tiptap-viewer pre { @apply my-4 rounded-lg border border-gray-200 bg-gray-50 overflow-hidden; @@ -417,11 +498,132 @@ body { /* Code block with language class */ .tiptap-code-block { - @apply my-4 rounded-lg border border-gray-200 bg-gray-50 overflow-hidden; + @apply my-4 rounded-lg border border-gray-200 bg-gray-50 overflow-visible; } .tiptap-code-block code { - @apply block p-4 overflow-x-auto text-sm font-mono leading-6 text-gray-800 bg-transparent; + @apply font-mono text-sm leading-6 bg-transparent p-0; +} + +/* Reset pre styles inside code block component */ +.tiptap-code-block .code-pre { + margin: 0; + padding: 0.25rem 0.6rem; + border: none; + border-radius: 0; + background: transparent; + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + font-size: 0.875rem; + line-height: 1.35; + white-space: pre; + overflow-x: auto; + font-variant-ligatures: none; + font-feature-settings: "liga" 0; + letter-spacing: 0; +} + +.tiptap-code-block .code-pre code { + display: block; + font-family: inherit; + font-size: inherit; + line-height: inherit; + white-space: inherit; + background: transparent; + padding: 0; + margin: 0; + font-variant-ligatures: inherit; + font-feature-settings: inherit; +} + +.tiptap-code-block .code-editor-layer { + display: block; + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace !important; + font-size: 0.875rem !important; + line-height: 1.5rem !important; + white-space: pre !important; +} + +/* Keep highlight layer metrics identical to editor layer */ +.tiptap-code-block .hljs, +.tiptap-code-block .hljs * { + font-style: normal; + font-weight: 400; + font-variant-ligatures: inherit; + font-feature-settings: inherit; + letter-spacing: 0; + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace !important; + font-size: 0.875rem !important; + line-height: 1.5rem !important; + white-space: pre !important; +} + +/* Ensure selection remains visible in code editor layer */ +.tiptap-code-block .code-editor-layer { + color: inherit; + caret-color: #0f172a; +} + +.dark .tiptap-code-block .code-editor-layer { + caret-color: #e2e8f0; +} + +.tiptap-code-block .code-editor-layer::selection { + background: rgba(59, 130, 246, 0.25); +} + +.tiptap-code-block .code-block-header { + @apply flex items-center px-3 py-1.5 border-b border-gray-200 bg-gray-50 text-xs font-medium text-gray-600 rounded-t-lg; + position: relative; + z-index: 2; +} + +.dark .tiptap-code-block .code-block-header { + @apply border-zinc-800 bg-zinc-900 text-zinc-300; +} + +.tiptap-code-block .code-block-content { + @apply rounded-b-lg; + background: inherit; +} + +.tiptap-code-block .code-block-lang-btn { + @apply inline-flex items-center gap-1.5 px-2 py-0.5 rounded bg-gray-100 text-gray-600 hover:bg-gray-200 transition-colors; +} + +.dark .tiptap-code-block .code-block-lang-btn { + @apply bg-zinc-800 text-zinc-200 hover:bg-zinc-700; +} + +.tiptap-code-block .code-block-lang-menu { + @apply absolute left-0 mt-1 w-44 max-h-64 overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg z-50 py-1; +} + +.dark .tiptap-code-block .code-block-lang-menu { + @apply border-zinc-700 bg-zinc-900; +} + +.tiptap-code-block .code-block-lang-item { + @apply w-full text-left px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 hover:text-gray-900; +} + +.dark .tiptap-code-block .code-block-lang-item { + @apply text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100; +} + +.tiptap-code-block .code-block-lang-item.is-active { + @apply bg-blue-50 text-blue-600 font-medium; +} + +.dark .tiptap-code-block .code-block-lang-item.is-active { + @apply bg-blue-900/30 text-blue-400; +} + +.hljs { + color: #24292e; +} + +.dark .hljs { + color: #c9d1d9; } /* Inline code */