@adia-ai/web-components 0.6.36 → 0.6.37

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.
Files changed (115) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/components/badge/badge.a2ui.json +10 -0
  3. package/components/badge/badge.css +70 -0
  4. package/components/badge/badge.yaml +20 -0
  5. package/components/blockquote/blockquote.a2ui.json +121 -0
  6. package/components/blockquote/blockquote.class.js +68 -0
  7. package/components/blockquote/blockquote.css +46 -0
  8. package/components/blockquote/blockquote.d.ts +31 -0
  9. package/components/blockquote/blockquote.js +17 -0
  10. package/components/blockquote/blockquote.yaml +124 -0
  11. package/components/button/button.css +11 -3
  12. package/components/calendar-picker/calendar-picker.a2ui.json +15 -0
  13. package/components/calendar-picker/calendar-picker.class.js +7 -1
  14. package/components/calendar-picker/calendar-picker.yaml +14 -0
  15. package/components/color-input/color-input.a2ui.json +2 -2
  16. package/components/color-input/color-input.class.js +9 -2
  17. package/components/color-input/color-input.yaml +2 -2
  18. package/components/combobox/combobox.class.js +4 -0
  19. package/components/context-menu/context-menu.a2ui.json +159 -0
  20. package/components/context-menu/context-menu.class.js +275 -0
  21. package/components/context-menu/context-menu.css +56 -0
  22. package/components/context-menu/context-menu.d.ts +70 -0
  23. package/components/context-menu/context-menu.js +17 -0
  24. package/components/context-menu/context-menu.yaml +136 -0
  25. package/components/date-range-picker/date-range-picker.a2ui.json +15 -0
  26. package/components/date-range-picker/date-range-picker.class.js +2 -0
  27. package/components/date-range-picker/date-range-picker.yaml +14 -0
  28. package/components/datetime-picker/datetime-picker.a2ui.json +15 -0
  29. package/components/datetime-picker/datetime-picker.class.js +3 -1
  30. package/components/datetime-picker/datetime-picker.d.ts +2 -0
  31. package/components/datetime-picker/datetime-picker.yaml +14 -0
  32. package/components/empty-state/empty-state.class.js +2 -0
  33. package/components/feed/feed.class.js +13 -5
  34. package/components/feed/feed.css +14 -0
  35. package/components/index.js +9 -0
  36. package/components/integration-card/integration-card.class.js +9 -0
  37. package/components/integration-card/integration-card.test.js +4 -3
  38. package/components/nav-group/nav-group.css +7 -1
  39. package/components/number-format/number-format.a2ui.json +180 -0
  40. package/components/number-format/number-format.class.js +96 -0
  41. package/components/number-format/number-format.css +18 -0
  42. package/components/number-format/number-format.d.ts +68 -0
  43. package/components/number-format/number-format.js +17 -0
  44. package/components/number-format/number-format.yaml +204 -0
  45. package/components/pagination/pagination.a2ui.json +19 -2
  46. package/components/pagination/pagination.class.js +90 -37
  47. package/components/pagination/pagination.css +32 -127
  48. package/components/pagination/pagination.d.ts +8 -2
  49. package/components/pagination/pagination.test.js +195 -0
  50. package/components/pagination/pagination.yaml +22 -1
  51. package/components/password-strength/password-strength.a2ui.json +152 -0
  52. package/components/password-strength/password-strength.class.js +157 -0
  53. package/components/password-strength/password-strength.css +80 -0
  54. package/components/password-strength/password-strength.d.ts +59 -0
  55. package/components/password-strength/password-strength.js +17 -0
  56. package/components/password-strength/password-strength.yaml +153 -0
  57. package/components/popover/popover.css +43 -23
  58. package/components/popover/popover.yaml +8 -4
  59. package/components/qr-code/QR-TEST.svg +4 -0
  60. package/components/qr-code/qr-code.a2ui.json +154 -0
  61. package/components/qr-code/qr-code.class.js +129 -0
  62. package/components/qr-code/qr-code.css +41 -0
  63. package/components/qr-code/qr-code.d.ts +83 -0
  64. package/components/qr-code/qr-code.js +17 -0
  65. package/components/qr-code/qr-code.yaml +203 -0
  66. package/components/qr-code/qr-encoder.js +633 -0
  67. package/components/relative-time/relative-time.a2ui.json +120 -0
  68. package/components/relative-time/relative-time.class.js +136 -0
  69. package/components/relative-time/relative-time.css +22 -0
  70. package/components/relative-time/relative-time.d.ts +51 -0
  71. package/components/relative-time/relative-time.js +17 -0
  72. package/components/relative-time/relative-time.yaml +133 -0
  73. package/components/segmented/segmented.class.js +5 -1
  74. package/components/select/select.class.js +4 -0
  75. package/components/skip-nav/skip-nav.a2ui.json +92 -0
  76. package/components/skip-nav/skip-nav.class.js +45 -0
  77. package/components/skip-nav/skip-nav.css +54 -0
  78. package/components/skip-nav/skip-nav.d.ts +27 -0
  79. package/components/skip-nav/skip-nav.js +12 -0
  80. package/components/skip-nav/skip-nav.yaml +68 -0
  81. package/components/slider/slider.a2ui.json +16 -1
  82. package/components/slider/slider.class.js +264 -122
  83. package/components/slider/slider.css +82 -2
  84. package/components/slider/slider.d.ts +19 -3
  85. package/components/slider/slider.test.js +55 -0
  86. package/components/slider/slider.yaml +28 -6
  87. package/components/table/table.class.js +29 -6
  88. package/components/table/table.css +31 -4
  89. package/components/table-toolbar/table-toolbar.class.js +3 -1
  90. package/components/tag/tag.a2ui.json +3 -2
  91. package/components/tag/tag.css +35 -11
  92. package/components/tag/tag.d.ts +14 -0
  93. package/components/tag/tag.test.js +35 -11
  94. package/components/tag/tag.yaml +13 -7
  95. package/components/toast/toast.class.js +12 -4
  96. package/components/toc/toc.a2ui.json +159 -0
  97. package/components/toc/toc.class.js +222 -0
  98. package/components/toc/toc.css +92 -0
  99. package/components/toc/toc.d.ts +61 -0
  100. package/components/toc/toc.js +17 -0
  101. package/components/toc/toc.yaml +180 -0
  102. package/components/toolbar/toolbar.class.js +3 -0
  103. package/components/visually-hidden/visually-hidden.a2ui.json +71 -0
  104. package/components/visually-hidden/visually-hidden.class.js +14 -0
  105. package/components/visually-hidden/visually-hidden.css +25 -0
  106. package/components/visually-hidden/visually-hidden.d.ts +26 -0
  107. package/components/visually-hidden/visually-hidden.js +12 -0
  108. package/components/visually-hidden/visually-hidden.yaml +54 -0
  109. package/core/anchor.js +19 -3
  110. package/dist/web-components.min.css +1 -1
  111. package/dist/web-components.min.js +100 -89
  112. package/package.json +1 -1
  113. package/styles/colors/semantics.css +11 -2
  114. package/styles/components.css +9 -0
  115. 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' && !c.hasAttribute('data-indicator'),
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);
@@ -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 },
@@ -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
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `<skip-nav-ui>` — keyboard skip-link for WCAG 2.4.1 (Bypass Blocks).
3
+ *
4
+ * Renders an <a href="[target]"> that's visually hidden until focused,
5
+ * then pops to the top-left of the viewport. On click, the browser
6
+ * navigates to the target hash AND we move focus programmatically (the
7
+ * native hash-jump doesn't always move focus; per spec it should land
8
+ * on the target so a screen-reader resumes there).
9
+ */
10
+
11
+ import { UIElement, html } from '../../core/element.js';
12
+
13
+ export class UISkipNav extends UIElement {
14
+ static properties = {
15
+ target: { type: String, default: '#main', reflect: true },
16
+ text: { type: String, default: 'Skip to main content', reflect: true },
17
+ };
18
+
19
+ static template = ({ target, text }) => html`<a href="${target}" data-skip-link>${text}</a>`;
20
+
21
+ connected() {
22
+ super.connected();
23
+ this.addEventListener('click', this.#onClick);
24
+ }
25
+
26
+ disconnected() {
27
+ super.disconnected();
28
+ this.removeEventListener('click', this.#onClick);
29
+ }
30
+
31
+ #onClick = (e) => {
32
+ const link = e.target.closest('[data-skip-link]');
33
+ if (!link) return;
34
+ const id = this.target.replace(/^#/, '');
35
+ const tgt = document.getElementById(id);
36
+ if (!tgt) return;
37
+ // Don't preventDefault — browser still updates the hash for the
38
+ // back-button. But we ALSO focus the target so AT cursor moves there
39
+ // (browser hash-jump scrolls but doesn't always move focus).
40
+ queueMicrotask(() => {
41
+ if (!tgt.hasAttribute('tabindex')) tgt.setAttribute('tabindex', '-1');
42
+ tgt.focus({ preventScroll: false });
43
+ });
44
+ };
45
+ }