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