seemore 1.12.3 → 1.12.5
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/README.md +10 -5
- package/package.json +1 -1
- package/src/app/entry.prerender.tsx +22 -3
- package/src/app/export/exportPage.ts +65 -2
- package/src/app/export/standalone.ts +22 -1
package/README.md
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
<br/>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="https://raw.githubusercontent.com/arifszn/seemore/main/assets/
|
|
5
|
-
<h1 align="center">seemore</h1>
|
|
4
|
+
<img src="https://raw.githubusercontent.com/arifszn/seemore/main/assets/logo.png" alt="seemore" width="280">
|
|
6
5
|
<h4 align="center">Let AI write the Markdown. Let seemore show it better — zero config documentation framework.</h4>
|
|
7
6
|
<p align="center">
|
|
8
7
|
<a href="https://www.npmjs.com/package/seemore">
|
|
9
8
|
<img src="https://img.shields.io/npm/v/seemore"/>
|
|
10
9
|
</a>
|
|
11
|
-
<a href="https://marketplace.visualstudio.com/items?itemName=arifszn.seemore-vscode">
|
|
12
|
-
<img src="https://img.shields.io/badge/VS_Code-Marketplace-007ACC?logo=visualstudiocode&logoColor=white"/>
|
|
13
|
-
</a>
|
|
14
10
|
<a href="https://open-vsx.org/extension/arifszn/seemore-vscode">
|
|
15
11
|
<img src="https://img.shields.io/badge/Open_VSX-Registry-C160EF?logo=eclipseide&logoColor=white"/>
|
|
16
12
|
</a>
|
|
13
|
+
<a href="https://marketplace.visualstudio.com/items?itemName=arifszn.seemore-vscode">
|
|
14
|
+
<img src="https://img.shields.io/badge/VS_Code-Marketplace-007ACC?logo=visualstudiocode&logoColor=white"/>
|
|
15
|
+
</a>
|
|
17
16
|
<a href="https://github.com/arifszn/seemore/actions/workflows/ci.yml">
|
|
18
17
|
<img src="https://github.com/arifszn/seemore/actions/workflows/ci.yml/badge.svg"/>
|
|
19
18
|
</a>
|
|
@@ -39,6 +38,12 @@ A folder of `.md` files has no order. You cannot click a link between files. You
|
|
|
39
38
|
|
|
40
39
|
**seemore** points at that folder and renders it as a real site. It does not move your files. It does not need any code.
|
|
41
40
|
|
|
41
|
+
## Why seemore?
|
|
42
|
+
|
|
43
|
+
Modern libraries can generate HTML, but that is not the same as previewing an existing project. seemore lets you view a folder of Markdown, such as a wiki or a notes folder, in the browser as a real site, without rewriting it, adding a build pipeline, or asking every teammate to learn HTML.
|
|
44
|
+
|
|
45
|
+
That is the value of zero configuration: you open the project and see it as intended. If you stop using seemore, your files stay exactly where they are. There is nothing to clean up.
|
|
46
|
+
|
|
42
47
|
<p align="center">
|
|
43
48
|
<img src="https://raw.githubusercontent.com/arifszn/seemore/main/packages/site/assets/home.png" alt="The seemore site: a terminal typing npx seemore to serve a folder of notes at localhost:4040, with an arrow pointing to the browser preview" width="640"/>
|
|
44
49
|
</p>
|
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';
|
|
@@ -82,7 +83,16 @@ function renderToHtml(element: ReactNode): Promise<{ html: string; failures: unk
|
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Components an MDX file can use without importing anything.
|
|
88
|
+
*
|
|
89
|
+
* Adding one that hides or defers content by default (as `CodeBlockTabs` does — Radix
|
|
90
|
+
* mounts only the active panel) needs the export path updated in the same change, or the
|
|
91
|
+
* new content goes missing from every exported file the way the tabs did:
|
|
92
|
+
* `exportComponents` below, which force-mounts it for the prerender; `captureTabPanels` in
|
|
93
|
+
* `exportPage.ts`, which harvests it for the browser export; and the runtime that wires the
|
|
94
|
+
* static markup back up (`RUNTIME` in `exportPage.ts`, `bindBehaviors` in `standalone.ts`).
|
|
95
|
+
*/
|
|
86
96
|
const PROVIDED_COMPONENTS = 'Callout, Card, Cards, CodeBlockTabs, Mermaid, D2 and Pdf';
|
|
87
97
|
|
|
88
98
|
/**
|
|
@@ -121,6 +131,11 @@ export interface ExportedArticle {
|
|
|
121
131
|
* single-page export. Rendering the component directly, rather than extracting the
|
|
122
132
|
* article from a full-page render, means no HTML parsing anywhere in the export path.
|
|
123
133
|
*
|
|
134
|
+
* Tabs render force-mounted: Radix keeps only the active panel's content in the tree, and
|
|
135
|
+
* a file with three of its four tabs empty is no export at all. Force-mounted panels hide
|
|
136
|
+
* through fumadocs' own `data-[state=inactive]:hidden` rule, which the stylesheet the
|
|
137
|
+
* export inlines already carries, and the file's runtime does the switching.
|
|
138
|
+
*
|
|
124
139
|
* Diagrams are absent here, as in every prerendered page (see `Mermaid.tsx`); the CLI
|
|
125
140
|
* export inlines a runtime that renders them when the file is opened.
|
|
126
141
|
*/
|
|
@@ -132,12 +147,16 @@ export async function renderArticle(url: string): Promise<ExportedArticle> {
|
|
|
132
147
|
if (page === undefined) throw new Error(`No page at ${url}.`);
|
|
133
148
|
|
|
134
149
|
const Content = page.default;
|
|
150
|
+
const exportComponents = {
|
|
151
|
+
...mdxComponents,
|
|
152
|
+
CodeBlockTab: (props: ComponentProps<typeof TabsContent>) => <TabsContent {...props} forceMount />,
|
|
153
|
+
};
|
|
135
154
|
// A router is still required: content links go through react-router's `Link`, which
|
|
136
155
|
// reads the routing context. A memory router with just this page is the smallest one.
|
|
137
156
|
const { html, failures } = await renderToHtml(
|
|
138
157
|
<StrictMode>
|
|
139
158
|
<MemoryRouter initialEntries={[withBase(config.base, url)]} basename={toBasename(config.base)}>
|
|
140
|
-
<Content components={
|
|
159
|
+
<Content components={exportComponents} />
|
|
141
160
|
</MemoryRouter>
|
|
142
161
|
</StrictMode>,
|
|
143
162
|
);
|
|
@@ -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'));
|