comark-ui 0.7.2 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +2 -2
  2. package/dist/{editor-BAdok66p.mjs → editor-DmxNPK7K.mjs} +522 -68
  3. package/dist/{editor-CNivEuMH.d.mts → editor-KD97dL_1.d.mts} +21 -3
  4. package/dist/editor-vue.d.mts +1 -1
  5. package/dist/editor-vue.mjs +1 -1
  6. package/dist/editor.d.mts +2 -2
  7. package/dist/editor.mjs +1 -1
  8. package/dist/html.d.mts +1 -1
  9. package/dist/html.mjs +3 -1
  10. package/dist/{index-jkwx6SyG.d.mts → index-Dbqxt4Qg.d.mts} +10 -2
  11. package/dist/index.d.mts +1 -1
  12. package/dist/index.mjs +1 -1
  13. package/dist/react.d.mts +2 -2
  14. package/dist/react.mjs +2 -2
  15. package/dist/skills/before-after-evidence/SKILL.md +72 -0
  16. package/dist/skills/before-after-evidence/agents/openai.yaml +3 -0
  17. package/dist/skills/before-after-evidence/references/blocks.md +54 -0
  18. package/dist/skills/before-after-evidence/references/example.md +37 -0
  19. package/dist/skills/before-after-evidence/references/measuring.md +50 -0
  20. package/dist/skills/comark-ui/SKILL.md +8 -1
  21. package/dist/skills/comark-ui/references/annotate.md +5 -3
  22. package/dist/skills/comark-ui/references/comments.md +43 -0
  23. package/dist/skills/comark-ui/references/lint.md +1 -1
  24. package/dist/skills/comark-ui/references/media.md +2 -2
  25. package/dist/skills/comark-ui/references/quiz.md +1 -1
  26. package/dist/skills/comark-ui/references/ui.md +3 -2
  27. package/dist/skills/comark-ui/references/zoom.md +1 -1
  28. package/dist/skills/review-document/SKILL.md +64 -0
  29. package/dist/skills/review-document/agents/openai.yaml +3 -0
  30. package/dist/skills/review-document/references/blocks.md +69 -0
  31. package/dist/skills/review-document/references/example.md +51 -0
  32. package/dist/skills/teach-lesson/SKILL.md +78 -0
  33. package/dist/skills/teach-lesson/agents/openai.yaml +3 -0
  34. package/dist/skills/teach-lesson/references/blocks.md +91 -0
  35. package/dist/skills/teach-lesson/references/example.md +70 -0
  36. package/dist/skills/visual-explanation/SKILL.md +67 -0
  37. package/dist/skills/visual-explanation/agents/openai.yaml +3 -0
  38. package/dist/skills/visual-explanation/references/blocks.md +89 -0
  39. package/dist/skills/visual-explanation/references/example.md +25 -0
  40. package/dist/skills.d.mts +18 -0
  41. package/dist/skills.mjs +326 -0
  42. package/dist/{src-DROB05U6.mjs → src-PkmYCSNu.mjs} +1 -0
  43. package/dist/svelte/index.d.ts +1 -1
  44. package/dist/svelte/index.js +1 -1
  45. package/dist/vue.d.mts +2 -2
  46. package/dist/vue.mjs +2 -2
  47. package/package.json +46 -44
