@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
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createReactBlockSpec } from '@blocknote/react';
|
|
4
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
5
|
+
import { useLixTheme as useTheme } from '../hooks/useLixTheme';
|
|
6
|
+
|
|
7
|
+
const darkConfig = {
|
|
8
|
+
startOnLoad: false,
|
|
9
|
+
securityLevel: 'loose',
|
|
10
|
+
theme: 'dark',
|
|
11
|
+
themeVariables: {
|
|
12
|
+
primaryColor: '#232d3f',
|
|
13
|
+
primaryTextColor: '#e4e4e7',
|
|
14
|
+
primaryBorderColor: '#c4b5fd',
|
|
15
|
+
lineColor: '#8b8fa3',
|
|
16
|
+
secondaryColor: '#1e1e2e',
|
|
17
|
+
tertiaryColor: '#141a26',
|
|
18
|
+
fontFamily: "'lixFont', sans-serif",
|
|
19
|
+
fontSize: '16px',
|
|
20
|
+
nodeTextColor: '#e4e4e7',
|
|
21
|
+
nodeBorder: '#c4b5fd',
|
|
22
|
+
mainBkg: '#232d3f',
|
|
23
|
+
clusterBkg: '#1a1f2e',
|
|
24
|
+
clusterBorder: '#333',
|
|
25
|
+
titleColor: '#c4b5fd',
|
|
26
|
+
edgeLabelBackground: '#141a26',
|
|
27
|
+
git0: '#c4b5fd',
|
|
28
|
+
git1: '#7c5cbf',
|
|
29
|
+
git2: '#4ade80',
|
|
30
|
+
git3: '#f59e0b',
|
|
31
|
+
git4: '#ef4444',
|
|
32
|
+
git5: '#3b82f6',
|
|
33
|
+
git6: '#ec4899',
|
|
34
|
+
git7: '#14b8a6',
|
|
35
|
+
gitBranchLabel0: '#e4e4e7',
|
|
36
|
+
gitBranchLabel1: '#e4e4e7',
|
|
37
|
+
gitBranchLabel2: '#e4e4e7',
|
|
38
|
+
gitBranchLabel3: '#e4e4e7',
|
|
39
|
+
gitInv0: '#141a26',
|
|
40
|
+
},
|
|
41
|
+
flowchart: { padding: 20, nodeSpacing: 50, rankSpacing: 60, curve: 'basis', htmlLabels: true, useMaxWidth: false },
|
|
42
|
+
sequence: { useMaxWidth: false, boxMargin: 10, noteMargin: 10, messageMargin: 35, mirrorActors: false },
|
|
43
|
+
gitGraph: { showBranches: true, showCommitLabel: true, mainBranchName: 'main', rotateCommitLabel: false },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const lightConfig = {
|
|
47
|
+
startOnLoad: false,
|
|
48
|
+
securityLevel: 'loose',
|
|
49
|
+
theme: 'default',
|
|
50
|
+
themeVariables: {
|
|
51
|
+
primaryColor: '#e8e0ff',
|
|
52
|
+
primaryTextColor: '#1a1a2e',
|
|
53
|
+
primaryBorderColor: '#7c5cbf',
|
|
54
|
+
lineColor: '#6b7280',
|
|
55
|
+
secondaryColor: '#f3f0ff',
|
|
56
|
+
tertiaryColor: '#f9fafb',
|
|
57
|
+
fontFamily: "'lixFont', sans-serif",
|
|
58
|
+
fontSize: '16px',
|
|
59
|
+
nodeTextColor: '#1a1a2e',
|
|
60
|
+
nodeBorder: '#7c5cbf',
|
|
61
|
+
mainBkg: '#e8e0ff',
|
|
62
|
+
clusterBkg: '#f3f0ff',
|
|
63
|
+
clusterBorder: '#d1d5db',
|
|
64
|
+
titleColor: '#7c5cbf',
|
|
65
|
+
edgeLabelBackground: '#f9fafb',
|
|
66
|
+
git0: '#7c5cbf',
|
|
67
|
+
git1: '#9b7bf7',
|
|
68
|
+
git2: '#16a34a',
|
|
69
|
+
git3: '#d97706',
|
|
70
|
+
git4: '#dc2626',
|
|
71
|
+
git5: '#2563eb',
|
|
72
|
+
git6: '#db2777',
|
|
73
|
+
git7: '#0d9488',
|
|
74
|
+
},
|
|
75
|
+
flowchart: { padding: 20, nodeSpacing: 50, rankSpacing: 60, curve: 'basis', htmlLabels: true, useMaxWidth: false },
|
|
76
|
+
sequence: { useMaxWidth: false, boxMargin: 10, noteMargin: 10, messageMargin: 35, mirrorActors: false },
|
|
77
|
+
gitGraph: { showBranches: true, showCommitLabel: true, mainBranchName: 'main', rotateCommitLabel: false },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let mermaidModule = null;
|
|
81
|
+
let mermaidLoadPromise = null;
|
|
82
|
+
let renderQueue = Promise.resolve();
|
|
83
|
+
let lastTheme = null;
|
|
84
|
+
|
|
85
|
+
async function getMermaid(isDark) {
|
|
86
|
+
if (!mermaidModule) {
|
|
87
|
+
if (!mermaidLoadPromise) {
|
|
88
|
+
// Import the full ESM bundle — the default 'mermaid' export maps to mermaid.core.mjs
|
|
89
|
+
// which strips gitGraph, pie, timeline, etc. via lazy-loading that breaks with webpack.
|
|
90
|
+
mermaidLoadPromise = import('mermaid').then(m => {
|
|
91
|
+
mermaidModule = m.default;
|
|
92
|
+
return mermaidModule;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
await mermaidLoadPromise;
|
|
96
|
+
}
|
|
97
|
+
const theme = isDark ? 'dark' : 'light';
|
|
98
|
+
if (lastTheme !== theme) {
|
|
99
|
+
lastTheme = theme;
|
|
100
|
+
mermaidModule.initialize(isDark ? darkConfig : lightConfig);
|
|
101
|
+
}
|
|
102
|
+
return mermaidModule;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Serialize render calls — mermaid is a singleton and concurrent renders cause conflicts
|
|
106
|
+
function queueRender(fn) {
|
|
107
|
+
renderQueue = renderQueue.then(fn, fn);
|
|
108
|
+
return renderQueue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Shared component that renders a mermaid diagram to SVG
|
|
112
|
+
function MermaidPreview({ diagram, isDark, interactive }) {
|
|
113
|
+
const containerRef = useRef(null);
|
|
114
|
+
const [svgHTML, setSvgHTML] = useState('');
|
|
115
|
+
const [error, setError] = useState('');
|
|
116
|
+
const [zoom, setZoom] = useState(1);
|
|
117
|
+
const [pan, setPan] = useState({ x: 0, y: 0 });
|
|
118
|
+
const dragging = useRef(false);
|
|
119
|
+
const dragStart = useRef({ x: 0, y: 0 });
|
|
120
|
+
const panStart = useRef({ x: 0, y: 0 });
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!diagram?.trim()) {
|
|
124
|
+
setSvgHTML('');
|
|
125
|
+
setError('');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
let cancelled = false;
|
|
129
|
+
const id = `mermaid-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
130
|
+
|
|
131
|
+
queueRender(async () => {
|
|
132
|
+
if (cancelled) return;
|
|
133
|
+
try {
|
|
134
|
+
const mermaid = await getMermaid(isDark);
|
|
135
|
+
if (cancelled) return;
|
|
136
|
+
|
|
137
|
+
// Normalize diagram type keywords to correct casing (mermaid is case-sensitive)
|
|
138
|
+
let diagramText = diagram.trim();
|
|
139
|
+
diagramText = diagramText.replace(/^\s*gitgraph/i, 'gitGraph');
|
|
140
|
+
diagramText = diagramText.replace(/^\s*sequencediagram/i, 'sequenceDiagram');
|
|
141
|
+
diagramText = diagramText.replace(/^\s*classDiagram/i, 'classDiagram');
|
|
142
|
+
diagramText = diagramText.replace(/^\s*stateDiagram/i, 'stateDiagram');
|
|
143
|
+
diagramText = diagramText.replace(/^\s*erDiagram/i, 'erDiagram');
|
|
144
|
+
diagramText = diagramText.replace(/^\s*gantt/i, 'gantt');
|
|
145
|
+
|
|
146
|
+
const tempDiv = document.createElement('div');
|
|
147
|
+
tempDiv.id = 'container-' + id;
|
|
148
|
+
tempDiv.style.cssText = 'position:fixed;top:0;left:0;width:100vw;opacity:0;pointer-events:none;z-index:-9999;';
|
|
149
|
+
document.body.appendChild(tempDiv);
|
|
150
|
+
|
|
151
|
+
const { svg } = await mermaid.render(id, diagramText, tempDiv);
|
|
152
|
+
tempDiv.remove();
|
|
153
|
+
|
|
154
|
+
if (!cancelled) {
|
|
155
|
+
const parser = new DOMParser();
|
|
156
|
+
const doc = parser.parseFromString(svg, 'image/svg+xml');
|
|
157
|
+
const svgEl = doc.querySelector('svg');
|
|
158
|
+
if (svgEl) {
|
|
159
|
+
svgEl.removeAttribute('width');
|
|
160
|
+
svgEl.setAttribute('style', 'width:100%;height:auto;max-width:100%;');
|
|
161
|
+
}
|
|
162
|
+
setSvgHTML(svgEl ? svgEl.outerHTML : svg);
|
|
163
|
+
setError('');
|
|
164
|
+
setZoom(1);
|
|
165
|
+
setPan({ x: 0, y: 0 });
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (!cancelled) {
|
|
169
|
+
setError(err.message || 'Invalid diagram syntax');
|
|
170
|
+
setSvgHTML('');
|
|
171
|
+
}
|
|
172
|
+
try { document.getElementById(id)?.remove(); } catch {}
|
|
173
|
+
try { document.getElementById('container-' + id)?.remove(); } catch {}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return () => { cancelled = true; };
|
|
178
|
+
}, [diagram, isDark]);
|
|
179
|
+
|
|
180
|
+
// Mouse wheel zoom
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!interactive) return;
|
|
183
|
+
const el = containerRef.current;
|
|
184
|
+
if (!el) return;
|
|
185
|
+
const handleWheel = (e) => {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
setZoom((z) => {
|
|
189
|
+
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
190
|
+
return Math.min(3, Math.max(0.3, z + delta));
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
el.addEventListener('wheel', handleWheel, { passive: false });
|
|
194
|
+
return () => el.removeEventListener('wheel', handleWheel);
|
|
195
|
+
}, [svgHTML, interactive]);
|
|
196
|
+
|
|
197
|
+
// Pan via drag
|
|
198
|
+
const handleMouseDown = useCallback((e) => {
|
|
199
|
+
if (!interactive || e.button !== 0) return;
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
dragging.current = true;
|
|
202
|
+
dragStart.current = { x: e.clientX, y: e.clientY };
|
|
203
|
+
panStart.current = { ...pan };
|
|
204
|
+
}, [pan, interactive]);
|
|
205
|
+
|
|
206
|
+
const handleMouseMove = useCallback((e) => {
|
|
207
|
+
if (!dragging.current) return;
|
|
208
|
+
const dx = e.clientX - dragStart.current.x;
|
|
209
|
+
const dy = e.clientY - dragStart.current.y;
|
|
210
|
+
setPan({ x: panStart.current.x + dx, y: panStart.current.y + dy });
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const handleMouseUp = useCallback(() => {
|
|
214
|
+
dragging.current = false;
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (!interactive) return;
|
|
219
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
220
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
221
|
+
return () => {
|
|
222
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
223
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
224
|
+
};
|
|
225
|
+
}, [handleMouseMove, handleMouseUp, interactive]);
|
|
226
|
+
|
|
227
|
+
const resetView = useCallback(() => {
|
|
228
|
+
setZoom(1);
|
|
229
|
+
setPan({ x: 0, y: 0 });
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
if (error) {
|
|
233
|
+
return (
|
|
234
|
+
<div className="mermaid-viewport mermaid-viewport--compact">
|
|
235
|
+
<pre style={{ color: '#f87171', fontSize: '12px', whiteSpace: 'pre-wrap', padding: '16px', margin: 0 }}>{error}</pre>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!diagram?.trim()) {
|
|
241
|
+
return (
|
|
242
|
+
<div className="mermaid-viewport mermaid-viewport--compact" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
243
|
+
<span style={{ color: 'var(--text-faint)', fontSize: '12px' }}>Preview will appear here...</span>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!svgHTML) {
|
|
249
|
+
return (
|
|
250
|
+
<div className="mermaid-viewport mermaid-viewport--compact" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
251
|
+
<span style={{ color: 'var(--text-faint)', fontSize: '13px' }}>Rendering...</span>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<div
|
|
258
|
+
ref={containerRef}
|
|
259
|
+
className={interactive ? 'mermaid-viewport' : 'mermaid-viewport mermaid-viewport--compact'}
|
|
260
|
+
onMouseDown={handleMouseDown}
|
|
261
|
+
>
|
|
262
|
+
<div
|
|
263
|
+
className="mermaid-block-svg"
|
|
264
|
+
style={{
|
|
265
|
+
transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
|
|
266
|
+
transformOrigin: 'center center',
|
|
267
|
+
}}
|
|
268
|
+
dangerouslySetInnerHTML={{ __html: svgHTML }}
|
|
269
|
+
/>
|
|
270
|
+
{interactive && (
|
|
271
|
+
<div className="mermaid-zoom-controls">
|
|
272
|
+
<button
|
|
273
|
+
onClick={(e) => { e.stopPropagation(); setZoom((z) => Math.min(3, z + 0.2)); }}
|
|
274
|
+
className="mermaid-zoom-btn"
|
|
275
|
+
title="Zoom in"
|
|
276
|
+
>
|
|
277
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
278
|
+
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
279
|
+
</svg>
|
|
280
|
+
</button>
|
|
281
|
+
<span className="mermaid-zoom-label">{Math.round(zoom * 100)}%</span>
|
|
282
|
+
<button
|
|
283
|
+
onClick={(e) => { e.stopPropagation(); setZoom((z) => Math.max(0.3, z - 0.2)); }}
|
|
284
|
+
className="mermaid-zoom-btn"
|
|
285
|
+
title="Zoom out"
|
|
286
|
+
>
|
|
287
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
288
|
+
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
289
|
+
</svg>
|
|
290
|
+
</button>
|
|
291
|
+
<button
|
|
292
|
+
onClick={(e) => { e.stopPropagation(); resetView(); }}
|
|
293
|
+
className="mermaid-zoom-btn"
|
|
294
|
+
title="Reset view"
|
|
295
|
+
>
|
|
296
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
297
|
+
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/><polyline points="1 4 1 10 7 10"/>
|
|
298
|
+
</svg>
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const MermaidBlock = createReactBlockSpec(
|
|
307
|
+
{
|
|
308
|
+
type: 'mermaidBlock',
|
|
309
|
+
propSchema: {
|
|
310
|
+
diagram: { default: '' },
|
|
311
|
+
},
|
|
312
|
+
content: 'none',
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
render: ({ block, editor }) => {
|
|
316
|
+
const { isDark } = useTheme();
|
|
317
|
+
const [editing, setEditing] = useState(!block.props.diagram);
|
|
318
|
+
const [value, setValue] = useState(block.props.diagram || '');
|
|
319
|
+
const [livePreview, setLivePreview] = useState(block.props.diagram || '');
|
|
320
|
+
const inputRef = useRef(null);
|
|
321
|
+
const debounceRef = useRef(null);
|
|
322
|
+
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
if (editing && inputRef.current) inputRef.current.focus();
|
|
325
|
+
}, [editing]);
|
|
326
|
+
|
|
327
|
+
// Debounced live preview update while typing
|
|
328
|
+
const handleCodeChange = useCallback((e) => {
|
|
329
|
+
const v = e.target.value;
|
|
330
|
+
setValue(v);
|
|
331
|
+
clearTimeout(debounceRef.current);
|
|
332
|
+
debounceRef.current = setTimeout(() => setLivePreview(v), 400);
|
|
333
|
+
}, []);
|
|
334
|
+
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
return () => clearTimeout(debounceRef.current);
|
|
337
|
+
}, []);
|
|
338
|
+
|
|
339
|
+
const save = useCallback(() => {
|
|
340
|
+
editor.updateBlock(block, { props: { diagram: value } });
|
|
341
|
+
setEditing(false);
|
|
342
|
+
}, [editor, block, value]);
|
|
343
|
+
|
|
344
|
+
const handleDelete = useCallback(() => {
|
|
345
|
+
try { editor.removeBlocks([block.id]); } catch {}
|
|
346
|
+
}, [editor, block.id]);
|
|
347
|
+
|
|
348
|
+
if (editing) {
|
|
349
|
+
return (
|
|
350
|
+
<div className="mermaid-block mermaid-block--editing">
|
|
351
|
+
<div className="mermaid-block-header">
|
|
352
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c4b5fd" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
353
|
+
<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"/>
|
|
354
|
+
</svg>
|
|
355
|
+
<span>Mermaid Diagram</span>
|
|
356
|
+
<span style={{ marginLeft: 'auto', fontSize: '10px', color: 'var(--text-faint)' }}>Shift+Enter to save</span>
|
|
357
|
+
</div>
|
|
358
|
+
<textarea
|
|
359
|
+
ref={inputRef}
|
|
360
|
+
value={value}
|
|
361
|
+
onChange={handleCodeChange}
|
|
362
|
+
onKeyDown={(e) => {
|
|
363
|
+
if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); save(); }
|
|
364
|
+
if (e.key === 'Escape') { setEditing(false); setValue(block.props.diagram || ''); setLivePreview(block.props.diagram || ''); }
|
|
365
|
+
if (e.key === 'Tab') {
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
const start = e.target.selectionStart;
|
|
368
|
+
const end = e.target.selectionEnd;
|
|
369
|
+
const newVal = value.substring(0, start) + ' ' + value.substring(end);
|
|
370
|
+
setValue(newVal);
|
|
371
|
+
setLivePreview(newVal);
|
|
372
|
+
requestAnimationFrame(() => {
|
|
373
|
+
e.target.selectionStart = e.target.selectionEnd = start + 4;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}}
|
|
377
|
+
placeholder={`graph TD\n A[Start] --> B{Decision}\n B -->|Yes| C[OK]\n B -->|No| D[End]`}
|
|
378
|
+
rows={8}
|
|
379
|
+
className="mermaid-block-textarea"
|
|
380
|
+
/>
|
|
381
|
+
{/* Live preview panel */}
|
|
382
|
+
<div className="mermaid-live-preview">
|
|
383
|
+
<div className="mermaid-live-preview-label">Preview</div>
|
|
384
|
+
<MermaidPreview diagram={livePreview} isDark={isDark} interactive={false} />
|
|
385
|
+
</div>
|
|
386
|
+
<div className="mermaid-block-actions">
|
|
387
|
+
<button onClick={() => { setEditing(false); setValue(block.props.diagram || ''); setLivePreview(block.props.diagram || ''); }} className="mermaid-btn-cancel">Cancel</button>
|
|
388
|
+
<button onClick={save} className="mermaid-btn-save" disabled={!value.trim()}>Done</button>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!block.props.diagram) {
|
|
395
|
+
return (
|
|
396
|
+
<div onClick={() => setEditing(true)} className="mermaid-block mermaid-block--empty">
|
|
397
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
398
|
+
<rect x="3" y="3" width="7" height="7" rx="1.5" />
|
|
399
|
+
<rect x="14" y="3" width="7" height="7" rx="1.5" />
|
|
400
|
+
<rect x="8.5" y="14" width="7" height="7" rx="1.5" />
|
|
401
|
+
<line x1="6.5" y1="10" x2="6.5" y2="14" />
|
|
402
|
+
<line x1="17.5" y1="10" x2="17.5" y2="14" />
|
|
403
|
+
<line x1="6.5" y1="14" x2="8.5" y2="14" />
|
|
404
|
+
<line x1="17.5" y1="14" x2="15.5" y2="14" />
|
|
405
|
+
</svg>
|
|
406
|
+
<span>Click to add a Mermaid diagram</span>
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<div className="mermaid-block mermaid-block--rendered group" onDoubleClick={() => setEditing(true)}>
|
|
413
|
+
<MermaidPreview diagram={block.props.diagram} isDark={isDark} interactive={true} />
|
|
414
|
+
<div className="mermaid-block-hover">
|
|
415
|
+
<button onClick={() => setEditing(true)} className="mermaid-hover-btn" title="Edit diagram">
|
|
416
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
417
|
+
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
418
|
+
</svg>
|
|
419
|
+
</button>
|
|
420
|
+
<button onClick={handleDelete} className="mermaid-hover-btn mermaid-hover-delete" title="Delete">
|
|
421
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
422
|
+
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
423
|
+
</svg>
|
|
424
|
+
</button>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
},
|
|
429
|
+
}
|
|
430
|
+
);
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createReactBlockSpec } from '@blocknote/react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
export const PDFEmbedBlock = createReactBlockSpec(
|
|
7
|
+
{
|
|
8
|
+
type: 'pdfEmbed',
|
|
9
|
+
propSchema: {
|
|
10
|
+
url: { default: '' },
|
|
11
|
+
title: { default: '' },
|
|
12
|
+
fileSize: { default: '' },
|
|
13
|
+
pageCount: { default: '' },
|
|
14
|
+
},
|
|
15
|
+
content: 'none',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
render: ({ block, editor }) => {
|
|
19
|
+
const { url, title, fileSize, pageCount } = block.props;
|
|
20
|
+
const [inputUrl, setInputUrl] = useState(url || '');
|
|
21
|
+
const [loading, setLoading] = useState(false);
|
|
22
|
+
|
|
23
|
+
const handleSubmit = () => {
|
|
24
|
+
const trimmed = inputUrl.trim();
|
|
25
|
+
if (!trimmed) return;
|
|
26
|
+
|
|
27
|
+
setLoading(true);
|
|
28
|
+
// Extract filename from URL
|
|
29
|
+
const fileName = decodeURIComponent(trimmed.split('/').pop()?.split('?')[0] || 'document.pdf');
|
|
30
|
+
|
|
31
|
+
editor.updateBlock(block, {
|
|
32
|
+
props: {
|
|
33
|
+
url: trimmed,
|
|
34
|
+
title: fileName,
|
|
35
|
+
fileSize: fileSize || '',
|
|
36
|
+
pageCount: pageCount || '',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
setLoading(false);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleReplace = () => {
|
|
43
|
+
editor.updateBlock(block, {
|
|
44
|
+
props: { url: '', title: '', fileSize: '', pageCount: '' },
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleDelete = () => {
|
|
49
|
+
editor.removeBlocks([block]);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Empty state — show URL input
|
|
53
|
+
if (!url) {
|
|
54
|
+
return (
|
|
55
|
+
<div style={{
|
|
56
|
+
background: 'rgba(155, 123, 247, 0.04)',
|
|
57
|
+
border: '1.5px dashed rgba(155, 123, 247, 0.25)',
|
|
58
|
+
borderRadius: '12px',
|
|
59
|
+
padding: '24px',
|
|
60
|
+
}}>
|
|
61
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
|
|
62
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#9b7bf7" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
63
|
+
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
|
64
|
+
<polyline points="14 2 14 8 20 8" />
|
|
65
|
+
<line x1="16" y1="13" x2="8" y2="13" />
|
|
66
|
+
<line x1="16" y1="17" x2="8" y2="17" />
|
|
67
|
+
<polyline points="10 9 9 9 8 9" />
|
|
68
|
+
</svg>
|
|
69
|
+
<span style={{ fontSize: '14px', fontWeight: 600, color: 'var(--text-primary)' }}>Embed PDF</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
72
|
+
<input
|
|
73
|
+
type="text"
|
|
74
|
+
value={inputUrl}
|
|
75
|
+
onChange={(e) => setInputUrl(e.target.value)}
|
|
76
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
|
|
77
|
+
placeholder="Paste PDF link..."
|
|
78
|
+
style={{
|
|
79
|
+
flex: 1, background: 'var(--bg-app)', color: 'var(--text-primary)', border: '1px solid #232d3f',
|
|
80
|
+
borderRadius: '8px', padding: '8px 12px', fontSize: '13px', outline: 'none',
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
<button
|
|
84
|
+
onClick={handleSubmit}
|
|
85
|
+
disabled={!inputUrl.trim() || loading}
|
|
86
|
+
style={{
|
|
87
|
+
padding: '8px 16px', background: '#9b7bf7', color: 'white', border: 'none',
|
|
88
|
+
borderRadius: '8px', fontSize: '13px', fontWeight: 600, cursor: 'pointer',
|
|
89
|
+
opacity: !inputUrl.trim() || loading ? 0.4 : 1,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{loading ? '...' : 'Embed'}
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// PDF preview card
|
|
100
|
+
return (
|
|
101
|
+
<div style={{
|
|
102
|
+
display: 'flex', borderRadius: '12px', overflow: 'hidden',
|
|
103
|
+
border: '1px solid #232d3f', background: 'var(--bg-surface)',
|
|
104
|
+
}}>
|
|
105
|
+
{/* Left — PDF preview iframe */}
|
|
106
|
+
<div style={{
|
|
107
|
+
width: '200px', minHeight: '160px', flexShrink: 0,
|
|
108
|
+
background: '#0d1117', position: 'relative', overflow: 'hidden',
|
|
109
|
+
}}>
|
|
110
|
+
<iframe
|
|
111
|
+
src={`${url}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
|
|
112
|
+
title={title || 'PDF'}
|
|
113
|
+
style={{
|
|
114
|
+
width: '100%', height: '100%', border: 'none',
|
|
115
|
+
pointerEvents: 'none',
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
{/* Overlay gradient */}
|
|
119
|
+
<div style={{
|
|
120
|
+
position: 'absolute', inset: 0,
|
|
121
|
+
background: 'linear-gradient(135deg, rgba(155,123,247,0.08) 0%, transparent 60%)',
|
|
122
|
+
pointerEvents: 'none',
|
|
123
|
+
}} />
|
|
124
|
+
{/* PDF badge */}
|
|
125
|
+
<div style={{
|
|
126
|
+
position: 'absolute', bottom: '8px', left: '8px',
|
|
127
|
+
background: 'rgba(155, 123, 247, 0.2)', backdropFilter: 'blur(8px)',
|
|
128
|
+
borderRadius: '6px', padding: '3px 8px',
|
|
129
|
+
fontSize: '10px', fontWeight: 700, color: '#c4b5fd',
|
|
130
|
+
letterSpacing: '0.5px', textTransform: 'uppercase',
|
|
131
|
+
}}>
|
|
132
|
+
PDF
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Right — metadata */}
|
|
137
|
+
<div style={{ flex: 1, padding: '16px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
|
|
138
|
+
<div>
|
|
139
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
|
140
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9b7bf7" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
141
|
+
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
|
142
|
+
<polyline points="14 2 14 8 20 8" />
|
|
143
|
+
</svg>
|
|
144
|
+
<span style={{ fontSize: '14px', fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
145
|
+
{title || 'Document'}
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
|
150
|
+
{fileSize && (
|
|
151
|
+
<span style={{ fontSize: '12px', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
152
|
+
<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>
|
|
153
|
+
{fileSize}
|
|
154
|
+
</span>
|
|
155
|
+
)}
|
|
156
|
+
{pageCount && (
|
|
157
|
+
<span style={{ fontSize: '12px', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
158
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
|
|
159
|
+
{pageCount} pages
|
|
160
|
+
</span>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Actions */}
|
|
166
|
+
<div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
|
|
167
|
+
<a
|
|
168
|
+
href={url}
|
|
169
|
+
target="_blank"
|
|
170
|
+
rel="noopener noreferrer"
|
|
171
|
+
style={{
|
|
172
|
+
padding: '6px 12px', background: 'rgba(155,123,247,0.1)',
|
|
173
|
+
border: '1px solid rgba(155,123,247,0.25)', borderRadius: '6px',
|
|
174
|
+
fontSize: '12px', color: '#a78bfa', fontWeight: 500,
|
|
175
|
+
textDecoration: 'none', cursor: 'pointer',
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
Open PDF
|
|
179
|
+
</a>
|
|
180
|
+
<button onClick={handleReplace} style={{
|
|
181
|
+
padding: '6px 12px', background: 'rgba(255,255,255,0.04)',
|
|
182
|
+
border: '1px solid #232d3f', borderRadius: '6px',
|
|
183
|
+
fontSize: '12px', color: 'var(--text-muted)', cursor: 'pointer',
|
|
184
|
+
}}>
|
|
185
|
+
Replace
|
|
186
|
+
</button>
|
|
187
|
+
<button onClick={handleDelete} style={{
|
|
188
|
+
padding: '6px 12px', background: 'rgba(248,113,113,0.06)',
|
|
189
|
+
border: '1px solid rgba(248,113,113,0.2)', borderRadius: '6px',
|
|
190
|
+
fontSize: '12px', color: '#f87171', cursor: 'pointer',
|
|
191
|
+
}}>
|
|
192
|
+
Delete
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
);
|