@adia-ai/web-components 0.6.37 → 0.6.39

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 (73) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/components/accordion/accordion-item.a2ui.json +3 -0
  3. package/components/accordion/accordion-item.yaml +5 -0
  4. package/components/action-list/action-item.a2ui.json +5 -1
  5. package/components/action-list/action-item.yaml +7 -0
  6. package/components/card/card.a2ui.json +17 -1
  7. package/components/card/card.yaml +24 -1
  8. package/components/date-range-picker/date-range-picker.css +4 -4
  9. package/components/datetime-picker/datetime-picker.css +3 -3
  10. package/components/demo-toggle/demo-toggle.css +11 -11
  11. package/components/empty-state/empty-state.a2ui.json +9 -0
  12. package/components/empty-state/empty-state.yaml +15 -0
  13. package/components/feed/feed-item.a2ui.json +5 -0
  14. package/components/feed/feed-item.yaml +10 -0
  15. package/components/feed/feed.css +2 -2
  16. package/components/field/field.a2ui.json +6 -0
  17. package/components/field/field.css +18 -18
  18. package/components/field/field.yaml +10 -0
  19. package/components/heatmap/heatmap.css +1 -1
  20. package/components/index.js +3 -0
  21. package/components/inline-edit/inline-edit.a2ui.json +159 -0
  22. package/components/inline-edit/inline-edit.class.js +184 -0
  23. package/components/inline-edit/inline-edit.css +62 -0
  24. package/components/inline-edit/inline-edit.d.ts +52 -0
  25. package/components/inline-edit/inline-edit.js +12 -0
  26. package/components/inline-edit/inline-edit.yaml +125 -0
  27. package/components/inline-message/inline-message.css +1 -1
  28. package/components/list/list-item.a2ui.json +11 -1
  29. package/components/list/list-item.yaml +19 -0
  30. package/components/list/list.css +36 -6
  31. package/components/list-window/list-window.css +4 -4
  32. package/components/mark/mark.a2ui.json +109 -0
  33. package/components/mark/mark.class.js +22 -0
  34. package/components/mark/mark.css +39 -0
  35. package/components/mark/mark.d.ts +27 -0
  36. package/components/mark/mark.js +12 -0
  37. package/components/mark/mark.yaml +87 -0
  38. package/components/modal/modal.a2ui.json +9 -0
  39. package/components/modal/modal.css +8 -8
  40. package/components/modal/modal.yaml +14 -0
  41. package/components/nav-group/nav-group.a2ui.json +3 -0
  42. package/components/nav-group/nav-group.yaml +5 -0
  43. package/components/nav-item/nav-item.a2ui.json +3 -0
  44. package/components/nav-item/nav-item.yaml +5 -0
  45. package/components/option-card/option-card.css +9 -9
  46. package/components/segmented/segmented.class.js +10 -2
  47. package/components/select/select.a2ui.json +3 -0
  48. package/components/select/select.css +5 -5
  49. package/components/select/select.yaml +5 -0
  50. package/components/slider/slider.a2ui.json +6 -0
  51. package/components/slider/slider.yaml +10 -0
  52. package/components/stat/stat.css +18 -14
  53. package/components/stepper/stepper-item.a2ui.json +3 -0
  54. package/components/stepper/stepper-item.yaml +5 -0
  55. package/components/timeline/timeline-item.a2ui.json +8 -1
  56. package/components/timeline/timeline-item.yaml +12 -0
  57. package/components/timeline/timeline.css +19 -19
  58. package/components/tour/tour-step.a2ui.json +92 -0
  59. package/components/tour/tour-step.yaml +84 -0
  60. package/components/tour/tour.a2ui.json +172 -0
  61. package/components/tour/tour.class.js +309 -0
  62. package/components/tour/tour.css +135 -0
  63. package/components/tour/tour.d.ts +78 -0
  64. package/components/tour/tour.js +13 -0
  65. package/components/tour/tour.yaml +161 -0
  66. package/components/tree/tree-item.a2ui.json +5 -1
  67. package/components/tree/tree-item.yaml +7 -0
  68. package/components/tree/tree.a2ui.json +3 -0
  69. package/components/tree/tree.yaml +5 -0
  70. package/dist/web-components.min.css +1 -1
  71. package/dist/web-components.min.js +88 -74
  72. package/package.json +1 -1
  73. package/styles/components.css +3 -0
