@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.
- package/README.md +57 -173
- package/bin/dist/dependency-overlay.d.ts +7 -0
- package/bin/dist/dependency-overlay.d.ts.map +1 -0
- package/bin/dist/dependency-overlay.js +137 -0
- package/bin/dist/dependency-overlay.js.map +1 -0
- package/bin/dist/dory.js +4 -2
- package/bin/dist/dory.js.map +1 -1
- package/bin/dist/preview-server.d.ts.map +1 -1
- package/bin/dist/preview-server.js +4 -17
- package/bin/dist/preview-server.js.map +1 -1
- package/bin/dist/preview-static.d.ts +7 -0
- package/bin/dist/preview-static.d.ts.map +1 -0
- package/bin/dist/preview-static.js +70 -0
- package/bin/dist/preview-static.js.map +1 -0
- package/dist/index.html +19 -19
- package/dist/runtime/dory-runtime.css +1 -1
- package/dist/runtime/dory-runtime.js +305 -305
- package/docs/components/layout.mdx +3 -0
- package/docs/components/lists.mdx +2 -2
- package/docs/components/overview.mdx +1 -1
- package/docs/components/typography.mdx +4 -4
- package/docs/configuration/navigation.mdx +3 -3
- package/docs/contributing.mdx +47 -0
- package/docs/dory.json +13 -6
- package/docs/getting-started/automation.mdx +2 -2
- package/docs/getting-started/custom-components.mdx +60 -0
- package/docs/social-card.png +0 -0
- package/docs/social-card.svg +1 -1
- package/package.json +1 -1
- package/scripts/verify-component-examples.mjs +126 -0
- package/scripts/verify-runtime-parity.mjs +5 -4
- package/src/app.css +3 -0
- package/src/main.tsx +1 -1
- package/src/mdx/latex.test.ts +38 -0
- package/src/mdx/latex.tsx +42 -65
- package/src/mdx/preprocess.ts +2 -2
- package/src/mdx/processor.test.ts +135 -37
- package/src/mdx/processor.ts +56 -2
- package/src/mdx/specimen.tsx +32 -50
- package/src/mdx/tabs.tsx +15 -12
- package/src/mdx/text.tsx +2 -2
- package/src/mdx/type-scale.tsx +3 -3
- package/src/plugins/sanitize.ts +5 -4
- package/vite.config.ssr.ts +1 -1
- package/vite.config.ts +1 -1
- 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 `<` and `>`.</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://
|
|
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://
|
|
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
|
|
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://
|
|
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://
|
|
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://
|
|
70
|
+
[Visit the Dory site](https://clidey.github.io/dory/)
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
[Visit the Dory site](https://
|
|
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
|
|
209
|
-
lead: '
|
|
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
|
-
|
|
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://
|
|
3
|
+
"url": "https://clidey.github.io",
|
|
4
4
|
"image": "/social-card.png",
|
|
5
|
-
"version": "1.0
|
|
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
|
|
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/
|
|
43
|
-
"getting-started/
|
|
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
|
|
34
|
-
claude mcp add --transport http
|
|
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.
|
package/docs/social-card.png
CHANGED
|
Binary file
|
package/docs/social-card.svg
CHANGED
|
@@ -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">
|
|
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
|
@@ -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/
|
|
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: '
|
|
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('<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
|
+
});
|