comark-ui 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: before-after-evidence
3
- description: Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page. Use when a pull request, bug fix, or design change alters what people see, or when someone asks for before/after screenshots or visual evidence.
3
+ description: Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page, and a change in motion as a short recording. Use when a pull request, bug fix, or design change alters what people see or how it moves, or when someone asks for before/after screenshots, a recording, or visual evidence.
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -10,6 +10,13 @@ A before/after is evidence when a reviewer can check every claim against the two
10
10
 
11
11
  This skill writes the document. Your host decides where it's published and who can open it.
12
12
 
13
+ ## When the change is motion
14
+
15
+ An animation, a transition, a loading sequence, or feedback while dragging happens between frames, so record it. A still taken partway through a motion looks like a broken layout, and two of them can't show that anything moves. Follow [references/recording.md](references/recording.md) in place of steps 1 and 2, then write and check the document with steps 3 and 4, with the clip where the `::compare` goes.
16
+
17
+ - When the old behavior jumped straight to its end state, the after clip alone is the evidence, and the document's first sentence says what happened before.
18
+ - When the change retimes motion that already existed, such as its easing, its duration, or a stutter, record both sides with the same script.
19
+
13
20
  ## 1. Capture a comparable pair
14
21
 
15
22
  Take the after capture from the branch or its preview, and the before capture from a deployment or commit that still behaves the old way. Capture both with the same viewport, zoom, theme, data, scroll position, and open menus, so the change is the only difference.
