From 858a0c0081ddf5b4d6b1a61a7ab5db4200c5cea9 Mon Sep 17 00:00:00 2001 From: onmax Date: Wed, 21 Oct 2026 08:34:13 +0200 Subject: [PATCH 1/7] docs: one section per package, with an icon rail and a playground The sidebar is two columns, as on vitehub.dev: a rail with one icon per package, and the current package's pages. Each package page splits into an overview with its examples, getting started, components or syntax, and guides, with previous and next links between them. /play/ renders Markdown with every plugin that runs in a browser. Its presets are the examples at the top of each page, each example has a Playground link that opens it there, and a shared link carries the Markdown in its hash. comark.dev's playground can't load these plugins, so it can't be prefilled with them. --- AGENTS.md | 2 +- docs/build.ts | 401 +++++++++++++----- docs/pages/annotate.md | 300 ------------- docs/pages/annotate/components.md | 111 +++++ docs/pages/annotate/getting-started.md | 30 ++ docs/pages/annotate/guides.md | 40 ++ docs/pages/annotate/index.md | 128 ++++++ docs/pages/carousel.md | 139 ------ docs/pages/carousel/components.md | 52 +++ docs/pages/carousel/getting-started.md | 18 + docs/pages/carousel/guides.md | 26 ++ docs/pages/carousel/index.md | 52 +++ docs/pages/charts.md | 313 -------------- docs/pages/charts/getting-started.md | 28 ++ docs/pages/charts/guides.md | 59 +++ docs/pages/charts/index.md | 93 ++++ docs/pages/charts/syntax.md | 142 +++++++ docs/pages/embed/getting-started.md | 18 + docs/pages/embed/index.md | 31 ++ docs/pages/{embed.md => embed/syntax.md} | 50 +-- docs/pages/github-callouts.md | 100 ----- docs/pages/github-callouts/getting-started.md | 22 + docs/pages/github-callouts/index.md | 52 +++ docs/pages/github-callouts/syntax.md | 31 ++ docs/pages/guide/theming.md | 10 +- docs/pages/guide/validation.md | 10 +- docs/pages/include.md | 125 ------ docs/pages/include/getting-started.md | 18 + docs/pages/include/guides.md | 26 ++ docs/pages/include/index.md | 38 ++ docs/pages/include/syntax.md | 52 +++ docs/pages/index.md | 4 + docs/pages/lint/getting-started.md | 62 +++ docs/pages/{lint.md => lint/index.md} | 59 --- docs/pages/recipes/carousel.md | 2 +- docs/pages/recipes/zoom.md | 2 +- docs/pages/tabs.md | 141 ------ docs/pages/tabs/components.md | 56 +++ docs/pages/tabs/getting-started.md | 18 + docs/pages/tabs/guides.md | 30 ++ docs/pages/tabs/index.md | 46 ++ docs/pages/zoom.md | 139 ------ docs/pages/zoom/components.md | 36 ++ docs/pages/zoom/getting-started.md | 20 + docs/pages/zoom/guides.md | 48 +++ docs/pages/zoom/index.md | 44 ++ docs/play.ts | 120 ++++++ docs/preview.ts | 6 + docs/style.css | 380 ++++++++++++++--- e2e/annotate.e2e.ts | 4 +- e2e/play.e2e.ts | 34 ++ e2e/serve.ts | 2 + e2e/zoom.e2e.ts | 32 +- 53 files changed, 2264 insertions(+), 1538 deletions(-) delete mode 100644 docs/pages/annotate.md create mode 100644 docs/pages/annotate/components.md create mode 100644 docs/pages/annotate/getting-started.md create mode 100644 docs/pages/annotate/guides.md create mode 100644 docs/pages/annotate/index.md delete mode 100644 docs/pages/carousel.md create mode 100644 docs/pages/carousel/components.md create mode 100644 docs/pages/carousel/getting-started.md create mode 100644 docs/pages/carousel/guides.md create mode 100644 docs/pages/carousel/index.md delete mode 100644 docs/pages/charts.md create mode 100644 docs/pages/charts/getting-started.md create mode 100644 docs/pages/charts/guides.md create mode 100644 docs/pages/charts/index.md create mode 100644 docs/pages/charts/syntax.md create mode 100644 docs/pages/embed/getting-started.md create mode 100644 docs/pages/embed/index.md rename docs/pages/{embed.md => embed/syntax.md} (59%) delete mode 100644 docs/pages/github-callouts.md create mode 100644 docs/pages/github-callouts/getting-started.md create mode 100644 docs/pages/github-callouts/index.md create mode 100644 docs/pages/github-callouts/syntax.md delete mode 100644 docs/pages/include.md create mode 100644 docs/pages/include/getting-started.md create mode 100644 docs/pages/include/guides.md create mode 100644 docs/pages/include/index.md create mode 100644 docs/pages/include/syntax.md create mode 100644 docs/pages/lint/getting-started.md rename docs/pages/{lint.md => lint/index.md} (54%) delete mode 100644 docs/pages/tabs.md create mode 100644 docs/pages/tabs/components.md create mode 100644 docs/pages/tabs/getting-started.md create mode 100644 docs/pages/tabs/guides.md create mode 100644 docs/pages/tabs/index.md delete mode 100644 docs/pages/zoom.md create mode 100644 docs/pages/zoom/components.md create mode 100644 docs/pages/zoom/getting-started.md create mode 100644 docs/pages/zoom/guides.md create mode 100644 docs/pages/zoom/index.md create mode 100644 docs/play.ts create mode 100644 docs/preview.ts create mode 100644 e2e/play.e2e.ts diff --git a/AGENTS.md b/AGENTS.md index c5e45776..5a6aa072 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,7 @@ A pnpm workspace. Every package in `packages/` is published to npm under its own - `src/script.ts` is the browser script. `script()` is self-contained because the HTML renderer inlines its source. `src/client.ts` exports `start()`, which runs it with the bundled styles; Vue, React, and Svelte components call it on mount and stop it on unmount. - `src/theme.ts` resolves the shared `--comark-*` tokens, and `comark-zoom/theme` exports it alone. `comark-github-callouts`, `comark-embed`, and `comark-tabs` have it as a dev dependency and bundle only this entry, so they don't install zoom or carry its styles. - `src/html.ts`, `src/vue.ts`, `src/react.ts`, and `src/svelte/` are the renderers. They all output the same markup. -- `docs/` builds the docs site with the plugins themselves. Cloudflare serves it from `docs/dist`. +- `docs/` builds the docs site with the plugins themselves. Cloudflare serves it from `docs/dist`. Each package has a folder in `docs/pages/` with an overview, getting started, components or syntax, and guides; `docs/build.ts` lists the sections. `docs/play.ts` is the playground, bundled with Vite; its presets are the examples at the top of each page. ## Rules diff --git a/docs/build.ts b/docs/build.ts index 1d8142cf..6aecaf2c 100644 --- a/docs/build.ts +++ b/docs/build.ts @@ -3,6 +3,7 @@ import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; +import { deflateRawSync } from "node:zlib"; import { renderHtmlFromDocument } from "@comark/html"; import shiki from "@comark/html/plugins/shiki"; import type { ElementNode, Node, NodeHandler } from "comark"; @@ -10,6 +11,7 @@ import { parseMarkdown } from "comark"; import { escapeHtml, textContent } from "comark/utils"; import dark from "shiki/themes/vitesse-dark.mjs"; import light from "shiki/themes/vitesse-light.mjs"; +import { build } from "vite"; import annotate, { components as annotateComponents } from "../packages/annotate/src/html.ts"; import carousel, { components as carouselComponents } from "../packages/carousel/src/html.ts"; import charts, { components as chartsComponents } from "../packages/charts/src/html.ts"; @@ -20,14 +22,14 @@ import githubCallouts, { import include from "../packages/include/src/index.ts"; import tabs, { components as tabsComponents } from "../packages/tabs/src/html.ts"; import zoom, { components as zoomComponents } from "../packages/zoom/src/html.ts"; +import { previewDocument } from "./preview.ts"; const root = new URL("./", import.meta.url); const dist = new URL("dist/", root); const fonts = new URL("../node_modules/geist/dist/fonts/", root); const site = "https://comark.onmax.me"; -// Every page sits in pages/, so includes resolve from there. -const contentPlugins = () => [ - include({ root: fileURLToPath(new URL("pages/", root)) }), +// The plugins that run in a browser, the playground's set. +const browserPlugins = () => [ annotate(), carousel(), charts(), @@ -36,6 +38,11 @@ const contentPlugins = () => [ tabs(), zoom(), ]; +// Every page sits in pages/, so includes resolve from there. +const contentPlugins = () => [ + include({ root: fileURLToPath(new URL("pages/", root)) }), + ...browserPlugins(), +]; const plugins = [...contentPlugins(), shiki({ themes: { light, dark } })]; const components = { ...annotateComponents, @@ -47,39 +54,84 @@ const components = { ...zoomComponents, }; -// The index first, then the guides and recipes, then one page per package, then the comparisons, each at its own path. -const pages = [ - { slug: "", file: "index.md", label: "Overview" }, - { slug: "guide/renderers", file: "guide/renderers.md" }, - { slug: "guide/theming", file: "guide/theming.md" }, - { slug: "guide/validation", file: "guide/validation.md" }, - { slug: "guide/agents", file: "guide/agents.md" }, - { slug: "recipes/review", file: "recipes/review.md" }, - { slug: "recipes/zoom", file: "recipes/zoom.md" }, - { slug: "recipes/carousel", file: "recipes/carousel.md" }, - { slug: "annotate", file: "annotate.md", npm: "comark-annotate" }, - { slug: "zoom", file: "zoom.md", npm: "comark-zoom" }, - { slug: "carousel", file: "carousel.md", npm: "comark-carousel" }, - { slug: "tabs", file: "tabs.md", npm: "comark-tabs" }, - { slug: "github-callouts", file: "github-callouts.md", npm: "comark-github-callouts" }, - { slug: "lint", file: "lint.md", npm: "comark-lint" }, - { slug: "include", file: "include.md", npm: "comark-include" }, - { slug: "embed", file: "embed.md", npm: "comark-embed" }, - { slug: "charts", file: "charts.md", npm: "comark-charts" }, - { slug: "compare/charts", file: "compare/charts.md" }, -]; +// Lucide's icons, by name. +const icons = { + "book-open": ``, + "flask-conical": ``, + "pen-tool": ``, + "chart-column": ``, + "tv-minimal-play": ``, + "message-square-warning": ``, + "zoom-in": ``, + "gallery-horizontal-end": ``, + "panels-top-left": ``, + "file-input": ``, + "list-checks": ``, + "arrow-left": ``, + "arrow-right": ``, +}; +type IconName = keyof typeof icons; +const icon = (name: IconName, size = 18) => + ``; -// The sidebar, in order. A slug with no page yet, such as one whose package is still on a branch, is left out. -const groups = [ +// The rail, in order: the docs, the playground, then one section per package. A package whose folder isn't in +// pages/ yet, such as one still on a branch, is left out. +const packages = ( + [ + { slug: "annotate", group: "Review blocks", icon: "pen-tool" }, + { slug: "charts", group: "Review blocks", icon: "chart-column" }, + { slug: "embed", group: "Review blocks", icon: "tv-minimal-play" }, + { slug: "github-callouts", group: "Review blocks", icon: "message-square-warning" }, + { slug: "zoom", group: "Page behavior", icon: "zoom-in" }, + { slug: "carousel", group: "Page behavior", icon: "gallery-horizontal-end" }, + { slug: "tabs", group: "Page behavior", icon: "panels-top-left" }, + { slug: "include", group: "Authoring tools", icon: "file-input" }, + { slug: "lint", group: "Authoring tools", icon: "list-checks" }, + ] satisfies { slug: string; group: string; icon: IconName }[] +).filter((pkg) => existsSync(new URL(`pages/${pkg.slug}/index.md`, root))); +const fileOf = (slug: string) => + existsSync(new URL(`pages/${slug || "index"}.md`, root)) + ? `${slug || "index"}.md` + : `${slug}/index.md`; +type Section = { + slug: string; + label: string; + group: string; + icon: IconName; + npm?: string; + lists: { title?: string; slugs: string[] }[]; +}; +const sections: Section[] = [ { - title: "Guides", - slugs: ["", "guide/renderers", "guide/theming", "guide/validation", "guide/agents"], + slug: "", + label: "Docs", + group: "Comark plugins", + icon: "book-open", + lists: [ + { slugs: [""] }, + { + title: "Guides", + slugs: ["guide/renderers", "guide/theming", "guide/validation", "guide/agents"], + }, + { title: "Recipes", slugs: ["recipes/review", "recipes/zoom", "recipes/carousel"] }, + { title: "Compared", slugs: ["compare/charts"] }, + ], }, - { title: "Recipes", slugs: ["recipes/review", "recipes/zoom", "recipes/carousel"] }, - { title: "Review blocks", slugs: ["annotate", "charts", "embed", "github-callouts"] }, - { title: "Page behavior", slugs: ["zoom", "carousel", "tabs"] }, - { title: "Authoring tools", slugs: ["include", "lint"] }, - { title: "Compared", slugs: ["compare/charts"] }, + // Its column lists the presets, which come from the pages' examples. + { slug: "play", label: "Playground", group: "Comark plugins", icon: "flask-conical", lists: [] }, + ...packages.map((pkg) => ({ + ...pkg, + label: pkg.slug, + npm: `comark-${pkg.slug}`, + // A package's pages, in this order when it has them. + lists: [ + { + slugs: ["", "getting-started", "components", "syntax", "guides"] + .map((page) => (page ? `${pkg.slug}/${page}` : pkg.slug)) + .filter((slug) => existsSync(new URL(`pages/${fileOf(slug)}`, root))), + }, + ], + })), ]; const isElement = (node: Node): node is ElementNode => Array.isArray(node) && node[0] !== null; @@ -89,6 +141,10 @@ const str = (value: unknown) => : // SAFETY: undefined, null, objects, and arrays returned above, so only a string, number, or boolean is left. String(value as string | number | boolean); const isPre = (node: Node): node is ElementNode => isElement(node) && node[0] === "pre"; +const href = (slug: string) => `/${slug}${slug ? "/" : ""}`; +// An include reads files, so the playground can't run it. +const playable = (source: string) => !/^:+include\b/m.test(source); +const playLink = (source: string) => `/play/#md=${deflateRawSync(source).toString("base64url")}`; // An example shows the Markdown in its code block twice: rendered, and as source. const example: NodeHandler = async ([, attrs, ...children], state) => { @@ -101,8 +157,11 @@ const example: NodeHandler = async ([, attrs, ...children], state) => { }); if (preview.meta.issues.length) throw new Error(`Example "${str(attrs.label)}":\n${preview.meta.issues.join("\n")}`); + const play = playable(source) + ? `${icon("flask-conical", 13)}Playground` + : ""; return ( - `
` + + `
${play}
` + `
` + `
${await state.flow(["div", {}, ...preview.nodes], state)}
` + `
` @@ -141,51 +200,84 @@ const toc = (document: { nodes: Node[] }) => .join(""); const parsed = await Promise.all( - pages.map(async (page) => { - const document = await parseMarkdown( - await readFile(new URL(`pages/${page.file}`, root), "utf8"), - { plugins }, - ); - if (document.meta.issues.length) - throw new Error(`docs/pages/${page.file}:\n${document.meta.issues.join("\n")}`); - const [title, description, tagline, install] = [ - "title", - "description", - "tagline", - "install", - ].map((key) => str(document.frontmatter[key])); - const skill = - page.npm && existsSync(new URL(`../packages/${page.slug}/skills/${page.npm}/SKILL.md`, root)) - ? page.npm - : ""; - const label = page.label ?? (page.npm ? page.slug : title); - return { - ...page, - document, - title, - description, - tagline, - install, - skill, - label, - toc: toc(document), - }; - }), + sections.flatMap((section) => + section.lists.flatMap((list) => + list.slugs.map(async (slug) => { + const file = fileOf(slug); + const document = await parseMarkdown( + await readFile(new URL(`pages/${file}`, root), "utf8"), + { plugins }, + ); + if (document.meta.issues.length) + throw new Error(`docs/pages/${file}:\n${document.meta.issues.join("\n")}`); + const [title, description, tagline, install] = [ + "title", + "description", + "tagline", + "install", + ].map((key) => str(document.frontmatter[key])); + const skill = + section.npm && + existsSync(new URL(`../packages/${section.slug}/skills/${section.npm}/SKILL.md`, root)) + ? section.npm + : ""; + return { + slug, + section, + document, + title, + description, + tagline: tagline || description, + install, + skill, + label: slug === section.slug ? "Overview" : title, + toc: toc(document), + }; + }), + ), + ), ); -const grouped = groups.map((group) => ({ - ...group, - pages: group.slugs.flatMap((slug) => parsed.filter((page) => page.slug === slug)), -})); +type Page = (typeof parsed)[number]; +const pagesOf = (section: Section) => + section.lists.map((list) => ({ + ...list, + pages: list.slugs.flatMap((slug) => parsed.filter((page) => page.slug === slug)), + })); -// The index lists every other page of a group from its own front matter. -const cards: NodeHandler = ([, attrs]) => - `
${(grouped.find((group) => group.title === str(attrs.group))?.pages ?? []) - .filter((page) => page.slug) +// The playground's presets: every page's top examples, under the section they come from. +const presets = parsed.flatMap((page) => + page.document.nodes + .filter(isElement) + .filter(([tag]) => tag === "examples") + .flatMap(([, , ...panels]) => panels.filter(isElement)) + .map(([, attrs, ...children]) => ({ + id: `${page.section.slug || "docs"}-${str(attrs.label) + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, "")}`, + label: str(attrs.label), + group: page.section.label, + source: textContent(children.find(isPre) ?? ["pre", {}]), + })) + .filter((preset) => playable(preset.source)), +); + +// The index lists a group's packages, or a list of the docs' pages, from their own front matter. +const cards: NodeHandler = ([, attrs]) => { + const group = str(attrs.group); + const found = sections + .filter((section) => section.group === group) + .flatMap((section) => parsed.filter((page) => page.slug === section.slug)); + const pages = found.length + ? found + : (pagesOf(sections[0]).find((list) => list.title === group)?.pages ?? []); + return ``; +}; const github = ``; const userIcon = ``; @@ -208,19 +300,55 @@ function installerFor(install: string, skill: string) {
`; } -const sitemap = (current: string) => - grouped - .filter((group) => group.pages.length) + +// The first column: one icon per section, its name on hover. The current section's is marked. +const rail = (current: Section) => + ``; +// The second column: the current section's pages, or the playground's presets. +const column = (section: Section, current: string) => { + const lists = + section.slug === "play" + ? [...new Set(presets.map((preset) => preset.group))].map((group) => ({ + title: group, + links: presets + .filter((preset) => preset.group === group) + .map( + (preset) => + `${escapeHtml(preset.label)}`, + ), + })) + : pagesOf(section).map((list) => ({ + title: list.title, + links: list.pages.map( (page) => - `${escapeHtml(page.label)}`, - ) - .join("")}`, + `${escapeHtml(page.label)}`, + ), + })); + return ``; +}; +// Previous and next page in the section. +function pager(page: Page) { + const list = pagesOf(page.section).flatMap((list) => list.pages); + const i = list.indexOf(page); + const [previous, next] = [list[i - 1], list[i + 1]]; + if (!previous && !next) return ""; + const link = (target: Page | undefined, rel: string, arrow: IconName) => + target + ? `${rel === "prev" ? "Previous" : "Next"}${rel === "prev" ? icon(arrow, 14) : ""}${escapeHtml(target.label)}${rel === "next" ? icon(arrow, 14) : ""}` + : ""; + return ``; +} + const npmx = ``; const logo = ``; const menuIcon = ``; @@ -244,7 +372,7 @@ for (const box of document.querySelectorAll(".installer")) { box.addEventListener("click", e => { const b = e.target.closest("[data-audience],[data-pm]"); if (!b) return; if (b.dataset.audience) audience = b.dataset.audience; if (b.dataset.pm) pm = b.dataset.pm; update() }); field.addEventListener("click", async () => { await navigator.clipboard.writeText(field.dataset.copy); field.classList.add("copied"); setTimeout(() => field.classList.remove("copied"), 1200) }); } -for (const pre of document.querySelectorAll("pre")) { +for (const pre of document.querySelectorAll("main pre")) { const b = document.createElement("button"); b.className = "copy"; b.type = "button"; b.textContent = "Copy"; b.onclick = async () => { await navigator.clipboard.writeText(pre.querySelector("code").innerText.trimEnd()); b.textContent = "Copied"; setTimeout(() => b.textContent = "Copy", 1500) }; pre.append(b); @@ -265,6 +393,18 @@ await cp( new URL("../node_modules/@fontsource-variable/caveat/files/caveat-latin-wght-normal.woff2", root), new URL("fonts/Caveat-Variable.woff2", dist), ); +const style = await readFile(new URL("style.css", root), "utf8"); +// Pages inline the styles; the playground's preview loads them. +await writeFile(new URL("style.css", dist), style); +await build({ + configFile: false, + logLevel: "warn", + build: { + outDir: fileURLToPath(new URL("play/", dist)), + emptyOutDir: false, + lib: { entry: fileURLToPath(new URL("play.ts", root)), formats: ["es"], fileName: "play" }, + }, +}); // The packages' agent skills, served for Agent Skills Discovery: v0.2 with a digest, and v0.1, which `npx skills add` reads. const skills = await Promise.all( @@ -303,44 +443,99 @@ await writeFile( }), ); -for (const page of parsed) { - const out = new URL(page.slug ? `${page.slug}/` : "", dist); - await mkdir(out, { recursive: true }); - await writeFile( - new URL("index.html", out), - ` +// The page around the content: header, rail, the section's column, and a menu that holds both on narrow screens. +function shell(options: { + slug: string; + section: Section; + title: string; + description: string; + head?: string; + body: string; +}) { + const { slug, section } = options; + const nav = rail(section) + column(section, slug); + return ` -${page.title} · ${page.description} -\n - - +${escapeHtml(options.title)} +\n + + - +${options.head ?? ""}
${logo}comark plugins - - + +
- + +${options.body} +
+ + +`; +} + +for (const page of parsed) { + const out = new URL(page.slug ? `${page.slug}/` : "", dist); + const { npm } = page.section; + const subpage = npm && page.slug !== page.section.slug; + await mkdir(out, { recursive: true }); + await writeFile( + new URL("index.html", out), + shell({ + slug: page.slug, + section: page.section, + title: subpage ? `${page.title} · ${npm}` : `${page.title} · ${page.description}`, + description: page.tagline, + body: `
-

