@bendyline/squisq-editor-react 1.5.2 → 1.5.3
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.js +327 -253
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/RawEditor.tsx +49 -18
- package/src/VersionHistoryPanel.tsx +59 -17
- package/src/recorder/hooks/useMediaRecorder.ts +9 -1
- package/src/useMonacoLoader.ts +83 -0
package/package.json
CHANGED
package/src/RawEditor.tsx
CHANGED
|
@@ -7,29 +7,30 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useRef, useCallback, useEffect } from 'react';
|
|
10
|
-
import Editor, {
|
|
11
|
-
|
|
12
|
-
type OnMount,
|
|
13
|
-
type OnChange,
|
|
14
|
-
type BeforeMount,
|
|
15
|
-
} from '@monaco-editor/react';
|
|
16
|
-
import * as monaco from 'monaco-editor';
|
|
10
|
+
import Editor, { type OnMount, type OnChange, type BeforeMount } from '@monaco-editor/react';
|
|
11
|
+
import type * as monaco from 'monaco-editor';
|
|
17
12
|
import { useEditorContext } from './EditorContext';
|
|
18
13
|
import { getAvailableTemplates } from '@bendyline/squisq/doc';
|
|
19
14
|
import { suggestIcons, resolveIcon, iconGlyph } from '@bendyline/squisq/icons';
|
|
20
15
|
import { SQUISQ_MEDIA_MIME, parseSquisqMediaPayload } from './mediaDragMime';
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
import { useMonacoLoader } from './useMonacoLoader';
|
|
17
|
+
|
|
18
|
+
// Monaco is loaded lazily through `useMonacoLoader` (see the hook for the
|
|
19
|
+
// rationale). The type-only `import type * as monaco from 'monaco-editor'`
|
|
20
|
+
// above gives us `monaco.editor.IStandaloneCodeEditor`, `monaco.Range`,
|
|
21
|
+
// etc. for typing without pulling the package into the static module
|
|
22
|
+
// graph — which is the whole point: a consumer importing `JsonEditor` or
|
|
23
|
+
// a type from the package barrel no longer drags ~9MB of language
|
|
24
|
+
// services into the resolver.
|
|
23
25
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
26
|
+
// Consumers that *do* want the raw editor can still slim the bundle by
|
|
27
|
+
// aliasing `monaco-editor` to a custom entry in their bundler config.
|
|
28
|
+
// For example with Vite:
|
|
27
29
|
//
|
|
28
30
|
// resolve: { alias: [{ find: /^monaco-editor$/, replacement: './monaco-slim.ts' }] }
|
|
29
31
|
//
|
|
30
|
-
// Where monaco-slim.ts re-exports 'monaco-editor/esm/vs/editor/editor.api'
|
|
31
|
-
// only the language contributions needed (e.g. markdown, javascript
|
|
32
|
-
loader.config({ monaco });
|
|
32
|
+
// Where monaco-slim.ts re-exports 'monaco-editor/esm/vs/editor/editor.api'
|
|
33
|
+
// plus only the language contributions needed (e.g. markdown, javascript).
|
|
33
34
|
|
|
34
35
|
// Squisq Monaco themes: same syntax highlighting as vs / vs-dark, but with
|
|
35
36
|
// Monaco's internal gutter (line numbers + folding margin) and overview
|
|
@@ -83,6 +84,7 @@ export function RawEditor({
|
|
|
83
84
|
}: RawEditorProps) {
|
|
84
85
|
const { markdownSource, setMarkdownSource, setMonacoEditor, language, mentionProvider } =
|
|
85
86
|
useEditorContext();
|
|
87
|
+
const { monaco: monacoNs, ready: monacoReady } = useMonacoLoader();
|
|
86
88
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
|
87
89
|
const isExternalUpdate = useRef(false);
|
|
88
90
|
const completionDisposable = useRef<monaco.IDisposable | null>(null);
|
|
@@ -439,7 +441,10 @@ export function RawEditor({
|
|
|
439
441
|
// and weight; the codepoint character is the decoration's content.
|
|
440
442
|
useEffect(() => {
|
|
441
443
|
const editor = editorRef.current;
|
|
442
|
-
|
|
444
|
+
// `monacoNs` is read from the lazy loader's state rather than a
|
|
445
|
+
// top-level import. Re-run when it transitions from null → loaded
|
|
446
|
+
// so decorations show up the moment monaco is in hand.
|
|
447
|
+
if (!editor || !monacoNs) return;
|
|
443
448
|
if (language !== 'markdown') return;
|
|
444
449
|
const model = editor.getModel();
|
|
445
450
|
if (!model) return;
|
|
@@ -461,7 +466,7 @@ export function RawEditor({
|
|
|
461
466
|
// glyph as content prepended visually to that position.
|
|
462
467
|
const col = match.index + 1; // Monaco columns are 1-based
|
|
463
468
|
decorations.push({
|
|
464
|
-
range: new
|
|
469
|
+
range: new monacoNs.Range(line, col, line, col),
|
|
465
470
|
options: {
|
|
466
471
|
before: {
|
|
467
472
|
content: glyph,
|
|
@@ -477,10 +482,36 @@ export function RawEditor({
|
|
|
477
482
|
} else {
|
|
478
483
|
iconGlyphDecorations.current.set(decorations);
|
|
479
484
|
}
|
|
480
|
-
}, [markdownSource, language]);
|
|
485
|
+
}, [markdownSource, language, monacoNs]);
|
|
481
486
|
|
|
482
487
|
const effectiveTheme = SQUISQ_THEMES[theme] ?? theme;
|
|
483
488
|
|
|
489
|
+
// Wait for the lazy monaco namespace + `loader.config()` to settle
|
|
490
|
+
// before mounting `<Editor>`. Without this gate, the @monaco-editor/
|
|
491
|
+
// react singleton loader would fall back to its built-in CDN fetch
|
|
492
|
+
// for any consumer that hasn't aliased monaco-editor — which is the
|
|
493
|
+
// exact regression the lazy-loading move is meant to avoid.
|
|
494
|
+
if (!monacoReady) {
|
|
495
|
+
return (
|
|
496
|
+
<div
|
|
497
|
+
className={className}
|
|
498
|
+
style={{
|
|
499
|
+
width: '100%',
|
|
500
|
+
height: '100%',
|
|
501
|
+
display: 'flex',
|
|
502
|
+
alignItems: 'center',
|
|
503
|
+
justifyContent: 'center',
|
|
504
|
+
color: 'var(--squisq-editor-muted-foreground, #6a6258)',
|
|
505
|
+
fontSize: 13,
|
|
506
|
+
}}
|
|
507
|
+
data-testid="raw-editor"
|
|
508
|
+
data-monaco-loading
|
|
509
|
+
>
|
|
510
|
+
Loading editor…
|
|
511
|
+
</div>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
484
515
|
return (
|
|
485
516
|
<div className={className} style={{ width: '100%', height: '100%' }} data-testid="raw-editor">
|
|
486
517
|
<Editor
|
|
@@ -11,10 +11,67 @@
|
|
|
11
11
|
* a provider that has `allowVersioning` and a `container`.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
|
15
15
|
import { DiffEditor } from '@monaco-editor/react';
|
|
16
16
|
import type { Version } from '@bendyline/squisq/versions';
|
|
17
17
|
import { useEditorContext } from './EditorContext';
|
|
18
|
+
import { useMonacoLoader } from './useMonacoLoader';
|
|
19
|
+
|
|
20
|
+
interface LazyDiffEditorProps {
|
|
21
|
+
original: string;
|
|
22
|
+
modified: string;
|
|
23
|
+
theme: 'vs' | 'vs-dark';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lazyLoadingStyle: CSSProperties = {
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
justifyContent: 'center',
|
|
30
|
+
width: '100%',
|
|
31
|
+
height: '100%',
|
|
32
|
+
color: 'var(--squisq-editor-muted-foreground, #6a6258)',
|
|
33
|
+
fontSize: 12,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Defers the `<DiffEditor>` mount until the lazy monaco namespace +
|
|
38
|
+
* `loader.config()` are in place. Without the gate, the
|
|
39
|
+
* `@monaco-editor/react` singleton loader would fall back to its CDN
|
|
40
|
+
* default for any consumer that hasn't already mounted `<RawEditor>`.
|
|
41
|
+
*
|
|
42
|
+
* This wrapper is what makes `useMonacoLoader` worth using here —
|
|
43
|
+
* VersionHistoryPanel itself is always present in the toolbar, so
|
|
44
|
+
* subscribing at its level would defeat the lazy-load. The hook only
|
|
45
|
+
* fires when a snapshot is actually selected.
|
|
46
|
+
*/
|
|
47
|
+
function LazyDiffEditor({ original, modified, theme }: LazyDiffEditorProps) {
|
|
48
|
+
const { ready } = useMonacoLoader();
|
|
49
|
+
if (!ready) {
|
|
50
|
+
return <div style={lazyLoadingStyle}>Loading diff…</div>;
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
<DiffEditor
|
|
54
|
+
original={original}
|
|
55
|
+
modified={modified}
|
|
56
|
+
language="markdown"
|
|
57
|
+
theme={theme}
|
|
58
|
+
options={{
|
|
59
|
+
readOnly: true,
|
|
60
|
+
renderSideBySide: true,
|
|
61
|
+
minimap: { enabled: false },
|
|
62
|
+
scrollBeyondLastLine: false,
|
|
63
|
+
wordWrap: 'on',
|
|
64
|
+
automaticLayout: true,
|
|
65
|
+
fontSize: 12,
|
|
66
|
+
lineNumbers: 'off',
|
|
67
|
+
glyphMargin: false,
|
|
68
|
+
folding: false,
|
|
69
|
+
overviewRulerLanes: 0,
|
|
70
|
+
renderOverviewRuler: false,
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
18
75
|
|
|
19
76
|
interface PanelState {
|
|
20
77
|
loading: boolean;
|
|
@@ -206,25 +263,10 @@ export function VersionHistoryPanel() {
|
|
|
206
263
|
</span>
|
|
207
264
|
</div>
|
|
208
265
|
<div className="squisq-version-history-diff-body">
|
|
209
|
-
<
|
|
266
|
+
<LazyDiffEditor
|
|
210
267
|
original={state.selected.content}
|
|
211
268
|
modified={markdownSource}
|
|
212
|
-
language="markdown"
|
|
213
269
|
theme={diffTheme}
|
|
214
|
-
options={{
|
|
215
|
-
readOnly: true,
|
|
216
|
-
renderSideBySide: true,
|
|
217
|
-
minimap: { enabled: false },
|
|
218
|
-
scrollBeyondLastLine: false,
|
|
219
|
-
wordWrap: 'on',
|
|
220
|
-
automaticLayout: true,
|
|
221
|
-
fontSize: 12,
|
|
222
|
-
lineNumbers: 'off',
|
|
223
|
-
glyphMargin: false,
|
|
224
|
-
folding: false,
|
|
225
|
-
overviewRulerLanes: 0,
|
|
226
|
-
renderOverviewRuler: false,
|
|
227
|
-
}}
|
|
228
270
|
/>
|
|
229
271
|
</div>
|
|
230
272
|
</div>
|
|
@@ -344,7 +344,15 @@ export function useMediaRecorder(options: UseMediaRecorderOptions): UseMediaReco
|
|
|
344
344
|
|
|
345
345
|
// Final unmount cleanup — make sure we don't leak the camera light /
|
|
346
346
|
// screen-capture indicator if the component disappears mid-recording.
|
|
347
|
+
//
|
|
348
|
+
// `stopResolversRef.current` is captured at effect-run time into a
|
|
349
|
+
// local. That ref is initialized once at construction and never
|
|
350
|
+
// reassigned, so the local handle stays a live view onto the same
|
|
351
|
+
// mutable array — entries pushed by later `stop()` calls still
|
|
352
|
+
// appear here on cleanup. Capturing keeps `react-hooks/exhaustive-deps`
|
|
353
|
+
// satisfied without changing the runtime behavior.
|
|
347
354
|
useEffect(() => {
|
|
355
|
+
const pendingResolvers = stopResolversRef.current;
|
|
348
356
|
return () => {
|
|
349
357
|
const rec = recorderRef.current;
|
|
350
358
|
if (rec && rec.state !== 'inactive') {
|
|
@@ -356,7 +364,7 @@ export function useMediaRecorder(options: UseMediaRecorderOptions): UseMediaReco
|
|
|
356
364
|
}
|
|
357
365
|
releaseStream();
|
|
358
366
|
clearTicker();
|
|
359
|
-
|
|
367
|
+
pendingResolvers.splice(0).forEach((resolve) => resolve(null));
|
|
360
368
|
};
|
|
361
369
|
}, [releaseStream, clearTicker]);
|
|
362
370
|
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
monacoPromise = (
|
|
63
|
+
import('monaco-editor/esm/vs/editor/editor.api.js') as unknown as Promise<
|
|
64
|
+
typeof import('monaco-editor')
|
|
65
|
+
>
|
|
66
|
+
).then((m) => {
|
|
67
|
+
loader.config({ monaco: m });
|
|
68
|
+
monacoNamespace = m;
|
|
69
|
+
return m;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
let cancelled = false;
|
|
73
|
+
void monacoPromise.then((m) => {
|
|
74
|
+
if (cancelled) return;
|
|
75
|
+
setState({ monaco: m, ready: true });
|
|
76
|
+
});
|
|
77
|
+
return () => {
|
|
78
|
+
cancelled = true;
|
|
79
|
+
};
|
|
80
|
+
}, [state.ready]);
|
|
81
|
+
|
|
82
|
+
return state;
|
|
83
|
+
}
|