seemore 1.12.3 → 1.12.4
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
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Writable } from 'node:stream';
|
|
2
|
-
import { StrictMode, type ReactNode } from 'react';
|
|
2
|
+
import { StrictMode, type ComponentProps, type ReactNode } from 'react';
|
|
3
3
|
import { renderToPipeableStream } from 'react-dom/server';
|
|
4
4
|
import { MemoryRouter, RouterProvider, createMemoryRouter } from 'react-router';
|
|
5
|
+
import { TabsContent } from 'fumadocs-ui/components/tabs';
|
|
5
6
|
import { config } from 'virtual:seemore/config';
|
|
6
7
|
import { toBasename, withBase } from '../shared/base.js';
|
|
7
8
|
import { ogImagePath } from '../shared/og.js';
|
|
@@ -121,6 +122,11 @@ export interface ExportedArticle {
|
|
|
121
122
|
* single-page export. Rendering the component directly, rather than extracting the
|
|
122
123
|
* article from a full-page render, means no HTML parsing anywhere in the export path.
|
|
123
124
|
*
|
|
125
|
+
* Tabs render force-mounted: Radix keeps only the active panel's content in the tree, and
|
|
126
|
+
* a file with three of its four tabs empty is no export at all. Force-mounted panels hide
|
|
127
|
+
* through fumadocs' own `data-[state=inactive]:hidden` rule, which the stylesheet the
|
|
128
|
+
* export inlines already carries, and the file's runtime does the switching.
|
|
129
|
+
*
|
|
124
130
|
* Diagrams are absent here, as in every prerendered page (see `Mermaid.tsx`); the CLI
|
|
125
131
|
* export inlines a runtime that renders them when the file is opened.
|
|
126
132
|
*/
|
|
@@ -132,12 +138,16 @@ export async function renderArticle(url: string): Promise<ExportedArticle> {
|
|
|
132
138
|
if (page === undefined) throw new Error(`No page at ${url}.`);
|
|
133
139
|
|
|
134
140
|
const Content = page.default;
|
|
141
|
+
const exportComponents = {
|
|
142
|
+
...mdxComponents,
|
|
143
|
+
CodeBlockTab: (props: ComponentProps<typeof TabsContent>) => <TabsContent {...props} forceMount />,
|
|
144
|
+
};
|
|
135
145
|
// A router is still required: content links go through react-router's `Link`, which
|
|
136
146
|
// reads the routing context. A memory router with just this page is the smallest one.
|
|
137
147
|
const { html, failures } = await renderToHtml(
|
|
138
148
|
<StrictMode>
|
|
139
149
|
<MemoryRouter initialEntries={[withBase(config.base, url)]} basename={toBasename(config.base)}>
|
|
140
|
-
<Content components={
|
|
150
|
+
<Content components={exportComponents} />
|
|
141
151
|
</MemoryRouter>
|
|
142
152
|
</StrictMode>,
|
|
143
153
|
);
|
|
@@ -216,6 +216,42 @@ function buildExportToc(article: Element): Element | undefined {
|
|
|
216
216
|
return nav;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Radix Tabs keeps only the active panel's content mounted, so the article's DOM alone
|
|
221
|
+
* misses every inactive tab. Walk each tablist, select each trigger in turn — which also
|
|
222
|
+
* gives any diagram inside the panel its scroll-into-view — and record the panel's HTML
|
|
223
|
+
* while it is up. The first trigger is re-selected so the page is left as it was found.
|
|
224
|
+
*/
|
|
225
|
+
async function captureTabPanels(): Promise<Map<string, string>> {
|
|
226
|
+
const captured = new Map<string, string>();
|
|
227
|
+
const scrollX = window.scrollX;
|
|
228
|
+
const scrollY = window.scrollY;
|
|
229
|
+
|
|
230
|
+
for (const tablist of document.querySelectorAll('[role="tablist"]')) {
|
|
231
|
+
const triggers = Array.from(tablist.querySelectorAll<HTMLButtonElement>('button[role="tab"]'));
|
|
232
|
+
for (const trigger of triggers) {
|
|
233
|
+
// Radix activates a tab on `mousedown`, not `click` — and a React re-render means
|
|
234
|
+
// the panel's content appears a tick later, so both sides are handled by hand.
|
|
235
|
+
trigger.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window, button: 0 }));
|
|
236
|
+
const panel = document.getElementById(trigger.getAttribute('aria-controls') ?? '');
|
|
237
|
+
if (panel === null) continue;
|
|
238
|
+
await waitFor(() => panel.childElementCount > 0, 15_000);
|
|
239
|
+
panel.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
240
|
+
await waitFor(
|
|
241
|
+
() =>
|
|
242
|
+
panel.querySelector('.seemore-mermaid, .seemore-d2') === null ||
|
|
243
|
+
panel.querySelector('svg, .seemore-mermaid-error, .seemore-d2-error') !== null,
|
|
244
|
+
15_000,
|
|
245
|
+
);
|
|
246
|
+
captured.set(panel.id, panel.innerHTML);
|
|
247
|
+
}
|
|
248
|
+
triggers[0]?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window, button: 0 }));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
window.scrollTo(scrollX, scrollY);
|
|
252
|
+
return captured;
|
|
253
|
+
}
|
|
254
|
+
|
|
219
255
|
/** The article is the only thing taken, but dev leaves editor affordances inside it. */
|
|
220
256
|
function cleanArticleForExport(article: Element): Element {
|
|
221
257
|
const clone = article.cloneNode(true) as Element;
|
|
@@ -237,8 +273,9 @@ function escapeHtml(text: string): string {
|
|
|
237
273
|
}
|
|
238
274
|
|
|
239
275
|
/**
|
|
240
|
-
* The exported file's runtime: theme toggle, code copy, heading anchor copy,
|
|
241
|
-
* the behaviors kept, at roughly a kilobyte instead of the site bundle.
|
|
276
|
+
* The exported file's runtime: theme toggle, code copy, heading anchor copy, tabs,
|
|
277
|
+
* click-to-zoom — the behaviors kept, at roughly a kilobyte instead of the site bundle.
|
|
278
|
+
* Handed to React in
|
|
242
279
|
* hydration on the live page; here each is a few lines against the static DOM.
|
|
243
280
|
*
|
|
244
281
|
* Kept free of `</script>`-shaped sequences by construction: it is inlined verbatim.
|
|
@@ -280,6 +317,27 @@ const RUNTIME = `(function () {
|
|
|
280
317
|
});
|
|
281
318
|
});
|
|
282
319
|
|
|
320
|
+
// Tabs are exported as the static Radix markup the live page hydrated; the file wires
|
|
321
|
+
// them back up by hand — a click selects within its tablist and swaps the panels.
|
|
322
|
+
document.querySelectorAll('[role="tablist"]').forEach(function (tablist) {
|
|
323
|
+
var triggers = [].slice.call(tablist.querySelectorAll('button[role="tab"]'));
|
|
324
|
+
triggers.forEach(function (trigger) {
|
|
325
|
+
trigger.addEventListener('click', function () {
|
|
326
|
+
triggers.forEach(function (other) {
|
|
327
|
+
var active = other === trigger;
|
|
328
|
+
other.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
329
|
+
other.setAttribute('data-state', active ? 'active' : 'inactive');
|
|
330
|
+
other.setAttribute('tabindex', active ? '0' : '-1');
|
|
331
|
+
var panel = document.getElementById(other.getAttribute('aria-controls') || '');
|
|
332
|
+
if (panel) {
|
|
333
|
+
panel.hidden = !active;
|
|
334
|
+
panel.setAttribute('data-state', active ? 'active' : 'inactive');
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
283
341
|
// The live site's TOC follows the reader with fumadocs' own scroll tracking; in the file,
|
|
284
342
|
// the plainest version of the same behaviour — last heading above the fold wins.
|
|
285
343
|
var tocLinks = [].slice.call(document.querySelectorAll('.seemore-export-toc a'));
|
|
@@ -398,7 +456,12 @@ export async function exportPageAsHtml(): Promise<void> {
|
|
|
398
456
|
|
|
399
457
|
await prepareDiagrams();
|
|
400
458
|
|
|
459
|
+
const tabs = await captureTabPanels();
|
|
401
460
|
const clone = cleanArticleForExport(article);
|
|
461
|
+
for (const [id, html] of tabs) {
|
|
462
|
+
const panel = clone.querySelector(`[id="${CSS.escape(id)}"]`);
|
|
463
|
+
if (panel !== null) panel.innerHTML = html;
|
|
464
|
+
}
|
|
402
465
|
await inlineImages(clone);
|
|
403
466
|
|
|
404
467
|
const toc = buildExportToc(clone);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* bundled to an IIFE and inlined into the file, finishes the job when the page is opened:
|
|
7
7
|
* each diagram is rendered from the source `pre` the site's own components leave in
|
|
8
8
|
* prerendered output. It also binds the behaviors the export keeps — theme toggle, code
|
|
9
|
-
* copy, click-to-zoom — so the file behaves like the browser-exported one.
|
|
9
|
+
* copy, tabs, click-to-zoom — so the file behaves like the browser-exported one.
|
|
10
10
|
*
|
|
11
11
|
* The browser export ships a hand-written twin of the behavior half (the `RUNTIME` string
|
|
12
12
|
* in `exportPage.ts`): it needs no diagram half, because its diagrams are already SVG when
|
|
@@ -96,6 +96,27 @@ function bindBehaviors(): void {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// Tabs are exported as the static Radix markup the live page hydrated; the file wires
|
|
100
|
+
// them back up by hand — a click selects within its tablist and swaps the panels.
|
|
101
|
+
for (const tablist of document.querySelectorAll('[role="tablist"]')) {
|
|
102
|
+
const triggers = Array.from(tablist.querySelectorAll('button[role="tab"]'));
|
|
103
|
+
for (const trigger of triggers) {
|
|
104
|
+
trigger.addEventListener('click', () => {
|
|
105
|
+
for (const other of triggers) {
|
|
106
|
+
const active = other === trigger;
|
|
107
|
+
other.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
108
|
+
other.setAttribute('data-state', active ? 'active' : 'inactive');
|
|
109
|
+
other.setAttribute('tabindex', active ? '0' : '-1');
|
|
110
|
+
const panel = document.getElementById(other.getAttribute('aria-controls') ?? '');
|
|
111
|
+
if (panel !== null) {
|
|
112
|
+
panel.hidden = !active;
|
|
113
|
+
panel.setAttribute('data-state', active ? 'active' : 'inactive');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
// The live site's TOC follows the reader with fumadocs' own scroll tracking; in the file,
|
|
100
121
|
// the plainest version of the same behaviour — last heading above the fold wins.
|
|
101
122
|
const tocLinks = Array.from(document.querySelectorAll<HTMLAnchorElement>('.seemore-export-toc a'));
|