seemore 1.4.0 → 1.4.1
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/app/mdx/D2.tsx +19 -3
- package/src/app/mdx/DiagramLoader.tsx +11 -0
- package/src/app/mdx/Mermaid.tsx +16 -2
- package/src/app/mdx/useInView.ts +31 -0
- package/src/app/styles/globals.css +8 -0
package/package.json
CHANGED
package/src/app/mdx/D2.tsx
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { useEffect, useId, useState } from 'react';
|
|
1
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import { DiagramLoader } from './DiagramLoader.js';
|
|
3
|
+
import { useInView } from './useInView.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* D2 compiles to SVG via a WASM build in the browser — same reasoning as `Mermaid`: doing
|
|
@@ -7,10 +9,17 @@ import { useEffect, useId, useState } from 'react';
|
|
|
7
9
|
*/
|
|
8
10
|
export function D2({ chart }: { chart: string }) {
|
|
9
11
|
const id = useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
12
|
+
const container = useRef<HTMLDivElement>(null);
|
|
13
|
+
const inView = useInView(container);
|
|
10
14
|
const [svg, setSvg] = useState<string>();
|
|
11
15
|
const [error, setError] = useState<string>();
|
|
12
16
|
|
|
13
17
|
useEffect(() => {
|
|
18
|
+
// D2's compile/render pass is synchronous WASM work and can't be interrupted, so a large
|
|
19
|
+
// diagram freezes the main thread — including any pending navigation — the moment it
|
|
20
|
+
// starts. Waiting until the diagram is actually scrolled into view keeps that off the
|
|
21
|
+
// critical path of landing on the page.
|
|
22
|
+
if (!inView) return;
|
|
14
23
|
let cancelled = false;
|
|
15
24
|
|
|
16
25
|
void (async () => {
|
|
@@ -31,7 +40,7 @@ export function D2({ chart }: { chart: string }) {
|
|
|
31
40
|
return () => {
|
|
32
41
|
cancelled = true;
|
|
33
42
|
};
|
|
34
|
-
}, [chart, id]);
|
|
43
|
+
}, [inView, chart, id]);
|
|
35
44
|
|
|
36
45
|
if (error !== undefined) {
|
|
37
46
|
return (
|
|
@@ -43,11 +52,18 @@ export function D2({ chart }: { chart: string }) {
|
|
|
43
52
|
|
|
44
53
|
return (
|
|
45
54
|
<div
|
|
55
|
+
ref={container}
|
|
46
56
|
className="seemore-d2"
|
|
47
57
|
// The diagram is generated by D2 from the page's own source, not from user input.
|
|
48
58
|
dangerouslySetInnerHTML={svg === undefined ? undefined : { __html: svg }}
|
|
49
59
|
>
|
|
50
|
-
{svg === undefined ?
|
|
60
|
+
{svg === undefined ? (
|
|
61
|
+
inView ? (
|
|
62
|
+
<DiagramLoader />
|
|
63
|
+
) : (
|
|
64
|
+
<pre className="seemore-d2-source">{chart}</pre>
|
|
65
|
+
)
|
|
66
|
+
) : undefined}
|
|
51
67
|
</div>
|
|
52
68
|
);
|
|
53
69
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Shown by `Mermaid`/`D2` while a diagram's synchronous render pass is in progress. */
|
|
2
|
+
export function DiagramLoader() {
|
|
3
|
+
return (
|
|
4
|
+
<div className="seemore-diagram-loading" role="status">
|
|
5
|
+
<span className="seemore-diagram-spinner" aria-hidden="true" />
|
|
6
|
+
<span className="sr-only">Rendering diagram…</span>
|
|
7
|
+
</div>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default DiagramLoader;
|
package/src/app/mdx/Mermaid.tsx
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import { DiagramLoader } from './DiagramLoader.js';
|
|
3
|
+
import { useInView } from './useInView.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Mermaid runs in the browser.
|
|
@@ -11,10 +13,16 @@ import { useEffect, useId, useRef, useState } from 'react';
|
|
|
11
13
|
export function Mermaid({ chart }: { chart: string }) {
|
|
12
14
|
const id = useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
13
15
|
const container = useRef<HTMLDivElement>(null);
|
|
16
|
+
const inView = useInView(container);
|
|
14
17
|
const [svg, setSvg] = useState<string>();
|
|
15
18
|
const [error, setError] = useState<string>();
|
|
16
19
|
|
|
17
20
|
useEffect(() => {
|
|
21
|
+
// Mermaid's layout pass is synchronous and can't be interrupted, so a large diagram
|
|
22
|
+
// freezes the main thread — including any pending navigation — the moment it starts.
|
|
23
|
+
// Waiting until the diagram is actually scrolled into view keeps that off the critical
|
|
24
|
+
// path of landing on the page.
|
|
25
|
+
if (!inView) return;
|
|
18
26
|
let cancelled = false;
|
|
19
27
|
|
|
20
28
|
void (async () => {
|
|
@@ -32,7 +40,7 @@ export function Mermaid({ chart }: { chart: string }) {
|
|
|
32
40
|
return () => {
|
|
33
41
|
cancelled = true;
|
|
34
42
|
};
|
|
35
|
-
}, [chart, id]);
|
|
43
|
+
}, [inView, chart, id]);
|
|
36
44
|
|
|
37
45
|
if (error !== undefined) {
|
|
38
46
|
return (
|
|
@@ -49,7 +57,13 @@ export function Mermaid({ chart }: { chart: string }) {
|
|
|
49
57
|
// The diagram is generated by mermaid from the page's own source, not from user input.
|
|
50
58
|
dangerouslySetInnerHTML={svg === undefined ? undefined : { __html: svg }}
|
|
51
59
|
>
|
|
52
|
-
{svg === undefined ?
|
|
60
|
+
{svg === undefined ? (
|
|
61
|
+
inView ? (
|
|
62
|
+
<DiagramLoader />
|
|
63
|
+
) : (
|
|
64
|
+
<pre className="seemore-mermaid-source">{chart}</pre>
|
|
65
|
+
)
|
|
66
|
+
) : undefined}
|
|
53
67
|
</div>
|
|
54
68
|
);
|
|
55
69
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect, useState, type RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True once the element has entered (or nearly entered) the viewport, and stays true
|
|
5
|
+
* afterward — used to defer starting expensive diagram rendering until it's actually needed,
|
|
6
|
+
* rather than the moment a page mounts.
|
|
7
|
+
*/
|
|
8
|
+
export function useInView(ref: RefObject<Element | null>): boolean {
|
|
9
|
+
const [inView, setInView] = useState(false);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (inView) return;
|
|
13
|
+
const node = ref.current;
|
|
14
|
+
if (node === null) return;
|
|
15
|
+
|
|
16
|
+
// Diagrams sit well below the fold on a typical docs page; a margin means scrolling to
|
|
17
|
+
// one feels instant rather than triggering a visible render-in-place.
|
|
18
|
+
const observer = new IntersectionObserver(
|
|
19
|
+
([entry]) => {
|
|
20
|
+
if (entry?.isIntersecting) setInView(true);
|
|
21
|
+
},
|
|
22
|
+
{ rootMargin: '200px' },
|
|
23
|
+
);
|
|
24
|
+
observer.observe(node);
|
|
25
|
+
return () => observer.disconnect();
|
|
26
|
+
}, [ref, inView]);
|
|
27
|
+
|
|
28
|
+
return inView;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default useInView;
|
|
@@ -292,6 +292,14 @@
|
|
|
292
292
|
@apply my-6 rounded-lg border border-fd-border p-4 text-sm text-fd-muted-foreground;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
.seemore-diagram-loading {
|
|
296
|
+
@apply my-6 flex items-center justify-center py-8;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.seemore-diagram-spinner {
|
|
300
|
+
@apply h-6 w-6 animate-spin rounded-full border-2 border-fd-muted-foreground/30 border-t-fd-muted-foreground;
|
|
301
|
+
}
|
|
302
|
+
|
|
295
303
|
/* Inline editing (dev only, `content.edit`). The article is the positioning context for
|
|
296
304
|
the editor layer, which sits over the block being edited. */
|
|
297
305
|
.seemore-editable {
|