comark-time 0.0.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/LICENSE ADDED
@@ -0,0 +1,69 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 onmax
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ The icons are Lucide's (https://lucide.dev), under its license:
26
+
27
+ ISC License
28
+
29
+ Copyright (c) 2026 Lucide Icons and Contributors
30
+
31
+ Permission to use, copy, modify, and/or distribute this software for any
32
+ purpose with or without fee is hereby granted, provided that the above
33
+ copyright notice and this permission notice appear in all copies.
34
+
35
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
36
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
37
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
38
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
39
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
40
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
41
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
42
+
43
+ ---
44
+
45
+ The following Lucide icons are derived from the Feather project:
46
+
47
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
48
+
49
+ The MIT License (MIT) (for the icons listed above)
50
+
51
+ Copyright (c) 2013-present Cole Bemis
52
+
53
+ Permission is hereby granted, free of charge, to any person obtaining a copy
54
+ of this software and associated documentation files (the "Software"), to deal
55
+ in the Software without restriction, including without limitation the rights
56
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
57
+ copies of the Software, and to permit persons to whom the Software is
58
+ furnished to do so, subject to the following conditions:
59
+
60
+ The above copyright notice and this permission notice shall be included in all
61
+ copies or substantial portions of the Software.
62
+
63
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
64
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
65
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
66
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
67
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
68
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
69
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # comark-time
2
+
3
+ [![npm version](https://comark-ui.onmax.me/badge.svg?source=npm-version&package=comark-time&icon=npm)](https://npmx.dev/package/comark-time)
4
+ [![npm downloads](https://comark-ui.onmax.me/badge.svg?source=npm-downloads&package=comark-time&period=month&icon=npm)](https://npmx.dev/package/comark-time)
5
+ [![license](https://comark-ui.onmax.me/badge.svg?label=license&value=MIT)](https://npmx.dev/package/comark-time)
6
+ [![docs](https://comark-ui.onmax.me/badge.svg?label=docs&value=comark-ui.onmax.me)](https://comark-ui.onmax.me/time/)
7
+
8
+ Dates as relative time: "today", "2 days ago", "in 3 weeks". Past a threshold, 4 weeks by default, the text becomes a date, and hover or focus opens a tooltip with the full date and time. A [Comark](https://comark.dev) plugin for HTML, Vue, React, and Svelte.
9
+
10
+ ```sh
11
+ pnpm add comark comark-time
12
+ ```
13
+
14
+ ```ts
15
+ import { createHtmlRenderer } from "@comark/html";
16
+ import time, { components } from "comark-time/html";
17
+
18
+ const render = createHtmlRenderer({ plugins: [time()], components });
19
+ ```
20
+
21
+ ```md
22
+ Opened :time[2026-11-02], last touched :time[2026-11-04T11:59:56Z], due :time[2026-11-25].
23
+
24
+ The first sighting was :time[2026-09-30]{threshold="8w"}.
25
+ ```
26
+
27
+ `:time[2026-11-02]` reads "2 days ago" and opens "Mon, Nov 2, 2026" in a tooltip. A date alone counts whole days from today; with a time of day, it counts down to the second. The plugin works out the words, the date, and the tooltip while parsing, so it needs no script, and every `:time` in a document is measured from one `now`. Pass `now` to pin it, `locale` and `timeZone` to set the words and the day, and `threshold` to move the switch to a date or write `threshold="none"` to keep a date relative.
28
+
29
+ > [!NOTE]
30
+ > Rendered HTML keeps the words it was parsed with. Re-render cached pages, or render in the reader's browser with Vue, React, or Svelte.
31
+
32
+ The plugin adds every problem to `meta.issues`, so a host can reject a document before publishing it: a `:time` with no date, a date that isn't ISO, a `threshold` that isn't a number and a unit, and a prop it doesn't read. Each issue names its block and says how to fix it.
33
+
34
+ ## Renderers
35
+
36
+ | Renderer | Import |
37
+ | --- | --- |
38
+ | `@comark/html` | `comark-time/html`: the styles are inlined |
39
+ | `@comark/vue` | `comark-time/vue` |
40
+ | `@comark/react` | `comark-time/react` |
41
+ | `@comark/svelte` | `comark-time/svelte` |
42
+
43
+ Each entry exports the parse plugin as `default` and the renderer's `components`. The Vue, React, and Svelte components add the styles when they mount; import `comark-time/style.css` with your styles so a server-rendered page doesn't shift. The styles read the shared `--comark-*` tokens and sit in a `comark` CSS layer, so any plain rule of your own wins.
@@ -0,0 +1,20 @@
1
+ import { t as css } from "./styles-DQyyDP4r.mjs";
2
+ //#region ../../shared/styles.ts
3
+ /**
4
+ * Adds a package's styles to the page once, unless the page already carries them as a `style` or a `link` with
5
+ * `data-comark-<name>`, which is how the HTML renderer inlines them and how an app that imports `style.css` marks them.
6
+ */
7
+ function addStyles$1(name, css) {
8
+ const attribute = `data-comark-${name}`;
9
+ if (document.querySelector(`style[${attribute}],link[${attribute}]`)) return;
10
+ const style = document.createElement("style");
11
+ style.setAttribute(attribute, "");
12
+ style.textContent = css;
13
+ document.head.append(style);
14
+ }
15
+ //#endregion
16
+ //#region src/client.ts
17
+ /** Adds the time styles to the page once. The Vue, React, and Svelte components call it when they mount. */
18
+ const addStyles = () => addStyles$1("time", css);
19
+ //#endregion
20
+ export { addStyles as t };
@@ -0,0 +1,14 @@
1
+ //#region src/styles.d.ts
2
+ /**
3
+ * A time as plain text that tints on hover. Its tooltip is a pill that opens above the text, or below it where the
4
+ * page has no room, and grows from the edge that touches the text: the browser's anchor positioning places it and
5
+ * flips it, and an anchored container query turns the origin and the nudge around with it. Hover waits 300ms and focus
6
+ * doesn't, and a tap on a touch screen focuses. Override `--ctm-enter` and `--ctm-exit`, the durations. The rules sit
7
+ * in the `comark` layer, so any unlayered rule of the host's wins.
8
+ */
9
+ export declare const css: string;
10
+ //#endregion
11
+ //#region src/client.d.ts
12
+ /** Adds the time styles to the page once. The Vue, React, and Svelte components call it when they mount. */
13
+ export declare const addStyles: () => void;
14
+ //#endregion
@@ -0,0 +1,3 @@
1
+ import { t as css } from "./styles-DQyyDP4r.mjs";
2
+ import { t as addStyles } from "./client-CNlaCtxa.mjs";
3
+ export { addStyles, css };
@@ -0,0 +1,7 @@
1
+ import _default, { TIME_TAG, TimeAttributes, TimeMeta, TimeOptions } from "./index.mjs";
2
+ import { NodeHandler } from "comark";
3
+ //#region src/html.d.ts
4
+ /** The HTML renderer for `:time`. Pass it as `components`; it inlines the styles once per document. */
5
+ export declare const components: Record<"relative-time", NodeHandler>;
6
+ //#endregion
7
+ export { TIME_TAG, type TimeAttributes, type TimeMeta, type TimeOptions, _default as default };
package/dist/html.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import { n as src_default, r as rawText, t as TIME_TAG } from "./src-ukyJ5xwD.mjs";
2
+ import { t as css } from "./styles-DQyyDP4r.mjs";
3
+ import { escapeHtml } from "comark/utils";
4
+ //#region ../../shared/html.ts
5
+ /** A value as the inside of a double-quoted HTML attribute. */
6
+ const attr = (value) => escapeHtml(rawText(value), { "\"": "&quot;" });
7
+ //#endregion
8
+ //#region src/html.ts
9
+ const styled = /* @__PURE__ */ new WeakSet();
10
+ const styles = ({ context }) => {
11
+ if (styled.has(context)) return "";
12
+ styled.add(context);
13
+ return `<style data-comark-time>${css}</style>`;
14
+ };
15
+ const time = ([, attrs], state) => {
16
+ const { id, datetime, mode, label, tip } = attrs;
17
+ if (!datetime || !label) return "";
18
+ return `${styles(state)}<time class="ctm" datetime="${attr(datetime)}" data-mode="${attr(mode)}" tabindex="0" aria-describedby="${attr(id)}">${attr(label)}<span class="ctm-tip" role="tooltip" id="${attr(id)}"><span class="ctm-pill">${attr(tip)}</span></span></time>`;
19
+ };
20
+ /** The HTML renderer for `:time`. Pass it as `components`; it inlines the styles once per document. */
21
+ const components = { "relative-time": time };
22
+ //#endregion
23
+ export { TIME_TAG, components, src_default as default };
@@ -0,0 +1,44 @@
1
+ //#region src/index.d.ts
2
+ export type TimeMeta = {
3
+ /** One readable sentence per problem with a `:time`. Other plugins add theirs too. */
4
+ issues: string[];
5
+ /** The comark-linter rule of each issue, by its sentence, so the linter reports it under its own code. */
6
+ issueRules: Record<string, string>;
7
+ };
8
+ export type TimeOptions = {
9
+ /**
10
+ * When parsing starts, as a date, an ISO string, or milliseconds. Every `:time` in a document is measured against
11
+ * it, so the output is the same each time it's rendered. Defaults to the moment the document is parsed.
12
+ */
13
+ now?: Date | string | number;
14
+ /** How far from now a date stops reading as relative, as a number and a unit (`3w`), or `none`. Default `4w`. */
15
+ threshold?: string;
16
+ /** A BCP 47 tag for the words and the date format. Defaults to the runtime's locale. */
17
+ locale?: string;
18
+ /**
19
+ * An IANA zone for the time of day and for which day is today. Defaults to the runtime's zone.
20
+ * Local times in a clock gap are invalid; repeated times use the earlier instant.
21
+ */
22
+ timeZone?: string;
23
+ };
24
+ /**
25
+ * What a `:time` becomes. `datetime` is the ISO date (or the UTC instant, with a time), `label` what the page shows,
26
+ * `tip` the text of the tooltip, and `mode` whether `label` is relative or a date.
27
+ */
28
+ export type TimeAttributes = {
29
+ id?: string;
30
+ datetime?: string;
31
+ mode?: "relative" | "absolute";
32
+ label?: string;
33
+ tip?: string;
34
+ };
35
+ /** The tag a `:time` becomes, so a `<time>` that another plugin or the author writes never reaches its components. */
36
+ export declare const TIME_TAG = "relative-time";
37
+ /**
38
+ * Turns `:time[2026-10-12]` into a `relative-time` node that reads "3 days ago" and, from `threshold` on, "Oct 12" or
39
+ * "Oct 12, 2025", with the full date as its tooltip. Everything is worked out against one `now` per document, so
40
+ * rendering twice gives the same words. Problems are added to `meta.issues`.
41
+ */
42
+ declare const _default: import("comark").ComarkPluginFactory<TimeOptions, TimeMeta, {}>;
43
+ //#endregion
44
+ export { _default as default };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export { t as TIME_TAG, n as default } from "./src-ukyJ5xwD.mjs";
@@ -0,0 +1,16 @@
1
+ import _default, { TIME_TAG, TimeAttributes, TimeMeta, TimeOptions } from "./index.mjs";
2
+ //#region src/react.d.ts
3
+ /** `:time` for `@comark/react`. */
4
+ export declare function RelativeTime({ id, datetime, mode, label, tip }: TimeAttributes): import("react").DetailedReactHTMLElement<{
5
+ className: string;
6
+ dateTime: string;
7
+ "data-mode": "relative" | "absolute" | undefined;
8
+ tabIndex: number;
9
+ "aria-describedby": string | undefined;
10
+ }, HTMLElement> | null;
11
+ /** Pass these as `components` to `@comark/react`. */
12
+ export declare const components: {
13
+ RelativeTime: typeof RelativeTime;
14
+ };
15
+ //#endregion
16
+ export { TIME_TAG, type TimeAttributes, type TimeMeta, type TimeOptions, _default as default };
package/dist/react.mjs ADDED
@@ -0,0 +1,24 @@
1
+ import { n as src_default, t as TIME_TAG } from "./src-ukyJ5xwD.mjs";
2
+ import { t as addStyles } from "./client-CNlaCtxa.mjs";
3
+ import { createElement, useEffect } from "react";
4
+ //#region src/react.ts
5
+ /** `:time` for `@comark/react`. */
6
+ function RelativeTime({ id, datetime, mode, label, tip }) {
7
+ useEffect(addStyles, []);
8
+ if (!datetime || !label) return null;
9
+ return createElement("time", {
10
+ className: "ctm",
11
+ dateTime: datetime,
12
+ "data-mode": mode,
13
+ tabIndex: 0,
14
+ "aria-describedby": id
15
+ }, label, createElement("span", {
16
+ className: "ctm-tip",
17
+ role: "tooltip",
18
+ id
19
+ }, createElement("span", { className: "ctm-pill" }, tip)));
20
+ }
21
+ /** Pass these as `components` to `@comark/react`. */
22
+ const components = { RelativeTime };
23
+ //#endregion
24
+ export { RelativeTime, TIME_TAG, components, src_default as default };
@@ -0,0 +1,267 @@
1
+ import { defineComarkPlugin } from "comark";
2
+ import { resolveAttributes } from "comark/render";
3
+ //#region ../../shared/nodes.ts
4
+ const isElement = (node) => Array.isArray(node) && node[0] !== null;
5
+ const childrenOf = ([, , ...rest]) => rest;
6
+ //#endregion
7
+ //#region ../../shared/text.ts
8
+ /**
9
+ * A prop value as text. Comark writes strings, numbers, and booleans, so an object, array, function, or missing value
10
+ * is no text. The `typeof` check, unlike `instanceof`, can't throw on a revoked proxy.
11
+ */
12
+ function scalar(value) {
13
+ switch (typeof value) {
14
+ case "string": return value;
15
+ case "number":
16
+ case "boolean":
17
+ case "bigint":
18
+ case "symbol": return String(value);
19
+ default: return;
20
+ }
21
+ }
22
+ /** A prop value as text, without its surrounding whitespace, or empty when it has none. */
23
+ const text = (value) => scalar(value)?.trim() ?? "";
24
+ /** A prop value as text exactly as written, or empty when it has none. */
25
+ const rawText = (value) => scalar(value) ?? "";
26
+ /** FNV-1a, in base 36: short, and the same every time the same text is hashed, so ids derived from it are stable. */
27
+ function hash(source) {
28
+ let value = 2166136261;
29
+ for (let at = 0; at < source.length; at++) value = Math.imul(value ^ source.charCodeAt(at), 16777619);
30
+ return (value >>> 0).toString(36);
31
+ }
32
+ //#endregion
33
+ //#region src/index.ts
34
+ /** The tag a `:time` becomes, so a `<time>` that another plugin or the author writes never reaches its components. */
35
+ const TIME_TAG = "relative-time";
36
+ const DAY = 864e5;
37
+ const UNITS = {
38
+ s: 1e3,
39
+ m: 6e4,
40
+ h: 36e5,
41
+ d: DAY,
42
+ w: 7 * DAY,
43
+ mo: 30 * DAY,
44
+ y: 365 * DAY
45
+ };
46
+ const THRESHOLD = /^(\d+(?:\.\d+)?)\s*(s|m|h|d|w|mo|y)$/i;
47
+ const DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?\s*(Z|[+-]\d{2}:?\d{2})?)?$/i;
48
+ const PROPS = ["threshold"];
49
+ /** `3w` or `12h` in milliseconds, `Infinity` for `none`, and `undefined` for anything else. */
50
+ function duration(value) {
51
+ if (/^(none|never)$/i.test(value)) return Number.POSITIVE_INFINITY;
52
+ const [, amount, unit] = THRESHOLD.exec(value) ?? [];
53
+ const per = new Map(Object.entries(UNITS)).get(unit?.toLowerCase() ?? "");
54
+ return amount && per ? Number(amount) * per : void 0;
55
+ }
56
+ const parts = (zone, at) => {
57
+ return Object.fromEntries(new Intl.DateTimeFormat("en-US", {
58
+ timeZone: zone,
59
+ hourCycle: "h23",
60
+ year: "numeric",
61
+ month: "numeric",
62
+ day: "numeric",
63
+ hour: "numeric",
64
+ minute: "numeric",
65
+ second: "numeric"
66
+ }).formatToParts(at).map((part) => [part.type, Number(part.value)]));
67
+ };
68
+ /** How far `zone` is ahead of UTC at `at`, in milliseconds. */
69
+ function offsetAt(zone, at) {
70
+ const p = parts(zone, at);
71
+ return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - Math.floor(at / 1e3) * 1e3;
72
+ }
73
+ /** A date or date and time as written. A date alone is a calendar day (`wall`), and a time with no offset is `zone`'s. */
74
+ function moment(value, zone) {
75
+ const match = DATE.exec(value);
76
+ if (!match) return void 0;
77
+ const field = (at) => Number(match[at] ?? 0);
78
+ const [year, month, day, hour, minute, second] = [
79
+ field(1),
80
+ field(2),
81
+ field(3),
82
+ field(4),
83
+ field(5),
84
+ field(6)
85
+ ];
86
+ const millisecond = Number((match[7] ?? "").padEnd(3, "0"));
87
+ const stamp = Date.UTC(year, month - 1, day, hour, minute, second, millisecond);
88
+ const check = new Date(stamp);
89
+ if (check.getUTCFullYear() !== year || check.getUTCMonth() !== month - 1 || check.getUTCDate() !== day || hour > 23 || minute > 59 || second > 59) return void 0;
90
+ if (match[4] === void 0) return {
91
+ at: Math.floor(stamp / DAY) * DAY,
92
+ wall: true
93
+ };
94
+ const offset = match[8];
95
+ if (offset?.toUpperCase() === "Z") return {
96
+ at: stamp,
97
+ wall: false
98
+ };
99
+ if (offset) {
100
+ const digits = offset.replace(/\D/g, "");
101
+ const hours = Number(digits.slice(0, 2));
102
+ const part = Number(digits.slice(2));
103
+ if (hours > 23 || part > 59) return void 0;
104
+ const minutes = hours * 60 + part;
105
+ return {
106
+ at: stamp - (offset.startsWith("-") ? -1 : 1) * minutes * 6e4,
107
+ wall: false
108
+ };
109
+ }
110
+ const at = [...new Set([
111
+ stamp - DAY,
112
+ stamp,
113
+ stamp + DAY
114
+ ].map((at) => stamp - offsetAt(zone, at)))].filter((at) => at + offsetAt(zone, at) === stamp).sort((a, b) => a - b)[0];
115
+ return at === void 0 ? void 0 : {
116
+ at,
117
+ wall: false
118
+ };
119
+ }
120
+ const iso = (when) => when.wall ? new Date(when.at).toISOString().slice(0, 10) : new Date(when.at).toISOString().replace(/\.000Z$/, "Z");
121
+ /**
122
+ * Every formatter, built once when the plugin is created: a bad `locale` or `timeZone` throws there, where the
123
+ * developer sees it, rather than in the middle of a document. A date alone is formatted in UTC, so no zone moves its day.
124
+ */
125
+ function formatters(locale, zone) {
126
+ const date = (timeZone, extra) => new Intl.DateTimeFormat(locale, {
127
+ timeZone,
128
+ ...extra
129
+ });
130
+ const both = (extra) => ({
131
+ wall: date("UTC", extra),
132
+ zoned: date(zone, extra)
133
+ });
134
+ const short = {
135
+ day: "numeric",
136
+ month: "short"
137
+ };
138
+ return {
139
+ relative: new Intl.RelativeTimeFormat(locale, { numeric: "auto" }),
140
+ short: both(short),
141
+ shortYear: both({
142
+ ...short,
143
+ year: "numeric"
144
+ }),
145
+ day: date("UTC", {
146
+ weekday: "short",
147
+ ...short,
148
+ year: "numeric"
149
+ }),
150
+ full: date(zone, {
151
+ weekday: "short",
152
+ ...short,
153
+ year: "numeric",
154
+ hour: "numeric",
155
+ minute: "2-digit",
156
+ timeZoneName: "short"
157
+ }),
158
+ clock: date(zone, {
159
+ hour: "numeric",
160
+ minute: "2-digit",
161
+ timeZoneName: "short"
162
+ })
163
+ };
164
+ }
165
+ /** The distance to `ms` from now in the largest unit it fills, rounded toward now: "2 days ago", "in 3 weeks". */
166
+ function ago(relative, ms, wall) {
167
+ const size = wall ? Math.max(Math.abs(ms), DAY) : Math.abs(ms);
168
+ const [unit, per] = size < UNITS.m ? ["second", UNITS.s] : size < UNITS.h ? ["minute", UNITS.m] : size < DAY ? ["hour", UNITS.h] : size < UNITS.w ? ["day", DAY] : size < UNITS.mo ? ["week", UNITS.w] : size < UNITS.y ? ["month", UNITS.mo] : ["year", UNITS.y];
169
+ return relative.format(Math.trunc(ms / per) || 0, unit);
170
+ }
171
+ function props(node, name, issues) {
172
+ for (const key of Object.keys(node[1])) {
173
+ const prop = key.replace(/^:/, "");
174
+ if (!PROPS.includes(prop) && prop !== "id" && prop !== "class") issues.push({
175
+ rule: "comark/time/unknown-prop",
176
+ message: `${name}: \`${prop}\` isn't a prop of \`:time\`, so it's ignored. Use \`threshold\`.`
177
+ });
178
+ }
179
+ }
180
+ const textOf = (node) => !Array.isArray(node) ? node : isElement(node) ? childrenOf(node).map(textOf).join("") : "";
181
+ /**
182
+ * Turns `:time[2026-10-12]` into a `relative-time` node that reads "3 days ago" and, from `threshold` on, "Oct 12" or
183
+ * "Oct 12, 2025", with the full date as its tooltip. Everything is worked out against one `now` per document, so
184
+ * rendering twice gives the same words. Problems are added to `meta.issues`.
185
+ */
186
+ var src_default = defineComarkPlugin((settings = {}) => {
187
+ const zone = settings.timeZone ?? new Intl.DateTimeFormat().resolvedOptions().timeZone;
188
+ const format = formatters(settings.locale, zone);
189
+ const fallback = duration(settings.threshold ?? "4w");
190
+ if (fallback === void 0) throw new RangeError(`comark-time: threshold "${settings.threshold}" isn't a duration. Write a number and a unit, such as "3w", or "none".`);
191
+ const configuredNow = settings.now === void 0 ? void 0 : new Date(settings.now).getTime();
192
+ if (configuredNow !== void 0 && !Number.isFinite(configuredNow)) throw new RangeError("comark-time: now isn't a valid date. Use a date, an ISO string, or milliseconds.");
193
+ return {
194
+ name: "comark-time",
195
+ post(state) {
196
+ const { tree } = state;
197
+ const data = {
198
+ frontmatter: tree.frontmatter,
199
+ meta: tree.meta,
200
+ data: {},
201
+ props: {}
202
+ };
203
+ const now = configuredNow ?? Date.now();
204
+ const today = Math.floor((now + offsetAt(zone, now)) / DAY);
205
+ const year = parts(zone, now).year;
206
+ const issues = [];
207
+ const prefix = `ctm-${hash(state.markdown)}`;
208
+ let count = 0;
209
+ const asWritten = (node, shown) => void node.splice(0, node.length, "span", {}, ...shown ? [shown] : []);
210
+ const read = (node) => {
211
+ const name = `\`:time\` ${++count}`;
212
+ const shown = textOf(node).trim();
213
+ props(node, name, issues);
214
+ const attrs = resolveAttributes(node[1], data, { parseJson: true });
215
+ const wanted = text(attrs.threshold);
216
+ const limit = wanted ? duration(wanted) : fallback;
217
+ if (limit === void 0) issues.push({
218
+ rule: "comark/time/invalid-threshold",
219
+ message: `${name}: \`threshold="${wanted}"\` isn't a duration. Write a number and a unit, such as \`threshold="3w"\`, or \`none\` to keep it relative.`
220
+ });
221
+ if (!shown) {
222
+ issues.push({
223
+ rule: "comark/time/missing-date",
224
+ message: `${name} has no date. Write the date in brackets, such as \`:time[2026-10-12]\`.`
225
+ });
226
+ return asWritten(node, "");
227
+ }
228
+ const when = moment(shown, zone);
229
+ if (!when) {
230
+ issues.push({
231
+ rule: "comark/time/invalid-date",
232
+ message: `${name}: "${shown}" isn't a date, so it's shown as written. Write it as \`2026-10-12\` or \`2026-10-12T14:03:00Z\`.`
233
+ });
234
+ return asWritten(node, shown);
235
+ }
236
+ const distance = when.wall ? (when.at / DAY - today) * DAY : when.at - now;
237
+ const relative = ago(format.relative, distance, when.wall);
238
+ const absolute = Math.abs(distance) >= (limit ?? fallback);
239
+ const sameYear = (when.wall ? new Date(when.at).getUTCFullYear() : parts(zone, when.at).year) === year;
240
+ const label = absolute ? (sameYear ? format.short : format.shortYear)[when.wall ? "wall" : "zoned"].format(when.at) : relative;
241
+ const tip = !absolute ? (when.wall ? format.day : format.full).format(when.at) : when.wall ? relative : `${relative} · ${format.clock.format(when.at)}`;
242
+ node.splice(0, node.length, TIME_TAG, {
243
+ id: `${prefix}-${count}`,
244
+ datetime: iso(when),
245
+ mode: absolute ? "absolute" : "relative",
246
+ label,
247
+ tip
248
+ });
249
+ };
250
+ const walk = (nodes) => {
251
+ for (const node of nodes) {
252
+ if (!isElement(node) || node[1].$?.html) continue;
253
+ if (node[0] === "time" && node[1].datetime === void 0) read(node);
254
+ else walk(childrenOf(node));
255
+ }
256
+ };
257
+ walk(tree.nodes);
258
+ tree.meta.issues = [...tree.meta.issues ?? [], ...issues.map((issue) => issue.message)];
259
+ tree.meta.issueRules = {
260
+ ...tree.meta.issueRules,
261
+ ...Object.fromEntries(issues.map((issue) => [issue.message, issue.rule]))
262
+ };
263
+ }
264
+ };
265
+ });
266
+ //#endregion
267
+ export { src_default as n, rawText as r, TIME_TAG as t };
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ @layer comark{:where(.ctm){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#fff)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#0a0a0a)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#737373)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,#e5e5e5)));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#f5f5f5)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#a1a1a1)));--_comark-shadow:var(--comark-shadow,0 4px 12px rgb(0 0 0/.12));--_comark-radius:var(--comark-radius,calc(var(--ui-radius,calc(var(--radius,.625rem)/2))*2));--_comark-font:var(--comark-font,var(--font-sans,ui-sans-serif,system-ui,sans-serif))}@media (prefers-color-scheme:dark){:where(:not(.light,.light *)):is(:where(.ctm)){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#0a0a0a)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#fafafa)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#a1a1a1)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,rgb(255 255 255/.1))));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#262626)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#737373)));--_comark-shadow:var(--comark-shadow,0 4px 12px rgb(0 0 0/.5))}}:where(.dark,.dark *):is(:where(.ctm)){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#0a0a0a)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#fafafa)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#a1a1a1)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,rgb(255 255 255/.1))));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#262626)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#737373)));--_comark-shadow:var(--comark-shadow,0 4px 12px rgb(0 0 0/.5))}:where(.ctm){--ctm-enter:140ms;--ctm-exit:100ms}.ctm{display:inline;margin:0 -.125rem;padding:0 .125rem;border-radius:calc(var(--_comark-radius) / 2);font-variant-numeric:tabular-nums;white-space:nowrap;cursor:default;transition:background-color 150ms ease-out}.ctm:focus{outline:2px solid var(--_comark-ring);outline-offset:1px}.ctm:focus:not(:focus-visible){outline:0}.ctm:focus{background:var(--_comark-accent)}@media (hover:hover) and (pointer:fine){.ctm:hover{background:var(--_comark-accent)}}.ctm-tip{position:fixed;z-index:10;display:block;width:max-content;max-width:min(24rem,calc(100vw - 1rem));margin:0;padding:0;visibility:hidden;pointer-events:none;white-space:normal;transition:visibility 0s linear var(--ctm-exit)}.ctm-pill{display:block;padding:.25rem .5rem;border:1px solid var(--_comark-border);border-radius:calc(var(--_comark-radius) - 2px);background:var(--_comark-background);box-shadow:var(--_comark-shadow);color:var(--_comark-foreground);font:400 .8125rem/1.4 var(--_comark-font);text-align:center;opacity:0;scale:.97;translate:0 calc(var(--ctm-dir,1) * 4px);transform-origin:50% 100%;transition:opacity var(--ctm-exit) ease-in,scale var(--ctm-exit) ease-in,translate var(--ctm-exit) ease-in}.ctm:focus .ctm-tip{visibility:visible;transition-delay:0s}.ctm:focus .ctm-pill{opacity:1;scale:1;translate:0 0;transition:opacity var(--ctm-enter) cubic-bezier(.32,.72,0,1),scale var(--ctm-enter) cubic-bezier(.32,.72,0,1),translate var(--ctm-enter) cubic-bezier(.32,.72,0,1)}@media (hover:hover) and (pointer:fine){.ctm:hover .ctm-tip{visibility:visible;transition-delay:0s}.ctm:hover .ctm-pill{opacity:1;scale:1;translate:0 0;transition:opacity var(--ctm-enter) cubic-bezier(.32,.72,0,1) 300ms,scale var(--ctm-enter) cubic-bezier(.32,.72,0,1) 300ms,translate var(--ctm-enter) cubic-bezier(.32,.72,0,1) 300ms}.ctm:focus .ctm-pill{transition-delay:0s}}@supports (anchor-scope:all){.ctm{anchor-name:--ctm;anchor-scope:--ctm}.ctm-tip{position-anchor:--ctm;position-area:top;justify-self:anchor-center;position-try-fallbacks:--ctm-end,--ctm-start,--ctm-below,--ctm-below-end,--ctm-below-start;margin:.375rem .25rem;container-type:anchored}@position-try --ctm-end{position-area:top span-left;justify-self:end}@position-try --ctm-start{position-area:top span-right;justify-self:start}@position-try --ctm-below{position-area:bottom;justify-self:anchor-center}@position-try --ctm-below-end{position-area:bottom span-left;justify-self:end}@position-try --ctm-below-start{position-area:bottom span-right;justify-self:start}@container anchored(fallback:--ctm-below) or anchored(fallback:--ctm-below-end) or anchored(fallback:--ctm-below-start){.ctm-pill{--ctm-dir:-1;transform-origin:50% 0}}}@supports not (anchor-scope:all){.ctm{position:relative}.ctm-tip{position:absolute;bottom:100%;left:0;margin-bottom:.375rem}}@media (prefers-reduced-motion:reduce){.ctm,.ctm-tip,.ctm-pill{transition:none!important}.ctm-pill{scale:1;translate:none}}}
@@ -0,0 +1,165 @@
1
+ //#region ../zoom/dist/theme.mjs
2
+ const tokens = [
3
+ [
4
+ "background",
5
+ "ui-bg",
6
+ "background",
7
+ "#fff",
8
+ "#0a0a0a"
9
+ ],
10
+ [
11
+ "foreground",
12
+ "ui-text-highlighted",
13
+ "foreground",
14
+ "#0a0a0a",
15
+ "#fafafa"
16
+ ],
17
+ [
18
+ "muted",
19
+ "ui-text-muted",
20
+ "muted-foreground",
21
+ "#737373",
22
+ "#a1a1a1"
23
+ ],
24
+ [
25
+ "border",
26
+ "ui-border",
27
+ "border",
28
+ "#e5e5e5",
29
+ "rgb(255 255 255/.1)"
30
+ ],
31
+ [
32
+ "accent",
33
+ "ui-bg-elevated",
34
+ "accent",
35
+ "#f5f5f5",
36
+ "#262626"
37
+ ],
38
+ [
39
+ "ring",
40
+ "ui-primary",
41
+ "ring",
42
+ "#a1a1a1",
43
+ "#737373"
44
+ ]
45
+ ];
46
+ const groups = {
47
+ status: Object.values({
48
+ success: [
49
+ "success",
50
+ "ui-success",
51
+ "success",
52
+ "#259f56",
53
+ "#59d38c"
54
+ ],
55
+ danger: [
56
+ "danger",
57
+ "ui-error",
58
+ "destructive",
59
+ "#ee3533",
60
+ "#fd736d"
61
+ ],
62
+ warning: [
63
+ "warning",
64
+ "ui-warning",
65
+ "",
66
+ "#d57506",
67
+ "#fbbf24"
68
+ ],
69
+ info: [
70
+ "info",
71
+ "ui-info",
72
+ "",
73
+ "#2563eb",
74
+ "#60a5fa"
75
+ ]
76
+ }),
77
+ primary: [
78
+ [
79
+ "primary",
80
+ "ui-primary",
81
+ "primary",
82
+ "var(--_comark-foreground)",
83
+ "var(--_comark-foreground)"
84
+ ],
85
+ [
86
+ "primary-foreground",
87
+ "ui-text-inverted",
88
+ "primary-foreground",
89
+ "var(--_comark-background)",
90
+ "var(--_comark-background)"
91
+ ],
92
+ [
93
+ "secondary",
94
+ "ui-secondary",
95
+ "",
96
+ "var(--_comark-muted)",
97
+ "var(--_comark-muted)"
98
+ ]
99
+ ],
100
+ shadow: [[
101
+ "shadow",
102
+ "",
103
+ "",
104
+ "0 4px 12px rgb(0 0 0/.12)",
105
+ "0 4px 12px rgb(0 0 0/.5)"
106
+ ]]
107
+ };
108
+ const chain = (token, scheme) => {
109
+ const shadcn = token[2] ? `var(--${token[2]},${token[scheme]})` : token[scheme];
110
+ const nuxt = token[1] ? `var(--${token[1]},${shadcn})` : shadcn;
111
+ return `var(--comark-${token[0]},${nuxt})`;
112
+ };
113
+ const resolve = (list, scheme) => list.map((token) => `--_comark-${token[0]}:${chain(token, scheme)}`).join(";");
114
+ /**
115
+ * `declarations` on `selector` in dark mode: the system's, unless a `.light` class sits on the element or above it, or
116
+ * a `.dark` class there whatever the system says. Either class pins a page's scheme.
117
+ */
118
+ function dark(selector, declarations) {
119
+ return `@media (prefers-color-scheme:dark){:where(:not(.light,.light *)):is(${selector}){${declarations}}}:where(.dark,.dark *):is(${selector}){${declarations}}`;
120
+ }
121
+ /**
122
+ * Resolves the `--comark-*` tokens on `selector`, falling back to Nuxt UI's variables, then shadcn/ui's, then shadcn's
123
+ * neutral theme. Nuxt UI's corners are twice its `--ui-radius`, so the radius halves shadcn's to double it back.
124
+ * They resolve on the component rather than `:root`, so a `--comark-*` set on any ancestor still inherits down.
125
+ * `:where()` keeps them at zero specificity. The neutral theme follows the system's color mode, and a `.light` or
126
+ * `.dark` class on the component or above it pins one (see `dark()`).
127
+ *
128
+ * Every block gets the core tokens. Name `"status"` for `--comark-success`, `-danger`, `-warning`, and `-info`,
129
+ * `"primary"` for `--comark-primary`, `-primary-foreground`, and `-secondary`, and `"shadow"` for `--comark-shadow`, so
130
+ * a block that never draws them doesn't carry them.
131
+ */
132
+ function theme(selector, ...extras) {
133
+ const where = `:where(${selector})`;
134
+ const list = [...tokens, ...extras.flatMap((name) => groups[name])];
135
+ return `${where}{${resolve(list, 3)};--_comark-radius:var(--comark-radius,calc(var(--ui-radius,calc(var(--radius,.625rem)/2))*2));--_comark-font:var(--comark-font,var(--font-sans,ui-sans-serif,system-ui,sans-serif))}` + dark(where, resolve(list, 4));
136
+ }
137
+ //#endregion
138
+ //#region src/styles.ts
139
+ const ease = "cubic-bezier(.32,.72,0,1)";
140
+ /**
141
+ * A time as plain text that tints on hover. Its tooltip is a pill that opens above the text, or below it where the
142
+ * page has no room, and grows from the edge that touches the text: the browser's anchor positioning places it and
143
+ * flips it, and an anchored container query turns the origin and the nudge around with it. Hover waits 300ms and focus
144
+ * doesn't, and a tap on a touch screen focuses. Override `--ctm-enter` and `--ctm-exit`, the durations. The rules sit
145
+ * in the `comark` layer, so any unlayered rule of the host's wins.
146
+ */
147
+ const css = `@layer comark{${[
148
+ theme(".ctm", "shadow"),
149
+ ":where(.ctm){--ctm-enter:140ms;--ctm-exit:100ms}",
150
+ ".ctm{display:inline;margin:0 -.125rem;padding:0 .125rem;border-radius:calc(var(--_comark-radius) / 2);font-variant-numeric:tabular-nums;white-space:nowrap;cursor:default;transition:background-color 150ms ease-out}",
151
+ ".ctm:focus{outline:2px solid var(--_comark-ring);outline-offset:1px}",
152
+ ".ctm:focus:not(:focus-visible){outline:0}",
153
+ ".ctm:focus{background:var(--_comark-accent)}",
154
+ "@media (hover:hover) and (pointer:fine){.ctm:hover{background:var(--_comark-accent)}}",
155
+ `.ctm-tip{position:fixed;z-index:10;display:block;width:max-content;max-width:min(24rem,calc(100vw - 1rem));margin:0;padding:0;visibility:hidden;pointer-events:none;white-space:normal;transition:visibility 0s linear var(--ctm-exit)}`,
156
+ `.ctm-pill{display:block;padding:.25rem .5rem;border:1px solid var(--_comark-border);border-radius:calc(var(--_comark-radius) - 2px);background:var(--_comark-background);box-shadow:var(--_comark-shadow);color:var(--_comark-foreground);font:400 .8125rem/1.4 var(--_comark-font);text-align:center;opacity:0;scale:.97;translate:0 calc(var(--ctm-dir,1) * 4px);transform-origin:50% 100%;transition:opacity var(--ctm-exit) ease-in,scale var(--ctm-exit) ease-in,translate var(--ctm-exit) ease-in}`,
157
+ ".ctm:focus .ctm-tip{visibility:visible;transition-delay:0s}",
158
+ `.ctm:focus .ctm-pill{opacity:1;scale:1;translate:0 0;transition:opacity var(--ctm-enter) ${ease},scale var(--ctm-enter) ${ease},translate var(--ctm-enter) ${ease}}`,
159
+ `@media (hover:hover) and (pointer:fine){.ctm:hover .ctm-tip{visibility:visible;transition-delay:0s}.ctm:hover .ctm-pill{opacity:1;scale:1;translate:0 0;transition:opacity var(--ctm-enter) ${ease} 300ms,scale var(--ctm-enter) ${ease} 300ms,translate var(--ctm-enter) ${ease} 300ms}.ctm:focus .ctm-pill{transition-delay:0s}}`,
160
+ "@supports (anchor-scope:all){.ctm{anchor-name:--ctm;anchor-scope:--ctm}.ctm-tip{position-anchor:--ctm;position-area:top;justify-self:anchor-center;position-try-fallbacks:--ctm-end,--ctm-start,--ctm-below,--ctm-below-end,--ctm-below-start;margin:.375rem .25rem;container-type:anchored}@position-try --ctm-end{position-area:top span-left;justify-self:end}@position-try --ctm-start{position-area:top span-right;justify-self:start}@position-try --ctm-below{position-area:bottom;justify-self:anchor-center}@position-try --ctm-below-end{position-area:bottom span-left;justify-self:end}@position-try --ctm-below-start{position-area:bottom span-right;justify-self:start}@container anchored(fallback:--ctm-below) or anchored(fallback:--ctm-below-end) or anchored(fallback:--ctm-below-start){.ctm-pill{--ctm-dir:-1;transform-origin:50% 0}}}",
161
+ "@supports not (anchor-scope:all){.ctm{position:relative}.ctm-tip{position:absolute;bottom:100%;left:0;margin-bottom:.375rem}}",
162
+ "@media (prefers-reduced-motion:reduce){.ctm,.ctm-tip,.ctm-pill{transition:none!important}.ctm-pill{scale:1;translate:none}}"
163
+ ].join("")}}`;
164
+ //#endregion
165
+ export { css as t };
@@ -0,0 +1,12 @@
1
+ <!-- `:time` for `@comark/svelte`. -->
2
+ <script>
3
+ import { onMount } from "svelte";
4
+ import { addStyles } from "comark-time/client";
5
+
6
+ let { id = undefined, datetime = undefined, mode = undefined, label = undefined, tip = undefined } = $props();
7
+ onMount(addStyles);
8
+ </script>
9
+
10
+ {#if datetime && label}
11
+ <time class="ctm" {datetime} data-mode={mode} tabindex="0" aria-describedby={id}>{label}<span class="ctm-tip" role="tooltip" {id}><span class="ctm-pill">{tip}</span></span></time>
12
+ {/if}
@@ -0,0 +1,8 @@
1
+ import type { Component } from "svelte";
2
+ import type { TimeAttributes } from "comark-time";
3
+
4
+ export { default, TIME_TAG } from "comark-time";
5
+ export type { TimeAttributes, TimeMeta, TimeOptions } from "comark-time";
6
+
7
+ export declare const RelativeTime: Component<TimeAttributes>;
8
+ export declare const components: { RelativeTime: typeof RelativeTime };
@@ -0,0 +1,6 @@
1
+ // `:time` for `@comark/svelte`. Pass `components` to it; the parse plugin comes from `comark-time`.
2
+ import RelativeTime from "./RelativeTime.svelte";
3
+
4
+ export { default, TIME_TAG } from "comark-time";
5
+ export { RelativeTime };
6
+ export const components = { RelativeTime };
package/dist/vue.d.mts ADDED
@@ -0,0 +1,38 @@
1
+ import _default, { TIME_TAG, TimeAttributes, TimeMeta, TimeOptions } from "./index.mjs";
2
+ //#region src/vue.d.ts
3
+ /** `:time` for `@comark/vue`. */
4
+ export declare const RelativeTime: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
5
+ id: StringConstructor;
6
+ datetime: StringConstructor;
7
+ mode: StringConstructor;
8
+ label: StringConstructor;
9
+ tip: StringConstructor;
10
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
11
+ [key: string]: any;
12
+ }> | null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
13
+ id: StringConstructor;
14
+ datetime: StringConstructor;
15
+ mode: StringConstructor;
16
+ label: StringConstructor;
17
+ tip: StringConstructor;
18
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
19
+ /** Pass these as `components` to `@comark/vue`. */
20
+ export declare const components: {
21
+ RelativeTime: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
22
+ id: StringConstructor;
23
+ datetime: StringConstructor;
24
+ mode: StringConstructor;
25
+ label: StringConstructor;
26
+ tip: StringConstructor;
27
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
28
+ [key: string]: any;
29
+ }> | null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
30
+ id: StringConstructor;
31
+ datetime: StringConstructor;
32
+ mode: StringConstructor;
33
+ label: StringConstructor;
34
+ tip: StringConstructor;
35
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
36
+ };
37
+ //#endregion
38
+ export { TIME_TAG, type TimeAttributes, type TimeMeta, type TimeOptions, _default as default };
package/dist/vue.mjs ADDED
@@ -0,0 +1,34 @@
1
+ import { n as src_default, t as TIME_TAG } from "./src-ukyJ5xwD.mjs";
2
+ import { t as addStyles } from "./client-CNlaCtxa.mjs";
3
+ import { defineComponent, h, onMounted } from "vue";
4
+ //#region src/vue.ts
5
+ /** `:time` for `@comark/vue`. */
6
+ const RelativeTime = defineComponent({
7
+ name: "RelativeTime",
8
+ inheritAttrs: false,
9
+ props: {
10
+ id: String,
11
+ datetime: String,
12
+ mode: String,
13
+ label: String,
14
+ tip: String
15
+ },
16
+ setup(props) {
17
+ onMounted(addStyles);
18
+ return () => props.datetime && props.label ? h("time", {
19
+ class: "ctm",
20
+ datetime: props.datetime,
21
+ "data-mode": props.mode,
22
+ tabindex: 0,
23
+ "aria-describedby": props.id
24
+ }, [props.label, h("span", {
25
+ class: "ctm-tip",
26
+ role: "tooltip",
27
+ id: props.id
28
+ }, [h("span", { class: "ctm-pill" }, props.tip)])]) : null;
29
+ }
30
+ });
31
+ /** Pass these as `components` to `@comark/vue`. */
32
+ const components = { RelativeTime };
33
+ //#endregion
34
+ export { RelativeTime, TIME_TAG, components, src_default as default };
package/package.json CHANGED
@@ -1,11 +1,92 @@
1
1
  {
2
2
  "name": "comark-time",
3
- "version": "0.0.0",
4
- "description": "Placeholder to claim the name. The real package is in https://github.com/onmax/comark-ui/tree/main/packages/time",
3
+ "version": "0.10.0",
4
+ "description": "Dates as relative time that turn into a date past a threshold, with the full date in a tooltip. A Comark plugin.",
5
+ "keywords": [
6
+ "ago",
7
+ "comark",
8
+ "date",
9
+ "datetime",
10
+ "markdown",
11
+ "mdc",
12
+ "plugin",
13
+ "relative",
14
+ "time"
15
+ ],
16
+ "homepage": "https://comark-ui.onmax.me/time",
17
+ "bugs": "https://github.com/onmax/comark-ui/issues",
5
18
  "license": "MIT",
19
+ "author": "onmax",
6
20
  "repository": {
7
21
  "type": "git",
8
22
  "url": "git+https://github.com/onmax/comark-ui.git",
9
23
  "directory": "packages/time"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "sideEffects": [
30
+ "*.css"
31
+ ],
32
+ "exports": {
33
+ ".": "./dist/index.mjs",
34
+ "./client": "./dist/client.mjs",
35
+ "./html": "./dist/html.mjs",
36
+ "./react": "./dist/react.mjs",
37
+ "./style.css": "./dist/style.css",
38
+ "./svelte": {
39
+ "types": "./dist/svelte/index.d.ts",
40
+ "svelte": "./dist/svelte/index.js",
41
+ "default": "./dist/svelte/index.js"
42
+ },
43
+ "./vue": "./dist/vue.mjs",
44
+ "./package.json": "./package.json"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "provenance": true
49
+ },
50
+ "devDependencies": {
51
+ "@comark/html": "^0.9.0",
52
+ "@comark/react": "^0.9.0",
53
+ "@comark/vue": "^0.9.0",
54
+ "@types/react": "^19.3.0",
55
+ "@types/react-dom": "^19.3.0",
56
+ "comark": "^0.9.0",
57
+ "react": "^19.3.1",
58
+ "react-dom": "^19.3.1",
59
+ "svelte": "^5.57.2",
60
+ "vue": "^3.5.44",
61
+ "comark-zoom": "0.10.0"
62
+ },
63
+ "peerDependencies": {
64
+ "@comark/html": "^0.9.0",
65
+ "comark": "^0.9.0",
66
+ "react": ">=18",
67
+ "svelte": ">=5",
68
+ "vue": ">=3.5"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@comark/html": {
72
+ "optional": true
73
+ },
74
+ "react": {
75
+ "optional": true
76
+ },
77
+ "svelte": {
78
+ "optional": true
79
+ },
80
+ "vue": {
81
+ "optional": true
82
+ }
83
+ },
84
+ "engines": {
85
+ "node": ">=22.12.0"
86
+ },
87
+ "scripts": {
88
+ "build": "tsdown",
89
+ "dev": "tsdown --watch",
90
+ "doctor": "vite-doctor . --no-cache --max-warnings 0 --format text"
10
91
  }
11
- }
92
+ }