@@ -0,0 +1,326 @@
1
+ //#region src/skills.ts
2
+ /** Every agent skill this version of comark-ui ships, in the order of their names. */
3
+ const skills = [
4
+ {
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.",
7
+ "frontmatter": {
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.",
10
+ "license": "MIT"
11
+ },
12
+ "files": [
13
+ {
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. Three things make it so, and the reader can't repair any of them: two captures of the same view, marks that sit on what changed, and notes in the words the screen shows.\n\nThis skill writes the document. Where it's published, and who can open it, is up to your host.\n\n## 1. Capture a comparable pair\n\nTake **after** from the branch or its preview, and **before** 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 dialog centered vertically that grows by one line, so the same element sits at the same place in both.\n\nIf one side can't be captured 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, and on a half-width image that's 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. How the work went (the checks you ran, retries, CI) goes 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 lint-comark 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. For a local screenshot-only check, copy the complete `::compare` block, including its nested screenshots and annotations, into `evidence-screenshots.md` beside `evidence.md`.\n\n Ensure comark-annotate is a direct dependency of this workspace package. If it is only transitive, read the installed Comark UI version and add that exact version with the project's package manager: for pnpm, `pnpm add -D comark-annotate@<version>`, substituting that version. Add `-w` when installing at the workspace root. With no Comark UI packages installed, omit `@<version>`. This links the executable without fetching a different renderer release via `@latest`. 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.\n\n 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, since 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
+ },
17
+ {
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"
20
+ },
21
+ {
22
+ "path": "references/blocks.md",
23
+ "text": "# Blocks for before/after evidence\n\nThese blocks come with `comark-ui`, or alone with `comark-annotate` and `comark-diff`. The comark-ui skill (`vp dlx skills add onmax/comark-ui`) documents every block in full.\n\n## `::compare`\n\nTwo `:::screenshot` blocks inside, before then after. They're titled Before and After unless they set `title`, and their notes are numbered across both.\n\n| Prop | What it does |\n| --- | --- |\n| `title` | Names the comparison for screen readers. |\n| `mode` | `columns` (default) side by side, or `slider`: one full-width image with a divider to drag. Use `slider` when the change is small and in the same spot, such as one label. |\n\n## `:::screenshot`\n\n| Prop | What it does |\n| --- | --- |\n| `src` | Required. The image URL. |\n| `alt` | Required. What the image shows, in one phrase. |\n| `viewport` | The capture's size, like `640x420`. Set the same on both sides; a compare reports a mismatch. |\n| `href` | The page you captured, such as the preview deployment, linked by its host above the image. |\n| `title` | A heading above the image, in place of Before or After. |\n\nOther Markdown inside the block, after its annotations, becomes the caption.\n\n## `::::annotation`\n\nA direct child of a screenshot. Its content is the note, in Markdown.\n\n| Prop | What it does |\n| --- | --- |\n| `x` `y` `w` `h` | Required. The marked rectangle as percentages of the image, without `%`. |\n| `mark` | `box` (default), `circle`, `underline`, `highlight`, `bracket`, `cross`, `check`, or `none`. |\n| `color` | `red` for a problem, `green` for the fix, `amber` for a warning, `violet` (default) to point something out, `gray`, or a hex color. |\n| `side` | `top` or `bottom` moves the note into the margin with an arrow; in a side-by-side compare, `left` and `right` notes join the numbered list. |\n\nNotes without a `side` are numbered under their image, which is the clearest default in a compare.\n\n## `::diff`\n\nTwo fenced blocks inside, the file before and after, starting at the same line.\n\n````md\n::diff{line=42 label=\"The change\"}\n```tsx [src/members/InviteDialog.tsx]\n<button onClick={send}>Send invite</button>\n```\n```tsx\n<button onClick={send} disabled={invalid}>Send invite</button>\n```\n::\n````\n\n`line` is the file's line number of the first line in both blocks. Keep a diff to the few lines that caused the change; the pull request has the rest.\n"
24
+ },
25
+ {
26
+ "path": "references/example.md",
27
+ "text": "The invite dialog now says what's wrong with an address, and waits for a valid one before it sends.\n\nOpen **Settings › Members**, select **Invite**, type `maria@acme`, and press Tab.\n\n::compare{title=\"Invite dialog with an incomplete address\"}\n:::screenshot{src=\"https://comark-ui.onmax.me/skill-invite-before.png\" alt=\"Invite dialog with a red email field and no message\" viewport=\"640x420\"}\n::::annotation{x=8.3 y=42.9 w=60.6 h=12.8 color=\"red\"}\nOnly the border turns red. Nothing says what to fix.\n::::\n\n::::annotation{x=72 y=63.5 w=19.7 h=11.5 color=\"red\"}\n**Send invite** still sends to `maria@acme`.\n::::\n:::\n\n:::screenshot{src=\"https://comark-ui.onmax.me/skill-invite-after.png\" alt=\"Invite dialog with an error under the email field and a disabled Send invite button\" viewport=\"640x420\" href=\"https://pr-218.preview.acme.example/settings/members\"}\n::::annotation{x=8.3 y=58.3 w=49 h=5.8 mark=\"underline\" color=\"green\"}\n\"Add the domain, like maria@acme.com\"\n::::\n\n::::annotation{x=72 y=71.8 w=19.7 h=11.5 color=\"green\"}\n**Send invite** waits for a valid address.\n::::\n:::\n::\n\n::diff{line=42 label=\"The change\"}\n```tsx [src/members/InviteDialog.tsx]\n<input id=\"email\" aria-invalid={invalid} value={email} onChange={edit} />\n<button className=\"primary\" onClick={send}>Send invite</button>\n```\n```tsx\n<input id=\"email\" aria-invalid={invalid} aria-describedby=\"email-error\" value={email} onChange={edit} />\n{invalid && <p id=\"email-error\" className=\"error\">Add the domain, like {suggestion}</p>}\n<button className=\"primary\" onClick={send} disabled={invalid}>Send invite</button>\n```\n::\n"
28
+ },
29
+ {
30
+ "path": "references/measuring.md",
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
+ },
35
+ {
36
+ "name": "comark-ui",
37
+ "description": "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.",
38
+ "frontmatter": {
39
+ "name": "comark-ui",
40
+ "description": "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."
41
+ },
42
+ "files": [
43
+ {
44
+ "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; the comark plugins render it as static HTML, from marks on screenshots and charts drawn from tables to code changes, diagrams, and steps. 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\nPrefer `comark-ui`, every plugin in one install, registered 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\nWhen a project wants 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. The `--comark-*` tokens in https://comark-ui.onmax.me/guide/theming/ theme every plugin at once. 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 })` in `comark-ui/nuxt-ui` in a `<style>`, so the page matches the app.\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 the skill 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`, `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. Each note says what changed or what is wrong in the words the screen or the table shows, so the reader can check it against the figure; a value the figure doesn't show stays out of the note.\n\n## Check the result\n\n1. Lint the file: `vp exec lint-comark report.md`, in a project with `comark-linter` and the plugins installed, where `comark-ui` counts as every plugin. It prints one line per problem with its line, its rule, and how to fix it, and exits with 1 while any remain. Fix every one, and run it again until it exits with 0. The [lint reference](references/lint.md) covers its flags, its config file, the library, ESLint, and Vite Doctor.\n2. Look at the render, made as the block's reference says. Open it in a browser and take a screenshot: each mark sits on its target, each number matches its note, and each chart says what your text says. If you can't verify something, say so when you hand the document over instead of guessing.\n"
46
+ },
47
+ {
48
+ "path": "agents/openai.yaml",
49
+ "text": "interface:\n display_name: \"comark-ui\"\n short_description: \"Write review documents and docs with every comark plugin\"\n"
50
+ },
51
+ {
52
+ "path": "references/annotate.md",
53
+ "text": "# Screenshots and compares\n\ncomark-annotate draws `::screenshot` and `::compare`. Their marks take the values under Marks in SKILL.md.\n\n## Choose the layout\n\nPick what explains the change best:\n\n- One `::screenshot` with marks, when a single state shows the point.\n- A `::compare` of two screenshots, when the change is the point. Capture both at the same viewport, zoom, scroll, and page state, and set the same `viewport` on both. Each image gets under half the page width, where the text of a full-page capture is too small to read, so clip both captures to the same region around the change, such as Playwright's `screenshot({ clip })`.\n- `::compare{mode=\"slider\"}` instead, when both screenshots show the same spot and the change is small, such as a number or a label at the same place. The reader drags a divider across one full-width image instead of reading two half-width ones. Side notes become numbered notes there, so skip `side`.\n- A side note for the two or three points that matter most. With `mark=\"none\"` and `to` on the exact spot, the arrow alone points and nothing covers the screen, which reads cleanest. Other notes stay numbered in the list under the image. In a side-by-side compare only `top` and `bottom` keep their arrows; left and right notes join the list.\n\nFor a review the reader should follow without a key, write it the way a designer marks up a board: the default ink with no `color`, every note a side note in the margin nearest its point, set off with `at` so its arrow sweeps in, and a `bracket` where a note covers a run of rows. Write the notes of a compare as a pair that argue the change: \"The type gets the headline…\" on the before, \"The message becomes the starting point.\" on the after.\n\nMark the change at its own size: the bar, field, or label that changed. What stayed the same goes unmarked, or in one sentence above the screenshots. Keep it to about six marks per screenshot, fewer in a compare.\n\n## Syntax\n\n```md\n::compare\n:::screenshot{src=\"before.png\" alt=\"Filter toolbar wrapping onto a second row\" viewport=\"1440x900\" href=\"https://pr12.preview.example.com/correlations\"}\n::::annotation{x=3.6 y=20.6 w=92.9 h=31.4 color=\"red\"}\nMoon phase and vibes don't fit, so the toolbar wraps and pushes the table down.\n::::\n:::\n\n:::screenshot{src=\"after.png\" alt=\"Filter toolbar on one row\" viewport=\"1440x900\"}\n::::annotation{x=87.6 y=24.2 w=7.4 h=9.9 mark=\"check\" color=\"green\"}\n**Apply** lines up with the chips.\n::::\n:::\n::\n```\n\n- `::compare`: `title` is optional and names the comparison for screen readers. `mode` is `columns` (default, side by side) or `slider`. `ink` colors every annotation in both screenshots that doesn't set its own `color`. Its two screenshots are titled Before and After unless they set their own.\n- `::screenshot`: `src` and `alt` are required; `title` (the heading above the image), `href` (the captured page, linked by its host), `viewport` (like `1440x900`), and `ink` are optional. Other content becomes the caption.\n- `::annotation`: `x y w h` are required percentages of the image, numbers from 0 to 100 without `%`. `mark` is `box` (default), `circle`, `underline`, `line` (straight through the middle, along the longer side), `highlight`, `bracket`, `cross`, `check`, or `none`, which needs a `side`. `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. `side` is `top`, `bottom`, `left`, or `right`; keep top or bottom notes at least a quarter of the image apart and left or right notes an eighth apart.\n- `at` moves a side note along its margin, as a percentage of the image (across for top and bottom, down for left and right). A note set off from its point reads away from it, so put it on the side with room: a top note at `at=40` pointing left of 40 reads to the right. `to=\"x,y\"` is the point the arrow lands on, measured like the box; without it the arrow meets the middle of the mark's facing edge. Both need a `side`.\n- Annotations must be direct children of a screenshot.\n\n## Measure every mark\n\nTake each mark's coordinates from the page, in the browser you captured. Measure the element and divide by the viewport:\n\n```js\nconst r = document.querySelector(\"SELECTOR\").getBoundingClientRect();\n[(r.x / innerWidth) * 100, (r.y / innerHeight) * 100, (r.width / innerWidth) * 100, (r.height / innerHeight) * 100].map((v) => +v.toFixed(1));\n```\n\nFor a full-page screenshot, add `scrollX`/`scrollY` and divide by `document.documentElement.scrollWidth` and `scrollHeight` instead.\nFor a clipped capture, `getBoundingClientRect()` and Playwright's main-frame `clip` both use viewport-relative coordinates, so the formula takes no scroll offsets. Make sure the clip fully contains every marked element. Subtract the clip's `x` and `y` and divide by its `width` and `height`; if an element crosses the clip edge, intersect its measured rectangle with the clip before normalizing so every value stays between 0 and 100. If the clip is in document coordinates, convert it to viewport coordinates before subtracting.\n\n## Preview\n\nEnsure comark-annotate is a direct dependency of the workspace package where you run the CLI. If it is only transitive, read the installed Comark UI version and add that exact version with the project's package manager: for pnpm, `pnpm add -D comark-annotate@<version>`, substituting that version. Add `-w` when installing at the workspace root. With no Comark UI packages installed, omit `@<version>`. This links the executable without fetching a different renderer release via `@latest`.\n\n```sh\nvp exec comark-annotate render report.md -o report.html\n```\n\n`render` writes a standalone page of the screenshots and compares, and relative `src` paths resolve next to it. Without comark-linter, `vp exec comark-annotate check report.md` lists these blocks' problems on its own. It names each one like `screenshot \"after.png\", annotation 2`, counting annotations within their screenshot, and exits with 1 while any remain.\n\n## Register\n\n- In `comark-ui`: on, in `comark-ui/html` only.\n- Alone: `pnpm add comark @comark/html comark-annotate`, then `createHtmlRenderer({ plugins: [annotate()], components })` with both from `comark-annotate/html`. It renders only to HTML. Screenshots reserve their `viewport`, or dimensions from a data URI. `annotate({ size: true })` also fetches remote screenshots at render time; enable it only for trusted documents, since requests follow redirects and can reach private networks. `annotate({ size: false })` disables measurement while retaining authored viewports. In `comark-ui`, put these options under `annotate`. Each document inlines the styles once, with style attributes for positions, so a strict Content Security Policy needs `style-src 'unsafe-inline'`.\n"
54
+ },
55
+ {
56
+ "path": "references/badge.md",
57
+ "text": "# Badges\n\ncomark-badge draws a small tinted label for a version, a status, or a deprecation. Use it to tag an API as deprecated or new, or a heading with the release that added it.\n\n## Syntax\n\n```md\n## useAsyncData :badge[deprecated]{color=\"warning\"}\n\nUse `useFetch` instead. :badge[v4]{color=\"info\"} :badge[stable]{color=\"success\"}\n\n::badge\n**v4.0.0**\n::\n```\n\nWrite a badge inline as `:badge[text]`, or as Nuxt UI's block `::badge` with its text inside. The plugin unwraps the block's paragraph, so it renders as one badge and keeps its inline Markdown.\n\nFor a prose badge, `color` is `primary` (default), `secondary`, `success`, `info`, `warning`, `error`, or `neutral`. The plugin shows any other color as primary and reports it, and it reports an empty badge. Nuxt UI's own prose badge ignores `color` and is always primary.\n\n## Write badges\n\n- Keep a badge to a word or two, such as a version or a status, and pick the color by meaning: `warning` for deprecated, `success` for new or stable, `error` for removed, and `info` for a version.\n- A badge in a heading joins the heading's id, so `## useFetch :badge[deprecated]` links as `#usefetch-deprecated`. Link to that id, or put the badge in the text under the heading to keep `#usefetch`.\n- The plugin drops `{#id}` and `{.class}` on a badge without a report, so set its look with `color` or the theme.\n\n## Image and live badges\n\nUse image mode for README-style label/value badges. Static images embed their SVG. Live sources fetch npm or GitHub data while parsing and embed it too, as of the build; with `download: false`, or when the data is unavailable, they load a hosted image.\n\n```md\n:badge[Comark UI]{label=\"built with\" icon=\"package\" color=\"#15803d\"}\n:badge{source=\"npm-version\" package=\"@scope/name\"}\n:badge{source=\"npm-downloads\" package=\"comark-ui\" period=\"month\"}\n:badge{source=\"github-actions\" repository=\"onmax/comark-ui\" workflow=\"ci.yml\" branch=\"main\"}\n```\n\n- `source`: `static`, `npm-version`, `npm-downloads`, or `github-actions`. A static image needs `value` or bracket text. npm needs `package`; GitHub needs a public `repository` and a YAML `workflow` filename. Set `branch` to select a branch; otherwise GitHub returns the latest run across branches.\n- `label` and `value` hold at most 80 characters. Use bound JSON string attributes when text contains quotes or braces; the builder produces these for you.\n- `icon`: `none`, `package`, `npm`, `npmx`, `nuxt`, `github`, `comark`, `shadcn`, or `check`. npm sources pick npm, npmx, or Nuxt to match the preset; GitHub Actions uses GitHub. Explicit icons override these defaults.\n- `preset`: `default`, `npmx`, `nuxt`, `flat`, or `for-the-badge`. npmx uses monospace text and a colored slash. Nuxt uses a navy label and green value block, with status colors for CI. Flat draws a compact 20px badge; for-the-badge draws a 28px uppercase badge. Both use npm as the default logo for npm sources. All presets except default ignore `style` and `labelColor`; `theme` still applies, though Nuxt, flat, and for-the-badge keep their palettes and only add an outline in dark mode.\n- `style`: `shadcn` by default, `subtle`, or `solid`; `theme`: `auto` by default, following the page's light or dark theme (on GitHub too), or `light` or `dark`. `color` and `labelColor` accept semantic names or six-digit hex colors. In the default preset, shadcn draws an outline with neutral text; `labelColor` applies to subtle and solid backgrounds. CI chooses its own status color. Images embed colors and do not inherit the document's CSS.\n- `period`: `day`, `week`, or `month` for downloads. Hosted values cache for five minutes for versions, one hour for downloads, and one minute for CI. Failures display `unavailable` for 30 seconds. Downloaded SVGs are snapshots.\n\nThe builder at `/badges/` lives in the docs layout, shares its configuration and export format in the query string, and exports Comark, Markdown, an image URL, and SVG. The image endpoint is `/badge.svg`, with the same props as query parameters. Preserve the existing prose syntax for inline tags that should follow the page's theme.\n\n## Register\n\n- In `comark-ui`: on, under the key `badge`, with optional `{ endpoint, download }` for live images.\n- Alone: `pnpm add comark @comark/html comark-badge`, then `createHtmlRenderer({ plugins: [badge()], components })` with both from `comark-badge/html`. It accepts `{ endpoint }`, an HTTPS URL without credentials, and `{ download: false }` to skip fetching while parsing, and needs no script. Colors read Nuxt UI's `--ui-*` variables, and primary follows shadcn/ui's `--primary`, else the text color. `comark-badge/vue`, `/react`, and `/svelte` render it too.\n\nHost images with `createBadgeHandler()` from `comark-badge/server`. Generate static SVG with `readBadge()` and `badgeSvg()` from `comark-badge/svg`. The server entry fetches only public APIs and uses no token.\n"
58
+ },
59
+ {
60
+ "path": "references/cards.md",
61
+ "text": "# Cards and columns\n\ncomark-cards draws cards, link cards, and card groups, and lays blocks out in columns, in Nuxt UI's syntax. Use link cards to send readers to the next pages, and columns to set two short texts side by side, such as a before and an after.\n\n## Syntax\n\n```md\n::card-group\n:::card{title=\"Renderers\" icon=\"lucide:rocket\" to=\"/guide/renderers/\"}\nRender with HTML, Vue, React, or Svelte.\n:::\n\n:::card{title=\"Source on GitHub\" icon=\"lucide:github\" to=\"https://github.com/onmax/comark-ui\" color=\"neutral\"}\nRead the code, or open an issue.\n:::\n::\n```\n\n| Prop | What it does |\n| --- | --- |\n| `title` | The card's title. A `#title` slot wins over it and can hold Markdown; then write the body after `#default`. |\n| `description` | The text shown when the card has no content of its own. |\n| `icon` | An icon above the title, in the card's color, named as in [icon](icon.md), such as `lucide:rocket`. The plugin reports one it can't draw. |\n| `to` | Makes it a link card. One link covers the card and takes the title as its name, and links in the card's text still open on their own. A web address opens in a new tab with an arrow in the corner. |\n| `target` | Where the link opens, such as `_self` to keep a web address in the tab. |\n| `color` | Tints the icon and a hovered link card's border: `primary` (default), `secondary`, `success`, `info`, `warning`, `error`, or `neutral`. |\n\n`::card-group` sets its cards in two columns, or one when the group is narrower than 36rem, so a group in a sidebar stacks too. A card works on its own as well.\n\n```md\n::columns{cols=\"2\"}\n:::column\n**Before**\n\nOne long page of options.\n:::\n\n:::column\n**After**\n\nEach option under its own heading.\n:::\n::\n```\n\nEach block directly inside `::columns` is one cell, and `:::column` groups several blocks into one cell. `cols` is 1 to 6, default 2. Every row stacks into one column when the block is narrower than 36rem.\n\n## Write link cards\n\n- Give every link card a `title`, since it names the link for screen readers. Without one, they hear \"Card link\".\n- Link with `to`. A card with `href` still links, but Nuxt UI ignores `href`, so the plugin reports it. A `javascript:`, `vbscript:`, or `data:` link doesn't link, and the plugin reports it too.\n- For screenshots side by side, use `::compare` from [annotate](annotate.md) instead of columns.\n\n## Register\n\n- In `comark-ui`: on, under the key `cards`, which takes only `true` or `false`. Its icons come from the preset's shared resolver, so `icon: { collections }` applies to them.\n- Alone: `pnpm add comark @comark/html comark-cards`, then `createHtmlRenderer({ plugins: [cards()], components })` with both from `comark-cards/html`. By default icons come from the Iconify API while parsing. To share one resolver with the page's other plugins, add `comark-icon`, make one with `createIcons()`, and pass it as `cards({ iconResolver })`. The output needs no script. `comark-cards/vue`, `/react`, and `/svelte` render it too.\n"
62
+ },
63
+ {
64
+ "path": "references/carousel.md",
65
+ "text": "# Carousels\n\ncomark-carousel turns the blocks inside `::carousel` into slides that scroll and snap, with arrows, a counter, arrow keys, and zoom. Use it for a sequence the reader pages through in one place, such as the screens of a flow or one page at several widths.\n\n## Syntax\n\n```md\n::carousel{title=\"Checkout at three widths\"}\n![Checkout at 1440 px, with the summary beside the form](checkout-1440.png)\n\n![Checkout at 768 px, with the summary under the form](checkout-768.png)\n\n![Checkout at 375 px, with the summary folded into a bar](checkout-375.png)\n::\n```\n\n- `::carousel`: every block inside is one slide, unless it holds `:::slide` blocks. `title` names the carousel for screen readers, default `Carousel`, and heads the zoomed view of a slide that has no title or image of its own. `columns` is `1` (default), `2`, or `3`, the slides a wide page shows at once; a narrow page shows one. `zoom=false` turns zoom off.\n- `:::slide`: one slide that holds several blocks. Its `title` heads the slide's zoomed view and doesn't show on the page. Once a carousel has a `:::slide`, the blocks outside every `:::slide` are dropped and reported.\n- A click zooms a slide, with the others in a strip below it. An image slide takes the image's alt text as its heading there.\n- Props can be bound, such as `:columns=\"2\"`. A bound `zoom` or `title` reads only the document and its frontmatter, since each slide takes them while parsing.\n\n## Choose the slides\n\n- A paragraph after an image becomes a slide of its own. Wrap an image and its text in a `:::slide` to keep them together, and write a heading inside it when the page should show the slide's name.\n- Each `:::screenshot` from comark-annotate is one slide with its marks and notes, and its `title` names the step. Use that for a walkthrough of annotated screens.\n- For one view before and after a change, write annotate's `::compare` instead. It is a two-column carousel already, titles the pair Before and After, and takes marks.\n\n## Register\n\n- In `comark-ui`: on, as `carousel`, with no options.\n- Alone: `pnpm add comark @comark/html comark-carousel`, then `createHtmlRenderer({ plugins: [carousel()], components })` with both from `comark-carousel/html`. `comark-carousel/vue`, `/react`, and `/svelte` export the same pair for those renderers. comark-zoom installs with it, so slides zoom without `zoom()`. A page rendered on a server with Vue, React, or Svelte needs zoom's styles too: add `comark-zoom` and import `comark-zoom/style.css` beside `comark-carousel/style.css`.\n"
66
+ },
67
+ {
68
+ "path": "references/cast.md",
69
+ "text": "# Terminal output\n\ncomark-cast replays an asciinema recording while parsing and shows what the terminal printed as colored text, with no player and no script. Use it to show a command's output in a review, such as a test run or a build, as text the reader can search and copy, colors included.\n\n## Syntax\n\n```md\nThe flaky homing test passes now:\n\n::cast{src=\"evidence/test.cast\" title=\"pnpm test\"}\n::\n```\n\n| Prop | What it does |\n| --- | --- |\n| `src` | The cast file, a local path under the host's `root`. It reads no URLs. |\n| `mode` | `screen` (default) shows the terminal's last screen, so a progress bar redrawn in place shows its last state and a cleared screen is gone. `log` shows every line printed, from the first. |\n| `title` | The caption above the terminal, such as the command. |\n\nTo carry the output without a file, paste the recorded cast into a code block inside the block in place of `src`. Set one or the other; with both, it shows the file and reports the pair.\n\n````md\n::cast{title=\"pnpm build\" mode=\"log\"}\n```json\n{\"version\": 2, \"width\": 52, \"height\": 3}\n[0.3, \"o\", \"$ pnpm build\\r\\n\"]\n[0.9, \"o\", \"\\u001b[32m✓ built in 412ms\\u001b[39m\\r\\n\"]\n```\n::\n````\n\nA cast is in asciinema's v2 or v3 format. Its first line is a JSON header whose `width` and `height` (`term.cols` and `term.rows` in v3) size the terminal, 80 by 24 by default. Each line after it is one event: output (`\"o\"`) prints, a resize (`\"r\"`) resizes, and input, markers, and the exit code draw nothing. A full-screen app such as `vim` or `less` leaves nothing behind once it quits, as in a real terminal.\n\n## Record the output\n\n```sh\nasciinema rec --cols 80 --rows 12 --command \"pnpm test\" evidence/test.cast\n```\n\n- Keep `--cols` at 80 or fewer, since wider lines scroll sideways.\n- Keep `--rows` small for `screen`, so the last screen holds only the end of the run.\n- Use `mode=\"log\"` when the part that matters scrolled off the screen, such as the first error of a long build.\n- Run the linter from the folder the `src` paths start from, since it reads them from the folder it runs in.\n\n## Register\n\n- In `comark-ui`: on, but it reads no cast files until you pass `cast: { root: \"docs\" }`, or `{ read }` to read them yourself. Inline casts render either way.\n- Alone: `pnpm add comark @comark/html comark-cast`, then `createHtmlRenderer({ plugins: [cast({ root: \"docs\" })], components })` with both from `comark-cast/html`. `root` (default `\".\"`, the working directory) is the folder `src` paths start from, and it refuses a path or symlink that leads out of it. `read(path)` returns a file's text or `undefined`; pass it in a browser or an edge runtime. `comark-cast/vue`, `/react`, and `/svelte` render it too.\n"
70
+ },
71
+ {
72
+ "path": "references/charts.md",
73
+ "text": "# Charts\n\ncomark-charts draws the Markdown table inside `::chart` as an SVG chart and keeps the table under it. People see the chart, and anyone reading the Markdown or the page, an agent included, reads the numbers from the table. So the table is the source of truth: write the real numbers, and name every column so its header labels the chart.\n\nOn the page, a Copy button over the chart's corner copies it as a PNG, and its menu copies the data as a table for a spreadsheet or as Markdown. A chart with marks copies its data, since the PNG would leave the marks out.\n\n## Write the table\n\n```md\n::chart{type=\"line\" title=\"Coffee cups and commits, per month (r = 0.99)\"}\n| month | coffee cups | commits |\n| --- | --: | --: |\n| 2026-04 | 820 | 610 |\n| 2026-05 | 890 | 640 |\n::\n```\n\n- The first column is x: one row per bar, point, or step. Every other column is a series of numbers, named by its header.\n- Cells hold plain numbers, like `1200` or `1,200`. Put the unit in the header or the title, like `size (kB)` or `Build time (ms)`. A cell that isn't a number is left out and reported.\n- Dates on x read as time when they look like `2026`, `2026-04`, `2026-04-15`, or an ISO date and time.\n- Inside the block, the table comes first, then any marks or Vega-Lite spec. Write explanations above or below the block.\n\nProps, all optional: `type` (below, default `bar` whatever the table holds, so write `type=\"line\"` for a trend), `x` (the x column, default the first), `y` (the columns to draw, comma-separated, default all the others), `title` (the caption and the chart's name for screen readers), `labels` (prints the numbers on bars, waterfall steps, dumbbell rows, sankey nodes, and gantt bars, as in `::chart{type=\"stacked\" labels}`; without it the axis and the data table carry them), and `horizontal` (turns `bar` and `stacked` bars sideways, for long names such as file paths; past 40 bars they turn on their own). Give every chart a `title` that says what it measures and in what unit. Two or more series always get a legend over the plot.\n\n## Choose the type\n\n| Data | `type` | Table |\n| --- | --- | --- |\n| One number per name, such as size per package | `bar` (default) | names, then one column; add `horizontal` for long names |\n| Several numbers per name, compared side by side | `bar` | names, then one column per series |\n| Parts that add up to a total per name | `stacked` | names, then one column per part |\n| A trend over time | `line` | dates, then one column per series |\n| Totals over time that add up | `area` | dates, then one column per part |\n| One measure against another | `scatter` | numbers, then one column per series |\n| Before and after for several metrics | `dumbbell` | metric names, then a `before` and an `after` column |\n| How changes add up to a total | `waterfall` | the start total, each change (negative for a drop), then the end total, which must be the start plus every change, in one column |\n| Shares of one whole, five or fewer | `pie` | names, then one column of non-negative numbers |\n| Steps or tasks over time, such as a build trace, a deploy, or a plan | `gantt` | the task, then a start and an end column, as numbers or dates; a column of names, such as a run or a phase, colors the bars |\n| Where an amount goes, such as a budget, a cashflow, or a bundle's bytes | `sankey` | where each flow comes from, where it goes, and how much; a flow can't loop back, and a node in the middle can't send out more than it takes in |\n\nWhen none of these fit, such as a box plot or a heatmap, put a Vega-Lite spec after the table in a fenced `vega-lite` block. The table is its data, so leave `data` out of the spec and name the table's headers as fields; marks don't apply.\n\nIn a `gantt`, each row is one bar in table order, and a row that ends where it starts is a milestone, drawn as a diamond. Write dates as `2026-04-15` or `2026-04-15T09:30`; a time without a zone is UTC. To compare runs, add a column naming the run and repeat the steps in each.\n\nFor a before and after of one or two metrics, grouped `bar` reads best; past three, use `dumbbell`. Keep the rows in the order the reader should read them: charts draw them in table order.\n\n## Mark what matters\n\nPoint at the value the text talks about with an `::annotation` inside the chart, after the table. `at` is the row's x value copied from the table (on a `sankey`, the node's name), and `series` picks one column in that row, or on a `gantt`, the group of the bar when a task repeats. `mark`, `color`, and `side` are under Marks in SKILL.md.\n\n```md\n::chart{type=\"line\" title=\"Coffee cups and commits, per month\"}\n| month | coffee cups | commits |\n| --- | --: | --: |\n| 2026-06 | 940 | 660 |\n| 2026-07 | 1,010 | 720 |\n:::annotation{at=\"2026-07\" series=\"commits\" mark=\"circle\" color=\"green\"}\nThe new espresso machine arrives, and commits pass 700.\n:::\n::\n```\n\nFor a target, budget, or limit, draw a line across the plot: `mark=\"line\"` with `value`, a number on the value axis, and no `at`. The axis grows to reach it.\n\n```md\n:::annotation{mark=\"line\" value=\"200\" color=\"red\" side=\"right\"}\nThe 200 ms budget.\n:::\n```\n\n`mark`, `color`, and `side` work as on screenshots: red for a problem, green for a fix, and `side=\"top\"` with `mark=\"none\"` for the one point that matters most. The renderer finds the bar, point, or slice and draws the mark around it, so there is nothing to measure. Keep it to two or three marks a chart.\n\n## Preview\n\nThe linter names each chart by its position, such as `` `::chart` 2 ``. No command renders a chart on its own. When you can render the document, with the app that publishes it or the renderer under Install in SKILL.md, open it and confirm the chart says what your text says: the right type, every series named, and the numbers in the expected order.\n\n## Register\n\n- In `comark-ui`: on. Marks on charts show only through `comark-ui/html`.\n- Alone: `pnpm add comark @comark/html comark-charts`, then `createHtmlRenderer({ plugins: [charts()], components })` with both from `comark-charts/html`. Flint and Vega draw each chart while parsing, so the browser runs no chart code. `comark-charts/vue`, `/react`, and `/svelte` place the same SVG without its marks. `charts({ draw: false })` only checks the charts, which suits a lint step.\n"
74
+ },
75
+ {
76
+ "path": "references/chat.md",
77
+ "text": "# comark-chat\n\nRenders AI SDK `UIMessage` parts in Vue, React, or Svelte: text as streaming Comark Markdown, images and files, and each run of reasoning, tool calls, and sources as one trace that folds open to the thought or the call's input, output, and error. It renders only. The app sends, stores, and lays out messages, for example with `useChat()`.\n\n## Render a message\n\n```vue\n<script setup lang=\"ts\">\nimport { MessageParts } from \"comark-chat/vue\";\n<\/script>\n\n<template>\n <MessageParts :parts=\"message.parts\" :streaming=\"isLast && status === 'streaming'\" />\n</template>\n```\n\n- `streaming` marks the message that is still arriving: the last thought reads \"Thinking\" and text shows a caret.\n- `plugins`, `components`, and `options` go to every Markdown part, so `comarkUi()`'s plugins let the model write the same blocks as documents.\n- Vue slots, React's `render` functions, and Svelte snippets replace a kind of part: `text`, `reasoning`, `tool`, `source`, `file`, `step`, and `fallback` for other parts, such as `data-*` parts, which render nothing without it. Vue and React also take `part`, which replaces any part. Each gets what `read()` made of the part, with `index` and `part`. `markdown` gets `{ value, streaming }` and replaces the Markdown of text and reasoning, for an app with its own Markdown component.\n- `read(parts, streaming)` from `comark-chat` is what every renderer uses. It never throws; tool values become JSON text, and only http, https, named data, and blob image URLs become links or images.\n\n## Register\n\n- In `comark-ui`: not included. `comark-ui` renders documents; install `comark-chat` next to it and pass `comarkUi()`'s `plugins` and `components` to `MessageParts`.\n- Alone: `pnpm add comark-chat @comark/vue` (or `@comark/react`, `@comark/svelte`), then import `MessageParts` from `comark-chat/vue`, `/react`, or `/svelte`. The components add their styles on mount; a server-rendered page also imports `comark-chat/style.css`.\n"
78
+ },
79
+ {
80
+ "path": "references/chips.md",
81
+ "text": "# Links and mentions as chips\n\ncomark-chips turns every web link and every `@user` into a chip with the site's favicon or the account's avatar. It downloads each image while parsing and inlines it, so readers' browsers ask no other site. Write links and mentions as usual; the props below change one chip.\n\n## Syntax\n\n```md\nMoves the build from [webpack](https://webpack.js.org) to [Vite](https://vite.dev), reviewed by @posva. Ask [Anthony](https://antfu.me){avatar=\"@antfu\"} about the plugin, or read the [migration guide](https://vite.dev/guide/migration){chip=false}. The docs render with :chip[Comark]{href=\"https://comark.dev\"}.\n```\n\n- Every `http` or `https` link becomes a chip with the site's favicon, and a bare URL loses its scheme. Relative links, anchors, `mailto:` links, a link around an image such as a badge, and a link in raw HTML stay plain.\n- `@user` becomes a chip that links to `https://github.com/user`, with the GitHub avatar. It must stand alone: `me@example.com`, an npm scope such as `@comark/html`, and anything in a link or in code stay text.\n- A link into an account shows the account's avatar, and link text written as `@handle` loses its `@`. That covers `github.com/user` and `x.com/user` (or `twitter.com/user`) with any page under them, `bsky.app/profile/handle`, `youtube.com/@handle`, and `gitlab.com/user`.\n- `:chip[Text]{href=\"https://…\"}` writes a chip for text that isn't a link. `href` is required and must be a web address.\n\n| Prop | On | What it does |\n| --- | --- | --- |\n| `chip=false` | a link | Keeps a plain link. |\n| `avatar=\"@user\"` | a link or `:chip` | Shows that GitHub account's avatar. |\n| `avatar=\"provider:name\"` | a link or `:chip` | Shows the account's avatar on any network unavatar.io knows, such as `bluesky:sindresorhus.com`. |\n| `avatar=\"https://…\"` | a link or `:chip` | Shows this image, round like an avatar. |\n| `title` | a link or `:chip` | Sets the tooltip. |\n\n## Mentions\n\nCopy each `@user` from the person's GitHub profile, since a mention always means a GitHub login. The linter downloads nothing, so it can't tell whether a login exists; a build that downloads reports one with no avatar. For someone on another network, link their profile, or set `avatar=\"provider:name\"`.\n\n## Register\n\n- In `comark-ui`: on, with the options below under `chips`. Pass `chips: { download: false }` when parsing in a browser, which may not download another site's images.\n- Alone: `pnpm add comark @comark/html comark-chips`, then `createHtmlRenderer({ plugins: [chips()], components })` with both from `comark-chips/html`. `comark-chips/vue`, `/react`, and `/svelte` render it too.\n\n`download` (default `true`) inlines each image; `false` leaves its URL for the reader's browser. `favicon(host)` and `avatar(login)` return an image URL in place of Google's favicon service and the GitHub avatar. `cache` is the folder that keeps images for a week, `node_modules/.cache/comark-chips` by default, and `false` keeps them in memory. Avatars from X, Bluesky, YouTube, GitLab, and `provider:name` come through unavatar.io, which allows 25 downloads a day per IP address. In CI, set `cache` to a folder you commit, such as `.comark/chips`, so a build downloads only new images.\n"
82
+ },
83
+ {
84
+ "path": "references/cjk.md",
85
+ "text": "# Emphasis in Chinese, Japanese, and Korean\n\ncomark-cjk lets `**`, `*`, and `~~` open and close next to Chinese, Japanese, and Korean punctuation. These languages put no space after `。`, `」`, or `.`, so CommonMark leaves the asterisks on the page when a `**` sits between that punctuation and the next word. A document in one of these languages needs it.\n\n## What changes\n\n```md\n**この文は太字。**次の文は普通です。\n\n会議は*「金曜日」*に移りました。\n\n**이 문장은 굵게.**다음 문장은 아닙니다.\n```\n\nThe syntax stays CommonMark's. Only the rule for where a delimiter opens and closes changes, and only next to a Chinese, Japanese, or Korean character. Other text parses as before, so `**foo.**bar` still shows its asterisks.\n\n- Write `**` and `*`. `_` and `__` still don't open or close inside a word.\n- GitHub follows CommonMark here. For text that has to read the same on GitHub, close the bold before the punctuation: `**太字**。`.\n\n## Register\n\n- In `comark-ui`: on by default. `cjk: false` turns it off; it takes no options.\n- Alone: `pnpm add comark comark-cjk`, then `plugins: [cjk()]` with `import cjk from \"comark-cjk\"`. It has no components and no styles, so it works in any renderer and in streaming parses.\n"
86
+ },
87
+ {
88
+ "path": "references/code-notes.md",
89
+ "text": "# Notes on lines of code\n\ncomark-code-notes turns a numbered comment at the end of a line, like `// <1>`, into a marker that links to its note in the numbered list right after the fence, and back. Use it when a reviewer would ask why a line is there.\n\n## Syntax\n\n````md\n```ts [src/pigeons.ts]\nexport async function countPigeons(plaza: string) {\n const res = await fetch(`/api/plazas/${plaza}/pigeons`) // <1>\n if (!res.ok) return 0 // <2>\n return (await res.json()).length\n}\n```\n\n1. One request per plaza, so the map loads the counts in parallel.\n2. A plaza the census hasn't reached yet counts as empty, so the page still loads.\n````\n\n- End a line with a comment that holds its note's number in angle brackets. The comment goes and a marker takes its place. A comment with words keeps them: `// retry once <2>` shows `// retry once` and marker 2.\n- A line can carry several markers, like `// <1> <2>`, and two lines can share a number.\n- Write the comment the language accepts, so the code stays valid: `// <1>`, `# <1>`, `-- <1>`, `; <1>`, `% <1>`, `/* <1> */`, or `<!-- <1> -->`. Any of them works in any fence.\n- Put the numbered list right after the fence, with nothing but a blank line between them. Item 1 is note 1, and a list that starts at `3.` numbers its notes from 3. Each note starts with a marker that links back to its line.\n- A fence with no numbered list right after it keeps its markers as written, and is reported. A ` ```md ` fence never changes, so show the syntax in one.\n- `meta.issues` also reports a marker with no note, a note with no marker, and Shiki running after the plugin.\n\n## Write the notes\n\nMark the lines a reviewer would ask about: the request that runs in parallel, the case that returns early, the number from a benchmark. Keep each note to a sentence or two, and put a longer explanation in the text around the block. The plugin pairs markers and notes by number alone, so read each note against its line. CSS draws the marker numbers, so copied code leaves them out. On GitHub, the comments stay in the code and the notes read as a list under it.\n\n## Register\n\n- In `comark-ui`: on as `codeNotes`, the last plugin. The preset leaves Shiki out; add it right before comark-code, as the [code reference](code.md) shows, which keeps it ahead of code notes.\n- Alone: `pnpm add comark @comark/html comark-code-notes`, then `createHtmlRenderer({ plugins: [shiki(), codeNotes()], components })` with `shiki` from `@comark/html/plugins/shiki`, and `codeNotes` and `components` from `comark-code-notes/html`. It goes after `shiki`; in the other order the code isn't highlighted and `meta.issues` says so. Without Shiki it draws the lines itself. It takes no options, and `comark-code-notes/vue`, `/react`, and `/svelte` render it too.\n"
90
+ },
91
+ {
92
+ "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`, `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]`, and `// [!code focus]` 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 that lands on the wrong line inside the block isn't reported, 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
+ },
95
+ {
96
+ "path": "references/colors.md",
97
+ "text": "# Color swatches\n\ncomark-colors puts a swatch before inline code that holds one color, and draws a `::palette` of named colors as a grid of tiles. Use it when a change touches colors, such as design tokens or a theme, so the reviewer sees each value.\n\n## Syntax\n\n```md\n| Token | Before | After |\n| --- | --- | --- |\n| `--primary` | `#3b82f6` | `oklch(0.55 0.2 264)` |\n| `--ring` | `#93c5fd` | `oklch(0.7 0.12 260)` |\n\n::palette\n- Background: `#fafaf9`\n- Foreground: `#1c1917`\n- Primary: `oklch(0.55 0.2 264)`\n- Overlay: `rgb(0 0 0 / 40%)`\n::\n```\n\n- Inline code that holds one color and nothing else gets a swatch, in text, a table, or a list. A translucent color sits on a checkerboard.\n- It reads hex colors of 3, 4, 6, or 8 digits and the CSS functions `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, `lab()`, `lch()`, `oklab()`, and `oklch()`, alpha included.\n- `::palette` holds a list, bulleted or numbered, of `name: color` lines, with the color in inline code or not. Each line becomes a tile with its name and value. It takes no props.\n- `meta.issues` reports a palette with no list, a line with no name or color, a color it can't draw, and content beside the list. The palette leaves out that line and drops that content.\n\n## Show a color change\n\nPut the old and new values side by side in a table, one token per row, as above, and give each theme its own `::palette` to show the whole set. Write each color as hex or one of the functions above, alone in its code span. In inline code, anything else stays plain code and nothing reports it: a named color such as `red`, `var(--primary)`, `color-mix()`, or a span with more in it, such as `` `color: #fff` ``. Code blocks never get swatches.\n\n## Register\n\n- In `comark-ui`: on. It takes no options.\n- Alone: `pnpm add comark @comark/html comark-colors`, then `createHtmlRenderer({ plugins: [colors()], components })` with both from `comark-colors/html`. `comark-colors/vue`, `/react`, and `/svelte` render it too. Every entry exports `isColor(value)`, the test the plugin runs on inline code.\n"
98
+ },
99
+ {
100
+ "path": "references/comments.md",
101
+ "text": "# Suggestions and comments\n\ncomark-comments draws suggested edits and comments on a span of prose, each with its note in a card in the margin beside its line, as in a Google Docs or Word review. Use it to review copy or a docs page, where each change needs a reason and a comment can get a reply. For marks with no notes or margin, which a host can also accept or reject, use [critic](critic.md). For a change to code, write a `::diff`.\n\n## Syntax\n\n```md\nTo :suggestion[sign in]{from=\"log in\" by=\"Claude\" note=\"The button says Sign in.\"}, enter your email.\n\n:suggestion[]{from=\"simply\" by=\"Claude\" note=\"“Simply” tells a stuck reader it should have been easy.\"} Enter the email you signed up with.\n\nThe link expires after :comment[15 minutes]{#ttl by=\"@onmax\" note=\"Is 15 minutes enough when email is slow?\"}.\n\n:comment{reply=\"ttl\" by=\"Claude\" note=\"Keeping 15. The email has a resend button.\"}\n```\n\n| Write | Is |\n| --- | --- |\n| `:suggestion[new]{from=\"old\"}` | A replacement: `old` struck, `new` inserted |\n| `:suggestion[]{from=\"old\"}` or `:suggestion{from=\"old\"}` | A deletion |\n| `:suggestion[new]` | An insertion |\n| `:comment[words]{note=\"…\"}` | A comment on `words` |\n| `:comment{reply=\"id\" note=\"…\"}` | A reply to the comment or suggestion with `#id` |\n\n| Prop | On | What it does |\n| --- | --- | --- |\n| `from` | suggestion | The text it replaces or deletes. Plain text. |\n| `by` | both, reply | The author, shown as written; a leading `@` is dropped. The avatar is the name's initial. |\n| `note` | both, reply | The card's text. Plain text. A suggestion with no note and no replies is only a redline, with no card. |\n| `mark` | suggestion | `track` (default): red strike, green insert. `pen`: a hand-drawn strike with the new words handwritten above. |\n| `#id` | both | The id a reply points at. Comments and suggestions share one id space. |\n| `reply` | reply | The `#id` of the thread it joins. |\n\n- A reply can sit anywhere in the document; it moves into its thread, and the paragraph it leaves empty is dropped. Put replies after the paragraph, or at the end.\n- Pen fits short changes only: both sides 24 characters or fewer, on one line, and plain text. Anything else draws as `track`, with no issue.\n- Write suggestions and comments in paragraphs and list items. In a heading or a table cell, and inside another one, they draw without a card, and the linter reports them.\n- On a page 50rem wide or more, each card sits in the right margin level with its line. On a narrow page the reader clicks a bubble to open its card under the line.\n- Don't overlap two anchors. Mark the words once, and reply to the thread.\n\n## Register\n\n- In `comark-ui`: on, as `comments`, in `comark-ui/html` only. `comments: { mark: \"pen\" }` draws every suggestion with the pen unless it sets its own `mark`.\n- Alone: `pnpm add comark @comark/html comark-comments`, then `createHtmlRenderer({ plugins: [comments()], components })` with `comments, { components }` from `comark-comments/html`. There are no Vue, React, or Svelte entries.\n"
102
+ },
103
+ {
104
+ "path": "references/critic.md",
105
+ "text": "# Suggested edits\n\ncomark-critic reads CriticMarkup: insertions, deletions, substitutions, highlights, and comments written inside the text. Use it to review prose in place, such as a docs page or a changelog, where the reader should see each change in its sentence. For a change to code, write a `::diff` instead.\n\n## Syntax\n\n```md\nThe deploy waits when more than {--twelve--}{++fifteen++} pigeons sit on the roof.\n\nCount them {~~after~>before~~} the stand-up, from the {==east window==}{>>The west one faces the parking lot.<<}.\n```\n\n| Mark | Write | Shows as |\n| --- | --- | --- |\n| Insertion | `{++added++}` | `<ins>`, tinted green |\n| Deletion | `{--removed--}` | `<del>`, struck through in red |\n| Substitution | `{~~old~>new~~}` | the deletion, then the insertion |\n| Highlight | `{==this==}` | `<mark>`, in amber |\n| Comment | `{>>why<<}` | a muted chip where it sits |\n\n- Put a comment right after the words it's about. After a highlight, it comments on the highlighted words, as above.\n- Text inside a mark is Markdown, so it can hold `**bold**`, code, and links. A `{.class}` right after a mark adds that class beside the mark's own.\n- Each mark opens and closes in one paragraph, so mark an edit that spans paragraphs once in each. A mark left open, or a substitution without `~>`, stays as written, and the linter reports it.\n- A mark inside a code span stays as written. Write `\\{++` to show the characters themselves.\n- Leave the spaces around a deleted word as they are. `a {--big--} duck` accepts as `a duck`, and HTML shows the two spaces as one.\n\n## Register\n\n- In `comark-ui`: on by default, in `view` mode, which shows every mark. `critic: { mode: \"accept\" }` renders the text with the changes made, and `mode: \"reject\"` renders it without them. Both drop the comments and show highlights as plain text. The plugin reports any other `mode` and shows the changes. To publish a reviewed draft, keep the marks in the source and parse it a second time in `accept` mode. No component adds the styles, so put `criticCss`, exported by every entry, in the page. Without it, the browser underlines `<ins>`, strikes `<del>`, paints `<mark>` yellow, and shows comments as plain text. `--ccm-ins`, `--ccm-del`, and `--ccm-mark` recolor the marks.\n- Alone: `pnpm add comark comark-critic`, then `plugins: [critic()]` with `import critic from \"comark-critic\"`, and the same `mode` option. Load the styles with `import \"comark-critic/style.css\"`, or inline the `css` export. It has no components, so it works in any renderer and in streaming parses.\n"
106
+ },
107
+ {
108
+ "path": "references/deflist.md",
109
+ "text": "# Definition lists\n\ncomark-deflist renders a term followed by `: definition` lines as a native `<dl>`, `<dt>`, and `<dd>`. Use it for a glossary, or for options whose descriptions run past a line.\n\n## Syntax\n\n````md\nFlaky test\n: A test that passes and fails on the same code.\n\nRubber duck\n: A listener who doesn't need to understand the bug.\n: A bath toy.\n\nDeploy freeze\n: No deploys from Friday noon to Monday.\n\n Hotfixes still ship, with two approvals:\n\n ```sh\n deploy --hotfix --approvers 2\n ```\n````\n\n- A term is one line of inline Markdown. Its definition starts on the next line, or after one blank line, with `:` or `~` and a space.\n- Give a term several definitions with one marker line each. Leave a blank line before the next term, since a line right after a definition continues that definition.\n- Indent lines past the marker to continue a definition with more paragraphs, a list, or code, with a blank line between its blocks. Start the definition with `:` and three spaces and indent the rest by four, so every block starts in the same column.\n- A definition of one paragraph renders its text straight inside `<dd>`, and one of several blocks keeps a `<p>` for each.\n- Write the space after the marker. Without it, the line joins the term's paragraph. `:3 to 1` stays text, and `:No deploys` becomes an empty inline component that drops the word `No`. The linter reports such a component only when its name isn't also an HTML tag, so it misses `:A test`.\n\n## Choose a list or a table\n\nUse a definition list when a definition runs past a line or holds more than a paragraph, since a table cell holds neither a list nor code. For short values side by side, such as options and their defaults, a table scans faster.\n\n## Register\n\n- In `comark-ui`: on by default. `deflist: false` turns it off; it takes no options.\n- Alone: `pnpm add comark comark-deflist`, then `plugins: [deflist()]` with `import deflist from \"comark-deflist\"`. It has no components, classes, or styles, so every renderer draws `dl`, `dt`, and `dd` as they are, and the page's own CSS styles them. It works inside any `::component` and in streaming parses.\n"
110
+ },
111
+ {
112
+ "path": "references/details.md",
113
+ "text": "# Folded sections\n\ncomark-details folds content behind a summary: `::details` for one block, `::accordion` for a run of questions, and `::collapsible` for optional content. Each one renders as a native `<details>` with a real `<summary>`, so it opens with no script, the browser's search finds the text inside, and it prints.\n\n## Syntax\n\n````md\n::details{summary=\"Full build log\"}\n```txt\nvite v7.1.4 building for production...\n✓ 412 modules transformed.\ndist/index.html 0.46 kB\n```\n::\n\n::accordion{type=\"single\" :defaultValue='[\"0\"]'}\n:::accordion-item{label=\"Is it free?\" icon=\"lucide:badge-dollar-sign\"}\nYes. Every package is MIT licensed.\n:::\n\n:::accordion-item{label=\"Does it need a script?\" description=\"No. Each item is a native details element.\"}\n:::\n::\n\n::collapsible{name=\"options\"}\n- `level`: the heading level that starts each step.\n- `start`: the number of the first step.\n::\n````\n\n| Block | Props |\n| --- | --- |\n| `::details` | `summary` is the text shown while it's closed. Without one it reads \"Details\", and the plugin reports it. `open` starts it open, and `icon` puts an icon before the summary. |\n| `::accordion` | `type` is `multiple` (default), where several items can be open at once, or `single`, where opening one closes the others. `defaultValue` lists the items that start open by position from 0, as `:defaultValue='[\"0\", \"2\"]'` or `defaultValue=\"1\"`. A `single` accordion opens only the first of them. |\n| `:::accordion-item` | `label` is the summary, and it's required. `icon` puts an icon before it, and `description` is the text shown when the item has no content of its own. |\n| `::collapsible` | `name` says what it hides, default `properties`, and the summary reads \"Show properties\" while closed and \"Hide properties\" while open. `openText` and `closeText` replace \"Show\" and \"Hide\", and `icon` puts an icon before the summary. |\n\nOn `::details`, a `#summary` slot replaces `summary` and can hold Markdown; write the body after `#default`. `icon` takes a name as in [icon](icon.md), such as `lucide:info`.\n\n## Choose the block\n\n- `::details` for one thing a reader may skip, such as a long log, a stack trace, or the answer to one question.\n- `::accordion` for several questions or sections in a row. Put every block inside an `:::accordion-item`, since content between items doesn't render, and an item outside `::accordion` renders on its own.\n- `::collapsible` for optional content under a heading or a field, such as the rarely used options in a list of props.\n\n## Register\n\n- In `comark-ui`: on, under the key `details`. Its icons come from the preset's shared resolver, so `icon: { collections }` applies to them.\n- Alone: `pnpm add comark @comark/html comark-details`, then `createHtmlRenderer({ plugins: [details()], components })` with both from `comark-details/html`. By default icons come from the Iconify API while parsing. To share one resolver with the page's other plugins, add `comark-icon`, make one with `createIcons()`, and pass it as `details({ iconResolver })`. The output needs no script. `comark-details/vue`, `/react`, and `/svelte` render it too.\n"
114
+ },
115
+ {
116
+ "path": "references/diagrams.md",
117
+ "text": "# Diagrams\n\ncomark-diagrams draws PlantUML, Graphviz, D2, DBML, GoAT, and Svgbob fences as SVG while parsing, through a Kroki server, and puts the drawing in the page with no script. Use it when a flow or a schema shows a change faster than prose.\n\n## Syntax\n\n````md\n```d2 [How a sighting reaches the map]\nphone -> api: POST /sightings\napi -> queue: dedupe\nqueue -> map: pigeon pin\n```\n````\n\n| Fence | Draws |\n| --- | --- |\n| `plantuml` | UML: sequence, class, state, and other diagrams |\n| `graphviz` or `dot` | Graphs in the DOT language |\n| `d2` | Boxes and arrows in D2's short syntax |\n| `dbml` | Database tables and their relations |\n| `goat` or `svgbob` | ASCII art, as lines and arrows |\n\n- The title in brackets after the language goes under the diagram and names it for screen readers. Give every diagram one.\n- Every other fence stays code, except `mermaid`, which comark-ui draws with comark-pele (see Mermaid below).\n- A diagram Kroki can't draw stays code, and `meta.issues` quotes the tool's error. So does one drawn while the server can't be reached; the next parse tries again.\n- The plugin drops anything that could run from the SVG: scripts, event handlers, embedded HTML, and animations. Write labels as plain text. A D2 Markdown label (`|md … |`) is embedded HTML, so it comes out empty, and nothing reports it.\n\n## Mermaid\n\nThrough comark-ui, a ` ```mermaid ` fence is drawn as SVG by comark-pele while rendering, with no network, and themed with the other blocks. A diagram Pele can't draw shows its source, and nothing goes in `meta.issues`, so check it in a render.\n\nWith `comark-ui/html`, a click, `Enter`, or `Space` zooms the diagram, and its buttons copy it as a PNG on the page's background or as its ` ```mermaid ` fence. The Vue, React, and Svelte entries draw the same buttons, and Pele zooms the diagram in place there. `zoom: false` in `comarkUi()` turns the zoom off; the buttons stay. A host with a strict Content Security Policy allows the buttons' script by the hash of `mermaidScript` from `comark-ui/html`, and Copy image needs `img-src data:`.\n\n## Check each diagram\n\nSend an internal architecture or a schema with sensitive names only to the host's own Kroki server. With the default `server`, each diagram's source goes to `https://kroki.io`, a public service.\n\nThe linter sends nothing, so it can't tell whether a diagram draws. Render the document, or post the source to the same Kroki server and read the answer:\n\n```sh\ncurl -s -H \"Content-Type: text/plain\" --data-binary @flow.d2 https://kroki.io/d2/svg\n```\n\nAn SVG means it draws; any other answer is the tool's error.\n\n## Register\n\n- In `comark-ui`: on, with the options below under `diagrams`.\n- Alone: `pnpm add comark @comark/html comark-diagrams`, then `createHtmlRenderer({ plugins: [diagrams()], components })` with both from `comark-diagrams/html`. `comark-diagrams/vue`, `/react`, and `/svelte` render it too.\n\n`server` is the Kroki server, `https://kroki.io` by default. `docker run -d -p 8000:8000 yuzutech/kroki` runs your own, which draws every fence above, and `diagrams({ server: \"http://localhost:8000\" })` points at it. `draw: false` leaves every fence as code and sends nothing, for a lint step. The plugin draws each diagram once per process, and a failed one again on the next parse.\n"
118
+ },
119
+ {
120
+ "path": "references/diff.md",
121
+ "text": "# Code changes\n\ncomark-diff draws a file's lines before and after as a suggested change, the way a pull request review shows one. Removed lines are red, added lines green, and the words that changed a shade deeper, under a header with the file and the count of lines added and removed. Use it to propose or explain an edit to one file.\n\n## Syntax\n\n````md\n::diff{line=212 label=\"Suggested change\"}\n```ts [packages/zoom/src/client.ts]\nfunction close() {\n dialog.close();\n history.back();\n}\n```\n```ts\nfunction close() {\n dialog.close();\n if (opened) history.back();\n}\n```\n::\n````\n\nInside go two fenced blocks, before then after, in the file's language, and nothing else. Leave the first block empty for a new file, or the second for a deleted one.\n\n| Prop | What it does |\n| --- | --- |\n| `file` | The file's path. Defaults to the name on the first block, like `` ```ts [src/a.ts] ``; one of the two is required. |\n| `line` | The file's line number of the first line in both blocks, a whole number from 1. Without it, the diff has no line numbers. |\n| `label` | A heading before the file name, such as \"Suggested change\". |\n\nA line a block highlights, like `` ```ts {2} ``, stays highlighted. `meta.issues` reports an unknown prop, content beside the blocks, a count of blocks other than two, a `diff` or `patch` block, a missing file name, two different file names or languages, a `line` that isn't a whole number from 1, a block over 200 lines, and a diff with no change.\n\n## Keep it to the change\n\n- Show one file per diff, with a few unchanged lines before and after the change. Split a change to two files into two diffs, and write explanations above or below the block.\n- Copy both blocks from the file, and start them at the same line, since `line` numbers both from there. Take the number from the file; the plugin can't check it.\n- For a change already written as a unified diff, such as `git diff` output, paste it into a `diff` fence outside `::diff`, as the [code reference](code.md) shows.\n\n## Register\n\n- In `comark-ui`: on, in `comark-ui/html` only.\n- Alone: `pnpm add comark @comark/html comark-diff`, then `createHtmlRenderer({ plugins: [diff()], components })` with both from `comark-diff/html`. It renders only to HTML, takes no options, and needs no script. Add Comark's `shiki` plugin, in any order, to keep the code's highlighting.\n"
122
+ },
123
+ {
124
+ "path": "references/embed.md",
125
+ "text": "# Players and link cards\n\ncomark-embed turns `::embed` into the provider's player, with the link under it, for YouTube, Vimeo, Loom, Spotify, CodePen, StackBlitz, and Figma, and into a link card for any other https page. It reads the player's address from the link alone and fetches nothing. Use it for a screen recording of a bug, a design file, or a live reproduction.\n\n## Syntax\n\n```md\n::embed{url=\"https://youtu.be/KCrXgy8qtjM\" title=\"Watch before standup: Vite in 100 Seconds\" start=30}\n::\n```\n\n| Prop | What it does |\n| --- | --- |\n| `url` | Required. An `https://` link; any other renders nothing. |\n| `title` | Names the player for screen readers and is the text of the link under it. Without it, the link shows the host. |\n| `start` | Whole seconds into a YouTube or Vimeo video. A YouTube link's own `t=`, like `t=1m30s`, works too. |\n\nClose the block with `::` on the next line. It drops anything inside and reports it.\n\n| Provider | Links it reads |\n| --- | --- |\n| YouTube | `youtube.com/watch?v=`, `youtu.be/`, `/shorts/` (drawn tall), `/embed/`, and `/live/`, played from `youtube-nocookie.com` |\n| Vimeo | `vimeo.com/<number>`, an unlisted link with its hash, and `player.vimeo.com/video/`, played with `dnt=1` |\n| Loom | `loom.com/share/<id>` |\n| Spotify | `open.spotify.com` track, album, playlist, episode, show, and artist links; a track or an episode gets the compact player |\n| CodePen | `codepen.io/<user>/pen/<id>` |\n| StackBlitz | `stackblitz.com/edit/<project>` and `stackblitz.com/github/<owner>/<repo>`, keeping options such as `file` and `view` |\n| Figma | `figma.com/design`, `file`, `proto`, `board`, `slides`, and `deck` links, keeping `node-id` |\n\nAny other `https://` link renders as a card with its `title` and host. A provider's link it can't read an ID from renders as a card too, and is reported. A link with a user name or password renders nothing.\n\n## Pick what to embed\n\n- Give every card a `title`. A card fetches nothing, so without one it shows only the host, and the plugin reports it.\n- For a live reproduction, embed a StackBlitz project that runs in the browser alone, such as one from the `typescript` starter. A project that runs in WebContainers, such as a Node or Vite starter, needs a cross-origin-isolated page and a supported browser, and otherwise shows \"Unable to run Embedded Project\". Nothing reports it.\n\n## Register\n\n- In `comark-ui`: on.\n- Alone: `pnpm add comark @comark/html comark-embed`, then `createHtmlRenderer({ plugins: [embed()], components })` with both from `comark-embed/html`. It takes no options and needs no script. `comark-embed/vue`, `/react`, and `/svelte` render it too.\n"
126
+ },
127
+ {
128
+ "path": "references/fields.md",
129
+ "text": "# Props and options\n\ncomark-fields lists a component's props or a function's options, each with its type, its default, and a required tag, and nests an object's properties under it. Use it in API docs, or in a review that adds or changes options.\n\n## Syntax\n\n```md\n::field-group\n:::field{name=\"title\" type=\"string\" required}\nThe text above the card's content.\n:::\n\n:::field{name=\"options\" type=\"object\" default=\"{}\"}\nWhat the plugin takes.\n\n::::field{name=\"download\" type=\"boolean\" default=\"true\"}\nInlines each image while parsing.\n::::\n:::\n::\n```\n\n| Prop | What it does |\n| --- | --- |\n| `name` | Required. The prop, option, or parameter, shown in code. |\n| `type` | Its type, as you'd write it in TypeScript. |\n| `required` | A bare `required` adds a required tag. `:required=\"false\"` leaves it off. |\n| `default` | Its default, shown in code. `:default=\"0\"` and `:default='{\"a\":1}'` show as written. |\n| `description` | The text shown when the field has no content of its own, or above its nested fields. On a field with text inside, the plugin ignores it and reports it. |\n\nThe text inside a field is its description. Fields inside a field describe an object's properties, with one more colon for each level. `::field-group` stacks fields with a line between them, and a field works on its own too. Nuxt UI's own field has no `default` and ignores it.\n\nTo fold rarely used options away, put their fields in a `::collapsible` from [details](details.md), as Nuxt UI's docs do:\n\n```md\n::field{name=\"options\" type=\"object\"}\n:::collapsible{name=\"options\"}\n::::field{name=\"sync\" type=\"string\"}\nA key shared by blocks that switch together.\n::::\n:::\n::\n```\n\n## Register\n\n- In `comark-ui`: on, under the key `fields`, with no options.\n- Alone: `pnpm add comark @comark/html comark-fields`, then `createHtmlRenderer({ plugins: [fields()], components })` with both from `comark-fields/html`. It takes no options and needs no script. `comark-fields/vue`, `/react`, and `/svelte` render it too.\n"
130
+ },
131
+ {
132
+ "path": "references/figure.md",
133
+ "text": "# Image captions\n\ncomark-figure turns an image with a title, alone in its paragraph, into a native `<figure>` with the title as its `<figcaption>`. Use it when an image needs a sentence under it that says why it's there.\n\n## Syntax\n\n```md\n![Bar chart of deploys per day, highest on Tuesday](deploys.png \"Most deploys land on Tuesday, after the Monday freeze\")\n```\n\n- The caption is the title, the quoted text after the URL. It is plain text, so Markdown in it shows as written.\n- The alt text stays on the image for screen readers, who hear both. Make them say different things. The alt text says what the image shows, and the caption says why it's there.\n- The image keeps its `src`, its alt text, and any `{attributes}`, and loses its `title`.\n- Give the image a paragraph of its own, with a blank line before and after it. That works at the top level, in a list item, a blockquote, or a definition, and in a `::component` beside another block. An image inside a sentence keeps its title as a tooltip.\n- When a `::component` holds nothing but the image, the plugin leaves the image as it is. Add another block to the component, or move the image out of it.\n- A linked image becomes a figure too: `[![alt](src \"Caption\")](https://example.com)` keeps the link around the image, with the caption under it, outside the link. A title on the link, `[![alt](src)](url \"Title\")`, stays the link's tooltip.\n\n## Numbered figures\n\nFor a figure the text refers to by number, give the image comark-xref's `{#fig-…}` id and no title, and xref captions it with \"Figure 1:\" and its alt text. `comark-ui` runs both plugins, and there an image with both a title and a `fig-` id becomes an unnumbered figure. xref then reports it as sharing its paragraph with text, and every `:ref` to it as pointing at nothing.\n\n## Register\n\n- In `comark-ui`: on by default. `figure: { unwrap: true }` also lifts every image alone in its paragraph out of the `<p>`, with a title or without, so the page's CSS can size those images apart from the ones inside a sentence. Comark's `autoUnwrap` does that only in list items and blockquotes.\n- Alone: `pnpm add comark comark-figure`, then `plugins: [figure()]` with `import figure from \"comark-figure\"`, and the same `unwrap` option. It has no components, classes, or styles, so it works in any renderer. With comark-zoom, the image in a figure still zooms, in either plugin order. Browsers indent a figure by 40px on each side until the page's CSS sets its margin.\n"
134
+ },
135
+ {
136
+ "path": "references/file-tree.md",
137
+ "text": "# File trees\n\ncomark-file-tree draws folders and files from a fenced block, marks entries added, removed, or changed, and puts a note on any line. Use it to show what a change adds, moves, or deletes, or how a project is laid out.\n\n## Syntax\n\n````md\n::file-tree\n```\npackages/\n steps/\n + src/\n + index.ts # the parse plugin\n ~ package.json\n - legacy.ts # folded into index.ts\n ...\nREADME.md\n```\n::\n````\n\nWrite one entry per line in one fenced block inside `::file-tree`, with any language or none. The block takes no props.\n\n| Written | What it does |\n| --- | --- |\n| Two spaces before a name | Puts it inside the entry above that starts further left. Four spaces or tabs work the same. |\n| `├──`, `└──`, and `│` | Output pasted from `tree`, in Unicode or ASCII. The plugin drops its `.` root and its `2 directories, 3 files` summary. |\n| A trailing `/` | Makes a folder. An entry with entries under it is a folder too. |\n| `+`, `-`, or `~` and a space before a name | Marks it added, removed, or changed, in a color, with the sign kept beside the name. |\n| ` # note` after a name | A note, shown after the name in a lighter color. The `#` needs a space on each side, so `#notes.md` stays a name. |\n| `...` | Stands for entries left out. |\n\n## Show a change\n\n- Write the tree in a fence, even a short one. A Markdown list turns `__init__.py` bold and `[id].vue` into a span, so the plugin reports it and renders the list as written.\n- List the folders on the path to each changed file, mark the files, and put `...` where unchanged entries sit. Give a file a note when the reason it changed fits in a few words.\n- Write any explanation outside the block, since text beside the fence doesn't render.\n\n## Register\n\n- In `comark-ui`: on, under the key `fileTree`. The preset passes its shared icon resolver, so every entry gets its file type's icon from Iconify's vscode-icons set, fetched from the Iconify API unless `icon: { collections }` holds `vscode-icons`. `fileTree: { icons }` adds icons of your own, as below.\n- Alone: `pnpm add comark @comark/html comark-file-tree`, then `createHtmlRenderer({ plugins: [fileTree()], components })` with both from `comark-file-tree/html`. Without options, every folder and file gets the same plain glyph and nothing is fetched. Add `comark-icon` and pass `fileTree({ iconResolver: createIcons() })` to draw each file type's icon. `icons` maps a file name, an extension such as `.vue`, or `name/` for a folder to any icon name from [icon](icon.md), and its `folder` and `file` keys set the fallbacks. A name wins over an extension, and an icon that doesn't load leaves the plain glyph. The output needs no script. `comark-file-tree/vue`, `/react`, and `/svelte` render it too.\n"
138
+ },
139
+ {
140
+ "path": "references/footnotes.md",
141
+ "text": "# Footnotes\n\ncomark-footnotes numbers notes in the order the text first references them and lists them in a section at the end of the page. Use a note for a source, a caveat, or a detail that would break the sentence, such as how you measured a number.\n\n## Syntax\n\n```md\nThe deploy waits when the roof holds more than 12 pigeons[^count], unless it's a hotfix^[Hotfixes need two approvals.].\n\n[^count]: Counted once, from the window, at 9:00.\n\n Pigeons that fly off mid-count still count.\n```\n\n- `[^label]` marks where a note applies, and a line that starts with `[^label]:` defines it, anywhere in the document. The number comes from the order of first reference, so name the label after the note, such as `[^count]`.\n- `^[text]` writes a note in place, with no label and no definition.\n- Indent lines by four spaces to continue a definition with more paragraphs, a list, or code. A line that isn't indented continues the paragraph above it.\n- Markdown in a note renders, and a note can reference another note. A note referenced twice links back to both places.\n- The plugin draws the section, its `Footnotes` heading, and the rule above it, so the document holds only the definitions.\n\nThe linter reports a reference with no definition, a definition nothing references, and a label defined twice. Only the first of two definitions shows. It misses a label with a space. `[^my note]` shows as `^my note`, and its definition disappears, so join the words with hyphens: `[^my-note]`.\n\n## Register\n\n- In `comark-ui`: on by default, in place of Comark's own footnotes plugin. `footnotes: { label: \"Notes\", hr: false, backRef: \"↑\" }` sets the section's heading (default `\"Footnotes\"`; `\"\"` leaves it out), whether a rule comes before the section (default `true`), and the link from a note back to its reference (default `\"↩\"`).\n- Alone: `pnpm add comark comark-footnotes`, then `plugins: [footnotes()]` with `import footnotes from \"comark-footnotes\"`, and the same options. It is named `footnotes`, and Comark keeps the first plugin of a name, so remove `comark/plugins/footnotes` from the list instead of adding this one after it. It has no components and writes the nodes and classes Comark's plugin writes, such as `footnote-ref` and `footnotes`, so styles made for it keep working.\n"
142
+ },
143
+ {
144
+ "path": "references/github.md",
145
+ "text": "# GitHub callouts, references, and HTML\n\ncomark-github renders three parts of GitHub's Markdown as GitHub does: `> [!NOTE]` alerts as callouts with an icon and a title, `@user` and `#123` as links, and raw HTML cut down to the tags GitHub allows. Use a callout for the one thing a reader must not miss, and write references and HTML as you would in a GitHub comment.\n\n## Syntax\n\n````md\n> [!WARNING] Breaking change\n> `render(markdown)` is gone. Create a renderer once with `createHtmlRenderer()` and reuse it.\n\nThanks @onmax: this fixes #51, and onmax/comark-ui#73 builds on it.\n\n<details>\n<summary>Test output</summary>\n\n```txt\n✓ test/callouts.test.ts (12 tests) 41ms\n```\n\n</details>\n````\n\n- Callouts: `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, or `[!CAUTION]` at the start of a quote's first line, in any case. Text after the marker on that line is the title, as plain text; without it the title is the type, such as Warning. The body is any Markdown on the quote's next lines, and a callout can sit in a list. Any other marker, such as `[!INFO]`, leaves a plain quote that shows the marker, and is reported.\n- References: `@user` links to `https://github.com/user`, `owner/name#123` to that issue or pull request, and a bare `#123` into the host's `repo`. With no `repo`, `#123` stays text. A reference must stand alone, so `me@example.com`, `C#9`, `@comark/html`, and anything in a link or in code are never references.\n- Raw HTML keeps GitHub's tags, such as `details`, `summary`, `kbd`, `sub`, `sup`, `img`, `picture`, and tables, and the attributes that set the layout or describe the content, such as `width`, `align`, `alt`, `title`, and `open`. `script` and `style` go with their content. Any other tag, such as `iframe`, `form`, or `video`, gives way to its content, so write a recording as an image for comark-media. `class`, `id`, `style`, and event handlers go. A URL must be relative, http, or https, or mailto in a link. Blocks written as Markdown, such as `::screenshot`, are untouched.\n\n## Choose the callout\n\n| Type | Use it for |\n| --- | --- |\n| `NOTE` | Context the reader should know, even when skimming |\n| `TIP` | A better or faster way to do something |\n| `IMPORTANT` | Something the reader needs to succeed |\n| `WARNING` | A risk that needs their attention now |\n| `CAUTION` | An action with consequences they can't undo |\n\n## Collapse long output\n\nPut a long log or a full test run in a `<details>`, with a `<summary>` that says what it holds, so the page stays short. Leave a blank line after `</summary>`, which GitHub needs to render the Markdown inside, and another before `</details>`. After a list, a `</details>` with no blank line before it joins the list, and the next block lands inside the details.\n\n## Register\n\n- In `comark-ui`: all three are on. Pass `github: { references: { repo: \"owner/name\" } }` so a bare `#123` links. `github: { callouts: { titles: { note: \"Nota\" } } }` renames the default titles for a site in another language, and `github: { html: false }` renders raw HTML as written. comark-chips runs first there, so `@user` renders as its avatar chip.\n- Alone: `pnpm add comark @comark/html comark-github`, then `createHtmlRenderer({ plugins: [callouts(), references({ repo: \"owner/name\" }), html()], components })` with all four from `comark-github/html`. It has no default export, so register the plugins you want, in any order; only callouts use `components`. `callouts()` takes the place of Comark's built-in alert plugin and `html()` of its html plugin, even under `registerDefaultPlugins: false`. `comark-github/vue`, `/react`, and `/svelte` render it too. A `repo` that isn't `owner/name` is reported, and `#123` stays text.\n"
146
+ },
147
+ {
148
+ "path": "references/icon.md",
149
+ "text": "# Icons\n\ncomark-icon draws `:icon` with any of Iconify's 200,000 icons, such as a logo beside a name or a status in a table. It resolves each icon while parsing and writes it into the page as a CSS image, so the reader's browser fetches nothing and runs no script.\n\n## Syntax\n\n```md\n| Check | Result |\n| --- | --- |\n| Unit tests | :icon{name=\"lucide:circle-check\" label=\"Passed\"} |\n| Visual diff | :icon{name=\"lucide:circle-x\" label=\"Failed\"} |\n\nBuilt with :icon{name=\"logos:vue\"} Vue and :icon{name=\"logos:vitejs\"} Vite.\n```\n\n| Prop | What it does |\n| --- | --- |\n| `name` | Required. `set:name`, such as `lucide:apple`, from a set at https://icones.js.org. Nuxt UI's `i-lucide-apple` works too. A URL, or a path that starts with `/`, `./`, or `../`, draws that image instead. `:name=\"frontmatter.icon\"` takes the name from the frontmatter. |\n| `label` | What the icon means, for screen readers. Without one the icon is decoration, and they skip it. |\n\nAn icon is as tall as the text. One in a single color, such as Lucide's, paints in the color of the text around it. One with colors of its own, such as `logos:vue`, keeps them.\n\nGive a `label` when the icon says something the words around it don't, such as a lone status in a table cell. Leave it out when a word beside the icon says the same. Copy each name from icones.js.org. The plugin leaves out an icon it can't draw, from a misspelled name, an unknown set, or a failed download, and reports it.\n\n## Register\n\n- In `comark-ui`: on. Its `icon` option takes `collections` and `fetch`, as below, and its resolver also draws the icons of cards, details, file trees, tabs, and code actions.\n- Alone: `pnpm add comark @comark/html comark-icon`, then `createHtmlRenderer({ plugins: [icon()], components })` with both from `comark-icon/html`. `comark-icon/vue`, `/react`, and `/svelte` render it too. With no options it downloads each icon from the Iconify API while parsing, once per process. To build offline, install the sets you use and pass them, as in `icon({ collections: { lucide }, fetch: false })` with `icons as lucide` from `@iconify-json/lucide`. A collection can also map names to SVG markup, for icons of your own, and `fetch` can be the URL of your own Iconify API. Plugins that draw icons take an `Icons` from `createIcons()`, so pass the same one to each and set the options once.\n"
150
+ },
151
+ {
152
+ "path": "references/include.md",
153
+ "text": "# Partials and snippets from files\n\ncomark-include pastes a Markdown file, or lines of a source file as a code block, into the document before Comark parses it. Use it to keep one copy of a section that several pages share, or to show real source that stays in step with the code.\n\n## Syntax\n\n```md\n## Install\n\n:include{src=\"./parts/install.md\"}\n\nThe bug estimate, straight from the source:\n\n:include{src=\"/src/ducks.ts\" region=\"estimate\"}\n```\n\n| Prop | What it does |\n| --- | --- |\n| `src` | Required. A local file. A relative path starts at the document's folder, or at the including partial's; a leading `/` starts at `root`. |\n| `lines` | `\"10:24\"` keeps lines 10 to 24, 1-based and inclusive; `\"10:\"` and `\":24\"` leave one end open. Lines count from the top of the file, frontmatter included. |\n| `region` | Keeps the lines between `#region estimate` and `#endregion estimate` and drops the marker lines. A marker starts its line, after a comment opener such as `//`, `#`, or `<!--`. |\n| `code` | Shows a Markdown file as a code block instead of pasting it. |\n| `lang` | Sets the code block's language in place of the extension, and makes a Markdown file a code block too. |\n\nThe plugin pastes a `.md`, `.mdc`, or `.markdown` file without its frontmatter, and that file's own includes resolve from its folder. Any other file becomes a fenced code block in its extension's language, with a fence longer than any backtick run inside. `lines` and `region` don't combine, and the plugin dedents the lines either one keeps.\n\n## Place it\n\n- Write `:include` with one colon, alone on its line. A `::include` takes in the rest of the document. An include that shares its line with text or a list marker, or sits in a blockquote, renders nothing. The plugin reports all three.\n- To put a partial in a list item, indent the include on its own line under the item's text. The pasted lines take its indent.\n- Close every `::` block inside the partial. The plugin closes one left open at the end of its paste, and reports it.\n- An include inside fenced code stays as written, so a page can show the syntax.\n- No Comark renderer runs on GitHub or npm, so a README there shows `:include` as written. In a README, use automd's `file` generator, which takes the same `src`, `lines`, `code`, and `lang` and writes the text into the file.\n\nThe plugin drops an include that fails and reports why, such as a missing file, a path that leaves `root`, a URL, a range past the end, a region the file doesn't mark, a cycle, or nesting past `depth`.\n\n## Lint it\n\ncomark-linter doesn't load comark-include, so it reports `:include` as an unknown component until a [lint config](lint.md) adds `include({ root })` to its `plugins`. That one `include()` serves every file in the run, so a relative `src` resolves from its `base`. In documents outside `base`, start `src` with `/` so it resolves the same in the app and the linter.\n\n## Register\n\n- In `comark-ui`: off, since it reads files from disk. `include: { root: \"content\", base: \"guide\" }` turns it on; `include: true` reads under the working directory.\n- Alone: `pnpm add comark comark-include`, then `parseMarkdown(markdown, { plugins: [include({ root: \"content\", base: \"guide\" })] })` with `include` from `comark-include`, first in `plugins`. It has no components, because the pasted text renders like the rest of the document in any renderer, and it doesn't work with streaming parses. `root` is the folder includes can't leave (default `\".\"`). `base` is the document's folder under `root` (default `\"\"`); Comark doesn't tell plugins where a document lives, so set it per document. In a browser or an edge runtime, pass `read(path)`, which returns a file's text by its POSIX path under `root`, or `undefined`, and may be async. The default reader refuses symlinks that lead out of `root`, and your own must refuse them too. `depth` caps nesting (default 8). `meta.includes` lists every file a document read or tried to read, so a watcher can rebuild the page when one changes or appears.\n"
154
+ },
155
+ {
156
+ "path": "references/keys.md",
157
+ "text": "# Keyboard shortcuts\n\ncomark-keys renders `++ctrl+shift+k++` as one `<kbd>` per key, joined by `+`, each named the way people read it. Use it wherever a guide or a bug report tells the reader which keys to press.\n\n## Syntax\n\n```md\nPress ++ctrl+shift+k++ to open the pigeon counter, and ++esc++ to close it. On a Mac, it's ++cmd+opt+k++.\n\nHold ++shift++ and press ++\"Duck Button\"++ to page the rubber duck.\n```\n\n- Write the keys between `++` and `++`, joined by single `+` signs, with no spaces. Names aren't case sensitive.\n- Letters and digits render uppercase, `f1` to `f24` as F1 to F24, and these names as their usual labels:\n - Modifiers: `ctrl`, `alt`, `shift`, `cmd`, `opt`, `option`, `meta`, `super`, `win`, `fn`, `altgr`.\n - Editing: `enter`, `return`, `tab`, `esc`, `space`, `backspace`, `bksp`, `del`, `ins`, `caps-lock`.\n - Moving: `up`, `down`, `left`, `right`, `home`, `end`, `pg-up`, `pg-dn`, `page-up`, `page-down`.\n - Others: `print-screen`, `prtsc`, `menu`.\n - Punctuation, by name: `plus`, `minus`, `equal`, `comma`, `period`, `slash`, `backslash`, `semicolon`, `colon`, `grave`, `tilde`, `bracket-left`, `bracket-right`, `brace-left`, `brace-right`, `less`, `greater`, `question`, `exclam`, `bar`, `underscore`, `single-quote`, `double-quote`.\n- Quote a key, in single or double quotes, to show any other text, such as a button, a mouse click, or a numpad key: `++ctrl+\"Click\"++`. A backslash escapes a quote inside.\n- `return` reads Enter. Write `++\"Return\"++` for a Mac's label.\n\nOne unknown name, a space, or a bare symbol, as in `++ctrl+/++`, leaves the whole shortcut as text, and nothing reports it. Check each name against the list, or quote the key. `++` right after a letter, a digit, or `+` never opens a shortcut, so `C++` stays text.\n\n## Register\n\n- In `comark-ui`: on by default. `keys: { separator: \"\", keys: { hyper: \"Hyper\" } }` changes what sits between two keys (default `\"+\"`; `\"\"` puts nothing) and adds keys or renames built-in ones, by lowercase name.\n- Alone: `pnpm add comark comark-keys`, then `plugins: [keys()]` with `import keys from \"comark-keys\"`, and the same options. It ships no styles, and browsers show `<kbd>` in a monospace font, so draw keycaps with the page's CSS on `.ckb kbd`. Each named key also gets a class with its full name, such as `ckb-control`. A `{.class}` after a shortcut replaces its `ckb` class, so skip it when the page styles `.ckb`.\n"
158
+ },
159
+ {
160
+ "path": "references/links.md",
161
+ "text": "# Heading links and new-tab links\n\ncomark-links has no blocks of its own. `anchors()` puts a `#` link beside each h2, h3, and h4, so readers can share a section. `external()` writes `target` and `rel` on links to other sites, and warns before any link opens a new tab.\n\n## What you write\n\n```md\n## Rollout plan {#rollout}\n\nWeek one is internal teams only. The [rollback steps](#rollback) apply from day one.\n\n## Rollback {#rollback}\n\nTurn off the `new-checkout` flag in **Settings**, then redeploy.\n```\n\n- A heading's `#` link uses its id. Comark makes the id from the heading's text, so write `{#rollout}` after a heading that other pages link to, and the link survives a reworded heading.\n- A link opens a new tab only when the host sets `target` or the link says `{target=\"_blank\"}`. Leave both off, so the reader decides; add one only where leaving loses the reader's work, such as a page with a half-filled form. Any link that opens a new tab, on your site or another, gets `rel=\"noopener\"`, a ↗, and \"(opens in a new tab)\" for screen readers, so leave that out of the link's text.\n- A link keeps the `target` and `rel` written on it, such as `{rel=\"me\"}` on a profile link, or `{target=\"_self\"}` to keep one link in the tab when the host opens the others in a new one.\n\n## Options\n\n| `anchors()` | Default | What it does |\n| --- | --- | --- |\n| `levels` | `[2, 3, 4]` | The heading levels that get a link. |\n| `label` | `Section titled \"…\"` | A function from the heading's text to what screen readers hear for the link. |\n| `icon` | `\"#\"` | What the link shows. |\n\nOnly a heading with an id gets a link, so under Comark's `headingIds: false` only headings with `{#id}` get one.\n\n| `external()` | Default | What it does |\n| --- | --- | --- |\n| `target` | `false` | Where external links open, such as `\"_blank\"`. |\n| `rel` | `[]` | Words added to external links' `rel`, such as `[\"nofollow\", \"ugc\"]`. |\n| `hosts` | `[]` | Your own hosts, whose links stay internal. Each entry matches one host exactly, so list every subdomain you link to. |\n| `test` | http, https, and `//` links not on `hosts` | A function of the `href` that says whether a link is external. It replaces the default, so `hosts` stops counting. |\n| `icon` | `true` | Draws ↗ after a link that opens a new tab. |\n| `label` | `\"(opens in a new tab)\"` | What screen readers hear after that link. `\"\"` leaves it out. |\n\n## Register\n\n- In `comark-ui`: both on, as `links: { anchors, external }`, and either takes `false` or its options. With no options, `external()` opens nothing in a new tab. comark-chips runs first and turns every web link into a chip, and a chip drops the link's `target` and `rel`, both its own and `external()`'s. Write `{chip=false}` on a web link that needs them, such as `{chip=false rel=\"me\"}`.\n- Alone: `pnpm add comark @comark/html comark-links`, then `createHtmlRenderer({ plugins: [anchors(), external()], components })` with all three from `comark-links/html`, which has no default export. Register either plugin or both, in any order. Either one needs the components, which draw the heading links and style the new-tab warning. `comark-links/vue`, `/react`, and `/svelte` export the same for those renderers.\n"
162
+ },
163
+ {
164
+ "path": "references/lint.md",
165
+ "text": "# Checking documents\n\ncomark-linter reports what Comark forgives, such as a `::` block never closed, a misspelled component, or a link to a missing file. It also reports every problem a plugin puts in `meta.issues`, each under its own rule. Check the result in SKILL.md runs it on one file; this page covers its flags, its config file, the library, ESLint, and Vite Doctor.\n\n## Run the command\n\n```sh\nvp exec lint-comark \"docs/**/*.md\" --format json --check-urls\n```\n\n- Pass files or globs. Quote a glob so the shell passes it as written; lint-comark expands it and skips `node_modules` and `dist`.\n- It prints `file:line:column rule message fix` for each problem, or `No problems.` when there are none. It exits with 0 when clean, 1 on any problem, and 2 when a file can't be read, a glob matches nothing, or the config fails to load.\n- `--format json` prints an array of `{ file, rule, line, column, message, fix }` instead.\n- `--check-urls` also requests every external URL, and reports one that answers 404 or 410, has no DNS record, or fails its TLS check. It caches answers for a day in `node_modules/.cache/comark-linter/urls.json`, except a 404, 410, 429, or 5xx, which it requests again on the next run.\n- `--config lint-comark.config.ts` loads a config. The command finds none on its own.\n\nRun it from the project root. A relative link or image resolves from the document's folder, and a `/` path from the working directory. Each must point at a file that exists, so save the screenshots before you lint.\n\nIt lints with every comark-ui package installed, `comark-ui` first; the [comark-ui reference](ui.md) says what it turns on there. Besides each plugin's own rules, named `comark/<package>/<kind>`, it reports unclosed blocks, unknown components, an unclosed `{` or a key set twice, broken links and anchors, frontmatter that isn't valid YAML, `javascript:` and other unsafe URLs, duplicate ids, images without alt text unless `{alt=\"\"}` marks them as decoration, and empty links. It also reports links that stop working for other readers: localhost, private network, and `file:` URLs, tunnels such as ngrok, and Vercel or Netlify deploy previews. In PR evidence, link the production page or the pull request, and write a local address as code, such as `` `localhost:3000` ``, since Comark links a bare URL.\n\n## Config file\n\n```ts\n// lint-comark.config.ts\nimport { defineConfig } from \"comark-linter\";\nimport include from \"comark-include\";\n\nexport default defineConfig({\n plugins: [include({ root: \"content\" })],\n components: [\"duck-count\"],\n links: { routes: { \"/\": \"content\" }, skip: [\"staging.example.com\"] },\n});\n```\n\n- `plugins` and `components` add to those of the installed packages. `components` takes names, or the components map you render with. Comark keeps the first plugin of each name and drops the rest, so a config can add plugins, such as your own or [include](include.md), and can't change the options of one an installed package registers.\n- It takes every library option below except `file`. Node reads a `.ts` config from 22.18 on; on older versions, write it as `.mjs`.\n\n`links` sets how the linter checks links. It applies `skip`, `block`, and `allow` in that order, then checks the file or the URL:\n\n| Option | What it does |\n| --- | --- |\n| `skip` | Links that skip every check, the built-in block list included. |\n| `block` | Patterns added to the built-in list of local, private, tunnel, and preview URLs. `false` turns that list off. |\n| `allow` | When set, the linter blocks a link to any external host it doesn't match. |\n| `routes` | Where a `/` link finds its file. A record maps URL prefixes to folders from `root`, and the longest prefix wins. A function gets the path and returns the file's path from `root`, `true` for a page with no Markdown to check, or `undefined` when nothing serves it. By default, a `/` link resolves from `root`, as on GitHub. |\n| `urls` | `true`, or these options, does what `--check-urls` does: `timeout` (10000 ms), `concurrency` (8, at most 2 to one host), `retries` (1), `cache` (the file above, or `false`), and `maxAge` (one day, in ms). |\n\nA pattern is a host (`example.com`), its subdomains (`*.example.com`), a host with a port or a path (`example.com:8080`, `example.com/docs`), a RegExp tested against the link as written, or a function that gets the resolved `URL`.\n\n## Library\n\n```ts\nimport { lint, lintFiles, resolveConfig } from \"comark-linter\";\nimport annotate, { components } from \"comark-annotate/html\";\n\nconst diagnostics = await lint(markdown, { plugins: [annotate()], components, file: \"docs/review.md\" });\nconst results = await lintFiles([\"docs/index.md\", \"docs/setup.md\"], await resolveConfig());\n```\n\n- `lint(markdown, options)` returns `{ rule, line, column?, message, fix }[]` in source order and never throws. A rule that crashes reports one diagnostic saying so, and the others still run.\n- Pass `plugins` and `components`, the ones you render with; without `components`, it skips the unknown-component check. Pass `file`, the document's path, to check its links to files. `root` defaults to the working directory, and `read`, `exists`, and `list` replace node:fs, by absolute path. Pass `headingIds` if you render with it, so anchors match, and `links` as above.\n- `lintFiles(paths, options)` parses each file once, checks links between them against each other's headings, and requests each URL once. It returns `{ file, text, diagnostics }` for each path, with no `text` when the file can't be read.\n- `resolveConfig(config)` returns the installed packages' plugins and components plus the config's, as the command uses them. `rules` maps every rule to its title.\n\n## ESLint\n\n```js\n// eslint.config.js\nimport comark from \"comark-linter/eslint\";\n\nexport default [comark.configs.recommended, { settings: { comark: { config: \"lint-comark.config.mjs\" } } }];\n```\n\nUse a JavaScript ES module (`export default { ... }`) for the linter config across supported Node versions. A `.ts` config requires Node 22.18+ or `--experimental-strip-types` on Node 22.12–22.17, including in the editor. Restart ESLint after editing the config or its imports; they are cached for the process lifetime.\n\nInstall `eslint` (9.15 or later) and `@eslint/markdown` (8 or later). The recommended config lints `**/*.md` and `**/*.mdc` with `@eslint/markdown`'s `markdown/commonmark` language and YAML frontmatter, and turns on every rule but `comark/dead-url`, which requests every URL; turn that one on in `rules` to check them. Each rule has the name the command prints and is configured like any ESLint rule, `<!-- eslint-disable-next-line comark/unknown-component -->` included. ESLint prints the fix after the message. `settings.comark.config`, from the working directory, loads the same config file as `--config`. oxlint can't run it: its JS plugins don't lint `.md` files, so run `lint-comark` beside `oxlint`.\n\n## Vite Doctor\n\n```ts\n// doctor.config.ts\nimport comark from \"comark-linter/doctor\";\nimport { defineDoctorConfig } from \"vite-doctor/config\";\n\nexport default defineDoctorConfig({\n extends: [\"comark/recommended\"],\n extensions: [comark({ include: \"content/**/*.md\" })],\n});\n```\n\nRun `vite-doctor . --config doctor.config.ts --max-warnings 0`. The extension takes the config file's options plus `include` (default `**/*.{md,mdc}`) and `exclude` (default `node_modules` and `dist`), and needs vite-doctor 0.1 or later. Each rule reports under its own code, from `COMARK0001` for the core rules and a hundred per package. Unsafe URLs, invalid frontmatter, and paths that leave a plugin's `root` are errors; the rest are warnings, which Vite Doctor passes unless you add `--max-warnings 0`.\n\n## Register\n\n- In `comark-ui`: not included. Install it beside the preset, which it finds on its own.\n- Alone: `pnpm add -D comark-linter`. The command is `lint-comark`, the library `comark-linter`, the ESLint plugin `comark-linter/eslint`, and the extension `comark-linter/doctor`. npm refused `comark-lint` as too close to `remark-lint`, hence the name.\n"
166
+ },
167
+ {
168
+ "path": "references/magic-move.md",
169
+ "text": "# Magic move\n\ncomark-magic-move animates code from one step to the next, as Shiki Magic Move does: the words and marks two steps share move to their new places, new ones fade in, and old ones fade out. Use it to tell a change to code one move at a time, such as a refactor in a tutorial.\n\n## Syntax\n\n````md\n::magic-move{title=\"Extract the rate\"}\n```ts [Start]\nconst total = price * 2\n```\n\n```ts [Extract the rate]\nconst rate = 2\nconst total = price * rate\n```\n::\n````\n\n- `::magic-move`: every code block inside is one step, in order. Anything else inside is dropped and reported, and a block with fewer than two code blocks is reported too. `title` names its tabs for screen readers.\n- The `[label]` after a fence's language names its step's tab, `Step 2` and so on without one. It isn't a file name there.\n- The steps are tabs over one code block, like a code group. Opening a tab moves the code there, from any step to any other.\n- Without the script, as in a feed reader, an email, or a screenshot of the page, the steps read one after another under their labels.\n\n## Choose the steps\n\n- Change one thing per step. When most of the code moves at once, the reader can't follow any of it.\n- Keep the code that stays the same identical, spaces aside, so it moves rather than fading out and in.\n- For a change a reviewer approves, write `::diff`: it shows every removed and added line at once, which an animation can't.\n\n## Register\n\n- In `comark-ui`: on, as `magicMove`, with no options.\n- Alone: `pnpm add comark @comark/html comark-magic-move`, then `createHtmlRenderer({ plugins: [magicMove()], components })` with both from `comark-magic-move/html`. `comark-magic-move/vue`, `/react`, and `/svelte` export the same pair for those renderers.\n"
170
+ },
171
+ {
172
+ "path": "references/mark.md",
173
+ "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
+ },
175
+ {
176
+ "path": "references/math.md",
177
+ "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"
178
+ },
179
+ {
180
+ "path": "references/media.md",
181
+ "text": "# Videos and audio\n\ncomark-media plays an image that points at a video or a sound file, and a GitHub attachment link alone on its line, with the alt text as the player's label. Use it for a screen recording in PR evidence: a transition, a loading state, or a sound, which a screenshot can't show.\n\n## Syntax\n\n```md\nThe spinner now stops once the build finishes:\n\n![The build spinner stops and the Deploy button turns green](./recordings/build.mp4){loop muted}\n```\n\n- A `src` that ends in `.mp4`, `.webm`, `.mov`, or `.m4v`, before any `?` or `#`, plays as a video, and one in `.mp3`, `.wav`, `.ogg`, or `.m4a` as audio. Every other image stays an image.\n- The alt text is required. It names the player for screen readers, and it's the text of a link to the file where the browser can't play it.\n- `autoplay`, `loop`, and `muted` are off by default. Browsers start a file by itself only when it's muted, so `autoplay` needs `muted`. `poster=\"frame.png\"` shows an image before a video plays; audio takes none. `{loop=false}` turns a prop off. A video's `width` and `height` must be paired whole pixel dimensions, like `{width=1280 height=720}`, to reserve its room before loading. Audio takes neither. Other props, such as a class, are dropped and reported.\n- A GitHub attachment link, `https://github.com/user-attachments/assets/<id>`, plays as a video when the bare URL sits alone in its paragraph, as on GitHub. With text of its own, or inside a sentence, it stays a link.\n\n## Record the evidence\n\n- Record only the part of the screen that changes, for a few seconds.\n- Encode it as H.264 `.mp4` for every browser, or `.webm` for a smaller file. A `.mov` from macOS doesn't always play in Firefox.\n- Say what happens in the alt text, so a reader who can't play the file still learns what it shows.\n- Add `loop muted` to a clip of one motion, so it reads like an animated screenshot once played.\n- A GitHub attachment from a private repository plays only for readers signed in with access to it. For a page that others read, download the file and serve it with the page.\n\n## Register\n\n- In `comark-ui`: on. `media: { controls: \"native\" }` keeps the browser's own controls and leaves the script out. Measurement reads only data URIs by default. `media: { size: true }` also measures remote videos while parsing; use it only for trusted documents, since requests follow redirects and can reach private networks. `media: { size: false }` disables measurement. Authored dimensions work with any setting. Alone, pass the same options to `media()`.\n- Alone: `pnpm add comark @comark/html comark-media`, then `createHtmlRenderer({ plugins: [media()], components })` with both from `comark-media/html`. `comark-media/vue`, `/react`, and `/svelte` render it too. Its place in `plugins` doesn't matter. It reads the tokens before any plugin sees the tree, so comark-zoom never zooms a video and comark-chips never turns an attachment link into a chip. The HTML renderer inlines the players' script once per document, and the browser's own controls show until it runs and whenever a file can't play.\n"
182
+ },
183
+ {
184
+ "path": "references/metrics.md",
185
+ "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- `: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
+ },
187
+ {
188
+ "path": "references/outline.md",
189
+ "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 rather than narrow a 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. The npm name is `comark-outline` because npm refused `comark-toc`.\n"
190
+ },
191
+ {
192
+ "path": "references/properties.md",
193
+ "text": "# A properties box from the frontmatter\n\ncomark-properties shows chosen frontmatter keys, such as a review's status, pull request, author, date, and tags, as a box of properties on the page. Use it when a reviewer should scan those facts first, and a script should read them from the frontmatter too.\n\n## Syntax\n\n```md\n---\nstatus: Ready for review\npr: https://github.com/onmax/comark-ui/pull/111\nauthor: onmax\ndate: 2026-10-23\ntags: [colors, docs]\n---\n\n# Palette page\n\n::properties\n::\n\nThe palette page now shows every token of the theme.\n```\n\n- The box shows the host's `keys`, by default `status`, `pr`, `author`, `date`, and `tags`, in that order. A key that's missing or empty is skipped, and the rest of the frontmatter, such as a layout or a slug, stays off the page.\n- The box goes first in the document. To put it somewhere else, such as under the title, write `::properties` there and close it with `::` on the next line. It takes no props and no content; choosing keys is the host's job.\n- Text, a number, `true`, and `false` show as written, and so does a date such as `2026-10-23`. A GitHub or GitLab pull request, merge request, or issue link shows as `owner/repo#123`, any other http(s) link as its host and path, and a list as one badge per item. An object or a list of lists doesn't show, and is reported.\n- Each label is its key capitalized, with `pr` as PR, unless the host names it.\n\n## Register\n\n- In `comark-ui`: on, with the default keys, so a document whose frontmatter has any of them gets the box at the top. `properties: { keys: [\"status\", \"owner\", \"due\"], labels: { due: \"Due date\" } }` picks the keys and their order and names their labels. `top: false` shows the box only where `::properties` sits.\n- Alone: `pnpm add comark @comark/html comark-properties`, then `createHtmlRenderer({ plugins: [properties()], components })` with both from `comark-properties/html`. It takes the same options. `comark-properties/vue`, `/react`, and `/svelte` render it too.\n"
194
+ },
195
+ {
196
+ "path": "references/qr.md",
197
+ "text": "# QR codes\n\ncomark-qr draws `:qr` as a QR code, an inline SVG encoded while parsing, with no script and no image to fetch. Put one beside a preview link, so a reviewer at a desk clicks and one with a phone scans.\n\n## Syntax\n\n```md\n| Preview | Phone |\n| --- | --- |\n| [pr-123.preview.example.com](https://pr-123.preview.example.com) | :qr{value=\"https://pr-123.preview.example.com\"} |\n```\n\n`value` is the one prop, and it's required: the text to encode, usually a link, up to about 2,300 characters. `:value=\"frontmatter.preview\"` takes it from the frontmatter. `:qr` is inline, so it sits in a sentence, a table cell, or a list item. Write a caption outside it, since the plugin drops text in its brackets. A `:qr` with no `value`, or one too long to encode, renders nothing and is reported.\n\nThe code is black on white in both color schemes and 8rem wide, with error correction level M and a two-module quiet zone. Screen readers announce it as \"QR code:\" followed by its value. Encode the same link the text beside it shows, so both open one page.\n\n## Register\n\n- In `comark-ui`: on. It takes no options.\n- Alone: `pnpm add comark @comark/html comark-qr`, then `createHtmlRenderer({ plugins: [qr()], components })` with both from `comark-qr/html`. `comark-qr/vue`, `/react`, and `/svelte` render it too. It has no options and no script; `--cqr-size` on `.cqr` sets the width.\n"
198
+ },
199
+ {
200
+ "path": "references/quiz.md",
201
+ "text": "# Lessons and quizzes\n\ncomark-quiz adds `::question`, `::poll`, and `::recall` blocks for interactive lessons. Questions check one or more answers and explain each option, polls collect votes, and recall cards reveal an answer for self-testing. Without its script, the prompt, options, and answer disclosure remain 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\nA question uses radios when one option is marked `[x]`; `multiple` makes it a checkbox question. Options are shuffled deterministically unless `shuffle=\"false\"`. Questions and recall cards share numbering, while polls are not numbered. A missing id is derived from the block content. Use `#hint` and `#explain` on questions, and `#answer` on recall cards. Polls accept `multiple` and a date in `closes`.\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`. The HTML renderer is required because the blocks need its inlined script; there are no Vue, React, or Svelte entries.\n"
202
+ },
203
+ {
204
+ "path": "references/reading-time.md",
205
+ "text": "# Reading time\n\ncomark-reading-time counts a document's words while parsing and writes them to `meta.readingTime` as `{ words, minutes }`. It has no syntax and draws nothing. The host's layout shows the numbers, such as \"7 min read\" under the title.\n\n## What it counts\n\n- The text a reader reads. It leaves out code blocks, frontmatter, and comments, and counts inline code.\n- Each Chinese, Japanese, or Korean character counts as a word, since those languages put no spaces between words.\n- `minutes` is `words` over `wordsPerMinute`, rounded up, so any text takes at least a minute. It is 0 only for a document with no words.\n- `countWords(text)`, also exported, counts any string the same way, such as an excerpt.\n\n## Show it\n\nWith `@comark/html`, parse once, read the meta, and render that document:\n\n```ts\nimport { renderHtmlFromDocument } from \"@comark/html\";\nimport { parseMarkdown } from \"comark\";\nimport comarkUi from \"comark-ui/html\";\n\nconst { plugins, components } = comarkUi();\nconst document = await parseMarkdown(markdown, { plugins });\nconst body = await renderHtmlFromDocument(document, { components });\nconst html = `<p class=\"byline\">${document.meta.readingTime.minutes} min read</p>${body}`;\n```\n\nWith `@comark/vue` or `@comark/react`, read `meta.readingTime` from the parsed document you pass to `MarkdownDocument`.\n\n## Register\n\n- In `comark-ui`: on in every entry. `readingTime: { wordsPerMinute: 230 }` sets the speed. The default, 200, is a cautious pace for technical prose; 230 to 250 suits light reading, and a site mostly in CJK may want more.\n- Alone: `pnpm add comark comark-reading-time`, then `parseMarkdown(markdown, { plugins: [readingTime()] })` with `readingTime` from `comark-reading-time`. It has no components, so it works with any renderer.\n"
206
+ },
207
+ {
208
+ "path": "references/sources.md",
209
+ "text": "# Citations and sources\n\ncomark-sources turns `:cite[n]` into a numbered link to the nth source in the document's `::sources` list, and gives each source a link back to every citation of it. Use it when the claims in a document rest on links, such as the research behind a change.\n\n## Syntax\n\n```md\nPigeons find home by the sun and by the earth's magnetic field :cite[1], and some by smell too :cite[2,3].\n\n::sources\n1. [Magnetoreception in homing pigeons](https://en.wikipedia.org/wiki/Magnetoreception)\n2. [Olfactory navigation](https://en.wikipedia.org/wiki/Pigeon_intelligence), on smell\n3. [How do homing pigeons navigate?](https://www.sciencefocus.com/nature/how-do-homing-pigeons-navigate)\n::\n```\n\n- `:cite[2]` cites the second source, and `:cite[1,3]` several. Each becomes a superscript `[1]` that links to its source. Hovering or focusing it shows a card with the source's title, its site, and the icon the list shows for it, a globe when there's none. Put a space before `:cite`, which the plugin removes; `word:cite[1]` stays text, and nothing reports it.\n- `::sources` holds one numbered list with one source per item, each with its link. Sources count by their place in the list. Text after the link, such as `, on smell`, stays in the item. `title` names the list, \"Sources\" by default. The plugin drops anything beside the list and reports it.\n- A document has one `::sources`, anywhere in it, and citations before and after it both count. A citation of a number the list doesn't have, a source with no link, and a second list are reported.\n\n## Register\n\n- In `comark-ui`: on. It takes no options.\n- Alone: `pnpm add comark @comark/html comark-sources`, then `createHtmlRenderer({ plugins: [sources()], components })` with both from `comark-sources/html`. `comark-sources/vue`, `/react`, and `/svelte` render it too. Citations are plain `sup` links that work in any renderer, and the `sources` component draws the list and carries the styles for both. It has no options. The HTML renderer inlines the card's script once per document, and the components start it when they mount.\n"
210
+ },
211
+ {
212
+ "path": "references/steps.md",
213
+ "text": "# Numbered steps\n\ncomark-steps turns the headings inside `::steps`, or one numbered list, into numbered steps joined by a line. Use it for instructions the reader follows in order, such as how to reproduce a bug or set up a project.\n\n## Syntax\n\n```md\n::steps\n### Open the dashboard\nGo to **Correlations** and pick the Berlin office.\n\n### Add three filters\nAdd **Moon phase**, **Vibes**, and **Rubber ducks**.\n\n### Narrow the window\nAt 1024 px wide, **Apply** drops under the filter chips.\n::\n```\n\n- Each `###` heading starts a step that holds everything up to the next one, code blocks and images included. The heading keeps its id, so a link to `#add-three-filters` and a table of contents still find it.\n- `level` is the level of the headings that start each step, from 2 to 6, default 3. Write `::steps{level=\"2\"}` for `##` headings. Deeper headings stay inside their step.\n- A block whose only content is one numbered list makes a step of each item, and a list that starts at 3 numbers its steps from 3. Use the list when every step fits on one line.\n- A `:::steps{level=\"4\"}` inside a step keeps its own headings and numbers.\n\nThe syntax is Nuxt UI's `::steps`, so the same Markdown renders under Nuxt UI.\n\n## Keep only steps inside\n\nPut any introduction above `::steps`, since the block holds only steps. A paragraph before the first heading renders above the steps, and a paragraph beside a numbered list turns the whole block into plain content. Both are reported. The block drops its own `#id` and `.class` without a report, so link to a step through its heading's id.\n\n## Register\n\n- In `comark-ui`: on, as `steps`, with no options.\n- Alone: `pnpm add comark @comark/html comark-steps`, then `createHtmlRenderer({ plugins: [steps()], components })` with both from `comark-steps/html`. `comark-steps/vue`, `/react`, and `/svelte` export the same pair for those renderers. It takes no options and goes anywhere in `plugins`.\n"
214
+ },
215
+ {
216
+ "path": "references/table.md",
217
+ "text": "# Tables from CSV, TSV, and JSON\n\ncomark-table turns CSV, TSV, or JSON from a fence or a file into a plain HTML table with a Copy button, which pastes as cells in a spreadsheet, and a menu that copies it as Markdown or CSV. With `sort`, its headers sort the rows. Use it for benchmark results, exports, and any data a tool wrote, so the page shows the numbers the run produced.\n\n## Syntax\n\n````md\n::table{sort}\n```csv\nBenchmark,Before (ms),After (ms)\nParse the duck census,\"1,204\",980\nRender the pond map,88,91\n```\n::\n````\n\nOr read the file the run wrote, and leave the block empty:\n\n```md\n::table{src=\"bench/results.csv\" sort}\n::\n```\n\n- A ` ```csv ` or ` ```tsv ` fence turns into a table anywhere, with the Copy button and no sorting. Wrap it in `::table` to set the props below.\n- A ` ```json ` fence stays code unless it sits inside `::table`. It holds an array of objects, one per row. Every key becomes a column, in the order first seen, and a nested value shows as JSON.\n- `src` starts at the document's folder, or at the host's `root` when it starts with `/`. It can't leave `root` or be a URL, and the extension picks the format. The plugin drops content beside `src` and reports it.\n- A Markdown table inside `::table{sort}` or `::table{copy}` gets the Copy button. A `::table` with no props renders its Markdown table plain.\n\n| Prop | What it does |\n| --- | --- |\n| `src` | A `.csv`, `.tsv`, or `.json` file to read. |\n| `format` | `csv`, `tsv`, or `json`, for a file or fence whose extension or language doesn't say. |\n| `sort` | Makes every header a button that sorts the rows, up then down. Numbers sort as numbers. |\n| `copy` | On by default. `copy=false` hides the Copy button and its menu. |\n\nCSV follows RFC 4180. A cell in double quotes may hold commas and line breaks, and `\"\"` is a quote inside one. TSV splits at tabs and has no quoting. The first row is the header, and every row needs one cell per column. A column whose cells are all numbers, with commas or a short unit such as `12 ms`, lines up on the right. A fence the plugin can't read stays code, and the plugin says why.\n\n## Fit the data to the page\n\n- Every row becomes HTML, so trim a large export to the rows the reader needs before you write it. A table wider than the page scrolls sideways in its frame.\n- `src` can't fetch a URL. Download the data next to the document, or paste it in a fence.\n- To number a table with [xref](xref.md), wrap a Markdown table in `::table{#tbl-name caption=\"...\"}` with no other props. A table that comark-table draws loses its `id`, so comark-xref reports every `:ref` to it.\n- To draw the numbers, write a `::chart` around a Markdown table instead, as [charts](charts.md) shows.\n\n## Register\n\n- In `comark-ui`: on. Fences render, and every `src` reports a missing file until you pass `table: { root: \"docs\" }` or `{ read }`.\n- Alone: `pnpm add comark @comark/html comark-table`, then `createHtmlRenderer({ plugins: [table({ root: \"docs\" })], components })` with both from `comark-table/html`. `root` is the folder `src` can't leave, default `.`, the working directory. `base` is the document's folder under it, where relative paths start. `read(path)` returns a file's text, for browsers and edge runtimes. `fences: false` keeps csv and tsv fences as code. `comark-table/vue`, `/react`, and `/svelte` render it too.\n"
218
+ },
219
+ {
220
+ "path": "references/tabs.md",
221
+ "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, and comark-code's `title=\"pnpm\"` names no tab.\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, update, run, `npx`/`dlx`, create, and init. 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"
222
+ },
223
+ {
224
+ "path": "references/tests.md",
225
+ "text": "# Test results\n\ncomark-tests reads a JUnit XML report into one line of passed, failed, and skipped counts with the run's duration, and folds each failure under it with its message and stack. Use it when a pull request or a CI summary reports a test run, so the reader sees the run itself.\n\n## Syntax\n\n````md\n::tests{title=\"Pond census tests\"}\n```xml\n<testsuite name=\"pond.test.ts\" time=\"0.42\">\n <testcase classname=\"pond.test.ts\" name=\"counts the ducks\" time=\"0.01\"/>\n <testcase classname=\"pond.test.ts\" name=\"counts the geese\" time=\"0.02\">\n <failure message=\"expected 3 to be 4\">AssertionError: expected 3 to be 4\n at pond.test.ts:12:20</failure>\n </testcase>\n <testcase classname=\"pond.test.ts\" name=\"maps the herons\"><skipped/></testcase>\n</testsuite>\n```\n::\n````\n\nOr read the report from a file, and leave the block empty:\n\n```md\n::tests{src=\"reports/junit.xml\" title=\"CI run\"}\n::\n```\n\n| Prop | What it does |\n| --- | --- |\n| `src` | The JUnit XML file to read. The path starts at the document's folder, or at the host's `root` when it starts with `/`, and it can't leave `root` or be a URL. Without `src`, the block holds one fence with the report. |\n| `title` | Shown before the counts. It also names the results for screen readers, which hear \"Test results\" without one. |\n\nThe block shows:\n\n- The counts of `<testcase>` elements. A case with `<skipped>` is skipped, one with `<failure>` or `<error>` failed, and the rest passed. Failed and skipped show only when there are any.\n- The duration, from the `time` of `<testsuites>`, else the sum of the suites' times, else the cases', written as `250 ms`, `4.2 s`, or `1 min 21 s`. A report without times shows none.\n- Each failure as a closed disclosure named `classname › name`, with its `message` and its stack in a code block. Failures print open.\n\nThe root is `<testsuites>` or a single `<testsuite>`. A report with another root renders nothing, and so does malformed XML, which the plugin reports with its line.\n\n## Use the run's own report\n\nPaste or point at the report the runner wrote, unedited, so the counts are the run's. Most runners write JUnit XML with a flag:\n\n| Runner | Command |\n| --- | --- |\n| Vitest | `vitest run --reporter=junit --outputFile=junit.xml` |\n| Jest | `jest --reporters=jest-junit` |\n| pytest | `pytest --junitxml=junit.xml` |\n| Go | `go test -v ./... 2>&1 \\| go-junit-report > junit.xml` |\n| Playwright | `PLAYWRIGHT_JUNIT_OUTPUT_NAME=junit.xml playwright test --reporter=junit` |\n\nWhen the host reads files, have CI write the report next to the document and point `src` at it. Otherwise paste it into the fence. A long report stays short on the page, since the summary is one line and each failure stays closed until a reader opens it.\n\n## Register\n\n- In `comark-ui`: on. A pasted report renders, and every `src` reports a missing file until you pass `tests: { root: \".\" }` or `{ read }`.\n- Alone: `pnpm add comark @comark/html comark-tests`, then `createHtmlRenderer({ plugins: [tests({ root: \".\" })], components })` with both from `comark-tests/html`. `root` is the folder `src` can't leave, default `.`, the working directory. `base` is the document's folder under it, where relative paths start. `read(path)` returns a file's text, for browsers and edge runtimes. The output needs no script. `comark-tests/vue`, `/react`, and `/svelte` render it too. With comark-code registered, each stack gets a Copy button.\n"
226
+ },
227
+ {
228
+ "path": "references/ui.md",
229
+ "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. Install in SKILL.md shows its basic use. This page covers its options, what it turns on, its 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`. `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 last, 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- By default, three plugins use the network while parsing: `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 }`, since 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; `annotate: { size: true }` does so at HTML render time. Enable these only for trusted documents, since requests follow redirects and can reach private networks. `{ size: false }` disables measurement for each 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- comark-critic has no component to add its styles, so put `criticCss`, which every entry exports, in the page.\n- The text around the blocks is styled by default, 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"
230
+ },
231
+ {
232
+ "path": "references/xref.md",
233
+ "text": "# Numbered figures and tables\n\ncomark-xref numbers labeled images and tables in document order, and turns `:ref{#id}` into a link that reads \"Figure 2\". Use it when the text refers to its images or tables by number, so the numbers stay right when you add, remove, or reorder them.\n\n## Syntax\n\n```md\nThe empty state in :ref{#fig-empty} replaces the spinner in :ref{#fig-loading}, and :ref{#tbl-load} has the load times.\n\n![The new empty state on the correlations page](empty.png){#fig-empty}\n\n![The old spinner on the correlations page](loading.png){#fig-loading}\n\n::table{#tbl-load caption=\"Time to first row, in ms\"}\n| Page | Before | After |\n| --- | --: | --: |\n| Correlations | 1840 | 620 |\n::\n```\n\n- `{#fig-name}` after an image alone in its paragraph makes it a `figure`, captioned \"Figure 1: \" and its alt text. The plugin doesn't number an image that shares its paragraph with text, and reports it.\n- `::table{#tbl-name caption=\"...\"}` around one Markdown table captions it \"Table 1: \" and its `caption`. A Markdown table takes no id on its own.\n- A label is `fig-` or `tbl-`, then letters, digits, `_`, or `-`. The plugin numbers figures and tables apart.\n- `:ref{#id}` becomes a link that reads \"Figure 1\" or \"Table 1\", to a label above or below it. The plugin drops text in `:ref[...]` and renders a `:ref` to an unknown label as `[id]`. It reports both, and a label used twice.\n\n## Label a screenshot\n\nThe plugin also numbers any other element with a `fig-` id as a figure, such as a `::figure` you caption yourself, and adds no caption to it. It numbers a `::screenshot` or `::compare` too, but their HTML drops the id, so the link leads nowhere. Wrap the screenshot in a `::div` that carries the label:\n\n```md\n:ref{#fig-toolbar} shows the toolbar on one row.\n\n::div{#fig-toolbar}\n:::screenshot{src=\"after.png\" alt=\"Filter toolbar on one row\"}\n:::\n::\n```\n\n## With other plugins\n\n- Leave the title off a labeled image, since its alt text is the caption. comark-figure, on in `comark-ui`, makes a titled image its own figure first, and xref then reports it as sharing its paragraph.\n- Keep comark-table's `sort`, `copy`, and `src` off a labeled `::table`. With them, comark-table takes the block, drops the label, and reports `caption` as an unknown prop.\n- Write `:ref{#fig-name}` where Pandoc writes `@fig-name`, which comark-chips and comark-github read as a mention.\n\n## Register\n\n- In `comark-ui`: on in every entry. `xref: { figure: \"Abbildung\", table: \"Tabelle\" }` sets the words before the numbers, `\"Figure\"` and `\"Table\"` by default.\n- Alone: `pnpm add comark comark-xref`, then `createHtmlRenderer({ plugins: [xref()] })` with `xref` from `comark-xref`. It has no components and no styles, so it works with any renderer. Its output is `figure`, `figcaption`, `caption`, and `a`, with the classes `cxr`, `cxr-label`, and `cxr-ref` for your own rules.\n"
234
+ },
235
+ {
236
+ "path": "references/zoom.md",
237
+ "text": "# Zoom\n\ncomark-zoom lets the reader click an image or a `::zoom` block to see it large, and closes it with Escape, Back, or a click around it. Images zoom with no markup. Wrap a table, code block, or diagram in `::zoom` when it's too small to read in place.\n\n## Syntax\n\n```md\n::zoom{title=\"Deploy retry policy\"}\n| Attempt | Wait before it |\n| --- | --- |\n| 1 | 1 s |\n| 2 | 4 s |\n| 3 | 16 s |\n::\n```\n\n- `::zoom` zooms everything inside it as one item. `title` heads the zoomed view. Items with the same `group` page through one zoomed view, with a strip of them below. `#id` and `.class` go on the block.\n- Every content image zooms on its own, with its alt text as the zoomed view's heading. It grows no larger than its file, so give a small image a larger file to open with `{data-zoom-src=\"dashboard@2x.png\"}`.\n- An image inside a link keeps its link and doesn't zoom. Images inside `::zoom`, `::carousel`, `::screenshot`, or `::compare` zoom with their block.\n- `{zoom=false}` keeps an image from zooming, and `{data-zoom}` makes any element zoom on its own.\n\ncomark-zoom reports no problems, so a misspelled prop such as `titel` is dropped silently and passes the linter. Spell `title` and `group` as written here.\n\n## Group or carousel\n\nA `group` leaves each item in its place in the text and pages through them only once one is zoomed. Use it for screenshots that sit apart, each next to the paragraph about it. A `::carousel` puts the items in one place with arrows instead. An item hidden when the view opens, such as one in a closed tab, stays out of its group.\n\n## In the page\n\nThe zoomed view fires `comark-zoom:open`, `comark-zoom:change`, and `comark-zoom:close` on the item, and they bubble. The `detail` of `open` and `change` holds the dialog's `header` and `footer`, a `layer` over the zoomed content where a host draws its own pins, and `copyOf(element)` and `originalOf(element)` to map between the page and the zoomed copy. `data-zoom-aside`, a selector, shows a caption beside the zoomed item at its own size, as annotate does with a screenshot's notes.\n\n## Register\n\n- In `comark-ui`: on, as `zoom`. `zoom: { images: false }` leaves images alone, so only `::zoom` blocks and elements marked `{data-zoom}` zoom. Data URI images get dimensions by default; one authored pixel side gets the other at the file's ratio. Images with both sides or a side such as `50%` keep them. `zoom: { size: true }` also measures remote images while parsing; use it only for trusted documents, since requests follow redirects and can reach private networks. `zoom: { size: false }` disables measurement. Alone, pass the same options to `zoom()`.\n- Alone: `pnpm add comark @comark/html comark-zoom`, then `createHtmlRenderer({ plugins: [zoom()], components })` with both from `comark-zoom/html`. `comark-zoom/vue`, `/react`, and `/svelte` export the same pair for those renderers. The plugin adds a node that starts the script, so pass the components even when only images zoom. A page with no renderer calls `start()` from `comark-zoom/client` once. comark-annotate and comark-carousel zoom their own blocks without it, so register `zoom()` for images and `::zoom`.\n"
238
+ }
239
+ ]
240
+ },
241
+ {
242
+ "name": "review-document",
243
+ "description": "Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, an audit, or any report whose reader must decide what to do next.",
244
+ "frontmatter": {
245
+ "name": "review-document",
246
+ "description": "Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, an audit, or any report whose reader must decide what to do next.",
247
+ "license": "MIT"
248
+ },
249
+ "files": [
250
+ {
251
+ "path": "SKILL.md",
252
+ "text": "---\nname: review-document\ndescription: Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, an audit, or any report whose reader must decide what to do next.\nlicense: MIT\n---\n\n# Review document\n\nA review's reader has one question: what do I do now? Answer it in the first two lines, then give each finding just enough evidence that the reader trusts it without redoing your work. This skill organizes findings you already have; finding them is the review itself.\n\nLength is the usual failure. A reader who has to dig through praise, process, and restated findings misses the one that blocks the merge. Every line below the verdict either supports a finding or says what you didn't check.\n\n## 1. Decide the verdict\n\nPick one: **approve**, **approve with changes**, **request changes**, or **blocked** on a named thing. Under it, write the reason in one sentence that names the finding behind it.\n\nDone when someone who reads only the verdict knows what to do.\n\n## 2. Keep the findings that change the decision\n\n- Keep a finding when fixing it, or not, changes what the reader should do.\n- Merge findings with one root cause into one, and list every place it appears.\n- Rank each one: **blocking** (wrong results, lost data, a security hole, broken for users), **should fix** (a real cost that can wait), or **nit**, only when the reader asked for them.\n\nOrder them blocking first. Done when every finding has a rank and no two share a cause.\n\n## 3. Back each finding\n\nEach finding is four parts, in this order:\n\n1. **A title that states the consequence**, for whom: \"Small avatars load the 512px image\", not \"Cache key issue\".\n2. **Where**: the file and line, linked when there's a URL.\n3. **Evidence**, the strongest you have: a failing test or command you ran (`::tests`, a fenced block of output), a screenshot of the wrong state (measured as the before-after-evidence skill describes), the lines that cause it (a fence with the line highlighted), then reasoning. Say which it is: a finding you reproduced reads differently from one you read in the code.\n4. **The fix**: a `::diff` of the smallest change, or one sentence when the fix is a decision for the reader.\n\nDone when every blocking finding has evidence you ran or captured, or says plainly that it comes from reading the code.\n\n## 4. Say what you checked\n\nEnd with two short lists: what you checked and how, and what you didn't check. A review that implies more coverage than it had loses the reader's trust in the parts that were real.\n\n## 5. Write it\n\nFollow [references/example.md](references/example.md):\n\n- The verdict in a callout: `> [!CAUTION]` for request changes or blocked, `> [!WARNING]` for approve with changes, `> [!TIP]` for approve.\n- One `##` heading per finding, numbered, with its rank in the heading. About a dozen lines each, evidence included.\n- No summary that repeats the findings, no section for an empty rank, no list of what's good unless the reader asked.\n\n[references/blocks.md](references/blocks.md) has the syntax for the callout, `::tests`, `::diff`, and highlighted lines.\n\n## 6. 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 lint-comark review.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 rendered page where it will be read and read it top to bottom as the reader would. Each finding's evidence should be visible without expanding anything except a long log, and each `::diff` should start at the line its finding names.\n\nDone when the page renders without problems and you've read it once as the reader.\n"
253
+ },
254
+ {
255
+ "path": "agents/openai.yaml",
256
+ "text": "interface:\n display_name: \"Review document\"\n short_description: \"Write findings as a verdict, evidence, and fixes\"\n"
257
+ },
258
+ {
259
+ "path": "references/blocks.md",
260
+ "text": "# Blocks for a review document\n\nThese blocks come with `comark-ui`, or alone with `comark-github`, `comark-tests`, `comark-diff`, and `comark-code`. The comark-ui skill (`vp dlx skills add onmax/comark-ui`) documents every block in full.\n\n## The verdict: a callout\n\n```md\n> [!CAUTION] Request changes\n> One blocking finding: after a large avatar loads, every smaller one shows the large image.\n```\n\nThe text after the marker is the title. `[!CAUTION]` fits request changes or blocked, `[!WARNING]` approve with changes, `[!TIP]` approve, and `[!NOTE]` context the reader needs before the findings. Any other marker stays a plain quote and is reported.\n\n## A test run: `::tests`\n\nPaste the JUnit XML the runner wrote, unedited, so the counts are the run's own:\n\n````md\n::tests{title=\"pnpm test src/avatars\"}\n```xml\n<testsuite name=\"cache.test.ts\" time=\"0.18\">\n <testcase classname=\"cache.test.ts\" name=\"returns a cached avatar\"/>\n <testcase classname=\"cache.test.ts\" name=\"returns a 32px avatar after a 512px one\">\n <failure message=\"expected 512 to be 32\">AssertionError: expected 512 to be 32</failure>\n </testcase>\n</testsuite>\n```\n::\n````\n\nIt shows the passed, failed, and skipped counts and each failure's message and stack. Most runners write the report with a flag: `vitest run --reporter=junit --outputFile=junit.xml`, `pytest --junitxml=junit.xml`, `jest --reporters=jest-junit`. For a command that isn't a test run, paste its output in a `txt` or `sh` fence instead.\n\n## The lines that cause it: a fence with `mark`\n\n````md\n```ts [src/avatars/cache.ts] mark={1}\nconst cache = new Map<string, Blob>();\n```\n````\n\n`mark={1}` highlights the block's first line; ranges look like `{3-4}` or `{1, 3-4}`. Copy a few lines from the file, the ones your text names.\n\n## The fix: `::diff`\n\n````md\n::diff{line=18 label=\"Fix\"}\n```ts [src/avatars/cache.ts]\nconst key = user.id;\n```\n```ts\nconst key = `${user.id}:${size}`;\n```\n::\n````\n\nTwo fenced blocks, before then after, starting at the file's line `line`. One file per diff, a few lines each.\n\n## A long log: `<details>`\n\n```md\n<details>\n<summary>Full test output</summary>\n\n…\n\n</details>\n```\n\nFold away anything the reader may skip, such as a full log under the failure that matters.\n"
261
+ },
262
+ {
263
+ "path": "references/example.md",
264
+ "text": "# Review: avatar cache\n\n> [!CAUTION] Request changes\n> One blocking finding: after a large avatar loads, every smaller one on the page shows the large image. The fix is one line.\n\nReviewed [acme/web#418](https://github.com/acme/web/pull/418) at `9f3c2e1`.\n\n## 1. Small avatars load the 512px image (blocking)\n\n`src/avatars/cache.ts:18`. The cache key is the user's id alone, so once the profile header loads someone's 512px avatar, the 32px avatars in comments get the same file. Reproduced with a test:\n\n::tests{title=\"pnpm test src/avatars\"}\n```xml\n<testsuite name=\"cache.test.ts\" time=\"0.18\">\n <testcase classname=\"cache.test.ts\" name=\"returns a cached avatar\" time=\"0.01\"/>\n <testcase classname=\"cache.test.ts\" name=\"returns a 32px avatar after a 512px one\" time=\"0.02\">\n <failure message=\"expected 512 to be 32\">AssertionError: expected 512 to be 32\n at cache.test.ts:27:31</failure>\n </testcase>\n</testsuite>\n```\n::\n\n::diff{line=18 label=\"Fix\"}\n```ts [src/avatars/cache.ts]\nconst key = user.id;\n```\n```ts\nconst key = `${user.id}:${size}`;\n```\n::\n\n## 2. The cache never evicts (should fix)\n\n`src/avatars/cache.ts:9`. Read in the code, not measured: the map grows by one entry per user and size for as long as the tab stays open, which on the team directory is thousands of images.\n\n```ts [src/avatars/cache.ts] mark={1}\nconst cache = new Map<string, Blob>();\n```\n\nCap it, such as an LRU of 500 entries, or decide the tab's lifetime is short enough not to.\n\n## Checked\n\n- Ran `pnpm test src/avatars`: one failure, above.\n- Opened a profile on the preview at 1440px and watched the comment avatars after the header loaded.\n\n## Not checked\n\n- Server rendering: the cache only runs in the browser, and I didn't confirm the server skips it.\n- Safari's memory use.\n"
265
+ }
266
+ ]
267
+ },
268
+ {
269
+ "name": "teach-lesson",
270
+ "description": "Write a short lesson on one concept, with simplified English, a worked example, and an application question. Use for a lesson, a self-check, or onboarding on one idea.",
271
+ "frontmatter": {
272
+ "name": "teach-lesson",
273
+ "description": "Write a short lesson on one concept, with simplified English, a worked example, and an application question. Use for a lesson, a self-check, or onboarding on one idea.",
274
+ "license": "MIT"
275
+ },
276
+ "files": [
277
+ {
278
+ "path": "SKILL.md",
279
+ "text": "---\nname: teach-lesson\ndescription: Write a short lesson on one concept, with simplified English, a worked example, and an application question. Use for a lesson, a self-check, or onboarding on one idea.\nlicense: MIT\n---\n\n# Teach lesson\n\nA lesson teaches one concept through a worked example and an application question. Keep it short enough to finish in five minutes.\n\n## 1. Pin the concept and the outcome\n\nInfer the reader's purpose and existing knowledge from the conversation. Focus on the gap their question reveals.\n\nName one concept and write a testable outcome, such as \"Predict what happens when two people save the same record.\" Use a verb such as predict, explain, choose, or spot.\n\nIf the outcome depends on another concept, link to its prerequisite lesson or teach that prerequisite first.\n\nDone when one sentence names the action the reader will perform and the situation they will apply it to.\n\n## 2. Find a worked example\n\nPick a case from the reader's world where the concept prevents a visible failure. Trace it in three or four moves with concrete names, status codes, or numbers. Label invented values as illustrative.\n\nVerify behavior against a primary source, such as code or a specification. For an existing design, find recorded reasons when available. Distinguish verified behavior, recorded reasons, and inferred reasons. Preserve uncertainty such as \"likely\", and say when a reason is unknown.\n\nDone when each move has a concrete value, its behavior matches the source, and each design reason is sourced or qualified.\n\n## 3. Write the lesson\n\nRead [references/example.md](references/example.md) as a model for the complete lesson. Write these parts in order:\n\n1. **The outcome**, one sentence under the title.\n2. **Why it matters**: the failure the concept prevents, told as the worked example's problem. Three or four sentences.\n3. **The idea**: give the smallest complete explanation in at most four sentences. Start with a plain definition, connect it to the reader's case, and explain how it solves the problem.\n4. **The worked example**: add the supporting detail, one move per step, showing the value that changed. For text and code, use [`::steps`](references/blocks.md#the-worked-example-steps). When diagrams help explain interacting parts, use the [diagram series guidance](references/blocks.md#build-a-diagram-across-steps).\n5. **The check**, below.\n6. **Read next**: one primary source, linked to the exact section, and the lesson that naturally follows.\n\n### Use STE 100 writing rules\n\nApply [ASD-STE100 Simplified Technical English](https://asd-ste100.org/) writing rules to the whole lesson, including the example, question, hints, and answer explanations.\n\n- Give each sentence one idea. Give each instruction one action.\n- Keep instructions within 20 words and descriptive sentences within 25 words. Use short paragraphs.\n- Use common words and active voice. Write instructions as direct commands.\n- Define each technical term in bold at first use. Use one term for each concept and keep its meaning consistent.\n- Keep technical names, code, units, and UI labels exact.\n- Put a condition before the action it controls. Make each pronoun refer to one clear subject.\n- Use literal wording and explain abbreviations. Split long noun groups into phrases.\n- State the mechanism directly, with the detail needed to follow it.\n\nDone when the outcome, problem, idea, worked example, and source link are present. Each term must have a definition before use, and each sentence must meet the applicable word limit. Preserve the example's behavior and evidence confidence.\n\n## 4. Write the check\n\nUse [`::question`](references/blocks.md#the-check-question) to test the outcome. Change one input of the worked example and ask the reader to predict, explain, choose, or spot the result.\n\n- Make each wrong option a real misconception, and give it a `>` rationale that says why it's tempting and why it's wrong.\n- Keep the options parallel, one idea each and close in length, so their shape gives no clue to the answer.\n- Add `#explain` for the one thing the reader should take away whatever they picked.\n- For a fact worth remembering a week later, optionally add a [`::recall` card](references/blocks.md#the-fact-to-keep-recall).\n\nDone when the question tests the stated outcome with one changed input, every option has a rationale, and the shared explanation resolves each misconception.\n\n## 5. 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 lint-comark lesson.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 rendered page where it will be read. Select each option and check its rationale and the shared explanation.\n\nDone when lint reports no problems, every worked step and diagram is visible, and each option reveals the expected result and rationale. Confirm that the correct answer follows from the example and that the lesson fits a five-minute read.\n"
280
+ },
281
+ {
282
+ "path": "agents/openai.yaml",
283
+ "text": "interface:\n display_name: \"Teach lesson\"\n short_description: \"Teach one concept with a worked example and a check\"\n"
284
+ },
285
+ {
286
+ "path": "references/blocks.md",
287
+ "text": "# Blocks for a lesson\n\nThese blocks come with `comark-ui`. The steps and questions also come separately with `comark-steps` and `comark-quiz`.\n\n## The worked example: `::steps`\n\n````md\n::steps\n### Both load version 7\n\nAna and Ben each `GET /articles/42`, and both get `ETag: \"v7\"`.\n\n### Ana saves first\n\n```http\nPUT /articles/42\nIf-Match: \"v7\"\n```\n::\n````\n\nEach `###` heading starts a step that holds everything up to the next one, code blocks included. Put any introduction above the block, since a paragraph before the first heading is reported. For steps of one line each, a numbered list inside `::steps` works too.\n\n## Build a diagram across steps\n\nUse a `mermaid` fence when a diagram helps explain interacting parts. Redraw the previous diagram and add one part or relationship at each move. Keep existing labels and participant order consistent.\n\nUse numbered Markdown headings for the worked example. Mermaid fences currently disappear inside `::steps`, so place them directly under these headings:\n\n````md\n### 1. The client requests the article\n\n```mermaid\nsequenceDiagram\n participant Client\n participant Server\n Client->>Server: GET /articles/42\n```\n\n### 2. The server returns a version label\n\n```mermaid\nsequenceDiagram\n participant Client\n participant Server\n Client->>Server: GET /articles/42\n Server-->>Client: ETag v7\n```\n````\n\nThese paths and version labels are illustrative. Use `sequenceDiagram` for messages or `flowchart LR` for a flow. comark-ui renders Mermaid as SVG, so the reader needs no diagram script.\n\n## The check: `::question`\n\n```md\n::question{#etag-no-if-match}\nBen's editor sends his `PUT` with no `If-Match` header. What happens?\n\n- [x] The server saves Ben's version and Ana's fix is lost\n > Without `If-Match` there's no condition to fail.\n- [ ] The server refuses the save with a 412 status code\n > A 412 needs a condition that failed.\n\n#hint\nWhat does the server compare the label against?\n\n#explain\n`If-Match` is opt-in.\n::\n```\n\n- Mark the right option `[x]`. One `[x]` makes radio buttons; add `multiple` to the block for checkboxes with several right options.\n- The `>` quote under an option is its rationale, shown once the reader answers.\n- `#hint` is optional, shown on request; `#explain` shows after any answer.\n- Options are shuffled the same way on every render; write `shuffle=\"false\"` only when their order means something, such as numbers in order.\n- Give the block an `#id` so a host can store the answer. Without one, the id comes from the question's text and changes when you edit it.\n\nWithout its script, the question and an answer disclosure still render as plain HTML.\n\n## The fact to keep: `::recall`\n\n```md\n::recall{#etag-412}\nWhat does a server answer when `If-Match` names an old version?\n\n#answer\n`412 Precondition Failed`, and it leaves the resource unchanged.\n::\n```\n\nThe reader answers in their head, then reveals `#answer`.\n"
288
+ },
289
+ {
290
+ "path": "references/example.md",
291
+ "text": "# Keep both writers' changes\n\nAfter this lesson, you can predict how a server handles two changes to the same record, with or without a version check.\n\n## Why it matters\n\nIn this example, Ana and Ben open the same help article. Ana corrects a spelling error and saves the article. Ben adds a paragraph to his older copy and saves it. His save removes Ana's correction without a warning. This is a **lost update**.\n\n## The idea\n\nAn **ETag** is a version label, such as `\"v7\"`, that the server sends with a resource. The **If-Match** header carries that label back to require the same version before saving a change. If the labels match, the server saves the change. If the labels differ, the server returns `412 Precondition Failed` and keeps the resource unchanged.\n\n## Worked example\n\n::steps\n### Both load version 7\n\nAna and Ben each `GET /articles/42`, and both get `ETag: \"v7\"`.\n\n### Ana saves first\n\n```http\nPUT /articles/42\nIf-Match: \"v7\"\n```\n\nThe article is still at `\"v7\"`. The server saves Ana's change and returns `200 OK` with `ETag: \"v8\"`.\n\n### Ben saves second\n\n```http\nPUT /articles/42\nIf-Match: \"v7\"\n```\n\nThe article is now at `\"v8\"`. The labels differ, so the server returns `412 Precondition Failed`. Ana's correction stays.\n\n### Ben reloads and merges\n\nBen's editor fetches `\"v8\"` and shows Ana's correction. Ben adds his paragraph to that version. He saves again with `If-Match: \"v8\"`.\n::\n\n## Check yourself\n\n::question{#etag-no-if-match}\nThis API accepts requests without `If-Match`. Ben's editor sends his `PUT` without that header. The article is at `\"v8\"`. What happens?\n\n- [x] The server saves Ben's version and removes Ana's correction\n > Without `If-Match`, the server has no version condition to check. The server saves Ben's version. This removes Ana's correction.\n- [ ] The server refuses the save with a 412 status code\n > A 412 response requires a condition that failed. Ben sent no condition, so the server has no labels to compare.\n- [ ] The server merges Ben's paragraph into version 8\n > The server does not combine the two versions. After a 412 response, the client must get the current version and combine the changes.\n\n#explain\nThe client must send its original version label for `If-Match` to protect the change. An API can require that header. If the header is absent, that API can return `428 Precondition Required`.\n::\n\n::recall{#etag-412}\nWhat does a server answer when `If-Match` names an old version?\n\n#answer\n`412 Precondition Failed`, and it leaves the resource unchanged.\n::\n\n## Read next\n\n[RFC 9110, section 13.1.1](https://www.rfc-editor.org/rfc/rfc9110#name-if-match) defines `If-Match`.\n\nThe next lesson explains weak ETags and why `W/\"v7\"` never matches `If-Match`.\n"
292
+ }
293
+ ]
294
+ },
295
+ {
296
+ "name": "visual-explanation",
297
+ "description": "Explains how something works with the smallest visual that answers the question, such as a sequence diagram, a call tree, a file tree, or a marked-up screenshot, placed beside the sentence it supports. Use when someone asks to be shown how a system, a flow, or a change works, or when the answer would otherwise take paragraphs of prose.",
298
+ "frontmatter": {
299
+ "name": "visual-explanation",
300
+ "description": "Explains how something works with the smallest visual that answers the question, such as a sequence diagram, a call tree, a file tree, or a marked-up screenshot, placed beside the sentence it supports. Use when someone asks to be shown how a system, a flow, or a change works, or when the answer would otherwise take paragraphs of prose.",
301
+ "license": "MIT"
302
+ },
303
+ "files": [
304
+ {
305
+ "path": "SKILL.md",
306
+ "text": "---\nname: visual-explanation\ndescription: Explains how something works with the smallest visual that answers the question, such as a sequence diagram, a call tree, a file tree, or a marked-up screenshot, placed beside the sentence it supports. Use when someone asks to be shown how a system, a flow, or a change works, or when the answer would otherwise take paragraphs of prose.\nlicense: MIT\n---\n\n# Visual explanation\n\nA good explanation answers one question with one picture the reader can check against the code. Prose carries the reasoning, and the visual carries the shape: what happens in what order, what calls what, what lives where. Each visual stands next to the sentence that tells the reader what to look for.\n\n## 1. Name the question\n\nWrite the question in one sentence, in the reader's words: \"What happens between `parseMarkdown` and the HTML?\" If it's two questions, answer the one they asked and offer the other.\n\nDone when the question fits on one line.\n\n## 2. Pick the view\n\nChoose by the shape of the answer:\n\n| The answer is… | Draw |\n| --- | --- |\n| The order of events between parts | A `mermaid` sequence diagram or flowchart |\n| Who calls whom | A call tree in a `text` fence, indented by call depth |\n| Where code lives, or which files a change touches | `::file-tree`, with `+`, `-`, and `~` marks |\n| The states something moves between | A `mermaid` `stateDiagram-v2` |\n| Something on screen | `::screenshot` with measured marks |\n| What a few lines do, line by line | A code fence with `// <1>` notes |\n| How a number changed | `::chart` or a two-row table |\n\nUse one primary view. Add a second only when it answers the question the first one raises, such as \"where is that in my code?\" after a flow diagram. [references/blocks.md](references/blocks.md) has the syntax of each.\n\nDone when you can say, in one sentence, what the reader will see in the view.\n\n## 3. Draw it from the source\n\nRead the code, the product, or the data before you draw. Every box, arrow, file, and label in the view is something the reader can find: a real function name, a real file path, a state the code really has. Leave out the parts that don't bear on the question; a diagram of everything answers nothing.\n\nKeep a diagram to about eight nodes or participants. Past that, split it by stage or zoom into the part the question is about.\n\nDone when every label in the view exists in the source, and nothing in it is there only for completeness.\n\n## 4. Write around it\n\n[references/example.md](references/example.md) shows the shape:\n\n1. A title that is the question, or its answer.\n2. Two or three sentences with the answer in plain words, ending on what to look at in the view.\n3. The view.\n4. One or two sentences for what the view can't show, such as a rule that applies at every step.\n5. A second view only if step 2 allowed one, with a sentence before it saying why it's there.\n\nDone when the explanation reads top to bottom in under a minute and every visual has a sentence beside it.\n\n## 5. 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 lint-comark explanation.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 rendered page where it will be read and look at each view at the width the reader has. A diagram whose labels are too small to read needs fewer nodes, not a wider page.\n\nDone when you've looked at the render and every label reads at its size.\n"
307
+ },
308
+ {
309
+ "path": "agents/openai.yaml",
310
+ "text": "interface:\n display_name: \"Visual explanation\"\n short_description: \"Explain how something works with one checkable visual\"\n"
311
+ },
312
+ {
313
+ "path": "references/blocks.md",
314
+ "text": "# Blocks for a visual explanation\n\nThese blocks come with `comark-ui`. The comark-ui skill (`vp dlx skills add onmax/comark-ui`) documents every block in full.\n\n## Diagrams: a `mermaid` fence\n\n````md\n```mermaid\nsequenceDiagram\n participant Browser\n participant Worker\n participant Assets\n Browser->>Worker: GET /zoom/\n Worker->>Assets: zoom/index.html\n Assets-->>Browser: prerendered page\n```\n````\n\ncomark-ui draws Mermaid as SVG while parsing, so the page needs no script to show it. Use `flowchart LR` for a pipeline, `sequenceDiagram` for messages between parts, and `stateDiagram-v2` for states. Write labels as the code names them. For a diagram in another language, `d2`, `dot`, `plantuml`, `dbml`, `goat`, and `svgbob` fences draw through comark-diagrams.\n\n## A call tree: a `text` fence\n\n````md\n```text\nrenderPage\n parseMarkdown\n plugins' post\n renderHtmlFromDocument\n components\n```\n````\n\nIndent by call depth, two spaces a level. Name functions as the code does.\n\n## Files: `::file-tree`\n\n````md\n::file-tree\n```\ndocs/\n build.ts # renders every page\n ~ style.css\n + pages/\n + skills/\n...\n```\n::\n````\n\nTwo spaces nest an entry, a trailing `/` makes a folder, `+`, `-`, and `~` mark added, removed, and changed, and ` # ` starts a note. `...` stands for entries left out. Write the tree in a fence; a Markdown list renders names like `__init__.py` as bold.\n\n## Line notes: `// <1>` and a list\n\n````md\n```ts\nconst document = await parseMarkdown(source, { plugins }); // <1>\nconst html = await renderHtmlFromDocument(document, { components }); // <2>\n```\n\n1. Every plugin runs here.\n2. Components only draw.\n````\n\nA numbered list right after the fence becomes the notes. The marker goes in a line-end comment in the block's language, such as `# <1>` in Python.\n\n## On screen: `::screenshot`\n\n```md\n::screenshot{src=\"https://example.com/settings.png\" alt=\"Settings page with the Save button\" viewport=\"1280x800\"}\n:::annotation{x=71.2 y=84.1 w=12.4 h=6.3}\n**Save** writes all three tabs at once.\n:::\n::\n```\n\nMeasure `x y w h` from the page as percentages of the image. The before-after-evidence skill explains how.\n\n## Numbers: `::chart`\n\n```md\n::chart{type=\"bar\" title=\"Build time, seconds\"}\n| Step | Before | After |\n| --- | --: | --: |\n| Parse | 4.1 | 1.2 |\n| Render | 2.3 | 2.2 |\n::\n```\n\nThe chart draws from the table, so the numbers in your text and the chart can't disagree.\n"
315
+ },
316
+ {
317
+ "path": "references/example.md",
318
+ "text": "# How a Comark document becomes HTML\n\nParsing and rendering are two separate calls, and the plugins do their work in the first. That's why a block's problems are known before any HTML exists.\n\n```mermaid\nflowchart LR\n md[Markdown] --> tokens[Tokens]\n tokens --> tree[Node tree]\n tree --> doc[Nodes and issues]\n doc --> html[HTML]\n```\n\nA plugin can join at three points: its Markdown rules while the text becomes tokens, `markdownItPost` on the tokens, and `post` on the tree. The last arrow is the renderer, which only maps each node to a component.\n\nIn code, the diagram is three lines:\n\n```ts\nconst document = await parseMarkdown(source, { plugins }); // <1>\nif (document.meta.issues.length) refuse(document.meta.issues); // <2>\nconst html = await renderHtmlFromDocument(document, { components }); // <3>\n```\n\n1. Every plugin runs here, in the order of `plugins`.\n2. Each issue is one sentence naming the block and the fix, so a host can refuse a document before it renders.\n3. Components only draw: `screenshot` maps to the figure's HTML, and nothing is parsed again.\n"
319
+ }
320
+ ]
321
+ }
322
+ ];
323
+ /** The comark-ui version the skills come from, for a host's cache key or a digest. */
324
+ const version = "0.7.5";
325
+ //#endregion
326
+ export { skills, version };