@clidey/dory 1.9.0 → 1.10.0

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.
Files changed (46) hide show
  1. package/README.md +57 -173
  2. package/bin/dist/dependency-overlay.d.ts +7 -0
  3. package/bin/dist/dependency-overlay.d.ts.map +1 -0
  4. package/bin/dist/dependency-overlay.js +137 -0
  5. package/bin/dist/dependency-overlay.js.map +1 -0
  6. package/bin/dist/dory.js +4 -2
  7. package/bin/dist/dory.js.map +1 -1
  8. package/bin/dist/preview-server.d.ts.map +1 -1
  9. package/bin/dist/preview-server.js +4 -17
  10. package/bin/dist/preview-server.js.map +1 -1
  11. package/bin/dist/preview-static.d.ts +7 -0
  12. package/bin/dist/preview-static.d.ts.map +1 -0
  13. package/bin/dist/preview-static.js +70 -0
  14. package/bin/dist/preview-static.js.map +1 -0
  15. package/dist/index.html +19 -19
  16. package/dist/runtime/dory-runtime.css +1 -1
  17. package/dist/runtime/dory-runtime.js +305 -305
  18. package/docs/components/layout.mdx +3 -0
  19. package/docs/components/lists.mdx +2 -2
  20. package/docs/components/overview.mdx +1 -1
  21. package/docs/components/typography.mdx +4 -4
  22. package/docs/configuration/navigation.mdx +3 -3
  23. package/docs/contributing.mdx +47 -0
  24. package/docs/dory.json +13 -6
  25. package/docs/getting-started/automation.mdx +2 -2
  26. package/docs/getting-started/custom-components.mdx +60 -0
  27. package/docs/social-card.png +0 -0
  28. package/docs/social-card.svg +1 -1
  29. package/package.json +1 -1
  30. package/scripts/verify-component-examples.mjs +126 -0
  31. package/scripts/verify-runtime-parity.mjs +5 -4
  32. package/src/app.css +3 -0
  33. package/src/main.tsx +1 -1
  34. package/src/mdx/latex.test.ts +38 -0
  35. package/src/mdx/latex.tsx +42 -65
  36. package/src/mdx/preprocess.ts +2 -2
  37. package/src/mdx/processor.test.ts +135 -37
  38. package/src/mdx/processor.ts +56 -2
  39. package/src/mdx/specimen.tsx +32 -50
  40. package/src/mdx/tabs.tsx +15 -12
  41. package/src/mdx/text.tsx +2 -2
  42. package/src/mdx/type-scale.tsx +3 -3
  43. package/src/plugins/sanitize.ts +5 -4
  44. package/vite.config.ssr.ts +1 -1
  45. package/vite.config.ts +1 -1
  46. package/docs/getting-started/whodb-ee-apps.mdx +0 -122
@@ -122,6 +122,8 @@ Use `sticky` on the code column so it stays visible while the prose scrolls.
122
122
 
123
123
  </Specimen>
124
124
 
125
+ Rows stack on narrow screens and use the requested column count from the small-screen breakpoint (640px). Columns can shrink within their grid so long content stays inside the preview.
126
+
125
127
  ## Specimen
126
128
 
127
129
  `<Specimen>` frames a component example with **Preview**, **Code** and **Props** tabs. Put the MDX source in an ` ```mdx ` fence, the rendered example after it, and optional `<Prop>` rows inside `<SpecimenProps>`. Tabs only appear for the parts you include.
@@ -149,6 +151,7 @@ Use `sticky` on the code column so it stays visible while the prose scrolls.
149
151
  <SpecimenProps>
150
152
 
151
153
  <Prop name="label" type="string">Mono caption in the header, usually the component tag. Write angle brackets as `&lt;` and `&gt;`.</Prop>
154
+ <Prop name="padding" type="'default' | 'none'">Preview spacing. Defaults to an inset preview. Use `none` only when the example provides its own padding.</Prop>
152
155
  <Prop name="children" type="ReactNode">An ` ```mdx ` fence (Code tab), a `<SpecimenProps>` block (Props tab), and the preview.</Prop>