@@ -65,6 +72,7 @@ Image `src` must be a URL the renderer can reach: a path next to the document, o
65
72
  ```
66
73
 
67
74
  Without Vite+, use `pnpm exec` with the same arguments. Open `evidence-screenshots.html` and take a screenshot of it. This CLI renders annotate, carousel, and zoom only; check any `::diff` or other blocks in the host's full Comark UI preview.
68
- 3. Check that each mark sits on its target and each note matches what its side shows. Measure a mark that misses again from the source; a box moved by eye stays a guess.
75
+
76
+ 3. Check that each mark sits on its target and each note matches what its side shows. A clip should start by itself and loop in the page. Measure a mark that misses again from the source; a box moved by eye stays a guess.
69
77
 
70
78
  Done when you've looked at the rendered page and every mark lands. Name any mark or block you couldn't check in your report.
@@ -1,3 +1,3 @@
1
1
  interface:
2
2
  display_name: "Before/after evidence"
3
- short_description: "Show a visible change with comparable, measured screenshots"
3
+ short_description: "Show a visible change with measured screenshots, or motion with a clip"
@@ -0,0 +1,130 @@
1
+ # Recording motion
2
+
3
+ A clip is evidence when the reviewer sees the motion at its real speed, starting and ending at rest, with nothing else on screen changing.
4
+
5
+ ## 1. Script the interaction
6
+
7
+ Drive the page with a script, such as Playwright, so a retake repeats the same steps at the same pace.
8
+
9
+ - Size the viewport to the component, the way you'd clip a still. The viewport is the frame.
10
+ - Rest about 0.7 s before each action and longer than the animation after it, about 1.3 s, so every motion starts and ends on a still frame.
11
+ - Show the motion two or three times, such as three tab switches, and keep the clip to a few seconds.
12
+ - Draw a pointer. A headless browser draws none, and without one the reader sees things change with nothing pressing them. Move the mouse to its target in steps, not in one jump.
13
+
14
+ ## 2. Capture the frames
15
+
16
+ Chrome's screencast sends a lossless PNG each time the page paints. Playwright's `recordVideo` is simpler, but it starts recording during page load and its VP8 encoding blurs small text.
17
+
18
+ Both capture CSS pixels, whatever `deviceScaleFactor` says. For sharp text, lay the page out at double size: double the viewport, the root `font-size`, and any font sizes set in `px`. Keep CSS `zoom` and `transform` off the page. Motion code often measures boxes with `getBoundingClientRect`, and under either one it draws its moving parts in the wrong place. On a tab transition recorded with `zoom: 2`, the leaving tokens appeared below the code block.
19
+
20
+ Save this as `record.mjs` in a workspace with `@playwright/test` and its Chromium browser installed, with ffmpeg on `PATH`. Adjust the viewport and tab names for the page, then run `node record.mjs <url>` with the page's URL.
21
+
22
+ ```js
23
+ import { chromium } from "@playwright/test";
24
+ import { execFileSync } from "node:child_process";
25
+ import { mkdirSync, writeFileSync } from "node:fs";
26
+
27
+ const url = process.argv[2];
28
+ if (!url) throw new Error("Usage: node record.mjs <url>");
29
+
30
+ const width = 1456,
31
+ height = 340,
32
+ dir = "clip"; // twice the component's CSS size
33
+ mkdirSync(dir, { recursive: true });
34
+ const browser = await chromium.launch();
35
+ const page = await browser.newPage({ viewport: { width, height } });
36
+ await page.goto(url);
37
+ await page.addStyleTag({ content: "html{font-size:32px}" }); // plus any px fonts, doubled
38
+
39
+ // An arrow whose tip follows the mouse, so it leaves the label it points at readable.
40
+ const arrow = encodeURIComponent(
41
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 20"><path d="M1 1v16l4-4 3 6 2-1-3-6h6z" fill="#111" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/></svg>',
42
+ );
43
+ await page.addStyleTag({
44
+ content: `#pointer{position:fixed;left:0;top:0;z-index:9;width:28px;height:40px;background:url("data:image/svg+xml,${arrow}");pointer-events:none}`,
45
+ });
46
+ await page.evaluate(() => {
47
+ const tip = document.body.appendChild(
48
+ Object.assign(document.createElement("div"), { id: "pointer" }),
49
+ );
50
+ addEventListener("mousemove", (e) => (tip.style.translate = `${e.clientX}px ${e.clientY}px`));
51
+ });
52
+
53
+ const cdp = await page.context().newCDPSession(page);
54
+ const frames = [];
55
+ cdp.on("Page.screencastFrame", ({ data, metadata, sessionId }) => {
56
+ frames.push({ data, at: metadata.timestamp });
57
+ cdp.send("Page.screencastFrameAck", { sessionId }).catch(() => {});
58
+ });
59
+ await cdp.send("Page.startScreencast", { format: "png", maxWidth: width, maxHeight: height });
60
+ await page.mouse.move(width - 80, height - 60); // paints the first frame
61
+ await page.waitForTimeout(700);
62
+ for (const name of ["Second", "Third", "First"]) {
63
+ const box = await page.getByRole("tab", { name }).boundingBox();
64
+ await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 14 });
65
+ await page.waitForTimeout(150); // lets the reader see where it presses
66
+ await page.mouse.down();
67
+ await page.mouse.up();
68
+ await page.waitForTimeout(1300);
69
+ }
70
+ const end = Date.now() / 1000;
71
+ await cdp.send("Page.stopScreencast");
72
+ await browser.close();
73
+
74
+ // A frame arrives only when the page paints, so each one holds until the next.
75
+ let list = "";
76
+ frames.forEach((frame, i) => {
77
+ writeFileSync(`${dir}/${i}.png`, Buffer.from(frame.data, "base64"));
78
+ list += `file '${i}.png'\nduration ${((frames[i + 1]?.at ?? end) - frame.at).toFixed(4)}\n`;
79
+ });
80
+ writeFileSync(`${dir}/frames.txt`, `${list}file '${frames.length - 1}.png'\n`);
81
+ execFileSync("ffmpeg", [
82
+ "-y",
83
+ "-f",
84
+ "concat",
85
+ "-i",
86
+ `${dir}/frames.txt`,
87
+ "-vf",
88
+ "fps=30,format=yuv420p",
89
+ "-c:v",
90
+ "libx264",
91
+ "-crf",
92
+ "18",
93
+ "-movflags",
94
+ "+faststart",
95
+ "-an",
96
+ `${dir}/clip.mp4`,
97
+ ]);
98
+ ```
99
+
100
+ The mp4 is H.264, so every browser plays it. A few seconds at this size come to a few hundred KB.
101
+
102
+ ## 3. Look at the clip
103
+
104
+ Pull frames from the middle of a motion and from a rest, and look at them:
105
+
106
+ ```sh
107
+ for t in 0.5 1.1 1.25 1.4; do ffmpeg -y -loglevel error -ss $t -i clip/clip.mp4 -frames:v 1 clip/at-$t.png; done
108
+ ```
109
+
110
+ A frame from the middle of a motion shows what reviewers can't see in a still: parts in between their two places, fading, or drawn where they don't belong.
111
+
112
+ Done when a frame from the middle of each motion shows the motion your document describes, and the clip starts and ends at rest.
113
+
114
+ ## 4. Write the document
115
+
116
+ Keep the shape of a before/after document, with the clip in place of the compare:
117
+
118
+ ```md
119
+ Code groups with `magic` now slide the tokens their snippets share into place when you switch tabs. Before, the code swapped all at once.
120
+
121
+ Open a `::code-group{magic}` and select **tinyjson5**, then **tinyini**.
122
+
123
+ ![Each tab switch slides import and from into place while the package names fade between them](magic-tabs.mp4){width=1456 height=340 autoplay loop muted}
124
+ ```
125
+
126
+ - The first sentence says what moved before, so the after clip alone carries the comparison.
127
+ - The alt text says what moves, for a reader whose browser can't play the file.
128
+ - `autoplay loop muted` plays the clip like an animated screenshot. Browsers autoplay only muted video.
129
+ - `width` and `height` are the clip's pixels, so the page keeps its room before the file loads. `ffprobe -v error -show_entries stream=width,height -of csv=p=0 clip.mp4` prints them.
130
+ - When the change retimes motion that already existed, put the before clip first, recorded with the same script, and title each with a line above it.
@@ -29,6 +29,7 @@ Press **Ctrl+K** to find the pigeon data.
29
29
  - `::tabs`: each `:::tabs-item` inside is one tab. Content outside every tab doesn't render, and is reported.
30
30
  - `:::tabs-item`: `label` is required and must differ from the other labels in its block, since `sync` matches tabs by label. `icon` draws an Iconify name, such as `lucide:apple`, or an image URL before the label.
31
31
  - `::code-group`: only code blocks go inside. Name each one's tab in brackets after its language, as in `sh [pnpm]`, with an optional icon after it, as in `sh [pnpm] icon=logos:pnpm`. The tab replaces the block's own title. `title="pnpm"` names no tab, so a block without brackets is reported.
32
+ - A code group animates a switch between two snippets that share at least half their tokens, and swaps unlike ones at once. `magic` on `::code-group` forces the animation on, and `magic="false"` turns it off. Leave it out unless the default is wrong for that group.
32
33
  - `sync`, on either block, is a key shared by blocks that switch together. Picking a tab picks the tab with the same label in every block with that key, and the next page that uses the key remembers it.
33
34
  - A code block marked `pm`, as in `sh pm`, holds package manager commands in any manager (npm, npx, pnpm, pnpx, yarn, bun, bunx, or deno) and becomes a code group with a tab each for npm, pnpm, yarn, and bun, synced as `pm`. The manager it's written in keeps its commands as written. It reads install, frozen install (`npm ci`), add with `-D`, `-E`, `-O`, `--save-peer`, or `-g`, remove, run, `npx`/`dlx`, create, and init. An update or a `pnpm exec` stays only in its own manager's tab, and the others are left out and reported. Only commands at the start of a line or after `&&`, `||`, or `;` change. Inside a `::code-group`, `pm` does nothing.
34
35
 
package/dist/skills.mjs CHANGED
@@ -3,20 +3,20 @@
3
3
  const skills = [
4
4
  {
5
5
  "name": "before-after-evidence",
6
- "description": "Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page. Use when a pull request, bug fix, or design change alters what people see, or when someone asks for before/after screenshots or visual evidence.",
6
+ "description": "Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page, and a change in motion as a short recording. Use when a pull request, bug fix, or design change alters what people see or how it moves, or when someone asks for before/after screenshots, a recording, or visual evidence.",
7
7
  "frontmatter": {
8
8
  "name": "before-after-evidence",
9
- "description": "Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page. Use when a pull request, bug fix, or design change alters what people see, or when someone asks for before/after screenshots or visual evidence.",
9
+ "description": "Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page, and a change in motion as a short recording. Use when a pull request, bug fix, or design change alters what people see or how it moves, or when someone asks for before/after screenshots, a recording, or visual evidence.",
10
10
  "license": "MIT"
11
11
  },
12
12
  "files": [
13
13
  {
14
14
  "path": "SKILL.md",
15
- "text": "---\nname: before-after-evidence\ndescription: Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page. Use when a pull request, bug fix, or design change alters what people see, or when someone asks for before/after screenshots or visual evidence.\nlicense: MIT\n---\n\n# Before/after evidence\n\nA before/after is evidence when a reviewer can check every claim against the two images and nothing else. The reader can't repair a bad pair, so get three things right: two captures of the same view, marks on what changed, and notes in the words the screen shows.\n\nThis skill writes the document. Your host decides where it's published and who can open it.\n\n## 1. Capture a comparable pair\n\nTake the after capture from the branch or its preview, and the before capture from a deployment or commit that still behaves the old way. Capture both with the same viewport, zoom, theme, data, scroll position, and open menus, so the change is the only difference.\n\n- Set up the state that shows the change before you capture: the long name that wrapped, the error that stayed hidden, the empty list.\n- Clip both captures to one region around the change, the same rectangle on each side. A `::compare` gives each image under half the page width, where a full-page capture's text is too small to read.\n- Pin anything that moves the layout between the two states, such as a vertically centered dialog that grows by one line, so the same element sits at the same place in both.\n\nIf you can't capture one side comparably, stop and say what blocked you. A description in place of a screenshot, or one side alone, isn't before/after evidence.\n\nDone when both images exist, have the same size, and differ only where the change is.\n\n## 2. Measure every mark\n\nMarks take `x`, `y`, `w`, and `h` as percentages of the image. Measure them. An estimate drifts a few percent, which on a half-width image lands on the neighbouring button.\n\n- **A page you captured:** read the element's box in the same browser, viewport, and state, minus the clip's offset. [references/measuring.md](references/measuring.md) has the snippet.\n- **A still you didn't capture**, such as a frame from a screen recording, a design export, or an image attached to a bug report: there is no DOM to ask. Measure in image pixels and divide by the image's natural width and height. Confirm each box by cropping the image to it and looking at the crop, as the same reference shows.\n\nMark the change at its own size: the field, the label, the button. Use `red` on the before side for the problem and `green` on the after side for the fix, and leave what stayed the same unmarked. Two or three marks a side usually tell the whole story.\n\nDone when every mark has numbers you measured, and you can name the element or the pixel rectangle each came from.\n\n## 3. Write the document\n\nStart from [references/example.md](references/example.md) and keep its shape:\n\n1. One sentence: what changed, and for whom.\n2. One line of steps to reach the screen, so a reviewer can reproduce it.\n3. The `::compare`, each note a few words in the screen's own words. A value the screenshot doesn't show stays out of its note.\n4. A `::diff` of the line that caused it, only when the reader would otherwise ask.\n\nThe images carry the evidence, so the text only points into them. Put how the work went (checks, retries, CI) in your report to the user, not in the document. [references/blocks.md](references/blocks.md) has the syntax of each block.\n\nImage `src` must be a URL the renderer can reach: a path next to the document, or an https URL your host gives you for the upload.\n\n## 4. Check the render\n\n1. Lint it, and fix until it reports no problems:\n\n ```sh\n vp dlx --package comark-linter --package comark-ui comark-lint evidence.md\n ```\n\n These commands use the [global Vite+ CLI](https://viteplus.dev/guide/global-cli). Without it, `pnpm dlx` takes the same arguments.\n\n2. Open the page where it will be read and take a screenshot of it. To check the screenshots alone, copy the whole `::compare` block, nested screenshots and annotations included, into `evidence-screenshots.md` beside `evidence.md`.\n\n Make comark-annotate a direct dependency of this workspace package. A transitive one isn't linked, and `@latest` can run a different release from the project's renderers. Match the version of the installed Comark UI packages: `pnpm add -D comark-annotate@<version>`, with `-w` at the workspace root. With no Comark UI packages installed, omit `@<version>`. Then run:\n\n ```sh\n vp exec comark-annotate render evidence-screenshots.md -o evidence-screenshots.html\n ```\n\n Without Vite+, use `pnpm exec` with the same arguments. Open `evidence-screenshots.html` and take a screenshot of it. This CLI renders annotate, carousel, and zoom only; check any `::diff` or other blocks in the host's full Comark UI preview.\n3. Check that each mark sits on its target and each note matches what its side shows. Measure a mark that misses again from the source; a box moved by eye stays a guess.\n\nDone when you've looked at the rendered page and every mark lands. Name any mark or block you couldn't check in your report.\n"
15
+ "text": "---\nname: before-after-evidence\ndescription: Shows a visible change as two comparable screenshots, before and after, with marks measured from the source and checked in the rendered page, and a change in motion as a short recording. Use when a pull request, bug fix, or design change alters what people see or how it moves, or when someone asks for before/after screenshots, a recording, or visual evidence.\nlicense: MIT\n---\n\n# Before/after evidence\n\nA before/after is evidence when a reviewer can check every claim against the two images and nothing else. The reader can't repair a bad pair, so get three things right: two captures of the same view, marks on what changed, and notes in the words the screen shows.\n\nThis skill writes the document. Your host decides where it's published and who can open it.\n\n## When the change is motion\n\nAn animation, a transition, a loading sequence, or feedback while dragging happens between frames, so record it. A still taken partway through a motion looks like a broken layout, and two of them can't show that anything moves. Follow [references/recording.md](references/recording.md) in place of steps 1 and 2, then write and check the document with steps 3 and 4, with the clip where the `::compare` goes.\n\n- When the old behavior jumped straight to its end state, the after clip alone is the evidence, and the document's first sentence says what happened before.\n- When the change retimes motion that already existed, such as its easing, its duration, or a stutter, record both sides with the same script.\n\n## 1. Capture a comparable pair\n\nTake the after capture from the branch or its preview, and the before capture from a deployment or commit that still behaves the old way. Capture both with the same viewport, zoom, theme, data, scroll position, and open menus, so the change is the only difference.\n\n- Set up the state that shows the change before you capture: the long name that wrapped, the error that stayed hidden, the empty list.\n- Clip both captures to one region around the change, the same rectangle on each side. A `::compare` gives each image under half the page width, where a full-page capture's text is too small to read.\n- Pin anything that moves the layout between the two states, such as a vertically centered dialog that grows by one line, so the same element sits at the same place in both.\n\nIf you can't capture one side comparably, stop and say what blocked you. A description in place of a screenshot, or one side alone, isn't before/after evidence.\n\nDone when both images exist, have the same size, and differ only where the change is.\n\n## 2. Measure every mark\n\nMarks take `x`, `y`, `w`, and `h` as percentages of the image. Measure them. An estimate drifts a few percent, which on a half-width image lands on the neighbouring button.\n\n- **A page you captured:** read the element's box in the same browser, viewport, and state, minus the clip's offset. [references/measuring.md](references/measuring.md) has the snippet.\n- **A still you didn't capture**, such as a frame from a screen recording, a design export, or an image attached to a bug report: there is no DOM to ask. Measure in image pixels and divide by the image's natural width and height. Confirm each box by cropping the image to it and looking at the crop, as the same reference shows.\n\nMark the change at its own size: the field, the label, the button. Use `red` on the before side for the problem and `green` on the after side for the fix, and leave what stayed the same unmarked. Two or three marks a side usually tell the whole story.\n\nDone when every mark has numbers you measured, and you can name the element or the pixel rectangle each came from.\n\n## 3. Write the document\n\nStart from [references/example.md](references/example.md) and keep its shape:\n\n1. One sentence: what changed, and for whom.\n2. One line of steps to reach the screen, so a reviewer can reproduce it.\n3. The `::compare`, each note a few words in the screen's own words. A value the screenshot doesn't show stays out of its note.\n4. A `::diff` of the line that caused it, only when the reader would otherwise ask.\n\nThe images carry the evidence, so the text only points into them. Put how the work went (checks, retries, CI) in your report to the user, not in the document. [references/blocks.md](references/blocks.md) has the syntax of each block.\n\nImage `src` must be a URL the renderer can reach: a path next to the document, or an https URL your host gives you for the upload.\n\n## 4. Check the render\n\n1. Lint it, and fix until it reports no problems:\n\n ```sh\n vp dlx --package comark-linter --package comark-ui comark-lint evidence.md\n ```\n\n These commands use the [global Vite+ CLI](https://viteplus.dev/guide/global-cli). Without it, `pnpm dlx` takes the same arguments.\n\n2. Open the page where it will be read and take a screenshot of it. To check the screenshots alone, copy the whole `::compare` block, nested screenshots and annotations included, into `evidence-screenshots.md` beside `evidence.md`.\n\n Make comark-annotate a direct dependency of this workspace package. A transitive one isn't linked, and `@latest` can run a different release from the project's renderers. Match the version of the installed Comark UI packages: `pnpm add -D comark-annotate@<version>`, with `-w` at the workspace root. With no Comark UI packages installed, omit `@<version>`. Then run:\n\n ```sh\n vp exec comark-annotate render evidence-screenshots.md -o evidence-screenshots.html\n ```\n\n Without Vite+, use `pnpm exec` with the same arguments. Open `evidence-screenshots.html` and take a screenshot of it. This CLI renders annotate, carousel, and zoom only; check any `::diff` or other blocks in the host's full Comark UI preview.\n\n3. Check that each mark sits on its target and each note matches what its side shows. A clip should start by itself and loop in the page. Measure a mark that misses again from the source; a box moved by eye stays a guess.\n\nDone when you've looked at the rendered page and every mark lands. Name any mark or block you couldn't check in your report.\n"
16
16
  },
17
17
  {
18
18
  "path": "agents/openai.yaml",
19
- "text": "interface:\n display_name: \"Before/after evidence\"\n short_description: \"Show a visible change with comparable, measured screenshots\"\n"
19
+ "text": "interface:\n display_name: \"Before/after evidence\"\n short_description: \"Show a visible change with measured screenshots, or motion with a clip\"\n"
20
20
  },
21
21
  {
22
22
  "path": "references/blocks.md",
@@ -29,6 +29,10 @@ const skills = [
29
29
  {
30
30
  "path": "references/measuring.md",
31
31
  "text": "# Measuring marks\n\nEvery mark is `x y w h`, each a percentage of the image from 0 to 100. Measure in the same coordinates the image was captured in.\n\n## A page you captured\n\nCapture both states with one clip, then read each element's box in the same page, before anything moves:\n\n```js\n// Playwright: one clip for both sides, so the same element sits at the same place.\nconst clip = { x: 56, y: 12, width: 528, height: 312 };\nawait page.screenshot({ path: \"after.png\", clip });\n\nconst box = await page.$eval(\"#email\", (el, clip) => {\n const r = el.getBoundingClientRect();\n return [\n ((r.x - clip.x) / clip.width) * 100,\n ((r.y - clip.y) / clip.height) * 100,\n (r.width / clip.width) * 100,\n (r.height / clip.height) * 100,\n ].map((v) => +v.toFixed(1));\n}, clip);\n// box is [x, y, w, h] for the annotation.\n```\n\n- Without a clip, divide by `innerWidth` and `innerHeight` and subtract nothing.\n- For a full-page capture, add `scrollX` and `scrollY` to the box and divide by `document.documentElement.scrollWidth` and `scrollHeight`.\n- An element that spans its container, such as a block paragraph, measures as wide as the container. Measure the text instead: give it `width: fit-content` while you capture, or measure a `Range` around its text.\n- If an element crosses the clip's edge, cut its box at the edge, so every value stays between 0 and 100.\n\n## A still you didn't capture\n\nA frame from a screen recording, a design export, or an image attached to an issue has no DOM. Measure in pixels:\n\n1. Read the image's natural size, such as with `magick identify frame.png` or `file frame.png`.\n2. Find the target's rectangle in pixels in an image viewer that shows the pointer's coordinates.\n3. Divide: `x = left / width * 100`, `y = top / height * 100`, `w = boxWidth / width * 100`, `h = boxHeight / height * 100`.\n4. Crop the image to that rectangle and look at the crop. It should show the target and little else:\n\n ```sh\n magick frame.png -crop 320x38+44+134 +repage crop.png\n ```\n\n With Python and Pillow, `Image.open(\"frame.png\").crop((44, 134, 364, 172)).save(\"crop.png\")` does the same.\n\nFor a pair of video frames, take both at the same playback size and use the same crop for both, as you would clip two page captures.\n\n## In the render\n\nThe rendered page is the last check. A mark that misses there has the wrong numbers: measure it again from the source.\n"