@@ -126,19 +126,27 @@
126
126
  min-height: var(--a-space-6);
127
127
  }
128
128
 
129
- :scope > [slot="icon"] {
129
+ /* Slot positioning uses descendant (not direct-child >) because the
130
+ template engine wraps `${...}` conditional renders in
131
+ <span style="display:contents"> housekeeping nodes — so slot="…"
132
+ children land as grandchildren in template-rendered hosts.
133
+ display:contents makes the wrapper layout-transparent (the grid
134
+ places the inner element), but CSS selectors still see the wrapper
135
+ as a real node. Descendant selectors handle both shapes (direct
136
+ authored children + template-engine-wrapped conditionals). */
137
+ :scope [slot="icon"] {
130
138
  grid-column: 1;
131
139
  grid-row: 1 / -1;
132
140
  align-self: center;
133
141
  color: var(--list-item-icon-color);
134
142
  }
135
143
 
136
- :scope > [slot="text"] {
144
+ :scope [slot="text"] {
137
145
  grid-column: 2;
138
146
  grid-row: 1;
139
147
  }
140
148
 
141
- :scope > [slot="description"] {
149
+ :scope [slot="description"] {
142
150
  grid-column: 2;
143
151
  grid-row: 2;
144
152
  color: var(--list-item-desc-color);
@@ -146,11 +154,33 @@
146
154
  line-height: 1.3;
147
155
  }
148
156
 
157
+ /* When a [slot="action"] child is present, the grid becomes 3-column:
158
+ icon | content | action. The :has() selector promotes the template
159
+ only when an action exists, so 2-column layouts stay unaffected.
160
+ Action is right-aligned + vertically centered (spans both content rows).
161
+ Used by onboarding-checklist-ui item rows + any consumer that needs an
162
+ end-aligned action button on a list-item row.
163
+ NOTE: NOT using `> [slot="action"]` (direct-child) — the template
164
+ engine wraps conditional `${...}` renders in <span style="display:
165
+ contents"> housekeeping nodes, so slot="action" lands as a grandchild
166
+ in template-rendered hosts. display:contents makes the wrapper a
167
+ transparent grid pass-through at layout time, but CSS selectors still
168
+ see it as a node. Descendant combinator handles both shapes. */
169
+ :scope:has([slot="action"]) {
170
+ grid-template-columns: auto 1fr auto;
171
+ }
172
+ :scope [slot="action"] {
173
+ grid-column: 3;
174
+ grid-row: 1 / -1;
175
+ align-self: center;
176
+ justify-self: end;
177
+ }
178
+
149
179
  /* Custom-content escape hatch — consumer authored a [slot="content"]
150
180
  child, the auto-stamp early-returned, and the consumer owns the
151
181
  full body. Span all columns so the consumer's layout isn't boxed
152
182
  into the content column. */
153
- :scope > [slot="content"] {
183
+ :scope [slot="content"] {
154
184
  grid-column: 1 / -1;
155
185
  }
156
186
 
@@ -167,8 +197,8 @@
167
197
  box-shadow: inset 2px 0 0 var(--a-accent-strong);
168
198
  }
169
199
 
170
- :scope[data-active] > [slot="icon"],
171
- :scope[data-active] > [slot="description"] {
200
+ :scope[data-active] [slot="icon"],
201
+ :scope[data-active] [slot="description"] {
172
202
  color: var(--a-accent-strong);
173
203
  }
174
204
  }
@@ -71,13 +71,13 @@
71
71
 
72
72
  /* Sticky slots — sit at the top/bottom of the scroll container so they
73
73
  stay visible while the window scrolls. */
74
- :scope > [slot="before"] {
74
+ :scope [slot="before"] {
75
75
  position: sticky;
76
76
  top: 0;
77
77
  z-index: 1;
78
78
  background: var(--list-window-bg, var(--list-window-bg-default));
79
79
  }
80
- :scope > [slot="after"] {
80
+ :scope [slot="after"] {
81
81
  position: sticky;
82
82
  bottom: 0;
83
83
  z-index: 1;
@@ -103,11 +103,11 @@
103
103
  :scope[empty] > [data-window] {
104
104
  display: none;
105
105
  }
106
- :scope[empty] > [slot="empty"] {
106
+ :scope[empty] [slot="empty"] {
107
107
  display: block;
108
108
  }
109
109
 
110
- :scope:not([empty]) > [slot="empty"] {
110
+ :scope:not([empty]) [slot="empty"] {
111
111
  display: none;
112
112
  }
113
113
 
@@ -0,0 +1,109 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/Mark.json",
4
+ "title": "Mark",
5
+ "description": "Highlighted inline text — token-driven theme-aware wrapper around the\nnative `<mark>` semantic. Use for search-result match highlighting,\ndiff additions in prose, \"new since last visit\" emphasis, and any\ninline text that needs a visual rail without changing its weight.\n\nDistinct from `<text-ui strong>` (semantic emphasis via weight) and\nfrom `<tag-ui>` (block-shaped pill chrome). mark-ui keeps the inline\ntext flow intact — same line-height, same baseline — just paints a\nbackground highlight behind the matched span.\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": "Mark"
18
+ },
19
+ "variant": {
20
+ "description": "Highlight color variant. Default `warning` is the conventional yellow-marker tone.",
21
+ "type": "string",
22
+ "enum": [
23
+ "warning",
24
+ "info",
25
+ "success",
26
+ "danger",
27
+ "muted"
28
+ ],
29
+ "default": "warning"
30
+ }
31
+ },
32
+ "required": [
33
+ "component"
34
+ ],
35
+ "unevaluatedProperties": false,
36
+ "x-adiaui": {
37
+ "anti_patterns": [
38
+ {
39
+ "fix": "<mark-ui>matched</mark-ui> — same semantic role but theme-aware.",
40
+ "why": "Native <mark> uses UA default (yellow-on-white) that doesn't adapt to theme — invisible in dark mode + jarring in light mode.",
41
+ "wrong": "<mark>matched</mark>"
42
+ }
43
+ ],
44
+ "category": "typography",
45
+ "composes": [],
46
+ "events": {},
47
+ "examples": [
48
+ {
49
+ "description": "Highlight the search query within a result title.",
50
+ "a2ui": "[\n { \"id\": \"row\", \"component\": \"Row\", \"children\": [\"pre\", \"mk\", \"post\"] },\n { \"id\": \"pre\", \"component\": \"Text\", \"textContent\": \"Quarterly \" },\n { \"id\": \"mk\", \"component\": \"Mark\", \"textContent\": \"revenue\" },\n { \"id\": \"post\", \"component\": \"Text\", \"textContent\": \" report — Q4 2025\" }\n]\n",
51
+ "name": "search-match"
52
+ }
53
+ ],
54
+ "keywords": [
55
+ "mark",
56
+ "highlight",
57
+ "search-match",
58
+ "emphasis"
59
+ ],
60
+ "name": "UIMark",
61
+ "related": [
62
+ "text",
63
+ "tag"
64
+ ],
65
+ "slots": {
66
+ "default": {
67
+ "description": "The text content to highlight (plain text or inline elements)."
68
+ }
69
+ },
70
+ "states": [
71
+ {
72
+ "description": "Default — highlighted background painted behind the slotted text.",
73
+ "name": "idle"
74
+ }
75
+ ],
76
+ "status": "stable",
77
+ "synonyms": {
78
+ "highlight": [
79
+ "mark",
80
+ "emphasis"
81
+ ],
82
+ "search-match": [
83
+ "mark",
84
+ "highlight"
85
+ ]
86
+ },
87
+ "tag": "mark-ui",
88
+ "tokens": {
89
+ "--mark-bg": {
90
+ "description": "Background color of the highlight.",
91
+ "default": "var(--a-warning-muted)"
92
+ },
93
+ "--mark-fg": {
94
+ "description": "Foreground (text) color inside the highlight.",
95
+ "default": "var(--a-warning-text)"
96
+ },
97
+ "--mark-px": {
98
+ "description": "Horizontal padding around the highlighted text.",
99
+ "default": "var(--a-space-0-5)"
100
+ },
101
+ "--mark-radius": {
102
+ "description": "Border radius of the highlight box.",
103
+ "default": "var(--a-radius-xs)"
104
+ }
105
+ },
106
+ "traits": [],
107
+ "version": 1
108
+ }
109
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `<mark-ui>` — token-driven inline text highlight (search-match,
3
+ * "new since", diff prose). Theme-aware wrapper around the native
4
+ * `<mark>` semantic role. Pure CSS atom — no template, no JS behavior.
5
+ */
6
+
7
+ import { UIElement } from '../../core/element.js';
8
+
9
+ export class UIMark extends UIElement {
10
+ static properties = {
11
+ variant: { type: String, default: 'warning', reflect: true }, // warning | info | success | danger | muted
12
+ };
13
+
14
+ static template = () => null;
15
+
16
+ connected() {
17
+ super.connected();
18
+ // Inherit the <mark> role for screen readers — "highlighted" / "marked"
19
+ // is the conventional AT announcement for search matches.
20
+ if (!this.hasAttribute('role')) this.setAttribute('role', 'mark');
21
+ }
22
+ }
@@ -0,0 +1,39 @@
1
+ @scope (mark-ui) {
2
+ :where(:scope) {
3
+ --mark-bg-default: var(--a-warning-muted);
4
+ --mark-fg-default: var(--a-warning-text);
5
+ --mark-px-default: var(--a-space-0-5);
6
+ --mark-radius-default: var(--a-radius-xs);
7
+ }
8
+
9
+ :scope {
10
+ /* Inline so it sits in normal text flow without breaking baselines.
11
+ Padding-inline only — vertical padding would push line-height. */
12
+ display: inline;
13
+ box-sizing: border-box;
14
+ padding-inline: var(--mark-px, var(--mark-px-default));
15
+ background: var(--mark-bg, var(--mark-bg-default));
16
+ color: var(--mark-fg, var(--mark-fg-default));
17
+ border-radius: var(--mark-radius, var(--mark-radius-default));
18
+ /* Preserve text decoration from ancestors (e.g. link underlines) */
19
+ text-decoration: inherit;
20
+ }
21
+
22
+ /* Variants — same role × state pair as alert-ui / tag-ui */
23
+ :scope[variant="info"] {
24
+ --mark-bg-default: var(--a-info-muted);
25
+ --mark-fg-default: var(--a-info-text);
26
+ }
27
+ :scope[variant="success"] {
28
+ --mark-bg-default: var(--a-success-muted);
29
+ --mark-fg-default: var(--a-success-text);
30
+ }
31
+ :scope[variant="danger"] {
32
+ --mark-bg-default: var(--a-danger-muted);
33
+ --mark-fg-default: var(--a-danger-text);
34
+ }
35
+ :scope[variant="muted"] {
36
+ --mark-bg-default: var(--a-bg-muted);
37
+ --mark-fg-default: var(--a-fg);
38
+ }
39
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `<mark-ui>` — Highlighted inline text — token-driven theme-aware wrapper around the
3
+ native `<mark>` semantic. Use for search-result match highlighting,
4
+ diff additions in prose, "new since last visit" emphasis, and any
5
+ inline text that needs a visual rail without changing its weight.
6
+
7
+ Distinct from `<text-ui strong>` (semantic emphasis via weight) and
8
+ from `<tag-ui>` (block-shaped pill chrome). mark-ui keeps the inline
9
+ text flow intact — same line-height, same baseline — just paints a
10
+ background highlight behind the matched span.
11
+
12
+ *
13
+ * @see https://ui-kit.exe.xyz/site/components/mark
14
+ *
15
+ * Type declarations generated by scripts/build/dts-codegen.mjs from
16
+ * the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
17
+ * run `npm run build:components`, then `npm run codegen:dts` to
18
+ * regenerate; or hand-author this file fully if rich event types are
19
+ * needed beyond what the yaml `events:` block can express.
20
+ */
21
+
22
+ import { UIElement } from '../../core/element.js';
23
+
24
+ export class UIMark extends UIElement {
25
+ /** Highlight color variant. Default `warning` is the conventional yellow-marker tone. */
26
+ variant: 'warning' | 'info' | 'success' | 'danger' | 'muted';
27
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `<mark-ui>` — auto-registers the tag on import.
3
+ *
4
+ * @see ../../USAGE.md#registration--auto-vs-explicit
5
+ */
6
+
7
+ import { defineIfFree } from '../../core/register.js';
8
+ import { UIMark } from './mark.class.js';
9
+
10
+ defineIfFree('mark-ui', UIMark);
11
+
12
+ export { UIMark };
@@ -0,0 +1,87 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: UIMark
3
+ tag: mark-ui
4
+ status: stable
5
+ component: Mark
6
+ category: typography
7
+ version: 1
8
+ description: |
9
+ Highlighted inline text — token-driven theme-aware wrapper around the
10
+ native `<mark>` semantic. Use for search-result match highlighting,
11
+ diff additions in prose, "new since last visit" emphasis, and any
12
+ inline text that needs a visual rail without changing its weight.
13
+
14
+ Distinct from `<text-ui strong>` (semantic emphasis via weight) and
15
+ from `<tag-ui>` (block-shaped pill chrome). mark-ui keeps the inline
16
+ text flow intact — same line-height, same baseline — just paints a
17
+ background highlight behind the matched span.
18
+ props:
19
+ variant:
20
+ description: Highlight color variant. Default `warning` is the conventional yellow-marker tone.
21
+ type: string
22
+ default: warning
23
+ enum:
24
+ - warning
25
+ - info
26
+ - success
27
+ - danger
28
+ - muted
29
+ reflect: true
30
+ events: {}
31
+ slots:
32
+ default:
33
+ description: The text content to highlight (plain text or inline elements).
34
+ states:
35
+ - name: idle
36
+ description: Default — highlighted background painted behind the slotted text.
37
+ traits: []
38
+ tokens:
39
+ --mark-bg:
40
+ description: Background color of the highlight.
41
+ default: var(--a-warning-muted)
42
+ --mark-fg:
43
+ description: Foreground (text) color inside the highlight.
44
+ default: var(--a-warning-text)
45
+ --mark-px:
46
+ description: Horizontal padding around the highlighted text.
47
+ default: var(--a-space-0-5)
48
+ --mark-radius:
49
+ description: Border radius of the highlight box.
50
+ default: var(--a-radius-xs)
51
+ a2ui:
52
+ rules:
53
+ - rule: 'Use mark-ui for search-result match highlighting — wrap the matched substring in <mark-ui> within the surrounding text. Inline; preserves text flow.'
54
+ reason: 'Search-result highlight pattern.'
55
+ - rule: 'variant=warning (default) is the conventional yellow-marker tone; info for brand-color highlights; success for "new since" / "added" emphasis; danger for "removed" / "deleted-line" in diff prose.'
56
+ reason: 'Semantic variant vocabulary.'
57
+ - rule: 'Distinct from <text-ui strong> (weight emphasis), <tag-ui> (block pill), <code-ui> (monospace). mark-ui is specifically the "visual rail behind matched text" use case.'
58
+ reason: 'Sibling-component boundary.'
59
+ anti_patterns:
60
+ - wrong: '<mark>matched</mark>'
61
+ why: 'Native <mark> uses UA default (yellow-on-white) that doesn''t adapt to theme — invisible in dark mode + jarring in light mode.'
62
+ fix: '<mark-ui>matched</mark-ui> — same semantic role but theme-aware.'
63
+ examples:
64
+ - name: search-match
65
+ description: Highlight the search query within a result title.
66
+ a2ui: |
67
+ [
68
+ { "id": "row", "component": "Row", "children": ["pre", "mk", "post"] },
69
+ { "id": "pre", "component": "Text", "textContent": "Quarterly " },
70
+ { "id": "mk", "component": "Mark", "textContent": "revenue" },
71
+ { "id": "post", "component": "Text", "textContent": " report — Q4 2025" }
72
+ ]
73
+ keywords:
74
+ - mark
75
+ - highlight
76
+ - search-match
77
+ - emphasis
78
+ synonyms:
79
+ highlight:
80
+ - mark
81
+ - emphasis
82
+ search-match:
83
+ - mark
84
+ - highlight
85
+ related:
86
+ - text
87
+ - tag
@@ -76,6 +76,15 @@
76
76
  "slots": {
77
77
  "default": {
78
78
  "description": "Content placed inside the modal surface. Accepts any elements (card-ui, command-ui, custom markup)."
79
+ },
80
+ "body": {
81
+ "description": "Main body region for prose, forms, or arbitrary content. Sits between header and footer; scrolls when content overflows."
82
+ },
83
+ "footer": {
84
+ "description": "Optional footer region rendered below the body, typically for action buttons. Author-fills with Confirm / Cancel buttons or similar trailing affordances."
85
+ },
86
+ "header": {
87
+ "description": "Optional header region rendered above the body. Author-fillable with title + supporting markup. CSS positions it at the top of the modal surface with consistent padding + the dismiss-close affordance."
79
88
  }
80
89
  },
81
90
  "states": [
@@ -40,10 +40,10 @@
40
40
  :scope[size="lg"] { --modal-width-default: min(48rem, 90vw); }
41
41
 
42
42
  /* ═══════ Closed ═══════ */
43
- :scope > [slot="dialog"]:not([open]) { display: none; }
43
+ :scope [slot="dialog"]:not([open]) { display: none; }
44
44
 
45
45
  /* ═══════ Full-viewport dialog shell ═══════ */
46
- :scope > [slot="dialog"][open] {
46
+ :scope [slot="dialog"][open] {
47
47
  box-sizing: border-box;
48
48
  position: fixed;
49
49
  inset: 0;
@@ -62,16 +62,16 @@
62
62
  }
63
63
 
64
64
  /* ═══════ Backdrop ═══════ */
65
- :scope > [slot="dialog"]::backdrop {
65
+ :scope [slot="dialog"]::backdrop {
66
66
  background: var(--modal-backdrop, var(--modal-backdrop-default));
67
67
  opacity: 0;
68
68
  transition: opacity var(--modal-duration, var(--modal-duration-default)) var(--modal-easing, var(--modal-easing-default));
69
69
  }
70
- :scope > [slot="dialog"][data-open]::backdrop { opacity: 1; }
71
- :scope > [slot="dialog"][data-closing]::backdrop { opacity: 0; }
70
+ :scope [slot="dialog"][data-open]::backdrop { opacity: 1; }
71
+ :scope [slot="dialog"][data-closing]::backdrop { opacity: 0; }
72
72
 
73
73
  /* ═══════ Content panel ═══════ */
74
- :scope > [slot="dialog"] > [slot="panel"] {
74
+ :scope [slot="dialog"] [slot="panel"] {
75
75
  box-sizing: border-box;
76
76
  display: flex;
77
77
  flex-direction: column;
@@ -89,7 +89,7 @@
89
89
  }
90
90
 
91
91
  /* ═══════ Open animation ═══════ */
92
- :scope > [slot="dialog"][data-open] > [slot="panel"] {
92
+ :scope [slot="dialog"][data-open] [slot="panel"] {
93
93
  transition: transform var(--modal-duration, var(--modal-duration-default)) var(--modal-easing, var(--modal-easing-default)),
94
94
  opacity var(--modal-duration, var(--modal-duration-default)) var(--modal-easing, var(--modal-easing-default));
95
95
  transform: scale(1);
@@ -97,7 +97,7 @@
97
97
  }
98
98
 
99
99
  /* ═══════ Close animation ═══════ */
100
- :scope > [slot="dialog"][data-closing] > [slot="panel"] {
100
+ :scope [slot="dialog"][data-closing] [slot="panel"] {
101
101
  transition: transform var(--modal-duration, var(--modal-duration-default)) var(--modal-easing, var(--modal-easing-default)),
102
102
  opacity var(--modal-duration, var(--modal-duration-default)) var(--modal-easing, var(--modal-easing-default));
103
103
  transform: scale(0.95);
@@ -45,6 +45,20 @@ events:
45
45
  slots:
46
46
  default:
47
47
  description: Content placed inside the modal surface. Accepts any elements (card-ui, command-ui, custom markup).
48
+ header:
49
+ description: >-
50
+ Optional header region rendered above the body. Author-fillable with
51
+ title + supporting markup. CSS positions it at the top of the modal
52
+ surface with consistent padding + the dismiss-close affordance.
53
+ body:
54
+ description: >-
55
+ Main body region for prose, forms, or arbitrary content. Sits between
56
+ header and footer; scrolls when content overflows.
57
+ footer:
58
+ description: >-
59
+ Optional footer region rendered below the body, typically for action
60
+ buttons. Author-fills with Confirm / Cancel buttons or similar trailing
61
+ affordances.
48
62
  states:
49
63
  - name: idle
50
64
  description: Default, ready for interaction.
@@ -98,6 +98,9 @@
98
98
  },
99
99
  "header": {
100
100
  "description": "Optional custom header. Auto-generated when missing."
101
+ },
102
+ "icon": {
103
+ "description": "Override the leading [icon] glyph in the auto-generated header with a custom slotted element. Mutually exclusive with the [icon] attribute."
101
104
  }
102
105
  },
103
106
  "states": [
@@ -63,6 +63,11 @@ slots:
63
63
  description: "Children — typically <nav-item-ui> rows."
64
64
  header:
65
65
  description: "Optional custom header. Auto-generated when missing."
66
+ icon:
67
+ description: >-
68
+ Override the leading [icon] glyph in the auto-generated header
69
+ with a custom slotted element. Mutually exclusive with the [icon]
70
+ attribute.
66
71
 
67
72
  states:
68
73
  - name: closed
@@ -105,6 +105,9 @@
105
105
  "slots": {
106
106
  "default": {
107
107
  "description": "Optional override of the default icon + text + badge stamping."
108
+ },
109
+ "icon": {
110
+ "description": "Override the leading [icon] glyph with a custom slotted element (custom icon-ui, image, avatar). Mutually exclusive with the [icon] attribute — slot child wins."
108
111
  }
109
112
  },
110
113
  "states": [
@@ -68,6 +68,11 @@ events:
68
68
  slots:
69
69
  default:
70
70
  description: "Optional override of the default icon + text + badge stamping."
71
+ icon:
72
+ description: >-
73
+ Override the leading [icon] glyph with a custom slotted element
74
+ (custom icon-ui, image, avatar). Mutually exclusive with the
75
+ [icon] attribute — slot child wins.
71
76
 
72
77
  states:
73
78
  - name: idle
@@ -21,10 +21,10 @@ option-card-ui[checked]::before {
21
21
  var(--option-card-radio-fill, var(--option-card-radio-fill-default)) 30% 100%
22
22
  );
23
23
  }
24
- option-card-ui[checked] > [slot="heading"] {
24
+ option-card-ui[checked] [slot="heading"] {
25
25
  color: var(--option-card-heading-color-checked, var(--option-card-heading-color-checked-default));
26
26
  }
27
- option-card-ui[checked] > [slot="icon"] {
27
+ option-card-ui[checked] [slot="icon"] {
28
28
  color: var(--option-card-icon-color-checked, var(--option-card-icon-color-checked-default));
29
29
  }
30
30
 
@@ -107,13 +107,13 @@ option-card-ui[checked] > [slot="icon"] {
107
107
 
108
108
  /* When an icon slot is present, insert a third column for it
109
109
  between indicator and heading. */
110
- :scope:has(> [slot="icon"]) {
110
+ :scope:has([slot="icon"]) {
111
111
  grid-template-columns: auto auto minmax(0, 1fr);
112
112
  grid-template-areas:
113
113
  "indicator icon heading"
114
114
  "indicator icon description";
115
115
  }
116
- :scope > [slot="icon"] {
116
+ :scope [slot="icon"] {
117
117
  grid-area: icon;
118
118
  align-self: start;
119
119
  color: var(--option-card-icon-color, var(--option-card-icon-color-default));
@@ -139,14 +139,14 @@ option-card-ui[checked] > [slot="icon"] {
139
139
  }
140
140
 
141
141
  /* ── Slots — heading + description ── */
142
- :scope > [slot="heading"] {
142
+ :scope [slot="heading"] {
143
143
  grid-area: heading;
144
144
  color: var(--option-card-heading-color, var(--option-card-heading-color-default));
145
145
  font-weight: var(--option-card-heading-weight, var(--option-card-heading-weight-default));
146
146
  font-size: var(--option-card-heading-size, var(--option-card-heading-size-default));
147
147
  transition: color var(--option-card-duration, var(--option-card-duration-default)) var(--option-card-easing, var(--option-card-easing-default));
148
148
  }
149
- :scope > [slot="description"] {
149
+ :scope [slot="description"] {
150
150
  grid-area: description;
151
151
  color: var(--option-card-desc-color, var(--option-card-desc-color-default));
152
152
  font-size: var(--option-card-desc-size, var(--option-card-desc-size-default));
@@ -162,7 +162,7 @@ option-card-ui[checked] > [slot="icon"] {
162
162
  margin-block-start: var(--a-space-3);
163
163
  display: none;
164
164
  }
165
- :scope:has(> [slot="icon"]) > :not([slot]) {
165
+ :scope:has([slot="icon"]) > :not([slot]) {
166
166
  grid-column: 3 / -1;
167
167
  }
168
168
  /* hover + [checked] state rules moved outside @scope — see Safari 17.x bug note at top.
@@ -184,7 +184,7 @@ option-card-ui[checked] > [slot="icon"] {
184
184
  padding: var(--a-space-4);
185
185
  align-items: start;
186
186
  }
187
- :scope[layout="tile"] > [slot="icon"] {
187
+ :scope[layout="tile"] [slot="icon"] {
188
188
  grid-area: icon;
189
189
  justify-self: start;
190
190
  align-self: start;
@@ -196,7 +196,7 @@ option-card-ui[checked] > [slot="icon"] {
196
196
  justify-self: end;
197
197
  margin-block-start: 0;
198
198
  }
199
- :scope[layout="tile"] > [slot="heading"] {
199
+ :scope[layout="tile"] [slot="heading"] {
200
200
  margin-block-start: var(--a-space-2);
201
201
  }
202
202
  :scope[layout="tile"] > :not([slot]) {
@@ -152,9 +152,17 @@ export class UISegmented extends UIFormElement {
152
152
  return;
153
153
  }
154
154
 
155
- // Lazy-create indicator (prepend so it's behind segments in paint order)
156
- const isNew = !this.#indicator;
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);
157
162
  if (isNew) {
163
+ for (const stale of this.querySelectorAll(':scope > [data-indicator]')) {
164
+ stale.remove();
165
+ }
158
166
  this.#indicator = document.createElement('span');
159
167
  this.#indicator.setAttribute('data-indicator', '');
160
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
  },