153
156
 
154
157
  </SpecimenProps>
@@ -123,12 +123,12 @@ Standard Markdown list items support inline formatting:
123
123
  - **Bold item** — important information
124
124
  - *Italic item* — supplementary detail
125
125
  - Item with `inline code` — technical terms
126
- - Item with a [link](https://dory.clidey.com) — external reference
126
+ - Item with a [link](https://clidey.github.io/dory/) — external reference
127
127
  ```
128
128
 
129
129
  - **Bold item** — important information
130
130
  - *Italic item* — supplementary detail
131
131
  - Item with `inline code` — technical terms
132
- - Item with a [link](https://dory.clidey.com) — external reference
132
+ - Item with a [link](https://clidey.github.io/dory/) — external reference
133
133
 
134
134
  </Specimen>
@@ -36,7 +36,7 @@ Dory ships with **24 built-in MDX components** you can use directly in `.mdx` fi
36
36
  <Card title="Mermaid Diagrams" href="/components/mermaid" icon="git-branch" description="Flowcharts, sequence diagrams, ER diagrams, and more." />
37
37
  <Card title="LaTeX Math" href="/components/latex" icon="sigma" description="Render mathematical expressions using KaTeX." />
38
38
  <Card title="Images" href="/components/images" icon="image" description="Clickable images with fullscreen modal overlay." />
39
- <Card title="Icons" href="/components/icons" icon="star" description="Inline icons from the WhoDB suite." />
39
+ <Card title="Icons" href="/components/icons" icon="star" description="Inline icons from the shared Clidey UX catalog." />
40
40
  </CardGroup>
41
41
 
42
42
  ## Utility Components
@@ -19,14 +19,14 @@ Markdown headings map to the display scale. Inline formatting covers emphasis, c
19
19
  ### Heading 3
20
20
  #### Heading 4
21
21
 
22
- **Bold**, *italic*, `inline code`, [a link](https://dory.clidey.com), <kbd>⌘K</kbd>, and <mark>a highlight</mark>.
22
+ **Bold**, *italic*, `inline code`, [a link](https://clidey.github.io/dory/), <kbd>⌘K</kbd>, and <mark>a highlight</mark>.
23
23
 
24
24
  > Blockquotes hold asides and quotes.
25
25
  ```
26
26
 
27
27
  <TypeScale />
28
28
 
29
- **Bold**, *italic*, `inline code`, [a link](https://dory.clidey.com), <kbd>⌘K</kbd>, and <mark>a highlight</mark>.
29
+ **Bold**, *italic*, `inline code`, [a link](https://clidey.github.io/dory/), <kbd>⌘K</kbd>, and <mark>a highlight</mark>.
30
30
 
31
31
  > Blockquotes hold asides and quotes.
32
32
 
@@ -67,10 +67,10 @@ Use standard Markdown link syntax for hyperlinks:
67
67
  <Specimen label="[link](url)">
68
68
 
69
69
  ```mdx
70
- [Visit the Dory site](https://dory.clidey.com)
70
+ [Visit the Dory site](https://clidey.github.io/dory/)
71
71
  ```
72
72
 
73
- [Visit the Dory site](https://dory.clidey.com)
73
+ [Visit the Dory site](https://clidey.github.io/dory/)
74
74
 
75
75
  </Specimen>
76
76
 
@@ -205,10 +205,10 @@ Add a `lead` string to show a larger introductory paragraph directly under the p
205
205
  ```mdx
206
206
  ---
207
207
  title: 'Introduction'
208
- description: 'What WhoDB is and how to get started.'
209
- lead: 'WhoDB is a database management tool for exploring schemas, managing data and visualizing relationships.'
208
+ description: 'What Dory is and how to get started.'
209
+ lead: 'Dory turns MDX pages into a documentation site with navigation, search, and interactive components.'
210
210
  meta:
211
- Edition: Community
211
+ License: MIT
212
212
  Updated: 2026-10-12
213
213
  Read: 4 min
214
214
  ---
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: 'Contributing to Dory'
3
+ description: 'Develop Dory, review its own documentation, and understand the publishing workflow.'
4
+ ---
5
+
6
+ Dory's public README introduces the product. This guide covers work on the repository itself. To create a separate documentation site, use the [quick start](/getting-started/quick-start).
7
+
8
+ ## Run the repository
9
+
10
+ ```bash
11
+ git clone https://github.com/clidey/dory.git
12
+ cd dory
13
+ pnpm install --frozen-lockfile --ignore-scripts
14
+ pnpm dev
15
+ ```
16
+
17
+ Edit Dory's own pages under `docs/`. Keep dependency lifecycle scripts disabled and preserve the repository's dependency integrity and release-age policies.
18
+
19
+ ## Validate changes
20
+
21
+ ```bash
22
+ pnpm test
23
+ node --import tsx --test bin/dependency-overlay.test.mts bin/preview-static.test.mts
24
+ pnpm build
25
+ node bin/dist/dory.js check
26
+ node scripts/verify-component-examples.mjs
27
+ ```
28
+
29
+ The component browser check uses installed Chrome (or `DORY_CHROME_PATH`), measures preview padding and clipping, exercises tabs, and verifies math loading and failure states on desktop and mobile. It saves screenshots and a report under ignored `.dory/component-review/`; inspect these before publishing.
30
+
31
+ Review browser behavior in light and dark modes. For custom components, verify interactions and focus handling; compilation alone cannot do that.
32
+
33
+ ## Preview the production site
34
+
35
+ ```bash
36
+ pnpm preview --host 127.0.0.1 --port 3188 --strictPort
37
+ ```
38
+
39
+ Open `http://127.0.0.1:3188/dory/`. Review code and the exact generated site before pushing or publishing changes.
40
+
41
+ ## Publish this repository's docs
42
+
43
+ The documentation workflow validates pull requests and deploys main-branch documentation to `https://clidey.github.io/dory/`. Its canonical URL and path are configured in `docs/dory.json`; GitHub Pages uses GitHub Actions as the publishing source.
44
+
45
+ Publish only the generated `dist/` artifact. Static Pages hosting includes navigation and search. Documentation chat and HTTP MCP need a separately hosted Dory server; do not put server credentials into static assets.
46
+
47
+ Imported MDX and component modules are executable source. Review them, build untrusted contributions without secrets or deployment credentials, and deploy reviewed artifacts separately. See the repository security policy for the trust boundary.
package/docs/dory.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "Dory",
3
- "url": "https://dory.clidey.com",
3
+ "url": "https://clidey.github.io",
4
4
  "image": "/social-card.png",
5
- "version": "1.0.8 · Latest",
5
+ "version": "1.10.0 \u00b7 Latest",
6
6
  "copyright": "Clidey",
7
7
  "repository": {
8
8
  "url": "https://github.com/clidey/dory",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "description": "Static documentation site generator for modern teams. Build beautiful docs from MDX files.",
13
13
  "seo": {
14
- "titleTemplate": "%s — Dory"
14
+ "titleTemplate": "%s \u2014 Dory"
15
15
  },
16
16
  "favicon": "./favicon.svg",
17
17
  "fonts": {
@@ -39,8 +39,8 @@
39
39
  "getting-started/installation",
40
40
  "getting-started/quick-start",
41
41
  "getting-started/automation",
42
- "getting-started/whodb-ee-apps",
43
- "getting-started/configuration"
42
+ "getting-started/configuration",
43
+ "getting-started/custom-components"
44
44
  ]
45
45
  },
46
46
  {
@@ -55,6 +55,12 @@
55
55
  "configuration/internationalization",
56
56
  "configuration/embed"
57
57
  ]
58
+ },
59
+ {
60
+ "group": "Contribute",
61
+ "pages": [
62
+ "contributing"
63
+ ]
58
64
  }
59
65
  ]
60
66
  },
@@ -125,5 +131,6 @@
125
131
  ]
126
132
  }
127
133
  ]
