comark-ui 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/{client-kUEiRbtp.mjs → client-zWY8yhUx.mjs} +1 -14
- package/dist/client.mjs +1 -1
- package/dist/{editor-9IcfELjG.mjs → editor-CW05gHp7.mjs} +46 -6
- package/dist/{editor-yoVL7Ajq.d.mts → editor-Dk8TRweM.d.mts} +13 -2
- package/dist/editor-vue.d.mts +1 -1
- package/dist/editor-vue.mjs +1 -1
- package/dist/editor.d.mts +2 -2
- package/dist/editor.mjs +1 -1
- package/dist/html.d.mts +1 -1
- package/dist/html.mjs +4 -2
- package/dist/{index-Cq3zytGw.d.mts → index-CNYr62nX.d.mts} +4 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/react.d.mts +1 -1
- package/dist/react.mjs +4 -2
- package/dist/skills/before-after-evidence/SKILL.md +11 -3
- package/dist/skills/before-after-evidence/agents/openai.yaml +1 -1
- package/dist/skills/before-after-evidence/references/recording.md +130 -0
- package/dist/skills/comark-ui/SKILL.md +3 -0
- package/dist/skills/comark-ui/references/annotate.md +1 -1
- package/dist/skills/comark-ui/references/tabs.md +1 -0
- package/dist/skills/comark-ui/references/time.md +28 -0
- package/dist/skills.mjs +16 -8
- package/dist/{src-MvpWiv9v.mjs → src-C3nMECXA.mjs} +2 -0
- package/dist/styles-vZMqDkIY.mjs +15 -0
- package/dist/svelte/index.js +2 -0
- package/dist/vue.d.mts +610 -1
- package/dist/vue.mjs +42 -3
- package/package.json +52 -51
package/README.md
CHANGED
|
@@ -33,6 +33,14 @@ comarkUi({
|
|
|
33
33
|
|
|
34
34
|
It also returns `tags` and `blocks`. `tags` is every element a parsed document can hold besides the components; with the components' names, it's the allowlist for Comark's `security` plugin. `blocks` are the blocks the plugins replace while parsing, such as `::code-group`, which a linter needs beside the components' names.
|
|
35
35
|
|
|
36
|
+
If a server already parsed your document, import Vue's `components` directly. This keeps the chart parsers and the rest of the parse preset out of the renderer bundle:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { components } from "comark-ui/vue";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The map includes every Vue renderer and the typeset styles. Use `comarkUi(options).components` when you need the map to follow plugin options.
|
|
43
|
+
|
|
36
44
|
## Renderers
|
|
37
45
|
|
|
38
46
|
| Renderer | Import |
|
|
@@ -55,7 +63,7 @@ Vue, React, and Svelte leave out annotate and diff, which render only to HTML.
|
|
|
55
63
|
| `ComarkKit` | The schema: StarterKit, task lists, inline images (drawn as players for video and audio), tables, footnotes, definition lists, CriticMarkup, and Comark's components, slots, and comments, each keeping the attributes it has no field for. |
|
|
56
64
|
| `fromMarkdown`, `toMarkdown` | Convert between Markdown and the editor's JSON with Comark's own parser and writer. Front matter stays beside the document. |
|
|
57
65
|
| `checkMarkdown(markdown, { plugins })` | Whether the editor can save the document without changing what readers see, judged with the plugins the page renders with. A document it can't keep opens read-only. |
|
|
58
|
-
| `Upload` | Uploads pasted and dropped files. Each shows a placeholder with its progress until `upload(file, progress)` returns its URL. |
|
|
66
|
+
| `Upload` | Uploads pasted and dropped files, and the files the `chooseFiles` command picks. Each shows a placeholder with its progress until `upload(file, progress)` returns its URL. |
|
|
59
67
|
| `createEditorCodec({ plugins })` | `{ extensions, split, join, fromMarkdown, toMarkdown, verify }` for a collaborative room, such as ViteHub Realtime's `editor` option. A room keeps the body as the editor's document and the frontmatter as its fields, so a text edit never overwrites the metadata. |
|
|
60
68
|
|
|
61
69
|
`comark-ui/editor/vue` binds the editor to a Markdown string with `useComarkEditor(markdown)` or `<ComarkEditor v-model="markdown" />`. Run `pnpm playground` in this package to try it.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as addStyles } from "./styles-vZMqDkIY.mjs";
|
|
1
2
|
import { theme } from "comark-zoom/theme";
|
|
2
3
|
//#region src/script.ts
|
|
3
4
|
/**
|
|
@@ -164,20 +165,6 @@ function start() {
|
|
|
164
165
|
window.__comarkMermaid = state;
|
|
165
166
|
return state.stop;
|
|
166
167
|
}
|
|
167
|
-
//#endregion
|
|
168
|
-
//#region ../../shared/styles.ts
|
|
169
|
-
/**
|
|
170
|
-
* Adds a package's styles to the page once, unless the page already carries them as a `style` or a `link` with
|
|
171
|
-
* `data-comark-<name>`, which is how the HTML renderer inlines them and how an app that imports `style.css` marks them.
|
|
172
|
-
*/
|
|
173
|
-
function addStyles(name, css) {
|
|
174
|
-
const attribute = `data-comark-${name}`;
|
|
175
|
-
if (document.querySelector(`style[${attribute}],link[${attribute}]`)) return;
|
|
176
|
-
const style = document.createElement("style");
|
|
177
|
-
style.setAttribute(attribute, "");
|
|
178
|
-
style.textContent = css;
|
|
179
|
-
document.head.append(style);
|
|
180
|
-
}
|
|
181
168
|
/**
|
|
182
169
|
* comark-pele's diagrams on the shared `--comark-*` tokens, on a card like comark-diagrams'. Pele paints with its
|
|
183
170
|
* `--pele-*` variables, so setting them on the wrapper themes every diagram, light and dark, without drawing it
|
package/dist/client.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { n as typesetStylesheet, t as typesetCss } from "./typeset-css-BOtdXbMg.mjs";
|
|
2
|
-
import { i as startMermaid, n as mermaidBar, r as mermaidCss, t as addMermaidStyles } from "./client-
|
|
2
|
+
import { i as startMermaid, n as mermaidBar, r as mermaidCss, t as addMermaidStyles } from "./client-zWY8yhUx.mjs";
|
|
3
3
|
export { addMermaidStyles, mermaidBar, mermaidCss, startMermaid, typesetCss, typesetStylesheet };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { t as addStyles } from "./styles-vZMqDkIY.mjs";
|
|
1
2
|
import { n as scalar } from "./text-D8j9clg3.mjs";
|
|
2
3
|
import cjk from "comark-cjk";
|
|
3
4
|
import critic from "comark-critic";
|
|
4
5
|
import deflist from "comark-deflist";
|
|
5
6
|
import footnotes from "comark-footnotes";
|
|
6
7
|
import { defineComarkPlugin, parseMarkdown } from "comark";
|
|
8
|
+
import { theme } from "comark-zoom/theme";
|
|
7
9
|
import { Extension, Mark, Node } from "@tiptap/core";
|
|
8
10
|
import Image from "@tiptap/extension-image";
|
|
9
11
|
import { TaskItem, TaskList } from "@tiptap/extension-list";
|
|
@@ -654,6 +656,7 @@ const isTaskItem = (child) => isElement(child) && child[0] === "li" && classes(c
|
|
|
654
656
|
const isCheckbox = (child) => isElement(child) && child[0] === "input" && child[1].type === "checkbox";
|
|
655
657
|
function readList(tag, props, children, reading) {
|
|
656
658
|
const items = children.filter(isElement);
|
|
659
|
+
if (tag === "ol" && items.some(isTaskItem)) reading.lossy = true;
|
|
657
660
|
if (tag === "ul" && items.length && items.every(isTaskItem)) return node("taskList", withProps({}, rest(withoutClass(props, "contains-task-list"))), items.map((item) => readTaskItem(item, reading)));
|
|
658
661
|
const listItems = items.map((item) => {
|
|
659
662
|
const blocks = readBlocks(childrenOf(item), reading);
|
|
@@ -1167,7 +1170,18 @@ const acceptMedia = (file) => /^(?:image|video|audio)\//.test(file.type);
|
|
|
1167
1170
|
function findUpload(state, id) {
|
|
1168
1171
|
return uploadKey.getState(state)?.find(void 0, void 0, (spec) => spec.id === id)[0]?.from;
|
|
1169
1172
|
}
|
|
1173
|
+
/** The placeholder's styles, which the editor adds to the page with the first one. */
|
|
1174
|
+
const uploadCss = `@layer comark{${[
|
|
1175
|
+
theme(".cue-upload", "primary"),
|
|
1176
|
+
`.cue-upload{display:inline-grid;position:relative;min-width:12rem;max-width:15rem;overflow:hidden;vertical-align:middle;border:1px solid var(--_comark-border);border-radius:var(--_comark-radius);background:var(--_comark-accent);color:var(--_comark-foreground);font:500 .75rem/1.4 var(--_comark-font)}`,
|
|
1177
|
+
`.cue-upload>img,.cue-upload>video{grid-area:1/1;width:100%;max-height:10rem;object-fit:cover;opacity:.55}`,
|
|
1178
|
+
`.cue-upload-label{grid-area:1/1;align-self:end;padding:.375rem .5rem .5rem;font-variant-numeric:tabular-nums;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}`,
|
|
1179
|
+
`.cue-upload>img~.cue-upload-label,.cue-upload>video~.cue-upload-label{background:color-mix(in srgb,var(--_comark-background) 80%,transparent)}`,
|
|
1180
|
+
`.cue-upload::after{content:"";position:absolute;inset:auto 0 0;height:2px;width:var(--cue-upload);background:var(--_comark-primary);transition:width .2s ease-out}`,
|
|
1181
|
+
`@media (prefers-reduced-motion:reduce){.cue-upload::after{transition:none}}`
|
|
1182
|
+
].join("")}}`;
|
|
1170
1183
|
function placeholder(pending) {
|
|
1184
|
+
addStyles("editor-upload", uploadCss);
|
|
1171
1185
|
const root = document.createElement("span");
|
|
1172
1186
|
root.className = "cue-upload";
|
|
1173
1187
|
root.contentEditable = "false";
|
|
@@ -1247,6 +1261,28 @@ function send(view, file, pos, upload, options, batch) {
|
|
|
1247
1261
|
options.onError?.(error, file);
|
|
1248
1262
|
});
|
|
1249
1263
|
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Opens the file picker and uploads what's picked at the cursor, with the same placeholders as a paste. Returns false
|
|
1266
|
+
* without an `upload` callback. A "/" menu's Image item calls it through the `chooseFiles` command.
|
|
1267
|
+
*/
|
|
1268
|
+
function chooseFiles(view, options) {
|
|
1269
|
+
if (!options.upload) return false;
|
|
1270
|
+
const input = document.createElement("input");
|
|
1271
|
+
input.type = "file";
|
|
1272
|
+
input.multiple = true;
|
|
1273
|
+
input.accept = options.pick ?? "image/*,video/*,audio/*";
|
|
1274
|
+
input.style.cssText = "position:fixed;left:-9999px";
|
|
1275
|
+
input.tabIndex = -1;
|
|
1276
|
+
input.addEventListener("change", () => {
|
|
1277
|
+
const files = Array.from(input.files ?? []);
|
|
1278
|
+
input.remove();
|
|
1279
|
+
if (files.length) Promise.all(startUploads(view, files, view.state.selection.from, options));
|
|
1280
|
+
}, { once: true });
|
|
1281
|
+
input.addEventListener("cancel", () => input.remove(), { once: true });
|
|
1282
|
+
document.body.append(input);
|
|
1283
|
+
input.click();
|
|
1284
|
+
return true;
|
|
1285
|
+
}
|
|
1250
1286
|
function insertImage(tr, pos, image) {
|
|
1251
1287
|
if (tr.doc.resolve(pos).parent.inlineContent) tr.insert(pos, image);
|
|
1252
1288
|
else tr.insert(pos, tr.doc.type.schema.nodes.paragraph.create(null, image));
|
|
@@ -1321,17 +1357,21 @@ const Upload = Extension.create({
|
|
|
1321
1357
|
addOptions: () => ({
|
|
1322
1358
|
upload: void 0,
|
|
1323
1359
|
accept: acceptMedia,
|
|
1360
|
+
pick: void 0,
|
|
1324
1361
|
onError: void 0
|
|
1325
1362
|
}),
|
|
1326
1363
|
addProseMirrorPlugins() {
|
|
1327
1364
|
return [uploadPlugin(this.options)];
|
|
1328
1365
|
},
|
|
1329
1366
|
addCommands() {
|
|
1330
|
-
return {
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1367
|
+
return {
|
|
1368
|
+
uploadFiles: (files) => ({ view }) => {
|
|
1369
|
+
if (!this.options.upload) return false;
|
|
1370
|
+
Promise.all(startUploads(view, files, view.state.selection.from, this.options));
|
|
1371
|
+
return true;
|
|
1372
|
+
},
|
|
1373
|
+
chooseFiles: () => ({ view, dispatch }) => dispatch ? chooseFiles(view, this.options) : Boolean(this.options.upload)
|
|
1374
|
+
};
|
|
1335
1375
|
}
|
|
1336
1376
|
});
|
|
1337
1377
|
//#endregion
|
|
@@ -1378,4 +1418,4 @@ function createEditorCodec(options = {}) {
|
|
|
1378
1418
|
};
|
|
1379
1419
|
}
|
|
1380
1420
|
//#endregion
|
|
1381
|
-
export {
|
|
1421
|
+
export { DefinitionList as C, InlineFootnote as D, FootnoteReference as E, DefinitionDescription as S, FootnoteDefinition as T, ComarkSlot as _, startUploads as a, CriticHighlight as b, uploadPlugin as c, toMarkdown as d, ComarkBlock as f, ComarkKit as g, ComarkInline as h, findUpload as i, checkMarkdown as l, ComarkImage as m, Upload as n, uploadCss as o, ComarkComment as p, chooseFiles as r, uploadKey as s, createEditorCodec as t, fromMarkdown as u, CriticComment as v, DefinitionTerm as w, CriticInsertion as x, CriticDeletion as y };
|
|
@@ -88,8 +88,10 @@ interface UploadOptions {
|
|
|
88
88
|
* bytes go; `fetch` can't report them, so use XMLHttpRequest's `upload.onprogress`. Without it, nothing uploads.
|
|
89
89
|
*/
|
|
90
90
|
upload?: (file: File, progress: (fraction: number) => void) => Promise<string>;
|
|
91
|
-
/** Which files to take from a paste or
|
|
91
|
+
/** Which files to take from a paste, a drop, or the file picker. Images, video, and audio by default. */
|
|
92
92
|
accept: (file: File) => boolean;
|
|
93
|
+
/** The file types `chooseFiles()` offers in the file picker, as an `<input accept>` value. Media by default. */
|
|
94
|
+
pick?: string;
|
|
93
95
|
/** Called when `upload` rejects. The placeholder is gone by then. */
|
|
94
96
|
onError?: (error: unknown, file: File) => void;
|
|
95
97
|
}
|
|
@@ -110,12 +112,19 @@ interface View {
|
|
|
110
112
|
declare const uploadKey: PluginKey<DecorationSet>;
|
|
111
113
|
/** Where placeholder `id` sits now, after every edit since it was added. */
|
|
112
114
|
declare function findUpload(state: EditorState, id: number): number | undefined;
|
|
115
|
+
/** The placeholder's styles, which the editor adds to the page with the first one. */
|
|
116
|
+
declare const uploadCss: string;
|
|
113
117
|
/**
|
|
114
118
|
* Uploads `files` into the document at `pos`. Each shows a placeholder there until its URL comes back, then becomes an
|
|
115
119
|
* image node, which comark-media plays when its URL ends in a video or audio extension. The placeholder is a
|
|
116
120
|
* decoration, so a collaborative room never sees a file that hasn't finished.
|
|
117
121
|
*/
|
|
118
122
|
declare function startUploads(view: View, files: File[], pos: number, options: UploadOptions): Promise<string | undefined>[];
|
|
123
|
+
/**
|
|
124
|
+
* Opens the file picker and uploads what's picked at the cursor, with the same placeholders as a paste. Returns false
|
|
125
|
+
* without an `upload` callback. A "/" menu's Image item calls it through the `chooseFiles` command.
|
|
126
|
+
*/
|
|
127
|
+
declare function chooseFiles(view: View, options: UploadOptions): boolean;
|
|
119
128
|
/** The plugin alone, for a ProseMirror editor without Tiptap. */
|
|
120
129
|
declare function uploadPlugin(options: UploadOptions): Plugin<DecorationSet>;
|
|
121
130
|
declare module "@tiptap/core" {
|
|
@@ -123,6 +132,8 @@ declare module "@tiptap/core" {
|
|
|
123
132
|
comarkUpload: {
|
|
124
133
|
/** Uploads files at the cursor, as a paste would, such as the ones a file picker returns. */
|
|
125
134
|
uploadFiles: (files: File[]) => ReturnType;
|
|
135
|
+
/** Opens the file picker and uploads the files picked at the cursor, as a "/" menu's Image item would. */
|
|
136
|
+
chooseFiles: () => ReturnType;
|
|
126
137
|
};
|
|
127
138
|
}
|
|
128
139
|
}
|
|
@@ -170,4 +181,4 @@ interface EditorCodecOptions {
|
|
|
170
181
|
}
|
|
171
182
|
declare function createEditorCodec(options?: EditorCodecOptions): EditorCodec;
|
|
172
183
|
//#endregion
|
|
173
|
-
export {
|
|
184
|
+
export { DefinitionDescription as A, ComarkKit as C, CriticDeletion as D, CriticComment as E, InlineFootnote as F, DefinitionTerm as M, FootnoteDefinition as N, CriticHighlight as O, FootnoteReference as P, ComarkInline as S, ComarkSlot as T, fromMarkdown as _, Upload as a, ComarkComment as b, findUpload as c, uploadKey as d, uploadPlugin as f, checkMarkdown as g, SplitMarkdown as h, PendingUpload as i, DefinitionList as j, CriticInsertion as k, startUploads as l, MarkdownCheck as m, EditorCodecOptions as n, UploadOptions as o, EditorDocument as p, createEditorCodec as r, chooseFiles as s, EditorCodec as t, uploadCss as u, toMarkdown as v, ComarkKitOptions as w, ComarkImage as x, ComarkBlock as y };
|
package/dist/editor-vue.d.mts
CHANGED
package/dist/editor-vue.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as isElement, t as childrenOf } from "./nodes-CwDBFXnO.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { d as toMarkdown, g as ComarkKit, l as checkMarkdown, u as fromMarkdown } from "./editor-CW05gHp7.mjs";
|
|
3
3
|
import { Editor, EditorContent } from "@tiptap/vue-3";
|
|
4
4
|
import { rowsOf } from "comark-properties";
|
|
5
5
|
import { Properties } from "comark-properties/vue";
|
package/dist/editor.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
export { ComarkBlock, ComarkComment, ComarkImage, ComarkInline, ComarkKit, type ComarkKitOptions, ComarkSlot, CriticComment, CriticDeletion, CriticHighlight, CriticInsertion, DefinitionDescription, DefinitionList, DefinitionTerm, type EditorCodec, type EditorCodecOptions, type EditorDocument, FootnoteDefinition, FootnoteReference, InlineFootnote, type MarkdownCheck, type PendingUpload, type SplitMarkdown, Upload, type UploadOptions, checkMarkdown, createEditorCodec, findUpload, fromMarkdown, startUploads, toMarkdown, uploadKey, uploadPlugin };
|
|
1
|
+
import { A as DefinitionDescription, C as ComarkKit, D as CriticDeletion, E as CriticComment, F as InlineFootnote, M as DefinitionTerm, N as FootnoteDefinition, O as CriticHighlight, P as FootnoteReference, S as ComarkInline, T as ComarkSlot, _ as fromMarkdown, a as Upload, b as ComarkComment, c as findUpload, d as uploadKey, f as uploadPlugin, g as checkMarkdown, h as SplitMarkdown, i as PendingUpload, j as DefinitionList, k as CriticInsertion, l as startUploads, m as MarkdownCheck, n as EditorCodecOptions, o as UploadOptions, p as EditorDocument, r as createEditorCodec, s as chooseFiles, t as EditorCodec, u as uploadCss, v as toMarkdown, w as ComarkKitOptions, x as ComarkImage, y as ComarkBlock } from "./editor-Dk8TRweM.mjs";
|
|
2
|
+
export { ComarkBlock, ComarkComment, ComarkImage, ComarkInline, ComarkKit, type ComarkKitOptions, ComarkSlot, CriticComment, CriticDeletion, CriticHighlight, CriticInsertion, DefinitionDescription, DefinitionList, DefinitionTerm, type EditorCodec, type EditorCodecOptions, type EditorDocument, FootnoteDefinition, FootnoteReference, InlineFootnote, type MarkdownCheck, type PendingUpload, type SplitMarkdown, Upload, type UploadOptions, checkMarkdown, chooseFiles, createEditorCodec, findUpload, fromMarkdown, startUploads, toMarkdown, uploadCss, uploadKey, uploadPlugin };
|
package/dist/editor.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { f as ComarkBlock, p as ComarkComment, m as ComarkImage, h as ComarkInline, g as ComarkKit, _ as ComarkSlot, v as CriticComment, y as CriticDeletion, b as CriticHighlight, x as CriticInsertion, S as DefinitionDescription, C as DefinitionList, w as DefinitionTerm, T as FootnoteDefinition, E as FootnoteReference, D as InlineFootnote, n as Upload, l as checkMarkdown, r as chooseFiles, t as createEditorCodec, i as findUpload, u as fromMarkdown, a as startUploads, d as toMarkdown, o as uploadCss, s as uploadKey, c as uploadPlugin } from "./editor-CW05gHp7.mjs";
|
package/dist/html.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as commentsCss, n as Preset, o as criticCss, r as PresetOptions } from "./index-
|
|
1
|
+
import { a as commentsCss, n as Preset, o as criticCss, r as PresetOptions } from "./index-CNYr62nX.mjs";
|
|
2
2
|
import { NodeHandler } from "comark";
|
|
3
3
|
//#region src/html.d.ts
|
|
4
4
|
/**
|
package/dist/html.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as criticCss, r as preset, t as commentsCss } from "./src-
|
|
1
|
+
import { n as criticCss, r as preset, t as commentsCss } from "./src-C3nMECXA.mjs";
|
|
2
2
|
import { t as typesetCss } from "./typeset-css-BOtdXbMg.mjs";
|
|
3
|
-
import { a as start, n as mermaidBar, r as mermaidCss } from "./client-
|
|
3
|
+
import { a as start, n as mermaidBar, r as mermaidCss } from "./client-zWY8yhUx.mjs";
|
|
4
4
|
import { t as rawText } from "./text-D8j9clg3.mjs";
|
|
5
5
|
import { escapeHtml } from "comark/utils";
|
|
6
6
|
import * as annotate from "comark-annotate/html";
|
|
@@ -38,6 +38,7 @@ import * as steps from "comark-steps/html";
|
|
|
38
38
|
import * as table from "comark-table/html";
|
|
39
39
|
import * as tabs from "comark-tabs/html";
|
|
40
40
|
import * as tests from "comark-tests/html";
|
|
41
|
+
import * as time from "comark-time/html";
|
|
41
42
|
import * as zoom from "comark-zoom/html";
|
|
42
43
|
//#region ../../shared/html.ts
|
|
43
44
|
/** A value as the inside of a double-quoted HTML attribute. */
|
|
@@ -107,6 +108,7 @@ var html_default = (options) => preset({
|
|
|
107
108
|
table,
|
|
108
109
|
tabs,
|
|
109
110
|
tests,
|
|
111
|
+
time,
|
|
110
112
|
typeset,
|
|
111
113
|
zoom
|
|
112
114
|
}, options);
|
|
@@ -28,6 +28,7 @@ import { QuizOptions } from "comark-quiz";
|
|
|
28
28
|
import { TableOptions } from "comark-table";
|
|
29
29
|
import { TabsOptions } from "comark-tabs";
|
|
30
30
|
import { TestsOptions } from "comark-tests";
|
|
31
|
+
import { TimeOptions } from "comark-time";
|
|
31
32
|
import { ZoomOptions } from "comark-zoom";
|
|
32
33
|
//#region src/mermaid-limits.d.ts
|
|
33
34
|
type MermaidOptions = PeleConfig & {
|
|
@@ -154,6 +155,8 @@ type PresetOptions = {
|
|
|
154
155
|
xref?: XrefOptions | boolean;
|
|
155
156
|
/** Reads no JUnit reports unless you pass `{ root }` or `{ read }`; inline reports render either way. */
|
|
156
157
|
tests?: TestsOptions | boolean;
|
|
158
|
+
/** `:time[2026-10-12]` as relative time: "3 days ago", a date past `threshold`, and the full date in a tooltip. */
|
|
159
|
+
time?: TimeOptions | boolean;
|
|
157
160
|
zoom?: ZoomOptions | boolean;
|
|
158
161
|
/**
|
|
159
162
|
* Highlights fenced code with your own Shiki, before comark-code. Off unless you pass one, so the preset bundles no
|
|
@@ -220,6 +223,7 @@ type Renderer<C> = {
|
|
|
220
223
|
tabs: Part<C, TabsOptions>;
|
|
221
224
|
outline: Part<C, OutlineOptions>;
|
|
222
225
|
tests: Part<C, TestsOptions>;
|
|
226
|
+
time: Part<C, TimeOptions>;
|
|
223
227
|
zoom: Part<C, ZoomOptions>;
|
|
224
228
|
code: Part<C, CodeOptions>;
|
|
225
229
|
codeNotes: Part<C>;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { n as typesetStylesheet, t as typesetCss } from "./typeset-css-DtOLAa1Z.mjs";
|
|
2
|
-
import { a as commentsCss, c as TYPESET_TAG, d as ShikiOptions, f as MermaidOptions, i as Renderer, l as typeset, n as Preset, o as criticCss, r as PresetOptions, s as preset, t as AnyPlugin, u as ShikiHighlighter } from "./index-
|
|
2
|
+
import { a as commentsCss, c as TYPESET_TAG, d as ShikiOptions, f as MermaidOptions, i as Renderer, l as typeset, n as Preset, o as criticCss, r as PresetOptions, s as preset, t as AnyPlugin, u as ShikiHighlighter } from "./index-CNYr62nX.mjs";
|
|
3
3
|
export { AnyPlugin, type MermaidOptions, Preset, PresetOptions, Renderer, type ShikiHighlighter, type ShikiOptions, TYPESET_TAG, commentsCss, criticCss, preset, typeset, typesetCss, typesetStylesheet };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as typeset, i as TYPESET_TAG, n as criticCss, r as preset, t as commentsCss } from "./src-
|
|
1
|
+
import { a as typeset, i as TYPESET_TAG, n as criticCss, r as preset, t as commentsCss } from "./src-C3nMECXA.mjs";
|
|
2
2
|
import { n as typesetStylesheet, t as typesetCss } from "./typeset-css-BOtdXbMg.mjs";
|
|
3
3
|
export { TYPESET_TAG, commentsCss, criticCss, preset, typeset, typesetCss, typesetStylesheet };
|
package/dist/react.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as commentsCss, n as Preset, o as criticCss, r as PresetOptions } from "./index-
|
|
1
|
+
import { a as commentsCss, n as Preset, o as criticCss, r as PresetOptions } from "./index-CNYr62nX.mjs";
|
|
2
2
|
import { ComponentType } from "react";
|
|
3
3
|
//#region src/react.d.ts
|
|
4
4
|
type AnyComponent = ComponentType<any>;
|
package/dist/react.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as criticCss, r as preset, t as commentsCss } from "./src-
|
|
1
|
+
import { n as criticCss, r as preset, t as commentsCss } from "./src-C3nMECXA.mjs";
|
|
2
2
|
import { t as typesetCss } from "./typeset-css-BOtdXbMg.mjs";
|
|
3
|
-
import { i as startMermaid, n as mermaidBar, t as addMermaidStyles } from "./client-
|
|
3
|
+
import { i as startMermaid, n as mermaidBar, t as addMermaidStyles } from "./client-zWY8yhUx.mjs";
|
|
4
4
|
import { createElement, useEffect } from "react";
|
|
5
5
|
import * as badge from "comark-badge/react";
|
|
6
6
|
import * as cards from "comark-cards/react";
|
|
@@ -33,6 +33,7 @@ import * as steps from "comark-steps/react";
|
|
|
33
33
|
import * as table from "comark-table/react";
|
|
34
34
|
import * as tabs from "comark-tabs/react";
|
|
35
35
|
import * as tests from "comark-tests/react";
|
|
36
|
+
import * as time from "comark-time/react";
|
|
36
37
|
import * as zoom from "comark-zoom/react";
|
|
37
38
|
//#region src/react.ts
|
|
38
39
|
const Pele = createPeleComponent({
|
|
@@ -90,6 +91,7 @@ var react_default = (options) => preset({
|
|
|
90
91
|
table,
|
|
91
92
|
tabs,
|
|
92
93
|
tests,
|
|
94
|
+
time,
|
|
93
95
|
typeset,
|
|
94
96
|
zoom
|
|
95
97
|
}, options);
|
|
@@ -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,9 +10,16 @@ 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
|
-
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.
|
|
22
|
+
Take the after capture from the branch or its preview, and the before capture from a deployment, or from a separate worktree at the base commit, that still behaves the old way. Do not use `git stash` to switch between them: worktrees of one repository share the stash, so another session can pop your changes. Capture both with the same viewport, zoom, theme, data, scroll position, and open menus, so the change is the only difference.
|
|
16
23
|
|
|
17
24
|
- Set up the state that shows the change before you capture: the long name that wrapped, the error that stayed hidden, the empty list.
|
|
18
25
|
- 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.
|
|
@@ -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
|
-
|
|
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.
|
|
@@ -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
|
+
{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.
|
|
@@ -17,6 +17,8 @@ pnpm add comark @comark/html comark-ui
|
|
|
17
17
|
|
|
18
18
|
Run the comark CLIs with the [global Vite+ CLI](https://viteplus.dev/guide/global-cli): `vp dlx` for a package the project doesn't install, `vp exec` for one it does. Without Vite+, `pnpm dlx` and `pnpm exec` take the same arguments.
|
|
19
19
|
|
|
20
|
+
If pnpm reports `ERR_PNPM_TRUST_DOWNGRADE`, read [pnpm trust errors](https://comark-ui.onmax.me/getting-started/#pnpm-trust-errors) for the publishing history and exact-version exceptions. Apply an exception only when the user's instructions authorize the policy change.
|
|
21
|
+
|
|
20
22
|
```ts
|
|
21
23
|
import { createHtmlRenderer } from "@comark/html";
|
|
22
24
|
import comarkUi from "comark-ui/html";
|
|
@@ -77,6 +79,7 @@ Read the reference for each block you use before you write it. Mix the blocks fr
|
|
|
77
79
|
| A test run's passed, failed, and skipped counts and its failures, from a JUnit report | `::tests` | [tests](references/tests.md) |
|
|
78
80
|
| A note, tip, or warning box, or long output folded away, as on GitHub | `> [!NOTE]`, `<details>` | [github](references/github.md) |
|
|
79
81
|
| The review's status, pull request, author, date, and tags in a box | frontmatter, `::properties` | [properties](references/properties.md) |
|
|
82
|
+
| When something happened or is due, as "2 days ago" or a date, with the full date on hover | `:time[2026-11-02]` | [time](references/time.md) |
|
|
80
83
|
| Claims backed by links, with numbered citations | `:cite`, `::sources` | [sources](references/sources.md) |
|
|
81
84
|
| An icon in a sentence or a table cell, such as a logo or a status | `:icon` | [icon](references/icon.md) |
|
|
82
85
|
| A preview link a reviewer can scan with a phone | `:qr` | [qr](references/qr.md) |
|
|
@@ -35,7 +35,7 @@ Moon phase and vibes don't fit, so the toolbar wraps and pushes the table down.
|
|
|
35
35
|
|
|
36
36
|
- `::compare`: `title` is optional and names the comparison for screen readers. `mode` is `columns` (default, side by side) or `slider`. `ink` colors every annotation in both screenshots that doesn't set its own `color`. Its two screenshots are titled Before and After unless they set their own.
|
|
37
37
|
- `::screenshot`: `src` and `alt` are required; `title` (the heading above the image), `href` (the captured page, linked by its host), `viewport` (like `1440x900`), and `ink` are optional. Other content becomes the caption.
|
|
38
|
-
- `::annotation`: `x y w h` are required percentages of the image, numbers from 0 to 100 without `%`. `mark` is `box` (default), `circle`, `underline`, `line` (straight through the middle, along the longer side), `highlight`, `bracket
|
|
38
|
+
- `::annotation`: `x y w h` are required percentages of the image, numbers from 0 to 100 without `%`. `mark` is `box` (default), `circle`, `underline`, `line` (straight through the middle, along the longer side), `highlight`, `bracket` (down the side of a box at least twice as tall as it is wide, and across a wide one, whatever its `side`), `cross`, `check`, or `none`, which needs a `side`. `color` is `violet` (default) to point something out, `red` for a problem, `amber` for a warning, `green` for a fix, `blue` for a second voice, `gray` for neutral, or a 3- or 6-digit hex color. `side` is `top`, `bottom`, `left`, or `right`; keep top or bottom notes at least a quarter of the image apart and left or right notes an eighth apart.
|
|
39
39
|
- `at` moves a side note along its margin, as a percentage of the image (across for top and bottom, down for left and right). A note set off from its point reads away from it, so put it on the side with room: a top note at `at=40` pointing left of 40 reads to the right. `to="x,y"` is the point the arrow lands on, measured like the box; without it the arrow meets the middle of the mark's facing edge. Both need a `side`.
|
|
40
40
|
- Annotations must be direct children of a screenshot.
|
|
41
41
|
|
|
@@ -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
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Dates as relative time
|
|
2
|
+
|
|
3
|
+
comark-time draws `:time[2026-11-02]` as the words a reader would say, such as "2 days ago" or "in 3 weeks", and past a threshold as a date, such as "Sep 30". Hover or focus opens a tooltip with the full date and time. Use it where a document says when something happened or will: when a pull request opened, when a benchmark ran, when a deadline falls. Write the date in ISO form and leave the words to the plugin.
|
|
4
|
+
|
|
5
|
+
## Syntax
|
|
6
|
+
|
|
7
|
+
```md
|
|
8
|
+
Opened :time[2026-11-02], last run :time[2026-11-04T11:59:56Z], due :time[2026-11-25].
|
|
9
|
+
|
|
10
|
+
The first sighting was :time[2026-09-30]{threshold="8w"}.
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- The date goes in the brackets: `2026-11-02`, or with a time of day, `2026-11-02T09:30:00Z`. A date alone counts whole days, so it reads "today", "yesterday", "2 days ago", or "in 3 days", never hours. A time with no offset is in the `timeZone` option's zone.
|
|
14
|
+
- `threshold` sets how far from now it stops reading as relative: a number and `s`, `m`, `h`, `d`, `w`, `mo`, or `y`, such as `3w`. The default is `4w`. `none` keeps it relative at any distance.
|
|
15
|
+
- Past the threshold the text is "Sep 30", or "Sep 30, 2025" in another year, and the tooltip shows how far away it is. Before it, the tooltip shows the full date, with the time and zone when the date has one.
|
|
16
|
+
- No script runs. The words are worked out while parsing and are fixed in rendered HTML, so "2 days ago" stays that until the document is parsed again.
|
|
17
|
+
|
|
18
|
+
## Write the dates
|
|
19
|
+
|
|
20
|
+
- Write the exact date from the source: the pull request's `createdAt`, the report's timestamp. Never write "yesterday" or "last week" in prose for a date a reader may look up.
|
|
21
|
+
- A zone-less date and time is read in the host's zone, which the reader can't see. Give a time an offset, such as `Z`, unless it is the author's local time.
|
|
22
|
+
- A date the plugin can't read, such as `next friday` or `2026-02-31`, is shown as written and reported. Fix it to ISO.
|
|
23
|
+
- Leave the threshold alone unless a date is better read as one or the other: `threshold="none"` for a date that must always read relative, a short one such as `1w` for a date that is better absolute within a month.
|
|
24
|
+
|
|
25
|
+
## Register
|
|
26
|
+
|
|
27
|
+
- In `comark-ui`: on. `comarkUi({ time: { now, threshold, locale, timeZone } })` passes its options; `now` pins the moment every date is measured from, which a host that caches rendered HTML should do. `time: false` turns it off.
|
|
28
|
+
- Alone: `pnpm add comark @comark/html comark-time`, then `createHtmlRenderer({ plugins: [time()], components })` with both from `comark-time/html`. `comark-time/vue`, `/react`, and `/svelte` render it too. It has no script.
|