32
+ },
33
+ {
34
+ "path": "references/recording.md",
35
+ "text": "# Recording motion\n\nA clip is evidence when the reviewer sees the motion at its real speed, starting and ending at rest, with nothing else on screen changing.\n\n## 1. Script the interaction\n\nDrive the page with a script, such as Playwright, so a retake repeats the same steps at the same pace.\n\n- Size the viewport to the component, the way you'd clip a still. The viewport is the frame.\n- Rest about 0.7 s before each action and longer than the animation after it, about 1.3 s, so every motion starts and ends on a still frame.\n- Show the motion two or three times, such as three tab switches, and keep the clip to a few seconds.\n- Draw a pointer. A headless browser draws none, and without one the reader sees things change with nothing pressing them. Move the mouse to its target in steps, not in one jump.\n\n## 2. Capture the frames\n\nChrome's screencast sends a lossless PNG each time the page paints. Playwright's `recordVideo` is simpler, but it starts recording during page load and its VP8 encoding blurs small text.\n\nBoth capture CSS pixels, whatever `deviceScaleFactor` says. For sharp text, lay the page out at double size: double the viewport, the root `font-size`, and any font sizes set in `px`. Keep CSS `zoom` and `transform` off the page. Motion code often measures boxes with `getBoundingClientRect`, and under either one it draws its moving parts in the wrong place. On a tab transition recorded with `zoom: 2`, the leaving tokens appeared below the code block.\n\nSave this as `record.mjs` in a workspace with `@playwright/test` and its Chromium browser installed, with ffmpeg on `PATH`. Adjust the viewport and tab names for the page, then run `node record.mjs <url>` with the page's URL.\n\n```js\nimport { chromium } from \"@playwright/test\";\nimport { execFileSync } from \"node:child_process\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\n\nconst url = process.argv[2];\nif (!url) throw new Error(\"Usage: node record.mjs <url>\");\n\nconst width = 1456,\n height = 340,\n dir = \"clip\"; // twice the component's CSS size\nmkdirSync(dir, { recursive: true });\nconst browser = await chromium.launch();\nconst page = await browser.newPage({ viewport: { width, height } });\nawait page.goto(url);\nawait page.addStyleTag({ content: \"html{font-size:32px}\" }); // plus any px fonts, doubled\n\n// An arrow whose tip follows the mouse, so it leaves the label it points at readable.\nconst arrow = encodeURIComponent(\n '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 14 20\"><path d=\"M1 1v16l4-4 3 6 2-1-3-6h6z\" fill=\"#111\" stroke=\"#fff\" stroke-width=\"1.2\" stroke-linejoin=\"round\"/></svg>',\n);\nawait page.addStyleTag({\n content: `#pointer{position:fixed;left:0;top:0;z-index:9;width:28px;height:40px;background:url(\"data:image/svg+xml,${arrow}\");pointer-events:none}`,\n});\nawait page.evaluate(() => {\n const tip = document.body.appendChild(\n Object.assign(document.createElement(\"div\"), { id: \"pointer\" }),\n );\n addEventListener(\"mousemove\", (e) => (tip.style.translate = `${e.clientX}px ${e.clientY}px`));\n});\n\nconst cdp = await page.context().newCDPSession(page);\nconst frames = [];\ncdp.on(\"Page.screencastFrame\", ({ data, metadata, sessionId }) => {\n frames.push({ data, at: metadata.timestamp });\n cdp.send(\"Page.screencastFrameAck\", { sessionId }).catch(() => {});\n});\nawait cdp.send(\"Page.startScreencast\", { format: \"png\", maxWidth: width, maxHeight: height });\nawait page.mouse.move(width - 80, height - 60); // paints the first frame\nawait page.waitForTimeout(700);\nfor (const name of [\"Second\", \"Third\", \"First\"]) {\n const box = await page.getByRole(\"tab\", { name }).boundingBox();\n await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 14 });\n await page.waitForTimeout(150); // lets the reader see where it presses\n await page.mouse.down();\n await page.mouse.up();\n await page.waitForTimeout(1300);\n}\nconst end = Date.now() / 1000;\nawait cdp.send(\"Page.stopScreencast\");\nawait browser.close();\n\n// A frame arrives only when the page paints, so each one holds until the next.\nlet list = \"\";\nframes.forEach((frame, i) => {\n writeFileSync(`${dir}/${i}.png`, Buffer.from(frame.data, \"base64\"));\n list += `file '${i}.png'\\nduration ${((frames[i + 1]?.at ?? end) - frame.at).toFixed(4)}\\n`;\n});\nwriteFileSync(`${dir}/frames.txt`, `${list}file '${frames.length - 1}.png'\\n`);\nexecFileSync(\"ffmpeg\", [\n \"-y\",\n \"-f\",\n \"concat\",\n \"-i\",\n `${dir}/frames.txt`,\n \"-vf\",\n \"fps=30,format=yuv420p\",\n \"-c:v\",\n \"libx264\",\n \"-crf\",\n \"18\",\n \"-movflags\",\n \"+faststart\",\n \"-an\",\n `${dir}/clip.mp4`,\n]);\n```\n\nThe mp4 is H.264, so every browser plays it. A few seconds at this size come to a few hundred KB.\n\n## 3. Look at the clip\n\nPull frames from the middle of a motion and from a rest, and look at them:\n\n```sh\nfor t in 0.5 1.1 1.25 1.4; do ffmpeg -y -loglevel error -ss $t -i clip/clip.mp4 -frames:v 1 clip/at-$t.png; done\n```\n\nA frame from the middle of a motion shows what reviewers can't see in a still: parts in between their two places, fading, or drawn where they don't belong.\n\nDone when a frame from the middle of each motion shows the motion your document describes, and the clip starts and ends at rest.\n\n## 4. Write the document\n\nKeep the shape of a before/after document, with the clip in place of the compare:\n\n```md\nCode groups with `magic` now slide the tokens their snippets share into place when you switch tabs. Before, the code swapped all at once.\n\nOpen a `::code-group{magic}` and select **tinyjson5**, then **tinyini**.\n\n![Each tab switch slides import and from into place while the package names fade between them](magic-tabs.mp4){width=1456 height=340 autoplay loop muted}\n```\n\n- The first sentence says what moved before, so the after clip alone carries the comparison.\n- The alt text says what moves, for a reader whose browser can't play the file.\n- `autoplay loop muted` plays the clip like an animated screenshot. Browsers autoplay only muted video.\n- `width` and `height` are the clip's pixels, so the page keeps its room before the file loads. `ffprobe -v error -show_entries stream=width,height -of csv=p=0 clip.mp4` prints them.\n- When the change retimes motion that already existed, put the before clip first, recorded with the same script, and title each with a line above it.\n"
32
36
  }
