@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.
@@ -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
+ );