comark-ui 0.8.2 → 0.9.1

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/dist/client.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { n as typesetStylesheet, t as typesetCss } from "./typeset-css-BYvWbqWz.mjs";
1
+ import { n as typesetStylesheet, t as typesetCss } from "./typeset-css-BOtdXbMg.mjs";
2
2
  import { i as startMermaid, n as mermaidBar, r as mermaidCss, t as addMermaidStyles } from "./client-kUEiRbtp.mjs";
3
3
  export { addMermaidStyles, mermaidBar, mermaidCss, startMermaid, typesetCss, typesetStylesheet };
package/dist/html.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as commentsCss, n as Preset, o as criticCss, r as PresetOptions } from "./index-BTyvRIPX.mjs";
1
+ import { a as commentsCss, n as Preset, o as criticCss, r as PresetOptions } from "./index-Cq3zytGw.mjs";
2
2
  import { NodeHandler } from "comark";
3
3
  //#region src/html.d.ts
4
4
  /**
package/dist/html.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { n as criticCss, r as preset, t as commentsCss } from "./src-Baygj6ZQ.mjs";
2
- import { t as typesetCss } from "./typeset-css-BYvWbqWz.mjs";
1
+ import { n as criticCss, r as preset, t as commentsCss } from "./src-MvpWiv9v.mjs";
2
+ import { t as typesetCss } from "./typeset-css-BOtdXbMg.mjs";
3
3
  import { a as start, n as mermaidBar, r as mermaidCss } from "./client-kUEiRbtp.mjs";
4
4
  import { t as rawText } from "./text-D8j9clg3.mjs";
5
5
  import { escapeHtml } from "comark/utils";
@@ -10,6 +10,7 @@ import { XrefOptions } from "comark-xref";
10
10
  import { ComarkPlugin } from "comark";
11
11
  import { CommentsOptions, css as commentsCss } from "comark-comments";
12
12
  import { PropertiesOptions } from "comark-properties";
13
+ import { PeleConfig } from "comark-pele";
13
14
  import { BadgeOptions } from "comark-badge";
14
15
  import { AnnotateOptions } from "comark-annotate";
15
16
  import { CastOptions } from "comark-cast";
@@ -23,7 +24,6 @@ import { AnchorsOptions, ExternalOptions } from "comark-links";
23
24
  import { MathOptions } from "comark-mathml";
24
25
  import { MediaOptions } from "comark-media";
25
26
  import { OutlineOptions } from "comark-outline";
26
- import { PeleConfig } from "comark-pele";
27
27
  import { QuizOptions } from "comark-quiz";
28
28
  import { TableOptions } from "comark-table";
29
29
  import { TabsOptions } from "comark-tabs";
@@ -37,6 +37,42 @@ type MermaidOptions = PeleConfig & {
37
37
  maxSource?: number;
38
38
  };
39
39
  //#endregion
40
+ //#region src/shiki.d.ts
41
+ type Token = {
42
+ content: string;
43
+ color?: string;
44
+ htmlStyle?: Record<string, string>;
45
+ };
46
+ /** What the preset needs of a Shiki highlighter: a `HighlighterCore` from `shiki/core` fits it. */
47
+ type ShikiHighlighter = {
48
+ codeToTokens(code: string, options: {
49
+ lang: string;
50
+ themes: {
51
+ light: string;
52
+ dark?: string;
53
+ };
54
+ }): {
55
+ tokens: Token[][];
56
+ };
57
+ };
58
+ type ShikiOptions = {
59
+ /**
60
+ * Your Shiki, with the fence's language loaded, or `undefined` to leave the block plain. It's called for each fenced
61
+ * block with a language that fits the remaining budget, so load each grammar once. Blocks past the limits skip it
62
+ * and report a limit issue without checking whether the language is supported.
63
+ */
64
+ highlighter: (language: string) => ShikiHighlighter | undefined | Promise<ShikiHighlighter | undefined>;
65
+ /** Theme names your highlighter has loaded. The dark one goes in `--shiki-dark`, which the typeset styles follow. */
66
+ themes: {
67
+ light: string;
68
+ dark?: string;
69
+ };
70
+ /** How many code blocks a document highlights. The ones after show without colors. */
71
+ maxBlocks?: number;
72
+ /** How many characters of code a document highlights, over all its blocks. A block past it shows without colors. */
73
+ maxSource?: number;
74
+ };
75
+ //#endregion
40
76
  //#region src/typeset.d.ts
41
77
  /** The node that adds the styles. The plugin adds one at the end of every document. */
42
78
  declare const TYPESET_TAG = "typeset-styles";
@@ -119,6 +155,12 @@ type PresetOptions = {
119
155
  /** Reads no JUnit reports unless you pass `{ root }` or `{ read }`; inline reports render either way. */
120
156
  tests?: TestsOptions | boolean;
121
157
  zoom?: ZoomOptions | boolean;
158
+ /**
159
+ * Highlights fenced code with your own Shiki, before comark-code. Off unless you pass one, so the preset bundles no
160
+ * grammars. A host that renders documents it didn't write can pass `{ maxBlocks, maxSource }`: the blocks past either
161
+ * limit show without colors, each with an issue.
162
+ */
163
+ shiki?: ShikiOptions | false;
122
164
  code?: CodeOptions | boolean;
123
165
  codeNotes?: boolean;
124
166
  /**
@@ -202,4 +244,4 @@ type Preset<C> = {
202
244
  /** Builds a renderer's preset. Use the one from `comark-ui/html`, `/vue`, `/react`, or `/svelte`. */
203
245
  declare function preset<C>(renderer: Renderer<C>, options?: PresetOptions): Preset<C>;
204
246
  //#endregion
205
- export { commentsCss as a, TYPESET_TAG as c, Renderer as i, typeset as l, Preset as n, criticCss as o, PresetOptions as r, preset as s, AnyPlugin as t, MermaidOptions as u };
247
+ export { commentsCss as a, TYPESET_TAG as c, ShikiOptions as d, MermaidOptions as f, Renderer as i, typeset as l, Preset as n, criticCss as o, PresetOptions as r, preset as s, AnyPlugin as t, ShikiHighlighter as u };
package/dist/index.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as typesetStylesheet, t as typesetCss } from "./typeset-css-DtOLAa1Z.mjs";
2
- import { a as commentsCss, c as TYPESET_TAG, i as Renderer, l as typeset, n as Preset, o as criticCss, r as PresetOptions, s as preset, t as AnyPlugin, u as MermaidOptions } from "./index-BTyvRIPX.mjs";
3
- export { AnyPlugin, type MermaidOptions, Preset, PresetOptions, Renderer, TYPESET_TAG, commentsCss, criticCss, preset, typeset, typesetCss, typesetStylesheet };
2
+ import { a as commentsCss, c as TYPESET_TAG, d as ShikiOptions, f as MermaidOptions, i as Renderer, l as typeset, n as Preset, o as criticCss, r as PresetOptions, s as preset, t as AnyPlugin, u as ShikiHighlighter } from "./index-Cq3zytGw.mjs";
3
+ export { AnyPlugin, type MermaidOptions, Preset, PresetOptions, Renderer, type ShikiHighlighter, type ShikiOptions, TYPESET_TAG, commentsCss, criticCss, preset, typeset, typesetCss, typesetStylesheet };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { a as typeset, i as TYPESET_TAG, n as criticCss, r as preset, t as commentsCss } from "./src-Baygj6ZQ.mjs";
2
- import { n as typesetStylesheet, t as typesetCss } from "./typeset-css-BYvWbqWz.mjs";
1
+ import { a as typeset, i as TYPESET_TAG, n as criticCss, r as preset, t as commentsCss } from "./src-MvpWiv9v.mjs";
2
+ import { n as typesetStylesheet, t as typesetCss } from "./typeset-css-BOtdXbMg.mjs";
3
3
  export { TYPESET_TAG, commentsCss, criticCss, preset, typeset, typesetCss, typesetStylesheet };
package/dist/react.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as commentsCss, n as Preset, o as criticCss, r as PresetOptions } from "./index-BTyvRIPX.mjs";
1
+ import { a as commentsCss, n as Preset, o as criticCss, r as PresetOptions } from "./index-Cq3zytGw.mjs";
2
2
  import { ComponentType } from "react";
3
3
  //#region src/react.d.ts
4
4
  type AnyComponent = ComponentType<any>;
package/dist/react.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { n as criticCss, r as preset, t as commentsCss } from "./src-Baygj6ZQ.mjs";
2
- import { t as typesetCss } from "./typeset-css-BYvWbqWz.mjs";
1
+ import { n as criticCss, r as preset, t as commentsCss } from "./src-MvpWiv9v.mjs";
2
+ import { t as typesetCss } from "./typeset-css-BOtdXbMg.mjs";
3
3
  import { i as startMermaid, n as mermaidBar, t as addMermaidStyles } from "./client-kUEiRbtp.mjs";
4
4
  import { createElement, useEffect } from "react";
5
5
  import * as badge from "comark-badge/react";
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: before-after-evidence
3
- description: Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page. Use when a pull request, bug fix, or design change alters what people see, or when someone asks for before/after screenshots or visual evidence.
3
+ description: Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page, and a change in motion as a short recording. Use when a pull request, bug fix, or design change alters what people see or how it moves, or when someone asks for before/after screenshots, a recording, or visual evidence.
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -10,6 +10,13 @@ A before/after is evidence when a reviewer can check every claim against the two
10
10
 
11
11
  This skill writes the document. Your host decides where it's published and who can open it.
12
12
 
13
+ ## When the change is motion
14
+
15
+ An animation, a transition, a loading sequence, or feedback while dragging happens between frames, so record it. A still taken partway through a motion looks like a broken layout, and two of them can't show that anything moves. Follow [references/recording.md](references/recording.md) in place of steps 1 and 2, then write and check the document with steps 3 and 4, with the clip where the `::compare` goes.
16
+
17
+ - When the old behavior jumped straight to its end state, the after clip alone is the evidence, and the document's first sentence says what happened before.
18
+ - When the change retimes motion that already existed, such as its easing, its duration, or a stutter, record both sides with the same script.
19
+
13
20
  ## 1. Capture a comparable pair
14
21
 
15
22
  Take the after capture from the branch or its preview, and the before capture from a deployment or commit that still behaves the old way. Capture both with the same viewport, zoom, theme, data, scroll position, and open menus, so the change is the only difference.
@@ -65,6 +72,7 @@ Image `src` must be a URL the renderer can reach: a path next to the document, o
65
72
  ```
66
73
 
67
74
  Without Vite+, use `pnpm exec` with the same arguments. Open `evidence-screenshots.html` and take a screenshot of it. This CLI renders annotate, carousel, and zoom only; check any `::diff` or other blocks in the host's full Comark UI preview.
68
- 3. Check that each mark sits on its target and each note matches what its side shows. Measure a mark that misses again from the source; a box moved by eye stays a guess.
75
+
76
+ 3. Check that each mark sits on its target and each note matches what its side shows. A clip should start by itself and loop in the page. Measure a mark that misses again from the source; a box moved by eye stays a guess.
69
77
 
70
78
  Done when you've looked at the rendered page and every mark lands. Name any mark or block you couldn't check in your report.
@@ -1,3 +1,3 @@
1
1
  interface:
2
2
  display_name: "Before/after evidence"
3
- short_description: "Show a visible change with comparable, measured screenshots"
3
+ short_description: "Show a visible change with measured screenshots, or motion with a clip"
@@ -0,0 +1,130 @@
1
+ # Recording motion
2
+
3
+ A clip is evidence when the reviewer sees the motion at its real speed, starting and ending at rest, with nothing else on screen changing.
4
+
5
+ ## 1. Script the interaction
6
+
7
+ Drive the page with a script, such as Playwright, so a retake repeats the same steps at the same pace.
8
+
9
+ - Size the viewport to the component, the way you'd clip a still. The viewport is the frame.
10
+ - Rest about 0.7 s before each action and longer than the animation after it, about 1.3 s, so every motion starts and ends on a still frame.
11
+ - Show the motion two or three times, such as three tab switches, and keep the clip to a few seconds.
12
+ - Draw a pointer. A headless browser draws none, and without one the reader sees things change with nothing pressing them. Move the mouse to its target in steps, not in one jump.
13
+
14
+ ## 2. Capture the frames
15
+
16
+ Chrome's screencast sends a lossless PNG each time the page paints. Playwright's `recordVideo` is simpler, but it starts recording during page load and its VP8 encoding blurs small text.
17
+
18
+ Both capture CSS pixels, whatever `deviceScaleFactor` says. For sharp text, lay the page out at double size: double the viewport, the root `font-size`, and any font sizes set in `px`. Keep CSS `zoom` and `transform` off the page. Motion code often measures boxes with `getBoundingClientRect`, and under either one it draws its moving parts in the wrong place. On a tab transition recorded with `zoom: 2`, the leaving tokens appeared below the code block.
19
+
20
+ Save this as `record.mjs` in a workspace with `@playwright/test` and its Chromium browser installed, with ffmpeg on `PATH`. Adjust the viewport and tab names for the page, then run `node record.mjs <url>` with the page's URL.
21
+
22
+ ```js
23
+ import { chromium } from "@playwright/test";
24
+ import { execFileSync } from "node:child_process";
25
+ import { mkdirSync, writeFileSync } from "node:fs";
26
+
27
+ const url = process.argv[2];
28
+ if (!url) throw new Error("Usage: node record.mjs <url>");
29
+
30
+ const width = 1456,
31
+ height = 340,
32
+ dir = "clip"; // twice the component's CSS size
33
+ mkdirSync(dir, { recursive: true });
34
+ const browser = await chromium.launch();
35
+ const page = await browser.newPage({ viewport: { width, height } });
36
+ await page.goto(url);
37
+ await page.addStyleTag({ content: "html{font-size:32px}" }); // plus any px fonts, doubled
38
+
39
+ // An arrow whose tip follows the mouse, so it leaves the label it points at readable.
40
+ const arrow = encodeURIComponent(
41
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 20"><path d="M1 1v16l4-4 3 6 2-1-3-6h6z" fill="#111" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/></svg>',
42
+ );
43
+ await page.addStyleTag({
44
+ content: `#pointer{position:fixed;left:0;top:0;z-index:9;width:28px;height:40px;background:url("data:image/svg+xml,${arrow}");pointer-events:none}`,
45
+ });
46
+ await page.evaluate(() => {
47
+ const tip = document.body.appendChild(
48
+ Object.assign(document.createElement("div"), { id: "pointer" }),
49
+ );
50
+ addEventListener("mousemove", (e) => (tip.style.translate = `${e.clientX}px ${e.clientY}px`));
51
+ });
52
+
53
+ const cdp = await page.context().newCDPSession(page);
54
+ const frames = [];
55
+ cdp.on("Page.screencastFrame", ({ data, metadata, sessionId }) => {
56
+ frames.push({ data, at: metadata.timestamp });
57
+ cdp.send("Page.screencastFrameAck", { sessionId }).catch(() => {});
58
+ });
59
+ await cdp.send("Page.startScreencast", { format: "png", maxWidth: width, maxHeight: height });
60
+ await page.mouse.move(width - 80, height - 60); // paints the first frame
61
+ await page.waitForTimeout(700);
62
+ for (const name of ["Second", "Third", "First"]) {
63
+ const box = await page.getByRole("tab", { name }).boundingBox();
64
+ await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 14 });
65
+ await page.waitForTimeout(150); // lets the reader see where it presses
66
+ await page.mouse.down();
67
+ await page.mouse.up();
68
+ await page.waitForTimeout(1300);
69
+ }
70
+ const end = Date.now() / 1000;
71
+ await cdp.send("Page.stopScreencast");
72
+ await browser.close();
73
+
74
+ // A frame arrives only when the page paints, so each one holds until the next.
75
+ let list = "";
76
+ frames.forEach((frame, i) => {
77
+ writeFileSync(`${dir}/${i}.png`, Buffer.from(frame.data, "base64"));
78
+ list += `file '${i}.png'\nduration ${((frames[i + 1]?.at ?? end) - frame.at).toFixed(4)}\n`;
79
+ });
80
+ writeFileSync(`${dir}/frames.txt`, `${list}file '${frames.length - 1}.png'\n`);
81
+ execFileSync("ffmpeg", [
82
+ "-y",
83
+ "-f",
84
+ "concat",
85
+ "-i",
86
+ `${dir}/frames.txt`,
87
+ "-vf",
88
+ "fps=30,format=yuv420p",
89
+ "-c:v",
90
+ "libx264",
91
+ "-crf",
92
+ "18",
93
+ "-movflags",
94
+ "+faststart",
95
+ "-an",
96
+ `${dir}/clip.mp4`,
97
+ ]);
98
+ ```
99
+
100
+ The mp4 is H.264, so every browser plays it. A few seconds at this size come to a few hundred KB.
101
+
102
+ ## 3. Look at the clip
103
+
104
+ Pull frames from the middle of a motion and from a rest, and look at them:
105
+
106
+ ```sh
107
+ for t in 0.5 1.1 1.25 1.4; do ffmpeg -y -loglevel error -ss $t -i clip/clip.mp4 -frames:v 1 clip/at-$t.png; done
108
+ ```
109
+
110
+ A frame from the middle of a motion shows what reviewers can't see in a still: parts in between their two places, fading, or drawn where they don't belong.
111
+
112
+ Done when a frame from the middle of each motion shows the motion your document describes, and the clip starts and ends at rest.
113
+
114
+ ## 4. Write the document
115
+
116
+ Keep the shape of a before/after document, with the clip in place of the compare:
117
+
118
+ ```md
119
+ Code groups with `magic` now slide the tokens their snippets share into place when you switch tabs. Before, the code swapped all at once.
120
+
121
+ Open a `::code-group{magic}` and select **tinyjson5**, then **tinyini**.
122
+
123
+ ![Each tab switch slides import and from into place while the package names fade between them](magic-tabs.mp4){width=1456 height=340 autoplay loop muted}
124
+ ```
125
+
126
+ - The first sentence says what moved before, so the after clip alone carries the comparison.
127
+ - The alt text says what moves, for a reader whose browser can't play the file.
128
+ - `autoplay loop muted` plays the clip like an animated screenshot. Browsers autoplay only muted video.
129
+ - `width` and `height` are the clip's pixels, so the page keeps its room before the file loads. `ffprobe -v error -show_entries stream=width,height -of csv=p=0 clip.mp4` prints them.
130
+ - When the change retimes motion that already existed, put the before clip first, recorded with the same script, and title each with a line above it.
@@ -45,7 +45,7 @@ Either way, the HTML renderer inlines the styles once per document, and the Vue,
45
45
 
46
46
  The `--comark-*` tokens theme every plugin at once; https://comark-ui.onmax.me/guide/theming/ lists them. In a Nuxt UI app the plugins follow its `--ui-*` variables with no setup. For a page rendered outside the app, such as review evidence, put the CSS from `nuxtUiTheme({ colors: appConfig.ui?.colors, css: mainCss })` (from `comark-ui/nuxt-ui`) in a `<style>`, so the page matches the app.
47
47
 
48
- A project that renders Markdown with remark moves to Comark with `remark-to-comark`; the [remark-to-comark reference](references/remark-to-comark.md) walks through it.
48
+ To move a project to Comark, start with `migrate-to-comark`, which finds its Markdown libraries and runs the migrator for each; see the [migrate-to-comark reference](references/migrate-to-comark.md). A project that renders Markdown with remark moves to Comark with `remark-to-comark`; the [remark-to-comark reference](references/remark-to-comark.md) walks through it. One that renders with markdown-it moves with `markdown-it-to-comark`; see the [markdown-it-to-comark reference](references/markdown-it-to-comark.md). A project on marked moves with `marked-to-comark`; see the [marked-to-comark reference](references/marked-to-comark.md).
49
49
 
50
50
  Problems land in `meta.issues` after `parseMarkdown`, one sentence each, and a host can refuse a document while any remain. For untrusted input, add Comark's `security` plugin with `blockedTags` such as `script`, `style`, and `iframe`; it cleans the Markdown inside blocks too.
51
51
 
@@ -23,12 +23,12 @@ function close() {
23
23
  | `showLineNumbers` | Numbers the lines. |
24
24
  | `startLineNumber=120` | Numbers the lines from 120, and turns numbers on. |
25
25
  | `title="src/zoom.ts"` | Titles the block, like Comark's `[src/zoom.ts]`. A block without one shows its language. |
26
- | `frame="terminal"` | Draws a terminal window. `frame="code"` or `frame="none"` draws a plain block. |
26
+ | `frame="terminal"` | Draws a terminal, with a line under its header. `frame="code"` or `frame="none"` draws a plain block. |
27
27
  | `wrap` | Starts the block with Wrap on. |
28
28
 
29
29
  A range is one line, `{7}`, a run, `{3-4}`, or a list of both, `{1, 3-4}`, always in braces. Ranges count the block's own lines from 1, folded lines included, whatever `startLineNumber` says: `startLineNumber=120 ins={3}` marks the line numbered 122.
30
30
 
31
- `sh`, `bash`, `zsh`, `shell`, `shellscript`, `shellsession`, `console`, and `ansi` blocks draw as terminals unless their title is a file name, such as `install.sh`. A terminal's header shows its title but not its language. In a code group the tab names the block, so a terminal there draws no window.
31
+ `sh`, `bash`, `zsh`, `shell`, `shellscript`, `shellsession`, `console`, and `ansi` blocks draw as terminals unless their title is a file name, such as `install.sh`. A terminal's header shows its title but not its language. In a code group the tab names the block, so a terminal there draws no line under its header.
32
32
 
33
33
  When the host runs Shiki's notation transformers, comments such as `// [!code ++]`, `// [!code --]`, `// [!code highlight]`, `// [!code focus]`, `// [!code error]`, and `// [!code warning]` mark lines too, and comark-code styles them.
