import { Show, untrack, createEffect, on, createSignal, For } from "solid-js"; import { createTiptapEditor, useEditorHTML } from "solid-tiptap"; import StarterKit from "@tiptap/starter-kit"; import Link from "@tiptap/extension-link"; import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; import Image from "@tiptap/extension-image"; import { Table } from "@tiptap/extension-table"; import { TableRow } from "@tiptap/extension-table-row"; import { TableHeader } from "@tiptap/extension-table-header"; import { TableCell } from "@tiptap/extension-table-cell"; import TaskList from "@tiptap/extension-task-list"; import TaskItem from "@tiptap/extension-task-item"; import Details from "@tiptap/extension-details"; import DetailsSummary from "@tiptap/extension-details-summary"; import DetailsContent from "@tiptap/extension-details-content"; import { Node } from "@tiptap/core"; import { createLowlight, common } from "lowlight"; import { Mermaid } from "./extensions/Mermaid"; import { ConditionalBlock } from "./extensions/ConditionalBlock"; import { ConditionalInline } from "./extensions/ConditionalInline"; import TextAlign from "@tiptap/extension-text-align"; import Superscript from "@tiptap/extension-superscript"; import Subscript from "@tiptap/extension-subscript"; import css from "highlight.js/lib/languages/css"; import js from "highlight.js/lib/languages/javascript"; import ts from "highlight.js/lib/languages/typescript"; import ocaml from "highlight.js/lib/languages/ocaml"; import rust from "highlight.js/lib/languages/rust"; import python from "highlight.js/lib/languages/python"; import java from "highlight.js/lib/languages/java"; import go from "highlight.js/lib/languages/go"; import c from "highlight.js/lib/languages/c"; import cpp from "highlight.js/lib/languages/cpp"; import csharp from "highlight.js/lib/languages/csharp"; import sql from "highlight.js/lib/languages/sql"; import bash from "highlight.js/lib/languages/bash"; import json from "highlight.js/lib/languages/json"; import yaml from "highlight.js/lib/languages/yaml"; import markdown from "highlight.js/lib/languages/markdown"; import xml from "highlight.js/lib/languages/xml"; import php from "highlight.js/lib/languages/php"; import ruby from "highlight.js/lib/languages/ruby"; import swift from "highlight.js/lib/languages/swift"; import kotlin from "highlight.js/lib/languages/kotlin"; import dockerfile from "highlight.js/lib/languages/dockerfile"; const lowlight = createLowlight(common); lowlight.register("css", css); lowlight.register("js", js); lowlight.register("javascript", js); lowlight.register("ts", ts); lowlight.register("typescript", ts); lowlight.register("ocaml", ocaml); lowlight.register("rust", rust); lowlight.register("python", python); lowlight.register("py", python); lowlight.register("java", java); lowlight.register("go", go); lowlight.register("golang", go); lowlight.register("c", c); lowlight.register("cpp", cpp); lowlight.register("c++", cpp); lowlight.register("csharp", csharp); lowlight.register("cs", csharp); lowlight.register("sql", sql); lowlight.register("bash", bash); lowlight.register("shell", bash); lowlight.register("sh", bash); lowlight.register("json", json); lowlight.register("yaml", yaml); lowlight.register("yml", yaml); lowlight.register("markdown", markdown); lowlight.register("md", markdown); lowlight.register("xml", xml); lowlight.register("html", xml); lowlight.register("php", php); lowlight.register("ruby", ruby); lowlight.register("rb", ruby); lowlight.register("swift", swift); lowlight.register("kotlin", kotlin); lowlight.register("kt", kotlin); lowlight.register("dockerfile", dockerfile); lowlight.register("docker", dockerfile); const AVAILABLE_LANGUAGES = [ { value: null, label: "Plain Text" }, { value: "bash", label: "Bash/Shell" }, { value: "c", label: "C" }, { value: "cpp", label: "C++" }, { value: "csharp", label: "C#" }, { value: "css", label: "CSS" }, { value: "dockerfile", label: "Dockerfile" }, { value: "go", label: "Go" }, { value: "html", label: "HTML" }, { value: "java", label: "Java" }, { value: "javascript", label: "JavaScript" }, { value: "json", label: "JSON" }, { value: "kotlin", label: "Kotlin" }, { value: "markdown", label: "Markdown" }, { value: "ocaml", label: "OCaml" }, { value: "php", label: "PHP" }, { value: "python", label: "Python" }, { value: "ruby", label: "Ruby" }, { value: "rust", label: "Rust" }, { value: "sql", label: "SQL" }, { value: "swift", label: "Swift" }, { value: "typescript", label: "TypeScript" }, { value: "xml", label: "XML" }, { value: "yaml", label: "YAML" } ] as const; const MERMAID_TEMPLATES = [ { name: "Flowchart", code: `graph TD A[Start] --> B{Decision} B -->|Yes| C[Option 1] B -->|No| D[Option 2] C --> E[End] D --> E` }, { name: "Sequence Diagram", code: `sequenceDiagram participant A as Alice participant B as Bob A->>B: Hello Bob! B->>A: Hello Alice!` }, { name: "State Diagram", code: `stateDiagram-v2 [*] --> Idle Idle --> Processing Processing --> Done Done --> [*]` }, { name: "Class Diagram", code: `classDiagram class Animal { +String name +makeSound() } class Dog { +bark() } Animal <|-- Dog` }, { name: "Entity Relationship", code: `erDiagram CUSTOMER ||--o{ ORDER : places ORDER ||--|{ LINE-ITEM : contains CUSTOMER { string name string email }` }, { name: "Gantt Chart", code: `gantt title Project Timeline dateFormat YYYY-MM-DD section Phase 1 Task 1 :a1, 2024-01-01, 30d Task 2 :after a1, 20d` }, { name: "Pie Chart", code: `pie title Languages Used "JavaScript" : 45 "TypeScript" : 30 "Python" : 15 "Go" : 10` } ]; interface ShortcutCategory { name: string; shortcuts: Array<{ keys: string; keysAlt?: string; description: string; }>; } const KEYBOARD_SHORTCUTS: ShortcutCategory[] = [ { name: "Text Formatting", shortcuts: [ { keys: "⌘ B", keysAlt: "Ctrl B", description: "Bold" }, { keys: "⌘ I", keysAlt: "Ctrl I", description: "Italic" }, { keys: "⌘ ⇧ X", keysAlt: "Ctrl Shift X", description: "Strikethrough" }, { keys: "⌘ E", keysAlt: "Ctrl E", description: "Inline Code" }, { keys: "⌘ .", keysAlt: "Ctrl .", description: "Superscript" }, { keys: "⌘ ,", keysAlt: "Ctrl ,", description: "Subscript" } ] }, { name: "Headings", shortcuts: [ { keys: "⌘ ⌥ 1", keysAlt: "Ctrl Alt 1", description: "Heading 1" }, { keys: "⌘ ⌥ 2", keysAlt: "Ctrl Alt 2", description: "Heading 2" }, { keys: "⌘ ⌥ 3", keysAlt: "Ctrl Alt 3", description: "Heading 3" }, { keys: "⌘ ⌥ 0", keysAlt: "Ctrl Alt 0", description: "Paragraph" } ] }, { name: "Lists", shortcuts: [ { keys: "⌘ ⇧ 7", keysAlt: "Ctrl Shift 7", description: "Ordered List" }, { keys: "⌘ ⇧ 8", keysAlt: "Ctrl Shift 8", description: "Bullet List" }, { keys: "⌘ ⇧ 9", keysAlt: "Ctrl Shift 9", description: "Task List" }, { keys: "Tab", keysAlt: "Tab", description: "Indent" }, { keys: "⇧ Tab", keysAlt: "Shift Tab", description: "Outdent" } ] }, { name: "Text Alignment", shortcuts: [ { keys: "⌘ ⇧ L", keysAlt: "Ctrl Shift L", description: "Align Left" }, { keys: "⌘ ⇧ E", keysAlt: "Ctrl Shift E", description: "Align Center" }, { keys: "⌘ ⇧ R", keysAlt: "Ctrl Shift R", description: "Align Right" }, { keys: "⌘ ⇧ J", keysAlt: "Ctrl Shift J", description: "Justify" } ] }, { name: "Insert", shortcuts: [ { keys: "⌘ K", keysAlt: "Ctrl K", description: "Insert/Edit Link" }, { keys: "⌘ ⇧ C", keysAlt: "Ctrl Shift C", description: "Code Block" }, { keys: "⌘ Enter", keysAlt: "Ctrl Enter", description: "Hard Break" }, { keys: "⌘ ⇧ -", keysAlt: "Ctrl Shift -", description: "Horizontal Rule" } ] }, { name: "Editing", shortcuts: [ { keys: "⌘ Z", keysAlt: "Ctrl Z", description: "Undo" }, { keys: "⌘ ⇧ Z", keysAlt: "Ctrl Shift Z", description: "Redo" }, { keys: "⌘ Y", keysAlt: "Ctrl Y", description: "Redo (Alt)" }, { keys: "⌘ A", keysAlt: "Ctrl A", description: "Select All" } ] }, { name: "Other", shortcuts: [ { keys: "⌘ ⇧ \\", keysAlt: "Ctrl Shift \\", description: "Clear Formatting" }, { keys: "ESC", keysAlt: "ESC", description: "Exit Fullscreen" } ] } ]; const isMac = () => { return ( typeof window !== "undefined" && /Mac|iPhone|iPad|iPod/.test(window.navigator.platform) ); }; interface IframeOptions { allowFullscreen: boolean; HTMLAttributes: { [key: string]: any; }; } declare module "@tiptap/core" { interface Commands { iframe: { setIframe: (options: { src: string }) => ReturnType; }; } } const IframeEmbed = Node.create({ name: "iframe", group: "block", atom: true, addOptions() { return { allowFullscreen: true, HTMLAttributes: { class: "iframe-wrapper" } }; }, addAttributes() { return { src: { default: null }, frameborder: { default: 0 }, allowfullscreen: { default: this.options.allowFullscreen, parseHTML: () => this.options.allowFullscreen } }; }, parseHTML() { return [ { tag: "iframe" } ]; }, renderHTML({ HTMLAttributes }) { return ["div", this.options.HTMLAttributes, ["iframe", HTMLAttributes]]; }, addCommands() { return { setIframe: (options: { src: string }) => ({ tr, dispatch }) => { const { selection } = tr; const node = this.type.create(options); if (dispatch) { tr.replaceRangeWith(selection.from, selection.to, node); } return true; } }; } }); export interface TextEditorProps { updateContent: (content: string) => void; preSet?: string; } export default function TextEditor(props: TextEditorProps) { let editorRef!: HTMLDivElement; let bubbleMenuRef!: HTMLDivElement; let containerRef!: HTMLDivElement; const [showBubbleMenu, setShowBubbleMenu] = createSignal(false); const [bubbleMenuPosition, setBubbleMenuPosition] = createSignal({ top: 0, left: 0 }); const [showLanguageSelector, setShowLanguageSelector] = createSignal(false); const [languageSelectorPosition, setLanguageSelectorPosition] = createSignal({ top: 0, left: 0 }); const [showTableMenu, setShowTableMenu] = createSignal(false); const [tableMenuPosition, setTableMenuPosition] = createSignal({ top: 0, left: 0 }); const [showMermaidTemplates, setShowMermaidTemplates] = createSignal(false); const [mermaidMenuPosition, setMermaidMenuPosition] = createSignal({ top: 0, left: 0 }); const [showKeyboardHelp, setShowKeyboardHelp] = createSignal(false); const [showConditionalConfig, setShowConditionalConfig] = createSignal(false); const [conditionalConfigPosition, setConditionalConfigPosition] = createSignal({ top: 0, left: 0 }); const [conditionalForm, setConditionalForm] = createSignal<{ conditionType: "auth" | "privilege" | "date" | "feature" | "env"; conditionValue: string; showWhen: "true" | "false"; inline: boolean; // New field for inline vs block }>({ conditionType: "auth", conditionValue: "authenticated", showWhen: "true", inline: false }); const [isFullscreen, setIsFullscreen] = createSignal(false); const [keyboardVisible, setKeyboardVisible] = createSignal(false); const [keyboardHeight, setKeyboardHeight] = createSignal(0); const editor = createTiptapEditor(() => ({ element: editorRef, extensions: [ StarterKit, CodeBlockLowlight.configure({ lowlight }), Link.configure({ openOnClick: true }), Image, IframeEmbed, TaskList, TaskItem.configure({ nested: true, HTMLAttributes: { class: "task-item" } }), Details.configure({ HTMLAttributes: { class: "tiptap-details" } }), DetailsSummary, DetailsContent.configure({ HTMLAttributes: { class: "details-content" } }), Table.configure({ resizable: true, HTMLAttributes: { class: "tiptap-table" } }), TableRow.configure({ HTMLAttributes: { class: "tiptap-table-row" } }), TableHeader.configure({ HTMLAttributes: { class: "tiptap-table-header" } }), TableCell.configure({ HTMLAttributes: { class: "tiptap-table-cell" } }), Mermaid, ConditionalBlock, ConditionalInline, TextAlign.configure({ types: ["heading", "paragraph"], alignments: ["left", "center", "right", "justify"], defaultAlignment: "left" }), Superscript, Subscript ], content: props.preSet || `

Hello! World

`, editorProps: { attributes: { class: "focus:outline-none" }, handleClickOn(view, pos, node, nodePos, event) { const target = event.target as HTMLElement; const summary = target.closest("summary"); if (summary) { const details = summary.closest('[data-type="details"]'); if (details) { const isOpen = details.hasAttribute("open"); if (isOpen) { details.removeAttribute("open"); } else { details.setAttribute("open", ""); } const content = details.querySelector( '[data-type="detailsContent"]' ); if (content) { if (isOpen) { content.setAttribute("hidden", "hidden"); } else { content.removeAttribute("hidden"); } } return true; // Prevent default behavior } } return false; } }, onUpdate: ({ editor }) => { untrack(() => { props.updateContent(editor.getHTML()); setTimeout(() => updateReferencesSection(editor), 100); }); }, onSelectionUpdate: ({ editor }) => { const { from, to } = editor.state.selection; const hasSelection = from !== to; if (hasSelection && !editor.state.selection.empty) { setShowBubbleMenu(true); const { view } = editor; const start = view.coordsAtPos(from); const end = view.coordsAtPos(to); const left = Math.max((start.left + end.left) / 2, 0); const top = Math.max(start.top - 10, 0); setBubbleMenuPosition({ top, left }); } else { setShowBubbleMenu(false); } } })); createEffect( on( () => props.preSet, (newContent) => { const instance = editor(); if (instance && newContent && instance.getHTML() !== newContent) { instance.commands.setContent(newContent, { emitUpdate: false }); } }, { defer: true } ) ); const updateReferencesSection = (editorInstance: any) => { if (!editorInstance) return; const doc = editorInstance.state.doc; const foundRefs = new Set(); doc.descendants((node: any) => { if (node.isText && node.marks) { const hasSuperscript = node.marks.some( (mark: any) => mark.type.name === "superscript" ); if (hasSuperscript) { const text = node.text || ""; const match = text.match(/^\[(.+?)\]$/); if (match) { foundRefs.add(match[1]); } } } }); if (foundRefs.size === 0) { let hasReferencesSection = false; let hrPos = -1; let sectionStartPos = -1; doc.descendants((node: any, pos: number) => { if (node.type.name === "heading" && node.textContent === "References") { hasReferencesSection = true; sectionStartPos = pos; } }); if (hasReferencesSection && sectionStartPos > 0) { doc.nodesBetween( Math.max(0, sectionStartPos - 50), sectionStartPos, (node: any, pos: number) => { if (node.type.name === "horizontalRule") { hrPos = pos; } } ); if (hrPos >= 0) { const tr = editorInstance.state.tr; tr.delete(hrPos, doc.content.size); editorInstance.view.dispatch(tr); } } return; } const refNumbers = Array.from(foundRefs).sort((a, b) => { const numA = parseInt(a); const numB = parseInt(b); if (!isNaN(numA) && !isNaN(numB)) { return numA - numB; } return a.localeCompare(b); }); let referencesHeadingPos = -1; let existingRefs = new Set(); doc.descendants((node: any, pos: number) => { if (node.type.name === "heading" && node.textContent === "References") { referencesHeadingPos = pos; } if (referencesHeadingPos >= 0 && node.type.name === "paragraph") { const match = node.textContent.match(/^\[(.+?)\]/); if (match) { existingRefs.add(match[1]); } } }); if (referencesHeadingPos === -1) { const content: any[] = [ { type: "horizontalRule" }, { type: "heading", attrs: { level: 2 }, content: [{ type: "text", text: "References" }] } ]; refNumbers.forEach((refNum) => { content.push({ type: "paragraph", content: [ { type: "text", text: `[${refNum}] `, marks: [{ type: "bold" }] } as any, { type: "text", text: "Add your reference text here" } ] }); }); const tr = editorInstance.state.tr; tr.insert( doc.content.size, editorInstance.schema.nodeFromJSON({ type: "doc", content }).content ); editorInstance.view.dispatch(tr); } else { const newRefs = refNumbers.filter((ref) => !existingRefs.has(ref)); if (newRefs.length > 0) { let insertPos = referencesHeadingPos; doc.nodesBetween( referencesHeadingPos, doc.content.size, (node: any, pos: number) => { if (pos > insertPos) { insertPos = pos + node.nodeSize; } } ); const content: any[] = []; newRefs.forEach((refNum) => { content.push({ type: "paragraph", content: [ { type: "text", text: `[${refNum}] `, marks: [{ type: "bold" }] } as any, { type: "text", text: "Add your reference text here" } ] }); }); const tr = editorInstance.state.tr; content.forEach((item) => { tr.insert(insertPos, editorInstance.schema.nodeFromJSON(item)); insertPos += editorInstance.schema.nodeFromJSON(item).nodeSize; }); editorInstance.view.dispatch(tr); } } }; const setLink = () => { const instance = editor(); if (!instance) return; const previousUrl = instance.getAttributes("link").href; const url = window.prompt("URL", previousUrl); if (url === null) return; if (url === "") { instance.chain().focus().extendMarkRange("link").unsetLink().run(); return; } instance .chain() .focus() .extendMarkRange("link") .setLink({ href: url }) .run(); }; const addIframe = () => { const instance = editor(); if (!instance) return; const url = window.prompt("URL"); if (url) { instance.commands.setIframe({ src: url }); } }; const addImage = () => { const instance = editor(); if (!instance) return; const url = window.prompt("URL"); if (url) { instance.chain().focus().setImage({ src: url }).run(); } }; const insertCollapsibleSection = () => { const instance = editor(); if (!instance) return; const title = window.prompt("Section title:", "Click to expand"); if (title !== null && title.trim() !== "") { const content = { type: "details", attrs: { open: true }, content: [ { type: "detailsSummary", content: [{ type: "text", text: title }] }, { type: "detailsContent", content: [ { type: "paragraph" } ] } ] }; const { from } = instance.state.selection; instance.chain().focus().insertContent(content).run(); setTimeout(() => { const { state } = instance; let targetPos = from; state.doc.nodesBetween(from, from + 200, (node, pos) => { if (node.type.name === "detailsContent") { targetPos = pos + 1; return false; // Stop iteration } }); if (targetPos > from) { instance.commands.setTextSelection(targetPos); instance.commands.focus(); } }, 10); } }; const insertCodeBlock = (language: string | null) => { const instance = editor(); if (!instance) return; instance.chain().focus().toggleCodeBlock().run(); if (language) { instance.chain().updateAttributes("codeBlock", { language }).run(); } setShowLanguageSelector(false); }; const showLanguagePicker = (e: MouseEvent) => { const buttonRect = (e.currentTarget as HTMLElement).getBoundingClientRect(); setLanguageSelectorPosition({ top: buttonRect.bottom + 5, left: buttonRect.left }); setShowLanguageSelector(!showLanguageSelector()); }; const insertTable = (rows: number, cols: number) => { const instance = editor(); if (!instance) return; instance .chain() .focus() .insertTable({ rows, cols, withHeaderRow: true }) .run(); setShowTableMenu(false); }; const showTableInserter = (e: MouseEvent) => { const buttonRect = (e.currentTarget as HTMLElement).getBoundingClientRect(); setTableMenuPosition({ top: buttonRect.bottom + 5, left: buttonRect.left }); setShowTableMenu(!showTableMenu()); }; const deleteTableWithConfirmation = () => { const instance = editor(); if (!instance) return; const confirmed = window.confirm( "Are you sure you want to delete this table?" ); if (!confirmed) return; instance.chain().focus().deleteTable().run(); }; const deleteRowWithConfirmation = () => { const instance = editor(); if (!instance) return; const { state } = instance; const { selection } = state; let rowNode = null; let depth = 0; for (let d = selection.$anchor.depth; d > 0; d--) { const node = selection.$anchor.node(d); if (node.type.name === "tableRow") { rowNode = node; depth = d; break; } } if (rowNode) { let hasContent = false; rowNode.descendants((node) => { if (node.textContent.trim().length > 0) { hasContent = true; return false; } }); if (hasContent) { const confirmed = window.confirm( "This row contains content. Are you sure you want to delete it?" ); if (!confirmed) return; } } instance.chain().focus().deleteRow().run(); }; const deleteColumnWithConfirmation = () => { const instance = editor(); if (!instance) return; const { state } = instance; const { selection } = state; const cellPos = selection.$anchor; let tableNode = null; let tableDepth = 0; for (let d = cellPos.depth; d > 0; d--) { const node = cellPos.node(d); if (node.type.name === "table") { tableNode = node; tableDepth = d; break; } } if (tableNode) { let colIndex = 0; const cellNode = cellPos.node(cellPos.depth); const rowNode = cellPos.node(cellPos.depth - 1); rowNode.forEach((node, offset, index) => { if ( cellPos.pos >= cellPos.start(cellPos.depth - 1) + offset && cellPos.pos < cellPos.start(cellPos.depth - 1) + offset + node.nodeSize ) { colIndex = index; } }); let hasContent = false; tableNode.descendants((node, pos, parent) => { if (parent && parent.type.name === "tableRow") { let currentCol = 0; parent.forEach((cell, offset, index) => { if (index === colIndex && cell.textContent.trim().length > 0) { hasContent = true; return false; } }); } }); if (hasContent) { const confirmed = window.confirm( "This column contains content. Are you sure you want to delete it?" ); if (!confirmed) return; } } instance.chain().focus().deleteColumn().run(); }; createEffect(() => { if (showLanguageSelector()) { const handleClickOutside = (e: MouseEvent) => { const target = e.target as HTMLElement; if ( !target.closest(".language-selector") && !target.closest("[data-language-picker-trigger]") ) { setShowLanguageSelector(false); } }; setTimeout(() => { document.addEventListener("click", handleClickOutside); }, 0); return () => document.removeEventListener("click", handleClickOutside); } }); createEffect(() => { if (showTableMenu()) { const handleClickOutside = (e: MouseEvent) => { const target = e.target as HTMLElement; if ( !target.closest(".table-menu") && !target.closest("[data-table-trigger]") ) { setShowTableMenu(false); } }; setTimeout(() => { document.addEventListener("click", handleClickOutside); }, 0); return () => document.removeEventListener("click", handleClickOutside); } }); createEffect(() => { if (showMermaidTemplates()) { const handleClickOutside = (e: MouseEvent) => { const target = e.target as HTMLElement; if ( !target.closest(".mermaid-menu") && !target.closest("[data-mermaid-trigger]") ) { setShowMermaidTemplates(false); } }; setTimeout(() => { document.addEventListener("click", handleClickOutside); }, 0); return () => document.removeEventListener("click", handleClickOutside); } }); createEffect(() => { if (showConditionalConfig()) { const handleClickOutside = (e: MouseEvent) => { const target = e.target as HTMLElement; if ( !target.closest(".conditional-config") && !target.closest("[data-conditional-trigger]") ) { setShowConditionalConfig(false); } }; setTimeout(() => { document.addEventListener("click", handleClickOutside); }, 0); return () => document.removeEventListener("click", handleClickOutside); } }); const showMermaidSelector = (e: MouseEvent) => { const buttonRect = (e.currentTarget as HTMLElement).getBoundingClientRect(); setMermaidMenuPosition({ top: buttonRect.bottom + 5, left: buttonRect.left }); setShowMermaidTemplates(!showMermaidTemplates()); }; const insertMermaidDiagram = (template: (typeof MERMAID_TEMPLATES)[0]) => { const instance = editor(); if (!instance) return; instance.chain().focus().setMermaid(template.code).run(); setShowMermaidTemplates(false); }; const showConditionalConfigurator = (e: MouseEvent) => { const buttonRect = (e.currentTarget as HTMLElement).getBoundingClientRect(); setConditionalConfigPosition({ top: buttonRect.bottom + 5, left: buttonRect.left }); const instance = editor(); if (instance?.isActive("conditionalBlock")) { const attrs = instance.getAttributes("conditionalBlock"); setConditionalForm({ conditionType: attrs.conditionType || "auth", conditionValue: attrs.conditionValue || "authenticated", showWhen: attrs.showWhen || "true", inline: false }); } else if (instance?.isActive("conditionalInline")) { const attrs = instance.getAttributes("conditionalInline"); setConditionalForm({ conditionType: attrs.conditionType || "auth", conditionValue: attrs.conditionValue || "authenticated", showWhen: attrs.showWhen || "true", inline: true }); } else { setConditionalForm({ conditionType: "auth", conditionValue: "authenticated", showWhen: "true", inline: false }); } setShowConditionalConfig(!showConditionalConfig()); }; const insertConditionalBlock = () => { const instance = editor(); if (!instance) return; const { conditionType, conditionValue, showWhen, inline } = conditionalForm(); if (inline) { if (instance.isActive("conditionalInline")) { instance .chain() .focus() .unsetConditionalInline() .setConditionalInline({ conditionType, conditionValue, showWhen }) .run(); } else { instance .chain() .focus() .setConditionalInline({ conditionType, conditionValue, showWhen }) .run(); } } else { if (instance.isActive("conditionalBlock")) { instance .chain() .focus() .updateConditionalBlock({ conditionType, conditionValue, showWhen }) .run(); } else { instance .chain() .focus() .setConditionalBlock({ conditionType, conditionValue, showWhen }) .run(); } } setShowConditionalConfig(false); }; const toggleFullscreen = () => { setIsFullscreen(!isFullscreen()); }; createEffect(() => { if (isFullscreen()) { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { setIsFullscreen(false); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); } }); createEffect(() => { if (typeof window === "undefined" || !window.visualViewport) return; const viewport = window.visualViewport; const initialHeight = viewport.height; const handleResize = () => { const currentHeight = viewport.height; const heightDiff = initialHeight - currentHeight; if (heightDiff > 150) { setKeyboardVisible(true); setKeyboardHeight(heightDiff); } else { setKeyboardVisible(false); setKeyboardHeight(0); } }; viewport.addEventListener("resize", handleResize); viewport.addEventListener("scroll", handleResize); return () => { viewport.removeEventListener("resize", handleResize); viewport.removeEventListener("scroll", handleResize); }; }); const TableGridSelector = () => { const [hoverCell, setHoverCell] = createSignal({ row: 0, col: 0 }); const maxRows = 10; const maxCols = 10; return (
Insert Table: {hoverCell().row + 1} × {hoverCell().col + 1}
{(_, idx) => { const row = Math.floor(idx() / maxCols); const col = idx() % maxCols; return (
setHoverCell({ row, col })} onClick={() => insertTable(row + 1, col + 1)} /> ); }}
); }; const ConditionalConfigurator = () => { return (

Conditional Block

{/* Condition Type Selector */} {/* Dynamic Condition Value Input based on type */} setConditionalForm({ ...conditionalForm(), conditionValue: e.currentTarget.value }) } />
Format: before:YYYY-MM-DD, after:YYYY-MM-DD, or between:YYYY-MM-DD,YYYY-MM-DD
setConditionalForm({ ...conditionalForm(), conditionValue: e.currentTarget.value }) } /> setConditionalForm({ ...conditionalForm(), conditionValue: e.currentTarget.value }) } />
Format: VAR_NAME:value or VAR_NAME:* for any truthy value
{/* Show When Toggle */} {/* Inline Toggle */} {/* Action Buttons */}
); }; return (
{(instance) => ( <> {/* Bubble Menu - appears when text is selected */}
{/* Table controls in bubble menu */}
{/* Language Selector Dropdown */}
{(lang) => ( )}
{/* Table Grid Selector */}
{/* Mermaid Template Selector */}
Select Diagram Type
{(template) => ( )}
{/* Conditional Configurator */}
{/* Main Toolbar - Pinned at top in fullscreen */}
{/* Text Alignment */}
{/* Table controls - shown when cursor is in a table */}
)}
{/* Keyboard Help Modal */}
setShowKeyboardHelp(false)} >
e.stopPropagation()} > {/* Header */}

Keyboard Shortcuts

{/* Shortcuts Grid */}
{(category) => (

{category.name}

{(shortcut) => (
{shortcut.description} {isMac() ? shortcut.keys : shortcut.keysAlt || shortcut.keys}
)}
)}
{/* Footer */}
Press ⌨ Help button to toggle this help
); }