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