@adia-ai/web-components 0.6.36 → 0.6.38
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/CHANGELOG.md +48 -1
- package/components/accordion/accordion-item.a2ui.json +3 -0
- package/components/accordion/accordion-item.yaml +5 -0
- package/components/action-list/action-item.a2ui.json +5 -1
- package/components/action-list/action-item.yaml +7 -0
- package/components/badge/badge.a2ui.json +10 -0
- package/components/badge/badge.css +70 -0
- package/components/badge/badge.yaml +20 -0
- package/components/blockquote/blockquote.a2ui.json +121 -0
- package/components/blockquote/blockquote.class.js +68 -0
- package/components/blockquote/blockquote.css +46 -0
- package/components/blockquote/blockquote.d.ts +31 -0
- package/components/blockquote/blockquote.js +17 -0
- package/components/blockquote/blockquote.yaml +124 -0
- package/components/button/button.css +11 -3
- package/components/calendar-picker/calendar-picker.a2ui.json +15 -0
- package/components/calendar-picker/calendar-picker.class.js +7 -1
- package/components/calendar-picker/calendar-picker.yaml +14 -0
- package/components/card/card.a2ui.json +17 -1
- package/components/card/card.yaml +24 -1
- package/components/color-input/color-input.a2ui.json +2 -2
- package/components/color-input/color-input.class.js +9 -2
- package/components/color-input/color-input.yaml +2 -2
- package/components/combobox/combobox.class.js +4 -0
- package/components/context-menu/context-menu.a2ui.json +159 -0
- package/components/context-menu/context-menu.class.js +275 -0
- package/components/context-menu/context-menu.css +56 -0
- package/components/context-menu/context-menu.d.ts +70 -0
- package/components/context-menu/context-menu.js +17 -0
- package/components/context-menu/context-menu.yaml +136 -0
- package/components/date-range-picker/date-range-picker.a2ui.json +15 -0
- package/components/date-range-picker/date-range-picker.class.js +2 -0
- package/components/date-range-picker/date-range-picker.yaml +14 -0
- package/components/datetime-picker/datetime-picker.a2ui.json +15 -0
- package/components/datetime-picker/datetime-picker.class.js +3 -1
- package/components/datetime-picker/datetime-picker.d.ts +2 -0
- package/components/datetime-picker/datetime-picker.yaml +14 -0
- package/components/empty-state/empty-state.a2ui.json +9 -0
- package/components/empty-state/empty-state.class.js +2 -0
- package/components/empty-state/empty-state.yaml +15 -0
- package/components/feed/feed-item.a2ui.json +5 -0
- package/components/feed/feed-item.yaml +10 -0
- package/components/feed/feed.class.js +13 -5
- package/components/feed/feed.css +14 -0
- package/components/field/field.a2ui.json +6 -0
- package/components/field/field.yaml +10 -0
- package/components/index.js +11 -0
- package/components/inline-edit/inline-edit.a2ui.json +159 -0
- package/components/inline-edit/inline-edit.class.js +184 -0
- package/components/inline-edit/inline-edit.css +62 -0
- package/components/inline-edit/inline-edit.d.ts +52 -0
- package/components/inline-edit/inline-edit.js +12 -0
- package/components/inline-edit/inline-edit.yaml +125 -0
- package/components/integration-card/integration-card.class.js +9 -0
- package/components/integration-card/integration-card.test.js +4 -3
- package/components/list/list-item.a2ui.json +8 -1
- package/components/list/list-item.yaml +12 -0
- package/components/list/list.css +36 -6
- package/components/mark/mark.a2ui.json +109 -0
- package/components/mark/mark.class.js +22 -0
- package/components/mark/mark.css +39 -0
- package/components/mark/mark.d.ts +27 -0
- package/components/mark/mark.js +12 -0
- package/components/mark/mark.yaml +87 -0
- package/components/modal/modal.a2ui.json +9 -0
- package/components/modal/modal.yaml +14 -0
- package/components/nav-group/nav-group.a2ui.json +3 -0
- package/components/nav-group/nav-group.css +7 -1
- package/components/nav-group/nav-group.yaml +5 -0
- package/components/nav-item/nav-item.a2ui.json +3 -0
- package/components/nav-item/nav-item.yaml +5 -0
- package/components/number-format/number-format.a2ui.json +180 -0
- package/components/number-format/number-format.class.js +96 -0
- package/components/number-format/number-format.css +18 -0
- package/components/number-format/number-format.d.ts +68 -0
- package/components/number-format/number-format.js +17 -0
- package/components/number-format/number-format.yaml +204 -0
- package/components/pagination/pagination.a2ui.json +19 -2
- package/components/pagination/pagination.class.js +90 -37
- package/components/pagination/pagination.css +32 -127
- package/components/pagination/pagination.d.ts +8 -2
- package/components/pagination/pagination.test.js +195 -0
- package/components/pagination/pagination.yaml +22 -1
- package/components/password-strength/password-strength.a2ui.json +152 -0
- package/components/password-strength/password-strength.class.js +157 -0
- package/components/password-strength/password-strength.css +80 -0
- package/components/password-strength/password-strength.d.ts +59 -0
- package/components/password-strength/password-strength.js +17 -0
- package/components/password-strength/password-strength.yaml +153 -0
- package/components/popover/popover.css +43 -23
- package/components/popover/popover.yaml +8 -4
- package/components/qr-code/QR-TEST.svg +4 -0
- package/components/qr-code/qr-code.a2ui.json +154 -0
- package/components/qr-code/qr-code.class.js +129 -0
- package/components/qr-code/qr-code.css +41 -0
- package/components/qr-code/qr-code.d.ts +83 -0
- package/components/qr-code/qr-code.js +17 -0
- package/components/qr-code/qr-code.yaml +203 -0
- package/components/qr-code/qr-encoder.js +633 -0
- package/components/relative-time/relative-time.a2ui.json +120 -0
- package/components/relative-time/relative-time.class.js +136 -0
- package/components/relative-time/relative-time.css +22 -0
- package/components/relative-time/relative-time.d.ts +51 -0
- package/components/relative-time/relative-time.js +17 -0
- package/components/relative-time/relative-time.yaml +133 -0
- package/components/segmented/segmented.class.js +15 -3
- package/components/select/select.a2ui.json +3 -0
- package/components/select/select.class.js +4 -0
- package/components/select/select.yaml +5 -0
- package/components/skip-nav/skip-nav.a2ui.json +92 -0
- package/components/skip-nav/skip-nav.class.js +45 -0
- package/components/skip-nav/skip-nav.css +54 -0
- package/components/skip-nav/skip-nav.d.ts +27 -0
- package/components/skip-nav/skip-nav.js +12 -0
- package/components/skip-nav/skip-nav.yaml +68 -0
- package/components/slider/slider.a2ui.json +22 -1
- package/components/slider/slider.class.js +264 -122
- package/components/slider/slider.css +82 -2
- package/components/slider/slider.d.ts +19 -3
- package/components/slider/slider.test.js +55 -0
- package/components/slider/slider.yaml +38 -6
- package/components/stat/stat.css +18 -14
- package/components/stepper/stepper-item.a2ui.json +3 -0
- package/components/stepper/stepper-item.yaml +5 -0
- package/components/table/table.class.js +29 -6
- package/components/table/table.css +31 -4
- package/components/table-toolbar/table-toolbar.class.js +3 -1
- package/components/tag/tag.a2ui.json +3 -2
- package/components/tag/tag.css +35 -11
- package/components/tag/tag.d.ts +14 -0
- package/components/tag/tag.test.js +35 -11
- package/components/tag/tag.yaml +13 -7
- package/components/timeline/timeline-item.a2ui.json +8 -1
- package/components/timeline/timeline-item.yaml +12 -0
- package/components/toast/toast.class.js +12 -4
- package/components/toc/toc.a2ui.json +159 -0
- package/components/toc/toc.class.js +222 -0
- package/components/toc/toc.css +92 -0
- package/components/toc/toc.d.ts +61 -0
- package/components/toc/toc.js +17 -0
- package/components/toc/toc.yaml +180 -0
- package/components/toolbar/toolbar.class.js +3 -0
- package/components/tree/tree-item.a2ui.json +5 -1
- package/components/tree/tree-item.yaml +7 -0
- package/components/tree/tree.a2ui.json +3 -0
- package/components/tree/tree.yaml +5 -0
- package/components/visually-hidden/visually-hidden.a2ui.json +71 -0
- package/components/visually-hidden/visually-hidden.class.js +14 -0
- package/components/visually-hidden/visually-hidden.css +25 -0
- package/components/visually-hidden/visually-hidden.d.ts +26 -0
- package/components/visually-hidden/visually-hidden.js +12 -0
- package/components/visually-hidden/visually-hidden.yaml +54 -0
- package/core/anchor.js +19 -3
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +100 -89
- package/package.json +1 -1
- package/styles/colors/semantics.css +11 -2
- package/styles/components.css +11 -0
- package/styles/resets.css +10 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/RelativeTime.json",
|
|
4
|
+
"title": "RelativeTime",
|
|
5
|
+
"description": "Display a timestamp as a human-readable relative time — \"3 hours ago\",\n\"in 2 days\", \"yesterday\". Wraps `Intl.RelativeTimeFormat` for i18n\n+ auto-updates on a configurable interval (default every 60 s) so\nrendered text stays current without re-mounting.\n\nSet `[datetime]` to an ISO 8601 timestamp; the element computes the\ndelta against `Date.now()` and renders the formatted string. Pair\nwith a `title` for full-precision-on-hover (the element sets one\nautomatically from a default locale-aware long format unless you\noverride).\n",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"component": {
|
|
17
|
+
"const": "RelativeTime"
|
|
18
|
+
},
|
|
19
|
+
"datetime": {
|
|
20
|
+
"description": "ISO 8601 timestamp (e.g. `2026-05-25T09:30:00Z`) or any string\nparseable by `Date`. Required for output — empty value renders\nnothing.\n",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"default": ""
|
|
23
|
+
},
|
|
24
|
+
"locale": {
|
|
25
|
+
"description": "BCP-47 locale tag for the formatter. Empty defaults to the document\nlocale (`<html lang>`) then to the browser default.\n",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"default": ""
|
|
28
|
+
},
|
|
29
|
+
"numeric": {
|
|
30
|
+
"description": "`Intl.RelativeTimeFormat` `numeric` option. `auto` lets the\nformatter substitute \"yesterday\" / \"tomorrow\" for ±1 day deltas;\n`always` keeps the numeric form (\"1 day ago\"). Defaults to `auto`.\n",
|
|
31
|
+
"type": "string",
|
|
32
|
+
"enum": [
|
|
33
|
+
"auto",
|
|
34
|
+
"always"
|
|
35
|
+
],
|
|
36
|
+
"default": "auto"
|
|
37
|
+
},
|
|
38
|
+
"timeStyle": {
|
|
39
|
+
"description": "`Intl.RelativeTimeFormat` `style` option. `long` = \"3 hours ago\",\n`short` = \"3 hr. ago\", `narrow` = \"3h ago\". Defaults to `long`.\n",
|
|
40
|
+
"type": "string",
|
|
41
|
+
"enum": [
|
|
42
|
+
"long",
|
|
43
|
+
"short",
|
|
44
|
+
"narrow"
|
|
45
|
+
],
|
|
46
|
+
"default": "long"
|
|
47
|
+
},
|
|
48
|
+
"updateInterval": {
|
|
49
|
+
"description": "Auto-refresh interval in seconds. The component restarts a\n`setInterval` tick that re-reads `Date.now()` and re-renders the\nrelative string. Set to `0` to disable auto-update (static\nrender). Default `60` (one minute) is the natural cadence for\n\"minutes ago\" granularity.\n",
|
|
50
|
+
"type": "number",
|
|
51
|
+
"default": 60
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"required": [
|
|
55
|
+
"component"
|
|
56
|
+
],
|
|
57
|
+
"unevaluatedProperties": false,
|
|
58
|
+
"x-adiaui": {
|
|
59
|
+
"anti_patterns": [],
|
|
60
|
+
"category": "display",
|
|
61
|
+
"composes": [],
|
|
62
|
+
"events": {},
|
|
63
|
+
"examples": [
|
|
64
|
+
{
|
|
65
|
+
"description": "A timestamp rendered as a long relative phrase, auto-updating every 60 s.",
|
|
66
|
+
"a2ui": "[\n {\n \"id\": \"ts\",\n \"component\": \"RelativeTime\",\n \"datetime\": \"2026-05-25T09:00:00Z\"\n }\n]\n",
|
|
67
|
+
"name": "default"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"description": "Tight UI affordances — narrow form for chat bubble timestamps.",
|
|
71
|
+
"a2ui": "[\n {\n \"id\": \"ts\",\n \"component\": \"RelativeTime\",\n \"datetime\": \"2026-05-25T11:30:00Z\",\n \"timeStyle\": \"narrow\"\n }\n]\n",
|
|
72
|
+
"name": "short-narrow"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"description": "A historical timestamp that doesn't need to tick (commit log row).",
|
|
76
|
+
"a2ui": "[\n {\n \"id\": \"ts\",\n \"component\": \"RelativeTime\",\n \"datetime\": \"2025-11-01T00:00:00Z\",\n \"updateInterval\": 0\n }\n]\n",
|
|
77
|
+
"name": "static-historical"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"keywords": [
|
|
81
|
+
"relative-time",
|
|
82
|
+
"timeago",
|
|
83
|
+
"time",
|
|
84
|
+
"timestamp",
|
|
85
|
+
"ago",
|
|
86
|
+
"elapsed",
|
|
87
|
+
"relative",
|
|
88
|
+
"duration"
|
|
89
|
+
],
|
|
90
|
+
"name": "UIRelativeTime",
|
|
91
|
+
"related": [
|
|
92
|
+
"text",
|
|
93
|
+
"badge",
|
|
94
|
+
"stat"
|
|
95
|
+
],
|
|
96
|
+
"slots": {},
|
|
97
|
+
"states": [
|
|
98
|
+
{
|
|
99
|
+
"description": "Default, displaying the formatted relative time.",
|
|
100
|
+
"name": "idle"
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"status": "stable",
|
|
104
|
+
"synonyms": {
|
|
105
|
+
"ago": [
|
|
106
|
+
"relative-time",
|
|
107
|
+
"timeago"
|
|
108
|
+
],
|
|
109
|
+
"timestamp": [
|
|
110
|
+
"relative-time",
|
|
111
|
+
"time",
|
|
112
|
+
"datetime"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
"tag": "relative-time-ui",
|
|
116
|
+
"tokens": {},
|
|
117
|
+
"traits": [],
|
|
118
|
+
"version": 1
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-side-effect class export for `<relative-time-ui>`.
|
|
3
|
+
*
|
|
4
|
+
* Importing this file gives you the class without auto-registering the
|
|
5
|
+
* tag. Useful for test isolation, subclassing with tag-name override,
|
|
6
|
+
* or selective composition.
|
|
7
|
+
*
|
|
8
|
+
* The auto-register path stays at `@adia-ai/web-components/components/relative-time`
|
|
9
|
+
* (which imports this file + calls `defineIfFree()`).
|
|
10
|
+
*
|
|
11
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* <relative-time-ui datetime="2026-05-25T09:00:00Z"></relative-time-ui>
|
|
16
|
+
*
|
|
17
|
+
* Renders a timestamp as a human-readable relative phrase
|
|
18
|
+
* ("3 hours ago", "in 2 days", "yesterday") using
|
|
19
|
+
* `Intl.RelativeTimeFormat`. Re-renders on a configurable tick
|
|
20
|
+
* so the displayed text stays current.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { UIElement } from '../../core/element.js';
|
|
24
|
+
|
|
25
|
+
// Threshold table — each row: { limit (seconds), unit, divisor }.
|
|
26
|
+
// First row whose absolute delta is ≤ limit wins.
|
|
27
|
+
const THRESHOLDS = [
|
|
28
|
+
{ limit: 60, unit: 'second', divisor: 1 },
|
|
29
|
+
{ limit: 60 * 60, unit: 'minute', divisor: 60 },
|
|
30
|
+
{ limit: 60 * 60 * 24, unit: 'hour', divisor: 60 * 60 },
|
|
31
|
+
{ limit: 60 * 60 * 24 * 7, unit: 'day', divisor: 60 * 60 * 24 },
|
|
32
|
+
{ limit: 60 * 60 * 24 * 30, unit: 'week', divisor: 60 * 60 * 24 * 7 },
|
|
33
|
+
{ limit: 60 * 60 * 24 * 365, unit: 'month', divisor: 60 * 60 * 24 * 30 },
|
|
34
|
+
{ limit: Infinity, unit: 'year', divisor: 60 * 60 * 24 * 365 },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export class UIRelativeTime extends UIElement {
|
|
38
|
+
static properties = {
|
|
39
|
+
datetime: { type: String, default: '', reflect: true },
|
|
40
|
+
timeStyle: { type: String, default: 'long', reflect: true, attribute: 'time-style' },
|
|
41
|
+
numeric: { type: String, default: 'auto', reflect: true },
|
|
42
|
+
locale: { type: String, default: '', reflect: true },
|
|
43
|
+
updateInterval: { type: Number, default: 60, reflect: true, attribute: 'update-interval' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
static template = () => null;
|
|
47
|
+
|
|
48
|
+
#tickHandle = null;
|
|
49
|
+
|
|
50
|
+
#resolveLocale() {
|
|
51
|
+
if (this.locale) return this.locale;
|
|
52
|
+
return this.ownerDocument?.documentElement?.lang || undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#format() {
|
|
56
|
+
if (!this.datetime) return '';
|
|
57
|
+
const target = new Date(this.datetime);
|
|
58
|
+
if (Number.isNaN(target.getTime())) return '';
|
|
59
|
+
const deltaSec = (target.getTime() - Date.now()) / 1000;
|
|
60
|
+
const abs = Math.abs(deltaSec);
|
|
61
|
+
const row = THRESHOLDS.find((t) => abs <= t.limit) || THRESHOLDS[THRESHOLDS.length - 1];
|
|
62
|
+
const value = Math.round(deltaSec / row.divisor);
|
|
63
|
+
try {
|
|
64
|
+
const fmt = new Intl.RelativeTimeFormat(this.#resolveLocale(), {
|
|
65
|
+
style: this.timeStyle,
|
|
66
|
+
numeric: this.numeric,
|
|
67
|
+
});
|
|
68
|
+
return fmt.format(value, row.unit);
|
|
69
|
+
} catch {
|
|
70
|
+
// Fallback if Intl.RelativeTimeFormat unsupported (very old browsers).
|
|
71
|
+
const past = deltaSec < 0;
|
|
72
|
+
const absVal = Math.abs(value);
|
|
73
|
+
return past ? `${absVal} ${row.unit}${absVal === 1 ? '' : 's'} ago` : `in ${absVal} ${row.unit}${absVal === 1 ? '' : 's'}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#fullTitle() {
|
|
78
|
+
if (!this.datetime) return '';
|
|
79
|
+
const target = new Date(this.datetime);
|
|
80
|
+
if (Number.isNaN(target.getTime())) return '';
|
|
81
|
+
try {
|
|
82
|
+
return new Intl.DateTimeFormat(this.#resolveLocale(), {
|
|
83
|
+
dateStyle: 'full',
|
|
84
|
+
timeStyle: 'long',
|
|
85
|
+
}).format(target);
|
|
86
|
+
} catch {
|
|
87
|
+
return target.toString();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
connected() {
|
|
92
|
+
super.connected();
|
|
93
|
+
if (!this.hasAttribute('role')) this.setAttribute('role', 'time');
|
|
94
|
+
this.#startTick();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render() {
|
|
98
|
+
const text = this.#format();
|
|
99
|
+
this.textContent = text;
|
|
100
|
+
// Set a hover-title with the full datetime for precision-on-hover
|
|
101
|
+
// unless the consumer set their own title attribute.
|
|
102
|
+
if (text && !this.hasAttribute('data-suppress-title')) {
|
|
103
|
+
this.setAttribute('title', this.#fullTitle());
|
|
104
|
+
}
|
|
105
|
+
// Mirror the timestamp to a `datetime` attribute on a child <time>
|
|
106
|
+
// would be ideal but the element IS the time; expose via aria.
|
|
107
|
+
if (this.datetime) {
|
|
108
|
+
this.setAttribute('datetime', this.datetime);
|
|
109
|
+
}
|
|
110
|
+
// Restart tick if updateInterval changed.
|
|
111
|
+
this.#startTick();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#startTick() {
|
|
115
|
+
this.#stopTick();
|
|
116
|
+
const interval = Number(this.updateInterval);
|
|
117
|
+
if (!Number.isFinite(interval) || interval <= 0) return;
|
|
118
|
+
this.#tickHandle = setInterval(() => {
|
|
119
|
+
// Re-render by re-running the formatter; signal-backed render() is
|
|
120
|
+
// overkill since only Date.now() changed.
|
|
121
|
+
this.textContent = this.#format();
|
|
122
|
+
}, interval * 1000);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#stopTick() {
|
|
126
|
+
if (this.#tickHandle != null) {
|
|
127
|
+
clearInterval(this.#tickHandle);
|
|
128
|
+
this.#tickHandle = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
disconnected() {
|
|
133
|
+
super.disconnected();
|
|
134
|
+
this.#stopTick();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
RELATIVE-TIME-UI — Auto-updating relative timestamp display.
|
|
3
|
+
═══════════════════════════════════════════════════════════════ */
|
|
4
|
+
|
|
5
|
+
@scope (relative-time-ui) {
|
|
6
|
+
:where(:scope) {
|
|
7
|
+
--relative-time-fg-default: var(--a-fg-muted);
|
|
8
|
+
--relative-time-font-default: inherit;
|
|
9
|
+
--relative-time-size-default: inherit;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
:scope {
|
|
13
|
+
/* Inline display so it sits inline with prose / inside a row.
|
|
14
|
+
Overridable via the standard size-attr CSS or by setting display
|
|
15
|
+
directly. */
|
|
16
|
+
display: inline;
|
|
17
|
+
color: var(--relative-time-fg, var(--relative-time-fg-default));
|
|
18
|
+
font-family: var(--relative-time-font, var(--relative-time-font-default));
|
|
19
|
+
font-size: var(--relative-time-size, var(--relative-time-size-default));
|
|
20
|
+
font-variant-numeric: tabular-nums;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<relative-time-ui>` — Display a timestamp as a human-readable relative time — "3 hours ago",
|
|
3
|
+
"in 2 days", "yesterday". Wraps `Intl.RelativeTimeFormat` for i18n
|
|
4
|
+
+ auto-updates on a configurable interval (default every 60 s) so
|
|
5
|
+
rendered text stays current without re-mounting.
|
|
6
|
+
|
|
7
|
+
Set `[datetime]` to an ISO 8601 timestamp; the element computes the
|
|
8
|
+
delta against `Date.now()` and renders the formatted string. Pair
|
|
9
|
+
with a `title` for full-precision-on-hover (the element sets one
|
|
10
|
+
automatically from a default locale-aware long format unless you
|
|
11
|
+
override).
|
|
12
|
+
|
|
13
|
+
*
|
|
14
|
+
* @see https://ui-kit.exe.xyz/site/components/relative-time
|
|
15
|
+
*
|
|
16
|
+
* Type declarations generated by scripts/build/dts-codegen.mjs from
|
|
17
|
+
* the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
|
|
18
|
+
* run `npm run build:components`, then `npm run codegen:dts` to
|
|
19
|
+
* regenerate; or hand-author this file fully if rich event types are
|
|
20
|
+
* needed beyond what the yaml `events:` block can express.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { UIElement } from '../../core/element.js';
|
|
24
|
+
|
|
25
|
+
export class UIRelativeTime extends UIElement {
|
|
26
|
+
/** ISO 8601 timestamp (e.g. `2026-05-25T09:30:00Z`) or any string
|
|
27
|
+
parseable by `Date`. Required for output — empty value renders
|
|
28
|
+
nothing.
|
|
29
|
+
*/
|
|
30
|
+
datetime: string;
|
|
31
|
+
/** BCP-47 locale tag for the formatter. Empty defaults to the document
|
|
32
|
+
locale (`<html lang>`) then to the browser default.
|
|
33
|
+
*/
|
|
34
|
+
locale: string;
|
|
35
|
+
/** `Intl.RelativeTimeFormat` `numeric` option. `auto` lets the
|
|
36
|
+
formatter substitute "yesterday" / "tomorrow" for ±1 day deltas;
|
|
37
|
+
`always` keeps the numeric form ("1 day ago"). Defaults to `auto`.
|
|
38
|
+
*/
|
|
39
|
+
numeric: 'auto' | 'always';
|
|
40
|
+
/** `Intl.RelativeTimeFormat` `style` option. `long` = "3 hours ago",
|
|
41
|
+
`short` = "3 hr. ago", `narrow` = "3h ago". Defaults to `long`.
|
|
42
|
+
*/
|
|
43
|
+
timeStyle: 'long' | 'short' | 'narrow';
|
|
44
|
+
/** Auto-refresh interval in seconds. The component restarts a
|
|
45
|
+
`setInterval` tick that re-reads `Date.now()` and re-renders the
|
|
46
|
+
relative string. Set to `0` to disable auto-update (static
|
|
47
|
+
render). Default `60` (one minute) is the natural cadence for
|
|
48
|
+
"minutes ago" granularity.
|
|
49
|
+
*/
|
|
50
|
+
updateInterval: number;
|
|
51
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<relative-time-ui>` — auto-registers the tag on import.
|
|
3
|
+
*
|
|
4
|
+
* For non-side-effect class import (test isolation, tag override), use
|
|
5
|
+
* the `class` subpath:
|
|
6
|
+
*
|
|
7
|
+
* import { UIRelativeTime } from '@adia-ai/web-components/components/relative-time/class';
|
|
8
|
+
*
|
|
9
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { defineIfFree } from '../../core/register.js';
|
|
13
|
+
import { UIRelativeTime } from './relative-time.class.js';
|
|
14
|
+
|
|
15
|
+
defineIfFree('relative-time-ui', UIRelativeTime);
|
|
16
|
+
|
|
17
|
+
export { UIRelativeTime };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
2
|
+
name: UIRelativeTime
|
|
3
|
+
tag: relative-time-ui
|
|
4
|
+
status: stable
|
|
5
|
+
component: RelativeTime
|
|
6
|
+
category: display
|
|
7
|
+
version: 1
|
|
8
|
+
description: |
|
|
9
|
+
Display a timestamp as a human-readable relative time — "3 hours ago",
|
|
10
|
+
"in 2 days", "yesterday". Wraps `Intl.RelativeTimeFormat` for i18n
|
|
11
|
+
+ auto-updates on a configurable interval (default every 60 s) so
|
|
12
|
+
rendered text stays current without re-mounting.
|
|
13
|
+
|
|
14
|
+
Set `[datetime]` to an ISO 8601 timestamp; the element computes the
|
|
15
|
+
delta against `Date.now()` and renders the formatted string. Pair
|
|
16
|
+
with a `title` for full-precision-on-hover (the element sets one
|
|
17
|
+
automatically from a default locale-aware long format unless you
|
|
18
|
+
override).
|
|
19
|
+
props:
|
|
20
|
+
datetime:
|
|
21
|
+
description: |
|
|
22
|
+
ISO 8601 timestamp (e.g. `2026-05-25T09:30:00Z`) or any string
|
|
23
|
+
parseable by `Date`. Required for output — empty value renders
|
|
24
|
+
nothing.
|
|
25
|
+
type: string
|
|
26
|
+
default: ""
|
|
27
|
+
reflect: true
|
|
28
|
+
timeStyle:
|
|
29
|
+
description: |
|
|
30
|
+
`Intl.RelativeTimeFormat` `style` option. `long` = "3 hours ago",
|
|
31
|
+
`short` = "3 hr. ago", `narrow` = "3h ago". Defaults to `long`.
|
|
32
|
+
type: string
|
|
33
|
+
default: long
|
|
34
|
+
enum: [long, short, narrow]
|
|
35
|
+
reflect: true
|
|
36
|
+
attribute: time-style
|
|
37
|
+
numeric:
|
|
38
|
+
description: |
|
|
39
|
+
`Intl.RelativeTimeFormat` `numeric` option. `auto` lets the
|
|
40
|
+
formatter substitute "yesterday" / "tomorrow" for ±1 day deltas;
|
|
41
|
+
`always` keeps the numeric form ("1 day ago"). Defaults to `auto`.
|
|
42
|
+
type: string
|
|
43
|
+
default: auto
|
|
44
|
+
enum: [auto, always]
|
|
45
|
+
reflect: true
|
|
46
|
+
locale:
|
|
47
|
+
description: |
|
|
48
|
+
BCP-47 locale tag for the formatter. Empty defaults to the document
|
|
49
|
+
locale (`<html lang>`) then to the browser default.
|
|
50
|
+
type: string
|
|
51
|
+
default: ""
|
|
52
|
+
reflect: true
|
|
53
|
+
updateInterval:
|
|
54
|
+
description: |
|
|
55
|
+
Auto-refresh interval in seconds. The component restarts a
|
|
56
|
+
`setInterval` tick that re-reads `Date.now()` and re-renders the
|
|
57
|
+
relative string. Set to `0` to disable auto-update (static
|
|
58
|
+
render). Default `60` (one minute) is the natural cadence for
|
|
59
|
+
"minutes ago" granularity.
|
|
60
|
+
type: number
|
|
61
|
+
default: 60
|
|
62
|
+
reflect: true
|
|
63
|
+
attribute: update-interval
|
|
64
|
+
events: {}
|
|
65
|
+
slots: {}
|
|
66
|
+
states:
|
|
67
|
+
- name: idle
|
|
68
|
+
description: Default, displaying the formatted relative time.
|
|
69
|
+
traits: []
|
|
70
|
+
tokens: {}
|
|
71
|
+
a2ui:
|
|
72
|
+
rules:
|
|
73
|
+
- rule: "Use for displaying a single timestamp as a relative phrase. Self-updates on a tick so the rendered text stays current; no parent re-render required."
|
|
74
|
+
reason: "Self-contained relative-time display."
|
|
75
|
+
- rule: "Set [datetime] to an ISO 8601 string. Empty datetime renders nothing — do not stamp a relative-time-ui element until you have a timestamp value."
|
|
76
|
+
reason: "Datetime contract."
|
|
77
|
+
- rule: "[update-interval=0] freezes the render (no tick). Use for historical timestamps that will never become 'just now' (audit-log rows, version-history entries from days+ ago)."
|
|
78
|
+
reason: "Performance — skip the tick where it's pointless."
|
|
79
|
+
anti_patterns: []
|
|
80
|
+
examples:
|
|
81
|
+
- name: default
|
|
82
|
+
description: A timestamp rendered as a long relative phrase, auto-updating every 60 s.
|
|
83
|
+
a2ui: |
|
|
84
|
+
[
|
|
85
|
+
{
|
|
86
|
+
"id": "ts",
|
|
87
|
+
"component": "RelativeTime",
|
|
88
|
+
"datetime": "2026-05-25T09:00:00Z"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
- name: short-narrow
|
|
92
|
+
description: Tight UI affordances — narrow form for chat bubble timestamps.
|
|
93
|
+
a2ui: |
|
|
94
|
+
[
|
|
95
|
+
{
|
|
96
|
+
"id": "ts",
|
|
97
|
+
"component": "RelativeTime",
|
|
98
|
+
"datetime": "2026-05-25T11:30:00Z",
|
|
99
|
+
"timeStyle": "narrow"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
- name: static-historical
|
|
103
|
+
description: A historical timestamp that doesn't need to tick (commit log row).
|
|
104
|
+
a2ui: |
|
|
105
|
+
[
|
|
106
|
+
{
|
|
107
|
+
"id": "ts",
|
|
108
|
+
"component": "RelativeTime",
|
|
109
|
+
"datetime": "2025-11-01T00:00:00Z",
|
|
110
|
+
"updateInterval": 0
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
keywords:
|
|
114
|
+
- relative-time
|
|
115
|
+
- timeago
|
|
116
|
+
- time
|
|
117
|
+
- timestamp
|
|
118
|
+
- ago
|
|
119
|
+
- elapsed
|
|
120
|
+
- relative
|
|
121
|
+
- duration
|
|
122
|
+
synonyms:
|
|
123
|
+
timestamp:
|
|
124
|
+
- relative-time
|
|
125
|
+
- time
|
|
126
|
+
- datetime
|
|
127
|
+
ago:
|
|
128
|
+
- relative-time
|
|
129
|
+
- timeago
|
|
130
|
+
related:
|
|
131
|
+
- text
|
|
132
|
+
- badge
|
|
133
|
+
- stat
|
|
@@ -107,7 +107,11 @@ export class UISegmented extends UIFormElement {
|
|
|
107
107
|
// Runs before the empty-guard so an all-bare-<segment> group still warns.
|
|
108
108
|
if (!UISegmented.#warnedNonSegment.has(this)) {
|
|
109
109
|
const bad = [...this.children].find(
|
|
110
|
-
(c) => c.tagName !== 'SEGMENT-UI'
|
|
110
|
+
(c) => c.tagName !== 'SEGMENT-UI'
|
|
111
|
+
&& !c.hasAttribute('data-indicator')
|
|
112
|
+
// Template engine wraps .map() results in <span style="display:contents">.
|
|
113
|
+
// These are transparent DOM housekeeping nodes — not consumer mistakes.
|
|
114
|
+
&& !(c.tagName === 'SPAN' && c.style.display === 'contents'),
|
|
111
115
|
);
|
|
112
116
|
if (bad) {
|
|
113
117
|
UISegmented.#warnedNonSegment.add(this);
|
|
@@ -148,9 +152,17 @@ export class UISegmented extends UIFormElement {
|
|
|
148
152
|
return;
|
|
149
153
|
}
|
|
150
154
|
|
|
151
|
-
//
|
|
152
|
-
|
|
155
|
+
// Indicator — there must be EXACTLY ONE [data-indicator] child at all
|
|
156
|
+
// times. If the instance ref is missing OR detached, strip every stale
|
|
157
|
+
// indicator from the DOM (defensive: re-renders after disconnect/
|
|
158
|
+
// reconnect cycles, HMR, or stamp() can leave the field nulled while
|
|
159
|
+
// the previous indicator span persists in DOM → multiple translateX'd
|
|
160
|
+
// pills paint simultaneously). Then create a fresh one.
|
|
161
|
+
const isNew = !this.#indicator || !this.contains(this.#indicator);
|
|
153
162
|
if (isNew) {
|
|
163
|
+
for (const stale of this.querySelectorAll(':scope > [data-indicator]')) {
|
|
164
|
+
stale.remove();
|
|
165
|
+
}
|
|
154
166
|
this.#indicator = document.createElement('span');
|
|
155
167
|
this.#indicator.setAttribute('data-indicator', '');
|
|
156
168
|
this.prepend(this.#indicator);
|
|
@@ -220,6 +220,9 @@
|
|
|
220
220
|
"upload"
|
|
221
221
|
],
|
|
222
222
|
"slots": {
|
|
223
|
+
"hint": {
|
|
224
|
+
"description": "Override slot for hint markup richer than the plain [hint] attribute string (inline links, code spans). Renders beneath the trigger at body-subtle typography. Mutually exclusive with [hint]."
|
|
225
|
+
},
|
|
223
226
|
"label": {
|
|
224
227
|
"description": "Label element above the trigger"
|
|
225
228
|
},
|
|
@@ -40,6 +40,10 @@ export class UISelect extends UIFormElement {
|
|
|
40
40
|
static properties = {
|
|
41
41
|
...UIFormElement.properties,
|
|
42
42
|
placeholder: { type: String, default: 'Select...', reflect: true },
|
|
43
|
+
// Universal [size] system — sm/md/lg → 24/30/36 px (with density).
|
|
44
|
+
// yaml documented this as reflect:true since v1; closing the
|
|
45
|
+
// static-properties gap so el.size = 'lg' actually updates.
|
|
46
|
+
size: { type: String, default: 'md', reflect: true },
|
|
43
47
|
open: { type: Boolean, default: false, reflect: true },
|
|
44
48
|
label: { type: String, default: '', reflect: true },
|
|
45
49
|
icon: { type: String, default: '', reflect: true },
|
|
@@ -220,6 +220,11 @@ slots:
|
|
|
220
220
|
description: Popover container for options (role=listbox)
|
|
221
221
|
trigger:
|
|
222
222
|
description: Button that opens the listbox
|
|
223
|
+
hint:
|
|
224
|
+
description: >-
|
|
225
|
+
Override slot for hint markup richer than the plain [hint] attribute
|
|
226
|
+
string (inline links, code spans). Renders beneath the trigger at
|
|
227
|
+
body-subtle typography. Mutually exclusive with [hint].
|
|
223
228
|
states:
|
|
224
229
|
- name: idle
|
|
225
230
|
description: Default, ready for interaction.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/SkipNav.json",
|
|
4
|
+
"title": "SkipNav",
|
|
5
|
+
"description": "Accessibility utility — visually-hidden link that becomes visible on\nkeyboard focus, letting keyboard users skip past repetitive navigation\nto the main content. WCAG 2.1 Success Criterion 2.4.1 \"Bypass Blocks\".\n\nPlace as the FIRST focusable element on the page (typically inside\n`<body>` before any nav / shell chrome). Target an `id` on your main\ncontent region (commonly `#main` / `#main-content`).\n",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"component": {
|
|
17
|
+
"const": "SkipNav"
|
|
18
|
+
},
|
|
19
|
+
"target": {
|
|
20
|
+
"description": "CSS id (with leading `#`) of the main content region the link skips to.",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"default": "#main"
|
|
23
|
+
},
|
|
24
|
+
"text": {
|
|
25
|
+
"description": "Link label. Defaults to \"Skip to main content\"; localize per site.",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"default": "Skip to main content"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"required": [
|
|
31
|
+
"component"
|
|
32
|
+
],
|
|
33
|
+
"unevaluatedProperties": false,
|
|
34
|
+
"x-adiaui": {
|
|
35
|
+
"anti_patterns": [
|
|
36
|
+
{
|
|
37
|
+
"fix": "<body><skip-nav-ui></skip-nav-ui><header>…nav…</header><main id=\"main\" tabindex=\"-1\">…</main></body>",
|
|
38
|
+
"why": "The skip link comes AFTER the nav — keyboard users have already tabbed through everything you wanted them to skip.",
|
|
39
|
+
"wrong": "<header>…nav…</header><skip-nav-ui></skip-nav-ui><main id=\"main\">…</main>"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"category": "utility",
|
|
43
|
+
"composes": [],
|
|
44
|
+
"events": {},
|
|
45
|
+
"examples": [
|
|
46
|
+
{
|
|
47
|
+
"description": "Standard skip-nav at the top of the page.",
|
|
48
|
+
"a2ui": "[\n { \"id\": \"skip\", \"component\": \"SkipNav\", \"target\": \"#main\" }\n]\n",
|
|
49
|
+
"name": "default"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"keywords": [
|
|
53
|
+
"skip-nav",
|
|
54
|
+
"skip-link",
|
|
55
|
+
"bypass-blocks",
|
|
56
|
+
"a11y",
|
|
57
|
+
"accessibility",
|
|
58
|
+
"keyboard-navigation"
|
|
59
|
+
],
|
|
60
|
+
"name": "UISkipNav",
|
|
61
|
+
"related": [
|
|
62
|
+
"visually-hidden"
|
|
63
|
+
],
|
|
64
|
+
"slots": {},
|
|
65
|
+
"states": [
|
|
66
|
+
{
|
|
67
|
+
"description": "Default — link is visually hidden but focusable.",
|
|
68
|
+
"name": "idle"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"description": "Link is focused via keyboard and visible at the top of the viewport.",
|
|
72
|
+
"attribute": ":focus-within",
|
|
73
|
+
"name": "focused"
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"status": "stable",
|
|
77
|
+
"synonyms": {},
|
|
78
|
+
"tag": "skip-nav-ui",
|
|
79
|
+
"tokens": {
|
|
80
|
+
"--skip-nav-bg": {
|
|
81
|
+
"description": "Background of the visible (focused) skip link.",
|
|
82
|
+
"default": "var(--a-accent-bg)"
|
|
83
|
+
},
|
|
84
|
+
"--skip-nav-fg": {
|
|
85
|
+
"description": "Foreground of the visible (focused) skip link.",
|
|
86
|
+
"default": "var(--a-accent-fg)"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"traits": [],
|
|
90
|
+
"version": 1
|
|
91
|
+
}
|
|
92
|
+
}
|