@elixpo/lixeditor 2.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2337 @@
1
+ "use client";
2
+
3
+ // src/editor/LixEditor.jsx
4
+ import { BlockNoteSchema, defaultBlockSpecs, defaultInlineContentSpecs, createCodeBlockSpec } from "@blocknote/core";
5
+ import { useCreateBlockNote, SuggestionMenuController, getDefaultReactSlashMenuItems, TableHandlesController } from "@blocknote/react";
6
+ import { BlockNoteView } from "@blocknote/mantine";
7
+ import { useCallback as useCallback7, useMemo, forwardRef, useImperativeHandle, useState as useState10, useRef as useRef8, useEffect as useEffect9 } from "react";
8
+
9
+ // src/hooks/useLixTheme.js
10
+ import { createContext, useContext, useState, useEffect } from "react";
11
+ "use client";
12
+ var LixThemeContext = createContext(null);
13
+ function LixThemeProvider({ children, defaultTheme = "light", storageKey = "lixeditor_theme" }) {
14
+ const [theme, setTheme] = useState(defaultTheme);
15
+ const [mounted, setMounted] = useState(false);
16
+ useEffect(() => {
17
+ if (storageKey) {
18
+ const saved = localStorage.getItem(storageKey);
19
+ if (saved === "dark" || saved === "light")
20
+ setTheme(saved);
21
+ }
22
+ setMounted(true);
23
+ }, [storageKey]);
24
+ useEffect(() => {
25
+ if (!mounted)
26
+ return;
27
+ document.documentElement.setAttribute("data-theme", theme);
28
+ if (storageKey)
29
+ localStorage.setItem(storageKey, theme);
30
+ }, [theme, mounted, storageKey]);
31
+ const toggleTheme = () => setTheme((t) => t === "dark" ? "light" : "dark");
32
+ const isDark = theme === "dark";
33
+ return <LixThemeContext.Provider value={{ theme, setTheme, toggleTheme, isDark, mounted }}>{children}</LixThemeContext.Provider>;
34
+ }
35
+ function useLixTheme() {
36
+ const ctx = useContext(LixThemeContext);
37
+ if (ctx)
38
+ return ctx;
39
+ const isDark = typeof document !== "undefined" && document.documentElement.getAttribute("data-theme") === "dark";
40
+ return { theme: isDark ? "dark" : "light", isDark, toggleTheme: () => {
41
+ }, setTheme: () => {
42
+ }, mounted: true };
43
+ }
44
+
45
+ // src/blocks/BlockEquation.jsx
46
+ import { createReactBlockSpec } from "@blocknote/react";
47
+ import { useState as useState2, useEffect as useEffect2, useRef, useCallback } from "react";
48
+ import katex from "katex";
49
+ "use client";
50
+ function stripDelimiters(raw) {
51
+ let s = raw.trim();
52
+ if (s.startsWith("\\[") && s.endsWith("\\]"))
53
+ return s.slice(2, -2).trim();
54
+ if (s.startsWith("$$") && s.endsWith("$$"))
55
+ return s.slice(2, -2).trim();
56
+ if (s.startsWith("\\(") && s.endsWith("\\)"))
57
+ return s.slice(2, -2).trim();
58
+ if (s.startsWith("$") && s.endsWith("$") && s.length > 2)
59
+ return s.slice(1, -1).trim();
60
+ return s;
61
+ }
62
+ function renderKaTeX(latex, displayMode = true) {
63
+ try {
64
+ return katex.renderToString(stripDelimiters(latex), { displayMode, throwOnError: false });
65
+ } catch {
66
+ return `<span style="color:#f87171">${latex}</span>`;
67
+ }
68
+ }
69
+ var BlockEquation = createReactBlockSpec(
70
+ {
71
+ type: "blockEquation",
72
+ propSchema: {
73
+ latex: { default: "" }
74
+ },
75
+ content: "none"
76
+ },
77
+ {
78
+ render: ({ block, editor }) => {
79
+ const [editing, setEditing] = useState2(!block.props.latex);
80
+ const [value, setValue] = useState2(block.props.latex || "");
81
+ const [livePreview, setLivePreview] = useState2(block.props.latex || "");
82
+ const inputRef = useRef(null);
83
+ const debounceRef = useRef(null);
84
+ useEffect2(() => {
85
+ if (editing)
86
+ inputRef.current?.focus();
87
+ }, [editing]);
88
+ const handleCodeChange = useCallback((e) => {
89
+ const v = e.target.value;
90
+ setValue(v);
91
+ clearTimeout(debounceRef.current);
92
+ debounceRef.current = setTimeout(() => setLivePreview(v), 200);
93
+ }, []);
94
+ useEffect2(() => {
95
+ return () => clearTimeout(debounceRef.current);
96
+ }, []);
97
+ const save = () => {
98
+ editor.updateBlock(block, { props: { latex: value } });
99
+ setEditing(false);
100
+ };
101
+ if (editing) {
102
+ return <div className="mermaid-block mermaid-block--editing">
103
+ <div className="mermaid-block-header">
104
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c4b5fd" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z" /></svg>
105
+ <span>LaTeX Equation</span>
106
+ <span style={{ marginLeft: "auto", fontSize: "10px", color: "var(--text-faint)" }}>Shift+Enter to save</span>
107
+ </div>
108
+ <textarea
109
+ ref={inputRef}
110
+ value={value}
111
+ onChange={handleCodeChange}
112
+ onKeyDown={(e) => {
113
+ if (e.key === "Enter" && e.shiftKey) {
114
+ e.preventDefault();
115
+ save();
116
+ }
117
+ if (e.key === "Escape") {
118
+ setEditing(false);
119
+ setValue(block.props.latex || "");
120
+ setLivePreview(block.props.latex || "");
121
+ }
122
+ }}
123
+ placeholder="E = mc^2"
124
+ rows={4}
125
+ className="mermaid-block-textarea"
126
+ />
127
+ {livePreview.trim() && <div className="latex-live-preview">
128
+ <div className="latex-live-preview-label">Preview</div>
129
+ <div dangerouslySetInnerHTML={{ __html: renderKaTeX(livePreview) }} />
130
+ </div>}
131
+ <div className="mermaid-block-actions">
132
+ <button onClick={() => {
133
+ setEditing(false);
134
+ setValue(block.props.latex || "");
135
+ setLivePreview(block.props.latex || "");
136
+ }} className="mermaid-btn-cancel">Cancel</button>
137
+ <button onClick={save} className="mermaid-btn-save" disabled={!value.trim()}>Done</button>
138
+ </div>
139
+ </div>;
140
+ }
141
+ const latex = block.props.latex;
142
+ if (!latex) {
143
+ return <div
144
+ onClick={() => setEditing(true)}
145
+ className="mermaid-block mermaid-block--empty"
146
+ >
147
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z" /></svg>
148
+ <span>Click to add a block equation</span>
149
+ </div>;
150
+ }
151
+ return <div
152
+ onClick={() => setEditing(true)}
153
+ className="editor-block-equation"
154
+ dangerouslySetInnerHTML={{ __html: renderKaTeX(latex) }}
155
+ />;
156
+ }
157
+ }
158
+ );
159
+
160
+ // src/blocks/MermaidBlock.jsx
161
+ import { createReactBlockSpec as createReactBlockSpec2 } from "@blocknote/react";
162
+ import { useState as useState3, useEffect as useEffect3, useRef as useRef2, useCallback as useCallback2 } from "react";
163
+ "use client";
164
+ var darkConfig = {
165
+ startOnLoad: false,
166
+ securityLevel: "loose",
167
+ theme: "dark",
168
+ themeVariables: {
169
+ primaryColor: "#232d3f",
170
+ primaryTextColor: "#e4e4e7",
171
+ primaryBorderColor: "#c4b5fd",
172
+ lineColor: "#8b8fa3",
173
+ secondaryColor: "#1e1e2e",
174
+ tertiaryColor: "#141a26",
175
+ fontFamily: "'lixFont', sans-serif",
176
+ fontSize: "16px",
177
+ nodeTextColor: "#e4e4e7",
178
+ nodeBorder: "#c4b5fd",
179
+ mainBkg: "#232d3f",
180
+ clusterBkg: "#1a1f2e",
181
+ clusterBorder: "#333",
182
+ titleColor: "#c4b5fd",
183
+ edgeLabelBackground: "#141a26",
184
+ git0: "#c4b5fd",
185
+ git1: "#7c5cbf",
186
+ git2: "#4ade80",
187
+ git3: "#f59e0b",
188
+ git4: "#ef4444",
189
+ git5: "#3b82f6",
190
+ git6: "#ec4899",
191
+ git7: "#14b8a6",
192
+ gitBranchLabel0: "#e4e4e7",
193
+ gitBranchLabel1: "#e4e4e7",
194
+ gitBranchLabel2: "#e4e4e7",
195
+ gitBranchLabel3: "#e4e4e7",
196
+ gitInv0: "#141a26"
197
+ },
198
+ flowchart: { padding: 20, nodeSpacing: 50, rankSpacing: 60, curve: "basis", htmlLabels: true, useMaxWidth: false },
199
+ sequence: { useMaxWidth: false, boxMargin: 10, noteMargin: 10, messageMargin: 35, mirrorActors: false },
200
+ gitGraph: { showBranches: true, showCommitLabel: true, mainBranchName: "main", rotateCommitLabel: false }
201
+ };
202
+ var lightConfig = {
203
+ startOnLoad: false,
204
+ securityLevel: "loose",
205
+ theme: "default",
206
+ themeVariables: {
207
+ primaryColor: "#e8e0ff",
208
+ primaryTextColor: "#1a1a2e",
209
+ primaryBorderColor: "#7c5cbf",
210
+ lineColor: "#6b7280",
211
+ secondaryColor: "#f3f0ff",
212
+ tertiaryColor: "#f9fafb",
213
+ fontFamily: "'lixFont', sans-serif",
214
+ fontSize: "16px",
215
+ nodeTextColor: "#1a1a2e",
216
+ nodeBorder: "#7c5cbf",
217
+ mainBkg: "#e8e0ff",
218
+ clusterBkg: "#f3f0ff",
219
+ clusterBorder: "#d1d5db",
220
+ titleColor: "#7c5cbf",
221
+ edgeLabelBackground: "#f9fafb",
222
+ git0: "#7c5cbf",
223
+ git1: "#9b7bf7",
224
+ git2: "#16a34a",
225
+ git3: "#d97706",
226
+ git4: "#dc2626",
227
+ git5: "#2563eb",
228
+ git6: "#db2777",
229
+ git7: "#0d9488"
230
+ },
231
+ flowchart: { padding: 20, nodeSpacing: 50, rankSpacing: 60, curve: "basis", htmlLabels: true, useMaxWidth: false },
232
+ sequence: { useMaxWidth: false, boxMargin: 10, noteMargin: 10, messageMargin: 35, mirrorActors: false },
233
+ gitGraph: { showBranches: true, showCommitLabel: true, mainBranchName: "main", rotateCommitLabel: false }
234
+ };
235
+ var mermaidModule = null;
236
+ var mermaidLoadPromise = null;
237
+ var renderQueue = Promise.resolve();
238
+ var lastTheme = null;
239
+ async function getMermaid(isDark) {
240
+ if (!mermaidModule) {
241
+ if (!mermaidLoadPromise) {
242
+ mermaidLoadPromise = import("mermaid").then((m) => {
243
+ mermaidModule = m.default;
244
+ return mermaidModule;
245
+ });
246
+ }
247
+ await mermaidLoadPromise;
248
+ }
249
+ const theme = isDark ? "dark" : "light";
250
+ if (lastTheme !== theme) {
251
+ lastTheme = theme;
252
+ mermaidModule.initialize(isDark ? darkConfig : lightConfig);
253
+ }
254
+ return mermaidModule;
255
+ }
256
+ function queueRender(fn) {
257
+ renderQueue = renderQueue.then(fn, fn);
258
+ return renderQueue;
259
+ }
260
+ function MermaidPreview({ diagram, isDark, interactive }) {
261
+ const containerRef = useRef2(null);
262
+ const [svgHTML, setSvgHTML] = useState3("");
263
+ const [error, setError] = useState3("");
264
+ const [zoom, setZoom] = useState3(1);
265
+ const [pan, setPan] = useState3({ x: 0, y: 0 });
266
+ const dragging = useRef2(false);
267
+ const dragStart = useRef2({ x: 0, y: 0 });
268
+ const panStart = useRef2({ x: 0, y: 0 });
269
+ useEffect3(() => {
270
+ if (!diagram?.trim()) {
271
+ setSvgHTML("");
272
+ setError("");
273
+ return;
274
+ }
275
+ let cancelled = false;
276
+ const id = `mermaid-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
277
+ queueRender(async () => {
278
+ if (cancelled)
279
+ return;
280
+ try {
281
+ const mermaid = await getMermaid(isDark);
282
+ if (cancelled)
283
+ return;
284
+ let diagramText = diagram.trim();
285
+ diagramText = diagramText.replace(/^\s*gitgraph/i, "gitGraph");
286
+ diagramText = diagramText.replace(/^\s*sequencediagram/i, "sequenceDiagram");
287
+ diagramText = diagramText.replace(/^\s*classDiagram/i, "classDiagram");
288
+ diagramText = diagramText.replace(/^\s*stateDiagram/i, "stateDiagram");
289
+ diagramText = diagramText.replace(/^\s*erDiagram/i, "erDiagram");
290
+ diagramText = diagramText.replace(/^\s*gantt/i, "gantt");
291
+ const tempDiv = document.createElement("div");
292
+ tempDiv.id = "container-" + id;
293
+ tempDiv.style.cssText = "position:fixed;top:0;left:0;width:100vw;opacity:0;pointer-events:none;z-index:-9999;";
294
+ document.body.appendChild(tempDiv);
295
+ const { svg } = await mermaid.render(id, diagramText, tempDiv);
296
+ tempDiv.remove();
297
+ if (!cancelled) {
298
+ const parser = new DOMParser();
299
+ const doc = parser.parseFromString(svg, "image/svg+xml");
300
+ const svgEl = doc.querySelector("svg");
301
+ if (svgEl) {
302
+ svgEl.removeAttribute("width");
303
+ svgEl.setAttribute("style", "width:100%;height:auto;max-width:100%;");
304
+ }
305
+ setSvgHTML(svgEl ? svgEl.outerHTML : svg);
306
+ setError("");
307
+ setZoom(1);
308
+ setPan({ x: 0, y: 0 });
309
+ }
310
+ } catch (err) {
311
+ if (!cancelled) {
312
+ setError(err.message || "Invalid diagram syntax");
313
+ setSvgHTML("");
314
+ }
315
+ try {
316
+ document.getElementById(id)?.remove();
317
+ } catch {
318
+ }
319
+ try {
320
+ document.getElementById("container-" + id)?.remove();
321
+ } catch {
322
+ }
323
+ }
324
+ });
325
+ return () => {
326
+ cancelled = true;
327
+ };
328
+ }, [diagram, isDark]);
329
+ useEffect3(() => {
330
+ if (!interactive)
331
+ return;
332
+ const el = containerRef.current;
333
+ if (!el)
334
+ return;
335
+ const handleWheel = (e) => {
336
+ e.preventDefault();
337
+ e.stopPropagation();
338
+ setZoom((z) => {
339
+ const delta = e.deltaY > 0 ? -0.1 : 0.1;
340
+ return Math.min(3, Math.max(0.3, z + delta));
341
+ });
342
+ };
343
+ el.addEventListener("wheel", handleWheel, { passive: false });
344
+ return () => el.removeEventListener("wheel", handleWheel);
345
+ }, [svgHTML, interactive]);
346
+ const handleMouseDown = useCallback2((e) => {
347
+ if (!interactive || e.button !== 0)
348
+ return;
349
+ e.preventDefault();
350
+ dragging.current = true;
351
+ dragStart.current = { x: e.clientX, y: e.clientY };
352
+ panStart.current = { ...pan };
353
+ }, [pan, interactive]);
354
+ const handleMouseMove = useCallback2((e) => {
355
+ if (!dragging.current)
356
+ return;
357
+ const dx = e.clientX - dragStart.current.x;
358
+ const dy = e.clientY - dragStart.current.y;
359
+ setPan({ x: panStart.current.x + dx, y: panStart.current.y + dy });
360
+ }, []);
361
+ const handleMouseUp = useCallback2(() => {
362
+ dragging.current = false;
363
+ }, []);
364
+ useEffect3(() => {
365
+ if (!interactive)
366
+ return;
367
+ window.addEventListener("mousemove", handleMouseMove);
368
+ window.addEventListener("mouseup", handleMouseUp);
369
+ return () => {
370
+ window.removeEventListener("mousemove", handleMouseMove);
371
+ window.removeEventListener("mouseup", handleMouseUp);
372
+ };
373
+ }, [handleMouseMove, handleMouseUp, interactive]);
374
+ const resetView = useCallback2(() => {
375
+ setZoom(1);
376
+ setPan({ x: 0, y: 0 });
377
+ }, []);
378
+ if (error) {
379
+ return <div className="mermaid-viewport mermaid-viewport--compact"><pre style={{ color: "#f87171", fontSize: "12px", whiteSpace: "pre-wrap", padding: "16px", margin: 0 }}>{error}</pre></div>;
380
+ }
381
+ if (!diagram?.trim()) {
382
+ return <div className="mermaid-viewport mermaid-viewport--compact" style={{ display: "flex", alignItems: "center", justifyContent: "center" }}><span style={{ color: "var(--text-faint)", fontSize: "12px" }}>Preview will appear here...</span></div>;
383
+ }
384
+ if (!svgHTML) {
385
+ return <div className="mermaid-viewport mermaid-viewport--compact" style={{ display: "flex", alignItems: "center", justifyContent: "center" }}><span style={{ color: "var(--text-faint)", fontSize: "13px" }}>Rendering...</span></div>;
386
+ }
387
+ return <div
388
+ ref={containerRef}
389
+ className={interactive ? "mermaid-viewport" : "mermaid-viewport mermaid-viewport--compact"}
390
+ onMouseDown={handleMouseDown}
391
+ >
392
+ <div
393
+ className="mermaid-block-svg"
394
+ style={{
395
+ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
396
+ transformOrigin: "center center"
397
+ }}
398
+ dangerouslySetInnerHTML={{ __html: svgHTML }}
399
+ />
400
+ {interactive && <div className="mermaid-zoom-controls">
401
+ <button
402
+ onClick={(e) => {
403
+ e.stopPropagation();
404
+ setZoom((z) => Math.min(3, z + 0.2));
405
+ }}
406
+ className="mermaid-zoom-btn"
407
+ title="Zoom in"
408
+ ><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
409
+ <line x1="12" y1="5" x2="12" y2="19" />
410
+ <line x1="5" y1="12" x2="19" y2="12" />
411
+ </svg></button>
412
+ <span className="mermaid-zoom-label">
413
+ {Math.round(zoom * 100)}
414
+ {"%"}
415
+ </span>
416
+ <button
417
+ onClick={(e) => {
418
+ e.stopPropagation();
419
+ setZoom((z) => Math.max(0.3, z - 0.2));
420
+ }}
421
+ className="mermaid-zoom-btn"
422
+ title="Zoom out"
423
+ ><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12" /></svg></button>
424
+ <button
425
+ onClick={(e) => {
426
+ e.stopPropagation();
427
+ resetView();
428
+ }}
429
+ className="mermaid-zoom-btn"
430
+ title="Reset view"
431
+ ><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
432
+ <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
433
+ <polyline points="1 4 1 10 7 10" />
434
+ </svg></button>
435
+ </div>}
436
+ </div>;
437
+ }
438
+ var MermaidBlock = createReactBlockSpec2(
439
+ {
440
+ type: "mermaidBlock",
441
+ propSchema: {
442
+ diagram: { default: "" }
443
+ },
444
+ content: "none"
445
+ },
446
+ {
447
+ render: ({ block, editor }) => {
448
+ const { isDark } = useLixTheme();
449
+ const [editing, setEditing] = useState3(!block.props.diagram);
450
+ const [value, setValue] = useState3(block.props.diagram || "");
451
+ const [livePreview, setLivePreview] = useState3(block.props.diagram || "");
452
+ const inputRef = useRef2(null);
453
+ const debounceRef = useRef2(null);
454
+ useEffect3(() => {
455
+ if (editing && inputRef.current)
456
+ inputRef.current.focus();
457
+ }, [editing]);
458
+ const handleCodeChange = useCallback2((e) => {
459
+ const v = e.target.value;
460
+ setValue(v);
461
+ clearTimeout(debounceRef.current);
462
+ debounceRef.current = setTimeout(() => setLivePreview(v), 400);
463
+ }, []);
464
+ useEffect3(() => {
465
+ return () => clearTimeout(debounceRef.current);
466
+ }, []);
467
+ const save = useCallback2(() => {
468
+ editor.updateBlock(block, { props: { diagram: value } });
469
+ setEditing(false);
470
+ }, [editor, block, value]);
471
+ const handleDelete = useCallback2(() => {
472
+ try {
473
+ editor.removeBlocks([block.id]);
474
+ } catch {
475
+ }
476
+ }, [editor, block.id]);
477
+ if (editing) {
478
+ return <div className="mermaid-block mermaid-block--editing">
479
+ <div className="mermaid-block-header">
480
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c4b5fd" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3l1.912 5.813a2 2 0 001.275 1.275L21 12l-5.813 1.912a2 2 0 00-1.275 1.275L12 21l-1.912-5.813a2 2 0 00-1.275-1.275L3 12l5.813-1.912a2 2 0 001.275-1.275L12 3z" /></svg>
481
+ <span>Mermaid Diagram</span>
482
+ <span style={{ marginLeft: "auto", fontSize: "10px", color: "var(--text-faint)" }}>Shift+Enter to save</span>
483
+ </div>
484
+ <textarea
485
+ ref={inputRef}
486
+ value={value}
487
+ onChange={handleCodeChange}
488
+ onKeyDown={(e) => {
489
+ if (e.key === "Enter" && e.shiftKey) {
490
+ e.preventDefault();
491
+ save();
492
+ }
493
+ if (e.key === "Escape") {
494
+ setEditing(false);
495
+ setValue(block.props.diagram || "");
496
+ setLivePreview(block.props.diagram || "");
497
+ }
498
+ if (e.key === "Tab") {
499
+ e.preventDefault();
500
+ const start = e.target.selectionStart;
501
+ const end = e.target.selectionEnd;
502
+ const newVal = value.substring(0, start) + " " + value.substring(end);
503
+ setValue(newVal);
504
+ setLivePreview(newVal);
505
+ requestAnimationFrame(() => {
506
+ e.target.selectionStart = e.target.selectionEnd = start + 4;
507
+ });
508
+ }
509
+ }}
510
+ placeholder={`graph TD
511
+ A[Start] --> B{Decision}
512
+ B -->|Yes| C[OK]
513
+ B -->|No| D[End]`}
514
+ rows={8}
515
+ className="mermaid-block-textarea"
516
+ />
517
+ <div className="mermaid-live-preview">
518
+ <div className="mermaid-live-preview-label">Preview</div>
519
+ <MermaidPreview diagram={livePreview} isDark={isDark} interactive={false} />
520
+ </div>
521
+ <div className="mermaid-block-actions">
522
+ <button onClick={() => {
523
+ setEditing(false);
524
+ setValue(block.props.diagram || "");
525
+ setLivePreview(block.props.diagram || "");
526
+ }} className="mermaid-btn-cancel">Cancel</button>
527
+ <button onClick={save} className="mermaid-btn-save" disabled={!value.trim()}>Done</button>
528
+ </div>
529
+ </div>;
530
+ }
531
+ if (!block.props.diagram) {
532
+ return <div onClick={() => setEditing(true)} className="mermaid-block mermaid-block--empty">
533
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
534
+ <rect x="3" y="3" width="7" height="7" rx="1.5" />
535
+ <rect x="14" y="3" width="7" height="7" rx="1.5" />
536
+ <rect x="8.5" y="14" width="7" height="7" rx="1.5" />
537
+ <line x1="6.5" y1="10" x2="6.5" y2="14" />
538
+ <line x1="17.5" y1="10" x2="17.5" y2="14" />
539
+ <line x1="6.5" y1="14" x2="8.5" y2="14" />
540
+ <line x1="17.5" y1="14" x2="15.5" y2="14" />
541
+ </svg>
542
+ <span>Click to add a Mermaid diagram</span>
543
+ </div>;
544
+ }
545
+ return <div className="mermaid-block mermaid-block--rendered group" onDoubleClick={() => setEditing(true)}>
546
+ <MermaidPreview diagram={block.props.diagram} isDark={isDark} interactive={true} />
547
+ <div className="mermaid-block-hover">
548
+ <button onClick={() => setEditing(true)} className="mermaid-hover-btn" title="Edit diagram"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
549
+ <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
550
+ <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
551
+ </svg></button>
552
+ <button onClick={handleDelete} className="mermaid-hover-btn mermaid-hover-delete" title="Delete"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
553
+ <polyline points="3 6 5 6 21 6" />
554
+ <path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
555
+ </svg></button>
556
+ </div>
557
+ </div>;
558
+ }
559
+ }
560
+ );
561
+
562
+ // src/blocks/TableOfContents.jsx
563
+ import { createReactBlockSpec as createReactBlockSpec3 } from "@blocknote/react";
564
+ "use client";
565
+ var TableOfContents = createReactBlockSpec3(
566
+ {
567
+ type: "tableOfContents",
568
+ propSchema: {},
569
+ content: "none"
570
+ },
571
+ {
572
+ render: ({ editor }) => {
573
+ const headings = editor.document.filter(
574
+ (b) => b.type === "heading" && b.content?.length > 0
575
+ );
576
+ return <div className="toc-block border border-[var(--border-default)] rounded-xl bg-[var(--bg-surface)] px-5 py-4 my-2 select-none">
577
+ <p className="text-[11px] uppercase tracking-wider text-[var(--text-muted)] font-bold mb-3">Table of Contents</p>
578
+ {headings.length === 0 ? <p className="text-[13px] text-[var(--text-faint)] italic">Add headings to see the outline here.</p> : <ul className="space-y-1.5">{headings.map((h) => {
579
+ const level = h.props?.level || 1;
580
+ const text = h.content.map((c) => c.text || "").join("");
581
+ return <li
582
+ key={h.id}
583
+ className="text-[13px] text-[#9b7bf7] hover:text-[#b69aff] cursor-pointer transition-colors"
584
+ style={{ paddingLeft: `${(level - 1) * 16}px` }}
585
+ onClick={() => editor.setTextCursorPosition(h.id)}
586
+ >{text}</li>;
587
+ })}</ul>}
588
+ </div>;
589
+ }
590
+ }
591
+ );
592
+
593
+ // src/blocks/InlineEquation.jsx
594
+ import { createReactInlineContentSpec } from "@blocknote/react";
595
+ import { useState as useState4, useRef as useRef3, useEffect as useEffect4, useCallback as useCallback3 } from "react";
596
+ import katex2 from "katex";
597
+ "use client";
598
+ function stripDelimiters2(raw) {
599
+ let s = raw.trim();
600
+ if (s.startsWith("\\(") && s.endsWith("\\)"))
601
+ return s.slice(2, -2).trim();
602
+ if (s.startsWith("\\[") && s.endsWith("\\]"))
603
+ return s.slice(2, -2).trim();
604
+ if (s.startsWith("$$") && s.endsWith("$$"))
605
+ return s.slice(2, -2).trim();
606
+ if (s.startsWith("$") && s.endsWith("$") && s.length > 2)
607
+ return s.slice(1, -1).trim();
608
+ return s;
609
+ }
610
+ function renderKaTeXInline(latex) {
611
+ try {
612
+ return katex2.renderToString(stripDelimiters2(latex), { displayMode: false, throwOnError: false });
613
+ } catch {
614
+ return `<span style="color:#f87171">${latex}</span>`;
615
+ }
616
+ }
617
+ function InlineEquationChip({ inlineContent }) {
618
+ const [editing, setEditing] = useState4(false);
619
+ const [value, setValue] = useState4(inlineContent.props.latex || "");
620
+ const inputRef = useRef3(null);
621
+ const popupRef = useRef3(null);
622
+ useEffect4(() => {
623
+ if (editing && inputRef.current)
624
+ inputRef.current.focus();
625
+ }, [editing]);
626
+ useEffect4(() => {
627
+ if (!editing)
628
+ return;
629
+ function handleClick(e) {
630
+ if (popupRef.current && !popupRef.current.contains(e.target)) {
631
+ setEditing(false);
632
+ }
633
+ }
634
+ document.addEventListener("mousedown", handleClick);
635
+ return () => document.removeEventListener("mousedown", handleClick);
636
+ }, [editing]);
637
+ const save = useCallback3(() => {
638
+ if (value.trim()) {
639
+ inlineContent.props.latex = value.trim();
640
+ }
641
+ setEditing(false);
642
+ }, [value, inlineContent]);
643
+ const html = renderKaTeXInline(inlineContent.props.latex);
644
+ const previewHtml = value.trim() ? renderKaTeXInline(value) : "";
645
+ return <span className="relative inline-flex items-center">
646
+ <span
647
+ onClick={(e) => {
648
+ e.preventDefault();
649
+ e.stopPropagation();
650
+ setValue(inlineContent.props.latex || "");
651
+ setEditing(!editing);
652
+ }}
653
+ className="inline-equation-chip"
654
+ dangerouslySetInnerHTML={{ __html: html }}
655
+ title={inlineContent.props.latex}
656
+ />
657
+ {editing && <div
658
+ ref={popupRef}
659
+ className="inline-equation-editor"
660
+ onMouseDown={(e) => e.stopPropagation()}
661
+ >
662
+ <input
663
+ ref={inputRef}
664
+ type="text"
665
+ className="inline-equation-editor-input"
666
+ value={value}
667
+ onChange={(e) => setValue(e.target.value)}
668
+ onKeyDown={(e) => {
669
+ if (e.key === "Enter") {
670
+ e.preventDefault();
671
+ save();
672
+ }
673
+ if (e.key === "Escape") {
674
+ setEditing(false);
675
+ }
676
+ }}
677
+ placeholder="E = mc^2"
678
+ />
679
+ {previewHtml && <div className="inline-equation-editor-preview" dangerouslySetInnerHTML={{ __html: previewHtml }} />}
680
+ <div className="inline-equation-editor-actions">
681
+ <button className="mermaid-btn-cancel" onClick={() => setEditing(false)}>Cancel</button>
682
+ <button className="mermaid-btn-save" disabled={!value.trim()} onClick={save}>Save</button>
683
+ </div>
684
+ </div>}
685
+ </span>;
686
+ }
687
+ var InlineEquation = createReactInlineContentSpec(
688
+ {
689
+ type: "inlineEquation",
690
+ propSchema: {
691
+ latex: { default: "x^2" }
692
+ },
693
+ content: "none"
694
+ },
695
+ {
696
+ render: (props) => <InlineEquationChip {...props} />
697
+ }
698
+ );
699
+
700
+ // src/blocks/DateInline.jsx
701
+ import { createReactInlineContentSpec as createReactInlineContentSpec2 } from "@blocknote/react";
702
+ import { useState as useState5, useRef as useRef4, useEffect as useEffect5, useCallback as useCallback4 } from "react";
703
+ "use client";
704
+ function MiniCalendar({ selectedDate, onSelect, onClose, anchorEl }) {
705
+ const ref = useRef4(null);
706
+ const [viewDate, setViewDate] = useState5(() => {
707
+ const d = selectedDate ? new Date(selectedDate) : new Date();
708
+ return { year: d.getFullYear(), month: d.getMonth() };
709
+ });
710
+ const [pos, setPos] = useState5(null);
711
+ useEffect5(() => {
712
+ function handleClick(e) {
713
+ if (ref.current && !ref.current.contains(e.target))
714
+ onClose();
715
+ }
716
+ document.addEventListener("mousedown", handleClick);
717
+ return () => document.removeEventListener("mousedown", handleClick);
718
+ }, [onClose]);
719
+ useEffect5(() => {
720
+ if (!anchorEl)
721
+ return;
722
+ const rect = anchorEl.getBoundingClientRect();
723
+ const calWidth = 240;
724
+ let left = rect.left;
725
+ left = Math.max(8, Math.min(left, window.innerWidth - calWidth - 8));
726
+ setPos({ top: rect.bottom + 4, left });
727
+ }, [anchorEl]);
728
+ const { year, month } = viewDate;
729
+ const firstDay = new Date(year, month, 1).getDay();
730
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
731
+ const today = new Date();
732
+ const todayStr = today.toISOString().split("T")[0];
733
+ const monthName = new Date(year, month).toLocaleDateString("en-US", { month: "long", year: "numeric" });
734
+ const days = [];
735
+ for (let i = 0; i < firstDay; i++)
736
+ days.push(null);
737
+ for (let d = 1; d <= daysInMonth; d++)
738
+ days.push(d);
739
+ const prev = () => setViewDate((v) => v.month === 0 ? { year: v.year - 1, month: 11 } : { ...v, month: v.month - 1 });
740
+ const next = () => setViewDate((v) => v.month === 11 ? { year: v.year + 1, month: 0 } : { ...v, month: v.month + 1 });
741
+ const toDateStr = (d) => `${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
742
+ if (!pos)
743
+ return null;
744
+ return <div
745
+ ref={ref}
746
+ className="fixed z-[100] rounded-xl shadow-2xl overflow-hidden"
747
+ style={{ backgroundColor: "var(--bg-app)", border: "1px solid var(--border-default)", width: "240px", top: pos.top, left: pos.left }}
748
+ onMouseDown={(e) => e.stopPropagation()}
749
+ >
750
+ <div className="flex items-center justify-between px-3 py-2" style={{ borderBottom: "1px solid var(--divider)" }}>
751
+ <button onClick={prev} className="w-6 h-6 flex items-center justify-center rounded hover:bg-[var(--bg-hover)] transition-colors" style={{ color: "var(--text-muted)" }}><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="15 18 9 12 15 6" /></svg></button>
752
+ <span className="text-[12px] font-semibold" style={{ color: "var(--text-primary)" }}>{monthName}</span>
753
+ <button onClick={next} className="w-6 h-6 flex items-center justify-center rounded hover:bg-[var(--bg-hover)] transition-colors" style={{ color: "var(--text-muted)" }}><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="9 18 15 12 9 6" /></svg></button>
754
+ </div>
755
+ <div className="grid grid-cols-7 px-2 pt-2">{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((d) => <div key={d} className="text-center text-[10px] font-medium py-1" style={{ color: "var(--text-faint)" }}>{d}</div>)}</div>
756
+ <div className="grid grid-cols-7 px-2 pb-2 gap-0.5">{days.map((d, i) => {
757
+ if (!d)
758
+ return <div key={`e${i}`} />;
759
+ const dateStr = toDateStr(d);
760
+ const isSelected = dateStr === selectedDate;
761
+ const isToday = dateStr === todayStr;
762
+ return <button
763
+ key={d}
764
+ onClick={() => {
765
+ onSelect(dateStr);
766
+ onClose();
767
+ }}
768
+ className="w-7 h-7 rounded-lg text-[11px] font-medium flex items-center justify-center transition-all"
769
+ style={{
770
+ backgroundColor: isSelected ? "#9b7bf7" : "transparent",
771
+ color: isSelected ? "white" : isToday ? "#9b7bf7" : "var(--text-body)",
772
+ border: isToday && !isSelected ? "1px solid #9b7bf7" : "1px solid transparent"
773
+ }}
774
+ onMouseEnter={(e) => {
775
+ if (!isSelected)
776
+ e.currentTarget.style.backgroundColor = "var(--bg-hover)";
777
+ }}
778
+ onMouseLeave={(e) => {
779
+ if (!isSelected)
780
+ e.currentTarget.style.backgroundColor = "transparent";
781
+ }}
782
+ >{d}</button>;
783
+ })}</div>
784
+ <div className="flex items-center justify-between px-3 py-1.5" style={{ borderTop: "1px solid var(--divider)" }}>
785
+ <button
786
+ onClick={() => {
787
+ onSelect("");
788
+ onClose();
789
+ }}
790
+ className="text-[10px] font-medium transition-colors"
791
+ style={{ color: "var(--text-faint)" }}
792
+ >Clear</button>
793
+ <button
794
+ onClick={() => {
795
+ onSelect(todayStr);
796
+ onClose();
797
+ }}
798
+ className="text-[10px] font-medium transition-colors"
799
+ style={{ color: "#9b7bf7" }}
800
+ >Today</button>
801
+ </div>
802
+ </div>;
803
+ }
804
+ function DateChip({ inlineContent }) {
805
+ const [showPicker, setShowPicker] = useState5(false);
806
+ const chipRef = useRef4(null);
807
+ const d = inlineContent.props.date;
808
+ let formatted;
809
+ try {
810
+ formatted = new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
811
+ } catch {
812
+ formatted = d;
813
+ }
814
+ const handleSelect = useCallback4((newDate) => {
815
+ if (newDate) {
816
+ inlineContent.props.date = newDate;
817
+ }
818
+ setShowPicker(false);
819
+ }, [inlineContent]);
820
+ return <span className="relative inline-flex items-center">
821
+ <span
822
+ ref={chipRef}
823
+ onClick={(e) => {
824
+ e.preventDefault();
825
+ e.stopPropagation();
826
+ setShowPicker(!showPicker);
827
+ }}
828
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[13px] font-medium mx-0.5 cursor-pointer transition-all hover:ring-2 hover:ring-[#9b7bf7]/30"
829
+ style={{ color: "#9b7bf7", backgroundColor: "rgba(155,123,247,0.06)", border: "1px solid rgba(155,123,247,0.15)" }}
830
+ title="Click to change date (Ctrl+D to insert)"
831
+ >
832
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
833
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2" strokeWidth={2} />
834
+ <line x1="16" y1="2" x2="16" y2="6" strokeWidth={2} />
835
+ <line x1="8" y1="2" x2="8" y2="6" strokeWidth={2} />
836
+ <line x1="3" y1="10" x2="21" y2="10" strokeWidth={2} />
837
+ </svg>
838
+ {formatted}
839
+ </span>
840
+ {showPicker && <MiniCalendar
841
+ selectedDate={d}
842
+ onSelect={handleSelect}
843
+ onClose={() => setShowPicker(false)}
844
+ anchorEl={chipRef.current}
845
+ />}
846
+ </span>;
847
+ }
848
+ var DateInline = createReactInlineContentSpec2(
849
+ {
850
+ type: "dateInline",
851
+ propSchema: {
852
+ date: { default: new Date().toISOString().split("T")[0] }
853
+ },
854
+ content: "none"
855
+ },
856
+ {
857
+ render: (props) => <DateChip {...props} />
858
+ }
859
+ );
860
+
861
+ // src/blocks/ImageBlock.jsx
862
+ import { createReactBlockSpec as createReactBlockSpec4 } from "@blocknote/react";
863
+ import { useState as useState6, useRef as useRef5, useCallback as useCallback5, useEffect as useEffect6 } from "react";
864
+ "use client";
865
+ var BlogImageBlock = createReactBlockSpec4(
866
+ {
867
+ type: "image",
868
+ propSchema: {
869
+ url: { default: "" },
870
+ caption: { default: "" },
871
+ previewWidth: { default: 740 },
872
+ name: { default: "" },
873
+ showPreview: { default: true }
874
+ },
875
+ content: "none"
876
+ },
877
+ {
878
+ render: (props) => <ImageRenderer {...props} />
879
+ }
880
+ );
881
+ function ImageRenderer({ block, editor }) {
882
+ const { url, caption } = block.props;
883
+ const [mode, setMode] = useState6("idle");
884
+ const [embedUrl, setEmbedUrl] = useState6("");
885
+ const [embedError, setEmbedError] = useState6("");
886
+ const [isDragOver, setIsDragOver] = useState6(false);
887
+ const [uploadStatus, setUploadStatus] = useState6("");
888
+ const [editingCaption, setEditingCaption] = useState6(false);
889
+ const [captionText, setCaptionText] = useState6(caption || "");
890
+ const fileInputRef = useRef5(null);
891
+ const blockRef = useRef5(null);
892
+ const embedInputRef = useRef5(null);
893
+ useEffect6(() => {
894
+ if (mode === "embed")
895
+ setTimeout(() => embedInputRef.current?.focus(), 50);
896
+ }, [mode]);
897
+ useEffect6(() => {
898
+ const el = blockRef.current;
899
+ if (!el)
900
+ return;
901
+ function handleKey(e) {
902
+ if ((e.key === "Backspace" || e.key === "Delete") && mode === "idle" && !url) {
903
+ e.preventDefault();
904
+ try {
905
+ editor.removeBlocks([block.id]);
906
+ } catch {
907
+ }
908
+ }
909
+ }
910
+ el.addEventListener("keydown", handleKey);
911
+ return () => el.removeEventListener("keydown", handleKey);
912
+ }, [editor, block.id, mode, url]);
913
+ const uploadFile = useCallback5(async (file) => {
914
+ if (!file || !file.type.startsWith("image/"))
915
+ return;
916
+ setMode("uploading");
917
+ setUploadStatus("Processing...");
918
+ try {
919
+ const reader = new FileReader();
920
+ reader.onload = () => {
921
+ editor.updateBlock(block.id, { props: { url: reader.result, name: file.name } });
922
+ setMode("idle");
923
+ };
924
+ reader.onerror = () => {
925
+ showFailToast("Failed to read image");
926
+ setMode("idle");
927
+ };
928
+ reader.readAsDataURL(file);
929
+ } catch {
930
+ setMode("idle");
931
+ }
932
+ }, [editor, block.id]);
933
+ const handlePaste = useCallback5((e) => {
934
+ const items = e.clipboardData?.items;
935
+ if (!items)
936
+ return;
937
+ for (const item of items) {
938
+ if (item.type.startsWith("image/")) {
939
+ e.preventDefault();
940
+ uploadFile(item.getAsFile());
941
+ return;
942
+ }
943
+ }
944
+ }, [uploadFile]);
945
+ const handleDrop = useCallback5((e) => {
946
+ e.preventDefault();
947
+ setIsDragOver(false);
948
+ const file = e.dataTransfer?.files?.[0];
949
+ if (file?.type.startsWith("image/"))
950
+ uploadFile(file);
951
+ }, [uploadFile]);
952
+ const handleEmbed = useCallback5(() => {
953
+ const trimmed = embedUrl.trim();
954
+ if (!trimmed)
955
+ return;
956
+ if (!trimmed.startsWith("http")) {
957
+ setEmbedError("URL must start with http:// or https://");
958
+ return;
959
+ }
960
+ editor.updateBlock(block.id, { props: { url: trimmed } });
961
+ setMode("idle");
962
+ setEmbedUrl("");
963
+ setEmbedError("");
964
+ }, [embedUrl, editor, block.id]);
965
+ const showFailToast = useCallback5((msg) => {
966
+ const toast = document.createElement("div");
967
+ toast.className = "blog-img-fail-toast";
968
+ toast.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="flex-shrink:0"><circle cx="8" cy="8" r="7" stroke="#f87171" stroke-width="1.5"/><path d="M8 4.5v4" stroke="#f87171" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="11" r=".75" fill="#f87171"/></svg><span>${msg}</span>`;
969
+ document.body.appendChild(toast);
970
+ setTimeout(() => {
971
+ toast.classList.add("blog-img-fail-toast--out");
972
+ }, 3200);
973
+ setTimeout(() => {
974
+ toast.remove();
975
+ }, 3600);
976
+ }, []);
977
+ const handleDelete = useCallback5(() => {
978
+ try {
979
+ editor.removeBlocks([block.id]);
980
+ } catch {
981
+ }
982
+ }, [editor, block.id]);
983
+ const handleReplace = useCallback5(() => {
984
+ editor.updateBlock(block.id, { props: { url: "" } });
985
+ setMode("idle");
986
+ }, [editor, block.id]);
987
+ const handleCaptionSave = useCallback5(() => {
988
+ editor.updateBlock(block.id, { props: { caption: captionText } });
989
+ setEditingCaption(false);
990
+ }, [editor, block.id, captionText]);
991
+ if (!url) {
992
+ return <div
993
+ ref={blockRef}
994
+ className="blog-img-empty"
995
+ tabIndex={0}
996
+ onPaste={handlePaste}
997
+ onDrop={handleDrop}
998
+ onDragOver={(e) => {
999
+ e.preventDefault();
1000
+ setIsDragOver(true);
1001
+ }}
1002
+ onDragLeave={() => setIsDragOver(false)}
1003
+ data-drag-over={isDragOver}
1004
+ >
1005
+ <input
1006
+ ref={fileInputRef}
1007
+ type="file"
1008
+ accept="image/*"
1009
+ onChange={(e) => {
1010
+ if (e.target.files?.[0])
1011
+ uploadFile(e.target.files[0]);
1012
+ e.target.value = "";
1013
+ }}
1014
+ style={{ display: "none" }}
1015
+ />
1016
+ {mode === "uploading" && <div className="blog-img-status">
1017
+ <div className="blog-img-spinner" />
1018
+ <span>{uploadStatus}</span>
1019
+ </div>}
1020
+ {mode === "idle" && <>
1021
+ <div className="blog-img-actions-row">
1022
+ <button className="blog-img-action" onClick={() => fileInputRef.current?.click()}>
1023
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
1024
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
1025
+ <polyline points="17 8 12 3 7 8" />
1026
+ <line x1="12" y1="3" x2="12" y2="15" />
1027
+ </svg>
1028
+ {"Upload"}
1029
+ </button>
1030
+ <button className="blog-img-action" onClick={() => setMode("embed")}>
1031
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
1032
+ <path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" />
1033
+ <path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" />
1034
+ </svg>
1035
+ {"Embed URL"}
1036
+ </button>
1037
+ </div>
1038
+ <p className="blog-img-hint">{"or drag & drop / paste an image"}</p>
1039
+ </>}
1040
+ {mode === "embed" && <div className="blog-img-input-row">
1041
+ <input
1042
+ ref={embedInputRef}
1043
+ type="url"
1044
+ value={embedUrl}
1045
+ onChange={(e) => {
1046
+ setEmbedUrl(e.target.value);
1047
+ setEmbedError("");
1048
+ }}
1049
+ onKeyDown={(e) => {
1050
+ if (e.key === "Enter")
1051
+ handleEmbed();
1052
+ if (e.key === "Escape") {
1053
+ setMode("idle");
1054
+ setEmbedUrl("");
1055
+ setEmbedError("");
1056
+ }
1057
+ }}
1058
+ placeholder="https://example.com/image.jpg"
1059
+ className="blog-img-url-input"
1060
+ />
1061
+ <button className="blog-img-submit-btn" onClick={handleEmbed} disabled={!embedUrl.trim()}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12" /></svg></button>
1062
+ <button className="blog-img-cancel-btn" onClick={() => {
1063
+ setMode("idle");
1064
+ setEmbedUrl("");
1065
+ setEmbedError("");
1066
+ }}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1067
+ <line x1="18" y1="6" x2="6" y2="18" />
1068
+ <line x1="6" y1="6" x2="18" y2="18" />
1069
+ </svg></button>
1070
+ {embedError && <span className="blog-img-error">{embedError}</span>}
1071
+ </div>}
1072
+ </div>;
1073
+ }
1074
+ return <div ref={blockRef} className="blog-img-loaded" tabIndex={0} onPaste={handlePaste}>
1075
+ <div className="blog-img-wrapper">
1076
+ <img src={url} alt={caption || "Image"} className="blog-img-main" draggable={false} />
1077
+ <div className="blog-img-hover-overlay"><div className="blog-img-hover-actions">
1078
+ <button className="blog-img-hover-btn" onClick={handleReplace}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1079
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
1080
+ <polyline points="17 8 12 3 7 8" />
1081
+ <line x1="12" y1="3" x2="12" y2="15" />
1082
+ </svg></button>
1083
+ <button className="blog-img-hover-btn" onClick={() => setEditingCaption(true)}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1084
+ <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
1085
+ <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
1086
+ </svg></button>
1087
+ <button className="blog-img-hover-btn blog-img-hover-delete" onClick={handleDelete}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1088
+ <polyline points="3 6 5 6 21 6" />
1089
+ <path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
1090
+ </svg></button>
1091
+ </div></div>
1092
+ </div>
1093
+ {editingCaption ? <input
1094
+ type="text"
1095
+ value={captionText}
1096
+ onChange={(e) => setCaptionText(e.target.value)}
1097
+ onKeyDown={(e) => {
1098
+ if (e.key === "Enter")
1099
+ handleCaptionSave();
1100
+ if (e.key === "Escape") {
1101
+ setEditingCaption(false);
1102
+ setCaptionText(caption || "");
1103
+ }
1104
+ }}
1105
+ onBlur={handleCaptionSave}
1106
+ placeholder="Add a caption..."
1107
+ className="blog-img-caption-input"
1108
+ autoFocus
1109
+ /> : <p
1110
+ className={`blog-img-caption ${caption ? "" : "blog-img-caption--empty"}`}
1111
+ onClick={() => {
1112
+ setCaptionText(caption || "");
1113
+ setEditingCaption(true);
1114
+ }}
1115
+ >{caption || "Add a caption..."}</p>}
1116
+ </div>;
1117
+ }
1118
+
1119
+ // src/blocks/ButtonBlock.jsx
1120
+ import { createReactBlockSpec as createReactBlockSpec5 } from "@blocknote/react";
1121
+ import { useState as useState7, useEffect as useEffect7, useRef as useRef6 } from "react";
1122
+ "use client";
1123
+ var BUTTON_ACTIONS = [
1124
+ { value: "link", label: "Open Link" },
1125
+ { value: "copy", label: "Copy Text" },
1126
+ { value: "scroll-top", label: "Scroll to Top" },
1127
+ { value: "share", label: "Share Page" }
1128
+ ];
1129
+ var BUTTON_VARIANTS = [
1130
+ { value: "primary", label: "Primary", cls: "bg-[#9b7bf7] text-[var(--text-primary)] hover:bg-[#b69aff]" },
1131
+ { value: "secondary", label: "Secondary", cls: "bg-[var(--bg-surface)] border border-[var(--border-default)] text-[var(--text-primary)] hover:border-[var(--border-hover)]" },
1132
+ { value: "accent", label: "Accent", cls: "bg-[#9b7bf7] text-[var(--text-primary)] hover:bg-[#b69aff]" }
1133
+ ];
1134
+ var ButtonBlock = createReactBlockSpec5(
1135
+ {
1136
+ type: "buttonBlock",
1137
+ propSchema: {
1138
+ label: { default: "Button" },
1139
+ action: { default: "link" },
1140
+ actionValue: { default: "" },
1141
+ variant: { default: "primary" }
1142
+ },
1143
+ content: "none"
1144
+ },
1145
+ {
1146
+ render: ({ block, editor }) => {
1147
+ const [editing, setEditing] = useState7(!block.props.label || block.props.label === "Button");
1148
+ const [label, setLabel] = useState7(block.props.label);
1149
+ const [action, setAction] = useState7(block.props.action);
1150
+ const [actionValue, setActionValue] = useState7(block.props.actionValue);
1151
+ const [variant, setVariant] = useState7(block.props.variant);
1152
+ const save = () => {
1153
+ editor.updateBlock(block, { props: { label, action, actionValue, variant } });
1154
+ setEditing(false);
1155
+ };
1156
+ if (editing) {
1157
+ return <div className="border border-[var(--border-default)] rounded-xl bg-[var(--bg-surface)] p-4 my-2 space-y-3">
1158
+ <p className="text-[11px] text-[var(--text-muted)] font-medium">Button Block</p>
1159
+ <input
1160
+ type="text"
1161
+ value={label}
1162
+ onChange={(e) => setLabel(e.target.value)}
1163
+ placeholder="Button label"
1164
+ className="w-full bg-[var(--bg-app)] border border-[var(--border-default)] rounded-lg px-3 py-2 text-[13px] text-[var(--text-primary)] outline-none focus:border-[var(--border-hover)] placeholder-[#6b7a8d]"
1165
+ />
1166
+ <div className="flex gap-2">
1167
+ <select value={action} onChange={(e) => setAction(e.target.value)} className="bg-[var(--bg-app)] border border-[var(--border-default)] rounded-lg px-3 py-2 text-[13px] text-[var(--text-primary)] outline-none flex-1">{BUTTON_ACTIONS.map((a) => <option key={a.value} value={a.value}>{a.label}</option>)}</select>
1168
+ <select value={variant} onChange={(e) => setVariant(e.target.value)} className="bg-[var(--bg-app)] border border-[var(--border-default)] rounded-lg px-3 py-2 text-[13px] text-[var(--text-primary)] outline-none">{BUTTON_VARIANTS.map((v) => <option key={v.value} value={v.value}>{v.label}</option>)}</select>
1169
+ </div>
1170
+ {(action === "link" || action === "copy") && <input
1171
+ type="text"
1172
+ value={actionValue}
1173
+ onChange={(e) => setActionValue(e.target.value)}
1174
+ placeholder={action === "link" ? "https://..." : "Text to copy"}
1175
+ className="w-full bg-[var(--bg-app)] border border-[var(--border-default)] rounded-lg px-3 py-2 text-[13px] text-[var(--text-primary)] outline-none focus:border-[var(--border-hover)] placeholder-[#6b7a8d]"
1176
+ />}
1177
+ <div className="flex justify-end gap-2">
1178
+ <button onClick={() => setEditing(false)} className="px-3 py-1 text-[12px] text-[#888] hover:text-[var(--text-primary)] transition-colors">Cancel</button>
1179
+ <button onClick={save} className="px-3 py-1 text-[12px] bg-[#9b7bf7] text-[var(--text-primary)] rounded-md font-medium hover:bg-[#b69aff] transition-colors">Done</button>
1180
+ </div>
1181
+ </div>;
1182
+ }
1183
+ const variantCls = BUTTON_VARIANTS.find((v) => v.value === variant)?.cls || BUTTON_VARIANTS[0].cls;
1184
+ return <div className="my-2" onDoubleClick={() => setEditing(true)}><button className={`px-5 py-2 rounded-lg text-[13px] font-medium transition-colors ${variantCls}`}>{label}</button></div>;
1185
+ }
1186
+ }
1187
+ );
1188
+
1189
+ // src/blocks/PDFEmbedBlock.jsx
1190
+ import { createReactBlockSpec as createReactBlockSpec6 } from "@blocknote/react";
1191
+ import { useState as useState8 } from "react";
1192
+ "use client";
1193
+ var PDFEmbedBlock = createReactBlockSpec6(
1194
+ {
1195
+ type: "pdfEmbed",
1196
+ propSchema: {
1197
+ url: { default: "" },
1198
+ title: { default: "" },
1199
+ fileSize: { default: "" },
1200
+ pageCount: { default: "" }
1201
+ },
1202
+ content: "none"
1203
+ },
1204
+ {
1205
+ render: ({ block, editor }) => {
1206
+ const { url, title, fileSize, pageCount } = block.props;
1207
+ const [inputUrl, setInputUrl] = useState8(url || "");
1208
+ const [loading, setLoading] = useState8(false);
1209
+ const handleSubmit = () => {
1210
+ const trimmed = inputUrl.trim();
1211
+ if (!trimmed)
1212
+ return;
1213
+ setLoading(true);
1214
+ const fileName = decodeURIComponent(trimmed.split("/").pop()?.split("?")[0] || "document.pdf");
1215
+ editor.updateBlock(block, {
1216
+ props: {
1217
+ url: trimmed,
1218
+ title: fileName,
1219
+ fileSize: fileSize || "",
1220
+ pageCount: pageCount || ""
1221
+ }
1222
+ });
1223
+ setLoading(false);
1224
+ };
1225
+ const handleReplace = () => {
1226
+ editor.updateBlock(block, {
1227
+ props: { url: "", title: "", fileSize: "", pageCount: "" }
1228
+ });
1229
+ };
1230
+ const handleDelete = () => {
1231
+ editor.removeBlocks([block]);
1232
+ };
1233
+ if (!url) {
1234
+ return <div style={{
1235
+ background: "rgba(155, 123, 247, 0.04)",
1236
+ border: "1.5px dashed rgba(155, 123, 247, 0.25)",
1237
+ borderRadius: "12px",
1238
+ padding: "24px"
1239
+ }}>
1240
+ <div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "12px" }}>
1241
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#9b7bf7" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
1242
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
1243
+ <polyline points="14 2 14 8 20 8" />
1244
+ <line x1="16" y1="13" x2="8" y2="13" />
1245
+ <line x1="16" y1="17" x2="8" y2="17" />
1246
+ <polyline points="10 9 9 9 8 9" />
1247
+ </svg>
1248
+ <span style={{ fontSize: "14px", fontWeight: 600, color: "var(--text-primary)" }}>Embed PDF</span>
1249
+ </div>
1250
+ <div style={{ display: "flex", gap: "8px" }}>
1251
+ <input
1252
+ type="text"
1253
+ value={inputUrl}
1254
+ onChange={(e) => setInputUrl(e.target.value)}
1255
+ onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
1256
+ placeholder="Paste PDF link..."
1257
+ style={{
1258
+ flex: 1,
1259
+ background: "var(--bg-app)",
1260
+ color: "var(--text-primary)",
1261
+ border: "1px solid #232d3f",
1262
+ borderRadius: "8px",
1263
+ padding: "8px 12px",
1264
+ fontSize: "13px",
1265
+ outline: "none"
1266
+ }}
1267
+ />
1268
+ <button
1269
+ onClick={handleSubmit}
1270
+ disabled={!inputUrl.trim() || loading}
1271
+ style={{
1272
+ padding: "8px 16px",
1273
+ background: "#9b7bf7",
1274
+ color: "white",
1275
+ border: "none",
1276
+ borderRadius: "8px",
1277
+ fontSize: "13px",
1278
+ fontWeight: 600,
1279
+ cursor: "pointer",
1280
+ opacity: !inputUrl.trim() || loading ? 0.4 : 1
1281
+ }}
1282
+ >{loading ? "..." : "Embed"}</button>
1283
+ </div>
1284
+ </div>;
1285
+ }
1286
+ return <div style={{
1287
+ display: "flex",
1288
+ borderRadius: "12px",
1289
+ overflow: "hidden",
1290
+ border: "1px solid #232d3f",
1291
+ background: "var(--bg-surface)"
1292
+ }}>
1293
+ <div style={{
1294
+ width: "200px",
1295
+ minHeight: "160px",
1296
+ flexShrink: 0,
1297
+ background: "#0d1117",
1298
+ position: "relative",
1299
+ overflow: "hidden"
1300
+ }}>
1301
+ <iframe
1302
+ src={`${url}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
1303
+ title={title || "PDF"}
1304
+ style={{
1305
+ width: "100%",
1306
+ height: "100%",
1307
+ border: "none",
1308
+ pointerEvents: "none"
1309
+ }}
1310
+ />
1311
+ <div style={{
1312
+ position: "absolute",
1313
+ inset: 0,
1314
+ background: "linear-gradient(135deg, rgba(155,123,247,0.08) 0%, transparent 60%)",
1315
+ pointerEvents: "none"
1316
+ }} />
1317
+ <div style={{
1318
+ position: "absolute",
1319
+ bottom: "8px",
1320
+ left: "8px",
1321
+ background: "rgba(155, 123, 247, 0.2)",
1322
+ backdropFilter: "blur(8px)",
1323
+ borderRadius: "6px",
1324
+ padding: "3px 8px",
1325
+ fontSize: "10px",
1326
+ fontWeight: 700,
1327
+ color: "#c4b5fd",
1328
+ letterSpacing: "0.5px",
1329
+ textTransform: "uppercase"
1330
+ }}>PDF</div>
1331
+ </div>
1332
+ <div style={{ flex: 1, padding: "16px", display: "flex", flexDirection: "column", justifyContent: "space-between" }}>
1333
+ <div>
1334
+ <div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "8px" }}>
1335
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9b7bf7" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
1336
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
1337
+ <polyline points="14 2 14 8 20 8" />
1338
+ </svg>
1339
+ <span style={{ fontSize: "14px", fontWeight: 600, color: "var(--text-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{title || "Document"}</span>
1340
+ </div>
1341
+ <div style={{ display: "flex", gap: "12px", flexWrap: "wrap" }}>
1342
+ {fileSize && <span style={{ fontSize: "12px", color: "var(--text-muted)", display: "flex", alignItems: "center", gap: "4px" }}>
1343
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" /></svg>
1344
+ {fileSize}
1345
+ </span>}
1346
+ {pageCount && <span style={{ fontSize: "12px", color: "var(--text-muted)", display: "flex", alignItems: "center", gap: "4px" }}>
1347
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1348
+ <path d="M4 19.5A2.5 2.5 0 016.5 17H20" />
1349
+ <path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z" />
1350
+ </svg>
1351
+ {pageCount}
1352
+ {" pages"}
1353
+ </span>}
1354
+ </div>
1355
+ </div>
1356
+ <div style={{ display: "flex", gap: "8px", marginTop: "12px" }}>
1357
+ <a
1358
+ href={url}
1359
+ target="_blank"
1360
+ rel="noopener noreferrer"
1361
+ style={{
1362
+ padding: "6px 12px",
1363
+ background: "rgba(155,123,247,0.1)",
1364
+ border: "1px solid rgba(155,123,247,0.25)",
1365
+ borderRadius: "6px",
1366
+ fontSize: "12px",
1367
+ color: "#a78bfa",
1368
+ fontWeight: 500,
1369
+ textDecoration: "none",
1370
+ cursor: "pointer"
1371
+ }}
1372
+ >Open PDF</a>
1373
+ <button onClick={handleReplace} style={{
1374
+ padding: "6px 12px",
1375
+ background: "rgba(255,255,255,0.04)",
1376
+ border: "1px solid #232d3f",
1377
+ borderRadius: "6px",
1378
+ fontSize: "12px",
1379
+ color: "var(--text-muted)",
1380
+ cursor: "pointer"
1381
+ }}>Replace</button>
1382
+ <button onClick={handleDelete} style={{
1383
+ padding: "6px 12px",
1384
+ background: "rgba(248,113,113,0.06)",
1385
+ border: "1px solid rgba(248,113,113,0.2)",
1386
+ borderRadius: "6px",
1387
+ fontSize: "12px",
1388
+ color: "#f87171",
1389
+ cursor: "pointer"
1390
+ }}>Delete</button>
1391
+ </div>
1392
+ </div>
1393
+ </div>;
1394
+ }
1395
+ }
1396
+ );
1397
+
1398
+ // src/editor/LinkPreviewTooltip.jsx
1399
+ import { useState as useState9, useEffect as useEffect8, useRef as useRef7, useCallback as useCallback6 } from "react";
1400
+ "use client";
1401
+ var previewCache = /* @__PURE__ */ new Map();
1402
+ var linkPreviewEndpoint = "/api/link-preview";
1403
+ function setLinkPreviewEndpoint(endpoint) {
1404
+ linkPreviewEndpoint = endpoint;
1405
+ }
1406
+ async function fetchPreview(url) {
1407
+ if (previewCache.has(url))
1408
+ return previewCache.get(url);
1409
+ try {
1410
+ const res = await fetch(`${linkPreviewEndpoint}?url=${encodeURIComponent(url)}`);
1411
+ if (!res.ok)
1412
+ throw new Error("fetch failed");
1413
+ const data = await res.json();
1414
+ previewCache.set(url, data);
1415
+ return data;
1416
+ } catch {
1417
+ const fallback = { title: new URL(url).hostname, description: "", image: "", favicon: "", domain: new URL(url).hostname };
1418
+ previewCache.set(url, fallback);
1419
+ return fallback;
1420
+ }
1421
+ }
1422
+ function LinkPreviewTooltip({ anchorEl, url, onClose }) {
1423
+ const [data, setData] = useState9(null);
1424
+ const [loading, setLoading] = useState9(true);
1425
+ const tooltipRef = useRef7(null);
1426
+ const hoverRef = useRef7(false);
1427
+ const hideTimerRef = useRef7(null);
1428
+ useEffect8(() => {
1429
+ if (!url)
1430
+ return;
1431
+ setLoading(true);
1432
+ fetchPreview(url).then((d) => {
1433
+ setData(d);
1434
+ setLoading(false);
1435
+ });
1436
+ }, [url]);
1437
+ const posRef = useRef7(null);
1438
+ if (!posRef.current && anchorEl) {
1439
+ const rect = anchorEl.getBoundingClientRect();
1440
+ const tooltipWidth = 320;
1441
+ const tooltipHeight = 300;
1442
+ let left = rect.left + rect.width / 2 - tooltipWidth / 2;
1443
+ left = Math.max(8, Math.min(left, window.innerWidth - tooltipWidth - 8));
1444
+ const spaceBelow = window.innerHeight - rect.bottom;
1445
+ if (spaceBelow >= tooltipHeight || spaceBelow >= rect.top) {
1446
+ posRef.current = { top: rect.bottom + 4, left };
1447
+ } else {
1448
+ posRef.current = { bottom: window.innerHeight - rect.top + 4, left, useBottom: true };
1449
+ }
1450
+ }
1451
+ const scheduleHide = useCallback6(() => {
1452
+ clearTimeout(hideTimerRef.current);
1453
+ hideTimerRef.current = setTimeout(() => {
1454
+ if (!hoverRef.current)
1455
+ onClose();
1456
+ }, 200);
1457
+ }, [onClose]);
1458
+ useEffect8(() => {
1459
+ if (!anchorEl)
1460
+ return;
1461
+ const onEnter = () => {
1462
+ hoverRef.current = true;
1463
+ clearTimeout(hideTimerRef.current);
1464
+ };
1465
+ const onLeave = () => {
1466
+ hoverRef.current = false;
1467
+ scheduleHide();
1468
+ };
1469
+ anchorEl.addEventListener("mouseenter", onEnter);
1470
+ anchorEl.addEventListener("mouseleave", onLeave);
1471
+ return () => {
1472
+ anchorEl.removeEventListener("mouseenter", onEnter);
1473
+ anchorEl.removeEventListener("mouseleave", onLeave);
1474
+ };
1475
+ }, [anchorEl, scheduleHide]);
1476
+ const onTooltipEnter = useCallback6(() => {
1477
+ hoverRef.current = true;
1478
+ clearTimeout(hideTimerRef.current);
1479
+ }, []);
1480
+ const onTooltipLeave = useCallback6(() => {
1481
+ hoverRef.current = false;
1482
+ scheduleHide();
1483
+ }, [scheduleHide]);
1484
+ useEffect8(() => {
1485
+ return () => clearTimeout(hideTimerRef.current);
1486
+ }, []);
1487
+ if (!url || !posRef.current)
1488
+ return null;
1489
+ const style = posRef.current.useBottom ? { bottom: posRef.current.bottom, left: posRef.current.left } : { top: posRef.current.top, left: posRef.current.left };
1490
+ return <div
1491
+ ref={tooltipRef}
1492
+ className="link-preview-tooltip"
1493
+ style={style}
1494
+ onMouseEnter={onTooltipEnter}
1495
+ onMouseLeave={onTooltipLeave}
1496
+ >{loading ? <div className="link-preview-loading">
1497
+ <div className="link-preview-skeleton" style={{ width: "60%", height: 12 }} />
1498
+ <div className="link-preview-skeleton" style={{ width: "90%", height: 10, marginTop: 8 }} />
1499
+ <div className="link-preview-skeleton" style={{ width: "40%", height: 10, marginTop: 4 }} />
1500
+ </div> : data ? <a href={url} target="_blank" rel="noopener noreferrer" className="link-preview-card">
1501
+ {data.image && <div className="link-preview-image"><img src={data.image} alt="" onError={(e) => {
1502
+ e.target.style.display = "none";
1503
+ }} /></div>}
1504
+ <div className="link-preview-body">
1505
+ <div className="link-preview-title">{data.title}</div>
1506
+ {data.description && <div className="link-preview-desc">{data.description.length > 120 ? data.description.slice(0, 120) + "..." : data.description}</div>}
1507
+ <div className="link-preview-domain">
1508
+ {data.favicon && <img src={data.favicon} alt="" className="link-preview-favicon" onError={(e) => {
1509
+ e.target.style.display = "none";
1510
+ }} />}
1511
+ <span>{data.domain}</span>
1512
+ </div>
1513
+ </div>
1514
+ </a> : null}</div>;
1515
+ }
1516
+ function useLinkPreview() {
1517
+ const [preview, setPreview] = useState9(null);
1518
+ const showTimerRef = useRef7(null);
1519
+ const show = useCallback6((anchorEl, url) => {
1520
+ clearTimeout(showTimerRef.current);
1521
+ showTimerRef.current = setTimeout(() => {
1522
+ setPreview({ anchorEl, url });
1523
+ }, 400);
1524
+ }, []);
1525
+ const hide = useCallback6(() => {
1526
+ clearTimeout(showTimerRef.current);
1527
+ setPreview(null);
1528
+ }, []);
1529
+ const cancel = useCallback6(() => {
1530
+ clearTimeout(showTimerRef.current);
1531
+ }, []);
1532
+ useEffect8(() => {
1533
+ return () => clearTimeout(showTimerRef.current);
1534
+ }, []);
1535
+ return { preview, show, hide, cancel };
1536
+ }
1537
+
1538
+ // src/editor/LixEditor.jsx
1539
+ "use client";
1540
+ var DEFAULT_LANGUAGES = {
1541
+ text: { name: "Text" },
1542
+ javascript: { name: "JavaScript", aliases: ["js"] },
1543
+ typescript: { name: "TypeScript", aliases: ["ts"] },
1544
+ python: { name: "Python", aliases: ["py"] },
1545
+ java: { name: "Java" },
1546
+ c: { name: "C" },
1547
+ cpp: { name: "C++" },
1548
+ csharp: { name: "C#", aliases: ["cs"] },
1549
+ go: { name: "Go" },
1550
+ rust: { name: "Rust", aliases: ["rs"] },
1551
+ ruby: { name: "Ruby", aliases: ["rb"] },
1552
+ php: { name: "PHP" },
1553
+ swift: { name: "Swift" },
1554
+ kotlin: { name: "Kotlin", aliases: ["kt"] },
1555
+ html: { name: "HTML" },
1556
+ css: { name: "CSS" },
1557
+ json: { name: "JSON" },
1558
+ yaml: { name: "YAML", aliases: ["yml"] },
1559
+ markdown: { name: "Markdown", aliases: ["md"] },
1560
+ bash: { name: "Bash", aliases: ["sh"] },
1561
+ shell: { name: "Shell" },
1562
+ sql: { name: "SQL" },
1563
+ graphql: { name: "GraphQL", aliases: ["gql"] },
1564
+ jsx: { name: "JSX" },
1565
+ tsx: { name: "TSX" },
1566
+ vue: { name: "Vue" },
1567
+ svelte: { name: "Svelte" },
1568
+ dart: { name: "Dart" },
1569
+ lua: { name: "Lua" },
1570
+ r: { name: "R" },
1571
+ scala: { name: "Scala" }
1572
+ };
1573
+ var LixEditor = forwardRef(function LixEditor2({
1574
+ initialContent,
1575
+ onChange,
1576
+ features = {},
1577
+ codeLanguages,
1578
+ extraBlockSpecs = [],
1579
+ extraInlineSpecs = [],
1580
+ slashMenuItems: extraSlashItems = [],
1581
+ placeholder = "Type '/' for commands...",
1582
+ collaboration,
1583
+ onReady,
1584
+ children
1585
+ }, ref) {
1586
+ const { isDark } = useLixTheme();
1587
+ const wrapperRef = useRef8(null);
1588
+ const editorLinkPreview = useLinkPreview();
1589
+ const f = {
1590
+ equations: true,
1591
+ mermaid: true,
1592
+ codeHighlighting: true,
1593
+ tableOfContents: true,
1594
+ images: true,
1595
+ buttons: true,
1596
+ pdf: true,
1597
+ dates: true,
1598
+ linkPreview: true,
1599
+ markdownLinks: true,
1600
+ ...features
1601
+ };
1602
+ const langs = codeLanguages || DEFAULT_LANGUAGES;
1603
+ const codeBlock = f.codeHighlighting ? createCodeBlockSpec({
1604
+ supportedLanguages: langs,
1605
+ createHighlighter: async () => {
1606
+ const { createHighlighter } = await import("shiki");
1607
+ return createHighlighter({
1608
+ themes: ["vitesse-dark", "vitesse-light"],
1609
+ langs: Object.keys(langs).filter((k) => k !== "text")
1610
+ });
1611
+ }
1612
+ }) : void 0;
1613
+ const schema = useMemo(() => {
1614
+ const blockSpecs = { ...defaultBlockSpecs };
1615
+ if (codeBlock)
1616
+ blockSpecs.codeBlock = codeBlock;
1617
+ if (f.equations)
1618
+ blockSpecs.blockEquation = BlockEquation({});
1619
+ if (f.mermaid)
1620
+ blockSpecs.mermaidBlock = MermaidBlock({});
1621
+ if (f.tableOfContents)
1622
+ blockSpecs.tableOfContents = TableOfContents({});
1623
+ if (f.images)
1624
+ blockSpecs.image = BlogImageBlock({});
1625
+ if (f.buttons)
1626
+ blockSpecs.buttonBlock = ButtonBlock({});
1627
+ if (f.pdf)
1628
+ blockSpecs.pdfEmbed = PDFEmbedBlock({});
1629
+ for (const spec of extraBlockSpecs) {
1630
+ if (spec.type && spec.spec)
1631
+ blockSpecs[spec.type] = spec.spec;
1632
+ }
1633
+ const inlineContentSpecs = { ...defaultInlineContentSpecs };
1634
+ if (f.equations)
1635
+ inlineContentSpecs.inlineEquation = InlineEquation;
1636
+ if (f.dates)
1637
+ inlineContentSpecs.dateInline = DateInline;
1638
+ for (const spec of extraInlineSpecs) {
1639
+ if (spec.type && spec.spec)
1640
+ inlineContentSpecs[spec.type] = spec.spec;
1641
+ }
1642
+ return BlockNoteSchema.create({ blockSpecs, inlineContentSpecs });
1643
+ }, []);
1644
+ const sanitized = useMemo(() => {
1645
+ if (!initialContent)
1646
+ return void 0;
1647
+ let blocks = initialContent;
1648
+ if (typeof blocks === "string") {
1649
+ try {
1650
+ blocks = JSON.parse(blocks);
1651
+ } catch {
1652
+ return void 0;
1653
+ }
1654
+ }
1655
+ if (!Array.isArray(blocks) || blocks.length === 0)
1656
+ return void 0;
1657
+ return blocks;
1658
+ }, [initialContent]);
1659
+ const editor = useCreateBlockNote({
1660
+ schema,
1661
+ ...collaboration ? { collaboration } : { initialContent: sanitized || void 0 },
1662
+ domAttributes: { editor: { class: "lix-editor" } },
1663
+ placeholders: { default: placeholder }
1664
+ });
1665
+ useImperativeHandle(ref, () => ({
1666
+ getDocument: () => editor.document,
1667
+ getEditor: () => editor,
1668
+ getBlocks: () => editor.document,
1669
+ getHTML: async () => await editor.blocksToHTMLLossy(editor.document),
1670
+ getMarkdown: async () => await editor.blocksToMarkdownLossy(editor.document)
1671
+ }), [editor]);
1672
+ useEffect9(() => {
1673
+ if (onReady)
1674
+ onReady();
1675
+ }, []);
1676
+ useEffect9(() => {
1677
+ if (!f.markdownLinks || !editor)
1678
+ return;
1679
+ const tiptap = editor._tiptapEditor;
1680
+ if (!tiptap)
1681
+ return;
1682
+ const handleInput = () => {
1683
+ const { state, view } = tiptap;
1684
+ const { $from } = state.selection;
1685
+ const textBefore = $from.parent.textBetween(0, $from.parentOffset, void 0, "\uFFFC");
1686
+ const imgMatch = textBefore.match(/!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)$/);
1687
+ if (imgMatch) {
1688
+ const [fullMatch2, alt, imgUrl] = imgMatch;
1689
+ const from2 = $from.pos - fullMatch2.length;
1690
+ view.dispatch(state.tr.delete(from2, $from.pos));
1691
+ const cursorBlock = editor.getTextCursorPosition().block;
1692
+ editor.insertBlocks(
1693
+ [{ type: "image", props: { url: imgUrl, caption: alt || "" } }],
1694
+ cursorBlock,
1695
+ "after"
1696
+ );
1697
+ requestAnimationFrame(() => {
1698
+ try {
1699
+ const block = editor.getTextCursorPosition().block;
1700
+ if (block?.type === "paragraph" && !(block.content || []).some((c) => c.text?.trim())) {
1701
+ editor.removeBlocks([block.id]);
1702
+ }
1703
+ } catch {
1704
+ }
1705
+ });
1706
+ return;
1707
+ }
1708
+ const match = textBefore.match(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)$/);
1709
+ if (!match)
1710
+ return;
1711
+ const [fullMatch, linkText, url] = match;
1712
+ const from = $from.pos - fullMatch.length;
1713
+ const linkMark = state.schema.marks.link.create({ href: url });
1714
+ const tr = state.tr.delete(from, $from.pos).insertText(linkText, from).addMark(from, from + linkText.length, linkMark);
1715
+ view.dispatch(tr);
1716
+ };
1717
+ tiptap.on("update", handleInput);
1718
+ return () => tiptap.off("update", handleInput);
1719
+ }, [editor, f.markdownLinks]);
1720
+ useEffect9(() => {
1721
+ if (!f.linkPreview)
1722
+ return;
1723
+ const wrapper = wrapperRef.current;
1724
+ if (!wrapper)
1725
+ return;
1726
+ const handleMouseOver = (e) => {
1727
+ const link = e.target.closest("a[href]");
1728
+ if (!link || link.closest(".bn-link-toolbar") || link.closest(".bn-toolbar"))
1729
+ return;
1730
+ const href = link.getAttribute("href");
1731
+ if (href && href.startsWith("http"))
1732
+ editorLinkPreview.show(link, href);
1733
+ };
1734
+ const handleMouseOut = (e) => {
1735
+ const link = e.target.closest("a[href]");
1736
+ if (!link)
1737
+ return;
1738
+ editorLinkPreview.cancel();
1739
+ };
1740
+ const handleClick = (e) => {
1741
+ if (!(e.ctrlKey || e.metaKey))
1742
+ return;
1743
+ const link = e.target.closest("a[href]");
1744
+ if (!link || link.closest(".bn-link-toolbar"))
1745
+ return;
1746
+ const href = link.getAttribute("href");
1747
+ if (href && href.startsWith("http")) {
1748
+ e.preventDefault();
1749
+ e.stopPropagation();
1750
+ window.open(href, "_blank", "noopener,noreferrer");
1751
+ }
1752
+ };
1753
+ const handleKeyDown = (e) => {
1754
+ if (e.ctrlKey || e.metaKey)
1755
+ wrapper.classList.add("ctrl-held");
1756
+ };
1757
+ const handleKeyUp = () => wrapper.classList.remove("ctrl-held");
1758
+ wrapper.addEventListener("mouseover", handleMouseOver);
1759
+ wrapper.addEventListener("mouseout", handleMouseOut);
1760
+ wrapper.addEventListener("click", handleClick);
1761
+ window.addEventListener("keydown", handleKeyDown);
1762
+ window.addEventListener("keyup", handleKeyUp);
1763
+ return () => {
1764
+ wrapper.removeEventListener("mouseover", handleMouseOver);
1765
+ wrapper.removeEventListener("mouseout", handleMouseOut);
1766
+ wrapper.removeEventListener("click", handleClick);
1767
+ window.removeEventListener("keydown", handleKeyDown);
1768
+ window.removeEventListener("keyup", handleKeyUp);
1769
+ };
1770
+ }, [f.linkPreview]);
1771
+ const getItems = useCallback7(async (query) => {
1772
+ const defaults = getDefaultReactSlashMenuItems(editor).filter((item) => !["video", "audio", "file"].includes(item.key));
1773
+ const custom = [];
1774
+ if (f.equations) {
1775
+ custom.push({
1776
+ title: "Block Equation",
1777
+ subtext: "LaTeX block equation",
1778
+ group: "Advanced",
1779
+ icon: <span style={{ fontSize: 16 }}>{"\u2211"}</span>,
1780
+ onItemClick: () => editor.insertBlocks([{ type: "blockEquation" }], editor.getTextCursorPosition().block, "after")
1781
+ });
1782
+ }
1783
+ if (f.mermaid) {
1784
+ custom.push({
1785
+ title: "Diagram",
1786
+ subtext: "Mermaid diagram (flowchart, sequence, etc.)",
1787
+ group: "Advanced",
1788
+ icon: <span style={{ fontSize: 14 }}>{"\u25C7"}</span>,
1789
+ onItemClick: () => editor.insertBlocks([{ type: "mermaidBlock" }], editor.getTextCursorPosition().block, "after")
1790
+ });
1791
+ }
1792
+ if (f.tableOfContents) {
1793
+ custom.push({
1794
+ title: "Table of Contents",
1795
+ subtext: "Auto-generated document outline",
1796
+ group: "Advanced",
1797
+ icon: <span style={{ fontSize: 14 }}>{"\u2630"}</span>,
1798
+ onItemClick: () => editor.insertBlocks([{ type: "tableOfContents" }], editor.getTextCursorPosition().block, "after")
1799
+ });
1800
+ }
1801
+ return [...defaults, ...custom, ...extraSlashItems].filter((item) => item.title.toLowerCase().includes(query.toLowerCase()));
1802
+ }, [editor, f, extraSlashItems]);
1803
+ const handleChange = useCallback7(() => {
1804
+ if (onChange)
1805
+ onChange(editor);
1806
+ }, [editor, onChange]);
1807
+ return <div className={`lix-editor-wrapper${""}`} ref={wrapperRef} style={{ position: "relative" }}>
1808
+ <BlockNoteView
1809
+ editor={editor}
1810
+ onChange={handleChange}
1811
+ theme={isDark ? "dark" : "light"}
1812
+ slashMenu={false}
1813
+ >
1814
+ <SuggestionMenuController triggerCharacter="/" getItems={getItems} />
1815
+ <TableHandlesController />
1816
+ {children}
1817
+ </BlockNoteView>
1818
+ {f.linkPreview && editorLinkPreview.preview && <LinkPreviewTooltip
1819
+ anchorEl={editorLinkPreview.preview.anchorEl}
1820
+ url={editorLinkPreview.preview.url}
1821
+ onClose={editorLinkPreview.hide}
1822
+ />}
1823
+ </div>;
1824
+ });
1825
+ var LixEditor_default = LixEditor;
1826
+
1827
+ // src/preview/LixPreview.jsx
1828
+ import { useEffect as useEffect10, useRef as useRef9, useState as useState11, useCallback as useCallback8 } from "react";
1829
+
1830
+ // src/preview/renderBlocks.js
1831
+ function renderBlocksToHTML(blocks) {
1832
+ if (!blocks || !blocks.length)
1833
+ return "";
1834
+ function inlineToHTML(content) {
1835
+ if (!content || !Array.isArray(content))
1836
+ return "";
1837
+ return content.map((c) => {
1838
+ if (c.type === "inlineEquation" && c.props?.latex) {
1839
+ return `<span class="lix-inline-equation" data-latex="${encodeURIComponent(c.props.latex)}"></span>`;
1840
+ }
1841
+ if (c.type === "dateInline" && c.props?.date) {
1842
+ let formatted;
1843
+ try {
1844
+ formatted = new Date(c.props.date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
1845
+ } catch {
1846
+ formatted = c.props.date;
1847
+ }
1848
+ return `<span class="lix-date-chip"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> ${formatted}</span>`;
1849
+ }
1850
+ if (c.type === "link" && c.href) {
1851
+ const linkText = c.content ? inlineToHTML(c.content) : (c.text || c.href).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1852
+ return `<a href="${c.href}">${linkText || c.href}</a>`;
1853
+ }
1854
+ let text = (c.text || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1855
+ if (!text)
1856
+ return "";
1857
+ const s = c.styles || {};
1858
+ if (s.bold)
1859
+ text = `<strong>${text}</strong>`;
1860
+ if (s.italic)
1861
+ text = `<em>${text}</em>`;
1862
+ if (s.strike)
1863
+ text = `<del>${text}</del>`;
1864
+ if (s.code)
1865
+ text = `<code>${text}</code>`;
1866
+ if (s.underline)
1867
+ text = `<u>${text}</u>`;
1868
+ if (s.textColor)
1869
+ text = `<span style="color:${s.textColor}">${text}</span>`;
1870
+ if (s.backgroundColor)
1871
+ text = `<span style="background:${s.backgroundColor};border-radius:3px;padding:0 2px">${text}</span>`;
1872
+ return text;
1873
+ }).join("");
1874
+ }
1875
+ const headings = [];
1876
+ function collectHeadings(blockList) {
1877
+ for (const block of blockList) {
1878
+ if (block.type === "heading") {
1879
+ const text = (block.content || []).map((c) => c.text || "").join("");
1880
+ if (text.trim()) {
1881
+ const id = `h-${text.trim().toLowerCase().replace(/[^\w]+/g, "-").slice(0, 40)}`;
1882
+ headings.push({ id, text: text.trim(), level: block.props?.level || 1 });
1883
+ }
1884
+ }
1885
+ if (block.children?.length)
1886
+ collectHeadings(block.children);
1887
+ }
1888
+ }
1889
+ collectHeadings(blocks);
1890
+ function renderBlock(block) {
1891
+ const content = inlineToHTML(block.content);
1892
+ const childrenHTML = block.children?.length ? renderListGroup(block.children) : "";
1893
+ switch (block.type) {
1894
+ case "tableOfContents":
1895
+ return "__TOC_PLACEHOLDER__";
1896
+ case "heading": {
1897
+ const level = block.props?.level || 1;
1898
+ const text = (block.content || []).map((c) => c.text || "").join("");
1899
+ const id = `h-${text.trim().toLowerCase().replace(/[^\w]+/g, "-").slice(0, 40)}`;
1900
+ return `<h${level} id="${id}">${content}</h${level}>${childrenHTML}`;
1901
+ }
1902
+ case "bulletListItem":
1903
+ return `<li class="lix-bullet">${content}${childrenHTML}</li>`;
1904
+ case "numberedListItem":
1905
+ return `<li class="lix-numbered">${content}${childrenHTML}</li>`;
1906
+ case "checkListItem": {
1907
+ const checked = !!block.props?.checked;
1908
+ const cb = `<span class="lix-checkbox${checked ? " lix-checkbox--checked" : ""}"><span class="lix-checkbox-icon">${checked ? '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>' : ""}</span></span>`;
1909
+ return `<li class="lix-check${checked ? " lix-check--checked" : ""}">${cb}<span class="lix-check-text">${content}</span>${childrenHTML}</li>`;
1910
+ }
1911
+ case "blockEquation":
1912
+ if (block.props?.latex)
1913
+ return `<div class="lix-block-equation" data-latex="${encodeURIComponent(block.props.latex)}"></div>${childrenHTML}`;
1914
+ return childrenHTML;
1915
+ case "mermaidBlock":
1916
+ if (block.props?.diagram)
1917
+ return `<div class="lix-mermaid-block" data-diagram="${encodeURIComponent(block.props.diagram)}"></div>${childrenHTML}`;
1918
+ return childrenHTML;
1919
+ case "divider":
1920
+ return `<hr class="lix-divider" />${childrenHTML}`;
1921
+ case "codeBlock": {
1922
+ const lang = block.props?.language || "";
1923
+ const code = (block.content || []).map((c) => c.text || "").join("");
1924
+ return `<pre><code class="language-${lang}">${code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</code></pre>${childrenHTML}`;
1925
+ }
1926
+ case "image":
1927
+ if (block.props?.url) {
1928
+ return `<figure><img src="${block.props.url}" alt="${block.props?.caption || ""}" />${block.props?.caption ? `<figcaption>${block.props.caption}</figcaption>` : ""}</figure>${childrenHTML}`;
1929
+ }
1930
+ return childrenHTML;
1931
+ case "table": {
1932
+ const rows = block.content?.rows || [];
1933
+ if (!rows.length)
1934
+ return childrenHTML;
1935
+ const headerRows = block.content?.headerRows || 0;
1936
+ let table = "<table>";
1937
+ rows.forEach((row, ri) => {
1938
+ table += "<tr>";
1939
+ (row.cells || []).forEach((cell) => {
1940
+ const tag = ri < headerRows ? "th" : "td";
1941
+ let cellContent;
1942
+ if (Array.isArray(cell))
1943
+ cellContent = cell;
1944
+ else if (cell?.content)
1945
+ cellContent = Array.isArray(cell.content) ? cell.content : [];
1946
+ else
1947
+ cellContent = [];
1948
+ table += `<${tag}>${inlineToHTML(cellContent)}</${tag}>`;
1949
+ });
1950
+ table += "</tr>";
1951
+ });
1952
+ table += "</table>";
1953
+ return table + childrenHTML;
1954
+ }
1955
+ case "paragraph":
1956
+ default:
1957
+ if (content)
1958
+ return `<p>${content}</p>${childrenHTML}`;
1959
+ return childrenHTML || "";
1960
+ }
1961
+ }
1962
+ function renderListGroup(blockList) {
1963
+ if (!blockList?.length)
1964
+ return "";
1965
+ const out = [];
1966
+ let i = 0;
1967
+ while (i < blockList.length) {
1968
+ const block = blockList[i];
1969
+ if (block.type === "bulletListItem") {
1970
+ let items = "";
1971
+ while (i < blockList.length && blockList[i].type === "bulletListItem") {
1972
+ items += renderBlock(blockList[i]);
1973
+ i++;
1974
+ }
1975
+ out.push(`<ul>${items}</ul>`);
1976
+ } else if (block.type === "numberedListItem") {
1977
+ let items = "";
1978
+ while (i < blockList.length && blockList[i].type === "numberedListItem") {
1979
+ items += renderBlock(blockList[i]);
1980
+ i++;
1981
+ }
1982
+ out.push(`<ol>${items}</ol>`);
1983
+ } else if (block.type === "checkListItem") {
1984
+ let items = "";
1985
+ while (i < blockList.length && blockList[i].type === "checkListItem") {
1986
+ items += renderBlock(blockList[i]);
1987
+ i++;
1988
+ }
1989
+ out.push(`<ul class="lix-checklist">${items}</ul>`);
1990
+ } else {
1991
+ out.push(renderBlock(block));
1992
+ i++;
1993
+ }
1994
+ }
1995
+ return out.join("\n");
1996
+ }
1997
+ let html = renderListGroup(blocks);
1998
+ if (headings.length > 0) {
1999
+ const tocItems = headings.map((h) => {
2000
+ const indent = (h.level - 1) * 16;
2001
+ return `<li><a href="#${h.id}" class="lix-toc-link" style="padding-left:${indent}px">${h.text}</a></li>`;
2002
+ }).join("");
2003
+ const tocHTML = `<div class="lix-toc-block"><p class="lix-toc-label">Table of Contents</p><ul class="lix-toc-list">${tocItems}</ul></div>`;
2004
+ html = html.replace("__TOC_PLACEHOLDER__", tocHTML);
2005
+ } else {
2006
+ html = html.replace("__TOC_PLACEHOLDER__", "");
2007
+ }
2008
+ return html;
2009
+ }
2010
+
2011
+ // src/preview/LixPreview.jsx
2012
+ "use client";
2013
+ function LixPreview({ blocks, html, features = {}, className = "" }) {
2014
+ const { isDark } = useLixTheme();
2015
+ const contentRef = useRef9(null);
2016
+ const linkPreview = useLinkPreview();
2017
+ const linkPreviewRef = useRef9(linkPreview);
2018
+ linkPreviewRef.current = linkPreview;
2019
+ const f = {
2020
+ equations: true,
2021
+ mermaid: true,
2022
+ codeHighlighting: true,
2023
+ linkPreview: true,
2024
+ ...features
2025
+ };
2026
+ const renderedHTML = blocks && blocks.length > 0 ? renderBlocksToHTML(blocks) : html || "";
2027
+ const effectGenRef = useRef9(0);
2028
+ useEffect10(() => {
2029
+ const root = contentRef.current;
2030
+ if (!root)
2031
+ return;
2032
+ const gen = ++effectGenRef.current;
2033
+ root.innerHTML = renderedHTML || "";
2034
+ function isStale() {
2035
+ return effectGenRef.current !== gen;
2036
+ }
2037
+ if (f.equations) {
2038
+ const eqEls = root.querySelectorAll(".lix-block-equation[data-latex]");
2039
+ const inlineEls = root.querySelectorAll(".lix-inline-equation[data-latex]");
2040
+ if (eqEls.length || inlineEls.length) {
2041
+ import("katex").then((mod) => {
2042
+ if (isStale())
2043
+ return;
2044
+ const katex3 = mod.default || mod;
2045
+ const strip = (raw) => {
2046
+ let s = raw.trim();
2047
+ if (s.startsWith("\\[") && s.endsWith("\\]"))
2048
+ return s.slice(2, -2).trim();
2049
+ if (s.startsWith("$$") && s.endsWith("$$"))
2050
+ return s.slice(2, -2).trim();
2051
+ if (s.startsWith("\\(") && s.endsWith("\\)"))
2052
+ return s.slice(2, -2).trim();
2053
+ if (s.startsWith("$") && s.endsWith("$") && s.length > 2)
2054
+ return s.slice(1, -1).trim();
2055
+ return s;
2056
+ };
2057
+ eqEls.forEach((el) => {
2058
+ if (!el.isConnected)
2059
+ return;
2060
+ try {
2061
+ el.innerHTML = katex3.renderToString(strip(decodeURIComponent(el.dataset.latex)), { displayMode: true, throwOnError: false });
2062
+ } catch (err) {
2063
+ el.innerHTML = `<span style="color:#f87171">${err.message}</span>`;
2064
+ }
2065
+ });
2066
+ inlineEls.forEach((el) => {
2067
+ if (!el.isConnected)
2068
+ return;
2069
+ try {
2070
+ el.innerHTML = katex3.renderToString(strip(decodeURIComponent(el.dataset.latex)), { displayMode: false, throwOnError: false });
2071
+ } catch (err) {
2072
+ el.innerHTML = `<span style="color:#f87171">${err.message}</span>`;
2073
+ }
2074
+ });
2075
+ }).catch(() => {
2076
+ });
2077
+ }
2078
+ }
2079
+ if (f.mermaid) {
2080
+ const mermaidEls = root.querySelectorAll(".lix-mermaid-block[data-diagram]");
2081
+ if (mermaidEls.length) {
2082
+ import("mermaid").then((mod) => {
2083
+ if (isStale())
2084
+ return;
2085
+ const mermaid = mod.default || mod;
2086
+ mermaid.initialize({
2087
+ startOnLoad: false,
2088
+ securityLevel: "loose",
2089
+ theme: isDark ? "dark" : "default",
2090
+ flowchart: { useMaxWidth: false, padding: 20 }
2091
+ });
2092
+ (async () => {
2093
+ for (const el of mermaidEls) {
2094
+ const id = `lix-mermaid-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
2095
+ try {
2096
+ const diagram = decodeURIComponent(el.dataset.diagram).trim();
2097
+ const tempDiv = document.createElement("div");
2098
+ tempDiv.id = "c-" + id;
2099
+ tempDiv.style.cssText = "position:fixed;top:0;left:0;width:100vw;opacity:0;pointer-events:none;z-index:-9999;";
2100
+ document.body.appendChild(tempDiv);
2101
+ const { svg } = await mermaid.render(id, diagram, tempDiv);
2102
+ tempDiv.remove();
2103
+ if (el.isConnected && !isStale()) {
2104
+ el.innerHTML = svg;
2105
+ const svgEl = el.querySelector("svg");
2106
+ if (svgEl) {
2107
+ svgEl.removeAttribute("width");
2108
+ svgEl.style.width = "100%";
2109
+ svgEl.style.height = "auto";
2110
+ }
2111
+ }
2112
+ } catch (err) {
2113
+ if (el.isConnected)
2114
+ el.innerHTML = `<pre style="color:#f87171;font-size:12px">${err.message || "Diagram error"}</pre>`;
2115
+ try {
2116
+ document.getElementById(id)?.remove();
2117
+ document.getElementById("c-" + id)?.remove();
2118
+ } catch {
2119
+ }
2120
+ }
2121
+ }
2122
+ })();
2123
+ }).catch(() => {
2124
+ });
2125
+ }
2126
+ }
2127
+ if (f.codeHighlighting) {
2128
+ const codeEls = root.querySelectorAll('pre > code[class*="language-"]');
2129
+ if (codeEls.length) {
2130
+ import("shiki").then(({ createHighlighter }) => {
2131
+ if (isStale())
2132
+ return;
2133
+ const langs = /* @__PURE__ */ new Set();
2134
+ codeEls.forEach((el) => {
2135
+ const m = el.className.match(/language-(\w+)/);
2136
+ if (m?.[1] && m[1] !== "text")
2137
+ langs.add(m[1]);
2138
+ });
2139
+ return createHighlighter({ themes: ["vitesse-dark", "vitesse-light"], langs: [...langs] }).then((hl) => {
2140
+ if (isStale())
2141
+ return;
2142
+ const theme = isDark ? "vitesse-dark" : "vitesse-light";
2143
+ codeEls.forEach((codeEl) => {
2144
+ const pre = codeEl.parentElement;
2145
+ if (!pre || pre.dataset.highlighted)
2146
+ return;
2147
+ pre.dataset.highlighted = "true";
2148
+ const m = codeEl.className.match(/language-(\w+)/);
2149
+ const lang = m?.[1] || "text";
2150
+ const code = codeEl.textContent || "";
2151
+ if (lang !== "text" && langs.has(lang)) {
2152
+ try {
2153
+ const html2 = hl.codeToHtml(code, { lang, theme });
2154
+ const tmp = document.createElement("div");
2155
+ tmp.innerHTML = html2;
2156
+ const shikiPre = tmp.querySelector("pre");
2157
+ if (shikiPre)
2158
+ codeEl.innerHTML = shikiPre.querySelector("code")?.innerHTML || codeEl.innerHTML;
2159
+ } catch {
2160
+ }
2161
+ }
2162
+ pre.style.position = "relative";
2163
+ const label = document.createElement("span");
2164
+ label.className = "lix-code-lang-label";
2165
+ label.textContent = lang;
2166
+ pre.appendChild(label);
2167
+ const btn = document.createElement("button");
2168
+ btn.className = "lix-code-copy-btn";
2169
+ btn.title = "Copy code";
2170
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
2171
+ btn.onclick = () => {
2172
+ navigator.clipboard.writeText(code);
2173
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>';
2174
+ btn.style.color = "#86efac";
2175
+ setTimeout(() => {
2176
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
2177
+ btn.style.color = "";
2178
+ }, 1500);
2179
+ };
2180
+ pre.appendChild(btn);
2181
+ });
2182
+ });
2183
+ }).catch(() => {
2184
+ });
2185
+ }
2186
+ }
2187
+ if (f.linkPreview) {
2188
+ const externalLinks = root.querySelectorAll('a[href^="http"]');
2189
+ const handlers = [];
2190
+ externalLinks.forEach((link) => {
2191
+ const href = link.getAttribute("href");
2192
+ if (!href)
2193
+ return;
2194
+ const onEnter = () => linkPreviewRef.current.show(link, href);
2195
+ const onLeave = () => linkPreviewRef.current.cancel();
2196
+ link.addEventListener("mouseenter", onEnter);
2197
+ link.addEventListener("mouseleave", onLeave);
2198
+ handlers.push({ el: link, onEnter, onLeave });
2199
+ });
2200
+ return () => handlers.forEach(({ el, onEnter, onLeave }) => {
2201
+ el.removeEventListener("mouseenter", onEnter);
2202
+ el.removeEventListener("mouseleave", onLeave);
2203
+ });
2204
+ }
2205
+ }, [renderedHTML, isDark]);
2206
+ return <div className={`lix-preview ${className}`}>
2207
+ <div ref={contentRef} className="lix-preview-content" />
2208
+ {f.linkPreview && linkPreview.preview && <LinkPreviewTooltip
2209
+ anchorEl={linkPreview.preview.anchorEl}
2210
+ url={linkPreview.preview.url}
2211
+ onClose={linkPreview.hide}
2212
+ />}
2213
+ </div>;
2214
+ }
2215
+
2216
+ // src/editor/KeyboardShortcutsModal.jsx
2217
+ import { useEffect as useEffect11, useRef as useRef10 } from "react";
2218
+ "use client";
2219
+ var SHORTCUT_GROUPS = [
2220
+ {
2221
+ title: "General",
2222
+ shortcuts: [
2223
+ { keys: ["Ctrl", "S"], desc: "Save & sync to cloud" },
2224
+ { keys: ["Ctrl", "O"], desc: "Import markdown file (.md)" },
2225
+ { keys: ["Ctrl", "Shift", "I"], desc: "Invite collaborators" },
2226
+ { keys: ["Ctrl", "D"], desc: "Insert date chip" },
2227
+ { keys: ["Ctrl", "Shift", "P"], desc: "Toggle editor / preview" },
2228
+ { keys: ["Ctrl", "Z"], desc: "Undo" },
2229
+ { keys: ["Ctrl", "Shift", "Z"], desc: "Redo" },
2230
+ { keys: ["Ctrl", "A"], desc: "Select all" },
2231
+ { keys: ["?"], desc: "Show this help" }
2232
+ ]
2233
+ },
2234
+ {
2235
+ title: "Text Formatting",
2236
+ shortcuts: [
2237
+ { keys: ["Ctrl", "B"], desc: "Bold" },
2238
+ { keys: ["Ctrl", "I"], desc: "Italic" },
2239
+ { keys: ["Ctrl", "U"], desc: "Underline" },
2240
+ { keys: ["Ctrl", "E"], desc: "Code (inline)" },
2241
+ { keys: ["Ctrl", "Shift", "S"], desc: "Strikethrough" },
2242
+ { keys: ["Ctrl", "K"], desc: "Add link" }
2243
+ ]
2244
+ },
2245
+ {
2246
+ title: "Blocks",
2247
+ shortcuts: [
2248
+ { keys: ["/"], desc: "Slash commands menu" },
2249
+ { keys: ["Space"], desc: "AI assistant (on empty line)" },
2250
+ { keys: ["@"], desc: "Mention user/blog/org" },
2251
+ { keys: ["Tab"], desc: "Indent block" },
2252
+ { keys: ["Shift", "Tab"], desc: "Outdent block" },
2253
+ { keys: ["Enter"], desc: "New block" },
2254
+ { keys: ["Backspace"], desc: "Delete block (when empty)" }
2255
+ ]
2256
+ },
2257
+ {
2258
+ title: "AI",
2259
+ shortcuts: [
2260
+ { keys: ["Space"], desc: "Open AI prompt (empty line)" },
2261
+ { keys: ["\u2605", "click"], desc: "AI edit selected text" },
2262
+ { keys: ["Esc"], desc: "Cancel AI / close menu" }
2263
+ ]
2264
+ }
2265
+ ];
2266
+ function KeyboardShortcutsModal({ onClose }) {
2267
+ const ref = useRef10(null);
2268
+ useEffect11(() => {
2269
+ function handleKey(e) {
2270
+ if (e.key === "Escape")
2271
+ onClose();
2272
+ }
2273
+ function handleClick(e) {
2274
+ if (ref.current && !ref.current.contains(e.target))
2275
+ onClose();
2276
+ }
2277
+ document.addEventListener("keydown", handleKey);
2278
+ document.addEventListener("mousedown", handleClick);
2279
+ return () => {
2280
+ document.removeEventListener("keydown", handleKey);
2281
+ document.removeEventListener("mousedown", handleClick);
2282
+ };
2283
+ }, [onClose]);
2284
+ return <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/30 backdrop-blur-sm"><div
2285
+ ref={ref}
2286
+ className="w-full max-w-[520px] max-h-[80vh] rounded-2xl shadow-2xl overflow-hidden"
2287
+ style={{ backgroundColor: "var(--card-bg)", border: "1px solid var(--border-default)", boxShadow: "var(--shadow-lg)" }}
2288
+ >
2289
+ <div className="flex items-center justify-between px-6 py-4" style={{ borderBottom: "1px solid var(--divider)" }}>
2290
+ <div className="flex items-center gap-2.5">
2291
+ <ion-icon name="keypad-outline" style={{ fontSize: "18px", color: "#9b7bf7" }} />
2292
+ <h2 className="text-[15px] font-bold" style={{ color: "var(--text-primary)" }}>Keyboard Shortcuts</h2>
2293
+ </div>
2294
+ <button
2295
+ onClick={onClose}
2296
+ className="transition-colors p-1"
2297
+ style={{ color: "var(--text-faint)" }}
2298
+ ><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
2299
+ <line x1="18" y1="6" x2="6" y2="18" />
2300
+ <line x1="6" y1="6" x2="18" y2="18" />
2301
+ </svg></button>
2302
+ </div>
2303
+ <div className="overflow-y-auto max-h-[calc(80vh-60px)] p-6 space-y-6 scrollbar-thin">{SHORTCUT_GROUPS.map((group) => <div key={group.title}>
2304
+ <h3 className="text-[11px] font-semibold uppercase tracking-wider mb-3" style={{ color: "#9b7bf7" }}>{group.title}</h3>
2305
+ <div className="space-y-1">{group.shortcuts.map((s, i) => <div key={i} className="flex items-center justify-between py-1.5">
2306
+ <span className="text-[13px]" style={{ color: "var(--text-body)" }}>{s.desc}</span>
2307
+ <div className="flex items-center gap-1">{s.keys.map((key, j) => <span key={j}>
2308
+ {j > 0 && <span className="text-[10px] mx-0.5" style={{ color: "var(--text-faint)" }}>+</span>}
2309
+ <kbd
2310
+ className="inline-block min-w-[24px] text-center px-1.5 py-0.5 text-[11px] font-medium rounded-md"
2311
+ style={{ color: "var(--text-secondary)", backgroundColor: "var(--bg-surface)", border: "1px solid var(--border-default)" }}
2312
+ >{key}</kbd>
2313
+ </span>)}</div>
2314
+ </div>)}</div>
2315
+ </div>)}</div>
2316
+ </div></div>;
2317
+ }
2318
+ export {
2319
+ BlockEquation,
2320
+ ButtonBlock,
2321
+ DateInline,
2322
+ BlogImageBlock as ImageBlock,
2323
+ InlineEquation,
2324
+ KeyboardShortcutsModal,
2325
+ LinkPreviewTooltip,
2326
+ LixEditor_default as LixEditor,
2327
+ LixPreview,
2328
+ LixThemeProvider,
2329
+ MermaidBlock,
2330
+ PDFEmbedBlock,
2331
+ TableOfContents,
2332
+ renderBlocksToHTML,
2333
+ setLinkPreviewEndpoint,
2334
+ useLinkPreview,
2335
+ useLixTheme
2336
+ };
2337
+ //# sourceMappingURL=index.js.map