33
37
  ]
34
38
  },
@@ -234,7 +238,7 @@ const skills = [
234
238
  },
235
239
  {
236
240
  "path": "references/tabs.md",
237
- "text": "# Tabs and code groups\n\ncomark-tabs draws `::tabs` of any Markdown, `::code-group` of code blocks, and a tab per package manager for a code block marked `pm`. Use tabs for versions of one thing where each reader needs only one, such as an operating system, a framework, or a package manager.\n\n## Syntax\n\n````md\n::code-group{sync=\"pm\"}\n```sh [pnpm]\npnpm add comark-tabs\n```\n\n```sh [npm]\nnpm install comark-tabs\n```\n::\n\n::tabs{sync=\"os\"}\n:::tabs-item{label=\"macOS\"}\nPress **⌘K** to find the pigeon data.\n:::\n\n:::tabs-item{label=\"Windows\"}\nPress **Ctrl+K** to find the pigeon data.\n:::\n::\n````\n\n- `::tabs`: each `:::tabs-item` inside is one tab. Content outside every tab doesn't render, and is reported.\n- `:::tabs-item`: `label` is required and must differ from the other labels in its block, since `sync` matches tabs by label. `icon` draws an Iconify name, such as `lucide:apple`, or an image URL before the label.\n- `::code-group`: only code blocks go inside. Name each one's tab in brackets after its language, as in `sh [pnpm]`, with an optional icon after it, as in `sh [pnpm] icon=logos:pnpm`. The tab replaces the block's own title. `title=\"pnpm\"` names no tab, so a block without brackets is reported.\n- `sync`, on either block, is a key shared by blocks that switch together. Picking a tab picks the tab with the same label in every block with that key, and the next page that uses the key remembers it.\n- A code block marked `pm`, as in `sh pm`, holds package manager commands in any manager (npm, npx, pnpm, pnpx, yarn, bun, bunx, or deno) and becomes a code group with a tab each for npm, pnpm, yarn, and bun, synced as `pm`. The manager it's written in keeps its commands as written. It reads install, frozen install (`npm ci`), add with `-D`, `-E`, `-O`, `--save-peer`, or `-g`, remove, run, `npx`/`dlx`, create, and init. An update or a `pnpm exec` stays only in its own manager's tab, and the others are left out and reported. Only commands at the start of a line or after `&&`, `||`, or `;` change. Inside a `::code-group`, `pm` does nothing.\n\n## Choose the block\n\n- `::code-group` when every tab is one code block, such as one config file per framework. Give every install snippet `sync=\"pm\"`, the key `pm` blocks use, so the reader picks a package manager once.\n- `pm` for package manager commands, so you write them once in the manager you use. A command with no form in a manager, such as `npm install -g` in yarn, or one it doesn't read, such as `npm publish`, leaves those tabs out and is reported. Write that block as a `::code-group{sync=\"pm\"}` by hand.\n- `::tabs` when a tab holds prose, images, or several blocks.\n\nPut only alternatives in tabs. Anything every reader needs goes outside them, since a reader sees one tab at a time. For a before and after, use annotate's `::compare` or comark-diff's `::diff`, which show both sides at once.\n\n## Register\n\n- In `comark-ui`: on, as `tabs`. `tabs: { managers }` sets the tabs a `pm` block gets, in order. The default is `[\"npm\", \"pnpm\", \"yarn\", \"bun\"]`, and `\"deno\"` can join. `tabs: { icons }` gives each tab without an icon one by its label, such as `{ pnpm: \"logos:pnpm\" }`. Icons resolve through the preset's `icon` options, from the Iconify API unless its `collections` hold the set.\n- Alone: `pnpm add comark @comark/html comark-tabs`, then `createHtmlRenderer({ plugins: [tabs()], components })` with both from `comark-tabs/html`. `comark-tabs/vue`, `/react`, and `/svelte` (Svelte 5.20 or later) export the same pair for those renderers. It takes `managers`, `icons`, and `iconResolver`, a `createIcons()` from comark-icon to share with `:icon`. List it before Shiki, so Shiki highlights the tabs `pm` adds.\n"
241
+ "text": "# Tabs and code groups\n\ncomark-tabs draws `::tabs` of any Markdown, `::code-group` of code blocks, and a tab per package manager for a code block marked `pm`. Use tabs for versions of one thing where each reader needs only one, such as an operating system, a framework, or a package manager.\n\n## Syntax\n\n````md\n::code-group{sync=\"pm\"}\n```sh [pnpm]\npnpm add comark-tabs\n```\n\n```sh [npm]\nnpm install comark-tabs\n```\n::\n\n::tabs{sync=\"os\"}\n:::tabs-item{label=\"macOS\"}\nPress **⌘K** to find the pigeon data.\n:::\n\n:::tabs-item{label=\"Windows\"}\nPress **Ctrl+K** to find the pigeon data.\n:::\n::\n````\n\n- `::tabs`: each `:::tabs-item` inside is one tab. Content outside every tab doesn't render, and is reported.\n- `:::tabs-item`: `label` is required and must differ from the other labels in its block, since `sync` matches tabs by label. `icon` draws an Iconify name, such as `lucide:apple`, or an image URL before the label.\n- `::code-group`: only code blocks go inside. Name each one's tab in brackets after its language, as in `sh [pnpm]`, with an optional icon after it, as in `sh [pnpm] icon=logos:pnpm`. The tab replaces the block's own title. `title=\"pnpm\"` names no tab, so a block without brackets is reported.\n- A code group animates a switch between two snippets that share at least half their tokens, and swaps unlike ones at once. `magic` on `::code-group` forces the animation on, and `magic=\"false\"` turns it off. Leave it out unless the default is wrong for that group.\n- `sync`, on either block, is a key shared by blocks that switch together. Picking a tab picks the tab with the same label in every block with that key, and the next page that uses the key remembers it.\n- A code block marked `pm`, as in `sh pm`, holds package manager commands in any manager (npm, npx, pnpm, pnpx, yarn, bun, bunx, or deno) and becomes a code group with a tab each for npm, pnpm, yarn, and bun, synced as `pm`. The manager it's written in keeps its commands as written. It reads install, frozen install (`npm ci`), add with `-D`, `-E`, `-O`, `--save-peer`, or `-g`, remove, run, `npx`/`dlx`, create, and init. An update or a `pnpm exec` stays only in its own manager's tab, and the others are left out and reported. Only commands at the start of a line or after `&&`, `||`, or `;` change. Inside a `::code-group`, `pm` does nothing.\n\n## Choose the block\n\n- `::code-group` when every tab is one code block, such as one config file per framework. Give every install snippet `sync=\"pm\"`, the key `pm` blocks use, so the reader picks a package manager once.\n- `pm` for package manager commands, so you write them once in the manager you use. A command with no form in a manager, such as `npm install -g` in yarn, or one it doesn't read, such as `npm publish`, leaves those tabs out and is reported. Write that block as a `::code-group{sync=\"pm\"}` by hand.\n- `::tabs` when a tab holds prose, images, or several blocks.\n\nPut only alternatives in tabs. Anything every reader needs goes outside them, since a reader sees one tab at a time. For a before and after, use annotate's `::compare` or comark-diff's `::diff`, which show both sides at once.\n\n## Register\n\n- In `comark-ui`: on, as `tabs`. `tabs: { managers }` sets the tabs a `pm` block gets, in order. The default is `[\"npm\", \"pnpm\", \"yarn\", \"bun\"]`, and `\"deno\"` can join. `tabs: { icons }` gives each tab without an icon one by its label, such as `{ pnpm: \"logos:pnpm\" }`. Icons resolve through the preset's `icon` options, from the Iconify API unless its `collections` hold the set.\n- Alone: `pnpm add comark @comark/html comark-tabs`, then `createHtmlRenderer({ plugins: [tabs()], components })` with both from `comark-tabs/html`. `comark-tabs/vue`, `/react`, and `/svelte` (Svelte 5.20 or later) export the same pair for those renderers. It takes `managers`, `icons`, and `iconResolver`, a `createIcons()` from comark-icon to share with `:icon`. List it before Shiki, so Shiki highlights the tabs `pm` adds.\n"
238
242
  },