128
- }
134
+ },
135
+ "basePath": "/dory"
129
136
  }
@@ -30,8 +30,8 @@ A Dory site served with `dory preview` also exposes a public, read-only Streamab
30
30
  Add the endpoint once in your MCP client:
31
31
 
32
32
  ```bash
33
- codex mcp add whodb-docs --url https://docs.whodb.com/mcp
34
- claude mcp add --transport http whodb-docs https://docs.whodb.com/mcp
33
+ codex mcp add example-docs --url https://docs.example.com/mcp
34
+ claude mcp add --transport http example-docs https://docs.example.com/mcp
35
35
  ```
36
36
 
37
37
  Replace the example URL with your Dory site. If the docs run behind a reverse proxy, forward `/mcp` to the Dory preview server. Its `dory.json` `url` must match the public hostname; for an additional preview hostname, set `DORY_MCP_ALLOWED_HOSTS` to a comma-separated list. The endpoint rejects other hostnames and cross-origin browser requests.
@@ -0,0 +1,60 @@
1
+ ---
2
+ title: 'Custom interactive components'
3
+ description: 'Import a local component into static MDX and let readers try its real behavior.'
4
+ ---
5
+
6
+ Static Dory projects can import JSX components into an MDX page. Use a local component module for hooks, events, and stateful interactions; Dory preserves its imported name during preprocessing.
7
+
8
+ ## Put the implementation under docs
9
+
10
+ ```tsx
11
+ // docs/examples/counter.tsx
12
+ import { useState } from 'react';
13
+ import { Button } from '@clidey/ux';
14
+
15
+ export function Counter() {
16
+ const [count, setCount] = useState(0);
17
+ return <div>
18
+ <Button onClick={() => setCount(value => value + 1)}>Add one</Button>
19
+ <p role="status">Clicked {count} times</p>
20
+ </div>;
21
+ }
22
+ ```
23
+
24
+ ## Import it into a page
25
+
26
+ ```mdx
27
+ ---
28
+ title: 'Try the button'
29
+ ---
30
+
31
+ import { Counter } from '../examples/counter.tsx';
32
+
33
+ <Counter />
34
+ ```
35
+
36
+ Default imports, named aliases, and namespace imports are supported. Keep imported component names explicit. An unimported custom tag retains the existing prose-escaping behavior; a name mentioned inside a fenced example does not register a component.
37
+
38
+ ## Keep state in the module
39
+
40
+ Dory treats inline MDX text expressions as literal code. Put state displays, event handlers, and business logic in the imported component module instead of inline MDX expressions. Import a scoped stylesheet from the module if the example needs layout styling.
41
+
42
+ Static builds and prerendering alias React to Preact compatibility. Keep browser-only work in effects or event handlers, and make the initial render work without `window` or `document`.
43
+
44
+ ## Build boundaries
45
+
46
+ The CLI stages the documentation directory into an isolated build workspace. Keep local modules and styles under that directory. Imports reaching into a sibling application source tree do not survive staging.
47
+
48
+ The build resolves Dory's installed dependencies first, then makes missing installed dependencies from the consuming project available up to its nearest workspace or lockfile root. React uses Dory's Preact compatibility aliases.
49
+
50
+ For an intentional package replacement, set `DORY_PACKAGE_OVERRIDES` to a JSON object mapping package names to absolute package directories. Dory validates each name, directory, and matching package manifest before creating the isolated dependency overlay. Installed dependencies are not modified.
51
+
52
+ ## Security and trusted source
53
+
54
+ Imported components are executable application code. Their modules may execute during bundling and server rendering with the build process's privileges, then in the reader's browser. Review their source and dependencies before building. The staging directory is not a security sandbox. Build pull requests on disposable runners without secrets, persisted checkout credentials, or deployment tokens, and deploy a reviewed artifact in a separate job. See the repository security policy.
55
+
56
+ ## Static source and embedded content
57
+
58
+ This feature applies to trusted documentation built from a local checkout. It does not enable custom imports in Dory's sandboxed embedded runtime. That runtime keeps its existing component allowlist, import restrictions, and content security policy.
59
+
60
+ Run `dory check`, build the site, then test the interactions in the actual output. Compilation alone does not prove a button updates state or an overlay handles focus correctly.
Binary file
@@ -13,5 +13,5 @@
13
13
  <text x="76" y="420" fill="#b2bdd0" font-family="Hanken Grotesk,Arial,sans-serif" font-size="26">Build beautiful documentation from MDX.</text>
