duckfn-docs-kit 0.3.0 → 0.4.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/AGENTS.md CHANGED
@@ -1,234 +1,294 @@
1
- # duckfn-docs-kit — usage guide for agents and site authors
2
-
3
- This is the **consumer-facing** companion to [`README.md`](./README.md): the README says what to
4
- import and how to wire it into `docusaurus.config.ts`, this file states the **contracts and the
5
- traps** — the things that are easy to get subtly wrong when you write a docs page or review one.
6
-
7
- Read it before writing runnable SQL blocks or configuring preloads.
8
-
9
- > Developing the kit itself? Its internal conventions (rendering contract, shadow-DOM rules,
10
- > release flow) are in `CONVENTIONS.md` in the repository. That file is **not** published; this one
11
- > is, so it is what a dependency reader sees.
12
-
13
- ## The three things that most often go wrong
14
-
15
- 1. **A runnable block shows only the result of its *last* statement.** Stacking several independent
16
- examples in one block means the reader sees one result and the others silently vanish. One
17
- example per block; several statements only for a preamble (`SET`, `CREATE`, …) that the last
18
- query needs.
19
- 2. **`show: "html"` / `"iframe"` / `"svg"` need to be told which column holds the markup.** With more
20
- than one result column you **must** name it: `"field": "<column>"`. Without it the renderer has
21
- nothing to preview. `"tab_name"` names the column that labels each preview tab, and without it
22
- tabs read `Row 1`, `Row 2`, …
23
- 3. **Only a block whose info string is JSON with `"type":"duckfn"` becomes runnable.** A bare
24
- ```` ```sql ```` block (or any other metastring) stays a plain, un-runnable code block — no Run
25
- button, nothing executed.
26
-
27
- ## Install and wire it up
28
-
29
- See [`README.md`](./README.md) — install, the three `docusaurus.config.ts` plugins, and the one CSS
30
- import. Everything below assumes that is already done.
31
-
32
- ## Runnable SQL blocks
33
-
34
- ### The shape
35
-
36
- A fenced block whose info string is a JSON config. The block itself becomes a CodeMirror editor
37
- with a Run button; nothing executes until the reader clicks it.
38
-
39
- ````md
40
- ```sql {"type":"duckfn"}
41
- SELECT 40 + 2 AS answer;
42
- ```
43
- ````
44
-
45
- Works in `.md` and `.mdx` alike — the metastring is rewritten during the build, before either format
46
- is compiled.
47
-
48
- ### Config reference
49
-
50
- | Field | Meaning |
51
- | --- | --- |
52
- | `type` | `"duckfn"`. Required — this is what makes the block runnable. |
53
- | `show` | `table` (default), `text`, `html`, `iframe`, `svg`. See *Result renderers*. |
54
- | `field` | The column holding the markup, for `html` / `iframe` / `svg`. Required when the result has more than one column. |
55
- | `tab_name` | The column whose value labels each preview tab. Defaults to `Row N`. |
56
- | `option.width` · `option.height` | CSS lengths for the preview box (`"100%"`, `"640px"`). |
57
- | `option.sandbox` | Sandbox tokens for the iframe, replacing the default `allow-scripts`. Widen deliberately. |
58
- | `extensions` | Extra extension names to `LOAD` before this block runs, on top of the site's preloads. |
59
- | `repository` | Where those extensions come from: `community`, `core`, or a repository URL. |
60
- | `allowUnsignedExtensions` | Accept an unverifiable signature. Site-wide via the preload config, or per block; the first block to initialise the engine settles it. |
61
- | `expect` | `ok` (default) or `error`. `error` declares "this block must fail" — see *Testing*. |
62
-
63
- ### Behaviour you have to design around
64
-
65
- - **The result shown is the last statement's.** A block with `SET …; CREATE …; SELECT …` shows the
66
- `SELECT`. A block with three independent `SELECT`s shows the third one only.
67
- - **Default `show`:** a single column with a single row renders as `text`; anything else renders as
68
- a `table`. Set `show` explicitly when the shape matters.
69
- - **One DuckDB-Wasm instance per page, one connection per page.** Blocks on the same page share
70
- state — a table or macro created in one block is visible to the next — and pages are isolated
71
- from each other. Do not write a block that depends on another *page*.
72
- - **The site's preloaded extensions are already loaded.** Call into them directly; do not add
73
- `extensions` for the extension the site documents.
74
- - **Errors are a result, not a broken block.** A failing statement renders its message in the result
75
- area and keeps whatever the reader typed.
76
- - **Every result has a tab strip** (even a plain table), and the fullscreen toggle lives at its right
77
- end. Table results bring sorting, resizable rows/columns, a right-click menu and header drag.
78
-
79
- ### Result renderers
80
-
81
- | `show` | What it renders | Needs |
82
- | --- | --- | --- |
83
- | `table` | The result grid. | — |
84
- | `text` | A bare scalar, as one line. | A single column/row result. |
85
- | `html` / `iframe` | One tab per row; the markup goes into a sandboxed `iframe` (`srcdoc`), with a trailing `Table` tab that is always last. | `field` (unless the result has exactly one column). `tab_name` to label tabs. |
86
- | `svg` | The markup is spliced **into the page** (one tab per row, trailing `Table` tab). | Same as above. |
87
-
88
- Two facts worth knowing before you pick one:
89
-
90
- - `html` and `iframe` are the *same* renderer. The frame is sandboxed with `allow-scripts` and
91
- **without** `allow-same-origin`, so a report's JavaScript runs while the frame keeps an opaque
92
- origin — that is what makes charts work, and it is also why the parent page cannot read
93
- `iframe.contentDocument` (it is `null` by design).
94
- - `svg` shares the page, so anything that could execute or navigate — `script`, `foreignObject`,
95
- `on*` handlers, `javascript:` links — is stripped before insertion.
96
-
97
- ### Examples
98
-
99
- A table result, and a scalar that degrades to text:
100
-
101
- ````md
102
- ```sql {"type":"duckfn","show":"table"}
103
- SELECT * FROM range(10) WHERE range > 5;
104
- ```
105
-
106
- ```sql {"type":"duckfn"}
107
- SELECT 1;
108
- ```
109
- ````
110
-
111
- An HTML (or iframe) report — note `field` and `tab_name`:
112
-
113
- ````md
114
- ```sql {"type":"duckfn","show":"iframe","field":"html","tab_name":"label","option":{"height":"170px"}}
115
- SELECT * FROM (VALUES
116
- ('Bars', '<!doctype html><body><h4>Quarterly revenue</h4><svg viewBox="0 0 240 80">…</svg></body>')
117
- ) AS t(label, html);
118
- ```
119
- ````
120
-
121
- An inline SVG, and an extension that is not preloaded:
122
-
123
- ````md
124
- ```sql {"type":"duckfn","show":"svg","option":{"height":"140px"}}
125
- SELECT '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40"/></svg>';
126
- ```
127
-
128
- ```sql {"type":"duckfn","show":"table","extensions":["inet"]}
129
- SELECT '127.0.0.1'::INET::VARCHAR AS ip;
130
- ```
131
- ````
132
-
133
- ### Mistakes to check for when reviewing a page
134
-
135
- - Several independent examples stacked in one block — only the last result is visible. **Split
136
- them into one block per example.**
137
- - `show: "html"` / `"iframe"` / `"svg"` with a multi-column result and no `field`.
138
- - No `tab_name`, so the preview tabs read `Row 1`, `Row 2`, … instead of something meaningful.
139
- - A bare ```` ```sql ```` block where a runnable one was intended (it renders as a plain listing).
140
- - Declaring `extensions` for the extension the site already preloads.
141
- - A block that depends on a table created on a *different* page.
142
-
143
- ## Preloading extensions (the `dfkExtensions` plugin)
144
-
145
- The site declares an ordered preload list; the kit fetches the release assets at dev/build startup,
146
- injects the list into every page and loads them — in order — while DuckDB initialises. Blocks can
147
- then call into those extensions without declaring anything.
148
-
149
- Three source kinds:
150
-
151
- | Entry | Meaning |
152
- | --- | --- |
153
- | `'json'` | A name: `LOAD json` from the official repository. |
154
- | `{name: 'h3', repository: 'community'}` | `community`, `core` or a repository URL. On wasm `INSTALL` only records *where* a later `LOAD` fetches from. |
155
- | `{url: 'duckdb-extensions/x.duckdb_extension.wasm', release: {repository, asset}}` | The site serves the file itself; with `release`, the build fetches that asset from the repository's latest release (cached by sha256 in `<siteDir>/.cache/duckfn-docs-kit/`). |
156
-
157
- Constraints that bite:
158
-
159
- - **The file name is a contract**: the text before the first dot is the entry symbol DuckDB looks
160
- up, so a release asset named `duckfn-wasm_eh.duckdb_extension.wasm` must be served as
161
- `duckfn.duckdb_extension.wasm` (the `url` decides the file name).
162
- - **Platforms must match**: a `wasm_eh` extension needs a runtime bundle on the `eh` platform. Pin
163
- `@duckdb/duckdb-wasm` to the exact version whose bundled DuckDB is ABI-compatible with the
164
- extension build.
165
- - **Unsigned third-party extensions need `allowUnsignedExtensions: true`** (the WebAssembly
166
- equivalent of `duckdb -unsigned`). Community extensions are signed and load without it.
167
-
168
- ## Testing the blocks (`duckfn-sql-verify`)
169
-
170
- The kit runs every block in a **real browser**, on the same runtime the page uses, so what CI
171
- checks is what a reader gets:
172
-
173
- ```bash
174
- duckfn-sql-verify --site . # or: npx duckfn-sql-verify --site .
175
- ```
176
-
177
- It collects every runnable block (`docs/` plus each `i18n/<locale>/…/current/` by default), runs it
178
- in DuckDB-Wasm **in a headless browser** (driven with `playwright-core`, launching your system
179
- Chrome/Edge via `executablePath` — so no browser download) with the site's extension loaded, and
180
- fails the process when a block does not behave as it declares. It is fully offline: the engine and
181
- the extension are served from local files. Wire it into `package.json` as
182
- `"test": "duckfn-sql-verify --site ."`.
183
-
184
- - **A block that demonstrates a failure must say so**: `{"type":"duckfn","expect":"error"}`. The
185
- check is two-way — a block that declares `error` and starts succeeding is reported too — and a
186
- `-- error:` comment in the SQL is *not* read; only the metadata counts.
187
- - Useful options: `--extension <path|url>`, `--platform eh|mvp`, `--content <dir>` (repeatable),
188
- `--browser <path>` (the Chrome/Edge executable; otherwise a detected one or `DFK_BROWSER`),
189
- `--timeout <ms>`, `--report <file>`, `--quiet`. It needs `playwright-core`, which the kit lists
190
- as a dependency; unlike `playwright` it never downloads a browser.
191
- - The default extension is the single file under `static/duckdb-extensions/`.
192
- - **File-system examples stay plain code blocks.** Under DuckDB-Wasm the raw file system is not
193
- POSIX-faithful: `dfn_file_exists` reads a missing file as "opened" (always `true`), writes/append
194
- byte order is wrong, and there is no existence primitive in DuckDB's C API to correct it. So keep
195
- any `COPY … TO` / `output_dir` / file-write demo as a non-runnable block and note the reason.
196
-
197
- ## TOC collapse control (`dfkTocToggle()`)
198
-
199
- ```ts
200
- plugins: [dfkTocToggle()],
201
- ```
202
-
203
- Adds a collapse button to the desktop table of contents and remembers the choice in
204
- `localStorage` (`duckfn:toc-collapsed`). Custom labels come from the plugin's `labels` option,
205
- keyed by a lower-cased `html-lang` prefix (`{en: {hide, show}, 'zh-hans': {…}}`).
206
-
207
- ## Version placeholder (`remarkVersionPlaceholder`)
208
-
209
- ```ts
210
- remarkPlugins: [[remarkVersionPlaceholder, {version: DUCKFN_VERSION}]],
211
- ```
212
-
213
- Replaces `{{DUCKFN_VERSION}}` inside `text`, `inlineCode` and `code` nodes, so a release updates one
214
- file instead of every page. It only touches that exact placeholder — anything else is left alone.
215
-
216
- ## Home-page components
217
-
218
- `<dfk-hero>`, `<dfk-features>`, `<dfk-next-steps>` (and `<dfk-sql>`) are registered by the barrel
219
- import (`duckfn-docs-kit`). The home components are **setter-driven and do not reflect attributes**:
220
- give them named setters (`setTitle`, `setTagline`, `setFeatures`, …), not markup content. They
221
- render into shadow roots, inherit `--duckfn-*` / `--ifm-*` CSS variables from the page, and are
222
- safe to call before the element is connected.
223
-
224
- ## Troubleshooting
225
-
226
- | Symptom | Likely cause |
227
- | --- | --- |
228
- | A code block has no Run button | Its info string is not JSON with `"type":"duckfn"`. |
229
- | The block runs but nothing appears in the preview | `show: "html"` / `"iframe"` / `"svg"` without `field` on a multi-column result. |
230
- | The preview tabs are labelled `Row 1`, `Row 2`, … | No `tab_name`. |
231
- | Clicking Run shows an error like `Table with name … does not exist` | The block depends on something created on another page, or on a statement that is no longer the last one in its block. |
232
- | `LOAD` fails with a signature error | `allowUnsignedExtensions: true` missing for a third-party asset. |
233
- | A preloaded extension fails to load | The served file name's pre-dot part does not match the extension's entry symbol, or the platform does not match the runtime bundle. |
234
- | Only the last of several examples shows a result | That is the contract — split the block. |
1
+ # duckfn-docs-kit — usage guide for agents and site authors
2
+
3
+ This is the **consumer-facing** companion to [`README.md`](./README.md): the README says what to
4
+ import and how to wire it into `docusaurus.config.ts`, this file states the **contracts and the
5
+ traps** — the things that are easy to get subtly wrong when you write a docs page or review one.
6
+
7
+ Read it before writing runnable SQL blocks or configuring preloads.
8
+
9
+ > Developing the kit itself? Its internal conventions (rendering contract, shadow-DOM rules,
10
+ > release flow) are in `CONVENTIONS.md` in the repository. That file is **not** published; this one
11
+ > is, so it is what a dependency reader sees.
12
+
13
+ ## The three things that most often go wrong
14
+
15
+ 1. **A runnable block shows only the result of its *last* statement.** Stacking several independent
16
+ examples in one block means the reader sees one result and the others silently vanish. One
17
+ example per block; several statements only for a preamble (`SET`, `CREATE`, …) that the last
18
+ query needs.
19
+ 2. **`show: "html"` / `"iframe"` / `"svg"` / `"mermaid"` need to be told which column holds the
20
+ markup.** With more than one result column you **must** name it: `"field": "<column>"`. Without
21
+ it the renderer has nothing to preview. `"tab_name"` names the column that labels each preview
22
+ tab, and without it tabs read `Row 1`, `Row 2`, …
23
+ 3. **Only a block whose info string is JSON with `"type":"duckfn"` becomes runnable.** A bare
24
+ ```` ```sql ```` block (or any other metastring) stays a plain, un-runnable code block — no Run
25
+ button, nothing executed.
26
+
27
+ ## Install and wire it up
28
+
29
+ See [`README.md`](./README.md) — install, the three `docusaurus.config.ts` plugins, and the one CSS
30
+ import. Everything below assumes that is already done.
31
+
32
+ ## Runnable SQL blocks
33
+
34
+ ### The shape
35
+
36
+ A fenced block whose info string is a JSON config. The block itself becomes a CodeMirror editor
37
+ with a Run button; nothing executes until the reader clicks it.
38
+
39
+ ````md
40
+ ```sql {"type":"duckfn"}
41
+ SELECT 40 + 2 AS answer;
42
+ ```
43
+ ````
44
+
45
+ Works in `.md` and `.mdx` alike — the metastring is rewritten during the build, before either format
46
+ is compiled.
47
+
48
+ ### Config reference
49
+
50
+ | Field | Meaning |
51
+ | --- | --- |
52
+ | `type` | `"duckfn"`. Required — this is what makes the block runnable. |
53
+ | `show` | `table` (default), `text`, `html`, `iframe`, `svg`, `mermaid`. See *Result renderers*. |
54
+ | `field` | The column holding the markup, for `html` / `iframe` / `svg` / `mermaid`. Required when the result has more than one column. |
55
+ | `tab_name` | The column whose value labels each preview tab. Defaults to `Row N`. |
56
+ | `option.width` · `option.height` | CSS lengths for the preview box (`"100%"`, `"640px"`). |
57
+ | `option.sandbox` | Sandbox tokens for the iframe, replacing the default `allow-scripts`. Widen deliberately. |
58
+ | `extensions` | Extra extension names to `LOAD` before this block runs, on top of the site's preloads. |
59
+ | `repository` | Where those extensions come from: `community`, `core`, or a repository URL. |
60
+ | `allowUnsignedExtensions` | Accept an unverifiable signature. Site-wide via the preload config, or per block; the first block to initialise the engine settles it. |
61
+ | `expect` | `ok` (default) or `error`. `error` declares "this block must fail" — see *Testing*. |
62
+
63
+ ### Behaviour you have to design around
64
+
65
+ - **The result shown is the last statement's.** A block with `SET …; CREATE …; SELECT …` shows the
66
+ `SELECT`. A block with three independent `SELECT`s shows the third one only.
67
+ - **Default `show`:** a single column with a single row renders as `text`; anything else renders as
68
+ a `table`. Set `show` explicitly when the shape matters.
69
+ - **One DuckDB-Wasm instance per page, one connection per page.** Blocks on the same page share
70
+ state — a table or macro created in one block is visible to the next — and pages are isolated
71
+ from each other. Do not write a block that depends on another *page*.
72
+ - **The site's preloaded extensions are already loaded.** Call into them directly; do not add
73
+ `extensions` for the extension the site documents.
74
+ - **Errors are a result, not a broken block.** A failing statement renders its message in the result
75
+ area and keeps whatever the reader typed.
76
+ - **Every result has a tab strip** (even a plain table), and the fullscreen toggle lives at its right
77
+ end. Table results bring sorting, resizable rows/columns, a right-click menu and header drag.
78
+
79
+ ### Result renderers
80
+
81
+ | `show` | What it renders | Needs |
82
+ | --- | --- | --- |
83
+ | `table` | The result grid. | — |
84
+ | `text` | A bare scalar, as one line. | A single column/row result. |
85
+ | `html` / `iframe` | One tab per row; the markup goes into a sandboxed `iframe` (`srcdoc`), with a trailing `Table` tab that is always last. | `field` (unless the result has exactly one column). `tab_name` to label tabs. |
86
+ | `svg` | The markup is spliced **into the page** (one tab per row, trailing `Table` tab). | Same as above. |
87
+ | `mermaid` | The cell is handed to `<dfk-mermaid>`, so the reader gets a real diagram with zoom, fullscreen, source editing and SVG download. One tab per row, trailing `Table` tab. | Same as above. |
88
+
89
+ Two facts worth knowing before you pick one:
90
+
91
+ - `html` and `iframe` are the *same* renderer. The frame is sandboxed with `allow-scripts` and
92
+ **without** `allow-same-origin`, so a report's JavaScript runs while the frame keeps an opaque
93
+ origin — that is what makes charts work, and it is also why the parent page cannot read
94
+ `iframe.contentDocument` (it is `null` by design).
95
+ - `svg` shares the page, so anything that could execute or navigate — `script`, `foreignObject`,
96
+ `on*` handlers, `javascript:` links — is stripped before insertion.
97
+
98
+ ### Examples
99
+
100
+ A table result, and a scalar that degrades to text:
101
+
102
+ ````md
103
+ ```sql {"type":"duckfn","show":"table"}
104
+ SELECT * FROM range(10) WHERE range > 5;
105
+ ```
106
+
107
+ ```sql {"type":"duckfn"}
108
+ SELECT 1;
109
+ ```
110
+ ````
111
+
112
+ An HTML (or iframe) report — note `field` and `tab_name`:
113
+
114
+ ````md
115
+ ```sql {"type":"duckfn","show":"iframe","field":"html","tab_name":"label","option":{"height":"170px"}}
116
+ SELECT * FROM (VALUES
117
+ ('Bars', '<!doctype html><body><h4>Quarterly revenue</h4><svg viewBox="0 0 240 80">…</svg></body>')
118
+ ) AS t(label, html);
119
+ ```
120
+ ````
121
+
122
+ An inline SVG, and an extension that is not preloaded:
123
+
124
+ ````md
125
+ ```sql {"type":"duckfn","show":"svg","option":{"height":"140px"}}
126
+ SELECT '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40"/></svg>';
127
+ ```
128
+
129
+ ```sql {"type":"duckfn","show":"table","extensions":["inet"]}
130
+ SELECT '127.0.0.1'::INET::VARCHAR AS ip;
131
+ ```
132
+ ````
133
+
134
+ A diagram built by a query (the same element a ```` ```mermaid ```` fence produces):
135
+
136
+ ````md
137
+ ```sql {"type":"duckfn","show":"mermaid"}
138
+ SELECT 'flowchart LR' || chr(10)
139
+ || ' A["a SELECT"] --> B["one result cell"]' AS diagram;
140
+ ```
141
+ ````
142
+
143
+ ### Mistakes to check for when reviewing a page
144
+
145
+ - Several independent examples stacked in one block — only the last result is visible. **Split
146
+ them into one block per example.**
147
+ - `show: "html"` / `"iframe"` / `"svg"` / `"mermaid"` with a multi-column result and no `field`.
148
+ - No `tab_name`, so the preview tabs read `Row 1`, `Row 2`, … instead of something meaningful.
149
+ - A bare ```` ```sql ```` block where a runnable one was intended (it renders as a plain listing).
150
+ - Declaring `extensions` for the extension the site already preloads.
151
+ - A block that depends on a table created on a *different* page.
152
+
153
+ ## Mermaid diagrams (`remarkMermaid`)
154
+
155
+ ````ts
156
+ // docusaurus.config.ts, in the docs preset's `remarkPlugins`
157
+ remarkPlugins: [remarkMermaid],
158
+ ````
159
+
160
+ A ```` ```mermaid ```` fence becomes a `<dfk-mermaid>` element that renders the diagram **in the
161
+ browser** (mermaid is a lazy `import()`, so a page with no diagram never downloads it). Do **not**
162
+ also install `@docusaurus/theme-mermaid` or list it in `themes`, and do not set `markdown.mermaid`:
163
+ the kit's element replaces both, and two renderers on one page would fight.
164
+
165
+ What the reader gets, in the element's top-right corner on hover: **reset zoom** (wheel zooms,
166
+ dragging pans once zoomed), **fullscreen**, **edit the source** in a CodeMirror dialog, and
167
+ **download SVG**.
168
+
169
+ - **The file is named after the section it sits in.** `1. Expansion.svg`, not
170
+ `mermaid-diagram.svg`, in a cascade that walks from the most specific source to the most general:
171
+ the diagram's own title (mermaid frontmatter, `---\ntitle: …\n---`) → the nearest heading above
172
+ it → the document title → `mermaid-diagram.svg`. So a diagram is best named by the heading it
173
+ lives under; give it a frontmatter `title:` when the heading is not the name you want. The name
174
+ goes through `filenamify`, so nothing a filesystem chokes on (`:`, `?`, `*`, `|`, …) reaches the
175
+ file.
176
+ - **The diagram is inert until it is zoomed.** At fit the pointer is the browser's: the cursor is
177
+ the normal one (an I-beam over a label), and dragging selects text — the labels are still text,
178
+ and copying one should work. Zooming in is what turns the pointer into a `grab` hand and gives a
179
+ drag to panning; **Reset zoom** hands it back. There is no mode switch to remember.
180
+
181
+ - **The palette is a site choice, not a page one.** The kit's default is the `neo` look with
182
+ `redux-color` / `redux-dark-color`; a site overrides it in its own config, which also keeps the
183
+ kit fork-free for downstream docs sites:
184
+
185
+ ````ts
186
+ remarkMermaid({config: {theme: {light: 'neutral', dark: 'dark'}, options: {look: 'classic'}}})
187
+ ````
188
+
189
+ `theme` is per colour mode (the element re-renders on a theme switch); `look` has no light/dark
190
+ counterpart and lives in `options`. Mermaid silently ignores a value it does not recognise, so
191
+ check a diagram in a browser — a build proves nothing here.
192
+ - **Diagrams render per page, in the page's own colour mode** — read from `<html data-theme>` (the
193
+ attribute written before first paint), not from a framework hook. That is what keeps a dark-mode
194
+ first load from painting a light diagram and then a dark one.
195
+ - **Labels**: keep them quoted (`A["text"]`), use `<br/>` for a line break, and avoid a bare `#` or
196
+ an unescaped `&`. A syntax error shows up in the page, not in the build.
197
+ - A ```` ```mermaid ```` fence inside a longer fence (documenting it, as here) is *not* turned into
198
+ a diagram — it is text inside the outer code block.
199
+
200
+ ## Preloading extensions (the `dfkExtensions` plugin)
201
+
202
+ The site declares an ordered preload list; the kit fetches the release assets at dev/build startup,
203
+ injects the list into every page and loads them — in order — while DuckDB initialises. Blocks can
204
+ then call into those extensions without declaring anything.
205
+
206
+ Three source kinds:
207
+
208
+ | Entry | Meaning |
209
+ | --- | --- |
210
+ | `'json'` | A name: `LOAD json` from the official repository. |
211
+ | `{name: 'h3', repository: 'community'}` | `community`, `core` or a repository URL. On wasm `INSTALL` only records *where* a later `LOAD` fetches from. |
212
+ | `{url: 'duckdb-extensions/x.duckdb_extension.wasm', release: {repository, asset}}` | The site serves the file itself; with `release`, the build fetches that asset from the repository's latest release (cached by sha256 in `<siteDir>/.cache/duckfn-docs-kit/`). |
213
+
214
+ Constraints that bite:
215
+
216
+ - **The file name is a contract**: the text before the first dot is the entry symbol DuckDB looks
217
+ up, so a release asset named `duckfn-wasm_eh.duckdb_extension.wasm` must be served as
218
+ `duckfn.duckdb_extension.wasm` (the `url` decides the file name).
219
+ - **Platforms must match**: a `wasm_eh` extension needs a runtime bundle on the `eh` platform. Pin
220
+ `@duckdb/duckdb-wasm` to the exact version whose bundled DuckDB is ABI-compatible with the
221
+ extension build.
222
+ - **Unsigned third-party extensions need `allowUnsignedExtensions: true`** (the WebAssembly
223
+ equivalent of `duckdb -unsigned`). Community extensions are signed and load without it.
224
+
225
+ ## Testing the blocks (`duckfn-sql-verify`)
226
+
227
+ The kit runs every block in a **real browser**, on the same runtime the page uses, so what CI
228
+ checks is what a reader gets:
229
+
230
+ ```bash
231
+ duckfn-sql-verify --site . # or: npx duckfn-sql-verify --site .
232
+ ```
233
+
234
+ It collects every runnable block (`docs/` plus each `i18n/<locale>/…/current/` by default), runs it
235
+ in DuckDB-Wasm **in a headless browser** (driven with `playwright-core`, launching your system
236
+ Chrome/Edge via `executablePath` — so no browser download) with the site's extension loaded, and
237
+ fails the process when a block does not behave as it declares. It is fully offline: the engine and
238
+ the extension are served from local files. Wire it into `package.json` as
239
+ `"test": "duckfn-sql-verify --site ."`.
240
+
241
+ - **A block that demonstrates a failure must say so**: `{"type":"duckfn","expect":"error"}`. The
242
+ check is two-way — a block that declares `error` and starts succeeding is reported too — and a
243
+ `-- error:` comment in the SQL is *not* read; only the metadata counts.
244
+ - Useful options: `--extension <path|url>`, `--platform eh|mvp`, `--content <dir>` (repeatable),
245
+ `--browser <path>` (the Chrome/Edge executable; otherwise a detected one or `DFK_BROWSER`),
246
+ `--timeout <ms>`, `--report <file>`, `--quiet`. It needs `playwright-core`, which the kit lists
247
+ as a dependency; unlike `playwright` it never downloads a browser.
248
+ - The default extension is the single file under `static/duckdb-extensions/`.
249
+ - **File-system examples stay plain code blocks.** Under DuckDB-Wasm the raw file system is not
250
+ POSIX-faithful: `dfn_file_exists` reads a missing file as "opened" (always `true`), writes/append
251
+ byte order is wrong, and there is no existence primitive in DuckDB's C API to correct it. So keep
252
+ any `COPY … TO` / `output_dir` / file-write demo as a non-runnable block and note the reason.
253
+
254
+ ## TOC collapse control (`dfkTocToggle()`)
255
+
256
+ ```ts
257
+ plugins: [dfkTocToggle()],
258
+ ```
259
+
260
+ Adds a collapse button to the desktop table of contents and remembers the choice in
261
+ `localStorage` (`duckfn:toc-collapsed`). Custom labels come from the plugin's `labels` option,
262
+ keyed by a lower-cased `html-lang` prefix (`{en: {hide, show}, 'zh-hans': {…}}`).
263
+
264
+ ## Version placeholder (`remarkVersionPlaceholder`)
265
+
266
+ ```ts
267
+ remarkPlugins: [[remarkVersionPlaceholder, {version: DUCKFN_VERSION}]],
268
+ ```
269
+
270
+ Replaces `{{DUCKFN_VERSION}}` inside `text`, `inlineCode` and `code` nodes, so a release updates one
271
+ file instead of every page. It only touches that exact placeholder — anything else is left alone.
272
+
273
+ ## Home-page components
274
+
275
+ `<dfk-hero>`, `<dfk-features>`, `<dfk-next-steps>` (along with `<dfk-sql>` and `<dfk-mermaid>`,
276
+ which the plugins generate) are registered by the barrel import (`duckfn-docs-kit`). The home
277
+ components are **setter-driven and do not reflect attributes**:
278
+ give them named setters (`setTitle`, `setTagline`, `setFeatures`, …), not markup content. They
279
+ render into shadow roots, inherit `--duckfn-*` / `--ifm-*` CSS variables from the page, and are
280
+ safe to call before the element is connected.
281
+
282
+ ## Troubleshooting
283
+
284
+ | Symptom | Likely cause |
285
+ | --- | --- |
286
+ | A code block has no Run button | Its info string is not JSON with `"type":"duckfn"`. |
287
+ | The block runs but nothing appears in the preview | `show: "html"` / `"iframe"` / `"svg"` without `field` on a multi-column result. |
288
+ | The preview tabs are labelled `Row 1`, `Row 2`, … | No `tab_name`. |
289
+ | Clicking Run shows an error like `Table with name … does not exist` | The block depends on something created on another page, or on a statement that is no longer the last one in its block. |
290
+ | `LOAD` fails with a signature error | `allowUnsignedExtensions: true` missing for a third-party asset. |
291
+ | A preloaded extension fails to load | The served file name's pre-dot part does not match the extension's entry symbol, or the platform does not match the runtime bundle. |
292
+ | Only the last of several examples shows a result | That is the contract — split the block. |
293
+ | A ```` ```mermaid ```` fence renders as a plain code block | `remarkMermaid` is not in the docs preset's `remarkPlugins`; and if `@docusaurus/theme-mermaid` is still installed, remove it and `markdown.mermaid`. |
294
+ | A diagram is blank, or shows a message instead | The mermaid source does not parse — the message carries mermaid's own error text. |
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # duckfn-docs-kit
2
2
 