34
34
 
@@ -0,0 +1,28 @@
1
+ # Moving a project from markdown-it
2
+
3
+ markdown-it-to-comark moves a project that renders Markdown with markdown-it to Comark. It reports what it finds as diagnostics, `MIC0101` to `MIC0303`, each with a fix and a link to https://comark-ui.onmax.me/markdown-it-to-comark/diagnostics/.
4
+
5
+ ## Run it
6
+
7
+ ```sh
8
+ vp exec markdown-it-to-comark check . --json
9
+ vp exec markdown-it-to-comark content "**/*.md" --write
10
+ vp exec markdown-it-to-comark rewrite . --write
11
+ vp exec markdown-it-to-comark check . --json
12
+ ```
13
+
14
+ - `check` lists the markdown-it instances and framework hooks it finds, what each option and plugin becomes, the instances `rewrite` can replace and why the others stay, and the Markdown files `content` would change.
15
+ - `rewrite` replaces `const md = new MarkdownIt(options).use(plugin)` with `const md = comarkOptions({ ...options, plugins: [markdownItPlugin(plugin)] })` and each `md.render(src)` with `(await renderHtml(src, md))`. It only does so when every `render` call is in an async function, since Comark renders asynchronously. Install `comark`, `@comark/html`, and `markdown-it-to-comark`.
16
+ - `content` escapes `{`, `[`, and `:name` that Comark would read as attributes, spans, or components, and keeps an edit only if markdown-it renders the file byte for byte as before. It reads with the project's preset, options, and plugins, so `::: tip` containers and `[^1]` footnotes stay as written.
17
+ - Run `check` again at the end. What's left is work for you: framework setups (MIC0101), plugins and hand-made changes nobody compared (MIC0105), setups it couldn't rewrite (MIC0201), and blocks left as written (MIC0303).
18
+
19
+ ## What it leaves to you
20
+
21
+ - A framework's instance, such as VitePress' `markdown.config(md)` or Eleventy's `setLibrary("md", …)`, stays as it is: the framework renders. Render with `renderHtml(markdown, comarkOptions())` where the framework lets you.
22
+ - A markdown-it plugin keeps its output on Comark through `markdownItPlugin(plugin, ...options)` from `markdown-it-to-comark/plugins`, renderer rules included. A comark-ui package that does the same job is reported with it (MIC0102); its markup differs.
23
+ - A renderer rule set by hand (`md.renderer.rules.fence = …`) moves into a plugin function registered with `markdownItPlugin()`.
24
+
25
+ ## Register
26
+
27
+ - In `comark-ui`: not included. Install it beside the preset for the migration.
28
+ - Alone: `pnpm add -D markdown-it-to-comark`. It installs the command `markdown-it-to-comark`, the library, the Comark setup `markdown-it-to-comark/setup` and the bridge `markdown-it-to-comark/plugins` (add the package to your runtime dependencies when rewritten code imports them), and the Vite Doctor extension `markdown-it-to-comark/doctor`.
@@ -0,0 +1,28 @@
1
+ # Moving a project from marked
2
+
3
+ marked-to-comark moves a project that renders Markdown with marked to Comark. It reports what it finds as diagnostics, `MKC0101` to `MKC0304`, each with a fix and a link to https://comark-ui.onmax.me/marked-to-comark/diagnostics/.
4
+
5
+ ## Run it
6
+
7
+ ```sh
8
+ vp exec marked-to-comark check . --json
9
+ vp exec marked-to-comark content "posts/**/*.md" --write
10
+ vp exec marked-to-comark rewrite . --write
11
+ vp exec marked-to-comark check . --json
12
+ ```
13
+
14
+ - `check` lists the marked setups it finds (the shared `marked`, `new Marked()` instances) and what each option and extension becomes, the calls `rewrite` can replace and why the others stay, and the Markdown files `content` would change. `--json` prints each finding with its code, file, line, fix, and docs link.
15
+ - `rewrite` replaces `marked.parse(markdown)` inside an async function with `await renderHtml(markdown, comarkOptions({ … }))`, importing `comarkOptions` from `marked-to-comark/setup`. Install what the new code imports: `comark`, `@comark/html`, and `marked-to-comark`.
16
+ - `content` escapes `{`, `[`, and `:name` that Comark would read as attributes, spans, or components, so marked still renders every file byte for byte as before. It checks each edit with the project's own marked. Run it before `rewrite`, which removes the marked setup it reads options from.
17
+ - Run `check` again at the end. What's left is work for you: custom renderers and extensions (MKC0105), options with no counterpart (MKC0106), calls it couldn't rewrite (MKC0201), and blocks left as written (MKC0303).
18
+
19
+ ## What it leaves to you
20
+
21
+ - A call in a function that isn't async stays: `renderHtml` returns a promise. Make the function async, update its callers, and run `rewrite` again.
22
+ - A custom `renderer` method becomes a component for the element it draws (`components: { a }` in @comark/html, or your framework's renderer); a tokenizer or extension becomes a Comark plugin.
23
+ - Strings marked renders at runtime, such as chat replies, need no content changes. For a reply that streams in, render with `{ ...comarkOptions(), autoClose: true }` so unfinished Markdown looks finished at every step.
24
+
25
+ ## Register
26
+
27
+ - In `comark-ui`: not included. Install it beside the preset for the migration.
28
+ - Alone: `pnpm add -D marked-to-comark`. It installs the command `marked-to-comark`, the library `marked-to-comark`, the Comark setup `marked-to-comark/setup` (add it to your runtime dependencies when rewritten code imports it), and the Vite Doctor extension `marked-to-comark/doctor`.
@@ -0,0 +1,33 @@
1
+ # Moving a project to Comark
2
+
3
+ migrate-to-comark finds the Markdown libraries a project uses and moves it with each library's migrator: remark-to-comark, markdown-it-to-comark, and marked-to-comark. Start here when you don't know which library the project uses, or it uses more than one.
4
+
5
+ ## Run it
6
+
7
+ ```sh
8
+ npx migrate-to-comark --json # what it found and would change; writes nothing
9
+ npx migrate-to-comark --yes # write every change
10
+ npx migrate-to-comark --yes --only marked # one library at a time
11
+ ```
12
+
13
+ - It reads every package.json for remark, markdown-it, and marked, and the frameworks built on them: Astro, Docusaurus, and react-markdown for remark; VitePress, VuePress, and Eleventy for markdown-it; svelte-markdown and ngx-markdown for marked.
14
+ - It writes only what each migrator's `rewrite --write` and `content --write` would. A file two migrators want goes to the library its nearest package.json declares. When that doesn't settle it, `conflicts` in the JSON names the file and both migrators, and the file stays as it is.
15
+ - Code that renders Markdown switches last. While a Markdown file its migrator handles needs a hand fix or is in `conflicts`, `held` in the JSON names the code file and the Markdown files it waits on. Fix those, then run it again.
16
+ - In an agent's shell it can't ask, so without `--yes` it reports and writes nothing.
17
+
18
+ ## What it leaves to you
19
+
20
+ Everything each migrator reports. Run each one's check at the end, and follow its reference for the fixes:
21
+
22
+ ```sh
23
+ npx remark-to-comark check . --json
24
+ npx markdown-it-to-comark check . --json
25
+ npx marked-to-comark check . --json
26
+ ```
27
+
28
+ See the [remark-to-comark](remark-to-comark.md), [markdown-it-to-comark](markdown-it-to-comark.md), and [marked-to-comark](marked-to-comark.md) references.
29
+
30
+ ## Register
31
+
32
+ - In `comark-ui`: not included. Run it with `npx`, or install it beside the preset for the migration.
33
+ - Alone: `pnpm add -D migrate-to-comark`. It installs the command `migrate-to-comark`, the library `migrate-to-comark` (`detect`, `plan`, `apply`), and the three migrators.
@@ -49,7 +49,7 @@ Promise callbacks are microtasks, and a timer's callback is a task.
49
49
  - Options are shuffled the same way for every reader, so do not write options that point at others by place, such as "All of the above" or "Both A and B". Keep the options about as long as each other, since a right option much longer than the rest is reported.
50
50
  - Keep a block to six options or fewer. Mark at least one right option in a question, or ask it as a poll. Put no block inside another block's prompt, option, or slot.
51
51
 
52
- The HTML renderer inlines the quiz script and styles. Hosts can listen for `comark-quiz:answer` and `comark-quiz:ready`, and restore answers or results with `comark-quiz:state`; see [Saving answers](https://comark-ui.onmax.me/quiz/#saving-answers) on its page for the event shapes and storage behavior.
52
+ The HTML renderer inlines the quiz script and styles. Hosts can listen for `comark-quiz:answer` and `comark-quiz:ready`, and restore answers or results with `comark-quiz:state` (or `window.__comarkQuizState` set before the script runs). In an iframe, `data-quiz-relay` on the lesson's `<html>` relays all three over postMessage to the parent; see [Saving answers](https://comark-ui.onmax.me/quiz/#saving-answers) on its page for the event shapes and storage behavior.
53
53
 
54
54
  ## Register
55
55
 
@@ -5,10 +5,16 @@ remark-to-comark moves a project that renders Markdown with remark, rehype, unif
5
5
  ## Run it
6
6
 
7
7
  ```sh
8
- vp exec remark-to-comark check . --json
8
+ vp exec remark-to-comark migrate
9
+ ```
10
+
11
+ `migrate` runs `content --write`, then `rewrite --write`, then `check`, on a clean git tree (`--force` skips that check). To review each step first, run them yourself:
12
+
13
+ ```sh
14
+ vp exec remark-to-comark check --json
9
15
  vp exec remark-to-comark content "**/*.{md,mdx}" --write
10
- vp exec remark-to-comark rewrite . --write
11
- vp exec remark-to-comark check . --json
16
+ vp exec remark-to-comark rewrite --write
17
+ vp exec remark-to-comark check --json
12
18
  ```
13
19
 
14
20
  - `check` lists the remark setups it finds and what each plugin becomes, the unified chains `rewrite` can replace and why the others stay, the Markdown files `content` would change, and the MDX it leaves alone. `--json` prints each finding with its code, file, line, fix, and docs link.
@@ -29,6 +29,7 @@ Press **Ctrl+K** to find the pigeon data.
29
29
  - `::tabs`: each `:::tabs-item` inside is one tab. Content outside every tab doesn't render, and is reported.
30
30
  - `:::tabs-item`: `label` is required and must differ from the other labels in its block, since `sync` matches tabs by label. `icon` draws an Iconify name, such as `lucide:apple`, or an image URL before the label.
31
31
  - `::code-group`: only code blocks go inside. Name each one's tab in brackets after its language, as in `sh [pnpm]`, with an optional icon after it, as in `sh [pnpm] icon=logos:pnpm`. The tab replaces the block's own title. `title="pnpm"` names no tab, so a block without brackets is reported.
32
+ - A code group animates a switch between two snippets that share at least half their tokens, and swaps unlike ones at once. `magic` on `::code-group` forces the animation on, and `magic="false"` turns it off. Leave it out unless the default is wrong for that group.
32
33
  - `sync`, on either block, is a key shared by blocks that switch together. Picking a tab picks the tab with the same label in every block with that key, and the next page that uses the key remembers it.
33
34
  - A code block marked `pm`, as in `sh pm`, holds package manager commands in any manager (npm, npx, pnpm, pnpx, yarn, bun, bunx, or deno) and becomes a code group with a tab each for npm, pnpm, yarn, and bun, synced as `pm`. The manager it's written in keeps its commands as written. It reads install, frozen install (`npm ci`), add with `-D`, `-E`, `-O`, `--save-peer`, or `-g`, remove, run, `npx`/`dlx`, create, and init. An update or a `pnpm exec` stays only in its own manager's tab, and the others are left out and reported. Only commands at the start of a line or after `&&`, `||`, or `;` change. Inside a `::code-group`, `pm` does nothing.
34
35
 
@@ -4,7 +4,7 @@ comark-ui registers every comark-ui package for one renderer, in the order they
4
4
 
5
5
  ## Options
6
6
 
7
- `comarkUi(options)` returns `{ plugins, components }`. Each key is a package's name without `comark-`, in camelCase, such as `fileTree`, `codeNotes`, or `readingTime`. The exceptions are `math` for comark-mathml, `outline` for comark-outline, and `mermaid` and `typeset`, which are not packages. `false` turns a plugin off along with its components, and an object turns it on with those options. `true` turns on `include` and leaves the others as they are:
7
+ `comarkUi(options)` returns `{ plugins, components }`. Each key is a package's name without `comark-`, in camelCase, such as `fileTree`, `codeNotes`, or `readingTime`. The exceptions are `math` for comark-mathml, `outline` for comark-outline, and `mermaid`, `shiki`, and `typeset`, which are not packages. `false` turns a plugin off along with its components, and an object turns it on with those options. `true` turns on `include` and leaves the others as they are:
8
8
 
9
9
  ```ts
10
10
  comarkUi({
@@ -26,6 +26,7 @@ comarkUi({
26
26
  - Media sizing reads only data URIs by default. `zoom: { size: true }` and `media: { size: true }` also fetch remote files while parsing, and `annotate: { size: true }` does so at HTML render time. Enable these only for trusted documents, because the requests follow redirects and can reach private networks. `{ size: false }` turns measurement off for that plugin; authored dimensions and screenshot viewports still work.
27
27
  - `icon`'s options apply to every icon the preset draws, in cards, details, file trees, tabs, and code actions too.
28
28
  - `mermaid` draws `mermaid` fences with comark-pele, on the server and offline. In the HTML entry the diagram zooms with comark-zoom unless `zoom` is `false`. `mermaid: false` turns it off.
29
+ - `shiki` highlights fenced code with your own Shiki, before comark-code: `{ highlighter: (language) => shiki | undefined, themes: { light, dark } }`. It is off until you pass it, so the preset bundles no grammars. `maxBlocks` and `maxSource` cap how many blocks, and how many characters of code, a document highlights; the blocks past them show plain, each with an issue.
29
30
  - comark-critic has no component to add its styles, so put `criticCss`, which every entry exports, in the page.
30
31
  - The preset styles the text around the blocks, scoped to the element the document renders into, on the same `--comark-*` tokens. Pass `typeset: false` when the site already styles Markdown, such as with Tailwind Typography's `prose`.
31
32
 
package/dist/skills.mjs CHANGED
@@ -3,20 +3,20 @@
3
3
  const skills = [
4
4
  {
5
5
  "name": "before-after-evidence",
6
- "description": "Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page. Use when a pull request, bug fix, or design change alters what people see, or when someone asks for before/after screenshots or visual evidence.",
6
+ "description": "Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page, and a change in motion as a short recording. Use when a pull request, bug fix, or design change alters what people see or how it moves, or when someone asks for before/after screenshots, a recording, or visual evidence.",
7
7
  "frontmatter": {
8
8
  "name": "before-after-evidence",
9
- "description": "Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page. Use when a pull request, bug fix, or design change alters what people see, or when someone asks for before/after screenshots or visual evidence.",
9
+ "description": "Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page, and a change in motion as a short recording. Use when a pull request, bug fix, or design change alters what people see or how it moves, or when someone asks for before/after screenshots, a recording, or visual evidence.",
10
10
  "license": "MIT"
11
11
  },
12
12
  "files": [
13
13
  {
14
14
  "path": "SKILL.md",
15
- "text": "---\nname: before-after-evidence\ndescription: Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page. Use when a pull request, bug fix, or design change alters what people see, or when someone asks for before/after screenshots or visual evidence.\nlicense: MIT\n---\n\n# Before/after evidence\n\nA before/after is evidence when a reviewer can check every claim against the two images and nothing else. The reader can't repair a bad pair, so get three things right: two captures of the same view, marks on what changed, and notes in the words the screen shows.\n\nThis skill writes the document. Your host decides where it's published and who can open it.\n\n## 1. Capture a comparable pair\n\nTake the after capture from the branch or its preview, and the before capture from a deployment or commit that still behaves the old way. Capture both with the same viewport, zoom, theme, data, scroll position, and open menus, so the change is the only difference.\n\n- Set up the state that shows the change before you capture: the long name that wrapped, the error that stayed hidden, the empty list.\n- Clip both captures to one region around the change, the same rectangle on each side. A `::compare` gives each image under half the page width, where a full-page capture's text is too small to read.\n- Pin anything that moves the layout between the two states, such as a vertically centered dialog that grows by one line, so the same element sits at the same place in both.\n\nIf you can't capture one side comparably, stop and say what blocked you. A description in place of a screenshot, or one side alone, isn't before/after evidence.\n\nDone when both images exist, have the same size, and differ only where the change is.\n\n## 2. Measure every mark\n\nMarks take `x`, `y`, `w`, and `h` as percentages of the image. Measure them. An estimate drifts a few percent, which on a half-width image lands on the neighbouring button.\n\n- **A page you captured:** read the element's box in the same browser, viewport, and state, minus the clip's offset. [references/measuring.md](references/measuring.md) has the snippet.\n- **A still you didn't capture**, such as a frame from a screen recording, a design export, or an image attached to a bug report: there is no DOM to ask. Measure in image pixels and divide by the image's natural width and height. Confirm each box by cropping the image to it and looking at the crop, as the same reference shows.\n\nMark the change at its own size: the field, the label, the button. Use `red` on the before side for the problem and `green` on the after side for the fix, and leave what stayed the same unmarked. Two or three marks a side usually tell the whole story.\n\nDone when every mark has numbers you measured, and you can name the element or the pixel rectangle each came from.\n\n## 3. Write the document\n\nStart from [references/example.md](references/example.md) and keep its shape:\n\n1. One sentence: what changed, and for whom.\n2. One line of steps to reach the screen, so a reviewer can reproduce it.\n3. The `::compare`, each note a few words in the screen's own words. A value the screenshot doesn't show stays out of its note.\n4. A `::diff` of the line that caused it, only when the reader would otherwise ask.\n\nThe images carry the evidence, so the text only points into them. Put how the work went (checks, retries, CI) in your report to the user, not in the document. [references/blocks.md](references/blocks.md) has the syntax of each block.\n\nImage `src` must be a URL the renderer can reach: a path next to the document, or an https URL your host gives you for the upload.\n\n## 4. Check the render\n\n1. Lint it, and fix until it reports no problems:\n\n ```sh\n vp dlx --package comark-linter --package comark-ui comark-lint evidence.md\n ```\n\n These commands use the [global Vite+ CLI](https://viteplus.dev/guide/global-cli). Without it, `pnpm dlx` takes the same arguments.\n\n2. Open the page where it will be read and take a screenshot of it. To check the screenshots alone, copy the whole `::compare` block, nested screenshots and annotations included, into `evidence-screenshots.md` beside `evidence.md`.\n\n Make comark-annotate a direct dependency of this workspace package. A transitive one isn't linked, and `@latest` can run a different release from the project's renderers. Match the version of the installed Comark UI packages: `pnpm add -D comark-annotate@<version>`, with `-w` at the workspace root. With no Comark UI packages installed, omit `@<version>`. Then run:\n\n ```sh\n vp exec comark-annotate render evidence-screenshots.md -o evidence-screenshots.html\n ```\n\n Without Vite+, use `pnpm exec` with the same arguments. Open `evidence-screenshots.html` and take a screenshot of it. This CLI renders annotate, carousel, and zoom only; check any `::diff` or other blocks in the host's full Comark UI preview.\n3. Check that each mark sits on its target and each note matches what its side shows. Measure a mark that misses again from the source; a box moved by eye stays a guess.\n\nDone when you've looked at the rendered page and every mark lands. Name any mark or block you couldn't check in your report.\n"
15
+ "text": "---\nname: before-after-evidence\ndescription: Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page, and a change in motion as a short recording. Use when a pull request, bug fix, or design change alters what people see or how it moves, or when someone asks for before/after screenshots, a recording, or visual evidence.\nlicense: MIT\n---\n\n# Before/after evidence\n\nA before/after is evidence when a reviewer can check every claim against the two images and nothing else. The reader can't repair a bad pair, so get three things right: two captures of the same view, marks on what changed, and notes in the words the screen shows.\n\nThis skill writes the document. Your host decides where it's published and who can open it.\n\n## When the change is motion\n\nAn animation, a transition, a loading sequence, or feedback while dragging happens between frames, so record it. A still taken partway through a motion looks like a broken layout, and two of them can't show that anything moves. Follow [references/recording.md](references/recording.md) in place of steps 1 and 2, then write and check the document with steps 3 and 4, with the clip where the `::compare` goes.\n\n- When the old behavior jumped straight to its end state, the after clip alone is the evidence, and the document's first sentence says what happened before.\n- When the change retimes motion that already existed, such as its easing, its duration, or a stutter, record both sides with the same script.\n\n## 1. Capture a comparable pair\n\nTake the after capture from the branch or its preview, and the before capture from a deployment or commit that still behaves the old way. Capture both with the same viewport, zoom, theme, data, scroll position, and open menus, so the change is the only difference.\n\n- Set up the state that shows the change before you capture: the long name that wrapped, the error that stayed hidden, the empty list.\n- Clip both captures to one region around the change, the same rectangle on each side. A `::compare` gives each image under half the page width, where a full-page capture's text is too small to read.\n- Pin anything that moves the layout between the two states, such as a vertically centered dialog that grows by one line, so the same element sits at the same place in both.\n\nIf you can't capture one side comparably, stop and say what blocked you. A description in place of a screenshot, or one side alone, isn't before/after evidence.\n\nDone when both images exist, have the same size, and differ only where the change is.\n\n## 2. Measure every mark\n\nMarks take `x`, `y`, `w`, and `h` as percentages of the image. Measure them. An estimate drifts a few percent, which on a half-width image lands on the neighbouring button.\n\n- **A page you captured:** read the element's box in the same browser, viewport, and state, minus the clip's offset. [references/measuring.md](references/measuring.md) has the snippet.\n- **A still you didn't capture**, such as a frame from a screen recording, a design export, or an image attached to a bug report: there is no DOM to ask. Measure in image pixels and divide by the image's natural width and height. Confirm each box by cropping the image to it and looking at the crop, as the same reference shows.\n\nMark the change at its own size: the field, the label, the button. Use `red` on the before side for the problem and `green` on the after side for the fix, and leave what stayed the same unmarked. Two or three marks a side usually tell the whole story.\n\nDone when every mark has numbers you measured, and you can name the element or the pixel rectangle each came from.\n\n## 3. Write the document\n\nStart from [references/example.md](references/example.md) and keep its shape:\n\n1. One sentence: what changed, and for whom.\n2. One line of steps to reach the screen, so a reviewer can reproduce it.\n3. The `::compare`, each note a few words in the screen's own words. A value the screenshot doesn't show stays out of its note.\n4. A `::diff` of the line that caused it, only when the reader would otherwise ask.\n\nThe images carry the evidence, so the text only points into them. Put how the work went (checks, retries, CI) in your report to the user, not in the document. [references/blocks.md](references/blocks.md) has the syntax of each block.\n\nImage `src` must be a URL the renderer can reach: a path next to the document, or an https URL your host gives you for the upload.\n\n## 4. Check the render\n\n1. Lint it, and fix until it reports no problems:\n\n ```sh\n vp dlx --package comark-linter --package comark-ui comark-lint evidence.md\n ```\n\n These commands use the [global Vite+ CLI](https://viteplus.dev/guide/global-cli). Without it, `pnpm dlx` takes the same arguments.\n\n2. Open the page where it will be read and take a screenshot of it. To check the screenshots alone, copy the whole `::compare` block, nested screenshots and annotations included, into `evidence-screenshots.md` beside `evidence.md`.\n\n Make comark-annotate a direct dependency of this workspace package. A transitive one isn't linked, and `@latest` can run a different release from the project's renderers. Match the version of the installed Comark UI packages: `pnpm add -D comark-annotate@<version>`, with `-w` at the workspace root. With no Comark UI packages installed, omit `@<version>`. Then run:\n\n ```sh\n vp exec comark-annotate render evidence-screenshots.md -o evidence-screenshots.html\n ```\n\n Without Vite+, use `pnpm exec` with the same arguments. Open `evidence-screenshots.html` and take a screenshot of it. This CLI renders annotate, carousel, and zoom only; check any `::diff` or other blocks in the host's full Comark UI preview.\n\n3. Check that each mark sits on its target and each note matches what its side shows. A clip should start by itself and loop in the page. Measure a mark that misses again from the source; a box moved by eye stays a guess.\n\nDone when you've looked at the rendered page and every mark lands. Name any mark or block you couldn't check in your report.\n"
16
16
  },
17
17
  {
18
18
  "path": "agents/openai.yaml",
19
- "text": "interface:\n display_name: \"Before/after evidence\"\n short_description: \"Show a visible change with comparable, measured screenshots\"\n"
19
+ "text": "interface:\n display_name: \"Before/after evidence\"\n short_description: \"Show a visible change with measured screenshots, or motion with a clip\"\n"
20
20
  },
21
21
  {
22
22
  "path": "references/blocks.md",
@@ -29,6 +29,10 @@ const skills = [
29
29
  {
30
30
  "path": "references/measuring.md",
31
31
  "text": "# Measuring marks\n\nEvery mark is `x y w h`, each a percentage of the image from 0 to 100. Measure in the same coordinates the image was captured in.\n\n## A page you captured\n\nCapture both states with one clip, then read each element's box in the same page, before anything moves:\n\n```js\n// Playwright: one clip for both sides, so the same element sits at the same place.\nconst clip = { x: 56, y: 12, width: 528, height: 312 };\nawait page.screenshot({ path: \"after.png\", clip });\n\nconst box = await page.$eval(\"#email\", (el, clip) => {\n const r = el.getBoundingClientRect();\n return [\n ((r.x - clip.x) / clip.width) * 100,\n ((r.y - clip.y) / clip.height) * 100,\n (r.width / clip.width) * 100,\n (r.height / clip.height) * 100,\n ].map((v) => +v.toFixed(1));\n}, clip);\n// box is [x, y, w, h] for the annotation.\n```\n\n- Without a clip, divide by `innerWidth` and `innerHeight` and subtract nothing.\n- For a full-page capture, add `scrollX` and `scrollY` to the box and divide by `document.documentElement.scrollWidth` and `scrollHeight`.\n- An element that spans its container, such as a block paragraph, measures as wide as the container. Measure the text instead: give it `width: fit-content` while you capture, or measure a `Range` around its text.\n- If an element crosses the clip's edge, cut its box at the edge, so every value stays between 0 and 100.\n\n## A still you didn't capture\n\nA frame from a screen recording, a design export, or an image attached to an issue has no DOM. Measure in pixels:\n\n1. Read the image's natural size, such as with `magick identify frame.png` or `file frame.png`.\n2. Find the target's rectangle in pixels in an image viewer that shows the pointer's coordinates.\n3. Divide: `x = left / width * 100`, `y = top / height * 100`, `w = boxWidth / width * 100`, `h = boxHeight / height * 100`.\n4. Crop the image to that rectangle and look at the crop. It should show the target and little else:\n\n ```sh\n magick frame.png -crop 320x38+44+134 +repage crop.png\n ```\n\n With Python and Pillow, `Image.open(\"frame.png\").crop((44, 134, 364, 172)).save(\"crop.png\")` does the same.\n\nFor a pair of video frames, take both at the same playback size and use the same crop for both, as you would clip two page captures.\n\n## In the render\n\nThe rendered page is the last check. A mark that misses there has the wrong numbers: measure it again from the source.\n"
32
+ },
33
+ {
34
+ "path": "references/recording.md",
35
+ "text": "# Recording motion\n\nA clip is evidence when the reviewer sees the motion at its real speed, starting and ending at rest, with nothing else on screen changing.\n\n## 1. Script the interaction\n\nDrive the page with a script, such as Playwright, so a retake repeats the same steps at the same pace.\n\n- Size the viewport to the component, the way you'd clip a still. The viewport is the frame.\n- Rest about 0.7 s before each action and longer than the animation after it, about 1.3 s, so every motion starts and ends on a still frame.\n- Show the motion two or three times, such as three tab switches, and keep the clip to a few seconds.\n- Draw a pointer. A headless browser draws none, and without one the reader sees things change with nothing pressing them. Move the mouse to its target in steps, not in one jump.\n\n## 2. Capture the frames\n\nChrome's screencast sends a lossless PNG each time the page paints. Playwright's `recordVideo` is simpler, but it starts recording during page load and its VP8 encoding blurs small text.\n\nBoth capture CSS pixels, whatever `deviceScaleFactor` says. For sharp text, lay the page out at double size: double the viewport, the root `font-size`, and any font sizes set in `px`. Keep CSS `zoom` and `transform` off the page. Motion code often measures boxes with `getBoundingClientRect`, and under either one it draws its moving parts in the wrong place. On a tab transition recorded with `zoom: 2`, the leaving tokens appeared below the code block.\n\nSave this as `record.mjs` in a workspace with `@playwright/test` and its Chromium browser installed, with ffmpeg on `PATH`. Adjust the viewport and tab names for the page, then run `node record.mjs <url>` with the page's URL.\n\n```js\nimport { chromium } from \"@playwright/test\";\nimport { execFileSync } from \"node:child_process\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\n\nconst url = process.argv[2];\nif (!url) throw new Error(\"Usage: node record.mjs <url>\");\n\nconst width = 1456,\n height = 340,\n dir = \"clip\"; // twice the component's CSS size\nmkdirSync(dir, { recursive: true });\nconst browser = await chromium.launch();\nconst page = await browser.newPage({ viewport: { width, height } });\nawait page.goto(url);\nawait page.addStyleTag({ content: \"html{font-size:32px}\" }); // plus any px fonts, doubled\n\n// An arrow whose tip follows the mouse, so it leaves the label it points at readable.\nconst arrow = encodeURIComponent(\n '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 14 20\"><path d=\"M1 1v16l4-4 3 6 2-1-3-6h6z\" fill=\"#111\" stroke=\"#fff\" stroke-width=\"1.2\" stroke-linejoin=\"round\"/></svg>',\n);\nawait page.addStyleTag({\n content: `#pointer{position:fixed;left:0;top:0;z-index:9;width:28px;height:40px;background:url(\"data:image/svg+xml,${arrow}\");pointer-events:none}`,\n});\nawait page.evaluate(() => {\n const tip = document.body.appendChild(\n Object.assign(document.createElement(\"div\"), { id: \"pointer\" }),\n );\n addEventListener(\"mousemove\", (e) => (tip.style.translate = `${e.clientX}px ${e.clientY}px`));\n});\n\nconst cdp = await page.context().newCDPSession(page);\nconst frames = [];\ncdp.on(\"Page.screencastFrame\", ({ data, metadata, sessionId }) => {\n frames.push({ data, at: metadata.timestamp });\n cdp.send(\"Page.screencastFrameAck\", { sessionId }).catch(() => {});\n});\nawait cdp.send(\"Page.startScreencast\", { format: \"png\", maxWidth: width, maxHeight: height });\nawait page.mouse.move(width - 80, height - 60); // paints the first frame\nawait page.waitForTimeout(700);\nfor (const name of [\"Second\", \"Third\", \"First\"]) {\n const box = await page.getByRole(\"tab\", { name }).boundingBox();\n await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 14 });\n await page.waitForTimeout(150); // lets the reader see where it presses\n await page.mouse.down();\n await page.mouse.up();\n await page.waitForTimeout(1300);\n}\nconst end = Date.now() / 1000;\nawait cdp.send(\"Page.stopScreencast\");\nawait browser.close();\n\n// A frame arrives only when the page paints, so each one holds until the next.\nlet list = \"\";\nframes.forEach((frame, i) => {\n writeFileSync(`${dir}/${i}.png`, Buffer.from(frame.data, \"base64\"));\n list += `file '${i}.png'\\nduration ${((frames[i + 1]?.at ?? end) - frame.at).toFixed(4)}\\n`;\n});\nwriteFileSync(`${dir}/frames.txt`, `${list}file '${frames.length - 1}.png'\\n`);\nexecFileSync(\"ffmpeg\", [\n \"-y\",\n \"-f\",\n \"concat\",\n \"-i\",\n `${dir}/frames.txt`,\n \"-vf\",\n \"fps=30,format=yuv420p\",\n \"-c:v\",\n \"libx264\",\n \"-crf\",\n \"18\",\n \"-movflags\",\n \"+faststart\",\n \"-an\",\n `${dir}/clip.mp4`,\n]);\n```\n\nThe mp4 is H.264, so every browser plays it. A few seconds at this size come to a few hundred KB.\n\n## 3. Look at the clip\n\nPull frames from the middle of a motion and from a rest, and look at them:\n\n```sh\nfor t in 0.5 1.1 1.25 1.4; do ffmpeg -y -loglevel error -ss $t -i clip/clip.mp4 -frames:v 1 clip/at-$t.png; done\n```\n\nA frame from the middle of a motion shows what reviewers can't see in a still: parts in between their two places, fading, or drawn where they don't belong.\n\nDone when a frame from the middle of each motion shows the motion your document describes, and the clip starts and ends at rest.\n\n## 4. Write the document\n\nKeep the shape of a before/after document, with the clip in place of the compare:\n\n```md\nCode groups with `magic` now slide the tokens their snippets share into place when you switch tabs. Before, the code swapped all at once.\n\nOpen a `::code-group{magic}` and select **tinyjson5**, then **tinyini**.\n\n![Each tab switch slides import and from into place while the package names fade between them](magic-tabs.mp4){width=1456 height=340 autoplay loop muted}\n```\n\n- The first sentence says what moved before, so the after clip alone carries the comparison.\n- The alt text says what moves, for a reader whose browser can't play the file.\n- `autoplay loop muted` plays the clip like an animated screenshot. Browsers autoplay only muted video.\n- `width` and `height` are the clip's pixels, so the page keeps its room before the file loads. `ffprobe -v error -show_entries stream=width,height -of csv=p=0 clip.mp4` prints them.\n- When the change retimes motion that already existed, put the before clip first, recorded with the same script, and title each with a line above it.\n"
32
36
  }
33
37
  ]