14
14
  <path d="M76 512H1124" stroke="#344767"/>
15
15
  <text x="76" y="559" fill="#aebace" font-family="Hanken Grotesk,Arial,sans-serif" font-size="21">Open source. Built for teams.</text>
16
- <text x="1124" y="559" text-anchor="end" fill="#8caffb" font-family="Hanken Grotesk,Arial,sans-serif" font-size="21">dory.clidey.com</text>
16
+ <text x="1124" y="559" text-anchor="end" fill="#8caffb" font-family="Hanken Grotesk,Arial,sans-serif" font-size="21">clidey.github.io/dory</text>
17
17
  </svg>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@clidey/dory",
3
3
  "private": false,
4
- "version": "1.9.0",
4
+ "version": "1.10.0",
5
5
  "type": "module",
6
6
  "description": "A CLI tool for building and previewing documentation sites",
7
7
  "keywords": [
@@ -0,0 +1,126 @@
1
+ import assert from 'node:assert/strict';
2
+ import {mkdir, readFile, writeFile} from 'node:fs/promises';
3
+ import {dirname, join, resolve} from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+ import {chromium} from 'playwright-core';
6
+ import {preview} from 'vite';
7
+
8
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
9
+ const config = JSON.parse(await readFile(join(root, 'docs/dory.json'), 'utf8'));
10
+ const routes = config.navigation.tabs.flatMap(tab => tab.groups.flatMap(group => group.pages))
11
+ .map(page => typeof page === 'string' ? page : page.page).filter(page => page.startsWith('components/'));
12
+ const output = join(root, '.dory/component-review');
13
+ await mkdir(output, {recursive: true});
14
+ const report = {pages: 0, previews: 0, errors: [], screenshots: []};
15
+ let server;
16
+ let browser;
17
+ try {
18
+ let base = process.argv[2];
19
+ if (!base) {
20
+ server = await preview({root, preview: {host: '127.0.0.1', port: 3189, strictPort: true}});
21
+ base = server.resolvedUrls.local[0];
22
+ }
23
+ if (!base.endsWith('/')) base += '/';
24
+ browser = await chromium.launch({channel: 'chrome', headless: true, ...(process.env.DORY_CHROME_PATH ? {executablePath: process.env.DORY_CHROME_PATH} : {})});
25
+ // A missing lazy math asset must leave a readable equation and an error, not
26
+ // a blank spinner. This context has its own cache so the request is real.
27
+ const unavailable = await browser.newContext({viewport: {width: 390, height: 844}});
28
+ const failedMath = await unavailable.newPage();
29
+ await failedMath.route(/\/assets\/katex-[^/]+\.js$/, route => route.abort());
30
+ await failedMath.goto(new URL('components/latex/', base).href, {waitUntil: 'domcontentloaded'});
31
+ await failedMath.getByRole('alert').filter({hasText: 'The equation could not be rendered.'}).first().waitFor();
32
+ assert.ok((await failedMath.locator('[data-latex-fallback]').first().innerText()).trim(), 'Failed math retains readable content');
33
+ assert.equal(await failedMath.locator('.dory-latex .animate-spin').count(), 0, 'Failed math does not spin forever');
34
+ await unavailable.close();
35
+ report.mathFailureFallback = true;
36
+ for (const scenario of [
37
+ {name: 'desktop-dark', viewport: {width: 1440, height: 1000}, colorScheme: 'dark'},
38
+ {name: 'mobile-light', viewport: {width: 390, height: 844}, colorScheme: 'light'},
39
+ ]) {
40
+ const context = await browser.newContext({viewport: scenario.viewport, colorScheme: scenario.colorScheme});
41
+ const page = await context.newPage();
42
+ page.on('pageerror', error => report.errors.push(`${scenario.name}: ${error.message}`));
43
+ for (const route of routes) {
44
+ const label = `${scenario.name} ${route}`;
45
+ try {
46
+ const response = await page.goto(new URL(route + '/', base).href, {waitUntil: 'domcontentloaded'});
47
+ assert.equal(response.status(), 200, `${label}: HTTP status`);
48
+ await page.locator('article').waitFor();
49
+ await page.evaluate(() => document.fonts.ready);
50
+ assert.equal(await page.getByText('This page failed to render', {exact: true}).count(), 0, `${label}: render boundary`);
51
+ const previews = page.locator('[data-dory-preview]:visible');
52
+ assert.equal(await previews.count(), await page.locator('.dory-specimen:visible').count(), `${label}: every example has a visible preview`);
53
+ for (let index = 0; index < await previews.count(); index++) {
54
+ const panel = previews.nth(index);
55
+ const padding = await panel.evaluate(element => {
56
+ const style = getComputedStyle(element);
57
+ return [style.paddingTop, style.paddingRight, style.paddingBottom, style.paddingLeft].map(parseFloat);
58
+ });
59
+ const expected = await panel.getAttribute('data-padding');
60
+ assert.ok(padding.every(value => expected === 'none' ? value === 0 : value >= 16), `${label} preview ${index + 1}: missing inset (${padding})`);
61
+ const clipped = await panel.evaluate(element => {
62
+ const box = element.getBoundingClientRect();
63
+ const style = getComputedStyle(element);
64
+ const left = box.left + parseFloat(style.paddingLeft);
65
+ const right = box.right - parseFloat(style.paddingRight);
66
+ return [...element.querySelectorAll('.dory-latex, .dory-expandable, .dory-accordion, .dory-table, .dory-card, .dory-callout')]
67
+ .filter(child => child.getBoundingClientRect().width > 0)
68
+ .filter(child => { const bounds = child.getBoundingClientRect(); return bounds.left < left - 1 || bounds.right > right + 1; })
69
+ .map(child => child.className);
70
+ });
71
+ assert.deepEqual(clipped, [], `${label} preview ${index + 1}: content clipped by frame`);
72
+ report.previews++;
73
+ }
74
+ if (route === 'components/latex') {
75
+ await page.waitForFunction(() => {
76
+ const formulas = [...document.querySelectorAll('.dory-latex, .dory-latex-inline')];
77
+ return formulas.length > 0 && formulas.every(element => element.querySelector('.katex'));
78
+ }, undefined, {timeout: 15000});
79
+ assert.equal(await page.locator('[data-latex-fallback]').count(), 0, `${label}: math still loading`);
80
+ assert.equal(await page.locator('.katex-error').count(), 0, `${label}: invalid formula`);
81
+ }
82
+ if (route === 'components/typography') {
83
+ assert.equal(await page.locator('.dory-type-scale > div').count(), 4, `${label}: heading samples`);
84
+ assert.equal(await page.locator('.dory-type-scale').evaluate(element => element.scrollWidth > element.clientWidth + 1), false, `${label}: heading sample overflows`);
85
+ }
86
+ if (route === 'components/expandable') {
87
+ const specimen = page.locator('.dory-specimen').first();
88
+ const inner = specimen.locator('.dory-expandable').first();
89
+ const outerBounds = await specimen.boundingBox();
90
+ const innerBounds = await inner.boundingBox();
91
+ assert.ok(innerBounds.x - outerBounds.x >= 16, `${label}: nested border touches frame`);
92
+ await inner.getByRole('button', {name: 'Response Body', exact: true}).click();
93
+ await inner.locator('[data-state="open"]').first().waitFor();
94
+ }
95
+ const specimen = page.locator('.dory-specimen').first();
96
+ if (await specimen.count()) {
97
+ const codeTab = specimen.getByRole('tab', {name: 'Code', exact: true}).first();
98
+ if (await codeTab.count()) {
99
+ await codeTab.click();
100
+ await page.waitForFunction(element => element.getAttribute('aria-selected') === 'true', await codeTab.elementHandle());
101
+ assert.ok(await specimen.locator('[role="tabpanel"]:visible code').count(), `${label}: source panel missing`);
102
+ await specimen.getByRole('tab', {name: 'Preview', exact: true}).first().click();
103
+ }
104
+ if (['components/typography', 'components/expandable', 'components/latex', 'components/callouts'].includes(route)) {
105
+ const file = `${scenario.name}-${route.split('/')[1]}.png`;
106
+ await specimen.screenshot({path: join(output, file)});
107
+ report.screenshots.push(file);
108
+ }
109
+ }
110
+ assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), true, `${label}: page overflows horizontally`);
111
+ report.pages++;
112
+ console.log(`PASS ${label}`);
113
+ } catch (error) {
114
+ report.errors.push(error.message);
115
+ console.error(`FAIL ${error.message}`);
116
+ }
117
+ }
118
+ await context.close();
119
+ }
120
+ } finally {
121
+ await browser?.close();
122
+ await new Promise(resolve => server ? server.httpServer.close(resolve) : resolve());
123
+ await writeFile(join(output, 'report.json'), JSON.stringify(report, null, 2) + '\n');
124
+ }
125
+ console.log(JSON.stringify(report, null, 2));
126
+ if (report.errors.length) process.exitCode = 1;
@@ -6,7 +6,7 @@ import { preview } from 'vite';
6
6
 