${page.title}

${escapeHtml(page.tagline)}

+
${subpage ? `

${npm}

` : ""}

${escapeHtml(page.title)}

${escapeHtml(page.tagline)}

${page.install ? installerFor(page.install, page.skill) : ""}
${await renderHtmlFromDocument(page.document, { components: { ...components, example, examples, cards, matrix } })}
+${pager(page)}
${page.toc ? `` : ""}
- - - -`, +`, + }), ); console.log(`docs/dist/${page.slug ? `${page.slug}/` : ""}index.html written`); } + +// The playground opens on the first preset, rendered here so it shows before the script loads. +const playSection = sections.find((section) => section.slug === "play"); +const first = presets[0]; +if (playSection && first) { + const document = await parseMarkdown(first.source, { plugins: browserPlugins() }); + await mkdir(new URL("play/", dist), { recursive: true }); + await writeFile( + new URL("play/index.html", dist), + shell({ + slug: "play", + section: playSection, + title: "Playground · Comark plugins", + description: + "Write Comark with every plugin and see it rendered as you type. Start from any example in the docs.", + head: `\n`, + body: `
+
+
Markdown
+ + +
+ +
+ +`, + }), + ); + console.log("docs/dist/play/index.html written"); +} diff --git a/docs/pages/annotate.md b/docs/pages/annotate.md deleted file mode 100644 index 27ce3b25..00000000 --- a/docs/pages/annotate.md +++ /dev/null @@ -1,300 +0,0 @@ ---- -title: comark-annotate -description: Hand-drawn marks, numbered notes, and arrows on screenshots, written in Markdown. -tagline: Mark up screenshots in Markdown with hand-drawn marks, numbered notes, arrows, and before/after comparisons, and show code changes as suggested changes. -install: pnpm add comark @comark/html comark-annotate ---- - -::examples -:::example{label="Before / after"} -```md -::compare -:::screenshot{src="/before.png" alt="Filter toolbar wrapping onto a second row" viewport="640x250"} -::::annotation{x=5 y=29.6 w=90 h=45.3 color="red"} -Five filters don't fit, so the toolbar wraps and pushes the table down. -:::: - -::::annotation{x=50.9 y=55.4 w=10.5 h=14.2 mark="circle" color="red"} -**Apply** sits 6px lower than the chips. -:::: -::: - -:::screenshot{src="/after.png" alt="Filter toolbar on one row" viewport="640x250"} -::::annotation{x=67.9 y=34.8 w=13 h=14.2 mark="highlight" color="blue"} -Extra filters collapse into **+2 filters**. -:::: - -::::annotation{x=82.5 y=34.8 w=10.5 h=14.2 mark="check" color="green"} -**Apply** lines up with the chips. -:::: -::: -:: -``` -::: - -:::example{label="Slider"} -```md -::compare{mode="slider"} -:::screenshot{src="/before.png" alt="Filter toolbar wrapping onto a second row" viewport="640x250"} -::::annotation{x=50.9 y=55.4 w=10.5 h=14.2 mark="circle" color="red"} -**Apply** sits 6px lower than the chips. -:::: -::: - -:::screenshot{src="/after.png" alt="Filter toolbar on one row" viewport="640x250"} -::::annotation{x=82.5 y=34.8 w=10.5 h=14.2 mark="check" color="green"} -**Apply** lines up with the chips. -:::: -::: -:: -``` -::: - -:::example{label="Pointers"} -```md -::screenshot{src="/dashboard.png" alt="Forecast planning dashboard"} -:::annotation{x=37.6 y=31.9 w=11.9 h=3.3 mark="none" side="top"} -Units fell below plan. -::: - -:::annotation{x=66.4 y=16.4 w=29.4 h=22.2 mark="none" side="top"} -Accuracy now counts returns. -::: - -:::annotation{x=53.1 y=74.7 w=21.2 h=8.4 mark="none" side="left"} -Week 15 never loaded. -::: -:: -``` -::: - -:::example{label="Side notes"} -```md -::screenshot{src="/dashboard.png" alt="Forecast planning dashboard"} -:::annotation{x=37.6 y=31.9 w=11.9 h=3.3 mark="underline" side="left" color="red"} -Units fall below plan for the first time this quarter. -::: - -:::annotation{x=66.4 y=16.4 w=29.4 h=22.2 side="right"} -Accuracy now counts returns. -::: - -:::annotation{x=53.1 y=74.7 w=21.2 h=8.4 mark="highlight" side="left" color="amber"} -Week 15 is missing for City sneaker. -::: - -:::annotation{x=84.7 y=44.6 w=9.3 h=7.3 mark="circle"} -Notes without a `side` stay in the list. **Apply** waits until every filter is set. -::: -:: -``` -::: - -:::example{label="Marks"} -```md -::screenshot{src="/dashboard.png" alt="Forecast planning dashboard"} -:::annotation{x=81.8 y=5.3 w=14.1 h=7.3} -`box` frames an element. -::: - -:::annotation{x=37.6 y=31.9 w=11.9 h=3.3 mark="circle"} -`circle` rings a small detail. -::: - -:::annotation{x=4.2 y=6.1 w=23.4 h=5.8 mark="underline" color="blue"} -`underline` marks text. -::: - -:::annotation{x=53.1 y=74.7 w=21.2 h=8.4 mark="highlight" color="amber"} -`highlight` washes over an area. -::: - -:::annotation{x=6 y=44.6 w=45.2 h=7.3 mark="cross" color="red"} -`cross` strikes something out. -::: - -:::annotation{x=84.7 y=44.6 w=9.3 h=7.3 mark="check" color="green"} -`check` approves and sits beside its target. -::: -:: -``` -::: -:: - -Hover a mark or a note to highlight the other, and click a screenshot to zoom it. To share one, use its Copy button or press Ctrl+C (⌘C on a Mac) while it's focused; you get a PNG of the screenshot with its marks and notes. The browser's own Copy Image on the screenshot copies the same PNG once the pointer has been over it. If the image comes from another origin, its server has to send CORS headers. In an iframe, the frame needs `allow="clipboard-write"`. A comparison shows its screenshots side by side, or one at a time on a narrow page, and they zoom as one gallery; a slider lays one over the other, and you drag the divider between them. The page reads the same without a script; [zoom](/zoom/) and [carousel](/carousel/) come with the package. To page through screenshots spread across a page, or show a flow one step at a time, see the [recipes](/recipes/zoom/). - -## Usage - -Register the plugin and pass the HTML renderers as `components`: - -```ts -import { createHtmlRenderer } from "@comark/html"; -import annotate, { components } from "comark-annotate/html"; - -const render = createHtmlRenderer({ plugins: [annotate()], components }); -const html = await render(markdown); -``` - -The renderer inlines its styles and script once per document. Annotate renders only to HTML; [Renderers](/guide/renderers/) covers using it with other plugins, a strict Content Security Policy, and untrusted notes. - -## Components - -### screenshot - -An image with marks on it. Other content inside the block becomes the caption. - -::example -```md -::screenshot{src="/after.png" alt="Filter toolbar on one row" title="After" href="https://pr42.preview.example.com/plan"} -:::annotation{x=67.9 y=34.8 w=13 h=14.2} -Extra filters collapse into **+2 filters**. -::: -:: -``` -:: - -| Prop | | -| --- | --- | -| `src` | Image URL. Required. | -| `alt` | What the screenshot shows. Required. | -| `title` | Heading above the image, and the title of its zoomed view. | -| `href` | The page you captured, linked by its host next to the title. | -| `viewport` | Capture size in pixels, like `1440x900`. A compare checks that both sides match. | - -### annotation - -A mark on the screenshot and its note. The content is the note. Props can be inline, in a YAML props block, or bound with `:x`. Notes in the list under the image are numbered. Side notes sit beside or above the image and point with an arrow instead. - -::example -```md -::screenshot{src="/dashboard.png" alt="Forecast planning dashboard"} -:::annotation{x=81.8 y=5.3 w=14.1 h=7.3 color="blue"} -**Export CSV** includes the filters you set. -::: - -:::annotation{x=66.4 y=16.4 w=29.4 h=22.2 side="right" color="green"} -Accuracy now counts returns. -::: -:: -``` -:: - -| Prop | | -| --- | --- | -| `x` `y` `w` `h` | The marked area, as percentages of the image: numbers from 0 to 100, without `%`. All four are required. | -| `mark` | `box` (default), `circle`, `underline`, `highlight`, `cross`, `check`, or `none` to let a side note's arrow do the pointing. `none` needs a `side`. | -| `side` | `left`, `right`, or `top` puts the note beside or above the image with an arrow. | -| `color` | `blue` (default) points something out, `red` flags a problem, `amber` a warning, `green` a fix, `gray` anything neutral. Each has a dark-mode value. A 3- or 6-digit hex color like `#e11d48` works too, and its stamp gets whichever of a white or dark number reads better. | - -### compare - -Two screenshots side by side, titled **Before** and **After** unless they set their own `title`. Numbers continue from one side to the other, and the pair stacks on narrow screens. Notes with `side="left"` or `side="right"` join the numbered list under their screenshot, since beside it they'd sit against the other one and shrink their own image; notes with `side="top"` stay above. - -With `mode="slider"`, the two screenshots share one frame: Before shows left of a divider and After right of it. Drag anywhere on the pair, or focus the handle and use the arrow keys, to move it; a click on the screenshots that doesn't drag zooms the pair. Use it when both screenshots show the same spot and the change is small, so the reader sees it at full width. Side notes become numbered notes in the list, since an arrow would cross the divider. Without the script, the two screenshots stack. - -::example -```md -::compare{title="Toolbar alignment"} -:::screenshot{src="/before.png" alt="Toolbar wrapping" viewport="640x250"} -::::annotation{x=50.9 y=55.4 w=10.5 h=14.2 mark="circle" color="red"} -**Apply** sits lower than the chips. -:::: -::: - -:::screenshot{src="/after.png" alt="Toolbar on one row" viewport="640x250"} -::::annotation{x=82.5 y=34.8 w=10.5 h=14.2 mark="check" color="green"} -Fixed. -:::: -::: -:: -``` -:: - -| Prop | | -| --- | --- | -| `title` | What the comparison shows, read out by screen readers. Defaults to "Comparison". | -| `mode` | `columns` (default) puts the screenshots side by side; `slider` lays them over each other with a divider to drag. | - -### diff - -A code change, shown the way a pull request review shows a suggested change. Write the code before and after as two ordinary fenced blocks; the diff marks removed lines red and added lines green, and within a pair of changed lines, the words that changed. The header shows the file and how many lines were added and removed. Leave the first block empty for a new file, or the second for a deleted one. - -::example -````md -::diff{line=212 label="Suggested change"} -```ts [packages/zoom/src/client.ts] -function close() { - dialog.close(); - history.back(); -} -``` -```ts -function close() { - dialog.close(); - if (opened) history.back(); -} -``` -:: -```` -:: - -| Prop | | -| --- | --- | -| `file` | The file's path. Defaults to the name on the first block, like `` ```ts [src/a.ts] ``; one of the two is required. | -| `line` | The file's line number for the first line of both blocks. Without it, the diff has no line numbers. | -| `label` | A heading before the file name, such as "Suggested change". | - -Keep both blocks to the lines around the change; the plugin reports a block over 200 lines, two blocks in different languages or files, and a diff with no change. The line numbers and the `+` and `−` come from CSS, so copying a selection copies only the code. With Comark's `shiki` plugin, both blocks keep their highlighting, and a line a block highlights, like `` ```ts {2} ``, stays highlighted. One diff shows one file and takes no unified diff: it's for a change a reader should see, which a few lines before and after show best. - -## Guides - -### Side notes - -`side="left"`, `side="right"`, or `side="top"` moves a note out of the list and next to or above the image, with a hand-drawn arrow to its mark. Pair it with `mark="none"` to point without drawing a box. Side notes have no number because the arrow links them. In a compare, only notes above keep their arrow. On narrow screens they drop under the image, the arrows hide, and each note and its target get a matching letter. - -Notes on the same side need room. The plugin reports side notes whose marks sit close together vertically, since they would likely overlap. It can't measure the text, so check the rendered page too, and move one note to the other side or into the list. - -### Placing marks - -Measure the element in the page you captured and divide by the viewport. Don't estimate from the image. - -```js -const r = element.getBoundingClientRect(); -const box = { - x: (r.x / innerWidth) * 100, - y: (r.y / innerHeight) * 100, - w: (r.width / innerWidth) * 100, - h: (r.height / innerHeight) * 100, -}; -``` - -For a full-page screenshot, add the scroll offset and divide by the page size instead. - -### Validation - -The plugin adds a problem with any block to `meta.issues`, as [Validation](/guide/validation/) describes. A screenshot goes by its `src`, or by its position and `alt` when another screenshot shares it; an annotation goes by its position in its screenshot: - -```txt -screenshot "after.png", annotation 2: the box runs past the image edge. Keep x + w and y + h within 100. -``` - -Unknown props are reported with the valid ones, and so is a `:bound` prop that points at missing frontmatter. Bindings to `data` or `props` are left to the renderer. - -### Theming - -Screenshots, comparisons, and diffs follow the shared tokens in [Theming](/guide/theming/). The mark colors are separate from the theme, because each one means something: a plain `.ca { --ca-color: #0070f3; --ca-solid: #0070f3; --ca-ink: #fff }` changes the default marks, arrows, and notes (`--ca-color`), stamps (`--ca-solid`), and stamp numbers (`--ca-ink`), and `--ca-red`, `--ca-amber`, `--ca-green`, `--ca-blue`, and `--ca-gray` change the named ones. Side notes and labels use `--ca-note-font`; this page sets it to Caveat for the handwritten look. A diff's code uses `--ca-code-font`, and its colors come from `--ca-red` and `--ca-green`. - -## Agents - -The package ships an agent skill with the syntax, how to measure marks from the page, and how to check the result. The **For agents** button above installs it, and [Agents](/guide/agents/) covers the rest. - -It also comes with a small CLI that knows zoom and carousel too. `check` lists every problem in one or more files and exits with 1 if there are any; `render` writes a standalone page next to each file, which you can open and screenshot to confirm each mark sits on its target. - -```sh -npx comark-annotate check report.md other.md -npx comark-annotate render report.md -o report.html -``` - -## Credits - -The hand-drawn marks come from [AnnotationKit](https://annotationkit.vercel.app). diff --git a/docs/pages/annotate/components.md b/docs/pages/annotate/components.md new file mode 100644 index 00000000..fd4485fc --- /dev/null +++ b/docs/pages/annotate/components.md @@ -0,0 +1,111 @@ +--- +title: Components +description: screenshot, annotation, compare, and diff, and the props each takes. +--- + +## screenshot + +An image with marks on it. Other content inside the block becomes the caption. + +::example +```md +::screenshot{src="/after.png" alt="Filter toolbar on one row" title="After" href="https://pr42.preview.example.com/plan"} +:::annotation{x=67.9 y=34.8 w=13 h=14.2} +Extra filters collapse into **+2 filters**. +::: +:: +``` +:: + +| Prop | | +| --- | --- | +| `src` | Image URL. Required. | +| `alt` | What the screenshot shows. Required. | +| `title` | Heading above the image, and the title of its zoomed view. | +| `href` | The page you captured, linked by its host next to the title. | +| `viewport` | Capture size in pixels, like `1440x900`. A compare checks that both sides match. | + +## annotation + +A mark on the screenshot and its note. The content is the note. Props can be inline, in a YAML props block, or bound with `:x`. Notes in the list under the image are numbered. Side notes sit beside or above the image and point with an arrow instead. + +::example +```md +::screenshot{src="/dashboard.png" alt="Forecast planning dashboard"} +:::annotation{x=81.8 y=5.3 w=14.1 h=7.3 color="blue"} +**Export CSV** includes the filters you set. +::: + +:::annotation{x=66.4 y=16.4 w=29.4 h=22.2 side="right" color="green"} +Accuracy now counts returns. +::: +:: +``` +:: + +| Prop | | +| --- | --- | +| `x` `y` `w` `h` | The marked area, as percentages of the image: numbers from 0 to 100, without `%`. All four are required. | +| `mark` | `box` (default), `circle`, `underline`, `highlight`, `cross`, `check`, or `none` to let a side note's arrow do the pointing. `none` needs a `side`. | +| `side` | `left`, `right`, or `top` puts the note beside or above the image with an arrow. | +| `color` | `blue` (default) points something out, `red` flags a problem, `amber` a warning, `green` a fix, `gray` anything neutral. Each has a dark-mode value. A 3- or 6-digit hex color like `#e11d48` works too, and its stamp gets whichever of a white or dark number reads better. | + +## compare + +Two screenshots side by side, titled **Before** and **After** unless they set their own `title`. Numbers continue from one side to the other, and the pair stacks on narrow screens. Notes with `side="left"` or `side="right"` join the numbered list under their screenshot, since beside it they'd sit against the other one and shrink their own image; notes with `side="top"` stay above. + +With `mode="slider"`, the two screenshots share one frame: Before shows left of a divider and After right of it. Drag anywhere on the pair, or focus the handle and use the arrow keys, to move it; a click on the screenshots that doesn't drag zooms the pair. Use it when both screenshots show the same spot and the change is small, so the reader sees it at full width. Side notes become numbered notes in the list, since an arrow would cross the divider. Without the script, the two screenshots stack. + +::example +```md +::compare{title="Toolbar alignment"} +:::screenshot{src="/before.png" alt="Toolbar wrapping" viewport="640x250"} +::::annotation{x=50.9 y=55.4 w=10.5 h=14.2 mark="circle" color="red"} +**Apply** sits lower than the chips. +:::: +::: + +:::screenshot{src="/after.png" alt="Toolbar on one row" viewport="640x250"} +::::annotation{x=82.5 y=34.8 w=10.5 h=14.2 mark="check" color="green"} +Fixed. +:::: +::: +:: +``` +:: + +| Prop | | +| --- | --- | +| `title` | What the comparison shows, read out by screen readers. Defaults to "Comparison". | +| `mode` | `columns` (default) puts the screenshots side by side; `slider` lays them over each other with a divider to drag. | + +## diff + +A code change, shown the way a pull request review shows a suggested change. Write the code before and after as two ordinary fenced blocks; the diff marks removed lines red and added lines green, and within a pair of changed lines, the words that changed. The header shows the file and how many lines were added and removed. Leave the first block empty for a new file, or the second for a deleted one. + +::example +````md +::diff{line=212 label="Suggested change"} +```ts [packages/zoom/src/client.ts] +function close() { + dialog.close(); + history.back(); +} +``` +```ts +function close() { + dialog.close(); + if (opened) history.back(); +} +``` +:: +```` +:: + +| Prop | | +| --- | --- | +| `file` | The file's path. Defaults to the name on the first block, like `` ```ts [src/a.ts] ``; one of the two is required. | +| `line` | The file's line number for the first line of both blocks. Without it, the diff has no line numbers. | +| `label` | A heading before the file name, such as "Suggested change". | + +Keep both blocks to the lines around the change; the plugin reports a block over 200 lines, two blocks in different languages or files, and a diff with no change. The line numbers and the `+` and `−` come from CSS, so copying a selection copies only the code. With Comark's `shiki` plugin, both blocks keep their highlighting, and a line a block highlights, like `` ```ts {2} ``, stays highlighted. One diff shows one file and takes no unified diff: it's for a change a reader should see, which a few lines before and after show best. diff --git a/docs/pages/annotate/getting-started.md b/docs/pages/annotate/getting-started.md new file mode 100644 index 00000000..e9443ada --- /dev/null +++ b/docs/pages/annotate/getting-started.md @@ -0,0 +1,30 @@ +--- +title: Getting started +description: Install comark-annotate, register it, and give your agent its skill and CLI. +install: pnpm add comark @comark/html comark-annotate +--- + +## Usage + +Register the plugin and pass the HTML renderers as `components`: + +```ts +import { createHtmlRenderer } from "@comark/html"; +import annotate, { components } from "comark-annotate/html"; + +const render = createHtmlRenderer({ plugins: [annotate()], components }); +const html = await render(markdown); +``` + +The renderer inlines its styles and script once per document. Annotate renders only to HTML; [Renderers](/guide/renderers/) covers using it with other plugins, a strict Content Security Policy, and untrusted notes. + +## Agents + +The package ships an agent skill with the syntax, how to measure marks from the page, and how to check the result. The **For agents** button above installs it, and [Agents](/guide/agents/) covers the rest. + +It also comes with a small CLI that knows zoom and carousel too. `check` lists every problem in one or more files and exits with 1 if there are any; `render` writes a standalone page next to each file, which you can open and screenshot to confirm each mark sits on its target. + +```sh +npx comark-annotate check report.md other.md +npx comark-annotate render report.md -o report.html +``` diff --git a/docs/pages/annotate/guides.md b/docs/pages/annotate/guides.md new file mode 100644 index 00000000..e46b78c1 --- /dev/null +++ b/docs/pages/annotate/guides.md @@ -0,0 +1,40 @@ +--- +title: Guides +description: Side notes, placing marks, validation, and theming. +--- + +## Side notes + +`side="left"`, `side="right"`, or `side="top"` moves a note out of the list and next to or above the image, with a hand-drawn arrow to its mark. Pair it with `mark="none"` to point without drawing a box. Side notes have no number because the arrow links them. In a compare, only notes above keep their arrow. On narrow screens they drop under the image, the arrows hide, and each note and its target get a matching letter. + +Notes on the same side need room. The plugin reports side notes whose marks sit close together vertically, since they would likely overlap. It can't measure the text, so check the rendered page too, and move one note to the other side or into the list. + +## Placing marks + +Measure the element in the page you captured and divide by the viewport. Don't estimate from the image. + +```js +const r = element.getBoundingClientRect(); +const box = { + x: (r.x / innerWidth) * 100, + y: (r.y / innerHeight) * 100, + w: (r.width / innerWidth) * 100, + h: (r.height / innerHeight) * 100, +}; +``` + +For a full-page screenshot, add the scroll offset and divide by the page size instead. + +## Validation + +The plugin adds a problem with any block to `meta.issues`, as [Validation](/guide/validation/) describes. A screenshot goes by its `src`, or by its position and `alt` when another screenshot shares it; an annotation goes by its position in its screenshot: + +```txt +screenshot "after.png", annotation 2: the box runs past the image edge. Keep x + w and y + h within 100. +``` + +Unknown props are reported with the valid ones, and so is a `:bound` prop that points at missing frontmatter. Bindings to `data` or `props` are left to the renderer. + +## Theming + +Screenshots, comparisons, and diffs follow the shared tokens in [Theming](/guide/theming/). The mark colors are separate from the theme, because each one means something: a plain `.ca { --ca-color: #0070f3; --ca-solid: #0070f3; --ca-ink: #fff }` changes the default marks, arrows, and notes (`--ca-color`), stamps (`--ca-solid`), and stamp numbers (`--ca-ink`), and `--ca-red`, `--ca-amber`, `--ca-green`, `--ca-blue`, and `--ca-gray` change the named ones. Side notes and labels use `--ca-note-font`; this page sets it to Caveat for the handwritten look. A diff's code uses `--ca-code-font`, and its colors come from `--ca-red` and `--ca-green`. diff --git a/docs/pages/annotate/index.md b/docs/pages/annotate/index.md new file mode 100644 index 00000000..97cec9b8 --- /dev/null +++ b/docs/pages/annotate/index.md @@ -0,0 +1,128 @@ +--- +title: comark-annotate +description: Hand-drawn marks, numbered notes, and arrows on screenshots, written in Markdown. +tagline: Mark up screenshots in Markdown with hand-drawn marks, numbered notes, arrows, and before/after comparisons, and show code changes as suggested changes. +install: pnpm add comark @comark/html comark-annotate +--- + +::examples +:::example{label="Before / after"} +```md +::compare +:::screenshot{src="/before.png" alt="Filter toolbar wrapping onto a second row" viewport="640x250"} +::::annotation{x=5 y=29.6 w=90 h=45.3 color="red"} +Five filters don't fit, so the toolbar wraps and pushes the table down. +:::: + +::::annotation{x=50.9 y=55.4 w=10.5 h=14.2 mark="circle" color="red"} +**Apply** sits 6px lower than the chips. +:::: +::: + +:::screenshot{src="/after.png" alt="Filter toolbar on one row" viewport="640x250"} +::::annotation{x=67.9 y=34.8 w=13 h=14.2 mark="highlight" color="blue"} +Extra filters collapse into **+2 filters**. +:::: + +::::annotation{x=82.5 y=34.8 w=10.5 h=14.2 mark="check" color="green"} +**Apply** lines up with the chips. +:::: +::: +:: +``` +::: + +:::example{label="Slider"} +```md +::compare{mode="slider"} +:::screenshot{src="/before.png" alt="Filter toolbar wrapping onto a second row" viewport="640x250"} +::::annotation{x=50.9 y=55.4 w=10.5 h=14.2 mark="circle" color="red"} +**Apply** sits 6px lower than the chips. +:::: +::: + +:::screenshot{src="/after.png" alt="Filter toolbar on one row" viewport="640x250"} +::::annotation{x=82.5 y=34.8 w=10.5 h=14.2 mark="check" color="green"} +**Apply** lines up with the chips. +:::: +::: +:: +``` +::: + +:::example{label="Pointers"} +```md +::screenshot{src="/dashboard.png" alt="Forecast planning dashboard"} +:::annotation{x=37.6 y=31.9 w=11.9 h=3.3 mark="none" side="top"} +Units fell below plan. +::: + +:::annotation{x=66.4 y=16.4 w=29.4 h=22.2 mark="none" side="top"} +Accuracy now counts returns. +::: + +:::annotation{x=53.1 y=74.7 w=21.2 h=8.4 mark="none" side="left"} +Week 15 never loaded. +::: +:: +``` +::: + +:::example{label="Side notes"} +```md +::screenshot{src="/dashboard.png" alt="Forecast planning dashboard"} +:::annotation{x=37.6 y=31.9 w=11.9 h=3.3 mark="underline" side="left" color="red"} +Units fall below plan for the first time this quarter. +::: + +:::annotation{x=66.4 y=16.4 w=29.4 h=22.2 side="right"} +Accuracy now counts returns. +::: + +:::annotation{x=53.1 y=74.7 w=21.2 h=8.4 mark="highlight" side="left" color="amber"} +Week 15 is missing for City sneaker. +::: + +:::annotation{x=84.7 y=44.6 w=9.3 h=7.3 mark="circle"} +Notes without a `side` stay in the list. **Apply** waits until every filter is set. +::: +:: +``` +::: + +:::example{label="Marks"} +```md +::screenshot{src="/dashboard.png" alt="Forecast planning dashboard"} +:::annotation{x=81.8 y=5.3 w=14.1 h=7.3} +`box` frames an element. +::: + +:::annotation{x=37.6 y=31.9 w=11.9 h=3.3 mark="circle"} +`circle` rings a small detail. +::: + +:::annotation{x=4.2 y=6.1 w=23.4 h=5.8 mark="underline" color="blue"} +`underline` marks text. +::: + +:::annotation{x=53.1 y=74.7 w=21.2 h=8.4 mark="highlight" color="amber"} +`highlight` washes over an area. +::: + +:::annotation{x=6 y=44.6 w=45.2 h=7.3 mark="cross" color="red"} +`cross` strikes something out. +::: + +:::annotation{x=84.7 y=44.6 w=9.3 h=7.3 mark="check" color="green"} +`check` approves and sits beside its target. +::: +:: +``` +::: +:: + +Hover a mark or a note to highlight the other, and click a screenshot to zoom it. To share one, use its Copy button or press Ctrl+C (⌘C on a Mac) while it's focused; you get a PNG of the screenshot with its marks and notes. The browser's own Copy Image on the screenshot copies the same PNG once the pointer has been over it. If the image comes from another origin, its server has to send CORS headers. In an iframe, the frame needs `allow="clipboard-write"`. A comparison shows its screenshots side by side, or one at a time on a narrow page, and they zoom as one gallery; a slider lays one over the other, and you drag the divider between them. The page reads the same without a script; [zoom](/zoom/) and [carousel](/carousel/) come with the package. To page through screenshots spread across a page, or show a flow one step at a time, see the [recipes](/recipes/zoom/). + +## Credits + +The hand-drawn marks come from [AnnotationKit](https://annotationkit.vercel.app). diff --git a/docs/pages/carousel.md b/docs/pages/carousel.md deleted file mode 100644 index 1fe29725..00000000 --- a/docs/pages/carousel.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: comark-carousel -description: A carousel of any Markdown content, written in Markdown. -tagline: Slides of images, code, or anything else that scroll and snap, with arrows, a counter, keys, and zoom. -install: pnpm add comark @comark/html comark-carousel ---- - -::examples -:::example{label="Images"} -```md -::carousel{title="Filters"} -![Filter toolbar wrapping](/before.png) - -![Filter toolbar on one row](/after.png) - -![Forecast planning dashboard](/dashboard.png) -:: -``` -::: - -:::example{label="Any content"} -````md -::carousel{title="Setup"} -:::slide{title="Install"} -```sh -pnpm add comark @comark/html comark-carousel -``` -::: - -:::slide{title="Register"} -Pass the plugin and its `components` to the renderer. -::: - -:::slide{title="Write"} -> Every block inside `::carousel` is a slide. -::: -:: -```` -::: - -:::example{label="Two columns"} -```md -::carousel{columns=2} -![Before](/before.png) - -![After](/after.png) -:: -``` -::: -:: - -Slides scroll and snap without a script, with a scrollbar to reach the others. The script swaps the scrollbar for arrows, a slide counter, and arrow keys, and a click zooms a slide. - -## Usage - -Register the plugin and pass the renderer's `components`: - -```ts -import { createHtmlRenderer } from "@comark/html"; -import carousel, { components } from "comark-carousel/html"; - -const render = createHtmlRenderer({ plugins: [carousel()], components }); -``` - -For Vue, React, or Svelte, import from `comark-carousel/vue`, `comark-carousel/react`, or `comark-carousel/svelte`, and import `comark-carousel/style.css` and `comark-zoom/style.css` with your styles when you render on a server. [Renderers](/guide/renderers/) covers the rest. - -## Components - -### carousel - -Every block inside `::carousel` is a slide, unless it holds `:::slide` blocks. - -::example -```md -::carousel{title="Filters"} -![Filter toolbar wrapping](/before.png) - -![Filter toolbar on one row](/after.png) -:: -``` -:: - -| Prop | | -| --- | --- | -| `title` | Name read out by screen readers, and the heading of a zoomed slide that has no title or image of its own. Default `Carousel`. | -| `columns` | `2` or `3` shows several slides at once on a wide page and one at a time on a narrow one. Default `1`. | -| `zoom` | `false` turns zoom off. | - -Props can be inline or bound, such as `:columns="2"`. Bind `zoom` and `title` to the document or its front matter, not to render-time data: each slide takes them while parsing. - -### slide - -One slide that holds several blocks. Once a carousel has a `:::slide`, only its slides count: anything else inside it is dropped and reported. A `:::slide` outside a carousel renders as plain content. - -::example -````md -::carousel{title="Setup"} -:::slide{title="Install"} -```sh -pnpm add comark @comark/html comark-carousel -``` -::: - -:::slide{title="Register"} -Pass the plugin and its `components` to the renderer. -::: -:: -```` -:: - -| Prop | | -| --- | --- | -| `title` | Heading of the slide when it's zoomed. | - -An image alone in a slide, or alone in its paragraph, is centered, and so is a link around one. An image inside a sentence stays in the text. - -## Guides - -### Keys - -With focus in a carousel, the left and right arrow keys page through it, following the reading direction on a right-to-left page. They leave alone keys pressed with Alt, Ctrl, Meta, or Shift, and keys meant for a form field, video, ``, menu, tab list, slider, combo box, tree, or a wide code block in a slide. From a link or a button, they page. A disabled arrow keeps its focus, so the keys keep working. Paging jumps instead of scrolling smoothly when the reader prefers reduced motion. - -### Validation - -The plugin adds a problem with any carousel to `meta.issues`, as [Validation](/guide/validation/) describes: - -```txt -carousel 2 ("Setup") drops the paragraph "Pass the plugin" because it sits outside every :::slide; move it into a :::slide. -``` - -It reports content outside every `:::slide`, a carousel with no slides, `columns` that isn't 1, 2, or 3, a `:::slide` outside a carousel, and a `:zoom` or `:title` that reads render-time data. - -### Zoom - -A click zooms a slide, with the others in a strip at the bottom. Paging in the zoomed view moves the carousel, so closing it lands on the slide you were looking at. Zoom comes from [comark-zoom](/zoom/), which comes with the package. For annotated screenshots in a carousel, see the [recipe](/recipes/carousel/). - -### Theming - -The arrows, counter, and slide panels follow the shared tokens in [Theming](/guide/theming/). Override `--cc-gap` to change the space between slides. Each slide sits on a soft panel as tall as the tallest one, so slides of different heights line up; set `--cc-surface` to change its color, or to `transparent` with `.cc-slide { padding: 0 }` to drop it. diff --git a/docs/pages/carousel/components.md b/docs/pages/carousel/components.md new file mode 100644 index 00000000..8c36f296 --- /dev/null +++ b/docs/pages/carousel/components.md @@ -0,0 +1,52 @@ +--- +title: Components +description: carousel and slide, and the props each takes. +--- + +## carousel + +Every block inside `::carousel` is a slide, unless it holds `:::slide` blocks. + +::example +```md +::carousel{title="Filters"} +![Filter toolbar wrapping](/before.png) + +![Filter toolbar on one row](/after.png) +:: +``` +:: + +| Prop | | +| --- | --- | +| `title` | Name read out by screen readers, and the heading of a zoomed slide that has no title or image of its own. Default `Carousel`. | +| `columns` | `2` or `3` shows several slides at once on a wide page and one at a time on a narrow one. Default `1`. | +| `zoom` | `false` turns zoom off. | + +Props can be inline or bound, such as `:columns="2"`. Bind `zoom` and `title` to the document or its front matter, not to render-time data: each slide takes them while parsing. + +## slide + +One slide that holds several blocks. Once a carousel has a `:::slide`, only its slides count: anything else inside it is dropped and reported. A `:::slide` outside a carousel renders as plain content. + +::example +````md +::carousel{title="Setup"} +:::slide{title="Install"} +```sh +pnpm add comark @comark/html comark-carousel +``` +::: + +:::slide{title="Register"} +Pass the plugin and its `components` to the renderer. +::: +:: +```` +:: + +| Prop | | +| --- | --- | +| `title` | Heading of the slide when it's zoomed. | + +An image alone in a slide, or alone in its paragraph, is centered, and so is a link around one. An image inside a sentence stays in the text. diff --git a/docs/pages/carousel/getting-started.md b/docs/pages/carousel/getting-started.md new file mode 100644 index 00000000..a7537837 --- /dev/null +++ b/docs/pages/carousel/getting-started.md @@ -0,0 +1,18 @@ +--- +title: Getting started +description: Install comark-carousel and register it. +install: pnpm add comark @comark/html comark-carousel +--- + +## Usage + +Register the plugin and pass the renderer's `components`: + +```ts +import { createHtmlRenderer } from "@comark/html"; +import carousel, { components } from "comark-carousel/html"; + +const render = createHtmlRenderer({ plugins: [carousel()], components }); +``` + +For Vue, React, or Svelte, import from `comark-carousel/vue`, `comark-carousel/react`, or `comark-carousel/svelte`, and import `comark-carousel/style.css` and `comark-zoom/style.css` with your styles when you render on a server. [Renderers](/guide/renderers/) covers the rest. diff --git a/docs/pages/carousel/guides.md b/docs/pages/carousel/guides.md new file mode 100644 index 00000000..d9fb42f0 --- /dev/null +++ b/docs/pages/carousel/guides.md @@ -0,0 +1,26 @@ +--- +title: Guides +description: Keys, validation, zoom, and theming. +--- + +## Keys + +With focus in a carousel, the left and right arrow keys page through it, following the reading direction on a right-to-left page. They leave alone keys pressed with Alt, Ctrl, Meta, or Shift, and keys meant for a form field, video, ``, menu, tab list, slider, combo box, tree, or a wide code block in a slide. From a link or a button, they page. A disabled arrow keeps its focus, so the keys keep working. Paging jumps instead of scrolling smoothly when the reader prefers reduced motion. + +## Validation + +The plugin adds a problem with any carousel to `meta.issues`, as [Validation](/guide/validation/) describes: + +```txt +carousel 2 ("Setup") drops the paragraph "Pass the plugin" because it sits outside every :::slide; move it into a :::slide. +``` + +It reports content outside every `:::slide`, a carousel with no slides, `columns` that isn't 1, 2, or 3, a `:::slide` outside a carousel, and a `:zoom` or `:title` that reads render-time data. + +## Zoom + +A click zooms a slide, with the others in a strip at the bottom. Paging in the zoomed view moves the carousel, so closing it lands on the slide you were looking at. Zoom comes from [comark-zoom](/zoom/), which comes with the package. For annotated screenshots in a carousel, see the [recipe](/recipes/carousel/). + +## Theming + +The arrows, counter, and slide panels follow the shared tokens in [Theming](/guide/theming/). Override `--cc-gap` to change the space between slides. Each slide sits on a soft panel as tall as the tallest one, so slides of different heights line up; set `--cc-surface` to change its color, or to `transparent` with `.cc-slide { padding: 0 }` to drop it. diff --git a/docs/pages/carousel/index.md b/docs/pages/carousel/index.md new file mode 100644 index 00000000..cbe7718a --- /dev/null +++ b/docs/pages/carousel/index.md @@ -0,0 +1,52 @@ +--- +title: comark-carousel +description: A carousel of any Markdown content, written in Markdown. +tagline: Slides of images, code, or anything else that scroll and snap, with arrows, a counter, keys, and zoom. +install: pnpm add comark @comark/html comark-carousel +--- + +::examples +:::example{label="Images"} +```md +::carousel{title="Filters"} +![Filter toolbar wrapping](/before.png) + +![Filter toolbar on one row](/after.png) + +![Forecast planning dashboard](/dashboard.png) +:: +``` +::: + +:::example{label="Any content"} +````md +::carousel{title="Setup"} +:::slide{title="Install"} +```sh +pnpm add comark @comark/html comark-carousel +``` +::: + +:::slide{title="Register"} +Pass the plugin and its `components` to the renderer. +::: + +:::slide{title="Write"} +> Every block inside `::carousel` is a slide. +::: +:: +```` +::: + +:::example{label="Two columns"} +```md +::carousel{columns=2} +![Before](/before.png) + +![After](/after.png) +:: +``` +::: +:: + +Slides scroll and snap without a script, with a scrollbar to reach the others. The script swaps the scrollbar for arrows, a slide counter, and arrow keys, and a click zooms a slide. diff --git a/docs/pages/charts.md b/docs/pages/charts.md deleted file mode 100644 index 335b1343..00000000 --- a/docs/pages/charts.md +++ /dev/null @@ -1,313 +0,0 @@ ---- -title: comark-charts -description: Charts from a Markdown table, drawn to SVG with no script. -tagline: Write a table and get a chart drawn to SVG, with no script. The table stays under it, so people see the chart and agents read the numbers. -install: pnpm add comark @comark/html comark-charts ---- - -::examples -:::example{label="Bars"} -```md -::chart{title="Bundle size by package"} -| package | size (kB) | -| --- | --: | -| annotate | 41.2 | -| carousel | 9.8 | -| embed | 12.4 | -| zoom | 18.1 | -| lint | 27.5 | -:: -``` -::: - -:::example{label="Lines"} -```md -::chart{title="Build time, before and after the cache (ms)"} -| month | before | after | -| --- | --: | --: | -| 2026-04 | 820 | 610 | -| 2026-05 | 890 | 622 | -| 2026-06 | 905 | 640 | -| 2026-07 | 960 | 575 | -| 2026-08 | 975 | 590 | -| 2026-09 | 1,030 | 601 | -| 2026-10 | 1,025 | 540 | -:: -``` -::: - -:::example{label="Marked"} -```md -::chart{title="Build time, before and after the cache (ms)"} -| month | before | after | -| --- | --: | --: | -| 2026-04 | 820 | 610 | -| 2026-05 | 890 | 622 | -| 2026-06 | 905 | 640 | -| 2026-07 | 960 | 575 | -| 2026-08 | 975 | 590 | -| 2026-09 | 1,030 | 601 | -| 2026-10 | 1,025 | 540 | -::::annotation{at="2026-07" series="after" mark="circle" color="green"} -The cache lands, and builds drop below 600 ms. -:::: -::::annotation{at="2026-09" series="before" mark="none" side="top" color="red"} -The slowest build without it. -:::: -:: -``` -::: - -:::example{label="Before / after"} -```md -::chart{type="dumbbell" title="Page speed, before and after the refactor (ms)"} -| metric | before | after | -| --- | --: | --: | -| first paint | 820 | 540 | -| largest paint | 1,450 | 980 | -| time to interactive | 2,100 | 1,320 | -| total blocking | 380 | 160 | -:: -``` -::: - -:::example{label="Waterfall"} -```md -::chart{type="waterfall" title="Bundle size through this release (kB)"} -| step | size | -| --- | --: | -| v0.1 | 412 | -| add charts | 38 | -| drop moment | -24 | -| split zoom | -11 | -| v0.2 | 415 | -:: -``` -::: -:: - -The chart's source is a plain Markdown table, so anyone reading the Markdown, a person or an agent, reads the numbers rather than a chart spec. The page shows the chart with the same table under it, folded into **Data**, so a tool that turns the page into text keeps the numbers too. Without the plugin, the table still renders as a table. - -## Usage - -Register the plugin and pass the renderer's `components`: - -```ts -import { createHtmlRenderer } from "@comark/html"; -import charts, { components } from "comark-charts/html"; - -const render = createHtmlRenderer({ plugins: [charts()], components }); -``` - -Charts are drawn while rendering, on the server or at build time, by [Flint](https://github.com/microsoft/flint-chart) and [Vega](https://vega.github.io), which the package installs. The parse plugin only checks the table, so it stays light. - -## Syntax - -```md -::chart{type="line" x="month" y="before, after" title="Build time (ms)"} -| month | before | after | -| --- | --: | --: | -| 2026-04 | 820 | 610 | -:: -``` - -Put one table between `::chart` and `::`: a header row, then one row per bar, point, or step. The first column is x, and every other column is a series of numbers, named by its header. Rows are drawn in table order. - -| Prop | What it does | -| --- | --- | -| `type` | The chart type, from the table below. Without it, dates on x make a line and anything else makes bars. | -| `x` | The column along the x axis. Default the first. | -| `y` | The columns to draw, separated by commas. Default every column but `x`. | -| `title` | The chart's caption, and its name for screen readers. | - -Cells hold plain numbers, with commas between thousands if you like, such as `1,200`; put units in the header or the title, such as `size (kB)`. x values in the form `2026`, `2026-04`, `2026-04-15`, or an ISO date and time sit on a time axis, and numbers on a number axis, except on bars, steps, and dumbbells, which give each value its own row. - -## Chart types - -| `type` | Use it for | Table | -| --- | --- | --- | -| `bar` | One or more numbers per name: sizes, counts, timings. | Names, then one column per series. Several series make grouped bars. | -| `stacked` | Parts that add up to a total per name. | Names, then one column per part. | -| `line` | A trend over time or over a number. | Dates or numbers, then one column per series. Each line is labelled at its end. | -| `area` | Totals over time that add up. | Dates, then one column per part, stacked. | -| `scatter` | One measure against another. | Numbers, then one column per series. | -| `dumbbell` | Before and after for several metrics. | Metric names, then two or more columns, such as `before` and `after`. | -| `waterfall` | How changes add up to a total. | The starting total, then each change, negative for a drop, then the end total, all in one column. | -| `pie` | Shares of one whole. | Names, then one column of numbers that aren't negative. | - -Bars turn sideways when their labels are long or there are many of them, so file paths and test names stay whole. In a waterfall, the first row is a total, and so is the last when it equals the running sum; the others are changes, green up and red down. - -::examples -:::example{label="Sideways bars"} -```md -::chart{title="Slowest tests (s)"} -| test | duration | -| --- | --: | -| e2e/annotate.e2e.ts › copies a zoomed screenshot | 8.4 | -| e2e/zoom.e2e.ts › closes on Escape | 5.1 | -| e2e/carousel.e2e.ts › snaps to the next slide | 4.7 | -| packages/lint/test/cli.test.ts › exits with 1 | 2.2 | -:: -``` -::: - -:::example{label="Grouped"} -```md -::chart{title="Render phases (ms)"} -| phase | v0.1 | v0.2 | -| --- | --: | --: | -| parse | 120 | 80 | -| render | 160 | 102 | -| hydrate | 200 | 124 | -:: -``` -::: - -:::example{label="Stacked"} -```md -::chart{type="stacked" title="Page weight by asset (kB)"} -| page | js | css | images | -| --- | --: | --: | --: | -| home | 120 | 18 | 240 | -| docs | 95 | 22 | 60 | -| blog | 70 | 15 | 180 | -:: -``` -::: - -:::example{label="Area"} -```md -::chart{type="area" title="Lines in the repo (thousands)"} -| month | code | docs | -| --- | --: | --: | -| 2026-06 | 40 | 20 | -| 2026-07 | 46 | 24 | -| 2026-08 | 52 | 28 | -| 2026-09 | 58 | 32 | -| 2026-10 | 64 | 36 | -:: -``` -::: - -:::example{label="Scatter"} -```md -::chart{type="scatter" title="Load time against page size"} -| size (kB) | cold (ms) | warm (ms) | -| --: | --: | --: | -| 40 | 210 | 90 | -| 85 | 320 | 120 | -| 120 | 410 | 150 | -| 200 | 650 | 210 | -| 260 | 760 | 260 | -:: -``` -::: - -:::example{label="Pie"} -```md -::chart{type="pie" title="Renderers in use (%)"} -| renderer | share | -| --- | --: | -| html | 58 | -| vue | 24 | -| react | 12 | -| svelte | 6 | -:: -``` -::: -:: - -## Marks - -Point at data with [comark-annotate](/annotate/)'s marks: put an `::annotation` inside the chart, after the table, and name the row it marks with `at`, the row's x value as the table writes it. Add `series` to mark one column's value rather than the whole row. - -```md -::chart{title="Bundle size by package (kB)"} -| package | size | -| --- | --: | -| annotate | 41.2 | -| zoom | 18.1 | -:::annotation{at="annotate" color="red"} -The largest, with the marks and their notes. -::: -:: -``` - -| Prop | What it does | -| --- | --- | -| `at` | Required. The x value of the row to mark, copied from the table. | -| `series` | The column to mark in that row. Without it, the mark covers every series in the row. | -| `mark`, `color`, `side` | As on a [screenshot](/annotate/#annotation): `box`, `circle`, `underline`, `highlight`, `cross`, `check`, or `none`; a named or hex color; and `left`, `right`, or `top` for a side note with an arrow. | - -The renderer finds the shape it draws for that value, a bar, a point, a slice, or a step, and draws the mark around it, so marks stay on their data when the numbers change. Notes are numbered under the chart, as under a screenshot, and a marked chart zooms like one. comark-annotate comes with comark-charts, and checks the marks the same way. - -## Guides - -### Validation - -The plugin checks every chart while parsing and adds the problems to `meta.issues`, so you can reject a document before publishing it: - -```ts -import { parseMarkdown } from "comark"; -import charts from "comark-charts"; - -const { meta } = await parseMarkdown(markdown, { plugins: [charts()] }); -if (meta.issues.length) throw new Error(meta.issues.join("\n")); -``` - -Each issue names its chart by its position in the document and says how to fix it: - -```txt -`::chart` 2: row 3 has `41 kB` in `size`, which isn't a number. Write the number alone, such as `1200` or `1,200`, and put the unit in the header, such as `size (ms)`. -``` - -It reports unknown props and types, columns that `x` or `y` name but the table doesn't have, cells that aren't numbers, tables without rows or with unnamed or repeated columns, an x value that appears twice, content besides the table and its annotations, and data that doesn't fit the type: a line or an area of one row, a scatter without numbers on x, a dumbbell with one column, a pie or a waterfall with several, and a pie with a negative number or nothing but zeros. - -Annotations are checked too: a missing or unknown `at`, a `series` the chart doesn't draw, and the mark, color, and side, as comark-annotate checks them on screenshots. - -Rendering never throws. A cell that isn't a number is left out, a mark it can't place is left out, a type that doesn't fit falls back to the default, a pie or a waterfall draws its first column, and a chart with nothing to draw shows its content as it is. [comark-lint](/lint/) reports the same issues with their line numbers. - -### Theming - -The styles follow [shadcn/ui](https://ui.shadcn.com) and the shared `--comark-*` tokens, like the other plugins, so a chart switches with the page's dark mode: - -| Token | Falls back to | Used for | -| --- | --- | --- | -| `--comark-foreground` | `--foreground` | Values and the caption | -| `--comark-muted` | `--muted-foreground` | Axis labels, the **Data** toggle, waterfall totals | -| `--comark-border` | `--border` | Grid lines and the data table's rules | -| `--comark-background` | `--background` | The edges between slices and around points | -| `--comark-font` | `--font-sans` | All text | -| `--comark-chart-1` to `-6` | `--chart-1` to `-6` | Series, in column order | - -Without either, series use six colors that read on light and dark pages: blue, orange, green, purple, rose, and yellow. A waterfall's increases and decreases are series 3 and 5. - -```css -:root { - --comark-chart-1: oklch(0.65 0.2 250); -} -``` - -The rules sit in a `comark` CSS layer, so any plain rule of your own wins. A chart is a card, a `figure.cch` with `data-type`, holding a `figcaption`, the SVG in `.cch-plot`, and the table in `details.cch-data` as its footer. Marks use comark-annotate's styles and colors. - -### Size - -A chart keeps the size its text was set for, sized to fit its card with room for its labels, rather than stretching to the column, so labels read the same in every chart. Values sit on the marks when they fit; on crowded or stacked bars, or with negative numbers, the value axis reads them instead. Lines name their series at their ends unless the ends sit close together, and then a legend does. No row is ever dropped: a long sideways chart grows taller. A narrower column scrolls it sideways, and print shrinks it to the page. A chart with marks shrinks to fit instead, since its marks sit at percentages of the chart. - -### Renderers - -Only `@comark/html` has a renderer, `comark-charts/html`, which inlines the styles once per document. Vega draws on the server, and a Vue, React, or Svelte component would ship it to the browser. Charts are static: there are no tooltips, and the **Data** table holds every number. - -## Agents - -The package ships an agent skill with the table shape for each chart type and how to check the result. Install it from this site: - -```sh -npx skills add https://comark.onmax.me -``` - -Agents read charts without the skill: in the Markdown and in the page, each chart's data is a table. - -## Credits - -[Flint](https://github.com/microsoft/flint-chart), by Microsoft Research, chooses the axes, scales, labels, and layout from what the data means, and its New York Times theme is the base of the look. [Vega](https://vega.github.io) draws the SVG. [Comark](https://comark.dev), by the [Comark team](https://github.com/comarkdown), parses the Markdown and renders the HTML. diff --git a/docs/pages/charts/getting-started.md b/docs/pages/charts/getting-started.md new file mode 100644 index 00000000..5a3e5d04 --- /dev/null +++ b/docs/pages/charts/getting-started.md @@ -0,0 +1,28 @@ +--- +title: Getting started +description: Install comark-charts, register it, and give your agent its skill. +install: pnpm add comark @comark/html comark-charts +--- + +## Usage + +Register the plugin and pass the renderer's `components`: + +```ts +import { createHtmlRenderer } from "@comark/html"; +import charts, { components } from "comark-charts/html"; + +const render = createHtmlRenderer({ plugins: [charts()], components }); +``` + +Charts are drawn while rendering, on the server or at build time, by [Flint](https://github.com/microsoft/flint-chart) and [Vega](https://vega.github.io), which the package installs. The parse plugin only checks the table, so it stays light. + +## Agents + +The package ships an agent skill with the table shape for each chart type and how to check the result. Install it from this site: + +```sh +npx skills add https://comark.onmax.me +``` + +Agents read charts without the skill: in the Markdown and in the page, each chart's data is a table. diff --git a/docs/pages/charts/guides.md b/docs/pages/charts/guides.md new file mode 100644 index 00000000..aaae2813 --- /dev/null +++ b/docs/pages/charts/guides.md @@ -0,0 +1,59 @@ +--- +title: Guides +description: Validation, theming, chart size, and renderers. +--- + +## Validation + +The plugin checks every chart while parsing and adds the problems to `meta.issues`, so you can reject a document before publishing it: + +```ts +import { parseMarkdown } from "comark"; +import charts from "comark-charts"; + +const { meta } = await parseMarkdown(markdown, { plugins: [charts()] }); +if (meta.issues.length) throw new Error(meta.issues.join("\n")); +``` + +Each issue names its chart by its position in the document and says how to fix it: + +```txt +`::chart` 2: row 3 has `41 kB` in `size`, which isn't a number. Write the number alone, such as `1200` or `1,200`, and put the unit in the header, such as `size (ms)`. +``` + +It reports unknown props and types, columns that `x` or `y` name but the table doesn't have, cells that aren't numbers, tables without rows or with unnamed or repeated columns, an x value that appears twice, content besides the table and its annotations, and data that doesn't fit the type: a line or an area of one row, a scatter without numbers on x, a dumbbell with one column, a pie or a waterfall with several, and a pie with a negative number or nothing but zeros. + +Annotations are checked too: a missing or unknown `at`, a `series` the chart doesn't draw, and the mark, color, and side, as comark-annotate checks them on screenshots. + +Rendering never throws. A cell that isn't a number is left out, a mark it can't place is left out, a type that doesn't fit falls back to the default, a pie or a waterfall draws its first column, and a chart with nothing to draw shows its content as it is. [comark-lint](/lint/) reports the same issues with their line numbers. + +## Theming + +The styles follow [shadcn/ui](https://ui.shadcn.com) and the shared `--comark-*` tokens, like the other plugins, so a chart switches with the page's dark mode: + +| Token | Falls back to | Used for | +| --- | --- | --- | +| `--comark-foreground` | `--foreground` | Values and the caption | +| `--comark-muted` | `--muted-foreground` | Axis labels, the **Data** toggle, waterfall totals | +| `--comark-border` | `--border` | Grid lines and the data table's rules | +| `--comark-background` | `--background` | The edges between slices and around points | +| `--comark-font` | `--font-sans` | All text | +| `--comark-chart-1` to `-6` | `--chart-1` to `-6` | Series, in column order | + +Without either, series use six colors that read on light and dark pages: blue, orange, green, purple, rose, and yellow. A waterfall's increases and decreases are series 3 and 5. + +```css +:root { + --comark-chart-1: oklch(0.65 0.2 250); +} +``` + +The rules sit in a `comark` CSS layer, so any plain rule of your own wins. A chart is a card, a `figure.cch` with `data-type`, holding a `figcaption`, the SVG in `.cch-plot`, and the table in `details.cch-data` as its footer. Marks use comark-annotate's styles and colors. + +## Size + +A chart keeps the size its text was set for, sized to fit its card with room for its labels, rather than stretching to the column, so labels read the same in every chart. Values sit on the marks when they fit; on crowded or stacked bars, or with negative numbers, the value axis reads them instead. Lines name their series at their ends unless the ends sit close together, and then a legend does. No row is ever dropped: a long sideways chart grows taller. A narrower column scrolls it sideways, and print shrinks it to the page. A chart with marks shrinks to fit instead, since its marks sit at percentages of the chart. + +## Renderers + +Only `@comark/html` has a renderer, `comark-charts/html`, which inlines the styles once per document. Vega draws on the server, and a Vue, React, or Svelte component would ship it to the browser. Charts are static: there are no tooltips, and the **Data** table holds every number. diff --git a/docs/pages/charts/index.md b/docs/pages/charts/index.md new file mode 100644 index 00000000..15c814ad --- /dev/null +++ b/docs/pages/charts/index.md @@ -0,0 +1,93 @@ +--- +title: comark-charts +description: Charts from a Markdown table, drawn to SVG with no script. +tagline: Write a table and get a chart drawn to SVG, with no script. The table stays under it, so people see the chart and agents read the numbers. +install: pnpm add comark @comark/html comark-charts +--- + +::examples +:::example{label="Bars"} +```md +::chart{title="Bundle size by package"} +| package | size (kB) | +| --- | --: | +| annotate | 41.2 | +| carousel | 9.8 | +| embed | 12.4 | +| zoom | 18.1 | +| lint | 27.5 | +:: +``` +::: + +:::example{label="Lines"} +```md +::chart{title="Build time, before and after the cache (ms)"} +| month | before | after | +| --- | --: | --: | +| 2026-04 | 820 | 610 | +| 2026-05 | 890 | 622 | +| 2026-06 | 905 | 640 | +| 2026-07 | 960 | 575 | +| 2026-08 | 975 | 590 | +| 2026-09 | 1,030 | 601 | +| 2026-10 | 1,025 | 540 | +:: +``` +::: + +:::example{label="Marked"} +```md +::chart{title="Build time, before and after the cache (ms)"} +| month | before | after | +| --- | --: | --: | +| 2026-04 | 820 | 610 | +| 2026-05 | 890 | 622 | +| 2026-06 | 905 | 640 | +| 2026-07 | 960 | 575 | +| 2026-08 | 975 | 590 | +| 2026-09 | 1,030 | 601 | +| 2026-10 | 1,025 | 540 | +::::annotation{at="2026-07" series="after" mark="circle" color="green"} +The cache lands, and builds drop below 600 ms. +:::: +::::annotation{at="2026-09" series="before" mark="none" side="top" color="red"} +The slowest build without it. +:::: +:: +``` +::: + +:::example{label="Before / after"} +```md +::chart{type="dumbbell" title="Page speed, before and after the refactor (ms)"} +| metric | before | after | +| --- | --: | --: | +| first paint | 820 | 540 | +| largest paint | 1,450 | 980 | +| time to interactive | 2,100 | 1,320 | +| total blocking | 380 | 160 | +:: +``` +::: + +:::example{label="Waterfall"} +```md +::chart{type="waterfall" title="Bundle size through this release (kB)"} +| step | size | +| --- | --: | +| v0.1 | 412 | +| add charts | 38 | +| drop moment | -24 | +| split zoom | -11 | +| v0.2 | 415 | +:: +``` +::: +:: + +The chart's source is a plain Markdown table, so anyone reading the Markdown, a person or an agent, reads the numbers rather than a chart spec. The page shows the chart with the same table under it, folded into **Data**, so a tool that turns the page into text keeps the numbers too. Without the plugin, the table still renders as a table. + +## Credits + +[Flint](https://github.com/microsoft/flint-chart), by Microsoft Research, chooses the axes, scales, labels, and layout from what the data means, and its New York Times theme is the base of the look. [Vega](https://vega.github.io) draws the SVG. [Comark](https://comark.dev), by the [Comark team](https://github.com/comarkdown), parses the Markdown and renders the HTML. diff --git a/docs/pages/charts/syntax.md b/docs/pages/charts/syntax.md new file mode 100644 index 00000000..d3a91316 --- /dev/null +++ b/docs/pages/charts/syntax.md @@ -0,0 +1,142 @@ +--- +title: Syntax +description: The table each chart type reads, and the marks you can draw on a chart. +--- + +```md +::chart{type="line" x="month" y="before, after" title="Build time (ms)"} +| month | before | after | +| --- | --: | --: | +| 2026-04 | 820 | 610 | +:: +``` + +Put one table between `::chart` and `::`: a header row, then one row per bar, point, or step. The first column is x, and every other column is a series of numbers, named by its header. Rows are drawn in table order. + +| Prop | What it does | +| --- | --- | +| `type` | The chart type, from the table below. Without it, dates on x make a line and anything else makes bars. | +| `x` | The column along the x axis. Default the first. | +| `y` | The columns to draw, separated by commas. Default every column but `x`. | +| `title` | The chart's caption, and its name for screen readers. | + +Cells hold plain numbers, with commas between thousands if you like, such as `1,200`; put units in the header or the title, such as `size (kB)`. x values in the form `2026`, `2026-04`, `2026-04-15`, or an ISO date and time sit on a time axis, and numbers on a number axis, except on bars, steps, and dumbbells, which give each value its own row. + +## Chart types + +| `type` | Use it for | Table | +| --- | --- | --- | +| `bar` | One or more numbers per name: sizes, counts, timings. | Names, then one column per series. Several series make grouped bars. | +| `stacked` | Parts that add up to a total per name. | Names, then one column per part. | +| `line` | A trend over time or over a number. | Dates or numbers, then one column per series. Each line is labelled at its end. | +| `area` | Totals over time that add up. | Dates, then one column per part, stacked. | +| `scatter` | One measure against another. | Numbers, then one column per series. | +| `dumbbell` | Before and after for several metrics. | Metric names, then two or more columns, such as `before` and `after`. | +| `waterfall` | How changes add up to a total. | The starting total, then each change, negative for a drop, then the end total, all in one column. | +| `pie` | Shares of one whole. | Names, then one column of numbers that aren't negative. | + +Bars turn sideways when their labels are long or there are many of them, so file paths and test names stay whole. In a waterfall, the first row is a total, and so is the last when it equals the running sum; the others are changes, green up and red down. + +::examples +:::example{label="Sideways bars"} +```md +::chart{title="Slowest tests (s)"} +| test | duration | +| --- | --: | +| e2e/annotate.e2e.ts › copies a zoomed screenshot | 8.4 | +| e2e/zoom.e2e.ts › closes on Escape | 5.1 | +| e2e/carousel.e2e.ts › snaps to the next slide | 4.7 | +| packages/lint/test/cli.test.ts › exits with 1 | 2.2 | +:: +``` +::: + +:::example{label="Grouped"} +```md +::chart{title="Render phases (ms)"} +| phase | v0.1 | v0.2 | +| --- | --: | --: | +| parse | 120 | 80 | +| render | 160 | 102 | +| hydrate | 200 | 124 | +:: +``` +::: + +:::example{label="Stacked"} +```md +::chart{type="stacked" title="Page weight by asset (kB)"} +| page | js | css | images | +| --- | --: | --: | --: | +| home | 120 | 18 | 240 | +| docs | 95 | 22 | 60 | +| blog | 70 | 15 | 180 | +:: +``` +::: + +:::example{label="Area"} +```md +::chart{type="area" title="Lines in the repo (thousands)"} +| month | code | docs | +| --- | --: | --: | +| 2026-06 | 40 | 20 | +| 2026-07 | 46 | 24 | +| 2026-08 | 52 | 28 | +| 2026-09 | 58 | 32 | +| 2026-10 | 64 | 36 | +:: +``` +::: + +:::example{label="Scatter"} +```md +::chart{type="scatter" title="Load time against page size"} +| size (kB) | cold (ms) | warm (ms) | +| --: | --: | --: | +| 40 | 210 | 90 | +| 85 | 320 | 120 | +| 120 | 410 | 150 | +| 200 | 650 | 210 | +| 260 | 760 | 260 | +:: +``` +::: + +:::example{label="Pie"} +```md +::chart{type="pie" title="Renderers in use (%)"} +| renderer | share | +| --- | --: | +| html | 58 | +| vue | 24 | +| react | 12 | +| svelte | 6 | +:: +``` +::: +:: + +## Marks + +Point at data with [comark-annotate](/annotate/)'s marks: put an `::annotation` inside the chart, after the table, and name the row it marks with `at`, the row's x value as the table writes it. Add `series` to mark one column's value rather than the whole row. + +```md +::chart{title="Bundle size by package (kB)"} +| package | size | +| --- | --: | +| annotate | 41.2 | +| zoom | 18.1 | +:::annotation{at="annotate" color="red"} +The largest, with the marks and their notes. +::: +:: +``` + +| Prop | What it does | +| --- | --- | +| `at` | Required. The x value of the row to mark, copied from the table. | +| `series` | The column to mark in that row. Without it, the mark covers every series in the row. | +| `mark`, `color`, `side` | As on a [screenshot](/annotate/components/#annotation): `box`, `circle`, `underline`, `highlight`, `cross`, `check`, or `none`; a named or hex color; and `left`, `right`, or `top` for a side note with an arrow. | + +The renderer finds the shape it draws for that value, a bar, a point, a slice, or a step, and draws the mark around it, so marks stay on their data when the numbers change. Notes are numbered under the chart, as under a screenshot, and a marked chart zooms like one. comark-annotate comes with comark-charts, and checks the marks the same way. diff --git a/docs/pages/embed/getting-started.md b/docs/pages/embed/getting-started.md new file mode 100644 index 00000000..d8cc45a9 --- /dev/null +++ b/docs/pages/embed/getting-started.md @@ -0,0 +1,18 @@ +--- +title: Getting started +description: Install comark-embed and register it. +install: pnpm add comark @comark/html comark-embed +--- + +## Usage + +Register the plugin and pass the renderer's `components`: + +```ts +import { createHtmlRenderer } from "@comark/html"; +import embed, { components } from "comark-embed/html"; + +const render = createHtmlRenderer({ plugins: [embed()], components }); +``` + +For Vue, React, or Svelte, import from `comark-embed/vue`, `comark-embed/react`, or `comark-embed/svelte`; the components add the styles when they mount. Import `comark-embed/style.css` with your styles so a server-rendered page doesn't shift. [Renderers](/guide/renderers/) covers using it with other plugins. diff --git a/docs/pages/embed/index.md b/docs/pages/embed/index.md new file mode 100644 index 00000000..31d82ddf --- /dev/null +++ b/docs/pages/embed/index.md @@ -0,0 +1,31 @@ +--- +title: comark-embed +description: Videos, audio, and live code from a link, written in Markdown. +tagline: Paste a YouTube, Vimeo, Loom, Spotify, CodePen, StackBlitz, or Figma link and get its player, with the link under it. Nothing is fetched. +install: pnpm add comark @comark/html comark-embed +--- + +::examples +:::example{label="Video"} +```md +::embed{url="https://www.youtube.com/watch?v=KCrXgy8qtjM" title="Vite in 100 Seconds"} +:: +``` +::: + +:::example{label="Live code"} +```md +::embed{url="https://stackblitz.com/edit/vitejs-vite?file=index.html" title="A Vite starter on StackBlitz"} +:: +``` +::: + +:::example{label="Link card"} +```md +::embed{url="https://comark.dev" title="Comark, Markdown with components"} +:: +``` +::: +:: + +The plugin reads the player's address from the link alone, so a document renders the same offline, in tests, and at build time. The link stays under every player, so the page still makes sense in a feed reader or where iframes are blocked, and in print each player gives way to its address. diff --git a/docs/pages/embed.md b/docs/pages/embed/syntax.md similarity index 59% rename from docs/pages/embed.md rename to docs/pages/embed/syntax.md index e794ae95..d5d8d8fe 100644 --- a/docs/pages/embed.md +++ b/docs/pages/embed/syntax.md @@ -1,50 +1,8 @@ --- -title: comark-embed -description: Videos, audio, and live code from a link, written in Markdown. -tagline: Paste a YouTube, Vimeo, Loom, Spotify, CodePen, StackBlitz, or Figma link and get its player, with the link under it. Nothing is fetched. -install: pnpm add comark @comark/html comark-embed +title: Syntax +description: The links embed reads, its props, and how to theme players and cards. --- -::examples -:::example{label="Video"} -```md -::embed{url="https://www.youtube.com/watch?v=KCrXgy8qtjM" title="Vite in 100 Seconds"} -:: -``` -::: - -:::example{label="Live code"} -```md -::embed{url="https://stackblitz.com/edit/vitejs-vite?file=index.html" title="A Vite starter on StackBlitz"} -:: -``` -::: - -:::example{label="Link card"} -```md -::embed{url="https://comark.dev" title="Comark, Markdown with components"} -:: -``` -::: -:: - -The plugin reads the player's address from the link alone, so a document renders the same offline, in tests, and at build time. The link stays under every player, so the page still makes sense in a feed reader or where iframes are blocked, and in print each player gives way to its address. - -## Usage - -Register the plugin and pass the renderer's `components`: - -```ts -import { createHtmlRenderer } from "@comark/html"; -import embed, { components } from "comark-embed/html"; - -const render = createHtmlRenderer({ plugins: [embed()], components }); -``` - -For Vue, React, or Svelte, import from `comark-embed/vue`, `comark-embed/react`, or `comark-embed/svelte`; the components add the styles when they mount. Import `comark-embed/style.css` with your styles so a server-rendered page doesn't shift. [Renderers](/guide/renderers/) covers using it with other plugins. - -## Syntax - ```md ::embed{url="https://youtu.be/KCrXgy8qtjM" title="Vite in 100 Seconds" start=30} :: @@ -72,9 +30,7 @@ Any other `https://` link renders as a card with its `title` and host. Cards don The plugin adds every problem to `meta.issues`, as [Validation](/guide/validation/) describes: a missing `url`, one that isn't `https://` (it renders nothing), a provider's link it can't read an ID from (it renders as a card), a card without a `title`, a `start` that isn't whole seconds or is set on a player that can't seek, and content inside the block. Each issue names its block and says how to fix it. -## Guides - -### Theming +## Theming Players sit in a bordered frame with their link under it, and cards follow shadcn/ui's Item: a globe, the title, and the host. Both follow the shared tokens in [Theming](/guide/theming/). Override `--ce-app-height` to change the height of CodePen, StackBlitz, and Figma: diff --git a/docs/pages/github-callouts.md b/docs/pages/github-callouts.md deleted file mode 100644 index 5dd3b22f..00000000 --- a/docs/pages/github-callouts.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: comark-github-callouts -description: GitHub's alerts as callouts with an icon and a title, written in Markdown. -tagline: Write > [!NOTE] through > [!CAUTION] as on GitHub, and get a shadcn-style callout on any page. -install: pnpm add comark @comark/html comark-github-callouts ---- - -::examples -:::example{label="Types"} -```md -> [!NOTE] -> Drafts save every few seconds, so closing the tab keeps your work. - -> [!TIP] -> Press ⌘K to search every document. - -> [!IMPORTANT] -> Releases ship from `main` only. - -> [!WARNING] -> Changing the slug breaks links people already shared. - -> [!CAUTION] -> Deleting a folder deletes every document in it. -``` -::: - -:::example{label="Custom title"} -```md -> [!IMPORTANT] Decision -> Keep the existing endpoint. Clients pin its response shape. -``` -::: - -:::example{label="Any content"} -```md -> [!WARNING] Breaking change in v2 -> The `render` option moved: -> -> - `render(markdown)` is now `createHtmlRenderer()(markdown)` -> - `components` keys are tag names -> -> ```ts -> const render = createHtmlRenderer({ plugins, components }); -> ``` -``` -::: -:: - -The five types are GitHub's. Each gets its icon, its color, and its title; the text after the marker on its line replaces the title. - -Comark's built-in [alert plugin](https://comark.dev/plugins/defaults/alert) already reads these markers: it tags the quote with its type and leaves the drawing to you. Use this package when you want them drawn: the icon, the title, and the styles in every renderer, plus a title written on the marker's line and configurable default titles. - -## Usage - -Register the plugin and pass the renderer's `components`: - -```ts -import { createHtmlRenderer } from "@comark/html"; -import githubCallouts, { components } from "comark-github-callouts/html"; - -const render = createHtmlRenderer({ plugins: [githubCallouts()], components }); -``` - -The plugin is named `alert`, so it takes the place of Comark's built-in alert plugin, and works with `registerDefaultPlugins: false` too. A quote without a marker, or with a type GitHub doesn't have, stays a quote. - -`githubCallouts({ titles: { note: "Nota", warning: "Aviso" } })` changes the default titles, for a site in another language. - -For Vue, React, or Svelte, import from `comark-github-callouts/vue`, `comark-github-callouts/react`, or `comark-github-callouts/svelte`; the components add the styles when they mount. Import `comark-github-callouts/style.css` with your styles so a server-rendered page doesn't shift. [Renderers](/guide/renderers/) covers using it with other plugins. - -## Syntax - -```md -> [!TYPE] Optional title -> The body: any Markdown, over as many lines as it needs. -``` - -| Type | Use it for | -| --- | --- | -| `NOTE` | Context the reader should know, even when skimming | -| `TIP` | A better or faster way to do something | -| `IMPORTANT` | Something the reader needs to succeed | -| `WARNING` | A risk that needs their attention now | -| `CAUTION` | An action with consequences they can't undo | - -The marker is case-insensitive. Callouts work inside lists and other blocks. - -## Guides - -### Theming - -Callouts follow shadcn/ui's Alert and the shared tokens in [Theming](/guide/theming/), and read shadcn's `--destructive` for `CAUTION`. Set `--cgc-note`, `--cgc-tip`, `--cgc-important`, `--cgc-warning`, or `--cgc-caution` to recolor one type: - -```css -.cgc { - --cgc-note: oklch(0.55 0.2 260); -} -``` - -A callout is a `.cgc` with `data-type`, holding `.cgc-icon`, `.cgc-title`, and `.cgc-body`. diff --git a/docs/pages/github-callouts/getting-started.md b/docs/pages/github-callouts/getting-started.md new file mode 100644 index 00000000..f15b390f --- /dev/null +++ b/docs/pages/github-callouts/getting-started.md @@ -0,0 +1,22 @@ +--- +title: Getting started +description: Install comark-github-callouts and register it. +install: pnpm add comark @comark/html comark-github-callouts +--- + +## Usage + +Register the plugin and pass the renderer's `components`: + +```ts +import { createHtmlRenderer } from "@comark/html"; +import githubCallouts, { components } from "comark-github-callouts/html"; + +const render = createHtmlRenderer({ plugins: [githubCallouts()], components }); +``` + +The plugin is named `alert`, so it takes the place of Comark's built-in alert plugin, and works with `registerDefaultPlugins: false` too. A quote without a marker, or with a type GitHub doesn't have, stays a quote. + +`githubCallouts({ titles: { note: "Nota", warning: "Aviso" } })` changes the default titles, for a site in another language. + +For Vue, React, or Svelte, import from `comark-github-callouts/vue`, `comark-github-callouts/react`, or `comark-github-callouts/svelte`; the components add the styles when they mount. Import `comark-github-callouts/style.css` with your styles so a server-rendered page doesn't shift. [Renderers](/guide/renderers/) covers using it with other plugins. diff --git a/docs/pages/github-callouts/index.md b/docs/pages/github-callouts/index.md new file mode 100644 index 00000000..ee00f04c --- /dev/null +++ b/docs/pages/github-callouts/index.md @@ -0,0 +1,52 @@ +--- +title: comark-github-callouts +description: GitHub's alerts as callouts with an icon and a title, written in Markdown. +tagline: Write > [!NOTE] through > [!CAUTION] as on GitHub, and get a shadcn-style callout on any page. +install: pnpm add comark @comark/html comark-github-callouts +--- + +::examples +:::example{label="Types"} +```md +> [!NOTE] +> Drafts save every few seconds, so closing the tab keeps your work. + +> [!TIP] +> Press ⌘K to search every document. + +> [!IMPORTANT] +> Releases ship from `main` only. + +> [!WARNING] +> Changing the slug breaks links people already shared. + +> [!CAUTION] +> Deleting a folder deletes every document in it. +``` +::: + +:::example{label="Custom title"} +```md +> [!IMPORTANT] Decision +> Keep the existing endpoint. Clients pin its response shape. +``` +::: + +:::example{label="Any content"} +```md +> [!WARNING] Breaking change in v2 +> The `render` option moved: +> +> - `render(markdown)` is now `createHtmlRenderer()(markdown)` +> - `components` keys are tag names +> +> ```ts +> const render = createHtmlRenderer({ plugins, components }); +> ``` +``` +::: +:: + +The five types are GitHub's. Each gets its icon, its color, and its title; the text after the marker on its line replaces the title. + +Comark's built-in [alert plugin](https://comark.dev/plugins/defaults/alert) already reads these markers: it tags the quote with its type and leaves the drawing to you. Use this package when you want them drawn: the icon, the title, and the styles in every renderer, plus a title written on the marker's line and configurable default titles. diff --git a/docs/pages/github-callouts/syntax.md b/docs/pages/github-callouts/syntax.md new file mode 100644 index 00000000..21134f3b --- /dev/null +++ b/docs/pages/github-callouts/syntax.md @@ -0,0 +1,31 @@ +--- +title: Syntax +description: The five callout types, their titles, and how to theme them. +--- + +```md +> [!TYPE] Optional title +> The body: any Markdown, over as many lines as it needs. +``` + +| Type | Use it for | +| --- | --- | +| `NOTE` | Context the reader should know, even when skimming | +| `TIP` | A better or faster way to do something | +| `IMPORTANT` | Something the reader needs to succeed | +| `WARNING` | A risk that needs their attention now | +| `CAUTION` | An action with consequences they can't undo | + +The marker is case-insensitive. Callouts work inside lists and other blocks. + +## Theming + +Callouts follow shadcn/ui's Alert and the shared tokens in [Theming](/guide/theming/), and read shadcn's `--destructive` for `CAUTION`. Set `--cgc-note`, `--cgc-tip`, `--cgc-important`, `--cgc-warning`, or `--cgc-caution` to recolor one type: + +```css +.cgc { + --cgc-note: oklch(0.55 0.2 260); +} +``` + +A callout is a `.cgc` with `data-type`, holding `.cgc-icon`, `.cgc-title`, and `.cgc-body`. diff --git a/docs/pages/guide/theming.md b/docs/pages/guide/theming.md index 8fa8d6b7..afa24731 100644 --- a/docs/pages/guide/theming.md +++ b/docs/pages/guide/theming.md @@ -60,8 +60,8 @@ Some things belong to one plugin, so they have their own variables: | Plugin | Variables | | --- | --- | -| [annotate](/annotate/#guides-theming) | `--ca-color`, `--ca-solid`, and `--ca-ink` for the default marks, stamps, and stamp numbers; `--ca-red`, `--ca-amber`, `--ca-green`, `--ca-blue`, and `--ca-gray` for the named colors; `--ca-note-font` and `--ca-code-font` | -| [carousel](/carousel/#guides-theming) | `--cc-gap` between slides, `--cc-surface` behind them | -| [embed](/embed/#guides-theming) | `--ce-app-height` for CodePen, StackBlitz, and Figma | -| [github-callouts](/github-callouts/#guides-theming) | `--cgc-note`, `--cgc-tip`, `--cgc-important`, `--cgc-warning`, and `--cgc-caution` | -| [zoom](/zoom/#guides-theming) | `data-zoom-context`, for items that need styles from their ancestors | +| [annotate](/annotate/guides/#theming) | `--ca-color`, `--ca-solid`, and `--ca-ink` for the default marks, stamps, and stamp numbers; `--ca-red`, `--ca-amber`, `--ca-green`, `--ca-blue`, and `--ca-gray` for the named colors; `--ca-note-font` and `--ca-code-font` | +| [carousel](/carousel/guides/#theming) | `--cc-gap` between slides, `--cc-surface` behind them | +| [embed](/embed/syntax/#theming) | `--ce-app-height` for CodePen, StackBlitz, and Figma | +| [github-callouts](/github-callouts/syntax/#theming) | `--cgc-note`, `--cgc-tip`, `--cgc-important`, `--cgc-warning`, and `--cgc-caution` | +| [zoom](/zoom/guides/#theming) | `data-zoom-context`, for items that need styles from their ancestors | diff --git a/docs/pages/guide/validation.md b/docs/pages/guide/validation.md index c78698fe..cd4e5c16 100644 --- a/docs/pages/guide/validation.md +++ b/docs/pages/guide/validation.md @@ -35,17 +35,17 @@ include "./x.md": file not found. It resolved to "guide/x.md" under the root; ch | Plugin | Reports | | --- | --- | -| [annotate](/annotate/#guides-validation) | Missing or unknown props, marks outside the image, side notes that would overlap, compares whose viewports differ, and diffs that are too long, mix files, or change nothing | -| [carousel](/carousel/#guides-validation) | Content outside every `:::slide`, empty carousels, bad `columns`, stray slides, and props bound to render-time data | +| [annotate](/annotate/guides/#validation) | Missing or unknown props, marks outside the image, side notes that would overlap, compares whose viewports differ, and diffs that are too long, mix files, or change nothing | +| [carousel](/carousel/guides/#validation) | Content outside every `:::slide`, empty carousels, bad `columns`, stray slides, and props bound to render-time data | | [embed](/embed/) | Missing or non-https links, links it can't read, cards without a `title`, and a bad `start` | -| [tabs](/tabs/#guides-validation) | Tabs without a label or with the same label, content outside every tab, anything but code blocks in a code group, empty blocks, and stray `:::tabs-item` | -| [include](/include/#guides-validation) | Missing files, paths that leave the root, cycles, and bad `lines` or `region` | +| [tabs](/tabs/guides/#validation) | Tabs without a label or with the same label, content outside every tab, anything but code blocks in a code group, empty blocks, and stray `:::tabs-item` | +| [include](/include/guides/#validation) | Missing files, paths that leave the root, cycles, and bad `lines` or `region` | github-callouts and zoom have nothing to report: a quote that isn't a callout stays a quote, and anything can zoom. ## Line numbers -`meta.issues` can't say where a problem is, because Comark gives nodes no source positions. [comark-lint](/lint/) finds the line of each issue and adds what Comark forgives silently: blocks never closed, misspelled components, broken anchors, and unsafe URLs. Run it in CI and in [Vite Doctor](/lint/#usage-vite-doctor): +`meta.issues` can't say where a problem is, because Comark gives nodes no source positions. [comark-lint](/lint/) finds the line of each issue and adds what Comark forgives silently: blocks never closed, misspelled components, broken anchors, and unsafe URLs. Run it in CI and in [Vite Doctor](/lint/getting-started/#vite-doctor): ```sh pnpm comark-lint "docs/**/*.md" diff --git a/docs/pages/include.md b/docs/pages/include.md deleted file mode 100644 index c397ecd3..00000000 --- a/docs/pages/include.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: comark-include -description: Markdown partials and code snippets from files, written in Markdown. -tagline: Paste a shared section, or a few lines of real source, into any page with one line, and keep a single copy of each. -install: pnpm add comark comark-include ---- - -::examples -:::example{label="Partial"} -```md -:include{src="./partials/install.md"} -``` -::: - -:::example{label="Region"} -```md -:include{src="./partials/greet.ts" region="message"} -``` -::: - -:::example{label="Lines"} -```md -:include{src="./partials/greet.ts" lines="1:3"} -``` -::: - -:::example{label="Source"} -```md -:include{src="./partials/install.md" code} -``` -::: -:: - -These examples include real files from this site's source. A Markdown file is pasted into the page, and any other file becomes a code block in the language of its extension. - -## Usage - -Register the plugin. It has no components, because the included text renders as the rest of the page does: - -```ts -import { parseMarkdown } from "comark"; -import include from "comark-include"; - -const document = await parseMarkdown(markdown, { - plugins: [include({ root: "content", base: "guide" })], -}); -``` - -## Syntax - -Each include sits alone on its own line, with one colon: - -```md -:include{src="./parts/setup.md"} - -:include{src="/src/index.ts" lines="10:24"} - -:include{src="/src/index.ts" region="setup"} -``` - -| Argument | | -| --- | --- | -| `src` | The file. A relative path starts at the page's folder, or at the including partial's; `/` starts at `root`. Required. | -| `lines` | `"10:24"`: lines 10 to 24, 1-based and inclusive. `"10:"` and `":24"` leave one end open. Lines count from the top of the file, frontmatter included. | -| `region` | The lines between `#region setup` and `#endregion setup`. A marker starts its line, after a comment opener if any: `//`, `#`, `/*`, `