comark-ui 0.7.6 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +3 -3
  2. package/dist/{client-Bjd6VcGQ.mjs → client-kUEiRbtp.mjs} +25 -8
  3. package/dist/client.mjs +2 -2
  4. package/dist/{editor-DmxNPK7K.mjs → editor-9IcfELjG.mjs} +108 -16
  5. package/dist/editor-vue.d.mts +21 -5
  6. package/dist/editor-vue.mjs +40 -9
  7. package/dist/{editor-KD97dL_1.d.mts → editor-yoVL7Ajq.d.mts} +27 -3
  8. package/dist/editor.d.mts +2 -2
  9. package/dist/editor.mjs +1 -1
  10. package/dist/html.d.mts +1 -1
  11. package/dist/html.mjs +7 -3
  12. package/dist/{index-Dbqxt4Qg.d.mts → index-DVGSDzBS.d.mts} +1 -1
  13. package/dist/index.d.mts +1 -1
  14. package/dist/index.mjs +1 -1
  15. package/dist/react.d.mts +1 -1
  16. package/dist/react.mjs +2 -2
  17. package/dist/skills/before-after-evidence/SKILL.md +11 -13
  18. package/dist/skills/before-after-evidence/references/blocks.md +1 -1
  19. package/dist/skills/comark-ui/SKILL.md +13 -9
  20. package/dist/skills/comark-ui/references/annotate.md +1 -1
  21. package/dist/skills/comark-ui/references/badge.md +1 -1
  22. package/dist/skills/comark-ui/references/cards.md +2 -1
  23. package/dist/skills/comark-ui/references/code-notes.md +1 -1
  24. package/dist/skills/comark-ui/references/code.md +3 -3
  25. package/dist/skills/comark-ui/references/comments.md +3 -3
  26. package/dist/skills/comark-ui/references/embed.md +3 -2
  27. package/dist/skills/comark-ui/references/keys.md +1 -1
  28. package/dist/skills/comark-ui/references/links.md +1 -1
  29. package/dist/skills/comark-ui/references/lint.md +21 -12
  30. package/dist/skills/comark-ui/references/magic-move.md +1 -1
  31. package/dist/skills/comark-ui/references/media.md +2 -2
  32. package/dist/skills/comark-ui/references/metrics.md +1 -0
  33. package/dist/skills/comark-ui/references/outline.md +2 -2
  34. package/dist/skills/comark-ui/references/properties.md +1 -1
  35. package/dist/skills/comark-ui/references/quiz.md +15 -3
  36. package/dist/skills/comark-ui/references/reading-time.md +1 -1
  37. package/dist/skills/comark-ui/references/remark-to-comark.md +29 -0
  38. package/dist/skills/comark-ui/references/steps.md +1 -1
  39. package/dist/skills/comark-ui/references/table.md +1 -0
  40. package/dist/skills/comark-ui/references/tabs.md +2 -2
  41. package/dist/skills/comark-ui/references/tests.md +1 -1
  42. package/dist/skills/comark-ui/references/ui.md +7 -6
  43. package/dist/skills/teach-lesson/SKILL.md +4 -4
  44. package/dist/skills/teach-lesson/references/blocks.md +6 -6
  45. package/dist/skills.mjs +34 -30
  46. package/dist/text-D8j9clg3.mjs +19 -0
  47. package/dist/{typeset-css-5aJ7cSmh.mjs → typeset-css-blSNpFNK.mjs} +2 -2
  48. package/dist/vue.d.mts +1 -1
  49. package/dist/vue.mjs +3 -3
  50. package/package.json +45 -45
