@adia-ai/web-components 0.6.34 → 0.6.35

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 (271) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/color/index.js +1 -1
  3. package/components/accordion/accordion-item.yaml +2 -2
  4. package/components/accordion/accordion.js +1 -1
  5. package/components/action-list/action-item.yaml +2 -2
  6. package/components/action-list/action-list.js +1 -1
  7. package/components/agent-artifact/{class.js → agent-artifact.class.js} +1 -1
  8. package/components/agent-artifact/agent-artifact.js +1 -1
  9. package/components/agent-feedback-bar/agent-feedback-bar.js +1 -1
  10. package/components/agent-questions/agent-questions.js +1 -1
  11. package/components/agent-reasoning/agent-reasoning.js +1 -1
  12. package/components/agent-suggestions/agent-suggestions.js +1 -1
  13. package/components/alert/alert.a2ui.json +64 -1
  14. package/components/alert/{class.js → alert.class.js} +189 -2
  15. package/components/alert/alert.css +78 -0
  16. package/components/alert/alert.d.ts +14 -0
  17. package/components/alert/alert.js +1 -1
  18. package/components/alert/alert.test.js +184 -0
  19. package/components/alert/alert.yaml +114 -1
  20. package/components/avatar/avatar-group.yaml +2 -2
  21. package/components/avatar/avatar.js +1 -1
  22. package/components/badge/badge.js +1 -1
  23. package/components/block/block.js +1 -1
  24. package/components/breadcrumb/breadcrumb.js +1 -1
  25. package/components/button/button.js +1 -1
  26. package/components/calendar-grid/calendar-grid.a2ui.json +10 -0
  27. package/components/calendar-grid/{class.js → calendar-grid.class.js} +30 -4
  28. package/components/calendar-grid/calendar-grid.css +20 -0
  29. package/components/calendar-grid/calendar-grid.d.ts +4 -0
  30. package/components/calendar-grid/calendar-grid.js +1 -1
  31. package/components/calendar-grid/calendar-grid.yaml +20 -0
  32. package/components/calendar-picker/calendar-picker.js +1 -1
  33. package/components/card/card.js +1 -1
  34. package/components/chart/chart.js +1 -1
  35. package/components/chart-legend/chart-legend.js +1 -1
  36. package/components/chat-thread/chat-input.a2ui.json +1 -1
  37. package/components/chat-thread/chat-input.js +6 -1
  38. package/components/chat-thread/chat-input.yaml +4 -1
  39. package/components/chat-thread/chat-thread.js +1 -1
  40. package/components/check/check.js +1 -1
  41. package/components/code/code.js +1 -1
  42. package/components/col/col.js +1 -1
  43. package/components/color-input/color-input.js +1 -1
  44. package/components/color-picker/color-picker.js +1 -1
  45. package/components/combobox/combobox.js +1 -1
  46. package/components/command/command.js +1 -1
  47. package/components/date-range-picker/{class.js → date-range-picker.class.js} +18 -2
  48. package/components/date-range-picker/date-range-picker.css +51 -5
  49. package/components/date-range-picker/date-range-picker.js +1 -1
  50. package/components/datetime-picker/{class.js → datetime-picker.class.js} +1 -1
  51. package/components/datetime-picker/datetime-picker.js +1 -1
  52. package/components/demo-toggle/demo-toggle.js +1 -1
  53. package/components/description-list/description-list.js +1 -1
  54. package/components/divider/divider.js +1 -1
  55. package/components/drawer/drawer.js +1 -1
  56. package/components/embed/embed.js +1 -1
  57. package/components/empty-state/empty-state.js +1 -1
  58. package/components/feed/feed.js +1 -1
  59. package/components/field/field.js +1 -1
  60. package/components/field/field.test.js +1 -1
  61. package/components/fields/fields.js +1 -1
  62. package/components/grid/grid.js +1 -1
  63. package/components/heatmap/heatmap.js +1 -1
  64. package/components/icon/icon.js +1 -1
  65. package/components/image/image.js +1 -1
  66. package/components/index.js +3 -0
  67. package/components/inline-message/inline-message.a2ui.json +143 -0
  68. package/components/inline-message/inline-message.class.js +169 -0
  69. package/components/inline-message/inline-message.css +75 -0
  70. package/components/inline-message/inline-message.d.ts +31 -0
  71. package/components/inline-message/inline-message.examples.md +19 -0
  72. package/components/inline-message/inline-message.js +17 -0
  73. package/components/inline-message/inline-message.test.js +203 -0
  74. package/components/inline-message/inline-message.yaml +205 -0
  75. package/components/input/input.css +1 -1
  76. package/components/input/input.js +1 -1
  77. package/components/input/input.yaml +5 -4
  78. package/components/inspector/inspector.js +1 -1
  79. package/components/integration-card/integration-card.js +1 -1
  80. package/components/kbd/kbd.js +1 -1
  81. package/components/link/link.js +1 -1
  82. package/components/list/list-item.yaml +2 -2
  83. package/components/list/list.js +1 -1
  84. package/components/list-window/list-window.js +1 -1
  85. package/components/loading-overlay/loading-overlay.a2ui.json +176 -0
  86. package/components/loading-overlay/loading-overlay.class.js +203 -0
  87. package/components/loading-overlay/loading-overlay.css +81 -0
  88. package/components/loading-overlay/loading-overlay.d.ts +24 -0
  89. package/components/loading-overlay/loading-overlay.examples.md +50 -0
  90. package/components/loading-overlay/loading-overlay.js +17 -0
  91. package/components/loading-overlay/loading-overlay.test.js +257 -0
  92. package/components/loading-overlay/loading-overlay.yaml +260 -0
  93. package/components/menu/menu-divider.yaml +1 -1
  94. package/components/menu/menu-item.yaml +1 -1
  95. package/components/menu/menu.a2ui.json +3 -0
  96. package/components/menu/menu.js +1 -1
  97. package/components/menu/menu.yaml +7 -0
  98. package/components/modal/{class.js → modal.class.js} +12 -1
  99. package/components/modal/modal.css +11 -1
  100. package/components/modal/modal.js +1 -1
  101. package/components/nav/nav.js +1 -1
  102. package/components/nav-group/nav-group.js +1 -1
  103. package/components/nav-item/nav-item.js +1 -1
  104. package/components/noodles/noodles.js +1 -1
  105. package/components/option-card/option-card.js +1 -1
  106. package/components/otp-input/otp-input.js +1 -1
  107. package/components/page/page.js +1 -1
  108. package/components/pagination/pagination.js +1 -1
  109. package/components/pane/pane.js +1 -1
  110. package/components/pipeline-status/pipeline-status.js +1 -1
  111. package/components/popover/popover.a2ui.json +8 -1
  112. package/components/popover/popover.js +1 -1
  113. package/components/popover/popover.yaml +14 -1
  114. package/components/progress/progress.js +1 -1
  115. package/components/progress-row/progress-row.js +1 -1
  116. package/components/radio/radio.js +1 -1
  117. package/components/range/range.js +1 -1
  118. package/components/rating/rating.js +1 -1
  119. package/components/richtext/richtext.js +1 -1
  120. package/components/row/row.js +1 -1
  121. package/components/search/search.js +1 -1
  122. package/components/segment/segment.js +1 -1
  123. package/components/segmented/segmented.js +1 -1
  124. package/components/select/select.a2ui.json +58 -4
  125. package/components/select/{class.js → select.class.js} +415 -6
  126. package/components/select/select.css +158 -0
  127. package/components/select/select.d.ts +31 -1
  128. package/components/select/select.js +1 -1
  129. package/components/select/select.test.js +202 -0
  130. package/components/select/select.yaml +126 -5
  131. package/components/skeleton/skeleton.js +1 -1
  132. package/components/slider/slider.js +1 -1
  133. package/components/spinner/spinner.a2ui.json +3 -2
  134. package/components/spinner/{class.js → spinner.class.js} +33 -3
  135. package/components/spinner/spinner.css +91 -35
  136. package/components/spinner/spinner.d.ts +2 -2
  137. package/components/spinner/spinner.js +1 -1
  138. package/components/spinner/spinner.test.js +49 -11
  139. package/components/spinner/spinner.yaml +9 -1
  140. package/components/stack/stack.js +1 -1
  141. package/components/step-progress/step-progress.js +1 -1
  142. package/components/stepper/stepper-item.yaml +1 -1
  143. package/components/stepper/stepper.js +1 -1
  144. package/components/stream/stream.js +1 -1
  145. package/components/swatch/swatch.js +1 -1
  146. package/components/swiper/swiper.js +1 -1
  147. package/components/switch/switch.js +1 -1
  148. package/components/table/table.css +1 -1
  149. package/components/table/table.js +1 -1
  150. package/components/table-toolbar/{class.js → table-toolbar.class.js} +1 -1
  151. package/components/table-toolbar/table-toolbar.js +1 -1
  152. package/components/tabs/tab.yaml +2 -2
  153. package/components/tabs/tabs.js +1 -1
  154. package/components/tag/tag.js +1 -1
  155. package/components/tags-input/tags-input.a2ui.json +337 -0
  156. package/components/tags-input/tags-input.class.js +776 -0
  157. package/components/tags-input/tags-input.css +201 -0
  158. package/components/tags-input/tags-input.d.ts +120 -0
  159. package/components/tags-input/tags-input.examples.md +92 -0
  160. package/components/tags-input/tags-input.js +17 -0
  161. package/components/tags-input/tags-input.test.js +368 -0
  162. package/components/tags-input/tags-input.yaml +367 -0
  163. package/components/text/text.js +1 -1
  164. package/components/textarea/textarea.a2ui.json +1 -1
  165. package/components/textarea/textarea.js +1 -1
  166. package/components/textarea/textarea.yaml +11 -8
  167. package/components/time-picker/time-picker.js +1 -1
  168. package/components/timeline/timeline-item.yaml +2 -2
  169. package/components/timeline/{class.js → timeline.class.js} +1 -1
  170. package/components/timeline/timeline.js +1 -1
  171. package/components/toast/toast.js +1 -1
  172. package/components/toggle-group/toggle-group.js +1 -1
  173. package/components/toggle-group/toggle-option.yaml +1 -1
  174. package/components/toggle-scheme/toggle-scheme.js +1 -1
  175. package/components/toolbar/toolbar-group.yaml +1 -1
  176. package/components/toolbar/toolbar.js +1 -1
  177. package/components/tooltip/tooltip.js +1 -1
  178. package/components/tree/tree-item.yaml +1 -1
  179. package/components/tree/tree.js +1 -1
  180. package/components/upload/upload.js +1 -1
  181. package/dist/web-components.min.css +1 -1
  182. package/dist/web-components.min.js +111 -90
  183. package/package.json +3 -3
  184. package/styles/components.css +3 -0
  185. /package/components/accordion/{class.js → accordion.class.js} +0 -0
  186. /package/components/action-list/{class.js → action-list.class.js} +0 -0
  187. /package/components/agent-feedback-bar/{class.js → agent-feedback-bar.class.js} +0 -0
  188. /package/components/agent-questions/{class.js → agent-questions.class.js} +0 -0
  189. /package/components/agent-reasoning/{class.js → agent-reasoning.class.js} +0 -0
  190. /package/components/agent-suggestions/{class.js → agent-suggestions.class.js} +0 -0
  191. /package/components/avatar/{class.js → avatar.class.js} +0 -0
  192. /package/components/badge/{class.js → badge.class.js} +0 -0
  193. /package/components/block/{class.js → block.class.js} +0 -0
  194. /package/components/breadcrumb/{class.js → breadcrumb.class.js} +0 -0
  195. /package/components/button/{class.js → button.class.js} +0 -0
  196. /package/components/calendar-picker/{class.js → calendar-picker.class.js} +0 -0
  197. /package/components/card/{class.js → card.class.js} +0 -0
  198. /package/components/chart/{class.js → chart.class.js} +0 -0
  199. /package/components/chart-legend/{class.js → chart-legend.class.js} +0 -0
  200. /package/components/chat-thread/{class.js → chat-thread.class.js} +0 -0
  201. /package/components/check/{class.js → check.class.js} +0 -0
  202. /package/components/code/{class.js → code.class.js} +0 -0
  203. /package/components/col/{class.js → col.class.js} +0 -0
  204. /package/components/color-input/{class.js → color-input.class.js} +0 -0
  205. /package/components/color-picker/{class.js → color-picker.class.js} +0 -0
  206. /package/components/combobox/{class.js → combobox.class.js} +0 -0
  207. /package/components/command/{class.js → command.class.js} +0 -0
  208. /package/components/demo-toggle/{class.js → demo-toggle.class.js} +0 -0
  209. /package/components/description-list/{class.js → description-list.class.js} +0 -0
  210. /package/components/divider/{class.js → divider.class.js} +0 -0
  211. /package/components/drawer/{class.js → drawer.class.js} +0 -0
  212. /package/components/embed/{class.js → embed.class.js} +0 -0
  213. /package/components/empty-state/{class.js → empty-state.class.js} +0 -0
  214. /package/components/feed/{class.js → feed.class.js} +0 -0
  215. /package/components/field/{class.js → field.class.js} +0 -0
  216. /package/components/fields/{class.js → fields.class.js} +0 -0
  217. /package/components/grid/{class.js → grid.class.js} +0 -0
  218. /package/components/heatmap/{class.js → heatmap.class.js} +0 -0
  219. /package/components/icon/{class.js → icon.class.js} +0 -0
  220. /package/components/image/{class.js → image.class.js} +0 -0
  221. /package/components/input/{class.js → input.class.js} +0 -0
  222. /package/components/inspector/{class.js → inspector.class.js} +0 -0
  223. /package/components/integration-card/{class.js → integration-card.class.js} +0 -0
  224. /package/components/kbd/{class.js → kbd.class.js} +0 -0
  225. /package/components/link/{class.js → link.class.js} +0 -0
  226. /package/components/list/{class.js → list.class.js} +0 -0
  227. /package/components/list-window/{class.js → list-window.class.js} +0 -0
  228. /package/components/menu/{class.js → menu.class.js} +0 -0
  229. /package/components/nav/{class.js → nav.class.js} +0 -0
  230. /package/components/nav-group/{class.js → nav-group.class.js} +0 -0
  231. /package/components/nav-item/{class.js → nav-item.class.js} +0 -0
  232. /package/components/noodles/{class.js → noodles.class.js} +0 -0
  233. /package/components/option-card/{class.js → option-card.class.js} +0 -0
  234. /package/components/otp-input/{class.js → otp-input.class.js} +0 -0
  235. /package/components/page/{class.js → page.class.js} +0 -0
  236. /package/components/pagination/{class.js → pagination.class.js} +0 -0
  237. /package/components/pane/{class.js → pane.class.js} +0 -0
  238. /package/components/pipeline-status/{class.js → pipeline-status.class.js} +0 -0
  239. /package/components/popover/{class.js → popover.class.js} +0 -0
  240. /package/components/progress/{class.js → progress.class.js} +0 -0
  241. /package/components/progress-row/{class.js → progress-row.class.js} +0 -0
  242. /package/components/radio/{class.js → radio.class.js} +0 -0
  243. /package/components/range/{class.js → range.class.js} +0 -0
  244. /package/components/rating/{class.js → rating.class.js} +0 -0
  245. /package/components/richtext/{class.js → richtext.class.js} +0 -0
  246. /package/components/row/{class.js → row.class.js} +0 -0
  247. /package/components/search/{class.js → search.class.js} +0 -0
  248. /package/components/segment/{class.js → segment.class.js} +0 -0
  249. /package/components/segmented/{class.js → segmented.class.js} +0 -0
  250. /package/components/skeleton/{class.js → skeleton.class.js} +0 -0
  251. /package/components/slider/{class.js → slider.class.js} +0 -0
  252. /package/components/stack/{class.js → stack.class.js} +0 -0
  253. /package/components/step-progress/{class.js → step-progress.class.js} +0 -0
  254. /package/components/stepper/{class.js → stepper.class.js} +0 -0
  255. /package/components/stream/{class.js → stream.class.js} +0 -0
  256. /package/components/swatch/{class.js → swatch.class.js} +0 -0
  257. /package/components/swiper/{class.js → swiper.class.js} +0 -0
  258. /package/components/switch/{class.js → switch.class.js} +0 -0
  259. /package/components/table/{class.js → table.class.js} +0 -0
  260. /package/components/tabs/{class.js → tabs.class.js} +0 -0
  261. /package/components/tag/{class.js → tag.class.js} +0 -0
  262. /package/components/text/{class.js → text.class.js} +0 -0
  263. /package/components/textarea/{class.js → textarea.class.js} +0 -0
  264. /package/components/time-picker/{class.js → time-picker.class.js} +0 -0
  265. /package/components/toast/{class.js → toast.class.js} +0 -0
  266. /package/components/toggle-group/{class.js → toggle-group.class.js} +0 -0
  267. /package/components/toggle-scheme/{class.js → toggle-scheme.class.js} +0 -0
  268. /package/components/toolbar/{class.js → toolbar.class.js} +0 -0
  269. /package/components/tooltip/{class.js → tooltip.class.js} +0 -0
  270. /package/components/tree/{class.js → tree.class.js} +0 -0
  271. /package/components/upload/{class.js → upload.class.js} +0 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Non-side-effect class export for `<loading-overlay-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, or
6
+ * selective composition.
7
+ *
8
+ * The auto-register path stays at `@adia-ai/web-components/components/loading-overlay`
9
+ * (which imports this file + calls `defineIfFree()`).
10
+ *
11
+ * @see ../../USAGE.md#registration--auto-vs-explicit
12
+ */
13
+
14
+ /**
15
+ * <loading-overlay-ui active label="Loading orders…"></loading-overlay-ui>
16
+ * <loading-overlay-ui active variant="transparent" delay="0">
17
+ * <skeleton-ui width="100%" height="1rem"></skeleton-ui>
18
+ * <skeleton-ui width="80%" height="1rem"></skeleton-ui>
19
+ * </loading-overlay-ui>
20
+ *
21
+ * Container-scoped busy overlay. Covers a positioned parent region with
22
+ * a spinner, skeleton, or custom busy indicator while async work is in
23
+ * flight. The host:
24
+ *
25
+ * 1. Absolutely positions to fill the offsetParent (consumer's
26
+ * responsibility to ensure that parent is `position: relative` or
27
+ * similar — documented in the spec / usage notes; we do NOT mutate
28
+ * parent layout styles).
29
+ * 2. Applies aria-busy="true" to the parent on activate; releases on
30
+ * deactivate / disconnect. This is screen-reader politeness; the
31
+ * backdrop's pointer-events:auto handles the click/focus block at
32
+ * the CSS level (no native [inert] attribute toggling — that's
33
+ * kept minimal to avoid disturbing the consumer's focus model).
34
+ * 3. Auto-stamps a centered <spinner-ui size="lg"> when the default
35
+ * slot is empty.
36
+ * 4. Honors a `[delay]` grace window (default 200ms) — if the active
37
+ * flag is cleared before the timer fires, the overlay never
38
+ * paints. Avoids flash on fast loads.
39
+ *
40
+ * Props (attributes):
41
+ * active — overlay visible (default: false)
42
+ * delay — grace window in ms before paint (default: 200)
43
+ * label — accessible label, also forwarded to auto-stamped spinner
44
+ * variant — default | transparent | blur
45
+ *
46
+ * Lifecycle: connected() seeds aria-label on host + caches parent
47
+ * reference for aria-busy bookkeeping. Disconnected() clears any
48
+ * pending delay timer and releases aria-busy on the parent. State
49
+ * changes are driven by the reactive [active] setter wrapper, which
50
+ * starts/cancels the timer + applies aria-busy when the timer fires.
51
+ *
52
+ * @see SPEC-034 (docs/specs/implementation-ready/SPEC-034-loading-overlay.md)
53
+ */
54
+
55
+ import { UIElement } from '../../core/element.js';
56
+
57
+ export class UILoadingOverlay extends UIElement {
58
+ static properties = {
59
+ active: { type: Boolean, default: false, reflect: true },
60
+ delay: { type: Number, default: 200, reflect: true },
61
+ label: { type: String, default: 'Loading…', reflect: true },
62
+ variant: { type: String, default: 'default', reflect: true },
63
+ };
64
+
65
+ // No stamped children authored by the template engine; auto-stamp of
66
+ // the default spinner happens imperatively in connected() / render()
67
+ // when the slot is empty. Authored children (skeleton/progress/etc.)
68
+ // are left in place per ADR-0033 (light-DOM substrate).
69
+ static template = () => null;
70
+
71
+ #parent = null;
72
+ #delayTimer = null;
73
+ #activeApplied = false; // tracks whether aria-busy / opacity actually painted
74
+
75
+ constructor() {
76
+ super();
77
+ // Wrap the auto-installed `active` setter so state transitions kick
78
+ // the delay timer synchronously with the assignment — matching the
79
+ // drawer-ui Safari pattern (avoids microtask scheduling delays).
80
+ const desc = Object.getOwnPropertyDescriptor(this, 'active');
81
+ if (desc?.set) {
82
+ const origSet = desc.set;
83
+ Object.defineProperty(this, 'active', {
84
+ get: desc.get,
85
+ set: (v) => {
86
+ const prev = !!desc.get.call(this);
87
+ origSet.call(this, v);
88
+ if (!!v !== prev) this.#syncActive(!!v);
89
+ },
90
+ configurable: true,
91
+ });
92
+ }
93
+ }
94
+
95
+ connected() {
96
+ // Host ARIA: role=status is a polite live region; aria-label
97
+ // names the operation. Both are seeded once; render() keeps the
98
+ // label fresh on prop changes.
99
+ if (!this.hasAttribute('role')) this.setAttribute('role', 'status');
100
+ if (!this.hasAttribute('aria-live')) this.setAttribute('aria-live', 'polite');
101
+ this.setAttribute('aria-label', this.label || 'Loading');
102
+
103
+ // Cache the parent so disconnected() can release aria-busy even if
104
+ // the element is detached before parentElement is re-readable.
105
+ this.#parent = this.parentElement;
106
+
107
+ // If the element mounted with [active] already set (declarative),
108
+ // honor it. The reactive setter wrapper only fires on subsequent
109
+ // assignments, so we kick the timer here too.
110
+ if (this.active) this.#syncActive(true);
111
+ }
112
+
113
+ disconnected() {
114
+ // Symmetric lifecycle: clear any in-flight timer + release aria-busy
115
+ // on the parent we cached at connect time.
116
+ this.#clearTimer();
117
+ if (this.#activeApplied) this.#releaseBusy();
118
+ this.#parent = null;
119
+ }
120
+
121
+ render() {
122
+ // Keep the host label fresh if [label] is mutated after connect.
123
+ // Forward it to an auto-stamped spinner-ui (if present) so screen
124
+ // readers hear a single coherent name.
125
+ const label = this.label || 'Loading';
126
+ this.setAttribute('aria-label', label);
127
+
128
+ const ownSpinner = this.querySelector(':scope > spinner-ui[data-loading-overlay-auto]');
129
+ if (ownSpinner && ownSpinner.getAttribute('label') !== label) {
130
+ ownSpinner.setAttribute('label', label);
131
+ }
132
+
133
+ // Ensure the auto-stamped spinner exists when the slot is empty.
134
+ // Authored children (consumer-supplied indicators) win; we only
135
+ // stamp when there are no element children at all.
136
+ if (this.children.length === 0) {
137
+ this.#stampDefaultSpinner();
138
+ }
139
+ }
140
+
141
+ // ── Internals ───────────────────────────────────────────────────────
142
+
143
+ #syncActive(next) {
144
+ // Clear any prior in-flight timer first; the new state owns the
145
+ // grace window.
146
+ this.#clearTimer();
147
+
148
+ if (next) {
149
+ const delay = Math.max(0, Number(this.delay) || 0);
150
+ if (delay === 0) {
151
+ this.#applyBusy();
152
+ } else {
153
+ this.#delayTimer = setTimeout(() => {
154
+ this.#delayTimer = null;
155
+ // Only paint if we're still active — caller may have cleared
156
+ // [active] during the grace window.
157
+ if (this.active) this.#applyBusy();
158
+ }, delay);
159
+ }
160
+ } else if (this.#activeApplied) {
161
+ this.#releaseBusy();
162
+ }
163
+ }
164
+
165
+ #applyBusy() {
166
+ const parent = this.#parent || this.parentElement;
167
+ if (parent && !parent.hasAttribute('aria-busy')) {
168
+ parent.setAttribute('aria-busy', 'true');
169
+ }
170
+ this.#activeApplied = true;
171
+ }
172
+
173
+ #releaseBusy() {
174
+ const parent = this.#parent || this.parentElement;
175
+ // Only remove aria-busy if WE set it (heuristic: matches "true").
176
+ // If the consumer authored their own aria-busy="true" before we
177
+ // mounted, we leave it; if they set "false" we leave it. Same
178
+ // shape as drawer-ui releasing dialog state.
179
+ if (parent && parent.getAttribute('aria-busy') === 'true') {
180
+ parent.removeAttribute('aria-busy');
181
+ }
182
+ this.#activeApplied = false;
183
+ }
184
+
185
+ #clearTimer() {
186
+ if (this.#delayTimer !== null) {
187
+ clearTimeout(this.#delayTimer);
188
+ this.#delayTimer = null;
189
+ }
190
+ }
191
+
192
+ #stampDefaultSpinner() {
193
+ // Imperative stamp — light-DOM child, marked with a data attribute
194
+ // so render()'s "did the consumer slot something?" check stays
195
+ // unambiguous on subsequent renders.
196
+ const sp = document.createElement('spinner-ui');
197
+ sp.setAttribute('size', 'lg');
198
+ sp.setAttribute('tone', 'subtle');
199
+ sp.setAttribute('label', this.label || 'Loading');
200
+ sp.setAttribute('data-loading-overlay-auto', '');
201
+ this.appendChild(sp);
202
+ }
203
+ }
@@ -0,0 +1,81 @@
1
+ @scope (loading-overlay-ui) {
2
+ /* ── Block 1 — TOKENS ─────────────────────────────────────────────
3
+ Loading-overlay covers a positioned parent. Backdrop tint, corner
4
+ radius, stacking order, and motion are all consumer-overridable
5
+ via the `var(--public, var(--public-default))` chain (per
6
+ component-token-contract.md §"The two-block @scope pattern"). */
7
+ :where(:scope) {
8
+ --loading-overlay-bg-default: var(--a-scrim-default);
9
+ --loading-overlay-radius-default: var(--a-radius-md);
10
+ --loading-overlay-z-default: 50;
11
+ --loading-overlay-duration-default: var(--a-duration);
12
+ --loading-overlay-easing-default: var(--a-easing-out);
13
+ --loading-overlay-gap-default: var(--a-space-3);
14
+ }
15
+
16
+ /* ── Block 2 — BASE
17
+ Idle state: hidden but mounted. The element stays in the DOM so
18
+ state transitions on `[active]` animate; `display: none` would
19
+ skip the transition entirely. We pair `opacity: 0` with
20
+ `pointer-events: none` so the dim overlay doesn't intercept
21
+ clicks on the underlying content when idle. */
22
+ :scope {
23
+ box-sizing: border-box;
24
+ position: absolute;
25
+ inset: 0;
26
+ display: grid;
27
+ place-items: center;
28
+ gap: var(--loading-overlay-gap, var(--loading-overlay-gap-default));
29
+ background: var(--loading-overlay-bg, var(--loading-overlay-bg-default));
30
+ border-radius: var(--loading-overlay-radius, var(--loading-overlay-radius-default));
31
+ z-index: var(--loading-overlay-z, var(--loading-overlay-z-default));
32
+ opacity: 0;
33
+ pointer-events: none;
34
+ transition:
35
+ opacity var(--loading-overlay-duration, var(--loading-overlay-duration-default))
36
+ var(--loading-overlay-easing, var(--loading-overlay-easing-default));
37
+ }
38
+
39
+ /* ── Active state — fade in + capture pointer events ─────────────
40
+ Active state is gated by the delay timer in JS (see loading-overlay.class.js); by
41
+ the time `[active]` is set, the grace window has elapsed and the
42
+ overlay should paint immediately on next frame. */
43
+ :scope[active] {
44
+ opacity: 1;
45
+ pointer-events: auto;
46
+ }
47
+
48
+ /* ── Variants ─────────────────────────────────────────────────────
49
+ transparent: no backdrop fill — useful when the indicator IS the
50
+ overlay (e.g. a full-area skeleton stack). The parent is still
51
+ marked aria-busy and pointer events still blocked.
52
+ blur: light scrim plus backdrop-filter blur on the underlying
53
+ content. Use sparingly — Safari < 18 paint cost. */
54
+ :scope[variant="transparent"] {
55
+ --loading-overlay-bg-default: transparent;
56
+ }
57
+
58
+ :scope[variant="blur"] {
59
+ --loading-overlay-bg-default: var(--a-scrim-weak);
60
+ backdrop-filter: blur(4px);
61
+ -webkit-backdrop-filter: blur(4px);
62
+ }
63
+
64
+ /* ── Auto-stamped spinner sizing ──────────────────────────────────
65
+ When the default slot is empty, the JS layer auto-stamps a
66
+ `<spinner-ui size="lg">` child. Style it as the default centered
67
+ indicator so consumer slots and the auto-stamp render identically. */
68
+ :scope > spinner-ui {
69
+ --spinner-color: var(--a-fg-muted);
70
+ }
71
+
72
+ /* ── Reduced motion ───────────────────────────────────────────────
73
+ WCAG 2.3.3 — suppress the fade. The overlay still paints / hides
74
+ instantly (no transition), and the auto-stamped spinner falls
75
+ back to its own reduced-motion ellipsis via spinner.css. */
76
+ @media (prefers-reduced-motion: reduce) {
77
+ :scope {
78
+ transition: none;
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `<loading-overlay-ui>` — Container-scoped busy overlay. Covers a positioned parent region with a centered spinner (auto-stamped) or slotted indicator (skeleton-ui, progress-ui, custom) while async work is in flight. Wires aria-busy onto the parent on connect; applies a [delay] grace window to avoid flash on fast loads. For viewport-scoped / route loaders use a dedicated route-loader pattern; for submit-button busy use <button-ui loading>.
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/loading-overlay
5
+ *
6
+ * Type declarations generated by scripts/build/dts-codegen.mjs from
7
+ * the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
8
+ * run `npm run build:components`, then `npm run codegen:dts` to
9
+ * regenerate; or hand-author this file fully if rich event types are
10
+ * needed beyond what the yaml `events:` block can express.
11
+ */
12
+
13
+ import { UIElement } from '../../core/element.js';
14
+
15
+ export class UILoadingOverlay extends UIElement {
16
+ /** When set, overlay is visible and the parent container is marked aria-busy. Toggle from consumer code; default hidden. */
17
+ active: string;
18
+ /** Suppress the overlay until this many ms elapse. Prevents flash on fast loads. Default 200ms. */
19
+ delay: string;
20
+ /** Accessible operation name used by the host aria-label and forwarded to the auto-stamped spinner. */
21
+ label: string;
22
+ /** Backdrop treatment — default (muted scrim), transparent (no backdrop fill — useful with full-area skeleton indicators), blur (light scrim with backdrop-filter blur). */
23
+ variant: string;
24
+ }
@@ -0,0 +1,50 @@
1
+ # loading-overlay — Examples
2
+
3
+ ## Default (auto-stamped spinner)
4
+
5
+ The host parent must be positioned (`position: relative` or similar);
6
+ the overlay does not mutate parent layout. When the default slot is
7
+ empty, a centered `<spinner-ui size="lg">` is auto-stamped.
8
+
9
+ ```html
10
+ <card-ui style="position: relative;">
11
+ <section>
12
+ <table-ui id="orders" data-stream-src="/api/orders"></table-ui>
13
+ <loading-overlay-ui active label="Loading orders…"></loading-overlay-ui>
14
+ </section>
15
+ </card-ui>
16
+ ```
17
+
18
+ ## Skeleton placeholder
19
+
20
+ Pair `variant="transparent"` with a slotted skeleton stack to render
21
+ shape-of-content placeholders instead of a centered spinner.
22
+
23
+ ```html
24
+ <div style="position: relative;">
25
+ <loading-overlay-ui active variant="transparent" label="Loading content">
26
+ <stack-ui>
27
+ <skeleton-ui width="100%" height="1rem"></skeleton-ui>
28
+ <skeleton-ui width="80%" height="1rem"></skeleton-ui>
29
+ <skeleton-ui width="60%" height="1rem"></skeleton-ui>
30
+ </stack-ui>
31
+ </loading-overlay-ui>
32
+ </div>
33
+ ```
34
+
35
+ ## Fast-load (no flash)
36
+
37
+ The default 200 ms grace window suppresses paint on fast-resolving
38
+ loads. If the underlying work resolves before the timer fires, the
39
+ overlay never paints.
40
+
41
+ ```html
42
+ <loading-overlay-ui active delay="200"></loading-overlay-ui>
43
+ ```
44
+
45
+ For known-slow operations (server-side jobs, large uploads) opt out
46
+ with `delay="0"`:
47
+
48
+ ```html
49
+ <loading-overlay-ui active delay="0" label="Generating report…"></loading-overlay-ui>
50
+ ```
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `<loading-overlay-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 { UILoadingOverlay } from '@adia-ai/web-components/components/loading-overlay/class';
8
+ *
9
+ * @see ../../USAGE.md#registration--auto-vs-explicit
10
+ */
11
+
12
+ import { defineIfFree } from '../../core/register.js';
13
+ import { UILoadingOverlay } from './loading-overlay.class.js';
14
+
15
+ defineIfFree('loading-overlay-ui', UILoadingOverlay);
16
+
17
+ export { UILoadingOverlay };
@@ -0,0 +1,257 @@
1
+ /**
2
+ * loading-overlay-ui tests — covers SPEC-034 § Verification.
3
+ *
4
+ * The component is JS-driven (delay timer + aria-busy bookkeeping) with
5
+ * a CSS-only fade transition. Tests split:
6
+ *
7
+ * 1. Defaults + property reflection.
8
+ * 2. aria-busy parent wiring (apply on activate, release on
9
+ * deactivate / disconnect).
10
+ * 3. [delay] grace-window suppression — fast-load no-flash.
11
+ * 4. Auto-stamped spinner-ui (default slot empty) vs slotted content.
12
+ * 5. CSS source contract — two-block @scope, token surface, variants.
13
+ */
14
+
15
+ import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
16
+ import { readFileSync } from 'node:fs';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { dirname, resolve } from 'node:path';
19
+
20
+ const HERE = dirname(fileURLToPath(import.meta.url));
21
+ const LO_CSS = readFileSync(resolve(HERE, 'loading-overlay.css'), 'utf8');
22
+
23
+ const tick = () => new Promise((r) => queueMicrotask(r));
24
+
25
+ beforeAll(async () => {
26
+ await import('./loading-overlay.js');
27
+ // Side-effect import so the auto-stamp can construct spinner-ui.
28
+ await import('../spinner/spinner.js');
29
+ });
30
+
31
+ function mountInPositionedParent(html) {
32
+ const parent = document.createElement('div');
33
+ parent.style.position = 'relative';
34
+ parent.style.width = '200px';
35
+ parent.style.height = '120px';
36
+ parent.id = 'lo-parent';
37
+ parent.innerHTML = html;
38
+ document.body.appendChild(parent);
39
+ return { parent, overlay: parent.querySelector('loading-overlay-ui') };
40
+ }
41
+
42
+ // ── 1. Defaults + property reflection ────────────────────────────────
43
+
44
+ describe('loading-overlay-ui — defaults', () => {
45
+ beforeEach(() => { document.body.innerHTML = ''; });
46
+
47
+ it('renders without props using documented defaults', async () => {
48
+ const { overlay } = mountInPositionedParent('<loading-overlay-ui></loading-overlay-ui>');
49
+ await tick();
50
+ expect(overlay.active).toBe(false);
51
+ expect(overlay.delay).toBe(200);
52
+ expect(overlay.label).toBe('Loading…');
53
+ expect(overlay.variant).toBe('default');
54
+ });
55
+
56
+ it('sets role="status" + aria-live="polite" on the host', async () => {
57
+ const { overlay } = mountInPositionedParent('<loading-overlay-ui></loading-overlay-ui>');
58
+ await tick();
59
+ expect(overlay.getAttribute('role')).toBe('status');
60
+ expect(overlay.getAttribute('aria-live')).toBe('polite');
61
+ });
62
+
63
+ it('seeds aria-label from the [label] prop', async () => {
64
+ const { overlay } = mountInPositionedParent('<loading-overlay-ui label="Saving"></loading-overlay-ui>');
65
+ await tick();
66
+ expect(overlay.getAttribute('aria-label')).toBe('Saving');
67
+ });
68
+
69
+ it('reflects [active], [variant], [delay], [label] to the host attributes', async () => {
70
+ const { overlay } = mountInPositionedParent('<loading-overlay-ui active variant="blur" delay="500" label="Wait"></loading-overlay-ui>');
71
+ await tick();
72
+ expect(overlay.hasAttribute('active')).toBe(true);
73
+ expect(overlay.getAttribute('variant')).toBe('blur');
74
+ expect(overlay.getAttribute('delay')).toBe('500');
75
+ expect(overlay.getAttribute('label')).toBe('Wait');
76
+ });
77
+ });
78
+
79
+ // ── 2. aria-busy parent wiring ───────────────────────────────────────
80
+
81
+ describe('loading-overlay-ui — aria-busy parent wiring', () => {
82
+ beforeEach(() => {
83
+ document.body.innerHTML = '';
84
+ vi.useFakeTimers();
85
+ });
86
+ afterEach(() => { vi.useRealTimers(); });
87
+
88
+ it('applies aria-busy="true" to parent on activate (after delay)', async () => {
89
+ const { parent } = mountInPositionedParent('<loading-overlay-ui delay="0"></loading-overlay-ui>');
90
+ await tick();
91
+ const overlay = parent.querySelector('loading-overlay-ui');
92
+ expect(parent.hasAttribute('aria-busy')).toBe(false);
93
+ overlay.active = true;
94
+ // delay=0 → applies synchronously via setTimeout(0)
95
+ vi.advanceTimersByTime(0);
96
+ expect(parent.getAttribute('aria-busy')).toBe('true');
97
+ });
98
+
99
+ it('releases aria-busy on deactivate', async () => {
100
+ const { parent } = mountInPositionedParent('<loading-overlay-ui active delay="0"></loading-overlay-ui>');
101
+ await tick();
102
+ vi.advanceTimersByTime(0);
103
+ expect(parent.getAttribute('aria-busy')).toBe('true');
104
+
105
+ const overlay = parent.querySelector('loading-overlay-ui');
106
+ overlay.active = false;
107
+ expect(parent.hasAttribute('aria-busy')).toBe(false);
108
+ });
109
+
110
+ it('releases aria-busy on disconnect', async () => {
111
+ const { parent } = mountInPositionedParent('<loading-overlay-ui active delay="0"></loading-overlay-ui>');
112
+ await tick();
113
+ vi.advanceTimersByTime(0);
114
+ expect(parent.getAttribute('aria-busy')).toBe('true');
115
+
116
+ const overlay = parent.querySelector('loading-overlay-ui');
117
+ overlay.remove();
118
+ expect(parent.hasAttribute('aria-busy')).toBe(false);
119
+ });
120
+
121
+ it('honors mount-time [active] (declarative)', async () => {
122
+ const { parent } = mountInPositionedParent('<loading-overlay-ui active delay="0"></loading-overlay-ui>');
123
+ await tick();
124
+ vi.advanceTimersByTime(0);
125
+ expect(parent.getAttribute('aria-busy')).toBe('true');
126
+ });
127
+ });
128
+
129
+ // ── 3. [delay] grace window ──────────────────────────────────────────
130
+
131
+ describe('loading-overlay-ui — [delay] grace window', () => {
132
+ beforeEach(() => {
133
+ document.body.innerHTML = '';
134
+ vi.useFakeTimers();
135
+ });
136
+ afterEach(() => { vi.useRealTimers(); });
137
+
138
+ it('suppresses paint when [active] is cleared before delay expires', async () => {
139
+ const { parent } = mountInPositionedParent('<loading-overlay-ui delay="200"></loading-overlay-ui>');
140
+ await tick();
141
+ const overlay = parent.querySelector('loading-overlay-ui');
142
+
143
+ overlay.active = true;
144
+ // Within the grace window — parent should NOT be marked yet.
145
+ vi.advanceTimersByTime(100);
146
+ expect(parent.hasAttribute('aria-busy')).toBe(false);
147
+
148
+ // Clear before timer fires.
149
+ overlay.active = false;
150
+ vi.advanceTimersByTime(200);
151
+ expect(parent.hasAttribute('aria-busy')).toBe(false);
152
+ });
153
+
154
+ it('paints after delay elapses when [active] stays true', async () => {
155
+ const { parent } = mountInPositionedParent('<loading-overlay-ui delay="200"></loading-overlay-ui>');
156
+ await tick();
157
+ const overlay = parent.querySelector('loading-overlay-ui');
158
+
159
+ overlay.active = true;
160
+ expect(parent.hasAttribute('aria-busy')).toBe(false);
161
+ vi.advanceTimersByTime(200);
162
+ expect(parent.getAttribute('aria-busy')).toBe('true');
163
+ });
164
+ });
165
+
166
+ // ── 4. Default-slot auto-stamp ───────────────────────────────────────
167
+
168
+ describe('loading-overlay-ui — auto-stamped spinner', () => {
169
+ beforeEach(() => { document.body.innerHTML = ''; });
170
+
171
+ it('auto-stamps a <spinner-ui size="lg"> when default slot is empty', async () => {
172
+ const { overlay } = mountInPositionedParent('<loading-overlay-ui></loading-overlay-ui>');
173
+ await tick();
174
+ const sp = overlay.querySelector(':scope > spinner-ui[data-loading-overlay-auto]');
175
+ expect(sp).not.toBeNull();
176
+ expect(sp.getAttribute('size')).toBe('lg');
177
+ });
178
+
179
+ it('does NOT auto-stamp when consumer slots their own indicator', async () => {
180
+ const { overlay } = mountInPositionedParent(
181
+ '<loading-overlay-ui><skeleton-ui width="100%" height="1rem"></skeleton-ui></loading-overlay-ui>'
182
+ );
183
+ await tick();
184
+ expect(overlay.querySelector(':scope > spinner-ui[data-loading-overlay-auto]')).toBeNull();
185
+ expect(overlay.querySelector(':scope > skeleton-ui')).not.toBeNull();
186
+ });
187
+
188
+ it('forwards [label] changes to the auto-stamped spinner', async () => {
189
+ const { overlay } = mountInPositionedParent('<loading-overlay-ui label="Loading"></loading-overlay-ui>');
190
+ await tick();
191
+ overlay.label = 'Uploading';
192
+ await tick();
193
+ const sp = overlay.querySelector(':scope > spinner-ui[data-loading-overlay-auto]');
194
+ expect(sp.getAttribute('label')).toBe('Uploading');
195
+ });
196
+ });
197
+
198
+ // ── 5. CSS source contract ───────────────────────────────────────────
199
+
200
+ describe('loading-overlay-ui — CSS source contract', () => {
201
+ it('opens with the canonical @scope (loading-overlay-ui) block', () => {
202
+ expect(LO_CSS).toMatch(/^@scope \(loading-overlay-ui\)/);
203
+ });
204
+
205
+ it('declares both the :where(:scope) token block and a :scope base block', () => {
206
+ expect(LO_CSS).toMatch(/:where\(:scope\)\s*\{/);
207
+ expect(LO_CSS).toMatch(/^\s*:scope\s*\{/m);
208
+ });
209
+
210
+ it('declares the documented component tokens with -default suffix', () => {
211
+ for (const tok of [
212
+ '--loading-overlay-bg-default',
213
+ '--loading-overlay-radius-default',
214
+ '--loading-overlay-z-default',
215
+ '--loading-overlay-duration-default',
216
+ '--loading-overlay-easing-default',
217
+ '--loading-overlay-gap-default',
218
+ ]) {
219
+ expect(LO_CSS).toContain(tok);
220
+ }
221
+ });
222
+
223
+ it('positions absolutely with inset:0 (fills the parent)', () => {
224
+ expect(LO_CSS).toMatch(/position:\s*absolute/);
225
+ expect(LO_CSS).toMatch(/inset:\s*0/);
226
+ });
227
+
228
+ it('idle state: opacity:0 + pointer-events:none', () => {
229
+ // The base :scope block carries both — fail-fast if either is dropped.
230
+ expect(LO_CSS).toMatch(/opacity:\s*0/);
231
+ expect(LO_CSS).toMatch(/pointer-events:\s*none/);
232
+ });
233
+
234
+ it('active state: opacity:1 + pointer-events:auto', () => {
235
+ expect(LO_CSS).toMatch(/:scope\[active\][^}]*opacity:\s*1/);
236
+ expect(LO_CSS).toMatch(/:scope\[active\][^}]*pointer-events:\s*auto/);
237
+ });
238
+
239
+ it('transparent variant drops the backdrop fill', () => {
240
+ expect(LO_CSS).toMatch(/:scope\[variant="transparent"\][^}]*--loading-overlay-bg-default:\s*transparent/);
241
+ });
242
+
243
+ it('blur variant applies backdrop-filter blur', () => {
244
+ expect(LO_CSS).toMatch(/:scope\[variant="blur"\][^}]*backdrop-filter:\s*blur/);
245
+ });
246
+
247
+ it('reduced-motion media query suppresses the fade transition', () => {
248
+ expect(LO_CSS).toMatch(/@media\s*\(prefers-reduced-motion:\s*reduce\)/);
249
+ expect(LO_CSS).toMatch(/transition:\s*none/);
250
+ });
251
+
252
+ it('uses semantic --a-* tokens; no raw hex / rgb / oklch in the file', () => {
253
+ expect(LO_CSS).not.toMatch(/#[0-9a-fA-F]{3,8}\b/);
254
+ expect(LO_CSS).not.toMatch(/\brgb\s*\(/);
255
+ expect(LO_CSS).not.toMatch(/\boklch\s*\(/);
256
+ });
257
+ });