3
3
  Shared building blocks for [duckfn](https://github.com/shijianjs/duckfn)-family
4
- DuckDB extension documentation sites: runnable SQL blocks, extension preloading,
5
- a TOC collapse control, the home-page web components and the version-placeholder
6
- remark plugin. Each one is a self-contained entry point that a Docusaurus 3 site
7
- wires into its own config — the alternative is copying the same glue into every
8
- extension's docs site.
4
+ DuckDB extension documentation sites: runnable SQL blocks, mermaid diagrams,
5
+ extension preloading, a TOC collapse control, the home-page web components and
6
+ the version-placeholder remark plugin. Each one is a self-contained entry point
7
+ that a Docusaurus 3 site wires into its own config — the alternative is copying
8
+ the same glue into every extension's docs site.
9
9
 
10
10
  Plain TypeScript over the native DOM: no React and no UI framework of its own.
11
11
  The components are retained-mode classes — they build their DOM once, expose
@@ -22,9 +22,10 @@ npm install duckfn-docs-kit
22
22
 
23
23
  | Import | Runs in | What it provides |
24
24
  | --- | --- | --- |
25
- | `duckfn-docs-kit` | browser | Home-page custom elements (`<dfk-hero>`, `<dfk-features>`, `<dfk-next-steps>`, `<dfk-sql>`), `registerDfkElements()` and the value types their setters accept |
25
+ | `duckfn-docs-kit` | browser | Custom elements (`<dfk-hero>`, `<dfk-features>`, `<dfk-next-steps>`, `<dfk-sql>`, `<dfk-mermaid>`), `registerDfkElements()` and the value types their setters accept |
26
26
  | `duckfn-docs-kit/remark` | Node (build) | `remarkVersionPlaceholder`: replaces `{{DUCKFN_VERSION}}` inside `text` / `inlineCode` / `code` nodes |
27
27
  | `duckfn-docs-kit/sql/remark` | Node (build) | `remarkRunnableSql`: turns fenced `sql {"type":"duckfn",…}` blocks into `<dfk-sql>` elements |
28
+ | `duckfn-docs-kit/mermaid/remark` | Node (build) | `remarkMermaid`: turns ```` ```mermaid ```` fences into `<dfk-mermaid>` diagrams |
28
29
  | `duckfn-docs-kit/sql/extensions` | Node (build) | `dfkExtensions()` Docusaurus plugin: preloads a site's DuckDB extensions before the first block runs |
29
30
  | `duckfn-docs-kit/toc-toggle/plugin` | Node (build) | `dfkTocToggle()` Docusaurus plugin: adds the TOC collapse control |
30
31
  | `duckfn-docs-kit/toc-toggle/TocToggle` | browser | The TOC collapse class, for a site that drives it itself |
@@ -43,6 +44,7 @@ import {dfkExtensions} from 'duckfn-docs-kit/sql/extensions';
43
44
  import {dfkTocToggle} from 'duckfn-docs-kit/toc-toggle/plugin';
44
45
  import {remarkVersionPlaceholder} from 'duckfn-docs-kit/remark';
45
46
  import {remarkRunnableSql} from 'duckfn-docs-kit/sql/remark';
47
+ import {remarkMermaid} from 'duckfn-docs-kit/mermaid/remark';
46
48
  import {DUCKFN_VERSION} from './duckfn-version';
47
49
 
48
50
  export default {
@@ -54,6 +56,8 @@ export default {
54
56
  remarkPlugins: [
55
57
  [remarkVersionPlaceholder, {version: DUCKFN_VERSION}],
56
58
  remarkRunnableSql,
59
+ // ```mermaid fences become diagrams; no @docusaurus/theme-mermaid.
60
+ remarkMermaid,
57
61
  ],
58
62
  },
59
63
  },
@@ -0,0 +1,22 @@
1
+ /**
2
+ * A compact icon-only button with a hover/focus tooltip, built once and then
3
+ * mutated. Shared by `<dfk-sql>`'s code-block cluster and `<dfk-mermaid>`'s
4
+ * diagram cluster: both want the same "floating icon row over content" idiom,
5
+ * and neither wants a second implementation of it.
6
+ *
7
+ * The tooltip is also the accessible name — an icon-only control has no text to
8
+ * fall back on. The two class names are written by this module and styled in
9
+ * *each* consumer's shadow-root CSS; that duplication is unavoidable (a shadow
10
+ * boundary stops one sheet from reaching the other tree), so the rules carry the
11
+ * same names and are kept in sync by hand.
12
+ */
13
+ export declare class IconButton {
14
+ #private;
15
+ readonly root: HTMLButtonElement;
16
+ constructor(icon: string, onClick: () => void);
17
+ setIcon(icon: string): void;
18
+ setLabel(text: string): void;
19
+ /** Marks a toggle as currently on (e.g. the SQL block's wrap toggle). */
20
+ setOn(on: boolean): void;
21
+ setDisabled(disabled: boolean): void;
22
+ }