@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,274 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createReactBlockSpec } from '@blocknote/react';
|
|
4
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Image block for @elixpo/lixeditor.
|
|
8
|
+
* Supports: Upload (base64), Embed URL, Paste, Drag & Drop, Captions.
|
|
9
|
+
* Same class names as LixBlogs for CSS compatibility.
|
|
10
|
+
*
|
|
11
|
+
* For custom upload (e.g. cloud storage), consumers should use the LixBlogs
|
|
12
|
+
* BlogImageBlock or override via extraBlockSpecs.
|
|
13
|
+
*/
|
|
14
|
+
export const BlogImageBlock = createReactBlockSpec(
|
|
15
|
+
{
|
|
16
|
+
type: 'image',
|
|
17
|
+
propSchema: {
|
|
18
|
+
url: { default: '' },
|
|
19
|
+
caption: { default: '' },
|
|
20
|
+
previewWidth: { default: 740 },
|
|
21
|
+
name: { default: '' },
|
|
22
|
+
showPreview: { default: true },
|
|
23
|
+
},
|
|
24
|
+
content: 'none',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
render: (props) => <ImageRenderer {...props} />,
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
function ImageRenderer({ block, editor }) {
|
|
32
|
+
const { url, caption } = block.props;
|
|
33
|
+
const [mode, setMode] = useState('idle'); // idle | embed | uploading
|
|
34
|
+
const [embedUrl, setEmbedUrl] = useState('');
|
|
35
|
+
const [embedError, setEmbedError] = useState('');
|
|
36
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
37
|
+
const [uploadStatus, setUploadStatus] = useState('');
|
|
38
|
+
const [editingCaption, setEditingCaption] = useState(false);
|
|
39
|
+
const [captionText, setCaptionText] = useState(caption || '');
|
|
40
|
+
const fileInputRef = useRef(null);
|
|
41
|
+
const blockRef = useRef(null);
|
|
42
|
+
const embedInputRef = useRef(null);
|
|
43
|
+
|
|
44
|
+
// Focus input when mode changes
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (mode === 'embed') setTimeout(() => embedInputRef.current?.focus(), 50);
|
|
47
|
+
}, [mode]);
|
|
48
|
+
|
|
49
|
+
// Keyboard: backspace to delete when focused
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const el = blockRef.current;
|
|
52
|
+
if (!el) return;
|
|
53
|
+
function handleKey(e) {
|
|
54
|
+
if ((e.key === 'Backspace' || e.key === 'Delete') && mode === 'idle' && !url) {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
try { editor.removeBlocks([block.id]); } catch {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
el.addEventListener('keydown', handleKey);
|
|
60
|
+
return () => el.removeEventListener('keydown', handleKey);
|
|
61
|
+
}, [editor, block.id, mode, url]);
|
|
62
|
+
|
|
63
|
+
// Upload — reads file as base64 data URL
|
|
64
|
+
const uploadFile = useCallback(async (file) => {
|
|
65
|
+
if (!file || !file.type.startsWith('image/')) return;
|
|
66
|
+
setMode('uploading');
|
|
67
|
+
setUploadStatus('Processing...');
|
|
68
|
+
try {
|
|
69
|
+
const reader = new FileReader();
|
|
70
|
+
reader.onload = () => {
|
|
71
|
+
editor.updateBlock(block.id, { props: { url: reader.result, name: file.name } });
|
|
72
|
+
setMode('idle');
|
|
73
|
+
};
|
|
74
|
+
reader.onerror = () => {
|
|
75
|
+
showFailToast('Failed to read image');
|
|
76
|
+
setMode('idle');
|
|
77
|
+
};
|
|
78
|
+
reader.readAsDataURL(file);
|
|
79
|
+
} catch {
|
|
80
|
+
setMode('idle');
|
|
81
|
+
}
|
|
82
|
+
}, [editor, block.id]);
|
|
83
|
+
|
|
84
|
+
// Paste handler
|
|
85
|
+
const handlePaste = useCallback((e) => {
|
|
86
|
+
const items = e.clipboardData?.items;
|
|
87
|
+
if (!items) return;
|
|
88
|
+
for (const item of items) {
|
|
89
|
+
if (item.type.startsWith('image/')) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
uploadFile(item.getAsFile());
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, [uploadFile]);
|
|
96
|
+
|
|
97
|
+
// Drag and drop
|
|
98
|
+
const handleDrop = useCallback((e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
setIsDragOver(false);
|
|
101
|
+
const file = e.dataTransfer?.files?.[0];
|
|
102
|
+
if (file?.type.startsWith('image/')) uploadFile(file);
|
|
103
|
+
}, [uploadFile]);
|
|
104
|
+
|
|
105
|
+
// Embed URL submit
|
|
106
|
+
const handleEmbed = useCallback(() => {
|
|
107
|
+
const trimmed = embedUrl.trim();
|
|
108
|
+
if (!trimmed) return;
|
|
109
|
+
if (!trimmed.startsWith('http')) {
|
|
110
|
+
setEmbedError('URL must start with http:// or https://');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
editor.updateBlock(block.id, { props: { url: trimmed } });
|
|
114
|
+
setMode('idle');
|
|
115
|
+
setEmbedUrl('');
|
|
116
|
+
setEmbedError('');
|
|
117
|
+
}, [embedUrl, editor, block.id]);
|
|
118
|
+
|
|
119
|
+
// Toast on failure
|
|
120
|
+
const showFailToast = useCallback((msg) => {
|
|
121
|
+
const toast = document.createElement('div');
|
|
122
|
+
toast.className = 'blog-img-fail-toast';
|
|
123
|
+
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>`;
|
|
124
|
+
document.body.appendChild(toast);
|
|
125
|
+
setTimeout(() => { toast.classList.add('blog-img-fail-toast--out'); }, 3200);
|
|
126
|
+
setTimeout(() => { toast.remove(); }, 3600);
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const handleDelete = useCallback(() => {
|
|
130
|
+
try { editor.removeBlocks([block.id]); } catch {}
|
|
131
|
+
}, [editor, block.id]);
|
|
132
|
+
|
|
133
|
+
const handleReplace = useCallback(() => {
|
|
134
|
+
editor.updateBlock(block.id, { props: { url: '' } });
|
|
135
|
+
setMode('idle');
|
|
136
|
+
}, [editor, block.id]);
|
|
137
|
+
|
|
138
|
+
const handleCaptionSave = useCallback(() => {
|
|
139
|
+
editor.updateBlock(block.id, { props: { caption: captionText } });
|
|
140
|
+
setEditingCaption(false);
|
|
141
|
+
}, [editor, block.id, captionText]);
|
|
142
|
+
|
|
143
|
+
// ─── No image yet ───
|
|
144
|
+
if (!url) {
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
ref={blockRef}
|
|
148
|
+
className="blog-img-empty"
|
|
149
|
+
tabIndex={0}
|
|
150
|
+
onPaste={handlePaste}
|
|
151
|
+
onDrop={handleDrop}
|
|
152
|
+
onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }}
|
|
153
|
+
onDragLeave={() => setIsDragOver(false)}
|
|
154
|
+
data-drag-over={isDragOver}
|
|
155
|
+
>
|
|
156
|
+
<input
|
|
157
|
+
ref={fileInputRef}
|
|
158
|
+
type="file"
|
|
159
|
+
accept="image/*"
|
|
160
|
+
onChange={(e) => { if (e.target.files?.[0]) uploadFile(e.target.files[0]); e.target.value = ''; }}
|
|
161
|
+
style={{ display: 'none' }}
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
{/* Uploading state */}
|
|
165
|
+
{mode === 'uploading' && (
|
|
166
|
+
<div className="blog-img-status">
|
|
167
|
+
<div className="blog-img-spinner" />
|
|
168
|
+
<span>{uploadStatus}</span>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{/* Idle — 2 action buttons (no AI) */}
|
|
173
|
+
{mode === 'idle' && (
|
|
174
|
+
<>
|
|
175
|
+
<div className="blog-img-actions-row">
|
|
176
|
+
<button className="blog-img-action" onClick={() => fileInputRef.current?.click()}>
|
|
177
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
|
178
|
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
|
179
|
+
<polyline points="17 8 12 3 7 8" />
|
|
180
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
181
|
+
</svg>
|
|
182
|
+
Upload
|
|
183
|
+
</button>
|
|
184
|
+
<button className="blog-img-action" onClick={() => setMode('embed')}>
|
|
185
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
|
186
|
+
<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" />
|
|
187
|
+
<path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" />
|
|
188
|
+
</svg>
|
|
189
|
+
Embed URL
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
<p className="blog-img-hint">or drag & drop / paste an image</p>
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{/* Embed URL input */}
|
|
197
|
+
{mode === 'embed' && (
|
|
198
|
+
<div className="blog-img-input-row">
|
|
199
|
+
<input
|
|
200
|
+
ref={embedInputRef}
|
|
201
|
+
type="url"
|
|
202
|
+
value={embedUrl}
|
|
203
|
+
onChange={(e) => { setEmbedUrl(e.target.value); setEmbedError(''); }}
|
|
204
|
+
onKeyDown={(e) => {
|
|
205
|
+
if (e.key === 'Enter') handleEmbed();
|
|
206
|
+
if (e.key === 'Escape') { setMode('idle'); setEmbedUrl(''); setEmbedError(''); }
|
|
207
|
+
}}
|
|
208
|
+
placeholder="https://example.com/image.jpg"
|
|
209
|
+
className="blog-img-url-input"
|
|
210
|
+
/>
|
|
211
|
+
<button className="blog-img-submit-btn" onClick={handleEmbed} disabled={!embedUrl.trim()}>
|
|
212
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
213
|
+
<polyline points="20 6 9 17 4 12" />
|
|
214
|
+
</svg>
|
|
215
|
+
</button>
|
|
216
|
+
<button className="blog-img-cancel-btn" onClick={() => { setMode('idle'); setEmbedUrl(''); setEmbedError(''); }}>
|
|
217
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
218
|
+
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
219
|
+
</svg>
|
|
220
|
+
</button>
|
|
221
|
+
{embedError && <span className="blog-img-error">{embedError}</span>}
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── Image loaded ───
|
|
229
|
+
return (
|
|
230
|
+
<div ref={blockRef} className="blog-img-loaded" tabIndex={0} onPaste={handlePaste}>
|
|
231
|
+
<div className="blog-img-wrapper">
|
|
232
|
+
<img src={url} alt={caption || 'Image'} className="blog-img-main" draggable={false} />
|
|
233
|
+
<div className="blog-img-hover-overlay">
|
|
234
|
+
<div className="blog-img-hover-actions">
|
|
235
|
+
<button className="blog-img-hover-btn" onClick={handleReplace}>
|
|
236
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
237
|
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
|
|
238
|
+
</svg>
|
|
239
|
+
</button>
|
|
240
|
+
<button className="blog-img-hover-btn" onClick={() => setEditingCaption(true)}>
|
|
241
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
242
|
+
<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"/>
|
|
243
|
+
</svg>
|
|
244
|
+
</button>
|
|
245
|
+
<button className="blog-img-hover-btn blog-img-hover-delete" onClick={handleDelete}>
|
|
246
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
247
|
+
<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"/>
|
|
248
|
+
</svg>
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
{editingCaption ? (
|
|
254
|
+
<input
|
|
255
|
+
type="text"
|
|
256
|
+
value={captionText}
|
|
257
|
+
onChange={(e) => setCaptionText(e.target.value)}
|
|
258
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleCaptionSave(); if (e.key === 'Escape') { setEditingCaption(false); setCaptionText(caption || ''); } }}
|
|
259
|
+
onBlur={handleCaptionSave}
|
|
260
|
+
placeholder="Add a caption..."
|
|
261
|
+
className="blog-img-caption-input"
|
|
262
|
+
autoFocus
|
|
263
|
+
/>
|
|
264
|
+
) : (
|
|
265
|
+
<p
|
|
266
|
+
className={`blog-img-caption ${caption ? '' : 'blog-img-caption--empty'}`}
|
|
267
|
+
onClick={() => { setCaptionText(caption || ''); setEditingCaption(true); }}
|
|
268
|
+
>
|
|
269
|
+
{caption || 'Add a caption...'}
|
|
270
|
+
</p>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createReactInlineContentSpec } from '@blocknote/react';
|
|
4
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
5
|
+
import katex from 'katex';
|
|
6
|
+
|
|
7
|
+
function stripDelimiters(raw) {
|
|
8
|
+
let s = raw.trim();
|
|
9
|
+
if (s.startsWith('\\(') && s.endsWith('\\)')) return s.slice(2, -2).trim();
|
|
10
|
+
if (s.startsWith('\\[') && s.endsWith('\\]')) return s.slice(2, -2).trim();
|
|
11
|
+
if (s.startsWith('$$') && s.endsWith('$$')) return s.slice(2, -2).trim();
|
|
12
|
+
if (s.startsWith('$') && s.endsWith('$') && s.length > 2) return s.slice(1, -1).trim();
|
|
13
|
+
return s;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function renderKaTeXInline(latex) {
|
|
17
|
+
try {
|
|
18
|
+
return katex.renderToString(stripDelimiters(latex), { displayMode: false, throwOnError: false });
|
|
19
|
+
} catch {
|
|
20
|
+
return `<span style="color:#f87171">${latex}</span>`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function InlineEquationChip({ inlineContent }) {
|
|
25
|
+
const [editing, setEditing] = useState(false);
|
|
26
|
+
const [value, setValue] = useState(inlineContent.props.latex || '');
|
|
27
|
+
const inputRef = useRef(null);
|
|
28
|
+
const popupRef = useRef(null);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (editing && inputRef.current) inputRef.current.focus();
|
|
32
|
+
}, [editing]);
|
|
33
|
+
|
|
34
|
+
// Close on click outside
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!editing) return;
|
|
37
|
+
function handleClick(e) {
|
|
38
|
+
if (popupRef.current && !popupRef.current.contains(e.target)) {
|
|
39
|
+
setEditing(false);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
document.addEventListener('mousedown', handleClick);
|
|
43
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
44
|
+
}, [editing]);
|
|
45
|
+
|
|
46
|
+
const save = useCallback(() => {
|
|
47
|
+
if (value.trim()) {
|
|
48
|
+
inlineContent.props.latex = value.trim();
|
|
49
|
+
}
|
|
50
|
+
setEditing(false);
|
|
51
|
+
}, [value, inlineContent]);
|
|
52
|
+
|
|
53
|
+
const html = renderKaTeXInline(inlineContent.props.latex);
|
|
54
|
+
|
|
55
|
+
// Live preview while editing
|
|
56
|
+
const previewHtml = value.trim() ? renderKaTeXInline(value) : '';
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<span className="relative inline-flex items-center">
|
|
60
|
+
<span
|
|
61
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setValue(inlineContent.props.latex || ''); setEditing(!editing); }}
|
|
62
|
+
className="inline-equation-chip"
|
|
63
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
64
|
+
title={inlineContent.props.latex}
|
|
65
|
+
/>
|
|
66
|
+
{editing && (
|
|
67
|
+
<div
|
|
68
|
+
ref={popupRef}
|
|
69
|
+
className="inline-equation-editor"
|
|
70
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
71
|
+
>
|
|
72
|
+
<input
|
|
73
|
+
ref={inputRef}
|
|
74
|
+
type="text"
|
|
75
|
+
className="inline-equation-editor-input"
|
|
76
|
+
value={value}
|
|
77
|
+
onChange={(e) => setValue(e.target.value)}
|
|
78
|
+
onKeyDown={(e) => {
|
|
79
|
+
if (e.key === 'Enter') { e.preventDefault(); save(); }
|
|
80
|
+
if (e.key === 'Escape') { setEditing(false); }
|
|
81
|
+
}}
|
|
82
|
+
placeholder="E = mc^2"
|
|
83
|
+
/>
|
|
84
|
+
{previewHtml && (
|
|
85
|
+
<div className="inline-equation-editor-preview" dangerouslySetInnerHTML={{ __html: previewHtml }} />
|
|
86
|
+
)}
|
|
87
|
+
<div className="inline-equation-editor-actions">
|
|
88
|
+
<button className="mermaid-btn-cancel" onClick={() => setEditing(false)}>Cancel</button>
|
|
89
|
+
<button className="mermaid-btn-save" disabled={!value.trim()} onClick={save}>Save</button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</span>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const InlineEquation = createReactInlineContentSpec(
|
|
98
|
+
{
|
|
99
|
+
type: 'inlineEquation',
|
|
100
|
+
propSchema: {
|
|
101
|
+
latex: { default: 'x^2' },
|
|
102
|
+
},
|
|
103
|
+
content: 'none',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
render: (props) => <InlineEquationChip {...props} />,
|
|
107
|
+
}
|
|
108
|
+
);
|