34
38
  },
@@ -42,7 +46,7 @@ const skills = [
42
46
  "files": [
43
47
  {
44
48
  "path": "SKILL.md",
45
- "text": "---\nname: comark-ui\ndescription: Writes PR evidence, explanations of how something works, and docs pages in Markdown with comark-ui. Use when a document needs an annotated screenshot, a before/after, a chart, a diagram, a code change, or another comark block, or when setting up comark-ui or a comark-* package.\n---\n\n# comark-ui\n\nYou write Markdown, and the comark plugins render it as static HTML. The reader sees only what you measured and wrote down, so measure every mark from the page, take every number from the data, and check the render before you hand it over.\n\n## Install\n\nInstall `comark-ui` by default. It holds every plugin and registers them in the order they need:\n\n```sh\npnpm add comark @comark/html comark-ui\n```\n\nRun the comark CLIs with the [global Vite+ CLI](https://viteplus.dev/guide/global-cli): `vp dlx` for a package the project doesn't install, `vp exec` for one it does. Without Vite+, `pnpm dlx` and `pnpm exec` take the same arguments.\n\n```ts\nimport { createHtmlRenderer } from \"@comark/html\";\nimport comarkUi from \"comark-ui/html\";\n\nconst render = createHtmlRenderer(comarkUi());\n```\n\nEvery plugin is on except `include`. The [comark-ui reference](references/ui.md) covers its options, the plugins that read files or use the network, and its Vue, React, and Svelte entries.\n\nTo use only some plugins, install those packages and register each plugin with its components:\n\n```sh\npnpm add comark @comark/html comark-annotate\n```\n\n```ts\nimport { createHtmlRenderer } from \"@comark/html\";\nimport annotate, { components } from \"comark-annotate/html\";\n\nconst render = createHtmlRenderer({ plugins: [annotate()], components });\n```\n\nEach reference ends with a Register section that says how its package registers alone and which options it needs.\n\nEither way, the HTML renderer inlines the styles once per document, and the Vue, React, and Svelte components add them on mount. On a server-rendered page, import each package's `style.css`, such as `comark-cards/style.css`, so the page doesn't shift.\n\nThe `--comark-*` tokens theme every plugin at once; https://comark-ui.onmax.me/guide/theming/ lists them. In a Nuxt UI app the plugins follow its `--ui-*` variables with no setup. For a page rendered outside the app, such as review evidence, put the CSS from `nuxtUiTheme({ colors: appConfig.ui?.colors, css: mainCss })` (from `comark-ui/nuxt-ui`) in a `<style>`, so the page matches the app.\n\nA project that renders Markdown with remark moves to Comark with `remark-to-comark`; the [remark-to-comark reference](references/remark-to-comark.md) walks through it.\n\nProblems land in `meta.issues` after `parseMarkdown`, one sentence each, and a host can refuse a document while any remain. For untrusted input, add Comark's `security` plugin with `blockedTags` such as `script`, `style`, and `iframe`; it cleans the Markdown inside blocks too.\n\n## Shape the document\n\nThis skill teaches the blocks. Four sibling skills teach a whole document, what goes in it and in what order: `before-after-evidence`, `review-document`, `visual-explanation`, and `teach-lesson`. When the user asks for one of those and it isn't installed, suggest `vp dlx skills add onmax/comark-ui` and picking it from the list.\n\n## Pick the blocks\n\nRead the reference for each block you use before you write it. Mix the blocks freely with ordinary Markdown, such as headings, tables, and lists.\n\n### Review blocks\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| One state of a screen, with marks, numbered notes, or arrows | `::screenshot` | [annotate](references/annotate.md) |\n| A change on screen, before and after | `::compare` | [annotate](references/annotate.md) |\n| A command's output, such as a test run or a build, as colored text | `::cast` | [cast](references/cast.md) |\n| Numbers, such as sizes, timings, before/after metrics, a trend, or a timeline | `::chart` | [charts](references/charts.md) |\n| A flow, a sequence, an architecture, or a schema as a diagram | a `mermaid`, `d2`, `dot`, `plantuml`, `dbml`, `goat`, or `svgbob` fence | [diagrams](references/diagrams.md) |\n| Links and `@mentions` with the site's favicon or the person's avatar | links, `@user`, `:chip` | [chips](references/chips.md) |\n| Colors, such as design tokens before and after, or a palette | a color in inline code, `::palette` | [colors](references/colors.md) |\n| A suggested change to one file, before and after | `::diff` | [diff](references/diff.md) |\n| Suggested edits and comment threads on prose, such as a copy review, each note in a card in the margin | `:suggestion`, `:comment` | [comments](references/comments.md) |\n| A video, a design file, or a live reproduction from its link | `::embed` | [embed](references/embed.md) |\n| A screen recording or a sound that a screenshot can't show | `![alt](clip.mp4)` | [media](references/media.md) |\n| A number before and after, a table of befores and afters, or progress | `:delta`, `::stats`, `:progress` | [metrics](references/metrics.md) |\n| Data from a CSV, TSV, or JSON fence or file, as a table that copies and sorts | `::table`, a `csv` fence | [table](references/table.md) |\n| A test run's passed, failed, and skipped counts and its failures, from a JUnit report | `::tests` | [tests](references/tests.md) |\n| A note, tip, or warning box, or long output folded away, as on GitHub | `> [!NOTE]`, `<details>` | [github](references/github.md) |\n| The review's status, pull request, author, date, and tags in a box | frontmatter, `::properties` | [properties](references/properties.md) |\n| Claims backed by links, with numbered citations | `:cite`, `::sources` | [sources](references/sources.md) |\n| An icon in a sentence or a table cell, such as a logo or a status | `:icon` | [icon](references/icon.md) |\n| A preview link a reviewer can scan with a phone | `:qr` | [qr](references/qr.md) |\n| Why a line of code is there, as numbered notes under it | `// <1>` and a numbered list | [code-notes](references/code-notes.md) |\n\n### Page behavior\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| A stable link to a section, or a link that opens a new tab | `{#id}`, `{target=\"_blank\"}` | [links](references/links.md) |\n| A table, code block, or diagram too small to read in place | `::zoom` | [zoom](references/zoom.md) |\n| A sequence to page through in one place, such as the screens of a flow | `::carousel` | [carousel](references/carousel.md) |\n| Versions of one thing where each reader needs only one, such as an OS or a package manager | `::tabs`, `::code-group`, `pm` | [tabs](references/tabs.md) |\n| Code with lines marked added, removed, or highlighted, or a pasted `git diff` | `ins={4}` after a fence's language, `diff lang=\"ts\"` | [code](references/code.md) |\n\n### Docs blocks\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| Content a reader may skip, such as a long log, behind a summary | `::details`, `::accordion`, `::collapsible` | [details](references/details.md) |\n| Folders and files, with what a change added, removed, or changed | `::file-tree` | [file-tree](references/file-tree.md) |\n| A component's props or a function's options, with types and defaults | `::field` | [fields](references/fields.md) |\n| A version, status, or deprecation tag in a heading or a sentence | `:badge` | [badge](references/badge.md) |\n| Links to the next pages as cards, or blocks side by side | `::card-group`, `::columns` | [cards](references/cards.md) |\n| Instructions the reader follows in order, such as repro or setup steps | `::steps` | [steps](references/steps.md) |\n| Code that changes over several steps, such as a refactor told one move at a time | `::magic-move` | [magic-move](references/magic-move.md) |\n| An interactive lesson question, poll, or recall card | `::question`, `::poll`, `::recall` | [quiz](references/quiz.md) |\n\n### Markdown syntax\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| Bold or italics in Chinese, Japanese, or Korean text | `**…**`, `*…*` | [cjk](references/cjk.md) |\n| An image with a caption under it | `![alt](src \"Caption\")` | [figure](references/figure.md) |\n| A highlighted phrase or a superscript | `==text==`, `^2^` | [mark](references/mark.md) |\n| A suggested edit or a comment inside the text | `{++ ++}`, `{-- --}`, `{~~ ~> ~~}`, `{== ==}`, `{>> <<}` | [critic](references/critic.md) |\n| Terms and their definitions, such as a glossary | `Term`, then `: definition` | [deflist](references/deflist.md) |\n| A numbered note at the end of the page, such as a source or a caveat | `[^label]`, `^[note]` | [footnotes](references/footnotes.md) |\n| A formula, such as a complexity bound or how a number was computed | `$…$`, `$$…$$` | [math](references/math.md) |\n| A keyboard shortcut | `++ctrl+k++` | [keys](references/keys.md) |\n\n### Authoring tools\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| A shared section, or real source lines, pasted from a file | `:include{src}` | [include](references/include.md) |\n| A table of contents of the page's headings | `::toc` | [outline](references/outline.md) |\n| Figures and tables numbered and referred to by number | `{#fig-name}`, `:ref{#fig-name}` | [xref](references/xref.md) |\n| Word count and reading time, for the page's layout | nothing; it writes `meta.readingTime` | [reading-time](references/reading-time.md) |\n\n### Chat\n\n| To show | Use | Reference |\n| --- | --- | --- |\n| An AI SDK chat message, with its reasoning, tool calls, sources, and files | `MessageParts` from `comark-chat` | [chat](references/chat.md) |\n\n## Marks\n\nScreenshots and charts draw the same marks. Each `::annotation` is one mark and its note, written in Markdown.\n\n- `mark` is `box` (default), `circle`, `underline`, `line`, `highlight`, `bracket`, `cross`, `check`, or `none`, which draws nothing, so its note needs a `side` and the arrow alone points.\n- `color` is `violet` (default) to point something out, `red` for a problem, `amber` for a warning, `green` for a fix, `blue` for a second voice, `gray` for neutral, or a 3- or 6-digit hex color.\n- `side` is `top`, `bottom`, `left`, or `right`, and puts the note in that margin with an arrow to its mark. Notes without one are numbered in a list under the figure.\n\nMark only what your text talks about. Write each note in the words the screen or table shows, saying what changed or what is wrong, so the reader can check it against the figure. Leave out a value the figure doesn't show.\n\n## Check the result\n\n1. Lint the file with `vp exec comark-lint report.md`, in a project with `comark-linter` and the plugins installed (`comark-ui` counts as every plugin). It prints one line per problem with its line, its rule, and the fix, and exits with 1 while any remain. Fix every problem and run it again until it exits with 0. The [lint reference](references/lint.md) covers the flags, the config file, the library, ESLint, and Vite Doctor.\n2. Render the document as the block's reference says, open it in a browser, and take a screenshot. Each mark must sit on its target, each number must match its note, and each chart must say what your text says. Name anything you couldn't verify when you hand the document over.\n"
49
+ "text": "---\nname: comark-ui\ndescription: Writes PR evidence, explanations of how something works, and docs pages in Markdown with comark-ui. Use when a document needs an annotated screenshot, a before/after, a chart, a diagram, a code change, or another comark block, or when setting up comark-ui or a comark-* package.\n---\n\n# comark-ui\n\nYou write Markdown, and the comark plugins render it as static HTML. The reader sees only what you measured and wrote down, so measure every mark from the page, take every number from the data, and check the render before you hand it over.\n\n## Install\n\nInstall `comark-ui` by default. It holds every plugin and registers them in the order they need:\n\n```sh\npnpm add comark @comark/html comark-ui\n```\n\nRun the comark CLIs with the [global Vite+ CLI](https://viteplus.dev/guide/global-cli): `vp dlx` for a package the project doesn't install, `vp exec` for one it does. Without Vite+, `pnpm dlx` and `pnpm exec` take the same arguments.\n\n```ts\nimport { createHtmlRenderer } from \"@comark/html\";\nimport comarkUi from \"comark-ui/html\";\n\nconst render = createHtmlRenderer(comarkUi());\n```\n\nEvery plugin is on except `include`. The [comark-ui reference](references/ui.md) covers its options, the plugins that read files or use the network, and its Vue, React, and Svelte entries.\n\nTo use only some plugins, install those packages and register each plugin with its components:\n\n```sh\npnpm add comark @comark/html comark-annotate\n```\n\n```ts\nimport { createHtmlRenderer } from \"@comark/html\";\nimport annotate, { components } from \"comark-annotate/html\";\n\nconst render = createHtmlRenderer({ plugins: [annotate()], components });\n```\n\nEach reference ends with a Register section that says how its package registers alone and which options it needs.\n\nEither way, the HTML renderer inlines the styles once per document, and the Vue, React, and Svelte components add them on mount. On a server-rendered page, import each package's `style.css`, such as `comark-cards/style.css`, so the page doesn't shift.\n\nThe `--comark-*` tokens theme every plugin at once; https://comark-ui.onmax.me/guide/theming/ lists them. In a Nuxt UI app the plugins follow its `--ui-*` variables with no setup. For a page rendered outside the app, such as review evidence, put the CSS from `nuxtUiTheme({ colors: appConfig.ui?.colors, css: mainCss })` (from `comark-ui/nuxt-ui`) in a `<style>`, so the page matches the app.\n\nTo move a project to Comark, start with `migrate-to-comark`, which finds its Markdown libraries and runs the migrator for each; see the [migrate-to-comark reference](references/migrate-to-comark.md). A project that renders Markdown with remark moves to Comark with `remark-to-comark`; the [remark-to-comark reference](references/remark-to-comark.md) walks through it. One that renders with markdown-it moves with `markdown-it-to-comark`; see the [markdown-it-to-comark reference](references/markdown-it-to-comark.md). A project on marked moves with `marked-to-comark`; see the [marked-to-comark reference](references/marked-to-comark.md).\n\nProblems land in `meta.issues` after `parseMarkdown`, one sentence each, and a host can refuse a document while any remain. For untrusted input, add Comark's `security` plugin with `blockedTags` such as `script`, `style`, and `iframe`; it cleans the Markdown inside blocks too.\n\n## Shape the document\n\nThis skill teaches the blocks. Four sibling skills teach a whole document, what goes in it and in what order: `before-after-evidence`, `review-document`, `visual-explanation`, and `teach-lesson`. When the user asks for one of those and it isn't installed, suggest `vp dlx skills add onmax/comark-ui` and picking it from the list.\n\n## Pick the blocks\n\nRead the reference for each block you use before you write it. Mix the blocks freely with ordinary Markdown, such as headings, tables, and lists.\n\n### Review blocks\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| One state of a screen, with marks, numbered notes, or arrows | `::screenshot` | [annotate](references/annotate.md) |\n| A change on screen, before and after | `::compare` | [annotate](references/annotate.md) |\n| A command's output, such as a test run or a build, as colored text | `::cast` | [cast](references/cast.md) |\n| Numbers, such as sizes, timings, before/after metrics, a trend, or a timeline | `::chart` | [charts](references/charts.md) |\n| A flow, a sequence, an architecture, or a schema as a diagram | a `mermaid`, `d2`, `dot`, `plantuml`, `dbml`, `goat`, or `svgbob` fence | [diagrams](references/diagrams.md) |\n| Links and `@mentions` with the site's favicon or the person's avatar | links, `@user`, `:chip` | [chips](references/chips.md) |\n| Colors, such as design tokens before and after, or a palette | a color in inline code, `::palette` | [colors](references/colors.md) |\n| A suggested change to one file, before and after | `::diff` | [diff](references/diff.md) |\n| Suggested edits and comment threads on prose, such as a copy review, each note in a card in the margin | `:suggestion`, `:comment` | [comments](references/comments.md) |\n| A video, a design file, or a live reproduction from its link | `::embed` | [embed](references/embed.md) |\n| A screen recording or a sound that a screenshot can't show | `![alt](clip.mp4)` | [media](references/media.md) |\n| A number before and after, a table of befores and afters, or progress | `:delta`, `::stats`, `:progress` | [metrics](references/metrics.md) |\n| Data from a CSV, TSV, or JSON fence or file, as a table that copies and sorts | `::table`, a `csv` fence | [table](references/table.md) |\n| A test run's passed, failed, and skipped counts and its failures, from a JUnit report | `::tests` | [tests](references/tests.md) |\n| A note, tip, or warning box, or long output folded away, as on GitHub | `> [!NOTE]`, `<details>` | [github](references/github.md) |\n| The review's status, pull request, author, date, and tags in a box | frontmatter, `::properties` | [properties](references/properties.md) |\n| Claims backed by links, with numbered citations | `:cite`, `::sources` | [sources](references/sources.md) |\n| An icon in a sentence or a table cell, such as a logo or a status | `:icon` | [icon](references/icon.md) |\n| A preview link a reviewer can scan with a phone | `:qr` | [qr](references/qr.md) |\n| Why a line of code is there, as numbered notes under it | `// <1>` and a numbered list | [code-notes](references/code-notes.md) |\n\n### Page behavior\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| A stable link to a section, or a link that opens a new tab | `{#id}`, `{target=\"_blank\"}` | [links](references/links.md) |\n| A table, code block, or diagram too small to read in place | `::zoom` | [zoom](references/zoom.md) |\n| A sequence to page through in one place, such as the screens of a flow | `::carousel` | [carousel](references/carousel.md) |\n| Versions of one thing where each reader needs only one, such as an OS or a package manager | `::tabs`, `::code-group`, `pm` | [tabs](references/tabs.md) |\n| Code with lines marked added, removed, or highlighted, or a pasted `git diff` | `ins={4}` after a fence's language, `diff lang=\"ts\"` | [code](references/code.md) |\n\n### Docs blocks\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| Content a reader may skip, such as a long log, behind a summary | `::details`, `::accordion`, `::collapsible` | [details](references/details.md) |\n| Folders and files, with what a change added, removed, or changed | `::file-tree` | [file-tree](references/file-tree.md) |\n| A component's props or a function's options, with types and defaults | `::field` | [fields](references/fields.md) |\n| A version, status, or deprecation tag in a heading or a sentence | `:badge` | [badge](references/badge.md) |\n| Links to the next pages as cards, or blocks side by side | `::card-group`, `::columns` | [cards](references/cards.md) |\n| Instructions the reader follows in order, such as repro or setup steps | `::steps` | [steps](references/steps.md) |\n| Code that changes over several steps, such as a refactor told one move at a time | `::magic-move` | [magic-move](references/magic-move.md) |\n| An interactive lesson question, poll, or recall card | `::question`, `::poll`, `::recall` | [quiz](references/quiz.md) |\n\n### Markdown syntax\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| Bold or italics in Chinese, Japanese, or Korean text | `**…**`, `*…*` | [cjk](references/cjk.md) |\n| An image with a caption under it | `![alt](src \"Caption\")` | [figure](references/figure.md) |\n| A highlighted phrase or a superscript | `==text==`, `^2^` | [mark](references/mark.md) |\n| A suggested edit or a comment inside the text | `{++ ++}`, `{-- --}`, `{~~ ~> ~~}`, `{== ==}`, `{>> <<}` | [critic](references/critic.md) |\n| Terms and their definitions, such as a glossary | `Term`, then `: definition` | [deflist](references/deflist.md) |\n| A numbered note at the end of the page, such as a source or a caveat | `[^label]`, `^[note]` | [footnotes](references/footnotes.md) |\n| A formula, such as a complexity bound or how a number was computed | `$…$`, `$$…$$` | [math](references/math.md) |\n| A keyboard shortcut | `++ctrl+k++` | [keys](references/keys.md) |\n\n### Authoring tools\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| A shared section, or real source lines, pasted from a file | `:include{src}` | [include](references/include.md) |\n| A table of contents of the page's headings | `::toc` | [outline](references/outline.md) |\n| Figures and tables numbered and referred to by number | `{#fig-name}`, `:ref{#fig-name}` | [xref](references/xref.md) |\n| Word count and reading time, for the page's layout | nothing; it writes `meta.readingTime` | [reading-time](references/reading-time.md) |\n\n### Chat\n\n| To show | Use | Reference |\n| --- | --- | --- |\n| An AI SDK chat message, with its reasoning, tool calls, sources, and files | `MessageParts` from `comark-chat` | [chat](references/chat.md) |\n\n## Marks\n\nScreenshots and charts draw the same marks. Each `::annotation` is one mark and its note, written in Markdown.\n\n- `mark` is `box` (default), `circle`, `underline`, `line`, `highlight`, `bracket`, `cross`, `check`, or `none`, which draws nothing, so its note needs a `side` and the arrow alone points.\n- `color` is `violet` (default) to point something out, `red` for a problem, `amber` for a warning, `green` for a fix, `blue` for a second voice, `gray` for neutral, or a 3- or 6-digit hex color.\n- `side` is `top`, `bottom`, `left`, or `right`, and puts the note in that margin with an arrow to its mark. Notes without one are numbered in a list under the figure.\n\nMark only what your text talks about. Write each note in the words the screen or table shows, saying what changed or what is wrong, so the reader can check it against the figure. Leave out a value the figure doesn't show.\n\n## Check the result\n\n1. Lint the file with `vp exec comark-lint report.md`, in a project with `comark-linter` and the plugins installed (`comark-ui` counts as every plugin). It prints one line per problem with its line, its rule, and the fix, and exits with 1 while any remain. Fix every problem and run it again until it exits with 0. The [lint reference](references/lint.md) covers the flags, the config file, the library, ESLint, and Vite Doctor.\n2. Render the document as the block's reference says, open it in a browser, and take a screenshot. Each mark must sit on its target, each number must match its note, and each chart must say what your text says. Name anything you couldn't verify when you hand the document over.\n"
46
50
  },
47
51
  {
48
52
  "path": "agents/openai.yaml",
@@ -90,7 +94,7 @@ const skills = [
90
94
  },
91
95
  {
92
96
  "path": "references/code.md",
93
- "text": "# Code blocks\n\ncomark-code puts Copy and Wrap on every code block. It also reads options after a fence's language, written the way Expressive Code writes them. They mark lines as added, deleted, or worth a look, fold lines away, number them, and frame shell sessions as a terminal. Use the markers to point at the lines your text talks about, such as the fix in a pull request.\n\n## Syntax\n\n````md\n```ts title=\"src/zoom.ts\" del={3} ins={4} mark={1}\nfunction close() {\n dialog.close();\n history.back();\n if (opened) history.back();\n}\n```\n````\n\n| Option | What it does |\n| --- | --- |\n| `ins={4}` | Marks lines as added, in green with a `+`. |\n| `del={3}` | Marks lines as deleted, in red with a `−`. Copy leaves them out, so the block copies as the code after the change. |\n| `mark={1}` | Highlights lines to look at, like Comark's own `{1}`. |\n| `collapse={2-7}` | Folds lines behind a row that counts them. A click opens them. |\n| `showLineNumbers` | Numbers the lines. |\n| `startLineNumber=120` | Numbers the lines from 120, and turns numbers on. |\n| `title=\"src/zoom.ts\"` | Titles the block, like Comark's `[src/zoom.ts]`. A block without one shows its language. |\n| `frame=\"terminal\"` | Draws a terminal window. `frame=\"code\"` or `frame=\"none\"` draws a plain block. |\n| `wrap` | Starts the block with Wrap on. |\n\nA range is one line, `{7}`, a run, `{3-4}`, or a list of both, `{1, 3-4}`, always in braces. Ranges count the block's own lines from 1, folded lines included, whatever `startLineNumber` says: `startLineNumber=120 ins={3}` marks the line numbered 122.\n\n`sh`, `bash`, `zsh`, `shell`, `shellscript`, `shellsession`, `console`, and `ansi` blocks draw as terminals unless their title is a file name, such as `install.sh`. A terminal's header shows its title but not its language. In a code group the tab names the block, so a terminal there draws no window.\n\nWhen the host runs Shiki's notation transformers, comments such as `// [!code ++]`, `// [!code --]`, `// [!code highlight]`, `// [!code focus]`, `// [!code error]`, and `// [!code warning]` mark lines too, and comark-code styles them.\n\n## Show a change\n\n- Mark a change of a few lines with `ins` and `del` on the code you write out.\n- Paste `git diff` output into a `diff` fence with `lang`, as below. The code highlights as that language, each leading `+` or `-` becomes `ins` or `del`, and the `@@` hunk headers and the file headers above them go away. Without `lang`, the fence stays a plain diff.\n- For a suggested change, the way a pull request review shows one, write comark-diff's `::diff` with the code before and after.\n- `mark` the lines your text names, and `collapse` the imports and setup the reader can skip.\n\n````md\n```diff lang=\"ts\"\n@@ -12,4 +12,4 @@ export function close() {\n function close() {\n dialog.close();\n- history.back();\n+ if (opened) history.back();\n }\n```\n````\n\nA range past the block's last line is reported. One that lands on the wrong line inside the block isn't, so count the lines before you mark them.\n\n## Add buttons\n\n`code({ actions })` adds the host's own buttons before Wrap and Copy, on the blocks in each action's `languages`, or on every block when it lists none. A button shows a `label`, an `icon`, or both. The icon is an Iconify name, such as `lucide:play`, or an image URL, and an icon-only button needs a `title` for its tooltip and screen readers.\n\n```ts\ncode({ actions: [{ id: \"run\", label: \"Run\", icon: \"lucide:play\", languages: [\"sh\", \"bash\"] }] });\n```\n\nThe page decides what a click does. `onCodeAction()` from `comark-code/client` calls your function with the block's code and language, and returns a function that stops listening:\n\n```ts\nimport { onCodeAction } from \"comark-code/client\";\n\nconst stop = onCodeAction(\"run\", ({ code, language }) => terminal.run(code));\n```\n\nA click sends a bubbling `comark-code-action` event with `{ action, code, language }` as its `detail`, so one listener on the page can handle every action. Icons resolve while parsing, through the `iconResolver` option, a `createIcons()` from comark-icon, or from the Iconify API without one. An icon that can't be drawn is reported, and its button shows its title instead.\n\n## Register\n\n- In `comark-ui`: on, as `code`. Pass `code: { actions }` for buttons of your own, and the preset's `icon` options resolve their icons. The preset leaves Shiki out. To add it, put it right before comark-code, which keeps it after comark-tabs:\n\n ```ts\n import shiki from \"comark/plugins/shiki\";\n\n const { plugins, components } = comarkUi();\n plugins.splice(plugins.findIndex((plugin) => plugin.name === \"comark-code\"), 0, shiki());\n ```\n\n- Alone: `pnpm add comark @comark/html comark-code`, then `createHtmlRenderer({ plugins: [code()], components })` with both from `comark-code/html`. `comark-code/vue`, `/react`, and `/svelte` (Svelte 5.20 or later) export the same pair for those renderers. With Shiki, list `shiki()` before `code()`. Shiki redraws every block, so comark-code listed first loses its markers, numbers, and folds, and reports it. Without Shiki, comark-code splits the lines itself.\n"
97
+ "text": "# Code blocks\n\ncomark-code puts Copy and Wrap on every code block. It also reads options after a fence's language, written the way Expressive Code writes them. They mark lines as added, deleted, or worth a look, fold lines away, number them, and frame shell sessions as a terminal. Use the markers to point at the lines your text talks about, such as the fix in a pull request.\n\n## Syntax\n\n````md\n```ts title=\"src/zoom.ts\" del={3} ins={4} mark={1}\nfunction close() {\n dialog.close();\n history.back();\n if (opened) history.back();\n}\n```\n````\n\n| Option | What it does |\n| --- | --- |\n| `ins={4}` | Marks lines as added, in green with a `+`. |\n| `del={3}` | Marks lines as deleted, in red with a `−`. Copy leaves them out, so the block copies as the code after the change. |\n| `mark={1}` | Highlights lines to look at, like Comark's own `{1}`. |\n| `collapse={2-7}` | Folds lines behind a row that counts them. A click opens them. |\n| `showLineNumbers` | Numbers the lines. |\n| `startLineNumber=120` | Numbers the lines from 120, and turns numbers on. |\n| `title=\"src/zoom.ts\"` | Titles the block, like Comark's `[src/zoom.ts]`. A block without one shows its language. |\n| `frame=\"terminal\"` | Draws a terminal, with a line under its header. `frame=\"code\"` or `frame=\"none\"` draws a plain block. |\n| `wrap` | Starts the block with Wrap on. |\n\nA range is one line, `{7}`, a run, `{3-4}`, or a list of both, `{1, 3-4}`, always in braces. Ranges count the block's own lines from 1, folded lines included, whatever `startLineNumber` says: `startLineNumber=120 ins={3}` marks the line numbered 122.\n\n`sh`, `bash`, `zsh`, `shell`, `shellscript`, `shellsession`, `console`, and `ansi` blocks draw as terminals unless their title is a file name, such as `install.sh`. A terminal's header shows its title but not its language. In a code group the tab names the block, so a terminal there draws no line under its header.\n\nWhen the host runs Shiki's notation transformers, comments such as `// [!code ++]`, `// [!code --]`, `// [!code highlight]`, `// [!code focus]`, `// [!code error]`, and `// [!code warning]` mark lines too, and comark-code styles them.\n\n## Show a change\n\n- Mark a change of a few lines with `ins` and `del` on the code you write out.\n- Paste `git diff` output into a `diff` fence with `lang`, as below. The code highlights as that language, each leading `+` or `-` becomes `ins` or `del`, and the `@@` hunk headers and the file headers above them go away. Without `lang`, the fence stays a plain diff.\n- For a suggested change, the way a pull request review shows one, write comark-diff's `::diff` with the code before and after.\n- `mark` the lines your text names, and `collapse` the imports and setup the reader can skip.\n\n````md\n```diff lang=\"ts\"\n@@ -12,4 +12,4 @@ export function close() {\n function close() {\n dialog.close();\n- history.back();\n+ if (opened) history.back();\n }\n```\n````\n\nA range past the block's last line is reported. One that lands on the wrong line inside the block isn't, so count the lines before you mark them.\n\n## Add buttons\n\n`code({ actions })` adds the host's own buttons before Wrap and Copy, on the blocks in each action's `languages`, or on every block when it lists none. A button shows a `label`, an `icon`, or both. The icon is an Iconify name, such as `lucide:play`, or an image URL, and an icon-only button needs a `title` for its tooltip and screen readers.\n\n```ts\ncode({ actions: [{ id: \"run\", label: \"Run\", icon: \"lucide:play\", languages: [\"sh\", \"bash\"] }] });\n```\n\nThe page decides what a click does. `onCodeAction()` from `comark-code/client` calls your function with the block's code and language, and returns a function that stops listening:\n\n```ts\nimport { onCodeAction } from \"comark-code/client\";\n\nconst stop = onCodeAction(\"run\", ({ code, language }) => terminal.run(code));\n```\n\nA click sends a bubbling `comark-code-action` event with `{ action, code, language }` as its `detail`, so one listener on the page can handle every action. Icons resolve while parsing, through the `iconResolver` option, a `createIcons()` from comark-icon, or from the Iconify API without one. An icon that can't be drawn is reported, and its button shows its title instead.\n\n## Register\n\n- In `comark-ui`: on, as `code`. Pass `code: { actions }` for buttons of your own, and the preset's `icon` options resolve their icons. The preset leaves Shiki out. To add it, put it right before comark-code, which keeps it after comark-tabs:\n\n ```ts\n import shiki from \"comark/plugins/shiki\";\n\n const { plugins, components } = comarkUi();\n plugins.splice(plugins.findIndex((plugin) => plugin.name === \"comark-code\"), 0, shiki());\n ```\n\n- Alone: `pnpm add comark @comark/html comark-code`, then `createHtmlRenderer({ plugins: [code()], components })` with both from `comark-code/html`. `comark-code/vue`, `/react`, and `/svelte` (Svelte 5.20 or later) export the same pair for those renderers. With Shiki, list `shiki()` before `code()`. Shiki redraws every block, so comark-code listed first loses its markers, numbers, and folds, and reports it. Without Shiki, comark-code splits the lines itself.\n"
94
98
  },
95
99
  {
96
100
  "path": "references/colors.md",
@@ -172,6 +176,14 @@ const skills = [
172
176
  "path": "references/mark.md",
173
177
  "text": "# Highlights and superscripts\n\ncomark-mark renders `==text==` as a native `<mark>` and `^text^` as `<sup>`. Use a highlight for the few words a reader should see first, such as the number that changed or the condition a fix adds, and a superscript for exponents and ordinals.\n\n## Syntax\n\n```md\nThe deploy waits while ==more than 12 pigeons== sit on the roof, about 10^3^ by Friday.\n\n==Do not deploy=={.warning} before the 1^st^ count of the day.\n```\n\n- `==text==` highlights. Markdown works inside, and `{attributes}` work after the closing pair, so `==Do not deploy=={.warning}` renders `<mark class=\"cmk-mark warning\">`. A single `=`, as in `a = b`, stays text, and so does a `==` with nothing to close it.\n- `^text^` is a superscript of plain text with no spaces; escape a space with a backslash, `^spaced\\ text^`. `[^1]` never opens one, so footnotes keep working.\n- There is no subscript, since GitHub reads `~text~` as strikethrough. Write `H<sub>2</sub>O`.\n\nHighlight a few words per paragraph at most. When you highlight a whole paragraph, nothing in it stands out. For a reviewer's highlight with a comment, use comark-critic's `{==text==}{>>comment<<}` instead.\n\n## Register\n\n- In `comark-ui`: on by default, with its components. `mark: false` turns it off; it takes no options.\n- Alone: `pnpm add comark comark-mark`, then `plugins: [mark()]` and `components` from `comark-mark/html`, `/vue`, `/react`, or `/svelte`. The components tint a highlight amber, readable on light and dark pages, and keep a superscript from opening a gap in its line. Set `--cmk-mark` to recolor it, on the page or on a class from `{.attributes}`. Without the components, every renderer draws plain `mark` and `sup`, which browsers paint yellow. It works beside any footnotes plugin and in streaming parses.\n"
174
178
  },
179
+ {
180
+ "path": "references/markdown-it-to-comark.md",
181
+ "text": "# Moving a project from markdown-it\n\nmarkdown-it-to-comark moves a project that renders Markdown with markdown-it to Comark. It reports what it finds as diagnostics, `MIC0101` to `MIC0303`, each with a fix and a link to https://comark-ui.onmax.me/markdown-it-to-comark/diagnostics/.\n\n## Run it\n\n```sh\nvp exec markdown-it-to-comark check . --json\nvp exec markdown-it-to-comark content \"**/*.md\" --write\nvp exec markdown-it-to-comark rewrite . --write\nvp exec markdown-it-to-comark check . --json\n```\n\n- `check` lists the markdown-it instances and framework hooks it finds, what each option and plugin becomes, the instances `rewrite` can replace and why the others stay, and the Markdown files `content` would change.\n- `rewrite` replaces `const md = new MarkdownIt(options).use(plugin)` with `const md = comarkOptions({ ...options, plugins: [markdownItPlugin(plugin)] })` and each `md.render(src)` with `(await renderHtml(src, md))`. It only does so when every `render` call is in an async function, since Comark renders asynchronously. Install `comark`, `@comark/html`, and `markdown-it-to-comark`.\n- `content` escapes `{`, `[`, and `:name` that Comark would read as attributes, spans, or components, and keeps an edit only if markdown-it renders the file byte for byte as before. It reads with the project's preset, options, and plugins, so `::: tip` containers and `[^1]` footnotes stay as written.\n- Run `check` again at the end. What's left is work for you: framework setups (MIC0101), plugins and hand-made changes nobody compared (MIC0105), setups it couldn't rewrite (MIC0201), and blocks left as written (MIC0303).\n\n## What it leaves to you\n\n- A framework's instance, such as VitePress' `markdown.config(md)` or Eleventy's `setLibrary(\"md\", …)`, stays as it is: the framework renders. Render with `renderHtml(markdown, comarkOptions())` where the framework lets you.\n- A markdown-it plugin keeps its output on Comark through `markdownItPlugin(plugin, ...options)` from `markdown-it-to-comark/plugins`, renderer rules included. A comark-ui package that does the same job is reported with it (MIC0102); its markup differs.\n- A renderer rule set by hand (`md.renderer.rules.fence = …`) moves into a plugin function registered with `markdownItPlugin()`.\n\n## Register\n\n- In `comark-ui`: not included. Install it beside the preset for the migration.\n- Alone: `pnpm add -D markdown-it-to-comark`. It installs the command `markdown-it-to-comark`, the library, the Comark setup `markdown-it-to-comark/setup` and the bridge `markdown-it-to-comark/plugins` (add the package to your runtime dependencies when rewritten code imports them), and the Vite Doctor extension `markdown-it-to-comark/doctor`.\n"
182
+ },
183
+ {
184
+ "path": "references/marked-to-comark.md",
185
+ "text": "# Moving a project from marked\n\nmarked-to-comark moves a project that renders Markdown with marked to Comark. It reports what it finds as diagnostics, `MKC0101` to `MKC0304`, each with a fix and a link to https://comark-ui.onmax.me/marked-to-comark/diagnostics/.\n\n## Run it\n\n```sh\nvp exec marked-to-comark check . --json\nvp exec marked-to-comark content \"posts/**/*.md\" --write\nvp exec marked-to-comark rewrite . --write\nvp exec marked-to-comark check . --json\n```\n\n- `check` lists the marked setups it finds (the shared `marked`, `new Marked()` instances) and what each option and extension becomes, the calls `rewrite` can replace and why the others stay, and the Markdown files `content` would change. `--json` prints each finding with its code, file, line, fix, and docs link.\n- `rewrite` replaces `marked.parse(markdown)` inside an async function with `await renderHtml(markdown, comarkOptions({ … }))`, importing `comarkOptions` from `marked-to-comark/setup`. Install what the new code imports: `comark`, `@comark/html`, and `marked-to-comark`.\n- `content` escapes `{`, `[`, and `:name` that Comark would read as attributes, spans, or components, so marked still renders every file byte for byte as before. It checks each edit with the project's own marked. Run it before `rewrite`, which removes the marked setup it reads options from.\n- Run `check` again at the end. What's left is work for you: custom renderers and extensions (MKC0105), options with no counterpart (MKC0106), calls it couldn't rewrite (MKC0201), and blocks left as written (MKC0303).\n\n## What it leaves to you\n\n- A call in a function that isn't async stays: `renderHtml` returns a promise. Make the function async, update its callers, and run `rewrite` again.\n- A custom `renderer` method becomes a component for the element it draws (`components: { a }` in @comark/html, or your framework's renderer); a tokenizer or extension becomes a Comark plugin.\n- Strings marked renders at runtime, such as chat replies, need no content changes. For a reply that streams in, render with `{ ...comarkOptions(), autoClose: true }` so unfinished Markdown looks finished at every step.\n\n## Register\n\n- In `comark-ui`: not included. Install it beside the preset for the migration.\n- Alone: `pnpm add -D marked-to-comark`. It installs the command `marked-to-comark`, the library `marked-to-comark`, the Comark setup `marked-to-comark/setup` (add it to your runtime dependencies when rewritten code imports it), and the Vite Doctor extension `marked-to-comark/doctor`.\n"
186
+ },
175
187
  {
176
188
  "path": "references/math.md",
177
189
  "text": "# Math\n\ncomark-mathml typesets LaTeX formulas to MathML while parsing, so the page loads no math script, fonts, or stylesheet. Use it for a formula the prose would garble, such as a complexity bound, a rate, or how a number in the report was computed. Plain numbers and units stay text.\n\n## Syntax\n\n```md\nThe cache holds $n$ entries, and each lookup costs \\(O(\\log n)\\).\n\n$$\n\\text{hit rate} = \\frac{\\text{hits}}{\\text{hits} + \\text{misses}}\n$$\n```\n\n- `$…$` and `\\(…\\)` are inline math. The opening `$` can't be followed by a space or a digit, and the closing one can't follow a space or come before a digit, so `$5 and $10` stay text. Escape a dollar before a letter as `\\$x`.\n- `$$…$$`, `\\[…\\]`, and a fence with the language `math` are display math, centered on their own line. `\\[1\\]`, numbers in brackets, stays a citation.\n- Math is never read inside code spans or code blocks.\n- `\\href`, `\\url`, and `\\includegraphics` don't render, and a macro that calls itself stops after 1,000 expansions.\n\nThe plugin reports a formula whose TeX doesn't typeset, with the typesetter's reason, and renders its TeX in its place. Fix the TeX, or put it in code when it isn't math.\n\n## Register\n\n- In `comark-ui`: on by default, in place of Comark's own math plugin. `math: { macros: { \"\\\\R\": \"\\\\mathbb{R}\" } }` defines commands every formula can use. `math: { output: \"htmlAndMathml\" }` uses KaTeX's HTML instead, which needs `katex` installed and `katex/dist/katex.css` on the page.\n- Alone: `pnpm add comark @comark/html comark-mathml`, then `plugins: [math()]` and `components` from `comark-mathml/html` (or `/vue`, `/react`, `/svelte`). It is named `math`, and Comark keeps the first plugin of a name, so remove `comark/plugins/math` from the list instead of adding this one after it.\n"
@@ -184,6 +196,10 @@ const skills = [
184
196
  "path": "references/metrics.md",
185
197
  "text": "# Numbers before and after\n\ncomark-metrics draws `:delta` as a number before and after with its arrow, percent, and verdict, `::stats` as a table of befores and afters with a Change column, and `:progress` as a progress bar. Use them where a pull request claims that a size or a speed changed, next to the claim. The plugin works out every change while parsing, so write the two numbers and leave the percent to it.\n\n## Syntax\n\n```md\nMoving the map to a worker cut the main bundle :delta{from=412 to=388 unit=\"kB\" better=\"lower\"} on every page.\n\n::stats{better=\"lower\"}\n| Metric | Before | After |\n| --- | --- | --- |\n| JavaScript | 412 kB | 388 kB |\n| Largest paint | 2.4 s | 1.9 s |\n| Requests | 38 | 41 |\n::\n\nTests moved to Vitest: :progress{value=17 max=20 label=\"Migrated\"}\n```\n\n- `:delta` reads `412 kB → 388 kB ↓ 5.8%`. `from` and `to` are required numbers, and `unit`, such as `kB`, `s`, or `%`, goes after both. `better` is `lower` for sizes and durations or `higher` for scores. It colors the change green when it's better and red when it's worse; without it the change is gray. The percent is of `from`, to one decimal. From 0 there is no percent, so the change is the difference, such as `↑ 1.5 s`.\n- `::stats` holds one Markdown table of three columns: a name, the value before, and the value after, under any headers. A cell is a number, with commas if you like, and an optional unit after it, the same in both cells of a row. `better` applies to every row, and `unit` names a difference from 0 for cells written without one. The plugin drops anything else inside the block and reports it.\n- Add `tiles` to `::stats` to show each row as a tile, with the name above the after value and the change below it. The table can then have two columns, a name and a value; a value with no before shows without a change. `better` and `unit` work as for the table.\n- `:progress` is a native `<progress>` with its percent after it. `value` is required: a fraction from 0 to 1, or a count up to `max`, which is 1 by default. `label` shows before the bar and names it for screen readers. The plugin clamps a `value` outside 0 to `max` and reports it.\n\n## Write the numbers\n\n- Copy the numbers from the tool's output as it prints them: `1,204` and `2.4 s` both read.\n- In `from`, `to`, and `value`, write the number alone and put the unit in `unit`. Write a percent of progress as `value=0.85`, or as `value=85 max=100`.\n- `better` covers the whole table. When some rows are better lower, such as sizes, and others better higher, such as scores, write two tables.\n\n## Register\n\n- In `comark-ui`: on. It takes no options.\n- Alone: `pnpm add comark @comark/html comark-metrics`, then `createHtmlRenderer({ plugins: [metrics()], components })` with both from `comark-metrics/html`. `comark-metrics/vue`, `/react`, and `/svelte` render it too. It has no options and no script.\n"
186
198
  },
199
+ {
200
+ "path": "references/migrate-to-comark.md",
201
+ "text": "# Moving a project to Comark\n\nmigrate-to-comark finds the Markdown libraries a project uses and moves it with each library's migrator: remark-to-comark, markdown-it-to-comark, and marked-to-comark. Start here when you don't know which library the project uses, or it uses more than one.\n\n## Run it\n\n```sh\nnpx migrate-to-comark --json # what it found and would change; writes nothing\nnpx migrate-to-comark --yes # write every change\nnpx migrate-to-comark --yes --only marked # one library at a time\n```\n\n- It reads every package.json for remark, markdown-it, and marked, and the frameworks built on them: Astro, Docusaurus, and react-markdown for remark; VitePress, VuePress, and Eleventy for markdown-it; svelte-markdown and ngx-markdown for marked.\n- It writes only what each migrator's `rewrite --write` and `content --write` would. A file two migrators want goes to the library its nearest package.json declares. When that doesn't settle it, `conflicts` in the JSON names the file and both migrators, and the file stays as it is.\n- Code that renders Markdown switches last. While a Markdown file its migrator handles needs a hand fix or is in `conflicts`, `held` in the JSON names the code file and the Markdown files it waits on. Fix those, then run it again.\n- In an agent's shell it can't ask, so without `--yes` it reports and writes nothing.\n\n## What it leaves to you\n\nEverything each migrator reports. Run each one's check at the end, and follow its reference for the fixes:\n\n```sh\nnpx remark-to-comark check . --json\nnpx markdown-it-to-comark check . --json\nnpx marked-to-comark check . --json\n```\n\nSee the [remark-to-comark](remark-to-comark.md), [markdown-it-to-comark](markdown-it-to-comark.md), and [marked-to-comark](marked-to-comark.md) references.\n\n## Register\n\n- In `comark-ui`: not included. Run it with `npx`, or install it beside the preset for the migration.\n- Alone: `pnpm add -D migrate-to-comark`. It installs the command `migrate-to-comark`, the library `migrate-to-comark` (`detect`, `plan`, `apply`), and the three migrators.\n"
202
+ },
187
203
  {
188
204
  "path": "references/outline.md",
189
205
  "text": "# Table of contents\n\ncomark-outline turns `::toc` into a nested list of links to the document's headings. Put one in a review or a docs page that runs past a screen or two, so the reader sees what it covers and jumps to the part they own.\n\n## Syntax\n\n```md\nThe redesign moves the filters into one row and adds an empty state.\n\n::toc{title=\"On this page\"}\n::\n\n## Filters\n\n### Toolbar\n\n## Empty state\n```\n\n| Prop | What it does |\n| --- | --- |\n| `depth` | The deepest heading it lists, a whole number from 2 to 6. Default 3, so h2 and h3. |\n| `title` | A small title above the list. It also names the `nav` for screen readers; without it, the name is \"Contents\". |\n\n- Close the block with `::` on the next line, with nothing between. The plugin drops and reports anything inside.\n- Put it after the summary paragraph, and keep headings short, since they become the links.\n- The list starts at h2, since the h1 is the page's title. Each heading nests under the closest heading above it of a higher level, so a skipped level still nests.\n- It lists headings inside other blocks too, such as each `###` step of a `::steps`. Set `depth=2` to keep them out.\n- Each link goes to the heading's id, its own `{#id}` when it has one, and reads as the heading's plain text.\n- Every `::toc` lists the whole document, wherever it sits. With no heading from h2 to `depth`, it renders nothing, and the plugin reports it.\n- Write `::toc` where other tools use `[[toc]]`, which Comark reads as a span.\n\n## Side outline\n\nThe `rail` option adds an \"On this page\" outline beside the text of every document with two headings or more to list, and marks the section being read. It isn't Markdown: a document can't turn it on, and `::toc` still works beside it. Write short h2 and h3 headings, since they become its links.\n\n- Options: `rail: true`, or `{ title, depth }` (default \"On this page\" and 3).\n- The page sets `--comark-inset-end` on `:root` for a panel that covers its right edge. While the outline keeps a column beside wide blocks, it sets `data-comark-rail` and `--comark-rail-width` on `<html>`, and the page shrinks `--comark-wide-end` by it, so wide blocks move toward the room before the text.\n- It hides where it doesn't fit, and hides rather than narrow a wide diagram.\n\n## Register\n\n- In `comark-ui`: on in every entry; `outline: { rail: true }` adds the side outline.\n- Alone: `pnpm add comark @comark/html comark-outline`, then `createHtmlRenderer({ plugins: [outline()], components })` with both from `comark-outline/html`. `comark-outline/vue`, `/react`, and `/svelte` render it too. It reads the headings itself, so it works in any plugin order and without Comark's toc plugin, which only fills `meta.toc` for a sidebar.\n"
@@ -198,7 +214,7 @@ const skills = [
198
214
  },
199
215
  {
200
216
  "path": "references/quiz.md",
201
- "text": "# Lessons and quizzes\n\ncomark-quiz adds `::question`, `::poll`, and `::recall` blocks for interactive lessons. A question checks one or more answers and explains each option, a poll collects votes, and a recall card hides an answer until the reader asks for it. Without the script, the prompt, options, and answer disclosure stay readable HTML.\n\n## Syntax\n\n```md\n::question{#event-loop-order}\nWhich callback runs first once the current script ends?\n\n- [x] A resolved promise's `then` callback\n > Promise callbacks are microtasks.\n- [ ] A `setTimeout(fn, 0)` callback\n > A timer queues a task.\n\n#hint\nTwo queues are involved.\n\n#explain\nAfter every task, the event loop empties the microtask queue.\n::\n\n::poll{#next-lesson}\nWhat should the next lesson cover?\n\n- [ ] Web Workers\n- [ ] Streams\n::\n\n::recall{#why-microtasks}\nWhy does a promise callback run before a zero-delay timer?\n\n#answer\nPromise callbacks are microtasks, and a timer's callback is a task.\n::\n```\n\n| Block | Props and slots |\n| --- | --- |\n| `::question` | `#id`, `multiple`, `shuffle=\"false\"`, `number=\"false\"`, slots `#hint` and `#explain`. |\n| `::poll` | `#id`, `multiple`, `closes` (an ISO date; after it the poll shows as closed). Options are never shuffled. |\n| `::recall` | `#id`, `number=\"false\"`, and the required slot `#answer`. |\n\n- Mark a right option `[x]` and a wrong one `[ ]`. One right option makes radios and several make checkboxes; `multiple` forces checkboxes even with one. Mark no option in a poll.\n- Put a `>` line under an option to say why it is right or wrong. Give every wrong option a reason, since that is the lesson for a reader who picked it.\n- Leave a blank line before `#hint` and `#explain`. Without it, the text above swallows the slot, and the plugin reports it.\n- Give every block an `#id`. Hosts save answers by id. Without one the id derives from the prompt and options, changes when you reword them, and the plugin reports it. Keep the id when you reword the prompt.\n- Questions and recall cards share one count, so a lesson numbers them 1, 2, 3 down the page. Polls are not numbered.\n- Options are shuffled the same way for every reader, so do not write options that point at others by place, such as \"All of the above\" or \"Both A and B\". Keep the options about as long as each other, since a right option much longer than the rest is reported.\n- Keep a block to six options or fewer. Mark at least one right option in a question, or ask it as a poll. Put no block inside another block's prompt, option, or slot.\n\nThe HTML renderer inlines the quiz script and styles. Hosts can listen for `comark-quiz:answer` and `comark-quiz:ready`, and restore answers or results with `comark-quiz:state`; see [Saving answers](https://comark-ui.onmax.me/quiz/#saving-answers) on its page for the event shapes and storage behavior.\n\n## Register\n\n- In `comark-ui`: on, as `quiz`, with optional `{ labels }` for translated UI text.\n- Alone: `pnpm add comark @comark/html comark-quiz`, then `createHtmlRenderer({ plugins: [quiz()], components })` with `quiz, { components }` from `comark-quiz/html`. Use the HTML renderer, since the blocks need its inlined script; there are no Vue, React, or Svelte entries.\n"
217
+ "text": "# Lessons and quizzes\n\ncomark-quiz adds `::question`, `::poll`, and `::recall` blocks for interactive lessons. A question checks one or more answers and explains each option, a poll collects votes, and a recall card hides an answer until the reader asks for it. Without the script, the prompt, options, and answer disclosure stay readable HTML.\n\n## Syntax\n\n```md\n::question{#event-loop-order}\nWhich callback runs first once the current script ends?\n\n- [x] A resolved promise's `then` callback\n > Promise callbacks are microtasks.\n- [ ] A `setTimeout(fn, 0)` callback\n > A timer queues a task.\n\n#hint\nTwo queues are involved.\n\n#explain\nAfter every task, the event loop empties the microtask queue.\n::\n\n::poll{#next-lesson}\nWhat should the next lesson cover?\n\n- [ ] Web Workers\n- [ ] Streams\n::\n\n::recall{#why-microtasks}\nWhy does a promise callback run before a zero-delay timer?\n\n#answer\nPromise callbacks are microtasks, and a timer's callback is a task.\n::\n```\n\n| Block | Props and slots |\n| --- | --- |\n| `::question` | `#id`, `multiple`, `shuffle=\"false\"`, `number=\"false\"`, slots `#hint` and `#explain`. |\n| `::poll` | `#id`, `multiple`, `closes` (an ISO date; after it the poll shows as closed). Options are never shuffled. |\n| `::recall` | `#id`, `number=\"false\"`, and the required slot `#answer`. |\n\n- Mark a right option `[x]` and a wrong one `[ ]`. One right option makes radios and several make checkboxes; `multiple` forces checkboxes even with one. Mark no option in a poll.\n- Put a `>` line under an option to say why it is right or wrong. Give every wrong option a reason, since that is the lesson for a reader who picked it.\n- Leave a blank line before `#hint` and `#explain`. Without it, the text above swallows the slot, and the plugin reports it.\n- Give every block an `#id`. Hosts save answers by id. Without one the id derives from the prompt and options, changes when you reword them, and the plugin reports it. Keep the id when you reword the prompt.\n- Questions and recall cards share one count, so a lesson numbers them 1, 2, 3 down the page. Polls are not numbered.\n- Options are shuffled the same way for every reader, so do not write options that point at others by place, such as \"All of the above\" or \"Both A and B\". Keep the options about as long as each other, since a right option much longer than the rest is reported.\n- Keep a block to six options or fewer. Mark at least one right option in a question, or ask it as a poll. Put no block inside another block's prompt, option, or slot.\n\nThe HTML renderer inlines the quiz script and styles. Hosts can listen for `comark-quiz:answer` and `comark-quiz:ready`, and restore answers or results with `comark-quiz:state` (or `window.__comarkQuizState` set before the script runs). In an iframe, `data-quiz-relay` on the lesson's `<html>` relays all three over postMessage to the parent; see [Saving answers](https://comark-ui.onmax.me/quiz/#saving-answers) on its page for the event shapes and storage behavior.\n\n## Register\n\n- In `comark-ui`: on, as `quiz`, with optional `{ labels }` for translated UI text.\n- Alone: `pnpm add comark @comark/html comark-quiz`, then `createHtmlRenderer({ plugins: [quiz()], components })` with `quiz, { components }` from `comark-quiz/html`. Use the HTML renderer, since the blocks need its inlined script; there are no Vue, React, or Svelte entries.\n"
202
218
  },
203
219
  {
204
220
  "path": "references/reading-time.md",
@@ -206,7 +222,7 @@ const skills = [
206
222
  },
207
223
  {
208
224
  "path": "references/remark-to-comark.md",
209
- "text": "# Moving a project from remark\n\nremark-to-comark moves a project that renders Markdown with remark, rehype, unified, or react-markdown to Comark. It reports what it finds as diagnostics, `RTC0101` to `RTC0303`, each with a fix and a link to https://comark-ui.onmax.me/remark-to-comark/diagnostics/.\n\n## Run it\n\n```sh\nvp exec remark-to-comark check . --json\nvp exec remark-to-comark content \"**/*.{md,mdx}\" --write\nvp exec remark-to-comark rewrite . --write\nvp exec remark-to-comark check . --json\n```\n\n- `check` lists the remark setups it finds and what each plugin becomes, the unified chains `rewrite` can replace and why the others stay, the Markdown files `content` would change, and the MDX it leaves alone. `--json` prints each finding with its code, file, line, fix, and docs link.\n- `rewrite` replaces a chain like `unified().use(remarkParse).use(remarkRehype).use(rehypeStringify).process(markdown)` with `renderHtml(markdown, comarkOptions({ … }))`, importing `comarkOptions` from `remark-to-comark/setup`. Install what the new code imports: `comark`, `@comark/html`, and `remark-to-comark`.\n- `content` escapes `{`, `[`, and `:name` that Comark would read as attributes, spans, or components, so remark still renders every file byte for byte as before. It parses with the project's syntax plugins, so `$x_{i}$` stays math. Run it before `rewrite`, which removes the remark code it reads them from.\n- Run `check` again at the end. What's left is work for you: framework configs (RTC0101), the project's own plugins (RTC0105), chains it couldn't rewrite (RTC0201), MDX (RTC0301), and blocks left as written (RTC0303).\n\n## What it leaves to you\n\n- A framework config, such as Astro's `markdown.remarkPlugins` or Docusaurus' `remarkPlugins`, stays as it is: the framework calls remark. Render with `renderHtml(markdown, comarkOptions())` where the framework lets you, such as a content loader or a route.\n- react-markdown becomes `<Markdown value={markdown} options={…} plugins={[…]} />` from `@comark/react`, with the plugins the findings name.\n- A plugin a comark-ui package replaces is reported with that package (RTC0102); its markup differs from the remark plugin's, so check pages that style it.\n- MDX stays MDX. Convert a page by hand only when its JSX just passes props: rename it to `.md`, run `content` on the prose, then write the components as Comark blocks. `content` escapes Comark syntax, so don't run it after you add them.\n\n## Register\n\n- In `comark-ui`: not included. Install it beside the preset for the migration.\n- Alone: `pnpm add -D remark-to-comark`. It installs the command `remark-to-comark`, the library `remark-to-comark`, the Comark setup `remark-to-comark/setup` (add it to your runtime dependencies when rewritten code imports it), the ports `remark-to-comark/plugins`, and the Vite Doctor extension `remark-to-comark/doctor`.\n"
225
+ "text": "# Moving a project from remark\n\nremark-to-comark moves a project that renders Markdown with remark, rehype, unified, or react-markdown to Comark. It reports what it finds as diagnostics, `RTC0101` to `RTC0303`, each with a fix and a link to https://comark-ui.onmax.me/remark-to-comark/diagnostics/.\n\n## Run it\n\n```sh\nvp exec remark-to-comark migrate\n```\n\n`migrate` runs `content --write`, then `rewrite --write`, then `check`, on a clean git tree (`--force` skips that check). To review each step first, run them yourself:\n\n```sh\nvp exec remark-to-comark check --json\nvp exec remark-to-comark content \"**/*.{md,mdx}\" --write\nvp exec remark-to-comark rewrite --write\nvp exec remark-to-comark check --json\n```\n\n- `check` lists the remark setups it finds and what each plugin becomes, the unified chains `rewrite` can replace and why the others stay, the Markdown files `content` would change, and the MDX it leaves alone. `--json` prints each finding with its code, file, line, fix, and docs link.\n- `rewrite` replaces a chain like `unified().use(remarkParse).use(remarkRehype).use(rehypeStringify).process(markdown)` with `renderHtml(markdown, comarkOptions({ … }))`, importing `comarkOptions` from `remark-to-comark/setup`. Install what the new code imports: `comark`, `@comark/html`, and `remark-to-comark`.\n- `content` escapes `{`, `[`, and `:name` that Comark would read as attributes, spans, or components, so remark still renders every file byte for byte as before. It parses with the project's syntax plugins, so `$x_{i}$` stays math. Run it before `rewrite`, which removes the remark code it reads them from.\n- Run `check` again at the end. What's left is work for you: framework configs (RTC0101), the project's own plugins (RTC0105), chains it couldn't rewrite (RTC0201), MDX (RTC0301), and blocks left as written (RTC0303).\n\n## What it leaves to you\n\n- A framework config, such as Astro's `markdown.remarkPlugins` or Docusaurus' `remarkPlugins`, stays as it is: the framework calls remark. Render with `renderHtml(markdown, comarkOptions())` where the framework lets you, such as a content loader or a route.\n- react-markdown becomes `<Markdown value={markdown} options={…} plugins={[…]} />` from `@comark/react`, with the plugins the findings name.\n- A plugin a comark-ui package replaces is reported with that package (RTC0102); its markup differs from the remark plugin's, so check pages that style it.\n- MDX stays MDX. Convert a page by hand only when its JSX just passes props: rename it to `.md`, run `content` on the prose, then write the components as Comark blocks. `content` escapes Comark syntax, so don't run it after you add them.\n\n## Register\n\n- In `comark-ui`: not included. Install it beside the preset for the migration.\n- Alone: `pnpm add -D remark-to-comark`. It installs the command `remark-to-comark`, the library `remark-to-comark`, the Comark setup `remark-to-comark/setup` (add it to your runtime dependencies when rewritten code imports it), the ports `remark-to-comark/plugins`, and the Vite Doctor extension `remark-to-comark/doctor`.\n"
210
226
  },
211
227
  {
212
228
  "path": "references/sources.md",
@@ -222,7 +238,7 @@ const skills = [
222
238
  },
223
239
  {
224
240
  "path": "references/tabs.md",
225
- "text": "# Tabs and code groups\n\ncomark-tabs draws `::tabs` of any Markdown, `::code-group` of code blocks, and a tab per package manager for a code block marked `pm`. Use tabs for versions of one thing where each reader needs only one, such as an operating system, a framework, or a package manager.\n\n## Syntax\n\n````md\n::code-group{sync=\"pm\"}\n```sh [pnpm]\npnpm add comark-tabs\n```\n\n```sh [npm]\nnpm install comark-tabs\n```\n::\n\n::tabs{sync=\"os\"}\n:::tabs-item{label=\"macOS\"}\nPress **⌘K** to find the pigeon data.\n:::\n\n:::tabs-item{label=\"Windows\"}\nPress **Ctrl+K** to find the pigeon data.\n:::\n::\n````\n\n- `::tabs`: each `:::tabs-item` inside is one tab. Content outside every tab doesn't render, and is reported.\n- `:::tabs-item`: `label` is required and must differ from the other labels in its block, since `sync` matches tabs by label. `icon` draws an Iconify name, such as `lucide:apple`, or an image URL before the label.\n- `::code-group`: only code blocks go inside. Name each one's tab in brackets after its language, as in `sh [pnpm]`, with an optional icon after it, as in `sh [pnpm] icon=logos:pnpm`. The tab replaces the block's own title. `title=\"pnpm\"` names no tab, so a block without brackets is reported.\n- `sync`, on either block, is a key shared by blocks that switch together. Picking a tab picks the tab with the same label in every block with that key, and the next page that uses the key remembers it.\n- A code block marked `pm`, as in `sh pm`, holds package manager commands in any manager (npm, npx, pnpm, pnpx, yarn, bun, bunx, or deno) and becomes a code group with a tab each for npm, pnpm, yarn, and bun, synced as `pm`. The manager it's written in keeps its commands as written. It reads install, frozen install (`npm ci`), add with `-D`, `-E`, `-O`, `--save-peer`, or `-g`, remove, run, `npx`/`dlx`, create, and init. An update or a `pnpm exec` stays only in its own manager's tab, and the others are left out and reported. Only commands at the start of a line or after `&&`, `||`, or `;` change. Inside a `::code-group`, `pm` does nothing.\n\n## Choose the block\n\n- `::code-group` when every tab is one code block, such as one config file per framework. Give every install snippet `sync=\"pm\"`, the key `pm` blocks use, so the reader picks a package manager once.\n- `pm` for package manager commands, so you write them once in the manager you use. A command with no form in a manager, such as `npm install -g` in yarn, or one it doesn't read, such as `npm publish`, leaves those tabs out and is reported. Write that block as a `::code-group{sync=\"pm\"}` by hand.\n- `::tabs` when a tab holds prose, images, or several blocks.\n\nPut only alternatives in tabs. Anything every reader needs goes outside them, since a reader sees one tab at a time. For a before and after, use annotate's `::compare` or comark-diff's `::diff`, which show both sides at once.\n\n## Register\n\n- In `comark-ui`: on, as `tabs`. `tabs: { managers }` sets the tabs a `pm` block gets, in order. The default is `[\"npm\", \"pnpm\", \"yarn\", \"bun\"]`, and `\"deno\"` can join. `tabs: { icons }` gives each tab without an icon one by its label, such as `{ pnpm: \"logos:pnpm\" }`. Icons resolve through the preset's `icon` options, from the Iconify API unless its `collections` hold the set.\n- Alone: `pnpm add comark @comark/html comark-tabs`, then `createHtmlRenderer({ plugins: [tabs()], components })` with both from `comark-tabs/html`. `comark-tabs/vue`, `/react`, and `/svelte` (Svelte 5.20 or later) export the same pair for those renderers. It takes `managers`, `icons`, and `iconResolver`, a `createIcons()` from comark-icon to share with `:icon`. List it before Shiki, so Shiki highlights the tabs `pm` adds.\n"
241
+ "text": "# Tabs and code groups\n\ncomark-tabs draws `::tabs` of any Markdown, `::code-group` of code blocks, and a tab per package manager for a code block marked `pm`. Use tabs for versions of one thing where each reader needs only one, such as an operating system, a framework, or a package manager.\n\n## Syntax\n\n````md\n::code-group{sync=\"pm\"}\n```sh [pnpm]\npnpm add comark-tabs\n```\n\n```sh [npm]\nnpm install comark-tabs\n```\n::\n\n::tabs{sync=\"os\"}\n:::tabs-item{label=\"macOS\"}\nPress **⌘K** to find the pigeon data.\n:::\n\n:::tabs-item{label=\"Windows\"}\nPress **Ctrl+K** to find the pigeon data.\n:::\n::\n````\n\n- `::tabs`: each `:::tabs-item` inside is one tab. Content outside every tab doesn't render, and is reported.\n- `:::tabs-item`: `label` is required and must differ from the other labels in its block, since `sync` matches tabs by label. `icon` draws an Iconify name, such as `lucide:apple`, or an image URL before the label.\n- `::code-group`: only code blocks go inside. Name each one's tab in brackets after its language, as in `sh [pnpm]`, with an optional icon after it, as in `sh [pnpm] icon=logos:pnpm`. The tab replaces the block's own title. `title=\"pnpm\"` names no tab, so a block without brackets is reported.\n- A code group animates a switch between two snippets that share at least half their tokens, and swaps unlike ones at once. `magic` on `::code-group` forces the animation on, and `magic=\"false\"` turns it off. Leave it out unless the default is wrong for that group.\n- `sync`, on either block, is a key shared by blocks that switch together. Picking a tab picks the tab with the same label in every block with that key, and the next page that uses the key remembers it.\n- A code block marked `pm`, as in `sh pm`, holds package manager commands in any manager (npm, npx, pnpm, pnpx, yarn, bun, bunx, or deno) and becomes a code group with a tab each for npm, pnpm, yarn, and bun, synced as `pm`. The manager it's written in keeps its commands as written. It reads install, frozen install (`npm ci`), add with `-D`, `-E`, `-O`, `--save-peer`, or `-g`, remove, run, `npx`/`dlx`, create, and init. An update or a `pnpm exec` stays only in its own manager's tab, and the others are left out and reported. Only commands at the start of a line or after `&&`, `||`, or `;` change. Inside a `::code-group`, `pm` does nothing.\n\n## Choose the block\n\n- `::code-group` when every tab is one code block, such as one config file per framework. Give every install snippet `sync=\"pm\"`, the key `pm` blocks use, so the reader picks a package manager once.\n- `pm` for package manager commands, so you write them once in the manager you use. A command with no form in a manager, such as `npm install -g` in yarn, or one it doesn't read, such as `npm publish`, leaves those tabs out and is reported. Write that block as a `::code-group{sync=\"pm\"}` by hand.\n- `::tabs` when a tab holds prose, images, or several blocks.\n\nPut only alternatives in tabs. Anything every reader needs goes outside them, since a reader sees one tab at a time. For a before and after, use annotate's `::compare` or comark-diff's `::diff`, which show both sides at once.\n\n## Register\n\n- In `comark-ui`: on, as `tabs`. `tabs: { managers }` sets the tabs a `pm` block gets, in order. The default is `[\"npm\", \"pnpm\", \"yarn\", \"bun\"]`, and `\"deno\"` can join. `tabs: { icons }` gives each tab without an icon one by its label, such as `{ pnpm: \"logos:pnpm\" }`. Icons resolve through the preset's `icon` options, from the Iconify API unless its `collections` hold the set.\n- Alone: `pnpm add comark @comark/html comark-tabs`, then `createHtmlRenderer({ plugins: [tabs()], components })` with both from `comark-tabs/html`. `comark-tabs/vue`, `/react`, and `/svelte` (Svelte 5.20 or later) export the same pair for those renderers. It takes `managers`, `icons`, and `iconResolver`, a `createIcons()` from comark-icon to share with `:icon`. List it before Shiki, so Shiki highlights the tabs `pm` adds.\n"
226
242
  },
227
243
  {
228
244
  "path": "references/tests.md",
@@ -230,7 +246,7 @@ const skills = [
230
246
  },
231
247
  {
232
248
  "path": "references/ui.md",
233
- "text": "# Every plugin in one install\n\ncomark-ui registers every comark-ui package for one renderer, in the order they need, with one icon resolver shared by `:icon`, cards, details, file trees, tabs, and code actions. SKILL.md's Install section shows the basic use. This page covers the options, what the preset turns on, the other renderers, and how the linter uses it.\n\n## Options\n\n`comarkUi(options)` returns `{ plugins, components }`. Each key is a package's name without `comark-`, in camelCase, such as `fileTree`, `codeNotes`, or `readingTime`. The exceptions are `math` for comark-mathml, `outline` for comark-outline, and `mermaid` and `typeset`, which are not packages. `false` turns a plugin off along with its components, and an object turns it on with those options. `true` turns on `include` and leaves the others as they are:\n\n```ts\ncomarkUi({\n zoom: false,\n code: { actions: [{ id: \"run\", label: \"Run\" }] },\n github: { callouts: false, references: { repo: \"owner/name\" } },\n table: { root: \"data\" },\n include: { root: \"content\", base: \"guide\" },\n});\n```\n\n`github` takes its three plugins by name, `callouts`, `references`, and `html`, and `links` takes `external` and `anchors`. Each package's reference lists its own options. The order stays fixed whatever you pass: `include` runs first, so the others read what it pastes, and `code` and `codeNotes` run late, after every plugin that writes code blocks.\n\n## What it turns on\n\n- Every plugin except `include`, which reads files from disk. `include: { root, base }` turns it on, or `{ read }` in a browser or an edge runtime. `include: true` reads under the working directory.\n- `cast`, `table`, and `tests` get a reader that finds no file. Inline casts, CSV fences, and pasted JUnit reports render, and a `src` file doesn't load until you pass `{ root }` or `{ read }`. Any other options object replaces that reader too, so the plugin reads under the working directory.\n- Three plugins use the network while parsing by default: `chips` downloads favicons and avatars, `icon` fetches icon sets from the Iconify API, and `diagrams` sends each diagram's source to `https://kroki.io`. In a browser, pass `chips: { download: false }`, because a browser may not download another site's images. To parse offline, also pass `icon: { fetch: false }` with your icon sets in `collections`, and `diagrams: { draw: false }`. To keep diagram sources private, pass your own Kroki `server`.\n- Media sizing reads only data URIs by default. `zoom: { size: true }` and `media: { size: true }` also fetch remote files while parsing, and `annotate: { size: true }` does so at HTML render time. Enable these only for trusted documents, because the requests follow redirects and can reach private networks. `{ size: false }` turns measurement off for that plugin; authored dimensions and screenshot viewports still work.\n- `icon`'s options apply to every icon the preset draws, in cards, details, file trees, tabs, and code actions too.\n- `mermaid` draws `mermaid` fences with comark-pele, on the server and offline. In the HTML entry the diagram zooms with comark-zoom unless `zoom` is `false`. `mermaid: false` turns it off.\n- comark-critic has no component to add its styles, so put `criticCss`, which every entry exports, in the page.\n- The preset styles the text around the blocks, scoped to the element the document renders into, on the same `--comark-*` tokens. Pass `typeset: false` when the site already styles Markdown, such as with Tailwind Typography's `prose`.\n\n## Renderers\n\n| Renderer | Entry |\n| --- | --- |\n| `@comark/html` | `comark-ui/html` |\n| `@comark/vue` | `comark-ui/vue` |\n| `@comark/react` | `comark-ui/react` |\n| `@comark/svelte` | `comark-ui/svelte` |\n\nThe Vue, React, and Svelte entries leave out annotate, comments, diff, and quiz, which render only to HTML. Screenshots, compares, diffs, suggestions and comments, and the marks on charts need `comark-ui/html`.\n\nTo use another renderer, install it in place of `@comark/html`, such as `pnpm add comark @comark/vue comark-ui`, and pass it `plugins` and `components` as you would one package's. The preset needs Vue 3.5, React 18, or Svelte 5.20 or later. Every entry exports the `PresetOptions` type, and `comark-ui` itself exports `preset(renderer, options)`, which builds the same pair from renderer entries you pass.\n\n## In the linter\n\ncomark-linter imports `comark-ui/html` when the project has it, and lints with all its plugins and components, ahead of any comark package installed on its own, so none runs twice. It calls `comarkUi()` with options of its own, so the app's options don't reach it:\n\n- It draws no chart or diagram, and downloads no chip image.\n- `::cast`, `::table`, and `::tests` read their `src` files from the working directory.\n- `include` stays off until a lint config adds it, as [include](include.md) says.\n\n## Register\n\n- In `comark-ui`: this is the preset itself, registered as Install in SKILL.md shows.\n- Alone: each plugin is its own package, registered as its reference says. The preset ships in lockstep with them, so `comark-ui@X` holds every plugin at version `X`.\n"
249
+ "text": "# Every plugin in one install\n\ncomark-ui registers every comark-ui package for one renderer, in the order they need, with one icon resolver shared by `:icon`, cards, details, file trees, tabs, and code actions. SKILL.md's Install section shows the basic use. This page covers the options, what the preset turns on, the other renderers, and how the linter uses it.\n\n## Options\n\n`comarkUi(options)` returns `{ plugins, components }`. Each key is a package's name without `comark-`, in camelCase, such as `fileTree`, `codeNotes`, or `readingTime`. The exceptions are `math` for comark-mathml, `outline` for comark-outline, and `mermaid`, `shiki`, and `typeset`, which are not packages. `false` turns a plugin off along with its components, and an object turns it on with those options. `true` turns on `include` and leaves the others as they are:\n\n```ts\ncomarkUi({\n zoom: false,\n code: { actions: [{ id: \"run\", label: \"Run\" }] },\n github: { callouts: false, references: { repo: \"owner/name\" } },\n table: { root: \"data\" },\n include: { root: \"content\", base: \"guide\" },\n});\n```\n\n`github` takes its three plugins by name, `callouts`, `references`, and `html`, and `links` takes `external` and `anchors`. Each package's reference lists its own options. The order stays fixed whatever you pass: `include` runs first, so the others read what it pastes, and `code` and `codeNotes` run late, after every plugin that writes code blocks.\n\n## What it turns on\n\n- Every plugin except `include`, which reads files from disk. `include: { root, base }` turns it on, or `{ read }` in a browser or an edge runtime. `include: true` reads under the working directory.\n- `cast`, `table`, and `tests` get a reader that finds no file. Inline casts, CSV fences, and pasted JUnit reports render, and a `src` file doesn't load until you pass `{ root }` or `{ read }`. Any other options object replaces that reader too, so the plugin reads under the working directory.\n- Three plugins use the network while parsing by default: `chips` downloads favicons and avatars, `icon` fetches icon sets from the Iconify API, and `diagrams` sends each diagram's source to `https://kroki.io`. In a browser, pass `chips: { download: false }`, because a browser may not download another site's images. To parse offline, also pass `icon: { fetch: false }` with your icon sets in `collections`, and `diagrams: { draw: false }`. To keep diagram sources private, pass your own Kroki `server`.\n- Media sizing reads only data URIs by default. `zoom: { size: true }` and `media: { size: true }` also fetch remote files while parsing, and `annotate: { size: true }` does so at HTML render time. Enable these only for trusted documents, because the requests follow redirects and can reach private networks. `{ size: false }` turns measurement off for that plugin; authored dimensions and screenshot viewports still work.\n- `icon`'s options apply to every icon the preset draws, in cards, details, file trees, tabs, and code actions too.\n- `mermaid` draws `mermaid` fences with comark-pele, on the server and offline. In the HTML entry the diagram zooms with comark-zoom unless `zoom` is `false`. `mermaid: false` turns it off.\n- `shiki` highlights fenced code with your own Shiki, before comark-code: `{ highlighter: (language) => shiki | undefined, themes: { light, dark } }`. It is off until you pass it, so the preset bundles no grammars. `maxBlocks` and `maxSource` cap how many blocks, and how many characters of code, a document highlights; the blocks past them show plain, each with an issue.\n- comark-critic has no component to add its styles, so put `criticCss`, which every entry exports, in the page.\n- The preset styles the text around the blocks, scoped to the element the document renders into, on the same `--comark-*` tokens. Pass `typeset: false` when the site already styles Markdown, such as with Tailwind Typography's `prose`.\n\n## Renderers\n\n| Renderer | Entry |\n| --- | --- |\n| `@comark/html` | `comark-ui/html` |\n| `@comark/vue` | `comark-ui/vue` |\n| `@comark/react` | `comark-ui/react` |\n| `@comark/svelte` | `comark-ui/svelte` |\n\nThe Vue, React, and Svelte entries leave out annotate, comments, diff, and quiz, which render only to HTML. Screenshots, compares, diffs, suggestions and comments, and the marks on charts need `comark-ui/html`.\n\nTo use another renderer, install it in place of `@comark/html`, such as `pnpm add comark @comark/vue comark-ui`, and pass it `plugins` and `components` as you would one package's. The preset needs Vue 3.5, React 18, or Svelte 5.20 or later. Every entry exports the `PresetOptions` type, and `comark-ui` itself exports `preset(renderer, options)`, which builds the same pair from renderer entries you pass.\n\n## In the linter\n\ncomark-linter imports `comark-ui/html` when the project has it, and lints with all its plugins and components, ahead of any comark package installed on its own, so none runs twice. It calls `comarkUi()` with options of its own, so the app's options don't reach it:\n\n- It draws no chart or diagram, and downloads no chip image.\n- `::cast`, `::table`, and `::tests` read their `src` files from the working directory.\n- `include` stays off until a lint config adds it, as [include](include.md) says.\n\n## Register\n\n- In `comark-ui`: this is the preset itself, registered as Install in SKILL.md shows.\n- Alone: each plugin is its own package, registered as its reference says. The preset ships in lockstep with them, so `comark-ui@X` holds every plugin at version `X`.\n"
234
250
  },
235
251
  {
236
252
  "path": "references/xref.md",
@@ -325,6 +341,6 @@ const skills = [
325
341
  }
326
342
  ];
327
343
  /** The comark-ui version the skills come from, for a host's cache key or a digest. */
328
- const version = "0.8.2";
344
+ const version = "0.9.1";
329
345
  //#endregion
330
346
  export { skills, version };
@@ -56,6 +56,80 @@ var mermaid_limits_default = defineComarkPlugin(({ maxDiagrams = Infinity, maxSo
56
56
  }
57
57
  }));
58
58
  //#endregion
59
+ //#region src/shiki.ts
60
+ function blocksOf(nodes, found = []) {
61
+ for (const node of nodes) {
62
+ if (!isElement(node)) continue;
63
+ if (node[0] === "pre" && node[1].$?.html !== 1) found.push(node);
64
+ else blocksOf(childrenOf(node), found);
65
+ }
66
+ return found;
67
+ }
68
+ const spanOf = (token) => {
69
+ const style = Object.entries(token.htmlStyle ?? (token.color ? { color: token.color } : {})).map(([name, value]) => `${name}:${value}`).join(";");
70
+ return style ? [
71
+ "span",
72
+ { style },
73
+ token.content
74
+ ] : token.content;
75
+ };
76
+ /**
77
+ * Highlights fenced code with the host's own Shiki, before comark-code reads the lines, within a document's limits, so
78
+ * a host that renders other people's documents bounds the time Shiki spends. Each block past them is a `meta.issues`
79
+ * entry. A grammar that fails leaves its block plain.
80
+ */
81
+ var shiki_default = defineComarkPlugin((options) => ({
82
+ name: "comark-ui-shiki",
83
+ async post({ tree }) {
84
+ const { highlighter, themes, maxBlocks = Infinity, maxSource = Infinity } = options;
85
+ const issues = [];
86
+ let drawn = 0;
87
+ let source = 0;
88
+ for (const [n, pre] of blocksOf(tree.nodes).entries()) {
89
+ const code = childrenOf(pre)[0];
90
+ const language = pre[1].language;
91
+ if (!language || !isElement(code) || code[0] !== "code") continue;
92
+ const [text, ...rest] = childrenOf(code);
93
+ if (text === void 0 || Array.isArray(text) || rest.length) continue;
94
+ const why = drawn >= maxBlocks ? ["comark/shiki/too-many-blocks", `a document highlights at most ${maxBlocks} code blocks. Move it to another document`] : source >= maxSource || source + text.length > maxSource ? ["comark/shiki/source-too-long", `a document highlights at most ${maxSource} characters of code. Shorten it or move it to another document`] : void 0;
95
+ if (why) {
96
+ issues.push([why[0], `Code block ${n + 1} shows without colors, because ${why[1]}.`]);
97
+ continue;
98
+ }
99
+ let shiki;
100
+ try {
101
+ shiki = await highlighter(language);
102
+ } catch {}
103
+ if (!shiki) continue;
104
+ let tokens;
105
+ try {
106
+ tokens = shiki.codeToTokens(text, {
107
+ lang: language,
108
+ themes
109
+ }).tokens;
110
+ } catch {
111
+ continue;
112
+ }
113
+ drawn++;
114
+ source += text.length;
115
+ const lines = tokens.flatMap((line, index) => [...index ? ["\n"] : [], [
116
+ "span",
117
+ { class: "line" },
118
+ ...line.map(spanOf)
119
+ ]]);
120
+ code.length = 2;
121
+ for (const line of lines) code.push(line);
122
+ pre[1].class = ["shiki", pre[1].class].filter(Boolean).join(" ");
123
+ }
124
+ if (!issues.length) return;
125
+ tree.meta.issues = [...tree.meta.issues ?? [], ...issues.map(([, message]) => message)];
126
+ tree.meta.issueRules = {
127
+ ...tree.meta.issueRules,
128
+ ...Object.fromEntries(issues.map(([rule, message]) => [message, rule]))
129
+ };
130
+ }
131
+ }));
132
+ //#endregion
59
133
  //#region src/typeset.ts
60
134
  /** The node that adds the styles. The plugin adds one at the end of every document. */
61
135
  const TYPESET_TAG = "typeset-styles";
@@ -183,6 +257,7 @@ function preset(renderer, options = {}) {
183
257
  parse(xref, options.xref);
184
258
  add(renderer.tests, options.tests, optionsOf(options.tests) ?? noFiles);
185
259
  add(renderer.zoom, options.zoom);
260
+ if (options.shiki) plugins.push(shiki_default(options.shiki));
186
261
  add(renderer.code, options.code, {
187
262
  iconResolver: icons,
188
263
  ...optionsOf(options.code)
@@ -194,8 +194,10 @@ const rules = `
194
194
  into the room the page gives it on each side, --comark-wide-start and --comark-wide-end, up to its own width
195
195
  (--comark-wide-width) and --comark-wide-max. It stays centered on the column while both sides have room, and moves
196
196
  toward the side that has more when one doesn't. Without room set, it keeps to the column. Only the document's own
197
- blocks grow, not one inside a list or tabs. */
198
- :scope > [data-wide] {
197
+ blocks grow, not one inside a list or tabs. A heading right above a comparison grows with it, so it starts where
198
+ Before does and reads as its title. comark-links may wrap the heading, and the comparison's styles may sit between them. */
199
+ :scope > [data-wide],
200
+ :scope > :is(h2, h3, h4, .cl-heading:has(> :is(h2, h3, h4))):has(+ .ca-compare, + style + .ca-compare) {
199
201
  /* Room a page works out from the window can come out negative on a phone, where the column fills it: none, then. */
200
202
  --_ty-start: max(0px, var(--comark-wide-start, 0px));
201
203
  --_ty-end: max(0px, var(--comark-wide-end, 0px));
package/dist/typeset.css CHANGED
@@ -1 +1 @@
1
- @layer comark{@layer typeset{@scope (.comark-typeset){:where(:scope){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#fff)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#0a0a0a)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#737373)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,#e5e5e5)));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#f5f5f5)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#a1a1a1)));--_comark-primary:var(--comark-primary,var(--ui-primary,var(--primary,var(--_comark-foreground))));--_comark-primary-foreground:var(--comark-primary-foreground,var(--ui-text-inverted,var(--primary-foreground,var(--_comark-background))));--_comark-secondary:var(--comark-secondary,var(--ui-secondary,var(--_comark-muted)));--_comark-radius:var(--comark-radius,calc(var(--ui-radius,calc(var(--radius,.625rem)/2))*2));--_comark-font:var(--comark-font,var(--font-sans,ui-sans-serif,system-ui,sans-serif))}@media(prefers-color-scheme:dark){:where(:scope){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#0a0a0a)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#fafafa)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#a1a1a1)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,rgb(255 255 255/.1))));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#262626)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#737373)));--_comark-primary:var(--comark-primary,var(--ui-primary,var(--primary,var(--_comark-foreground))));--_comark-primary-foreground:var(--comark-primary-foreground,var(--ui-text-inverted,var(--primary-foreground,var(--_comark-background))));--_comark-secondary:var(--comark-secondary,var(--ui-secondary,var(--_comark-muted)))}}:scope{--_ty-flow:var(--typeset-flow,1.25em);--_ty-muted:var(--_comark-muted);--_ty-rule:var(--_comark-border);--_ty-surface:var(--_comark-accent);--_ty-radius:calc(var(--_comark-radius) * 0.8);--_ty-mono:var(--comark-font-mono,var(--font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace));font-family:var(--_comark-font);font-size:calc(var(--typeset-size,1em) * 1.125);line-height:var(--typeset-leading,1.75);color:var(--_comark-foreground);overflow-wrap:break-word;background-image:var(--comark-grain,url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E"));background-repeat:repeat;background-size:var(--comark-grain-size,256px 256px);@media(min-width:48rem),print{font-size:var(--typeset-size,1em);}}:scope *:not(:where(.not-typeset,[data-not-typeset],.not-typeset *,[data-not-typeset] *)){&:where(p){margin-block:var(--_ty-flow) 0;}&:where(h1,h2,h3,h4,h5,h6){color:var(--_comark-foreground);font-family:var(--comark-font-heading,var(--font-heading,inherit));font-weight:600;margin-block-end:0;}&:where(h1){font-size:1.75em;line-height:1.3;margin-block-start:var(--_ty-flow);}&:where(h2){font-size:1.25em;line-height:1.4;margin-block-start:calc(var(--_ty-flow) * 1.4);}&:where(h3){font-size:1.125em;line-height:1.45;margin-block-start:var(--_ty-flow);}&:where(h4){font-size:1em;line-height:1.5;margin-block-start:var(--_ty-flow);}&:where(h5){font-size:0.875em;line-height:1.5;font-weight:500;color:var(--_ty-muted);margin-block-start:calc(var(--_ty-flow) / 0.875);}&:where(h6){font-size:0.8125em;line-height:1.5;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--_ty-muted);margin-block-start:calc(var(--_ty-flow) / 0.8125);}&:where([id]){scroll-margin-block-start:var(--_ty-flow);}&:where(h1 + *,h2 + *,h3 + *,h4 + *,h5 + *,h6 + *){margin-block-start:1em;}&:where(:is(h1,h2,h3,h4) code){font-size:0.9em;}&:where(a){color:inherit;font-weight:500;text-decoration-line:underline;text-decoration-color:color-mix(in oklab,currentColor 30%,transparent);}&:where(a:hover){text-decoration-color:currentColor;}&:where(a:focus-visible){outline:2px solid var(--_comark-ring);outline-offset:2px;border-radius:0.125em;}&:where(:is(h1,h2,h3,h4,h5,h6) a){font-weight:inherit;}&:where(strong,b){font-weight:600;}&:where(mark){background-color:color-mix(in oklab,oklch(0.86 0.17 95) 40%,transparent);color:inherit;padding:0.05em 0.15em;border-radius:0.2em;}&:where(abbr[title]){text-decoration-line:underline;text-decoration-style:dotted;text-underline-offset:0.2em;cursor:help;}&:where(del,s){color:var(--_ty-muted);text-decoration-line:line-through;}&:where(sup,sub){font-size:0.75em;line-height:0;position:relative;vertical-align:baseline;}&:where(sup){top:-0.5em;}&:where(sub){bottom:-0.25em;}&:where(sup a){text-decoration-line:none;font-weight:500;}&:where(ul){list-style-type:disc;}&:where(ol){list-style-type:decimal;}&:where(ul ul){list-style-type:circle;}&:where(ul ul ul){list-style-type:square;}&:where(ol[type="a" i]){list-style-type:lower-alpha;}&:where(ol[type="i" i]){list-style-type:lower-roman;}&:where(ol[type="A" s]){list-style-type:upper-alpha;}&:where(ol[type="I" s]){list-style-type:upper-roman;}&:where(ul,ol){margin-block:var(--_ty-flow) 0;padding-inline-start:1.5em;}&:where(li){margin-block-start:0.5em;padding-inline-start:0.4em;}&:where(li>p,li>ul,li>ol){margin-block-start:0.5em;}&:where(ul>li,ol>li)::marker{color:var(--_ty-muted);}&:where(ul.contains-task-list){list-style-type:none;padding-inline-start:0.25em;}&:where(li.task-list-item>input[type="checkbox"]){margin-inline-end:0.5em;vertical-align:-0.1em;accent-color:var(--_comark-primary);}&:where(details){margin-block:var(--_ty-flow) 0;}&:where(summary){cursor:pointer;font-weight:500;}&:where(summary)::marker{color:var(--_ty-muted);}&:where(kbd){font-family:inherit;font-size:0.85em;font-weight:500;border:1px solid var(--_ty-rule);border-block-end-width:2px;border-radius:0.35em;padding:0.0625em 0.35em;}&:where(dl){margin-block:var(--_ty-flow) 0;}&:where(dt){font-weight:500;margin-block-start:1em;}&:where(dt + dt){margin-block-start:0.25em;}&:where(dd){margin-block-start:0.25em;margin-inline-start:0;padding-inline-start:1em;color:var(--_ty-muted);}&:where(:not(pre)>code){background-color:var(--_ty-surface);font-family:var(--_ty-mono);font-size:0.85em;border-radius:0.35em;padding:0.125em 0.3em;}&:where(pre){background-color:var(--_ty-surface);font-family:var(--_ty-mono);font-size:0.875em;line-height:1.5;tab-size:2;direction:ltr;border-radius:var(--_ty-radius);padding:0.75em 1em;overflow-x:auto;margin-block:calc(var(--_ty-flow) / 0.875) 0;}&:where(pre code){background-color:transparent;font-family:inherit;font-size:inherit;padding:0;border-radius:0;}&:where(blockquote){border-inline-start:2px solid var(--_ty-rule);padding-inline-start:1em;margin-block:var(--_ty-flow) 0;margin-inline:0;}&:where(hr){border:0;border-block-start:1px solid var(--_ty-rule);margin-block:calc(var(--_ty-flow) * 2.4) 0;}&:where(hr + h1,hr + h2,hr + h3,hr + h4){margin-block-start:var(--_ty-flow);}&:where(.footnotes,[data-footnotes]){margin-block-start:calc(var(--_ty-flow) * 2);font-size:0.875em;color:var(--_ty-muted);}&:where(.footnotes>hr:first-child){margin-block-start:0;}&:where(.footnote-ref a,.footnote-backref){text-decoration-line:none;}&:where(math[display="block"]){margin-block:var(--_ty-flow) 0;overflow-x:auto;overflow-y:hidden;padding-block:0.25em;}&:where(img,video){border-radius:var(--_ty-radius);max-width:100%;height:auto;margin-block:var(--_ty-flow) 0;}&:where(:is(p,li,td,th,a) img){margin-block-start:0;}&:where(figure){margin-block:var(--_ty-flow) 0;margin-inline:0;}&:where(figcaption,caption){color:var(--_ty-muted);font-size:0.875em;text-align:center;margin-block-start:calc(0.75em / 0.875);}&:where(caption){caption-side:bottom;}&:where(table){display:block;width:max-content;max-width:100%;overflow-x:auto;font-size:1em;line-height:1.5;font-variant-numeric:tabular-nums;border-collapse:separate;border-spacing:0;border-block-end:1px solid var(--_ty-rule);margin-block:var(--_ty-flow) 0;}&:where(thead th){font-weight:500;text-align:start;padding:0.65em 1em;}&:where(:is(tbody,tfoot) :is(td,th)){padding:0.75em 1em;text-align:start;vertical-align:top;border-block-start:1px solid var(--_ty-rule);}&:where(tbody th,tfoot th){font-weight:500;}&:where(th:first-child,td:first-child){padding-inline-start:0;}&:where(th[align="center"],td[align="center"]){text-align:center;}&:where(th[align="right"],td[align="right"]){text-align:end;}&:where(li>blockquote,li>table,li>figure){margin-block-start:0.5em;}&:where(li>pre){margin-block-start:calc(0.5em / 0.875);}@media print{&:where(pre,table,blockquote,figure){break-inside:avoid;}&:where(h1,h2,h3,h4){break-after:avoid;}}@media(forced-colors:active){&:where(:not(pre)>code){border:1px solid;}}}.shiki:where(.dark *) span{&[style*="--shiki-dark:"]{color:var(--shiki-dark) !important;}&[style*="--shiki-dark-font-style:"]{font-style:var(--shiki-dark-font-style) !important;}&[style*="--shiki-dark-font-weight:"]{font-weight:var(--shiki-dark-font-weight) !important;}}@media(prefers-color-scheme:dark){.shiki:not(:where(.light *)) span{&[style*="--shiki-dark:"]{color:var(--shiki-dark) !important;}&[style*="--shiki-dark-font-style:"]{font-style:var(--shiki-dark-font-style) !important;}&[style*="--shiki-dark-font-weight:"]{font-weight:var(--shiki-dark-font-weight) !important;}}}:scope>[data-wide]{--_ty-start:max(0px,var(--comark-wide-start,0px));--_ty-end:max(0px,var(--comark-wide-end,0px));--_ty-wide:max(100%,min(var(--comark-wide-width,100vw),var(--comark-wide-max,72rem),100% + var(--_ty-start) + var(--_ty-end)));width:var(--_ty-wide);margin-inline:clamp(-1 * var(--_ty-start),(100% - var(--_ty-wide)) / 2,100% + var(--_ty-end) - var(--_ty-wide)) 0;}:scope>:where(:first-child):not(:where(.not-typeset,[data-not-typeset],.not-typeset *,[data-not-typeset] *)),:scope>:where(:first-child)>:where(:first-child):not(:where(.not-typeset,[data-not-typeset],.not-typeset *,[data-not-typeset] *)),:scope :where(li>:first-child,blockquote>:first-child,td>:first-child,th>:first-child,dd>:first-child,figure>:first-child,figure>picture>img ):not(:where(.not-typeset,[data-not-typeset],.not-typeset *,[data-not-typeset] *)){margin-block-start:0;}}}}
1
+ @layer comark{@layer typeset{@scope (.comark-typeset){:where(:scope){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#fff)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#0a0a0a)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#737373)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,#e5e5e5)));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#f5f5f5)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#a1a1a1)));--_comark-primary:var(--comark-primary,var(--ui-primary,var(--primary,var(--_comark-foreground))));--_comark-primary-foreground:var(--comark-primary-foreground,var(--ui-text-inverted,var(--primary-foreground,var(--_comark-background))));--_comark-secondary:var(--comark-secondary,var(--ui-secondary,var(--_comark-muted)));--_comark-radius:var(--comark-radius,calc(var(--ui-radius,calc(var(--radius,.625rem)/2))*2));--_comark-font:var(--comark-font,var(--font-sans,ui-sans-serif,system-ui,sans-serif))}@media(prefers-color-scheme:dark){:where(:not(.light,.light *)):is(:where(:scope)){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#0a0a0a)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#fafafa)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#a1a1a1)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,rgb(255 255 255/.1))));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#262626)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#737373)));--_comark-primary:var(--comark-primary,var(--ui-primary,var(--primary,var(--_comark-foreground))));--_comark-primary-foreground:var(--comark-primary-foreground,var(--ui-text-inverted,var(--primary-foreground,var(--_comark-background))));--_comark-secondary:var(--comark-secondary,var(--ui-secondary,var(--_comark-muted)))}}:where(.dark,.dark *):is(:where(:scope)){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#0a0a0a)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#fafafa)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#a1a1a1)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,rgb(255 255 255/.1))));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#262626)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#737373)));--_comark-primary:var(--comark-primary,var(--ui-primary,var(--primary,var(--_comark-foreground))));--_comark-primary-foreground:var(--comark-primary-foreground,var(--ui-text-inverted,var(--primary-foreground,var(--_comark-background))));--_comark-secondary:var(--comark-secondary,var(--ui-secondary,var(--_comark-muted)))}:scope{--_ty-flow:var(--typeset-flow,1.25em);--_ty-muted:var(--_comark-muted);--_ty-rule:var(--_comark-border);--_ty-surface:var(--_comark-accent);--_ty-radius:calc(var(--_comark-radius) * 0.8);--_ty-mono:var(--comark-font-mono,var(--font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace));font-family:var(--_comark-font);font-size:calc(var(--typeset-size,1em) * 1.125);line-height:var(--typeset-leading,1.75);color:var(--_comark-foreground);overflow-wrap:break-word;background-image:var(--comark-grain,url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E"));background-repeat:repeat;background-size:var(--comark-grain-size,256px 256px);@media(min-width:48rem),print{font-size:var(--typeset-size,1em);}}:scope *:not(:where(.not-typeset,[data-not-typeset],.not-typeset *,[data-not-typeset] *)){&:where(p){margin-block:var(--_ty-flow) 0;}&:where(h1,h2,h3,h4,h5,h6){color:var(--_comark-foreground);font-family:var(--comark-font-heading,var(--font-heading,inherit));font-weight:600;margin-block-end:0;}&:where(h1){font-size:1.75em;line-height:1.3;margin-block-start:var(--_ty-flow);}&:where(h2){font-size:1.25em;line-height:1.4;margin-block-start:calc(var(--_ty-flow) * 1.4);}&:where(h3){font-size:1.125em;line-height:1.45;margin-block-start:var(--_ty-flow);}&:where(h4){font-size:1em;line-height:1.5;margin-block-start:var(--_ty-flow);}&:where(h5){font-size:0.875em;line-height:1.5;font-weight:500;color:var(--_ty-muted);margin-block-start:calc(var(--_ty-flow) / 0.875);}&:where(h6){font-size:0.8125em;line-height:1.5;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--_ty-muted);margin-block-start:calc(var(--_ty-flow) / 0.8125);}&:where([id]){scroll-margin-block-start:var(--_ty-flow);}&:where(h1 + *,h2 + *,h3 + *,h4 + *,h5 + *,h6 + *){margin-block-start:1em;}&:where(:is(h1,h2,h3,h4) code){font-size:0.9em;}&:where(a){color:inherit;font-weight:500;text-decoration-line:underline;text-decoration-color:color-mix(in oklab,currentColor 30%,transparent);}&:where(a:hover){text-decoration-color:currentColor;}&:where(a:focus-visible){outline:2px solid var(--_comark-ring);outline-offset:2px;border-radius:0.125em;}&:where(:is(h1,h2,h3,h4,h5,h6) a){font-weight:inherit;}&:where(strong,b){font-weight:600;}&:where(mark){background-color:color-mix(in oklab,oklch(0.86 0.17 95) 40%,transparent);color:inherit;padding:0.05em 0.15em;border-radius:0.2em;}&:where(abbr[title]){text-decoration-line:underline;text-decoration-style:dotted;text-underline-offset:0.2em;cursor:help;}&:where(del,s){color:var(--_ty-muted);text-decoration-line:line-through;}&:where(sup,sub){font-size:0.75em;line-height:0;position:relative;vertical-align:baseline;}&:where(sup){top:-0.5em;}&:where(sub){bottom:-0.25em;}&:where(sup a){text-decoration-line:none;font-weight:500;}&:where(ul){list-style-type:disc;}&:where(ol){list-style-type:decimal;}&:where(ul ul){list-style-type:circle;}&:where(ul ul ul){list-style-type:square;}&:where(ol[type="a" i]){list-style-type:lower-alpha;}&:where(ol[type="i" i]){list-style-type:lower-roman;}&:where(ol[type="A" s]){list-style-type:upper-alpha;}&:where(ol[type="I" s]){list-style-type:upper-roman;}&:where(ul,ol){margin-block:var(--_ty-flow) 0;padding-inline-start:1.5em;}&:where(li){margin-block-start:0.5em;padding-inline-start:0.4em;}&:where(li>p,li>ul,li>ol){margin-block-start:0.5em;}&:where(ul>li,ol>li)::marker{color:var(--_ty-muted);}&:where(ul.contains-task-list){list-style-type:none;padding-inline-start:0.25em;}&:where(li.task-list-item>input[type="checkbox"]){margin-inline-end:0.5em;vertical-align:-0.1em;accent-color:var(--_comark-primary);}&:where(details){margin-block:var(--_ty-flow) 0;}&:where(summary){cursor:pointer;font-weight:500;}&:where(summary)::marker{color:var(--_ty-muted);}&:where(kbd){font-family:inherit;font-size:0.85em;font-weight:500;border:1px solid var(--_ty-rule);border-block-end-width:2px;border-radius:0.35em;padding:0.0625em 0.35em;}&:where(dl){margin-block:var(--_ty-flow) 0;}&:where(dt){font-weight:500;margin-block-start:1em;}&:where(dt + dt){margin-block-start:0.25em;}&:where(dd){margin-block-start:0.25em;margin-inline-start:0;padding-inline-start:1em;color:var(--_ty-muted);}&:where(:not(pre)>code){background-color:var(--_ty-surface);font-family:var(--_ty-mono);font-size:0.85em;border-radius:0.35em;padding:0.125em 0.3em;}&:where(pre){background-color:var(--_ty-surface);font-family:var(--_ty-mono);font-size:0.875em;line-height:1.5;tab-size:2;direction:ltr;border-radius:var(--_ty-radius);padding:0.75em 1em;overflow-x:auto;margin-block:calc(var(--_ty-flow) / 0.875) 0;}&:where(pre code){background-color:transparent;font-family:inherit;font-size:inherit;padding:0;border-radius:0;}&:where(blockquote){border-inline-start:2px solid var(--_ty-rule);padding-inline-start:1em;margin-block:var(--_ty-flow) 0;margin-inline:0;}&:where(hr){border:0;border-block-start:1px solid var(--_ty-rule);margin-block:calc(var(--_ty-flow) * 2.4) 0;}&:where(hr + h1,hr + h2,hr + h3,hr + h4){margin-block-start:var(--_ty-flow);}&:where(.footnotes,[data-footnotes]){margin-block-start:calc(var(--_ty-flow) * 2);font-size:0.875em;color:var(--_ty-muted);}&:where(.footnotes>hr:first-child){margin-block-start:0;}&:where(.footnote-ref a,.footnote-backref){text-decoration-line:none;}&:where(math[display="block"]){margin-block:var(--_ty-flow) 0;overflow-x:auto;overflow-y:hidden;padding-block:0.25em;}&:where(img,video){border-radius:var(--_ty-radius);max-width:100%;height:auto;margin-block:var(--_ty-flow) 0;}&:where(:is(p,li,td,th,a) img){margin-block-start:0;}&:where(figure){margin-block:var(--_ty-flow) 0;margin-inline:0;}&:where(figcaption,caption){color:var(--_ty-muted);font-size:0.875em;text-align:center;margin-block-start:calc(0.75em / 0.875);}&:where(caption){caption-side:bottom;}&:where(table){display:block;width:max-content;max-width:100%;overflow-x:auto;font-size:1em;line-height:1.5;font-variant-numeric:tabular-nums;border-collapse:separate;border-spacing:0;border-block-end:1px solid var(--_ty-rule);margin-block:var(--_ty-flow) 0;}&:where(thead th){font-weight:500;text-align:start;padding:0.65em 1em;}&:where(:is(tbody,tfoot) :is(td,th)){padding:0.75em 1em;text-align:start;vertical-align:top;border-block-start:1px solid var(--_ty-rule);}&:where(tbody th,tfoot th){font-weight:500;}&:where(th:first-child,td:first-child){padding-inline-start:0;}&:where(th[align="center"],td[align="center"]){text-align:center;}&:where(th[align="right"],td[align="right"]){text-align:end;}&:where(li>blockquote,li>table,li>figure){margin-block-start:0.5em;}&:where(li>pre){margin-block-start:calc(0.5em / 0.875);}@media print{&:where(pre,table,blockquote,figure){break-inside:avoid;}&:where(h1,h2,h3,h4){break-after:avoid;}}@media(forced-colors:active){&:where(:not(pre)>code){border:1px solid;}}}.shiki:where(.dark *) span{&[style*="--shiki-dark:"]{color:var(--shiki-dark) !important;}&[style*="--shiki-dark-font-style:"]{font-style:var(--shiki-dark-font-style) !important;}&[style*="--shiki-dark-font-weight:"]{font-weight:var(--shiki-dark-font-weight) !important;}}@media(prefers-color-scheme:dark){.shiki:not(:where(.light *)) span{&[style*="--shiki-dark:"]{color:var(--shiki-dark) !important;}&[style*="--shiki-dark-font-style:"]{font-style:var(--shiki-dark-font-style) !important;}&[style*="--shiki-dark-font-weight:"]{font-weight:var(--shiki-dark-font-weight) !important;}}}:scope>[data-wide],:scope>:is(h2,h3,h4,.cl-heading:has(>:is(h2,h3,h4))):has(+ .ca-compare,+ style + .ca-compare){--_ty-start:max(0px,var(--comark-wide-start,0px));--_ty-end:max(0px,var(--comark-wide-end,0px));--_ty-wide:max(100%,min(var(--comark-wide-width,100vw),var(--comark-wide-max,72rem),100% + var(--_ty-start) + var(--_ty-end)));width:var(--_ty-wide);margin-inline:clamp(-1 * var(--_ty-start),(100% - var(--_ty-wide)) / 2,100% + var(--_ty-end) - var(--_ty-wide)) 0;}:scope>:where(:first-child):not(:where(.not-typeset,[data-not-typeset],.not-typeset *,[data-not-typeset] *)),:scope>:where(:first-child)>:where(:first-child):not(:where(.not-typeset,[data-not-typeset],.not-typeset *,[data-not-typeset] *)),:scope :where(li>:first-child,blockquote>:first-child,td>:first-child,th>:first-child,dd>:first-child,figure>:first-child,figure>picture>img ):not(:where(.not-typeset,[data-not-typeset],.not-typeset *,[data-not-typeset] *)){margin-block-start:0;}}}}
package/dist/vue.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as commentsCss, n as Preset, o as criticCss, r as PresetOptions } from "./index-BTyvRIPX.mjs";
1
+ import { a as commentsCss, n as Preset, o as criticCss, r as PresetOptions } from "./index-Cq3zytGw.mjs";
2
2
  import { Component } from "vue";
3
3
  //#region src/vue.d.ts
4
4
  /** Every plugin with components for `@comark/vue`, which leaves out annotate, comments, diff, and quiz. */
package/dist/vue.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { n as criticCss, r as preset, t as commentsCss } from "./src-Baygj6ZQ.mjs";
2
- import { t as typesetCss } from "./typeset-css-BYvWbqWz.mjs";
1
+ import { n as criticCss, r as preset, t as commentsCss } from "./src-MvpWiv9v.mjs";
2
+ import { t as typesetCss } from "./typeset-css-BOtdXbMg.mjs";
3
3
  import { i as startMermaid, n as mermaidBar, t as addMermaidStyles } from "./client-kUEiRbtp.mjs";
4
4
  import * as properties from "comark-properties/vue";
5
- import { defineComponent, h, onBeforeUnmount, onMounted } from "vue";
5
+ import { defineAsyncComponent, defineComponent, h, onBeforeUnmount, onMounted } from "vue";
6
6
  import * as badge from "comark-badge/vue";
7
7
  import * as cards from "comark-cards/vue";
8
8
  import * as carousel from "comark-carousel/vue";
@@ -24,7 +24,7 @@ import * as magicMove from "comark-magic-move/vue";
24
24
  import * as mark from "comark-mark/vue";
25
25
  import * as math from "comark-mathml/vue";
26
26
  import * as media from "comark-media/vue";
27
- import pele, { createPeleComponent } from "comark-pele/vue";
27
+ import pele from "comark-pele";
28
28
  import * as metrics from "comark-metrics/vue";
29
29
  import * as outline from "comark-outline/vue";
30
30
  import * as qr from "comark-qr/vue";
@@ -35,10 +35,10 @@ import * as tabs from "comark-tabs/vue";
35
35
  import * as tests from "comark-tests/vue";
36
36
  import * as zoom from "comark-zoom/vue";
37
37
  //#region src/vue.ts
38
- const Pele = createPeleComponent({
38
+ const Pele = defineAsyncComponent(() => import("comark-pele/vue").then(({ createPeleComponent }) => createPeleComponent({
39
39
  onRender: addMermaidStyles,
40
40
  onError: addMermaidStyles
41
- });
41
+ })));
42
42
  const Mermaid = defineComponent({
43
43
  name: "ComarkMermaid",
44
44
  inheritAttrs: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comark-ui",
3
- "version": "0.8.2",
3
+ "version": "0.9.1",
4
4
  "description": "Every comark-ui block in one install: annotated screenshots, charts, diffs, tabs, zoom, and more, registered in the order they need with shared icons. Turn any of them off or pass its options.",
5
5
  "keywords": [
6
6
  "comark",
@@ -51,50 +51,50 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "comark-pele": "^1.1.0",
54
- "comark-annotate": "^0.8.2",
55
- "comark-cast": "^0.8.2",
56
- "comark-cards": "^0.8.2",
57
- "comark-charts": "^0.8.2",
58
- "comark-carousel": "^0.8.2",
59
- "comark-badge": "^0.8.2",
60
- "comark-chips": "^0.8.2",
61
- "comark-cjk": "^0.8.2",
62
- "comark-code": "^0.8.2",
63
- "comark-colors": "^0.8.2",
64
- "comark-critic": "^0.8.2",
65
- "comark-details": "^0.8.2",
66
- "comark-deflist": "^0.8.2",
67
- "comark-comments": "^0.8.2",
68
- "comark-diagrams": "^0.8.2",
69
- "comark-code-notes": "^0.8.2",
70
- "comark-embed": "^0.8.2",
71
- "comark-diff": "^0.8.2",
72
- "comark-fields": "^0.8.2",
73
- "comark-figure": "^0.8.2",
74
- "comark-file-tree": "^0.8.2",
75
- "comark-include": "^0.8.2",
76
- "comark-icon": "^0.8.2",
77
- "comark-keys": "^0.8.2",
78
- "comark-links": "^0.8.2",
79
- "comark-mathml": "^0.8.2",
80
- "comark-footnotes": "^0.8.2",
81
- "comark-mark": "^0.8.2",
82
- "comark-github": "^0.8.2",
83
- "comark-magic-move": "^0.8.2",
84
- "comark-metrics": "^0.8.2",
85
- "comark-media": "^0.8.2",
86
- "comark-outline": "^0.8.2",
87
- "comark-quiz": "^0.8.2",
88
- "comark-reading-time": "^0.8.2",
89
- "comark-steps": "^0.8.2",
90
- "comark-properties": "^0.8.2",
91
- "comark-table": "^0.8.2",
92
- "comark-tests": "^0.8.2",
93
- "comark-tabs": "^0.8.2",
94
- "comark-xref": "^0.8.2",
95
- "comark-qr": "^0.8.2",
96
- "comark-zoom": "^0.8.2",
97
- "comark-sources": "^0.8.2"
54
+ "comark-annotate": "^0.9.1",
55
+ "comark-cards": "^0.9.1",
56
+ "comark-carousel": "^0.9.1",
57
+ "comark-badge": "^0.9.1",
58
+ "comark-charts": "^0.9.1",
59
+ "comark-cast": "^0.9.1",
60
+ "comark-chips": "^0.9.1",
61
+ "comark-cjk": "^0.9.1",
62
+ "comark-code": "^0.9.1",
63
+ "comark-code-notes": "^0.9.1",
64
+ "comark-comments": "^0.9.1",
65
+ "comark-critic": "^0.9.1",
66
+ "comark-colors": "^0.9.1",
67
+ "comark-deflist": "^0.9.1",
68
+ "comark-diagrams": "^0.9.1",
69
+ "comark-embed": "^0.9.1",
70
+ "comark-diff": "^0.9.1",
71
+ "comark-fields": "^0.9.1",
72
+ "comark-details": "^0.9.1",
73
+ "comark-figure": "^0.9.1",
74
+ "comark-file-tree": "^0.9.1",
75
+ "comark-footnotes": "^0.9.1",
76
+ "comark-github": "^0.9.1",
77
+ "comark-include": "^0.9.1",
78
+ "comark-keys": "^0.9.1",
79
+ "comark-icon": "^0.9.1",
80
+ "comark-links": "^0.9.1",
81
+ "comark-magic-move": "^0.9.1",
82
+ "comark-mathml": "^0.9.1",
83
+ "comark-media": "^0.9.1",
84
+ "comark-mark": "^0.9.1",
85
+ "comark-outline": "^0.9.1",
86
+ "comark-qr": "^0.9.1",
87
+ "comark-metrics": "^0.9.1",
88
+ "comark-properties": "^0.9.1",
89
+ "comark-quiz": "^0.9.1",
90
+ "comark-sources": "^0.9.1",
91
+ "comark-table": "^0.9.1",
92
+ "comark-tabs": "^0.9.1",
93
+ "comark-reading-time": "^0.9.1",
94
+ "comark-tests": "^0.9.1",
95
+ "comark-steps": "^0.9.1",
96
+ "comark-xref": "^0.9.1",
97
+ "comark-zoom": "^0.9.1"
98
98
  },
99
99
  "devDependencies": {
100
100
  "@comark/html": "^0.8.1",