package/dist/skills.mjs CHANGED
@@ -12,7 +12,7 @@ const skills = [
12
12
  "files": [
13
13
  {
14
14
  "path": "SKILL.md",
15
- "text": "---\nname: before-after-evidence\ndescription: Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page. Use when a pull request, bug fix, or design change alters what people see, or when someone asks for before/after screenshots or visual evidence.\nlicense: MIT\n---\n\n# Before/after evidence\n\nA before/after is evidence when a reviewer can check every claim against the two images and nothing else. 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 comark-lint evidence.md\n ```\n\n These commands use the [global Vite+ CLI](https://viteplus.dev/guide/global-cli). Without it, `pnpm dlx` takes the same arguments.\n\n2. Open the page where it will be read and take a screenshot of it. 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"
15
+ "text": "---\nname: before-after-evidence\ndescription: Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page. Use when a pull request, bug fix, or design change alters what people see, or when someone asks for before/after screenshots or visual evidence.\nlicense: MIT\n---\n\n# Before/after evidence\n\nA before/after is evidence when a reviewer can check every claim against the two images and nothing else. The reader can't repair a bad pair, so get three things right: two captures of the same view, marks on what changed, and notes in the words the screen shows.\n\nThis skill writes the document. Your host decides where it's published and who can open it.\n\n## 1. Capture a comparable pair\n\nTake the after capture from the branch or its preview, and the before capture from a deployment or commit that still behaves the old way. Capture both with the same viewport, zoom, theme, data, scroll position, and open menus, so the change is the only difference.\n\n- Set up the state that shows the change before you capture: the long name that wrapped, the error that stayed hidden, the empty list.\n- Clip both captures to one region around the change, the same rectangle on each side. A `::compare` gives each image under half the page width, where a full-page capture's text is too small to read.\n- Pin anything that moves the layout between the two states, such as a vertically centered dialog that grows by one line, so the same element sits at the same place in both.\n\nIf you can't capture one side comparably, stop and say what blocked you. A description in place of a screenshot, or one side alone, isn't before/after evidence.\n\nDone when both images exist, have the same size, and differ only where the change is.\n\n## 2. Measure every mark\n\nMarks take `x`, `y`, `w`, and `h` as percentages of the image. Measure them. An estimate drifts a few percent, which on a half-width image lands on the neighbouring button.\n\n- **A page you captured:** read the element's box in the same browser, viewport, and state, minus the clip's offset. [references/measuring.md](references/measuring.md) has the snippet.\n- **A still you didn't capture**, such as a frame from a screen recording, a design export, or an image attached to a bug report: there is no DOM to ask. Measure in image pixels and divide by the image's natural width and height. Confirm each box by cropping the image to it and looking at the crop, as the same reference shows.\n\nMark the change at its own size: the field, the label, the button. Use `red` on the before side for the problem and `green` on the after side for the fix, and leave what stayed the same unmarked. Two or three marks a side usually tell the whole story.\n\nDone when every mark has numbers you measured, and you can name the element or the pixel rectangle each came from.\n\n## 3. Write the document\n\nStart from [references/example.md](references/example.md) and keep its shape:\n\n1. One sentence: what changed, and for whom.\n2. One line of steps to reach the screen, so a reviewer can reproduce it.\n3. The `::compare`, each note a few words in the screen's own words. A value the screenshot doesn't show stays out of its note.\n4. A `::diff` of the line that caused it, only when the reader would otherwise ask.\n\nThe images carry the evidence, so the text only points into them. Put how the work went (checks, retries, CI) in your report to the user, not in the document. [references/blocks.md](references/blocks.md) has the syntax of each block.\n\nImage `src` must be a URL the renderer can reach: a path next to the document, or an https URL your host gives you for the upload.\n\n## 4. Check the render\n\n1. Lint it, and fix until it reports no problems:\n\n ```sh\n vp dlx --package comark-linter --package comark-ui comark-lint evidence.md\n ```\n\n These commands use the [global Vite+ CLI](https://viteplus.dev/guide/global-cli). Without it, `pnpm dlx` takes the same arguments.\n\n2. Open the page where it will be read and take a screenshot of it. To check the screenshots alone, copy the whole `::compare` block, nested screenshots and annotations included, into `evidence-screenshots.md` beside `evidence.md`.\n\n Make comark-annotate a direct dependency of this workspace package. A transitive one isn't linked, and `@latest` can run a different release from the project's renderers. Match the version of the installed Comark UI packages: `pnpm add -D comark-annotate@<version>`, with `-w` at the workspace root. With no Comark UI packages installed, omit `@<version>`. Then run:\n\n ```sh\n vp exec comark-annotate render evidence-screenshots.md -o evidence-screenshots.html\n ```\n\n Without Vite+, use `pnpm exec` with the same arguments. Open `evidence-screenshots.html` and take a screenshot of it. This CLI renders annotate, carousel, and zoom only; check any `::diff` or other blocks in the host's full Comark UI preview.\n3. Check that each mark sits on its target and each note matches what its side shows. Measure a mark that misses again from the source; a box moved by eye stays a guess.\n\nDone when you've looked at the rendered page and every mark lands. Name any mark or block you couldn't check in your report.\n"
16
16
  },
17
17
  {
18
18
  "path": "agents/openai.yaml",
@@ -20,7 +20,7 @@ const skills = [
20
20
  },
21
21
  {
22
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"
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`, `line`, `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
24
  },
25
25
  {
26
26
  "path": "references/example.md",
@@ -42,7 +42,7 @@ const skills = [
42
42
  "files": [
43
43
  {
44
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 comark-lint 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"
45
+ "text": "---\nname: comark-ui\ndescription: Writes PR evidence, explanations of how something works, and docs pages in Markdown with comark-ui. Use when a document needs an annotated screenshot, a before/after, a chart, a diagram, a code change, or another comark block, or when setting up comark-ui or a comark-* package.\n---\n\n# comark-ui\n\nYou write Markdown, and the comark plugins render it as static HTML. The reader sees only what you measured and wrote down, so measure every mark from the page, take every number from the data, and check the render before you hand it over.\n\n## Install\n\nInstall `comark-ui` by default. It holds every plugin and registers them in the order they need:\n\n```sh\npnpm add comark @comark/html comark-ui\n```\n\nRun the comark CLIs with the [global Vite+ CLI](https://viteplus.dev/guide/global-cli): `vp dlx` for a package the project doesn't install, `vp exec` for one it does. Without Vite+, `pnpm dlx` and `pnpm exec` take the same arguments.\n\n```ts\nimport { createHtmlRenderer } from \"@comark/html\";\nimport comarkUi from \"comark-ui/html\";\n\nconst render = createHtmlRenderer(comarkUi());\n```\n\nEvery plugin is on except `include`. The [comark-ui reference](references/ui.md) covers its options, the plugins that read files or use the network, and its Vue, React, and Svelte entries.\n\nTo use only some plugins, install those packages and register each plugin with its components:\n\n```sh\npnpm add comark @comark/html comark-annotate\n```\n\n```ts\nimport { createHtmlRenderer } from \"@comark/html\";\nimport annotate, { components } from \"comark-annotate/html\";\n\nconst render = createHtmlRenderer({ plugins: [annotate()], components });\n```\n\nEach reference ends with a Register section that says how its package registers alone and which options it needs.\n\nEither way, the HTML renderer inlines the styles once per document, and the Vue, React, and Svelte components add them on mount. On a server-rendered page, import each package's `style.css`, such as `comark-cards/style.css`, so the page doesn't shift.\n\nThe `--comark-*` tokens theme every plugin at once; https://comark-ui.onmax.me/guide/theming/ lists them. In a Nuxt UI app the plugins follow its `--ui-*` variables with no setup. For a page rendered outside the app, such as review evidence, put the CSS from `nuxtUiTheme({ colors: appConfig.ui?.colors, css: mainCss })` (from `comark-ui/nuxt-ui`) in a `<style>`, so the page matches the app.\n\nA project that renders Markdown with remark moves to Comark with `remark-to-comark`; the [remark-to-comark reference](references/remark-to-comark.md) walks through it.\n\nProblems land in `meta.issues` after `parseMarkdown`, one sentence each, and a host can refuse a document while any remain. For untrusted input, add Comark's `security` plugin with `blockedTags` such as `script`, `style`, and `iframe`; it cleans the Markdown inside blocks too.\n\n## Shape the document\n\nThis skill teaches the blocks. Four sibling skills teach a whole document, what goes in it and in what order: `before-after-evidence`, `review-document`, `visual-explanation`, and `teach-lesson`. When the user asks for one of those and it isn't installed, suggest `vp dlx skills add onmax/comark-ui` and picking it from the list.\n\n## Pick the blocks\n\nRead the reference for each block you use before you write it. Mix the blocks freely with ordinary Markdown, such as headings, tables, and lists.\n\n### Review blocks\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| One state of a screen, with marks, numbered notes, or arrows | `::screenshot` | [annotate](references/annotate.md) |\n| A change on screen, before and after | `::compare` | [annotate](references/annotate.md) |\n| A command's output, such as a test run or a build, as colored text | `::cast` | [cast](references/cast.md) |\n| Numbers, such as sizes, timings, before/after metrics, a trend, or a timeline | `::chart` | [charts](references/charts.md) |\n| A flow, a sequence, an architecture, or a schema as a diagram | a `mermaid`, `d2`, `dot`, `plantuml`, `dbml`, `goat`, or `svgbob` fence | [diagrams](references/diagrams.md) |\n| Links and `@mentions` with the site's favicon or the person's avatar | links, `@user`, `:chip` | [chips](references/chips.md) |\n| Colors, such as design tokens before and after, or a palette | a color in inline code, `::palette` | [colors](references/colors.md) |\n| A suggested change to one file, before and after | `::diff` | [diff](references/diff.md) |\n| Suggested edits and comment threads on prose, such as a copy review, each note in a card in the margin | `:suggestion`, `:comment` | [comments](references/comments.md) |\n| A video, a design file, or a live reproduction from its link | `::embed` | [embed](references/embed.md) |\n| A screen recording or a sound that a screenshot can't show | `![alt](clip.mp4)` | [media](references/media.md) |\n| A number before and after, a table of befores and afters, or progress | `:delta`, `::stats`, `:progress` | [metrics](references/metrics.md) |\n| Data from a CSV, TSV, or JSON fence or file, as a table that copies and sorts | `::table`, a `csv` fence | [table](references/table.md) |\n| A test run's passed, failed, and skipped counts and its failures, from a JUnit report | `::tests` | [tests](references/tests.md) |\n| A note, tip, or warning box, or long output folded away, as on GitHub | `> [!NOTE]`, `<details>` | [github](references/github.md) |\n| The review's status, pull request, author, date, and tags in a box | frontmatter, `::properties` | [properties](references/properties.md) |\n| Claims backed by links, with numbered citations | `:cite`, `::sources` | [sources](references/sources.md) |\n| An icon in a sentence or a table cell, such as a logo or a status | `:icon` | [icon](references/icon.md) |\n| A preview link a reviewer can scan with a phone | `:qr` | [qr](references/qr.md) |\n| Why a line of code is there, as numbered notes under it | `// <1>` and a numbered list | [code-notes](references/code-notes.md) |\n\n### Page behavior\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| A stable link to a section, or a link that opens a new tab | `{#id}`, `{target=\"_blank\"}` | [links](references/links.md) |\n| A table, code block, or diagram too small to read in place | `::zoom` | [zoom](references/zoom.md) |\n| A sequence to page through in one place, such as the screens of a flow | `::carousel` | [carousel](references/carousel.md) |\n| Versions of one thing where each reader needs only one, such as an OS or a package manager | `::tabs`, `::code-group`, `pm` | [tabs](references/tabs.md) |\n| Code with lines marked added, removed, or highlighted, or a pasted `git diff` | `ins={4}` after a fence's language, `diff lang=\"ts\"` | [code](references/code.md) |\n\n### Docs blocks\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| Content a reader may skip, such as a long log, behind a summary | `::details`, `::accordion`, `::collapsible` | [details](references/details.md) |\n| Folders and files, with what a change added, removed, or changed | `::file-tree` | [file-tree](references/file-tree.md) |\n| A component's props or a function's options, with types and defaults | `::field` | [fields](references/fields.md) |\n| A version, status, or deprecation tag in a heading or a sentence | `:badge` | [badge](references/badge.md) |\n| Links to the next pages as cards, or blocks side by side | `::card-group`, `::columns` | [cards](references/cards.md) |\n| Instructions the reader follows in order, such as repro or setup steps | `::steps` | [steps](references/steps.md) |\n| Code that changes over several steps, such as a refactor told one move at a time | `::magic-move` | [magic-move](references/magic-move.md) |\n| An interactive lesson question, poll, or recall card | `::question`, `::poll`, `::recall` | [quiz](references/quiz.md) |\n\n### Markdown syntax\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| Bold or italics in Chinese, Japanese, or Korean text | `**…**`, `*…*` | [cjk](references/cjk.md) |\n| An image with a caption under it | `![alt](src \"Caption\")` | [figure](references/figure.md) |\n| A highlighted phrase or a superscript | `==text==`, `^2^` | [mark](references/mark.md) |\n| A suggested edit or a comment inside the text | `{++ ++}`, `{-- --}`, `{~~ ~> ~~}`, `{== ==}`, `{>> <<}` | [critic](references/critic.md) |\n| Terms and their definitions, such as a glossary | `Term`, then `: definition` | [deflist](references/deflist.md) |\n| A numbered note at the end of the page, such as a source or a caveat | `[^label]`, `^[note]` | [footnotes](references/footnotes.md) |\n| A formula, such as a complexity bound or how a number was computed | `$…$`, `$$…$$` | [math](references/math.md) |\n| A keyboard shortcut | `++ctrl+k++` | [keys](references/keys.md) |\n\n### Authoring tools\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| A shared section, or real source lines, pasted from a file | `:include{src}` | [include](references/include.md) |\n| A table of contents of the page's headings | `::toc` | [outline](references/outline.md) |\n| Figures and tables numbered and referred to by number | `{#fig-name}`, `:ref{#fig-name}` | [xref](references/xref.md) |\n| Word count and reading time, for the page's layout | nothing; it writes `meta.readingTime` | [reading-time](references/reading-time.md) |\n\n### Chat\n\n| To show | Use | Reference |\n| --- | --- | --- |\n| An AI SDK chat message, with its reasoning, tool calls, sources, and files | `MessageParts` from `comark-chat` | [chat](references/chat.md) |\n\n## Marks\n\nScreenshots and charts draw the same marks. Each `::annotation` is one mark and its note, written in Markdown.\n\n- `mark` is `box` (default), `circle`, `underline`, `line`, `highlight`, `bracket`, `cross`, `check`, or `none`, which draws nothing, so its note needs a `side` and the arrow alone points.\n- `color` is `violet` (default) to point something out, `red` for a problem, `amber` for a warning, `green` for a fix, `blue` for a second voice, `gray` for neutral, or a 3- or 6-digit hex color.\n- `side` is `top`, `bottom`, `left`, or `right`, and puts the note in that margin with an arrow to its mark. Notes without one are numbered in a list under the figure.\n\nMark only what your text talks about. Write each note in the words the screen or table shows, saying what changed or what is wrong, so the reader can check it against the figure. Leave out a value the figure doesn't show.\n\n## Check the result\n\n1. Lint the file with `vp exec comark-lint report.md`, in a project with `comark-linter` and the plugins installed (`comark-ui` counts as every plugin). It prints one line per problem with its line, its rule, and the fix, and exits with 1 while any remain. Fix every problem and run it again until it exits with 0. The [lint reference](references/lint.md) covers the flags, the config file, the library, ESLint, and Vite Doctor.\n2. Render the document as the block's reference says, open it in a browser, and take a screenshot. Each mark must sit on its target, each number must match its note, and each chart must say what your text says. Name anything you couldn't verify when you hand the document over.\n"
46
46
  },
47
47
  {
48
48
  "path": "agents/openai.yaml",
@@ -50,15 +50,15 @@ const skills = [
50
50
  },
51
51
  {
52
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"
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\nMake comark-annotate a direct dependency of the workspace package where you run the CLI, so its executable links. If it is only transitive, read the installed Comark UI version and add that exact version, such as `pnpm add -D comark-annotate@<version>` (add `-w` at the workspace root), because `@latest` could fetch a different renderer release. With no Comark UI packages installed, omit `@<version>`.\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
54
  },
55
55
  {
56
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"
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/` exports Comark, Markdown, an image URL, and SVG. The image endpoint is `/badge.svg`, with the same props as query parameters. For an inline tag that should follow the page's theme, keep the prose syntax above; an image embeds its own colors.\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
58
  },
59
59
  {
60
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"
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`. Any other value shows as `primary` and is reported. |\n| `variant` | `outline` (default) keeps the plain card, `soft` washes it in its `color`, and `subtle` washes it and draws its border in the `color` too. |\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
62
  },
63
63
  {
64
64
  "path": "references/carousel.md",
@@ -86,11 +86,11 @@ const skills = [
86
86
  },
87
87
  {
88
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"
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`, listed after `code`. 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
90
  },
91
91
  {
92
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"
93
+ "text": "# Code blocks\n\ncomark-code puts Copy and Wrap on every code block. It also reads options after a fence's language, written the way Expressive Code writes them. They mark lines as added, deleted, or worth a look, fold lines away, number them, and frame shell sessions as a terminal. Use the markers to point at the lines your text talks about, such as the fix in a pull request.\n\n## Syntax\n\n````md\n```ts title=\"src/zoom.ts\" del={3} ins={4} mark={1}\nfunction close() {\n dialog.close();\n history.back();\n if (opened) history.back();\n}\n```\n````\n\n| Option | What it does |\n| --- | --- |\n| `ins={4}` | Marks lines as added, in green with a `+`. |\n| `del={3}` | Marks lines as deleted, in red with a `−`. Copy leaves them out, so the block copies as the code after the change. |\n| `mark={1}` | Highlights lines to look at, like Comark's own `{1}`. |\n| `collapse={2-7}` | Folds lines behind a row that counts them. A click opens them. |\n| `showLineNumbers` | Numbers the lines. |\n| `startLineNumber=120` | Numbers the lines from 120, and turns numbers on. |\n| `title=\"src/zoom.ts\"` | Titles the block, like Comark's `[src/zoom.ts]`. A block without one shows its language. |\n| `frame=\"terminal\"` | Draws a terminal window. `frame=\"code\"` or `frame=\"none\"` draws a plain block. |\n| `wrap` | Starts the block with Wrap on. |\n\nA range is one line, `{7}`, a run, `{3-4}`, or a list of both, `{1, 3-4}`, always in braces. Ranges count the block's own lines from 1, folded lines included, whatever `startLineNumber` says: `startLineNumber=120 ins={3}` marks the line numbered 122.\n\n`sh`, `bash`, `zsh`, `shell`, `shellscript`, `shellsession`, `console`, and `ansi` blocks draw as terminals unless their title is a file name, such as `install.sh`. A terminal's header shows its title but not its language. In a code group the tab names the block, so a terminal there draws no window.\n\nWhen the host runs Shiki's notation transformers, comments such as `// [!code ++]`, `// [!code --]`, `// [!code highlight]`, `// [!code focus]`, `// [!code error]`, and `// [!code warning]` mark lines too, and comark-code styles them.\n\n## Show a change\n\n- Mark a change of a few lines with `ins` and `del` on the code you write out.\n- Paste `git diff` output into a `diff` fence with `lang`, as below. The code highlights as that language, each leading `+` or `-` becomes `ins` or `del`, and the `@@` hunk headers and the file headers above them go away. Without `lang`, the fence stays a plain diff.\n- For a suggested change, the way a pull request review shows one, write comark-diff's `::diff` with the code before and after.\n- `mark` the lines your text names, and `collapse` the imports and setup the reader can skip.\n\n````md\n```diff lang=\"ts\"\n@@ -12,4 +12,4 @@ export function close() {\n function close() {\n dialog.close();\n- history.back();\n+ if (opened) history.back();\n }\n```\n````\n\nA range past the block's last line is reported. One that lands on the wrong line inside the block isn't, so count the lines before you mark them.\n\n## Add buttons\n\n`code({ actions })` adds the host's own buttons before Wrap and Copy, on the blocks in each action's `languages`, or on every block when it lists none. A button shows a `label`, an `icon`, or both. The icon is an Iconify name, such as `lucide:play`, or an image URL, and an icon-only button needs a `title` for its tooltip and screen readers.\n\n```ts\ncode({ actions: [{ id: \"run\", label: \"Run\", icon: \"lucide:play\", languages: [\"sh\", \"bash\"] }] });\n```\n\nThe page decides what a click does. `onCodeAction()` from `comark-code/client` calls your function with the block's code and language, and returns a function that stops listening:\n\n```ts\nimport { onCodeAction } from \"comark-code/client\";\n\nconst stop = onCodeAction(\"run\", ({ code, language }) => terminal.run(code));\n```\n\nA click sends a bubbling `comark-code-action` event with `{ action, code, language }` as its `detail`, so one listener on the page can handle every action. Icons resolve while parsing, through the `iconResolver` option, a `createIcons()` from comark-icon, or from the Iconify API without one. An icon that can't be drawn is reported, and its button shows its title instead.\n\n## Register\n\n- In `comark-ui`: on, as `code`. Pass `code: { actions }` for buttons of your own, and the preset's `icon` options resolve their icons. The preset leaves Shiki out. To add it, put it right before comark-code, which keeps it after comark-tabs:\n\n ```ts\n import shiki from \"comark/plugins/shiki\";\n\n const { plugins, components } = comarkUi();\n plugins.splice(plugins.findIndex((plugin) => plugin.name === \"comark-code\"), 0, shiki());\n ```\n\n- Alone: `pnpm add comark @comark/html comark-code`, then `createHtmlRenderer({ plugins: [code()], components })` with both from `comark-code/html`. `comark-code/vue`, `/react`, and `/svelte` (Svelte 5.20 or later) export the same pair for those renderers. With Shiki, list `shiki()` before `code()`. Shiki redraws every block, so comark-code listed first loses its markers, numbers, and folds, and reports it. Without Shiki, comark-code splits the lines itself.\n"
94
94
  },
95
95
  {
96
96
  "path": "references/colors.md",
@@ -98,7 +98,7 @@ const skills = [
98
98
  },
99
99
  {
100
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"
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, or the image `avatars` gives that name. |\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`. `comments: { avatars: { onmax: \"https://github.com/onmax.png\" } }` gives authors an image by the name in `by`, without `@`, in any case.\n- Alone: `pnpm add comark @comark/html comark-comments`, then `createHtmlRenderer({ plugins: [comments()], components })` with `comments, { components }` from `comark-comments/html`, which takes the same `mark` and `avatars` options. There are no Vue, React, or Svelte entries.\n"
102
102
  },
103
103
  {
104
104
  "path": "references/critic.md",
@@ -122,7 +122,7 @@ const skills = [
122
122
  },
123
123
  {
124
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"
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, Figma, and X, 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| X | `x.com/<user>/status/<id>` and `twitter.com` links, `mobile.` ones too; the post grows to its own height |\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. Only an X post needs a script, which the HTML renderer inlines and the components start on mount. `comark-embed/vue`, `/react`, and `/svelte` render it too.\n"
126
126
  },
127
127
  {
128
128
  "path": "references/fields.md",
@@ -154,19 +154,19 @@ const skills = [
154
154
  },
155
155
  {
156
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"
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- Full names (`control`, `command`, `escape`, `delete`, `arrow-up`) and pymdownx.keys' other aliases work too. 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
158
  },
159
159
  {
160
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"
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 a ↗ and \"(opens in a new tab)\" for screen readers, so leave that out of the link's text. Any link with `target=\"_blank\"` also gets `rel=\"noopener\"`.\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
162
  },
163
163
  {
164
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 comark-lint \"docs/**/*.md\" --format json --check-urls\n```\n\n- Pass files or globs. Quote a glob so the shell passes it as written; comark-lint 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- It loads `comark-lint.config.ts` (or `.mts`, `.mjs`, `.js`) from the working directory, or a `lint-comark.config` file there. `--config path/to/config.ts` loads another one instead.\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// comark-lint.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: \"comark-lint.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 `comark-lint` 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 `comark-lint`, also installed as `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"
165
+ "text": "# Checking documents\n\ncomark-linter reports what Comark forgives, such as an unclosed `::` block, 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. SKILL.md's Check the result step runs it on one file. This page covers the flags, the config file, the library, ESLint, and Vite Doctor.\n\n## Run the command\n\n```sh\nvp exec comark-lint \"docs/**/*.md\" --format json --check-urls\n```\n\n- Pass files or globs. Quote a glob so the shell passes it as written; comark-lint 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, severity, line, column, message, fix }` instead. `severity` is `error` or `warn`; the exit code is 1 for either.\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 the next run requests again.\n- It loads `comark-lint.config.ts` (or `.mts`, `.mjs`, `.js`) from the working directory, or a `lint-comark.config` file there. `--config path/to/config.ts` loads another one instead.\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 an existing file, so save the screenshots before you lint.\n\nIt lints with every installed comark-ui package, `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:\n\n- unclosed blocks, unknown components, an unclosed `{`, and a key set twice\n- broken links and anchors, empty links, and duplicate ids\n- frontmatter that isn't valid YAML\n- `javascript:` and other unsafe URLs\n- images without alt text, unless `{alt=\"\"}` marks them as decoration\n- links that stop working for other readers: localhost, private network, and `file:` URLs, tunnels such as ngrok, and Vercel or Netlify deploy previews\n\nIn PR evidence, link the production page or the pull request. Write a local address as code, such as `` `localhost:3000` ``, since Comark turns a bare URL into a link.\n\n## Config file\n\n```ts\n// comark-lint.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, so a config can add plugins, such as your own or [include](include.md), but 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 older versions, write `.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, severity, 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 the `plugins` and `components` 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 the `headingIds` you render with 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: \"comark-lint.config.mjs\" } } }];\n```\n\nWrite the linter config as a JavaScript ES module (`export default { ... }`) so it loads on every supported Node version. A `.ts` config needs Node 22.18+, or `--experimental-strip-types` on Node 22.12–22.17, in the editor too. Restart ESLint after you edit the config or its imports, since it caches them for the process.\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. It 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`, a path from the working directory, loads the same config file as `--config`. oxlint can't run it, because its JS plugins don't lint `.md` files; run `comark-lint` 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`. It installs the command `comark-lint` (also `lint-comark`), the library `comark-linter`, the ESLint plugin `comark-linter/eslint`, and the Vite Doctor extension `comark-linter/doctor`.\n"
166
166
  },
167
167
  {
168
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"
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, 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
170
  },
171
171
  {
172
172
  "path": "references/mark.md",
@@ -178,19 +178,19 @@ const skills = [
178
178
  },
179
179
  {
180
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"
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 width=1280 height=720}\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. Give every video its `width` and `height`, paired whole pixel dimensions like `{width=1280 height=720}`, so the page keeps its room before the file loads; read them with `ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 clip.mp4`. 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
182
  },
183
183
  {
184
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"
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- Add `tiles` to `::stats` to show each row as a tile, with the name above the after value and the change below it. The table can then have two columns, a name and a value; a value with no before shows without a change. `better` and `unit` work as for the table.\n- `:progress` is a native `<progress>` with its percent after it. `value` is required: a fraction from 0 to 1, or a count up to `max`, which is 1 by default. `label` shows before the bar and names it for screen readers. The plugin clamps a `value` outside 0 to `max` and reports it.\n\n## Write the numbers\n\n- Copy the numbers from the tool's output as it prints them: `1,204` and `2.4 s` both read.\n- In `from`, `to`, and `value`, write the number alone and put the unit in `unit`. Write a percent of progress as `value=0.85`, or as `value=85 max=100`.\n- `better` covers the whole table. When some rows are better lower, such as sizes, and others better higher, such as scores, write two tables.\n\n## Register\n\n- In `comark-ui`: on. It takes no options.\n- Alone: `pnpm add comark @comark/html comark-metrics`, then `createHtmlRenderer({ plugins: [metrics()], components })` with both from `comark-metrics/html`. `comark-metrics/vue`, `/react`, and `/svelte` render it too. It has no options and no script.\n"
186
186
  },
187
187
  {
188
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"
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 hides rather than narrow a wide diagram.\n\n## Register\n\n- In `comark-ui`: on in every entry; `outline: { rail: true }` adds the side outline.\n- Alone: `pnpm add comark @comark/html comark-outline`, then `createHtmlRenderer({ plugins: [outline()], components })` with both from `comark-outline/html`. `comark-outline/vue`, `/react`, and `/svelte` render it too. It reads the headings itself, so it works in any plugin order and without Comark's toc plugin, which only fills `meta.toc` for a sidebar.\n"
190
190
  },
191
191
  {
192
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"
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 dashes and underscores as spaces, `pr` as PR, and `url` as URL, 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
194
  },
195
195
  {
196
196
  "path": "references/qr.md",
@@ -198,11 +198,15 @@ const skills = [
198
198
  },
199
199
  {
200
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"
201
+ "text": "# Lessons and quizzes\n\ncomark-quiz adds `::question`, `::poll`, and `::recall` blocks for interactive lessons. A question checks one or more answers and explains each option, a poll collects votes, and a recall card hides an answer until the reader asks for it. Without the script, the prompt, options, and answer disclosure stay readable HTML.\n\n## Syntax\n\n```md\n::question{#event-loop-order}\nWhich callback runs first once the current script ends?\n\n- [x] A resolved promise's `then` callback\n > Promise callbacks are microtasks.\n- [ ] A `setTimeout(fn, 0)` callback\n > A timer queues a task.\n\n#hint\nTwo queues are involved.\n\n#explain\nAfter every task, the event loop empties the microtask queue.\n::\n\n::poll{#next-lesson}\nWhat should the next lesson cover?\n\n- [ ] Web Workers\n- [ ] Streams\n::\n\n::recall{#why-microtasks}\nWhy does a promise callback run before a zero-delay timer?\n\n#answer\nPromise callbacks are microtasks, and a timer's callback is a task.\n::\n```\n\n| Block | Props and slots |\n| --- | --- |\n| `::question` | `#id`, `multiple`, `shuffle=\"false\"`, `number=\"false\"`, slots `#hint` and `#explain`. |\n| `::poll` | `#id`, `multiple`, `closes` (an ISO date; after it the poll shows as closed). Options are never shuffled. |\n| `::recall` | `#id`, `number=\"false\"`, and the required slot `#answer`. |\n\n- Mark a right option `[x]` and a wrong one `[ ]`. One right option makes radios and several make checkboxes; `multiple` forces checkboxes even with one. Mark no option in a poll.\n- Put a `>` line under an option to say why it is right or wrong. Give every wrong option a reason, since that is the lesson for a reader who picked it.\n- Leave a blank line before `#hint` and `#explain`. Without it, the text above swallows the slot, and the plugin reports it.\n- Give every block an `#id`. Hosts save answers by id. Without one the id derives from the prompt and options, changes when you reword them, and the plugin reports it. Keep the id when you reword the prompt.\n- Questions and recall cards share one count, so a lesson numbers them 1, 2, 3 down the page. Polls are not numbered.\n- Options are shuffled the same way for every reader, so do not write options that point at others by place, such as \"All of the above\" or \"Both A and B\". Keep the options about as long as each other, since a right option much longer than the rest is reported.\n- Keep a block to six options or fewer. Mark at least one right option in a question, or ask it as a poll. Put no block inside another block's prompt, option, or slot.\n\nThe HTML renderer inlines the quiz script and styles. Hosts can listen for `comark-quiz:answer` and `comark-quiz:ready`, and restore answers or results with `comark-quiz:state`; see [Saving answers](https://comark-ui.onmax.me/quiz/#saving-answers) on its page for the event shapes and storage behavior.\n\n## Register\n\n- In `comark-ui`: on, as `quiz`, with optional `{ labels }` for translated UI text.\n- Alone: `pnpm add comark @comark/html comark-quiz`, then `createHtmlRenderer({ plugins: [quiz()], components })` with `quiz, { components }` from `comark-quiz/html`. Use the HTML renderer, since the blocks need its inlined script; there are no Vue, React, or Svelte entries.\n"
202
202
  },
203
203
  {
204
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"
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; 0 or less counts at 200. 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/remark-to-comark.md",
209
+ "text": "# Moving a project from remark\n\nremark-to-comark moves a project that renders Markdown with remark, rehype, unified, or react-markdown to Comark. It reports what it finds as diagnostics, `RTC0101` to `RTC0303`, each with a fix and a link to https://comark-ui.onmax.me/remark-to-comark/diagnostics/.\n\n## Run it\n\n```sh\nvp exec remark-to-comark check . --json\nvp exec remark-to-comark content \"**/*.{md,mdx}\" --write\nvp exec remark-to-comark rewrite . --write\nvp exec remark-to-comark check . --json\n```\n\n- `check` lists the remark setups it finds and what each plugin becomes, the unified chains `rewrite` can replace and why the others stay, the Markdown files `content` would change, and the MDX it leaves alone. `--json` prints each finding with its code, file, line, fix, and docs link.\n- `rewrite` replaces a chain like `unified().use(remarkParse).use(remarkRehype).use(rehypeStringify).process(markdown)` with `renderHtml(markdown, comarkOptions({ … }))`, importing `comarkOptions` from `remark-to-comark/setup`. Install what the new code imports: `comark`, `@comark/html`, and `remark-to-comark`.\n- `content` escapes `{`, `[`, and `:name` that Comark would read as attributes, spans, or components, so remark still renders every file byte for byte as before. It parses with the project's syntax plugins, so `$x_{i}$` stays math. Run it before `rewrite`, which removes the remark code it reads them from.\n- Run `check` again at the end. What's left is work for you: framework configs (RTC0101), the project's own plugins (RTC0105), chains it couldn't rewrite (RTC0201), MDX (RTC0301), and blocks left as written (RTC0303).\n\n## What it leaves to you\n\n- A framework config, such as Astro's `markdown.remarkPlugins` or Docusaurus' `remarkPlugins`, stays as it is: the framework calls remark. Render with `renderHtml(markdown, comarkOptions())` where the framework lets you, such as a content loader or a route.\n- react-markdown becomes `<Markdown value={markdown} options={…} plugins={[…]} />` from `@comark/react`, with the plugins the findings name.\n- A plugin a comark-ui package replaces is reported with that package (RTC0102); its markup differs from the remark plugin's, so check pages that style it.\n- MDX stays MDX. Convert a page by hand only when its JSX just passes props: rename it to `.md`, run `content` on the prose, then write the components as Comark blocks. `content` escapes Comark syntax, so don't run it after you add them.\n\n## Register\n\n- In `comark-ui`: not included. Install it beside the preset for the migration.\n- Alone: `pnpm add -D remark-to-comark`. It installs the command `remark-to-comark`, the library `remark-to-comark`, the Comark setup `remark-to-comark/setup` (add it to your runtime dependencies when rewritten code imports it), the ports `remark-to-comark/plugins`, and the Vite Doctor extension `remark-to-comark/doctor`.\n"
206
210
  },
207
211
  {
208
212
  "path": "references/sources.md",
@@ -210,23 +214,23 @@ const skills = [
210
214
  },
211
215
  {
212
216
  "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"
217
+ "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; any other value falls back to 3 and is reported. 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
218
  },
215
219
  {
216
220
  "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"
221
+ "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- To tint a cell by status, write the whole cell as `[text]{tone=\"success\"}`, with `success`, `warning`, `error`, or `info`. A tone on the first cell tints the row. This works in Markdown, CSV, TSV, and JSON cells. A span that doesn't fill its cell, or another tone, leaves the cell uncolored and is reported.\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
222
  },
219
223
  {
220
224
  "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"
225
+ "text": "# Tabs and code groups\n\ncomark-tabs draws `::tabs` of any Markdown, `::code-group` of code blocks, and a tab per package manager for a code block marked `pm`. Use tabs for versions of one thing where each reader needs only one, such as an operating system, a framework, or a package manager.\n\n## Syntax\n\n````md\n::code-group{sync=\"pm\"}\n```sh [pnpm]\npnpm add comark-tabs\n```\n\n```sh [npm]\nnpm install comark-tabs\n```\n::\n\n::tabs{sync=\"os\"}\n:::tabs-item{label=\"macOS\"}\nPress **⌘K** to find the pigeon data.\n:::\n\n:::tabs-item{label=\"Windows\"}\nPress **Ctrl+K** to find the pigeon data.\n:::\n::\n````\n\n- `::tabs`: each `:::tabs-item` inside is one tab. Content outside every tab doesn't render, and is reported.\n- `:::tabs-item`: `label` is required and must differ from the other labels in its block, since `sync` matches tabs by label. `icon` draws an Iconify name, such as `lucide:apple`, or an image URL before the label.\n- `::code-group`: only code blocks go inside. Name each one's tab in brackets after its language, as in `sh [pnpm]`, with an optional icon after it, as in `sh [pnpm] icon=logos:pnpm`. The tab replaces the block's own title. `title=\"pnpm\"` names no tab, so a block without brackets is reported.\n- `sync`, on either block, is a key shared by blocks that switch together. Picking a tab picks the tab with the same label in every block with that key, and the next page that uses the key remembers it.\n- A code block marked `pm`, as in `sh pm`, holds package manager commands in any manager (npm, npx, pnpm, pnpx, yarn, bun, bunx, or deno) and becomes a code group with a tab each for npm, pnpm, yarn, and bun, synced as `pm`. The manager it's written in keeps its commands as written. It reads install, frozen install (`npm ci`), add with `-D`, `-E`, `-O`, `--save-peer`, or `-g`, remove, run, `npx`/`dlx`, create, and init. An update or a `pnpm exec` stays only in its own manager's tab, and the others are left out and reported. Only commands at the start of a line or after `&&`, `||`, or `;` change. Inside a `::code-group`, `pm` does nothing.\n\n## Choose the block\n\n- `::code-group` when every tab is one code block, such as one config file per framework. Give every install snippet `sync=\"pm\"`, the key `pm` blocks use, so the reader picks a package manager once.\n- `pm` for package manager commands, so you write them once in the manager you use. A command with no form in a manager, such as `npm install -g` in yarn, or one it doesn't read, such as `npm publish`, leaves those tabs out and is reported. Write that block as a `::code-group{sync=\"pm\"}` by hand.\n- `::tabs` when a tab holds prose, images, or several blocks.\n\nPut only alternatives in tabs. Anything every reader needs goes outside them, since a reader sees one tab at a time. For a before and after, use annotate's `::compare` or comark-diff's `::diff`, which show both sides at once.\n\n## Register\n\n- In `comark-ui`: on, as `tabs`. `tabs: { managers }` sets the tabs a `pm` block gets, in order. The default is `[\"npm\", \"pnpm\", \"yarn\", \"bun\"]`, and `\"deno\"` can join. `tabs: { icons }` gives each tab without an icon one by its label, such as `{ pnpm: \"logos:pnpm\" }`. Icons resolve through the preset's `icon` options, from the Iconify API unless its `collections` hold the set.\n- Alone: `pnpm add comark @comark/html comark-tabs`, then `createHtmlRenderer({ plugins: [tabs()], components })` with both from `comark-tabs/html`. `comark-tabs/vue`, `/react`, and `/svelte` (Svelte 5.20 or later) export the same pair for those renderers. It takes `managers`, `icons`, and `iconResolver`, a `createIcons()` from comark-icon to share with `:icon`. List it before Shiki, so Shiki highlights the tabs `pm` adds.\n"
222
226
  },
223
227
  {
224
228
  "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"
229
+ "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. In print, every failure prints 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
230
  },
227
231
  {
228
232
  "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"
233
+ "text": "# Every plugin in one install\n\ncomark-ui registers every comark-ui package for one renderer, in the order they need, with one icon resolver shared by `:icon`, cards, details, file trees, tabs, and code actions. SKILL.md's Install section shows the basic use. This page covers the options, what the preset turns on, the other renderers, and how the linter uses it.\n\n## Options\n\n`comarkUi(options)` returns `{ plugins, components }`. Each key is a package's name without `comark-`, in camelCase, such as `fileTree`, `codeNotes`, or `readingTime`. The exceptions are `math` for comark-mathml, `outline` for comark-outline, and `mermaid` and `typeset`, which are not packages. `false` turns a plugin off along with its components, and an object turns it on with those options. `true` turns on `include` and leaves the others as they are:\n\n```ts\ncomarkUi({\n zoom: false,\n code: { actions: [{ id: \"run\", label: \"Run\" }] },\n github: { callouts: false, references: { repo: \"owner/name\" } },\n table: { root: \"data\" },\n include: { root: \"content\", base: \"guide\" },\n});\n```\n\n`github` takes its three plugins by name, `callouts`, `references`, and `html`, and `links` takes `external` and `anchors`. Each package's reference lists its own options. The order stays fixed whatever you pass: `include` runs first, so the others read what it pastes, and `code` and `codeNotes` run late, after every plugin that writes code blocks.\n\n## What it turns on\n\n- Every plugin except `include`, which reads files from disk. `include: { root, base }` turns it on, or `{ read }` in a browser or an edge runtime. `include: true` reads under the working directory.\n- `cast`, `table`, and `tests` get a reader that finds no file. Inline casts, CSV fences, and pasted JUnit reports render, and a `src` file doesn't load until you pass `{ root }` or `{ read }`. Any other options object replaces that reader too, so the plugin reads under the working directory.\n- Three plugins use the network while parsing by default: `chips` downloads favicons and avatars, `icon` fetches icon sets from the Iconify API, and `diagrams` sends each diagram's source to `https://kroki.io`. In a browser, pass `chips: { download: false }`, because a browser may not download another site's images. To parse offline, also pass `icon: { fetch: false }` with your icon sets in `collections`, and `diagrams: { draw: false }`. To keep diagram sources private, pass your own Kroki `server`.\n- Media sizing reads only data URIs by default. `zoom: { size: true }` and `media: { size: true }` also fetch remote files while parsing, and `annotate: { size: true }` does so at HTML render time. Enable these only for trusted documents, because the requests follow redirects and can reach private networks. `{ size: false }` turns measurement off for that plugin; authored dimensions and screenshot viewports still work.\n- `icon`'s options apply to every icon the preset draws, in cards, details, file trees, tabs, and code actions too.\n- `mermaid` draws `mermaid` fences with comark-pele, on the server and offline. In the HTML entry the diagram zooms with comark-zoom unless `zoom` is `false`. `mermaid: false` turns it off.\n- comark-critic has no component to add its styles, so put `criticCss`, which every entry exports, in the page.\n- The preset styles the text around the blocks, scoped to the element the document renders into, on the same `--comark-*` tokens. Pass `typeset: false` when the site already styles Markdown, such as with Tailwind Typography's `prose`.\n\n## Renderers\n\n| Renderer | Entry |\n| --- | --- |\n| `@comark/html` | `comark-ui/html` |\n| `@comark/vue` | `comark-ui/vue` |\n| `@comark/react` | `comark-ui/react` |\n| `@comark/svelte` | `comark-ui/svelte` |\n\nThe Vue, React, and Svelte entries leave out annotate, comments, diff, and quiz, which render only to HTML. Screenshots, compares, diffs, suggestions and comments, and the marks on charts need `comark-ui/html`.\n\nTo use another renderer, install it in place of `@comark/html`, such as `pnpm add comark @comark/vue comark-ui`, and pass it `plugins` and `components` as you would one package's. The preset needs Vue 3.5, React 18, or Svelte 5.20 or later. Every entry exports the `PresetOptions` type, and `comark-ui` itself exports `preset(renderer, options)`, which builds the same pair from renderer entries you pass.\n\n## In the linter\n\ncomark-linter imports `comark-ui/html` when the project has it, and lints with all its plugins and components, ahead of any comark package installed on its own, so none runs twice. It calls `comarkUi()` with options of its own, so the app's options don't reach it:\n\n- It draws no chart or diagram, and downloads no chip image.\n- `::cast`, `::table`, and `::tests` read their `src` files from the working directory.\n- `include` stays off until a lint config adds it, as [include](include.md) says.\n\n## Register\n\n- In `comark-ui`: this is the preset itself, registered as Install in SKILL.md shows.\n- Alone: each plugin is its own package, registered as its reference says. The preset ships in lockstep with them, so `comark-ui@X` holds every plugin at version `X`.\n"
230
234
  },
231
235
  {
232
236
  "path": "references/xref.md",
@@ -267,16 +271,16 @@ const skills = [
267
271
  },
268
272
  {
269
273
  "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.",
274
+ "description": "Writes a short lesson on one concept in simplified English, with a worked example and a question the reader answers. Use when someone asks to learn or understand one concept, wants a tutorial or self-check they can test themselves on, or needs onboarding on one idea.",
271
275
  "frontmatter": {
272
276
  "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.",
277
+ "description": "Writes a short lesson on one concept in simplified English, with a worked example and a question the reader answers. Use when someone asks to learn or understand one concept, wants a tutorial or self-check they can test themselves on, or needs onboarding on one idea.",
274
278
  "license": "MIT"
275
279
  },
276
280
  "files": [
277
281
  {
278
282
  "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 comark-lint 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"
283
+ "text": "---\nname: teach-lesson\ndescription: Writes a short lesson on one concept in simplified English, with a worked example and a question the reader answers. Use when someone asks to learn or understand one concept, wants a tutorial or self-check they can test themselves on, or needs 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. It opens after the right answer or Show answer, so it must resolve every misconception the wrong options carry.\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 comark-lint 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. Pick the right option, or press Show answer, and check the shared explanation.\n\nDone when lint reports no problems, every worked step and diagram is visible, and each option reveals its rationale. Confirm that the correct answer follows from the example and that the lesson fits a five-minute read.\n"
280
284
  },
281
285
  {
282
286
  "path": "agents/openai.yaml",
@@ -284,7 +288,7 @@ const skills = [
284
288
  },
285
289
  {
286
290
  "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"
291
+ "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; content before the first heading renders above the steps and 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. At each move, redraw the previous diagram with one more part or relationship, and keep its labels and participant order.\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.\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 each right option `[x]`. One `[x]` makes radio buttons and several make checkboxes. Add `multiple` to show checkboxes even with one right option.\n- The `>` quote under an option is its rationale, shown once the reader answers.\n- `#hint` is optional and opens on request. `#explain` opens after the right answer or Show answer, not after a wrong pick.\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
292
  },
289
293
  {
290
294
  "path": "references/example.md",
@@ -321,6 +325,6 @@ const skills = [
321
325
  }
322
326
  ];
323
327
  /** The comark-ui version the skills come from, for a host's cache key or a digest. */
324
- const version = "0.7.6";
328
+ const version = "0.8.1";
325
329
  //#endregion
326
330
  export { skills, version };