239
243
  {
240
244
  "path": "references/tests.md",
@@ -337,6 +341,6 @@ const skills = [
337
341
  }
338
342
  ];
339
343
  /** The comark-ui version the skills come from, for a host's cache key or a digest. */
340
- const version = "0.9.0";
344
+ const version = "0.9.1";
341
345
  //#endregion
342
346
  export { skills, version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comark-ui",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Every comark-ui block in one install: annotated screenshots, charts, diffs, tabs, zoom, and more, registered in the order they need with shared icons. Turn any of them off or pass its options.",
5
5
  "keywords": [
6
6
  "comark",
@@ -51,50 +51,50 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "comark-pele": "^1.1.0",
54
- "comark-carousel": "^0.9.0",
55
- "comark-cards": "^0.9.0",
56
- "comark-badge": "^0.9.0",
57
- "comark-cast": "^0.9.0",
58
- "comark-charts": "^0.9.0",
59
- "comark-annotate": "^0.9.0",
60
- "comark-chips": "^0.9.0",
61
- "comark-cjk": "^0.9.0",
62
- "comark-code-notes": "^0.9.0",
63
- "comark-code": "^0.9.0",
64
- "comark-deflist": "^0.9.0",
65
- "comark-critic": "^0.9.0",
66
- "comark-comments": "^0.9.0",
67
- "comark-colors": "^0.9.0",
68
- "comark-details": "^0.9.0",
69
- "comark-diff": "^0.9.0",
70
- "comark-diagrams": "^0.9.0",
71
- "comark-fields": "^0.9.0",
72
- "comark-figure": "^0.9.0",
73
- "comark-embed": "^0.9.0",
74
- "comark-file-tree": "^0.9.0",
75
- "comark-footnotes": "^0.9.0",
76
- "comark-github": "^0.9.0",
77
- "comark-icon": "^0.9.0",
78
- "comark-include": "^0.9.0",
79
- "comark-keys": "^0.9.0",
80
- "comark-links": "^0.9.0",
81
- "comark-magic-move": "^0.9.0",
82
- "comark-mathml": "^0.9.0",
83
- "comark-mark": "^0.9.0",
84
- "comark-metrics": "^0.9.0",
85
- "comark-media": "^0.9.0",
86
- "comark-qr": "^0.9.0",
87
- "comark-quiz": "^0.9.0",
88
- "comark-reading-time": "^0.9.0",
89
- "comark-outline": "^0.9.0",
90
- "comark-properties": "^0.9.0",
91
- "comark-steps": "^0.9.0",
92
- "comark-sources": "^0.9.0",
93
- "comark-tabs": "^0.9.0",
94
- "comark-table": "^0.9.0",
95
- "comark-xref": "^0.9.0",
96
- "comark-zoom": "^0.9.0",
97
- "comark-tests": "^0.9.0"
54
+ "comark-annotate": "^0.9.1",
55
+ "comark-cards": "^0.9.1",
56
+ "comark-carousel": "^0.9.1",
57
+ "comark-badge": "^0.9.1",
58
+ "comark-charts": "^0.9.1",
59
+ "comark-cast": "^0.9.1",
60
+ "comark-chips": "^0.9.1",
61
+ "comark-cjk": "^0.9.1",
62
+ "comark-code": "^0.9.1",
63
+ "comark-code-notes": "^0.9.1",
64
+ "comark-comments": "^0.9.1",
65
+ "comark-critic": "^0.9.1",
66
+ "comark-colors": "^0.9.1",
67
+ "comark-deflist": "^0.9.1",
68
+ "comark-diagrams": "^0.9.1",
69
+ "comark-embed": "^0.9.1",
70
+ "comark-diff": "^0.9.1",
71
+ "comark-fields": "^0.9.1",
72
+ "comark-details": "^0.9.1",
73
+ "comark-figure": "^0.9.1",
74
+ "comark-file-tree": "^0.9.1",
75
+ "comark-footnotes": "^0.9.1",
76
+ "comark-github": "^0.9.1",
77
+ "comark-include": "^0.9.1",
78
+ "comark-keys": "^0.9.1",
79
+ "comark-icon": "^0.9.1",
80
+ "comark-links": "^0.9.1",
81
+ "comark-magic-move": "^0.9.1",
82
+ "comark-mathml": "^0.9.1",
83
+ "comark-media": "^0.9.1",
84
+ "comark-mark": "^0.9.1",
85
+ "comark-outline": "^0.9.1",
86
+ "comark-qr": "^0.9.1",
87
+ "comark-metrics": "^0.9.1",
88
+ "comark-properties": "^0.9.1",
89
+ "comark-quiz": "^0.9.1",
90
+ "comark-sources": "^0.9.1",
91
+ "comark-table": "^0.9.1",
92
+ "comark-tabs": "^0.9.1",
93
+ "comark-reading-time": "^0.9.1",
94
+ "comark-tests": "^0.9.1",
95
+ "comark-steps": "^0.9.1",
96
+ "comark-xref": "^0.9.1",
97
+ "comark-zoom": "^0.9.1"
98
98
  },
99
99
  "devDependencies": {
100
100
  "@comark/html": "^0.8.1",