@puredesktop/platform-editor 1.0.0-beta.1 → 2.1.0
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/package.json +1 -1
- package/src/CollapsePanel.tsx +35 -35
- package/src/DocumentDiffView.tsx +130 -130
- package/src/DocumentEditor.test.ts +118 -118
- package/src/DocumentEditor.tsx +2651 -2631
- package/src/alignedLineDiff.test.ts +52 -52
- package/src/alignedLineDiff.ts +67 -67
- package/src/collectionAssetSrc.ts +2 -2
- package/src/commentUtils.test.ts +350 -350
- package/src/commentUtils.ts +546 -546
- package/src/constants/toolKeys.ts +14 -14
- package/src/contentFormat.test.ts +27 -27
- package/src/contentFormat.ts +18 -18
- package/src/editorExtensions.ts +155 -155
- package/src/extensions/appliedChangeMark.ts +115 -115
- package/src/extensions/autoReviewPrompts.test.ts +529 -529
- package/src/extensions/autoReviewPrompts.ts +1442 -1442
- package/src/extensions/collectionImage.ts +26 -26
- package/src/extensions/collectionImagePaste.ts +215 -215
- package/src/extensions/commentMark.ts +223 -223
- package/src/extensions/documentAssetIdentity.ts +54 -54
- package/src/extensions/figure.ts +92 -92
- package/src/extensions/footnote.ts +186 -186
- package/src/extensions/indexMarker.ts +88 -88
- package/src/extensions/mathEditing.ts +239 -239
- package/src/extensions/mermaidBlock.tsx +297 -297
- package/src/extensions/slashCommands.test.ts +170 -170
- package/src/extensions/slashCommands.ts +746 -746
- package/src/extensions/smartTypography.test.ts +74 -74
- package/src/extensions/smartTypography.ts +120 -120
- package/src/index.ts +138 -137
- package/src/insertCollectionImage.test.ts +60 -60
- package/src/insertCollectionImage.ts +63 -63
- package/src/insertInlineAssetKind.test.ts +67 -67
- package/src/insertInlineAssetKind.ts +49 -49
- package/src/mermaidPreview.test.ts +54 -54
- package/src/mermaidPreview.ts +115 -115
- package/src/styled.ts +697 -676
- package/src/toolbar/ToolBtn.tsx +77 -63
- package/src/toolbar/Toolbar.test.tsx +325 -325
- package/src/toolbar/Toolbar.tsx +662 -624
- package/src/toolbar/groups/HeadingGroup.tsx +175 -153
- package/src/toolbar/overlayTypes.ts +28 -28
- package/src/useEditorExtensions.ts +22 -22
- package/src/utils/markdownUtils.ts +331 -331
|
@@ -1,297 +1,297 @@
|
|
|
1
|
-
import { Node, mergeAttributes } from '@tiptap/core'
|
|
2
|
-
import {
|
|
3
|
-
NodeViewWrapper,
|
|
4
|
-
ReactNodeViewRenderer,
|
|
5
|
-
type NodeViewProps,
|
|
6
|
-
} from '@tiptap/react'
|
|
7
|
-
import { useEffect, useState, type ChangeEvent } from 'react'
|
|
8
|
-
import { styled } from 'styled-components'
|
|
9
|
-
import { renderMermaidPreviewSvg } from '../mermaidPreview.js'
|
|
10
|
-
|
|
11
|
-
export interface MermaidBlockOptions {
|
|
12
|
-
HTMLAttributes: Record<string, unknown>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
declare module '@tiptap/core' {
|
|
16
|
-
interface Commands<ReturnType> {
|
|
17
|
-
mermaidBlock: {
|
|
18
|
-
/** Insert an editable Mermaid diagram block. */
|
|
19
|
-
insertMermaidBlock: (code?: string) => ReturnType
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const DEFAULT_MERMAID = `flowchart TD
|
|
25
|
-
A[Start] --> B[Draft]
|
|
26
|
-
B --> C[Review]
|
|
27
|
-
C --> D[Publish]`
|
|
28
|
-
|
|
29
|
-
function readMermaidCode(element: HTMLElement): string {
|
|
30
|
-
const source = element.matches('code')
|
|
31
|
-
? element
|
|
32
|
-
: element.querySelector('pre code.language-mermaid, code.language-mermaid')
|
|
33
|
-
return (source?.textContent ?? element.getAttribute('data-code') ?? '').trim()
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function createMermaidAssetId(): string {
|
|
37
|
-
return `asset_${
|
|
38
|
-
globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)
|
|
39
|
-
}`
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export const MermaidBlock = Node.create<MermaidBlockOptions>({
|
|
43
|
-
name: 'mermaidBlock',
|
|
44
|
-
group: 'block',
|
|
45
|
-
atom: true,
|
|
46
|
-
isolating: true,
|
|
47
|
-
defining: true,
|
|
48
|
-
draggable: true,
|
|
49
|
-
|
|
50
|
-
addOptions() {
|
|
51
|
-
return { HTMLAttributes: {} }
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
addAttributes() {
|
|
55
|
-
return {
|
|
56
|
-
code: {
|
|
57
|
-
default: DEFAULT_MERMAID,
|
|
58
|
-
parseHTML: element => readMermaidCode(element as HTMLElement),
|
|
59
|
-
rendered: false,
|
|
60
|
-
},
|
|
61
|
-
assetId: {
|
|
62
|
-
default: null,
|
|
63
|
-
parseHTML: element =>
|
|
64
|
-
(element as HTMLElement).getAttribute('data-asset-id')?.trim() ||
|
|
65
|
-
null,
|
|
66
|
-
},
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
parseHTML() {
|
|
71
|
-
return [
|
|
72
|
-
{ tag: 'figure[data-type="mermaid"]' },
|
|
73
|
-
{ tag: 'div[data-type="mermaid"]' },
|
|
74
|
-
{ tag: 'pre code.language-mermaid' },
|
|
75
|
-
]
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
renderHTML({ node, HTMLAttributes }) {
|
|
79
|
-
const code = String(node.attrs.code ?? DEFAULT_MERMAID)
|
|
80
|
-
const assetId = String(node.attrs.assetId ?? '').trim()
|
|
81
|
-
return [
|
|
82
|
-
'figure',
|
|
83
|
-
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
84
|
-
'data-type': 'mermaid',
|
|
85
|
-
...(assetId ? { 'data-asset-id': assetId } : {}),
|
|
86
|
-
class: 'mermaid-block',
|
|
87
|
-
}),
|
|
88
|
-
['pre', {}, ['code', { class: 'language-mermaid' }, code]],
|
|
89
|
-
]
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
addNodeView() {
|
|
93
|
-
return ReactNodeViewRenderer(MermaidBlockView)
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
addCommands() {
|
|
97
|
-
return {
|
|
98
|
-
insertMermaidBlock:
|
|
99
|
-
(code = DEFAULT_MERMAID) =>
|
|
100
|
-
({ commands }) =>
|
|
101
|
-
commands.insertContent({
|
|
102
|
-
type: this.name,
|
|
103
|
-
attrs: { code, assetId: createMermaidAssetId() },
|
|
104
|
-
}),
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
function MermaidBlockView({
|
|
110
|
-
node,
|
|
111
|
-
selected,
|
|
112
|
-
updateAttributes,
|
|
113
|
-
}: NodeViewProps): React.ReactElement {
|
|
114
|
-
const code = String(node.attrs.code ?? DEFAULT_MERMAID)
|
|
115
|
-
const assetId = String(node.attrs.assetId ?? '').trim()
|
|
116
|
-
const [isEditing, setIsEditing] = useState(false)
|
|
117
|
-
const [svg, setSvg] = useState('')
|
|
118
|
-
const [error, setError] = useState<string | null>(null)
|
|
119
|
-
const [isRendering, setIsRendering] = useState(true)
|
|
120
|
-
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
if (!assetId) updateAttributes({ assetId: createMermaidAssetId() })
|
|
123
|
-
}, [assetId, updateAttributes])
|
|
124
|
-
|
|
125
|
-
useEffect(() => {
|
|
126
|
-
let cancelled = false
|
|
127
|
-
setIsRendering(true)
|
|
128
|
-
const timeout = window.setTimeout(
|
|
129
|
-
() =>
|
|
130
|
-
void (async () => {
|
|
131
|
-
try {
|
|
132
|
-
const svg = await renderMermaidPreviewSvg(
|
|
133
|
-
code.trim() || DEFAULT_MERMAID,
|
|
134
|
-
)
|
|
135
|
-
if (cancelled) return
|
|
136
|
-
setSvg(svg)
|
|
137
|
-
setError(null)
|
|
138
|
-
} catch (err) {
|
|
139
|
-
if (cancelled) return
|
|
140
|
-
setSvg('')
|
|
141
|
-
setError(
|
|
142
|
-
err instanceof Error ? err.message : 'Could not render diagram.',
|
|
143
|
-
)
|
|
144
|
-
} finally {
|
|
145
|
-
if (!cancelled) setIsRendering(false)
|
|
146
|
-
}
|
|
147
|
-
})(),
|
|
148
|
-
120,
|
|
149
|
-
)
|
|
150
|
-
return () => {
|
|
151
|
-
cancelled = true
|
|
152
|
-
window.clearTimeout(timeout)
|
|
153
|
-
}
|
|
154
|
-
}, [code])
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
<NodeViewWrapper>
|
|
158
|
-
<StyledBlock $selected={selected}>
|
|
159
|
-
<StyledHeader contentEditable={false}>
|
|
160
|
-
<StyledEyebrow>Mermaid diagram</StyledEyebrow>
|
|
161
|
-
<StyledToggle
|
|
162
|
-
type="button"
|
|
163
|
-
onClick={() => setIsEditing(current => !current)}
|
|
164
|
-
>
|
|
165
|
-
{isEditing ? 'Preview' : 'Edit'}
|
|
166
|
-
</StyledToggle>
|
|
167
|
-
</StyledHeader>
|
|
168
|
-
{isEditing ? (
|
|
169
|
-
<StyledEditorPanel contentEditable={false}>
|
|
170
|
-
<StyledTextarea
|
|
171
|
-
value={code}
|
|
172
|
-
spellCheck={false}
|
|
173
|
-
onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
|
|
174
|
-
updateAttributes({ code: event.target.value })
|
|
175
|
-
}
|
|
176
|
-
/>
|
|
177
|
-
</StyledEditorPanel>
|
|
178
|
-
) : null}
|
|
179
|
-
<StyledPreview contentEditable={false}>
|
|
180
|
-
{isRendering ? (
|
|
181
|
-
<StyledLoading>Rendering diagram…</StyledLoading>
|
|
182
|
-
) : error ? (
|
|
183
|
-
<StyledError>
|
|
184
|
-
<strong>Mermaid could not render this diagram.</strong>
|
|
185
|
-
<span>{error}</span>
|
|
186
|
-
</StyledError>
|
|
187
|
-
) : (
|
|
188
|
-
<StyledSvg dangerouslySetInnerHTML={{ __html: svg }} />
|
|
189
|
-
)}
|
|
190
|
-
</StyledPreview>
|
|
191
|
-
</StyledBlock>
|
|
192
|
-
</NodeViewWrapper>
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const StyledBlock = styled.figure<{ $selected: boolean }>`
|
|
197
|
-
background: var(--platform-colors-surface);
|
|
198
|
-
border: 1px solid
|
|
199
|
-
${({ $selected }) =>
|
|
200
|
-
$selected
|
|
201
|
-
? 'var(--platform-colors-focus)'
|
|
202
|
-
: 'rgb(from var(--platform-colors-border) r g b / 0.9)'};
|
|
203
|
-
border-radius: 14px;
|
|
204
|
-
box-shadow: ${({ $selected }) =>
|
|
205
|
-
$selected
|
|
206
|
-
? '0 0 0 3px rgb(from var(--platform-colors-focus) r g b / 0.16)'
|
|
207
|
-
: 'none'};
|
|
208
|
-
margin: 1.4em 0;
|
|
209
|
-
overflow: hidden;
|
|
210
|
-
`
|
|
211
|
-
|
|
212
|
-
const StyledHeader = styled.div`
|
|
213
|
-
align-items: center;
|
|
214
|
-
border-bottom: 1px solid rgb(from var(--platform-colors-border) r g b / 0.68);
|
|
215
|
-
display: flex;
|
|
216
|
-
justify-content: space-between;
|
|
217
|
-
padding: 10px 12px;
|
|
218
|
-
`
|
|
219
|
-
|
|
220
|
-
const StyledEyebrow = styled.span`
|
|
221
|
-
color: var(--platform-colors-text-secondary);
|
|
222
|
-
font-size: 11px;
|
|
223
|
-
font-weight: 700;
|
|
224
|
-
letter-spacing: 0.12em;
|
|
225
|
-
text-transform: uppercase;
|
|
226
|
-
`
|
|
227
|
-
|
|
228
|
-
const StyledToggle = styled.button`
|
|
229
|
-
background: transparent;
|
|
230
|
-
border: 1px solid rgb(from var(--platform-colors-border) r g b / 0.82);
|
|
231
|
-
border-radius: 999px;
|
|
232
|
-
color: var(--platform-colors-text);
|
|
233
|
-
font-size: 12px;
|
|
234
|
-
font-weight: 650;
|
|
235
|
-
padding: 5px 10px;
|
|
236
|
-
|
|
237
|
-
&:hover {
|
|
238
|
-
background: var(--platform-colors-surface-hover);
|
|
239
|
-
}
|
|
240
|
-
`
|
|
241
|
-
|
|
242
|
-
const StyledEditorPanel = styled.div`
|
|
243
|
-
border-bottom: 1px solid rgb(from var(--platform-colors-border) r g b / 0.68);
|
|
244
|
-
padding: 12px;
|
|
245
|
-
`
|
|
246
|
-
|
|
247
|
-
const StyledTextarea = styled.textarea`
|
|
248
|
-
background: rgb(from var(--platform-colors-app-viewport) r g b / 0.74);
|
|
249
|
-
border: 1px solid rgb(from var(--platform-colors-border) r g b / 0.82);
|
|
250
|
-
border-radius: 10px;
|
|
251
|
-
color: var(--platform-colors-text);
|
|
252
|
-
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
253
|
-
font-size: 13px;
|
|
254
|
-
line-height: 1.45;
|
|
255
|
-
min-height: 160px;
|
|
256
|
-
padding: 12px;
|
|
257
|
-
resize: vertical;
|
|
258
|
-
width: 100%;
|
|
259
|
-
`
|
|
260
|
-
|
|
261
|
-
const StyledPreview = styled.div`
|
|
262
|
-
align-items: center;
|
|
263
|
-
background: rgb(from var(--platform-colors-app-viewport) r g b / 0.42);
|
|
264
|
-
display: flex;
|
|
265
|
-
justify-content: center;
|
|
266
|
-
min-height: 180px;
|
|
267
|
-
padding: 18px;
|
|
268
|
-
`
|
|
269
|
-
|
|
270
|
-
const StyledSvg = styled.div`
|
|
271
|
-
max-width: 100%;
|
|
272
|
-
overflow: auto;
|
|
273
|
-
|
|
274
|
-
svg {
|
|
275
|
-
display: block;
|
|
276
|
-
height: auto;
|
|
277
|
-
max-width: 100%;
|
|
278
|
-
}
|
|
279
|
-
`
|
|
280
|
-
|
|
281
|
-
const StyledLoading = styled.div`
|
|
282
|
-
color: var(--platform-colors-text-secondary);
|
|
283
|
-
font-size: 13px;
|
|
284
|
-
`
|
|
285
|
-
|
|
286
|
-
const StyledError = styled.div`
|
|
287
|
-
color: var(--platform-colors-text-danger);
|
|
288
|
-
display: grid;
|
|
289
|
-
gap: 6px;
|
|
290
|
-
justify-items: center;
|
|
291
|
-
text-align: center;
|
|
292
|
-
|
|
293
|
-
span {
|
|
294
|
-
color: var(--platform-colors-text-secondary);
|
|
295
|
-
font-size: 12px;
|
|
296
|
-
}
|
|
297
|
-
`
|
|
1
|
+
import { Node, mergeAttributes } from '@tiptap/core'
|
|
2
|
+
import {
|
|
3
|
+
NodeViewWrapper,
|
|
4
|
+
ReactNodeViewRenderer,
|
|
5
|
+
type NodeViewProps,
|
|
6
|
+
} from '@tiptap/react'
|
|
7
|
+
import { useEffect, useState, type ChangeEvent } from 'react'
|
|
8
|
+
import { styled } from 'styled-components'
|
|
9
|
+
import { renderMermaidPreviewSvg } from '../mermaidPreview.js'
|
|
10
|
+
|
|
11
|
+
export interface MermaidBlockOptions {
|
|
12
|
+
HTMLAttributes: Record<string, unknown>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare module '@tiptap/core' {
|
|
16
|
+
interface Commands<ReturnType> {
|
|
17
|
+
mermaidBlock: {
|
|
18
|
+
/** Insert an editable Mermaid diagram block. */
|
|
19
|
+
insertMermaidBlock: (code?: string) => ReturnType
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_MERMAID = `flowchart TD
|
|
25
|
+
A[Start] --> B[Draft]
|
|
26
|
+
B --> C[Review]
|
|
27
|
+
C --> D[Publish]`
|
|
28
|
+
|
|
29
|
+
function readMermaidCode(element: HTMLElement): string {
|
|
30
|
+
const source = element.matches('code')
|
|
31
|
+
? element
|
|
32
|
+
: element.querySelector('pre code.language-mermaid, code.language-mermaid')
|
|
33
|
+
return (source?.textContent ?? element.getAttribute('data-code') ?? '').trim()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createMermaidAssetId(): string {
|
|
37
|
+
return `asset_${
|
|
38
|
+
globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)
|
|
39
|
+
}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const MermaidBlock = Node.create<MermaidBlockOptions>({
|
|
43
|
+
name: 'mermaidBlock',
|
|
44
|
+
group: 'block',
|
|
45
|
+
atom: true,
|
|
46
|
+
isolating: true,
|
|
47
|
+
defining: true,
|
|
48
|
+
draggable: true,
|
|
49
|
+
|
|
50
|
+
addOptions() {
|
|
51
|
+
return { HTMLAttributes: {} }
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
addAttributes() {
|
|
55
|
+
return {
|
|
56
|
+
code: {
|
|
57
|
+
default: DEFAULT_MERMAID,
|
|
58
|
+
parseHTML: element => readMermaidCode(element as HTMLElement),
|
|
59
|
+
rendered: false,
|
|
60
|
+
},
|
|
61
|
+
assetId: {
|
|
62
|
+
default: null,
|
|
63
|
+
parseHTML: element =>
|
|
64
|
+
(element as HTMLElement).getAttribute('data-asset-id')?.trim() ||
|
|
65
|
+
null,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
parseHTML() {
|
|
71
|
+
return [
|
|
72
|
+
{ tag: 'figure[data-type="mermaid"]' },
|
|
73
|
+
{ tag: 'div[data-type="mermaid"]' },
|
|
74
|
+
{ tag: 'pre code.language-mermaid' },
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
79
|
+
const code = String(node.attrs.code ?? DEFAULT_MERMAID)
|
|
80
|
+
const assetId = String(node.attrs.assetId ?? '').trim()
|
|
81
|
+
return [
|
|
82
|
+
'figure',
|
|
83
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
84
|
+
'data-type': 'mermaid',
|
|
85
|
+
...(assetId ? { 'data-asset-id': assetId } : {}),
|
|
86
|
+
class: 'mermaid-block',
|
|
87
|
+
}),
|
|
88
|
+
['pre', {}, ['code', { class: 'language-mermaid' }, code]],
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
addNodeView() {
|
|
93
|
+
return ReactNodeViewRenderer(MermaidBlockView)
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
addCommands() {
|
|
97
|
+
return {
|
|
98
|
+
insertMermaidBlock:
|
|
99
|
+
(code = DEFAULT_MERMAID) =>
|
|
100
|
+
({ commands }) =>
|
|
101
|
+
commands.insertContent({
|
|
102
|
+
type: this.name,
|
|
103
|
+
attrs: { code, assetId: createMermaidAssetId() },
|
|
104
|
+
}),
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
function MermaidBlockView({
|
|
110
|
+
node,
|
|
111
|
+
selected,
|
|
112
|
+
updateAttributes,
|
|
113
|
+
}: NodeViewProps): React.ReactElement {
|
|
114
|
+
const code = String(node.attrs.code ?? DEFAULT_MERMAID)
|
|
115
|
+
const assetId = String(node.attrs.assetId ?? '').trim()
|
|
116
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
117
|
+
const [svg, setSvg] = useState('')
|
|
118
|
+
const [error, setError] = useState<string | null>(null)
|
|
119
|
+
const [isRendering, setIsRendering] = useState(true)
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!assetId) updateAttributes({ assetId: createMermaidAssetId() })
|
|
123
|
+
}, [assetId, updateAttributes])
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
let cancelled = false
|
|
127
|
+
setIsRendering(true)
|
|
128
|
+
const timeout = window.setTimeout(
|
|
129
|
+
() =>
|
|
130
|
+
void (async () => {
|
|
131
|
+
try {
|
|
132
|
+
const svg = await renderMermaidPreviewSvg(
|
|
133
|
+
code.trim() || DEFAULT_MERMAID,
|
|
134
|
+
)
|
|
135
|
+
if (cancelled) return
|
|
136
|
+
setSvg(svg)
|
|
137
|
+
setError(null)
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (cancelled) return
|
|
140
|
+
setSvg('')
|
|
141
|
+
setError(
|
|
142
|
+
err instanceof Error ? err.message : 'Could not render diagram.',
|
|
143
|
+
)
|
|
144
|
+
} finally {
|
|
145
|
+
if (!cancelled) setIsRendering(false)
|
|
146
|
+
}
|
|
147
|
+
})(),
|
|
148
|
+
120,
|
|
149
|
+
)
|
|
150
|
+
return () => {
|
|
151
|
+
cancelled = true
|
|
152
|
+
window.clearTimeout(timeout)
|
|
153
|
+
}
|
|
154
|
+
}, [code])
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<NodeViewWrapper>
|
|
158
|
+
<StyledBlock $selected={selected}>
|
|
159
|
+
<StyledHeader contentEditable={false}>
|
|
160
|
+
<StyledEyebrow>Mermaid diagram</StyledEyebrow>
|
|
161
|
+
<StyledToggle
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={() => setIsEditing(current => !current)}
|
|
164
|
+
>
|
|
165
|
+
{isEditing ? 'Preview' : 'Edit'}
|
|
166
|
+
</StyledToggle>
|
|
167
|
+
</StyledHeader>
|
|
168
|
+
{isEditing ? (
|
|
169
|
+
<StyledEditorPanel contentEditable={false}>
|
|
170
|
+
<StyledTextarea
|
|
171
|
+
value={code}
|
|
172
|
+
spellCheck={false}
|
|
173
|
+
onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
|
|
174
|
+
updateAttributes({ code: event.target.value })
|
|
175
|
+
}
|
|
176
|
+
/>
|
|
177
|
+
</StyledEditorPanel>
|
|
178
|
+
) : null}
|
|
179
|
+
<StyledPreview contentEditable={false}>
|
|
180
|
+
{isRendering ? (
|
|
181
|
+
<StyledLoading>Rendering diagram…</StyledLoading>
|
|
182
|
+
) : error ? (
|
|
183
|
+
<StyledError>
|
|
184
|
+
<strong>Mermaid could not render this diagram.</strong>
|
|
185
|
+
<span>{error}</span>
|
|
186
|
+
</StyledError>
|
|
187
|
+
) : (
|
|
188
|
+
<StyledSvg dangerouslySetInnerHTML={{ __html: svg }} />
|
|
189
|
+
)}
|
|
190
|
+
</StyledPreview>
|
|
191
|
+
</StyledBlock>
|
|
192
|
+
</NodeViewWrapper>
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const StyledBlock = styled.figure<{ $selected: boolean }>`
|
|
197
|
+
background: var(--platform-colors-surface);
|
|
198
|
+
border: 1px solid
|
|
199
|
+
${({ $selected }) =>
|
|
200
|
+
$selected
|
|
201
|
+
? 'var(--platform-colors-focus)'
|
|
202
|
+
: 'rgb(from var(--platform-colors-border) r g b / 0.9)'};
|
|
203
|
+
border-radius: 14px;
|
|
204
|
+
box-shadow: ${({ $selected }) =>
|
|
205
|
+
$selected
|
|
206
|
+
? '0 0 0 3px rgb(from var(--platform-colors-focus) r g b / 0.16)'
|
|
207
|
+
: 'none'};
|
|
208
|
+
margin: 1.4em 0;
|
|
209
|
+
overflow: hidden;
|
|
210
|
+
`
|
|
211
|
+
|
|
212
|
+
const StyledHeader = styled.div`
|
|
213
|
+
align-items: center;
|
|
214
|
+
border-bottom: 1px solid rgb(from var(--platform-colors-border) r g b / 0.68);
|
|
215
|
+
display: flex;
|
|
216
|
+
justify-content: space-between;
|
|
217
|
+
padding: 10px 12px;
|
|
218
|
+
`
|
|
219
|
+
|
|
220
|
+
const StyledEyebrow = styled.span`
|
|
221
|
+
color: var(--platform-colors-text-secondary);
|
|
222
|
+
font-size: 11px;
|
|
223
|
+
font-weight: 700;
|
|
224
|
+
letter-spacing: 0.12em;
|
|
225
|
+
text-transform: uppercase;
|
|
226
|
+
`
|
|
227
|
+
|
|
228
|
+
const StyledToggle = styled.button`
|
|
229
|
+
background: transparent;
|
|
230
|
+
border: 1px solid rgb(from var(--platform-colors-border) r g b / 0.82);
|
|
231
|
+
border-radius: 999px;
|
|
232
|
+
color: var(--platform-colors-text);
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
font-weight: 650;
|
|
235
|
+
padding: 5px 10px;
|
|
236
|
+
|
|
237
|
+
&:hover {
|
|
238
|
+
background: var(--platform-colors-surface-hover);
|
|
239
|
+
}
|
|
240
|
+
`
|
|
241
|
+
|
|
242
|
+
const StyledEditorPanel = styled.div`
|
|
243
|
+
border-bottom: 1px solid rgb(from var(--platform-colors-border) r g b / 0.68);
|
|
244
|
+
padding: 12px;
|
|
245
|
+
`
|
|
246
|
+
|
|
247
|
+
const StyledTextarea = styled.textarea`
|
|
248
|
+
background: rgb(from var(--platform-colors-app-viewport) r g b / 0.74);
|
|
249
|
+
border: 1px solid rgb(from var(--platform-colors-border) r g b / 0.82);
|
|
250
|
+
border-radius: 10px;
|
|
251
|
+
color: var(--platform-colors-text);
|
|
252
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
253
|
+
font-size: 13px;
|
|
254
|
+
line-height: 1.45;
|
|
255
|
+
min-height: 160px;
|
|
256
|
+
padding: 12px;
|
|
257
|
+
resize: vertical;
|
|
258
|
+
width: 100%;
|
|
259
|
+
`
|
|
260
|
+
|
|
261
|
+
const StyledPreview = styled.div`
|
|
262
|
+
align-items: center;
|
|
263
|
+
background: rgb(from var(--platform-colors-app-viewport) r g b / 0.42);
|
|
264
|
+
display: flex;
|
|
265
|
+
justify-content: center;
|
|
266
|
+
min-height: 180px;
|
|
267
|
+
padding: 18px;
|
|
268
|
+
`
|
|
269
|
+
|
|
270
|
+
const StyledSvg = styled.div`
|
|
271
|
+
max-width: 100%;
|
|
272
|
+
overflow: auto;
|
|
273
|
+
|
|
274
|
+
svg {
|
|
275
|
+
display: block;
|
|
276
|
+
height: auto;
|
|
277
|
+
max-width: 100%;
|
|
278
|
+
}
|
|
279
|
+
`
|
|
280
|
+
|
|
281
|
+
const StyledLoading = styled.div`
|
|
282
|
+
color: var(--platform-colors-text-secondary);
|
|
283
|
+
font-size: 13px;
|
|
284
|
+
`
|
|
285
|
+
|
|
286
|
+
const StyledError = styled.div`
|
|
287
|
+
color: var(--platform-colors-text-danger);
|
|
288
|
+
display: grid;
|
|
289
|
+
gap: 6px;
|
|
290
|
+
justify-items: center;
|
|
291
|
+
text-align: center;
|
|
292
|
+
|
|
293
|
+
span {
|
|
294
|
+
color: var(--platform-colors-text-secondary);
|
|
295
|
+
font-size: 12px;
|
|
296
|
+
}
|
|
297
|
+
`
|