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 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/icon.png" alt="seemore" width="40" height="40">
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,6 +1,6 @@
1
1
  {
2
2
  "name": "seemore",
3
- "version": "1.12.3",
3
+ "version": "1.12.5",
4
4
  "description": "Let AI write the Markdown. Let seemore show it better — zero config documentation framework.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- /** Components an MDX file can use without importing anything. */
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={mdxComponents} />
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, click-to-zoom —
241
- * the behaviors kept, at roughly a kilobyte instead of the site bundle. Handed to React in
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'));