7
7
  const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
8
8
  const docsRoot = join(root, 'docs');
9
- const pagePath = 'getting-started/whodb-ee-apps';
9
+ const pagePath = 'getting-started/automation';
10
10
 
11
11
  function docsFiles(directory = docsRoot) {
12
12
  return readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
@@ -19,6 +19,7 @@ function docsFiles(directory = docsRoot) {
19
19
  }
20
20
 
21
21
  const config = JSON.parse(readFileSync(join(docsRoot, 'dory.json'), 'utf8'));
22
+ const basePath = String(config.basePath || '').replace(/\/$/, '');
22
23
  const logo = readFileSync(join(docsRoot, 'logo/logo.svg')).toString('base64');
23
24
  config.logo = {
24
25
  light: `data:image/svg+xml;base64,${logo}`,
@@ -100,17 +101,17 @@ try {
100
101
  browser = await chromium.launch({ channel: 'chrome', headless: true });
101
102
  for (const theme of ['light', 'dark']) {
102
103
  const full = await browser.newPage({ viewport: { width: 2048, height: 1200 } });
103
- await full.goto(`${origin}/${pagePath}`, { waitUntil: 'networkidle' });
104
+ await full.goto(`${origin}${basePath}/${pagePath}`, { waitUntil: 'networkidle' });
104
105
  if (theme === 'dark') await full.locator('html').evaluate(element => { element.classList.remove('light'); element.classList.add('dark'); });
105
106
  await full.evaluate(() => document.fonts.ready);
106
107
  const fullMetrics = await full.evaluate(visualMetrics);
107
108
 
108
109
  const embed = await browser.newPage({ viewport: { width: 2048, height: 1200 } });
109
- await embed.goto(`${origin}/runtime/dory-runtime.html`, { waitUntil: 'networkidle' });
110
+ await embed.goto(`${origin}${basePath}/runtime/dory-runtime.html`, { waitUntil: 'networkidle' });
110
111
  await embed.evaluate(({ files, config, theme, pagePath }) => {
111
112
  window.postMessage({ __dory_docs: true, type: 'load', files, path: `docs/${pagePath}.mdx`, theme, locale: 'en_US', config }, '*');
112
113
  }, { files: docsFiles(), config, theme, pagePath });
113
- await embed.getByRole('heading', { name: 'Document WhoDB EE apps' }).waitFor();
114
+ await embed.getByRole('heading', { name: 'CLI and MCP automation' }).waitFor();
114
115
  await embed.evaluate(() => document.fonts.ready);
115
116
  const embedMetrics = await embed.evaluate(visualMetrics);
116
117
  compare(fullMetrics, embedMetrics, theme);
package/src/app.css CHANGED
@@ -3,6 +3,9 @@
3
3
  @import "tailwindcss/theme.css" layer(theme);
4
4
  @import "tailwindcss/utilities.css" layer(utilities);
5
5
 
6
+ /* Emit UX variants alongside Dory’s utilities so responsive rules stay ordered. */
7
+ @source "../node_modules/@clidey/ux/dist";
8
+
6
9
  @custom-variant dark (&:where(.dark, .dark *));
7
10
 
8
11
  .mermaid-wrapper .mermaid svg {
package/src/main.tsx CHANGED
@@ -43,7 +43,7 @@ if (embedTheme) document.documentElement.style.colorScheme = embedTheme;
43
43
 
44
44
  const tree = (
45
45
  <ThemeProvider defaultTheme={defaultTheme} storageKey={embedTheme ? `@clidey/dory/embed-theme-${embedTheme}` : '@clidey/dory/theme'}>
46
- <Toaster />
46
+ <Toaster closeButton />
47
47
  <ErrorBoundary>
48
48
  <Router base={BASE_PATH}>
49
49
  <AppWithMDX />
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createElement } from 'preact';
3
+ import { renderToString } from 'preact-render-to-string';
4
+ import katex from 'katex';
5
+ import { Latex, renderLatexExpression } from './latex';
6
+
7
+ describe('LaTeX rendering', () => {
8
+ it('server-renders a readable display fallback with the same client shell', () => {
9
+ const html = renderToString(createElement(Latex, { children: 'E = mc^2' }));
10
+ expect(html).toContain('dory-latex not-prose');
11
+ expect(html).toContain('aria-busy="true"');
12
+ expect(html).toContain('data-latex-fallback');
13
+ expect(html).toContain('E = mc^2');
14
+ expect(html).not.toContain('animate-spin');
15
+ });
16
+
17
+ it('keeps the inline fallback inline and escapes source text', () => {
18
+ const html = renderToString(createElement(Latex, { inline: true, children: '<img src=x onerror=alert(1)>' }));
19
+ expect(html).toMatch(/^<span /);
20
+ expect(html).not.toContain('<div');
21
+ expect(html).not.toContain('<img');
22
+ expect(html).toContain('&lt;img');
23
+ });
24
+
25
+ it('renders complex math with KaTeX and the requested mode', () => {
26
+ const expression = String.raw`\frac{a}{b}`;
27
+ expect(renderLatexExpression(katex, expression, false)).toContain('katex-display');
28
+ const inline = renderLatexExpression(katex, expression, true);
29
+ expect(inline).toContain('mfrac');
30
+ expect(inline).not.toContain('katex-display');
31
+ });
32
+
33
+ it('does not allow formulas to create trusted links, HTML classes, or image requests', () => {
34
+ expect(renderLatexExpression(katex, String.raw`\href{javascript:alert(1)}{click}`, true)).not.toMatch(/href=["']javascript:/);
35
+ expect(renderLatexExpression(katex, String.raw`\htmlClass{injected}{x}`, false)).not.toMatch(/class=["']injected/);
36
+ expect(renderLatexExpression(katex, String.raw`\includegraphics{https://example.test/tracker.png}`, false)).not.toContain('<img');
37
+ });
38
+ });