@bendyline/squisq-editor-react 1.5.2 → 1.6.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/dist/index.d.ts +757 -20
- package/dist/index.js +16910 -6844
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/BlockCardView.tsx +121 -0
- package/src/BlockPreviewPanel.tsx +69 -0
- package/src/BlockPropertiesPopover.tsx +191 -0
- package/src/EditorContext.tsx +143 -0
- package/src/EditorShell.tsx +200 -120
- package/src/FolderView.tsx +131 -0
- package/src/Icon.tsx +26 -0
- package/src/ImageEditor.tsx +69 -22
- package/src/OutlinePanel.tsx +38 -3
- package/src/PlainHtmlPreview.tsx +30 -3
- package/src/PreviewControls.tsx +180 -8
- package/src/RawEditor.tsx +216 -29
- package/src/RecorderEntry.tsx +9 -16
- package/src/TemplateAnnotation.ts +44 -0
- package/src/TemplatePicker.tsx +329 -54
- package/src/ThemeCustomizerPanel.tsx +30 -336
- package/src/ThemePicker.tsx +112 -3
- package/src/TimelineBlockPreview.tsx +37 -0
- package/src/TimelineTrack.tsx +671 -0
- package/src/Toolbar.tsx +528 -174
- package/src/Tooltip.tsx +22 -4
- package/src/TransitionPicker.tsx +351 -0
- package/src/VersionHistoryPanel.tsx +61 -31
- package/src/ViewMenuPanel.tsx +17 -14
- package/src/WysiwygEditor.tsx +161 -65
- package/src/__tests__/blockProperties.test.ts +92 -0
- package/src/__tests__/blockRange.test.ts +105 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +73 -0
- package/src/__tests__/createShapeLayer.test.ts +46 -0
- package/src/__tests__/drawingShapeRoundTrip.test.ts +49 -0
- package/src/__tests__/embeddedMedia.test.ts +48 -0
- package/src/__tests__/headingTransition.test.ts +138 -0
- package/src/__tests__/layoutChildRoundTrip.test.ts +71 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +10 -8
- package/src/__tests__/recorderMediaInsert.test.ts +86 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +18 -0
- package/src/__tests__/templatePickerMetadata.test.ts +32 -0
- package/src/__tests__/timelineSource.test.ts +134 -0
- package/src/__tests__/tiptapBridge.test.ts +92 -0
- package/src/__tests__/tiptapBridgeConformance.test.ts +47 -0
- package/src/__tests__/tooltip.test.tsx +72 -0
- package/src/__tests__/transitionCatalog.test.ts +64 -0
- package/src/__tests__/useBlockNavigator.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +24 -0
- package/src/__tests__/useTimelineClock.test.ts +21 -0
- package/src/blockProperties.ts +88 -0
- package/src/blockRange.ts +132 -0
- package/src/buildPreviewDoc.ts +98 -9
- package/src/customTemplates/AddBin.tsx +126 -0
- package/src/customTemplates/CustomLayoutManager.tsx +233 -0
- package/src/customTemplates/CustomTemplateContext.tsx +182 -0
- package/src/customTemplates/LayerToolbar.tsx +580 -0
- package/src/customTemplates/ShapeGlyph.tsx +47 -0
- package/src/customTemplates/TemplateDesigner.tsx +430 -0
- package/src/customTemplates/__tests__/library.test.ts +88 -0
- package/src/customTemplates/__tests__/normalizePositions.test.ts +109 -0
- package/src/customTemplates/__tests__/shapeDefs.test.ts +49 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +95 -0
- package/src/customTemplates/designer.css +673 -0
- package/src/customTemplates/index.ts +31 -0
- package/src/customTemplates/library.ts +97 -0
- package/src/customTemplates/normalizePositions.ts +75 -0
- package/src/customTemplates/shapeDefs.ts +131 -0
- package/src/customTemplates/thumbnail.tsx +63 -0
- package/src/customTemplates/tokenDefs.ts +60 -0
- package/src/customTemplates/useDocCustomTemplates.ts +52 -0
- package/src/customTemplates/useMemoryLayerAdapter.ts +123 -0
- package/src/customThemes/CustomThemeContext.tsx +179 -0
- package/src/customThemes/CustomThemeDialog.tsx +286 -0
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +64 -0
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +47 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +51 -0
- package/src/customThemes/customThemeLibrary.ts +97 -0
- package/src/customThemes/index.ts +31 -0
- package/src/customThemes/themeControls.tsx +229 -0
- package/src/customThemes/themeDraft.ts +272 -0
- package/src/customThemes/useDocCustomThemes.ts +49 -0
- package/src/diagram/DiagramCanvas.tsx +240 -0
- package/src/diagram/DiagramExtension.ts +209 -0
- package/src/diagram/DiagramMaximizedOverlay.tsx +46 -0
- package/src/diagram/DiagramWidget.tsx +270 -0
- package/src/diagram/diagramCommands.ts +604 -0
- package/src/diagram/diagramConstants.ts +17 -0
- package/src/diagram/useDiagramData.ts +126 -0
- package/src/embeddedMedia.ts +78 -0
- package/src/frontmatter.ts +29 -0
- package/src/headingTransition.ts +231 -0
- package/src/imageEditor/CanvasSurface.tsx +383 -88
- package/src/imageEditor/PropertiesPanel.tsx +47 -1
- package/src/imageEditor/Toolbar.tsx +229 -16
- package/src/imageEditor/createShapeLayer.ts +280 -0
- package/src/imageEditor/icons.tsx +34 -114
- package/src/imageEditor/image-editor.css +54 -5
- package/src/imageEditor/state.ts +23 -3
- package/src/index.ts +77 -0
- package/src/recorder/RecorderModal.tsx +120 -53
- package/src/recorder/RecorderPanel.tsx +2 -26
- package/src/recorder/hooks/useMediaRecorder.ts +17 -2
- package/src/recorder/insertMediaBlock.ts +30 -0
- package/src/resolveBlockVisual.ts +33 -0
- package/src/scene/Scene.tsx +540 -0
- package/src/scene/SceneBlockExtension.ts +198 -0
- package/src/scene/SceneBlockToolbar.tsx +201 -0
- package/src/scene/SceneBlockWidget.tsx +434 -0
- package/src/scene/ScenePropsBar.tsx +85 -0
- package/src/scene/SceneSelection.tsx +107 -0
- package/src/scene/SceneViewport.tsx +102 -0
- package/src/scene/ShapePalette.tsx +181 -0
- package/src/scene/__tests__/DiagramAdapter.test.ts +56 -0
- package/src/scene/__tests__/bezierEdit.test.ts +85 -0
- package/src/scene/__tests__/blockLayers.test.ts +57 -0
- package/src/scene/__tests__/shapeLayers.test.ts +106 -0
- package/src/scene/__tests__/useSceneHitTest.test.ts +90 -0
- package/src/scene/__tests__/useScenePanZoom.test.ts +103 -0
- package/src/scene/adapters/DiagramAdapter.ts +168 -0
- package/src/scene/adapters/DrawingAdapter.ts +415 -0
- package/src/scene/adapters/LayoutAdapter.ts +310 -0
- package/src/scene/adapters/blockLayers.ts +159 -0
- package/src/scene/commands/SceneCommand.ts +70 -0
- package/src/scene/commands/drawingCommands.ts +318 -0
- package/src/scene/commands/layoutCommands.ts +301 -0
- package/src/scene/hooks/useSceneHitTest.ts +105 -0
- package/src/scene/hooks/useScenePanZoom.ts +147 -0
- package/src/scene/hooks/useSceneSelection.ts +62 -0
- package/src/scene/index.ts +95 -0
- package/src/scene/layers/DiagramEdges.tsx +127 -0
- package/src/scene/layers/edgeGeometry.ts +77 -0
- package/src/scene/layers/nodeCard.tsx +145 -0
- package/src/scene/layers/renderLayer.tsx +70 -0
- package/src/scene/layers/shapeLayers.ts +201 -0
- package/src/scene/paths/bezierEdit.ts +208 -0
- package/src/scene/scene.css +649 -0
- package/src/scene/text/SceneTextOverlay.tsx +161 -0
- package/src/scene/text/sceneTextChannel.ts +40 -0
- package/src/scene/text/sceneTextConfig.ts +27 -0
- package/src/scene/text/sceneTiptap.ts +36 -0
- package/src/scene/text/useSceneTextEditing.ts +39 -0
- package/src/scene/tools/ConnectTool.ts +111 -0
- package/src/scene/tools/DrawingConnectTool.ts +161 -0
- package/src/scene/tools/PathTool.ts +158 -0
- package/src/scene/tools/PlaceTool.ts +47 -0
- package/src/scene/tools/SceneTool.ts +75 -0
- package/src/scene/tools/SelectTool.ts +284 -0
- package/src/scene/tools/ShapeTool.ts +144 -0
- package/src/scene/tools/TextTool.ts +72 -0
- package/src/scene/tools/TokenTool.ts +95 -0
- package/src/scene/tools/createDrawShapeTool.ts +82 -0
- package/src/styles/diagram.css +183 -0
- package/src/styles/editor.css +1615 -203
- package/src/styles/folder-view.css +210 -0
- package/src/styles/image-edit-affordance.css +2 -2
- package/src/styles/index.css +4 -0
- package/src/timelineSource.ts +244 -0
- package/src/tiptapBridge.ts +115 -34
- package/src/tooltipPlacement.ts +13 -0
- package/src/transitionCatalog.ts +159 -0
- package/src/types/monaco-shims.d.ts +10 -0
- package/src/useBlockNavigator.ts +153 -0
- package/src/useMonacoLoader.ts +105 -0
- package/src/useTimelineClock.ts +76 -0
- package/src/utils/dropUtils.ts +1 -1
package/src/tiptapBridge.ts
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import { templateLabel } from './TemplatePicker';
|
|
15
15
|
import { resolveIcon } from '@bendyline/squisq/icons';
|
|
16
|
+
import {
|
|
17
|
+
matchTrailingTemplateAnnotation,
|
|
18
|
+
matchTrailingPandocAttr,
|
|
19
|
+
tokenizeAttrTokens,
|
|
20
|
+
} from '@bendyline/squisq/markdown';
|
|
16
21
|
|
|
17
22
|
// Hoisted regex patterns for inline markdown ↔ HTML conversion
|
|
18
23
|
const RE_BOLD_STAR = /\*\*(.+?)\*\*/g;
|
|
@@ -194,20 +199,49 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
194
199
|
let text = headingMatch[2];
|
|
195
200
|
let attrs = '';
|
|
196
201
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
// Peel off trailing brace-blocks in any order: the squisq-native
|
|
203
|
+
// `{[template …]}` annotation and the Pandoc `{#id .class key=value}`
|
|
204
|
+
// attribute block may both appear at the end of the heading line.
|
|
205
|
+
// Loop until neither matches. The matchers are imported from core so
|
|
206
|
+
// this stays in sync with packages/core/src/markdown/convert.ts.
|
|
207
|
+
let templateInner: string | null = null;
|
|
208
|
+
let pandocInner: string | null = null;
|
|
209
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
210
|
+
let matched = false;
|
|
211
|
+
if (templateInner == null) {
|
|
212
|
+
const m = matchTrailingTemplateAnnotation(text);
|
|
213
|
+
if (m) {
|
|
214
|
+
templateInner = m.inner.trim();
|
|
215
|
+
text = text.slice(0, m.index).trimEnd();
|
|
216
|
+
matched = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (pandocInner == null) {
|
|
220
|
+
const m = matchTrailingPandocAttr(text);
|
|
221
|
+
if (m) {
|
|
222
|
+
pandocInner = m.inner.trim();
|
|
223
|
+
text = text.slice(0, m.index).trimEnd();
|
|
224
|
+
matched = true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!matched) break;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (templateInner != null) {
|
|
231
|
+
// Quote-aware tokenization (shared with the core parser) keeps a
|
|
232
|
+
// quoted value like caption="Beach at sunset" as one token. The
|
|
233
|
+
// tokens are stored raw — quotes included — so tiptapToMarkdown
|
|
234
|
+
// can re-join them into the annotation verbatim.
|
|
235
|
+
const tokens = tokenizeAttrTokens(templateInner);
|
|
236
|
+
attrs += ` data-template="${escapeHtml(tokens[0] ?? '')}"`;
|
|
206
237
|
const params = tokens.slice(1).filter((t) => t.includes('='));
|
|
207
238
|
if (params.length > 0) {
|
|
208
239
|
attrs += ` data-template-params="${escapeHtml(params.join(' '))}"`;
|
|
209
240
|
}
|
|
210
241
|
}
|
|
242
|
+
if (pandocInner != null) {
|
|
243
|
+
attrs += ` data-block-attrs="${escapeHtml(pandocInner)}"`;
|
|
244
|
+
}
|
|
211
245
|
|
|
212
246
|
outputBlocks.push(`<h${level}${attrs}>${inlineToHtml(text)}</h${level}>`);
|
|
213
247
|
continue;
|
|
@@ -227,17 +261,20 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
227
261
|
continue;
|
|
228
262
|
}
|
|
229
263
|
|
|
230
|
-
// Task list item
|
|
231
|
-
|
|
264
|
+
// Task list item. Text is optional so freshly-typed, still-empty tasks
|
|
265
|
+
// survive a round-trip. The checkbox itself is drawn by TaskItem's node
|
|
266
|
+
// view from the data-checked attribute, so we only emit the text content
|
|
267
|
+
// (in a paragraph, matching Tiptap's own node structure).
|
|
268
|
+
const taskMatch = line.match(/^[-*+]\s+\[([xX ])\]\s*(.*)$/);
|
|
232
269
|
if (taskMatch) {
|
|
233
270
|
if (!inList || listType !== 'task') {
|
|
234
271
|
flushList();
|
|
235
272
|
inList = true;
|
|
236
273
|
listType = 'task';
|
|
237
274
|
}
|
|
238
|
-
const
|
|
275
|
+
const checkedAttr = taskMatch[1].toLowerCase() === 'x' ? ' data-checked="true"' : '';
|
|
239
276
|
listItems.push(
|
|
240
|
-
`<li data-type="taskItem"${
|
|
277
|
+
`<li data-type="taskItem"${checkedAttr}><p>${inlineToHtml(taskMatch[2])}</p></li>`,
|
|
241
278
|
);
|
|
242
279
|
continue;
|
|
243
280
|
}
|
|
@@ -354,19 +391,26 @@ export function tiptapToMarkdown(html: string): string {
|
|
|
354
391
|
const attrs = headingMatch[2];
|
|
355
392
|
let text = htmlToInline(headingMatch[3]);
|
|
356
393
|
|
|
357
|
-
// Re-inject
|
|
394
|
+
// Re-inject heading annotations from data attributes. Canonical
|
|
395
|
+
// emit order: Pandoc `{#…}` first, then squisq `{[…]}` template
|
|
396
|
+
// annotation (matches blockToMdast in core/markdown/convert.ts).
|
|
397
|
+
const blockAttrsMatch = attrs.match(/data-block-attrs="([^"]*)"/);
|
|
358
398
|
const tmplMatch = attrs.match(/data-template="([^"]+)"/);
|
|
359
399
|
if (tmplMatch) {
|
|
360
|
-
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
// trailing copy of the template's label so existing documents
|
|
365
|
-
// self-heal on save.
|
|
366
|
-
const label = templateLabel(annotation);
|
|
400
|
+
// Strip an accidental trailing copy of the template label that an
|
|
401
|
+
// earlier broken build briefly rendered as real text inside the
|
|
402
|
+
// badge. Existing documents self-heal on save.
|
|
403
|
+
const label = templateLabel(tmplMatch[1]);
|
|
367
404
|
if (label && text.endsWith(label)) {
|
|
368
405
|
text = text.slice(0, -label.length).trimEnd();
|
|
369
406
|
}
|
|
407
|
+
}
|
|
408
|
+
if (blockAttrsMatch) {
|
|
409
|
+
const inner = unescapeHtml(blockAttrsMatch[1]);
|
|
410
|
+
text += ` {${inner}}`;
|
|
411
|
+
}
|
|
412
|
+
if (tmplMatch) {
|
|
413
|
+
let annotation = tmplMatch[1];
|
|
370
414
|
const paramsMatch = attrs.match(/data-template-params="([^"]+)"/);
|
|
371
415
|
if (paramsMatch) {
|
|
372
416
|
annotation += ' ' + unescapeHtml(paramsMatch[1]);
|
|
@@ -484,14 +528,16 @@ export function tiptapToMarkdown(html: string): string {
|
|
|
484
528
|
// Task list
|
|
485
529
|
const taskListMatch = remaining.match(/^<ul[^>]*data-type="taskList"[^>]*>(.*?)<\/ul>/s);
|
|
486
530
|
if (taskListMatch) {
|
|
487
|
-
const items = taskListMatch[1].matchAll(
|
|
488
|
-
/<li[^>]*data-type="taskItem"[^>]*(data-checked="true")?[^>]*>.*?<\/li>/gs,
|
|
489
|
-
);
|
|
531
|
+
const items = taskListMatch[1].matchAll(/<li[^>]*data-type="taskItem"[^>]*>.*?<\/li>/gs);
|
|
490
532
|
for (const item of items) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
533
|
+
// Read checked state from the attribute value only — a bare
|
|
534
|
+
// includes('checked') matches the substring in data-checked="false".
|
|
535
|
+
const checked = /data-checked="true"/.test(item[0]);
|
|
536
|
+
// Drop the checkbox chrome (<label>…</label>) before reading text;
|
|
537
|
+
// Tiptap puts the task text in the trailing content node, not the label.
|
|
538
|
+
const body = item[0].replace(/<label\b[^>]*>.*?<\/label>/s, '');
|
|
539
|
+
const text = htmlToInline(body.replace(/<[^>]+>/g, '').trim());
|
|
540
|
+
lines.push(`- [${checked ? 'x' : ' '}] ${text}`.trimEnd());
|
|
495
541
|
}
|
|
496
542
|
lines.push('');
|
|
497
543
|
remaining = remaining.slice(taskListMatch[0].length);
|
|
@@ -605,12 +651,28 @@ export function tiptapToMarkdown(html: string): string {
|
|
|
605
651
|
function renderListItem(prefix: string, html: string): string[] {
|
|
606
652
|
const indent = ' '.repeat(prefix.length);
|
|
607
653
|
|
|
654
|
+
// Pull out any block-level media nested in the item (e.g. a recording
|
|
655
|
+
// dropped onto a list bullet, or a clip dragged into one). The inline
|
|
656
|
+
// paragraph walk below ignores `<video>` / `<audio>` tags, so without
|
|
657
|
+
// this they'd be silently dropped on serialize and lost from the
|
|
658
|
+
// markdown source. We collect them and re-emit each as an indented
|
|
659
|
+
// continuation line so they stay inside the list item and round-trip
|
|
660
|
+
// through the markdown parser's block-media handler.
|
|
661
|
+
const media: string[] = [];
|
|
662
|
+
const htmlWithoutMedia = html.replace(
|
|
663
|
+
/<(video|audio)\b([^>]*)>(?:[\s\S]*?<\/\1>)?/gi,
|
|
664
|
+
(_full, tag: string, attrs: string) => {
|
|
665
|
+
media.push(serializeMediaTag(tag.toLowerCase() === 'video' ? 'video' : 'audio', attrs ?? ''));
|
|
666
|
+
return '';
|
|
667
|
+
},
|
|
668
|
+
);
|
|
669
|
+
|
|
608
670
|
// Split on </p><p> to detect paragraph breaks within the item
|
|
609
|
-
const paragraphs =
|
|
671
|
+
const paragraphs = htmlWithoutMedia
|
|
610
672
|
.split(/<\/p>\s*<p[^>]*>/i)
|
|
611
673
|
.map((p) => p.replace(/^<p[^>]*>/i, '').replace(/<\/p>\s*$/i, ''));
|
|
612
674
|
|
|
613
|
-
const
|
|
675
|
+
const textLines: string[] = [];
|
|
614
676
|
paragraphs.forEach((paragraph, pIdx) => {
|
|
615
677
|
const inline = htmlToInline(paragraph).trim();
|
|
616
678
|
if (!inline) return;
|
|
@@ -619,16 +681,35 @@ function renderListItem(prefix: string, html: string): string[] {
|
|
|
619
681
|
const subLines = inline.split('\n');
|
|
620
682
|
subLines.forEach((sub, sIdx) => {
|
|
621
683
|
if (pIdx === 0 && sIdx === 0) {
|
|
622
|
-
|
|
684
|
+
textLines.push(prefix + sub);
|
|
623
685
|
} else {
|
|
624
686
|
// Blank line separator between paragraphs (sIdx === 0 means new paragraph)
|
|
625
|
-
if (sIdx === 0)
|
|
626
|
-
|
|
687
|
+
if (sIdx === 0) textLines.push('');
|
|
688
|
+
textLines.push(indent + sub);
|
|
627
689
|
}
|
|
628
690
|
});
|
|
629
691
|
});
|
|
630
692
|
|
|
631
|
-
|
|
693
|
+
if (textLines.length === 0 && media.length === 0) return [prefix];
|
|
694
|
+
|
|
695
|
+
// Text first (its first line carries the bullet marker), then each media
|
|
696
|
+
// tag as its own indented continuation block. If the item is media-only,
|
|
697
|
+
// the first tag takes the bullet so the item isn't emitted empty.
|
|
698
|
+
const result: string[] = [];
|
|
699
|
+
if (textLines.length > 0) {
|
|
700
|
+
result.push(...textLines);
|
|
701
|
+
for (const tag of media) {
|
|
702
|
+
result.push('');
|
|
703
|
+
result.push(indent + tag);
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
result.push(prefix + media[0]);
|
|
707
|
+
for (const tag of media.slice(1)) {
|
|
708
|
+
result.push('');
|
|
709
|
+
result.push(indent + tag);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return result;
|
|
632
713
|
}
|
|
633
714
|
|
|
634
715
|
// ─── Table helpers ───────────────────────────────────────
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const EDGE_PADDING_PX = 8;
|
|
2
|
+
|
|
3
|
+
export function clampTooltipLeft(
|
|
4
|
+
anchorX: number,
|
|
5
|
+
tooltipWidth: number,
|
|
6
|
+
viewportWidth: number,
|
|
7
|
+
edgePadding = EDGE_PADDING_PX,
|
|
8
|
+
): number {
|
|
9
|
+
const minLeft = edgePadding;
|
|
10
|
+
const maxLeft = Math.max(minLeft, viewportWidth - edgePadding - tooltipWidth);
|
|
11
|
+
const centeredLeft = anchorX - tooltipWidth / 2;
|
|
12
|
+
return Math.min(maxLeft, Math.max(minLeft, centeredLeft));
|
|
13
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transitionCatalog
|
|
3
|
+
*
|
|
4
|
+
* Editor-facing, curated presentation of the block transition vocabulary.
|
|
5
|
+
*
|
|
6
|
+
* Core's `TRANSITION_TYPES` (packages/core/src/schemas/Transitions.ts) lists
|
|
7
|
+
* ~80 names, including legacy aliases and near-duplicate spellings. This
|
|
8
|
+
* catalog hand-picks the distinct, useful transitions, gives each a friendly
|
|
9
|
+
* label, and groups them for the toolbar's transition flyout — the same
|
|
10
|
+
* "core holds the truth, the editor holds the presentation" split the block
|
|
11
|
+
* `TemplatePicker` uses.
|
|
12
|
+
*
|
|
13
|
+
* Every `value` here MUST be a real `TransitionType`; `transitionCatalog.test.ts`
|
|
14
|
+
* enforces that so a renamed/removed core transition fails the build instead
|
|
15
|
+
* of silently producing an invalid `transition=` annotation. The catalog is
|
|
16
|
+
* intentionally NOT exhaustive — aliases and redundant spellings are omitted.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* How a transition takes a direction, driving which direction sub-control
|
|
21
|
+
* the picker shows. Mirrors core's `getTransitionVisualClass` dispatch:
|
|
22
|
+
* - `lrud`: left / right / up / down (push, wipe, cover, uncover, reveal, pan)
|
|
23
|
+
* - `axis`: horizontal / vertical (split, blinds)
|
|
24
|
+
* Entries without a model take no `transitionDirection`.
|
|
25
|
+
*/
|
|
26
|
+
export type DirectionModel = 'lrud' | 'axis';
|
|
27
|
+
|
|
28
|
+
export interface TransitionCatalogEntry {
|
|
29
|
+
/** Canonical `transition=` value — must be a core `TransitionType`. */
|
|
30
|
+
value: string;
|
|
31
|
+
/** Friendly label shown in the flyout and trigger. */
|
|
32
|
+
label: string;
|
|
33
|
+
/** Direction model, when the transition is directional. */
|
|
34
|
+
direction?: DirectionModel;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TransitionGroup {
|
|
38
|
+
title: string;
|
|
39
|
+
entries: TransitionCatalogEntry[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const TRANSITION_GROUPS: readonly TransitionGroup[] = [
|
|
43
|
+
{
|
|
44
|
+
title: 'Basic',
|
|
45
|
+
entries: [
|
|
46
|
+
{ value: 'fade', label: 'Fade' },
|
|
47
|
+
{ value: 'dissolve', label: 'Dissolve' },
|
|
48
|
+
{ value: 'flash', label: 'Flash' },
|
|
49
|
+
{ value: 'morph', label: 'Morph' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
title: 'Slide & Push',
|
|
54
|
+
entries: [
|
|
55
|
+
{ value: 'push', label: 'Push', direction: 'lrud' },
|
|
56
|
+
{ value: 'cover', label: 'Cover', direction: 'lrud' },
|
|
57
|
+
{ value: 'uncover', label: 'Uncover', direction: 'lrud' },
|
|
58
|
+
{ value: 'pan', label: 'Pan', direction: 'lrud' },
|
|
59
|
+
// Conveyor renders as a fixed-direction slide (no distinct 3D motion),
|
|
60
|
+
// so it lives with the slides rather than "3D & Motion".
|
|
61
|
+
{ value: 'conveyor', label: 'Conveyor' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
title: 'Reveal & Wipe',
|
|
66
|
+
entries: [
|
|
67
|
+
{ value: 'wipe', label: 'Wipe', direction: 'lrud' },
|
|
68
|
+
{ value: 'reveal', label: 'Reveal', direction: 'lrud' },
|
|
69
|
+
{ value: 'split', label: 'Split', direction: 'axis' },
|
|
70
|
+
// Blinds takes no `direction`: its horizontal/vertical web animations
|
|
71
|
+
// are identical (both map to the generic textured reveal), so a
|
|
72
|
+
// direction control here would be a visual no-op. Split, by contrast,
|
|
73
|
+
// has genuinely distinct horizontal/vertical keyframes.
|
|
74
|
+
{ value: 'blinds', label: 'Blinds' },
|
|
75
|
+
{ value: 'wedge', label: 'Wedge' },
|
|
76
|
+
{ value: 'wheel', label: 'Wheel' },
|
|
77
|
+
{ value: 'circle', label: 'Circle' },
|
|
78
|
+
{ value: 'diamond', label: 'Diamond' },
|
|
79
|
+
{ value: 'checkerboard', label: 'Checkerboard' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
title: '3D & Motion',
|
|
84
|
+
entries: [
|
|
85
|
+
{ value: 'zoom', label: 'Zoom' },
|
|
86
|
+
{ value: 'cube', label: 'Cube' },
|
|
87
|
+
{ value: 'flip', label: 'Flip' },
|
|
88
|
+
{ value: 'rotate', label: 'Rotate' },
|
|
89
|
+
{ value: 'doors', label: 'Doors' },
|
|
90
|
+
{ value: 'box', label: 'Box' },
|
|
91
|
+
{ value: 'gallery', label: 'Gallery' },
|
|
92
|
+
{ value: 'pageCurl', label: 'Page Curl' },
|
|
93
|
+
{ value: 'origami', label: 'Origami' },
|
|
94
|
+
{ value: 'ferrisWheel', label: 'Ferris Wheel' },
|
|
95
|
+
{ value: 'orbit', label: 'Orbit' },
|
|
96
|
+
{ value: 'flyThrough', label: 'Fly Through' },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
title: 'Effects',
|
|
101
|
+
entries: [
|
|
102
|
+
{ value: 'ripple', label: 'Ripple' },
|
|
103
|
+
{ value: 'honeycomb', label: 'Honeycomb' },
|
|
104
|
+
{ value: 'glitter', label: 'Glitter' },
|
|
105
|
+
{ value: 'vortex', label: 'Vortex' },
|
|
106
|
+
{ value: 'shred', label: 'Shred' },
|
|
107
|
+
{ value: 'fracture', label: 'Fracture' },
|
|
108
|
+
{ value: 'crush', label: 'Crush' },
|
|
109
|
+
{ value: 'prestige', label: 'Prestige' },
|
|
110
|
+
{ value: 'curtains', label: 'Curtains' },
|
|
111
|
+
{ value: 'drape', label: 'Drape' },
|
|
112
|
+
{ value: 'wind', label: 'Wind' },
|
|
113
|
+
{ value: 'fallOver', label: 'Fall Over' },
|
|
114
|
+
{ value: 'peelOff', label: 'Peel Off' },
|
|
115
|
+
{ value: 'airplane', label: 'Airplane' },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/** Flat list of every catalog entry, in group order. */
|
|
121
|
+
export const TRANSITION_ENTRIES: readonly TransitionCatalogEntry[] = TRANSITION_GROUPS.flatMap(
|
|
122
|
+
(g) => g.entries,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const ENTRY_BY_VALUE = new Map(TRANSITION_ENTRIES.map((e) => [e.value, e]));
|
|
126
|
+
|
|
127
|
+
/** Direction option lists keyed by model. */
|
|
128
|
+
export const DIRECTION_OPTIONS: Record<
|
|
129
|
+
DirectionModel,
|
|
130
|
+
readonly { value: string; label: string }[]
|
|
131
|
+
> = {
|
|
132
|
+
lrud: [
|
|
133
|
+
{ value: 'left', label: 'Left' },
|
|
134
|
+
{ value: 'right', label: 'Right' },
|
|
135
|
+
{ value: 'up', label: 'Up' },
|
|
136
|
+
{ value: 'down', label: 'Down' },
|
|
137
|
+
],
|
|
138
|
+
axis: [
|
|
139
|
+
{ value: 'horizontal', label: 'Horizontal' },
|
|
140
|
+
{ value: 'vertical', label: 'Vertical' },
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Look up a catalog entry by its `transition=` value. */
|
|
145
|
+
export function findTransitionEntry(value: string): TransitionCatalogEntry | undefined {
|
|
146
|
+
return ENTRY_BY_VALUE.get(value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Human label for a transition value. Returns 'None' for the empty value and
|
|
151
|
+
* falls back to a camelCase-humanized form for any valid-but-uncurated type
|
|
152
|
+
* (e.g. a hand-typed alias) so the trigger still reads sensibly.
|
|
153
|
+
*/
|
|
154
|
+
export function transitionLabel(value: string): string {
|
|
155
|
+
if (!value) return 'None';
|
|
156
|
+
const entry = ENTRY_BY_VALUE.get(value);
|
|
157
|
+
if (entry) return entry.label;
|
|
158
|
+
return value.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase());
|
|
159
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monaco ships `editor.main.js` (the full bundle with language contributions)
|
|
3
|
+
* but only publishes `.d.ts` for `editor.api`. We import the `.main.js`
|
|
4
|
+
* subpath at runtime to register all languages, then cast through
|
|
5
|
+
* `as unknown as typeof import('monaco-editor')` at the call site. This
|
|
6
|
+
* ambient declaration makes the subpath import resolve without a
|
|
7
|
+
* `@ts-expect-error` (which doesn't survive Prettier's formatting choice
|
|
8
|
+
* for the surrounding parenthesized expression — see useMonacoLoader.ts).
|
|
9
|
+
*/
|
|
10
|
+
declare module 'monaco-editor/esm/vs/editor/editor.main.js';
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useBlockNavigator
|
|
3
|
+
*
|
|
4
|
+
* Standalone hook powering the block-at-a-time editing view. Given a
|
|
5
|
+
* `(source, setSource)` pair it slices the markdown into blocks (via
|
|
6
|
+
* `blockRange.ts`), tracks which block is active, and exposes a derived
|
|
7
|
+
* **content channel** (`editorSource` / `setEditorSource`) plus navigation.
|
|
8
|
+
*
|
|
9
|
+
* It depends only on its arguments — no `EditorContext`, no `EditorShell` —
|
|
10
|
+
* so any host (an embedded single-block editor, a chat composer, a review
|
|
11
|
+
* surface) can drive a block-at-a-time UI by calling it directly.
|
|
12
|
+
*
|
|
13
|
+
* When `enabled` is false the channel is an identity passthrough: the editors
|
|
14
|
+
* see and write the full source exactly as before.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
18
|
+
import {
|
|
19
|
+
getBlockSlices,
|
|
20
|
+
spliceBlock,
|
|
21
|
+
lineToOffset,
|
|
22
|
+
offsetToLine,
|
|
23
|
+
sliceIndexAtOffset,
|
|
24
|
+
type BlockSlice,
|
|
25
|
+
} from './blockRange';
|
|
26
|
+
|
|
27
|
+
export interface BlockNavigator {
|
|
28
|
+
/** What the bound editor should show: the active slice (block mode) or the full source. */
|
|
29
|
+
editorSource: string;
|
|
30
|
+
/** What the bound editor writes through: splices back into the full source (block mode). */
|
|
31
|
+
setEditorSource: (s: string) => void;
|
|
32
|
+
/** Number of navigable blocks in the current source. */
|
|
33
|
+
blockCount: number;
|
|
34
|
+
/** Index of the active block (clamped into range). */
|
|
35
|
+
activeBlockKey: number;
|
|
36
|
+
/** Jump to a block by index (clamped). */
|
|
37
|
+
goToBlock: (key: number) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Select the block that owns a given 1-based source line — used by the
|
|
40
|
+
* outline, which knows a heading's source line but not its slice index
|
|
41
|
+
* (slice order includes the optional preamble, so it needn't match
|
|
42
|
+
* `flattenBlocks` order).
|
|
43
|
+
*/
|
|
44
|
+
goToBlockByLine: (line: number) => void;
|
|
45
|
+
/** 1-based source line where the active block begins (for outline highlight). */
|
|
46
|
+
activeBlockStartLine: number | null;
|
|
47
|
+
/** Move to the previous block (no-op at the start). */
|
|
48
|
+
prevBlock: () => void;
|
|
49
|
+
/** Move to the next block (no-op at the end). */
|
|
50
|
+
nextBlock: () => void;
|
|
51
|
+
/** Insert a new heading-defined block after the active one and move to it. */
|
|
52
|
+
addBlock: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UseBlockNavigatorOptions {
|
|
56
|
+
/**
|
|
57
|
+
* When false (the default), the channel passes through to the full source
|
|
58
|
+
* and navigation is inert. Hosts flip this on to enter block-at-a-time mode.
|
|
59
|
+
*/
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Heading inserted by {@link BlockNavigator.addBlock}. */
|
|
64
|
+
const NEW_BLOCK_MARKDOWN = '## New section\n\n';
|
|
65
|
+
|
|
66
|
+
export function useBlockNavigator(
|
|
67
|
+
source: string,
|
|
68
|
+
setSource: (s: string) => void,
|
|
69
|
+
opts?: UseBlockNavigatorOptions,
|
|
70
|
+
): BlockNavigator {
|
|
71
|
+
const enabled = opts?.enabled ?? false;
|
|
72
|
+
|
|
73
|
+
// Slices are derived straight from the live `source`, not from any
|
|
74
|
+
// debounced parse, so they're always consistent with what we splice into.
|
|
75
|
+
const slices = useMemo<BlockSlice[]>(() => getBlockSlices(source), [source]);
|
|
76
|
+
const blockCount = slices.length;
|
|
77
|
+
|
|
78
|
+
const [rawKey, setRawKey] = useState(0);
|
|
79
|
+
// Effective key is always in range even when the source shrank under us —
|
|
80
|
+
// the state can lag a re-slice for a render, but derivations never read a
|
|
81
|
+
// stale/out-of-bounds slice.
|
|
82
|
+
const activeBlockKey = blockCount === 0 ? 0 : Math.min(Math.max(rawKey, 0), blockCount - 1);
|
|
83
|
+
const active = slices[activeBlockKey] ?? null;
|
|
84
|
+
|
|
85
|
+
const goToBlock = useCallback((key: number) => {
|
|
86
|
+
setRawKey(key);
|
|
87
|
+
}, []);
|
|
88
|
+
const prevBlock = useCallback(() => {
|
|
89
|
+
setRawKey(activeBlockKey - 1);
|
|
90
|
+
}, [activeBlockKey]);
|
|
91
|
+
const nextBlock = useCallback(() => {
|
|
92
|
+
setRawKey(activeBlockKey + 1);
|
|
93
|
+
}, [activeBlockKey]);
|
|
94
|
+
const goToBlockByLine = useCallback(
|
|
95
|
+
(line: number) => {
|
|
96
|
+
const offset = lineToOffset(source, line);
|
|
97
|
+
const idx = sliceIndexAtOffset(slices, offset);
|
|
98
|
+
if (idx >= 0) setRawKey(idx);
|
|
99
|
+
},
|
|
100
|
+
[source, slices],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const activeBlockStartLine = active ? offsetToLine(source, active.range.startOffset) : null;
|
|
104
|
+
|
|
105
|
+
const editorSource = enabled && active ? active.text : source;
|
|
106
|
+
|
|
107
|
+
const setEditorSource = useCallback(
|
|
108
|
+
(next: string) => {
|
|
109
|
+
if (!enabled || !active) {
|
|
110
|
+
setSource(next);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Keep a blank line before the following block's heading so the splice
|
|
114
|
+
// can't glue the edited block onto the next one (the editor may emit a
|
|
115
|
+
// slice with no trailing newline).
|
|
116
|
+
const hasFollowing = active.range.endOffset < source.length;
|
|
117
|
+
const normalized = hasFollowing ? next.replace(/\s*$/, '') + '\n\n' : next;
|
|
118
|
+
setSource(spliceBlock(source, active.range, normalized));
|
|
119
|
+
},
|
|
120
|
+
[enabled, active, source, setSource],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const addBlock = useCallback(() => {
|
|
124
|
+
if (!active) {
|
|
125
|
+
// Empty / heading-less doc — append a heading and select it.
|
|
126
|
+
const base = source.replace(/\s*$/, '');
|
|
127
|
+
setSource((base ? base + '\n\n' : '') + NEW_BLOCK_MARKDOWN);
|
|
128
|
+
setRawKey(blockCount); // becomes the new last slice after re-slice
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const insertAt = active.range.endOffset;
|
|
132
|
+
const before = source.slice(0, insertAt);
|
|
133
|
+
const after = source.slice(insertAt);
|
|
134
|
+
const lead =
|
|
135
|
+
before === '' || before.endsWith('\n\n') ? '' : before.endsWith('\n') ? '\n' : '\n\n';
|
|
136
|
+
setSource(before + lead + NEW_BLOCK_MARKDOWN + after);
|
|
137
|
+
// The new block sits immediately after the active one.
|
|
138
|
+
setRawKey(activeBlockKey + 1);
|
|
139
|
+
}, [active, source, setSource, activeBlockKey, blockCount]);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
editorSource,
|
|
143
|
+
setEditorSource,
|
|
144
|
+
blockCount,
|
|
145
|
+
activeBlockKey,
|
|
146
|
+
goToBlock,
|
|
147
|
+
goToBlockByLine,
|
|
148
|
+
activeBlockStartLine,
|
|
149
|
+
prevBlock,
|
|
150
|
+
nextBlock,
|
|
151
|
+
addBlock,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useMonacoLoader
|
|
3
|
+
*
|
|
4
|
+
* Idempotently dynamic-imports `monaco-editor` and points the
|
|
5
|
+
* `@monaco-editor/react` singleton loader at the bundled copy. Replaces
|
|
6
|
+
* the historical top-of-module `import * as monaco from 'monaco-editor';
|
|
7
|
+
* loader.config({ monaco })` pattern, which forced every consumer of
|
|
8
|
+
* `@bendyline/squisq-editor-react` — including ones that only import
|
|
9
|
+
* `JsonEditor` or a type — to drag in monaco's ~9MB worth of language
|
|
10
|
+
* services and workers at module evaluation time.
|
|
11
|
+
*
|
|
12
|
+
* Hosts that want the smallest possible bundle can keep aliasing
|
|
13
|
+
* `monaco-editor` to a slim entry as before; the behavior is identical
|
|
14
|
+
* once the dynamic import settles.
|
|
15
|
+
*
|
|
16
|
+
* The promise is cached at module scope so the first subscriber
|
|
17
|
+
* anywhere in the app pays the import cost and every later subscriber
|
|
18
|
+
* reuses the same settled value.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useEffect, useState } from 'react';
|
|
22
|
+
import { loader } from '@monaco-editor/react';
|
|
23
|
+
|
|
24
|
+
/** In-flight (or settled) dynamic import shared across all callers. */
|
|
25
|
+
let monacoPromise: Promise<typeof import('monaco-editor')> | null = null;
|
|
26
|
+
|
|
27
|
+
/** Settled namespace once the promise resolves — read synchronously by mount-time consumers. */
|
|
28
|
+
let monacoNamespace: typeof import('monaco-editor') | null = null;
|
|
29
|
+
|
|
30
|
+
export interface UseMonacoLoaderResult {
|
|
31
|
+
/** The monaco namespace once loaded, or `null` while the import is in flight. */
|
|
32
|
+
monaco: typeof import('monaco-editor') | null;
|
|
33
|
+
/** Flips to `true` after the import settles. Gate `<Editor>` / `<DiffEditor>` renders on this. */
|
|
34
|
+
ready: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Subscribe to the lazy-loaded monaco namespace. The first caller
|
|
39
|
+
* triggers `import('monaco-editor')` and configures the
|
|
40
|
+
* `@monaco-editor/react` loader; subsequent callers receive the same
|
|
41
|
+
* cached value.
|
|
42
|
+
*/
|
|
43
|
+
export function useMonacoLoader(): UseMonacoLoaderResult {
|
|
44
|
+
const [state, setState] = useState<UseMonacoLoaderResult>(() => ({
|
|
45
|
+
monaco: monacoNamespace,
|
|
46
|
+
ready: monacoNamespace !== null,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (state.ready) return;
|
|
51
|
+
if (!monacoPromise) {
|
|
52
|
+
// Import the explicit ESM entry file rather than the bare
|
|
53
|
+
// package specifier `'monaco-editor'`. The package's
|
|
54
|
+
// `package.json` has no `main` / no `exports` (only `module`),
|
|
55
|
+
// which trips Vite's strict resolver on both static and
|
|
56
|
+
// dynamic imports of the bare name. Pointing at the file
|
|
57
|
+
// directly — the same path monaco's `module` field points at —
|
|
58
|
+
// turns this into a regular file-path resolve that doesn't
|
|
59
|
+
// care about the manifest's entry fields. Works identically
|
|
60
|
+
// in Vite dev / build, vitest's transform pipeline, and any
|
|
61
|
+
// bundler-using downstream consumer.
|
|
62
|
+
//
|
|
63
|
+
// Use `editor.main.js`, NOT `editor.api.js`. The `api` entry is
|
|
64
|
+
// the bare standalone API with zero language contributions —
|
|
65
|
+
// RawEditor's `defaultLanguage="typescript"` then mounts with
|
|
66
|
+
// no tokenizer registered, and every file renders as
|
|
67
|
+
// undifferentiated foreground text (the regression that surfaced
|
|
68
|
+
// when a `.ts` file in the chat workspace previewer showed up
|
|
69
|
+
// with no syntax coloring). `editor.main.js` re-exports the API
|
|
70
|
+
// and additionally pulls in `basic-languages/monaco.contribution`
|
|
71
|
+
// (TM grammars for ~70 languages) plus the four rich language
|
|
72
|
+
// services (css / html / json / typescript). This is what makes
|
|
73
|
+
// `defaultLanguage` actually do anything. The cost is the language
|
|
74
|
+
// bundle — but since we load it lazily on first EditorShell mount,
|
|
75
|
+
// it stays out of the resolver graph for consumers that only
|
|
76
|
+
// import types or `JsonEditor` from this package.
|
|
77
|
+
// `editor.main.js` ships without a sibling `.d.ts` (only
|
|
78
|
+
// `editor.api.d.ts` is published), so the subpath import has no
|
|
79
|
+
// resolvable declaration. The `declare module` shim in
|
|
80
|
+
// `src/types/monaco-shims.d.ts` makes it import as `any`; the
|
|
81
|
+
// `as unknown as` cast below pins the surface to the full namespace.
|
|
82
|
+
// At runtime `main` re-exports the entire `editor.api` surface
|
|
83
|
+
// alongside the language contributions, so the cast is sound.
|
|
84
|
+
monacoPromise = (
|
|
85
|
+
import('monaco-editor/esm/vs/editor/editor.main.js') as unknown as Promise<
|
|
86
|
+
typeof import('monaco-editor')
|
|
87
|
+
>
|
|
88
|
+
).then((m) => {
|
|
89
|
+
loader.config({ monaco: m });
|
|
90
|
+
monacoNamespace = m;
|
|
91
|
+
return m;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
let cancelled = false;
|
|
95
|
+
void monacoPromise.then((m) => {
|
|
96
|
+
if (cancelled) return;
|
|
97
|
+
setState({ monaco: m, ready: true });
|
|
98
|
+
});
|
|
99
|
+
return () => {
|
|
100
|
+
cancelled = true;
|
|
101
|
+
};
|
|
102
|
+
}, [state.ready]);
|
|
103
|
+
|
|
104
|
+
return state;
|
|
105
|
+
}
|