@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
@@ -21,18 +21,31 @@ inline upgrade prompts.
21
21
  import { UIElement } from '../../core/element.js';
22
22
 
23
23
  export type AlertCloseEvent = CustomEvent<unknown>;
24
+ export type AlertDunningActionEvent = CustomEvent<unknown>;
24
25
 
25
26
  export class UIAlert extends UIElement {
26
27
  /** Bold headline rendered as the first line of the alert content. Pair with [description] for the canonical "banner" pattern (headline + body). When [title] or [description] is set, the [text] prop is ignored. */
27
28
  title: string;
28
29
  /** Body text rendered as the second line of the alert content, below [title]. May be used alone (without [title]) for a single muted-body message. */
29
30
  description: string;
31
+ /** Dunning mode only — the unpaid amount as a decimal major-unit number (`24.50`, not minor units like `2450`). Formatted via `Intl.NumberFormat` using [currency] and [lang]. Ignored when [pattern] is not `dunning`. */
32
+ amount: number;
33
+ /** Dunning mode only — last 4 digits of the declined card. Rendered as "ending 4242". Ignored when [pattern] is not `dunning`. Attribute spelling: `card-last4`. */
34
+ cardLast4: string;
30
35
  /** Whether a close button is displayed. Alias [dismissible] is also accepted (same semantics, different spelling — the corpus and many libraries use both; both map to the same state). */
31
36
  closable: boolean;
37
+ /** Dunning mode only — ISO 4217 currency code driving the `Intl.NumberFormat` style="currency" rendering. Defaults to `USD`. */
38
+ currency: string;
32
39
  /** Public alias for [closable] — same semantics. Both attributes render the close button. Use whichever spelling matches your authoring style. */
33
40
  dismissible: boolean;
41
+ /** Dunning mode only — ISO 8601 due / failed-at timestamp. Formatted via `Intl.DateTimeFormat`. Ignored when [pattern] is not `dunning`. Attribute spelling: `due-at`. */
42
+ dueAt: string;
34
43
  /** Icon identifier displayed before the message content */
35
44
  icon: string;
45
+ /** Domain pattern mode. Default is empty (standard alert). Setting `pattern="dunning"` switches to billing dunning render mode — stamps a formatted amount + due-date + decline-reason from props and re-dispatches descendant `[data-dunning-action]` button clicks as a unified `dunning-action` event. Spec: SPEC-006. */
46
+ pattern: '' | 'dunning';
47
+ /** Dunning mode only — decline reason. Drives the leading icon (declined → `x-circle`, expired → `clock`, insufficient → `wallet`, network → `wifi-slash`). One of: `declined`, `expired`, `insufficient`, `network`, or empty. */
48
+ reason: '' | 'declined' | 'expired' | 'insufficient' | 'network';
36
49
  /** Single-line alert message. For two-line "headline + body" alerts, use [title] + [description] instead. For rich content (links, formatting), use the [slot="content"] slot. */
37
50
  text: string;
38
51
  /** Semantic color variant. */
@@ -44,4 +57,5 @@ export class UIAlert extends UIElement {
44
57
  options?: boolean | AddEventListenerOptions,
45
58
  ): void;
46
59
  addEventListener(type: 'close', listener: (ev: AlertCloseEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
60
+ addEventListener(type: 'dunning-action', listener: (ev: AlertDunningActionEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
47
61
  }
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIAlert } from './class.js';
13
+ import { UIAlert } from './alert.class.js';
14
14
 
15
15
  defineIfFree('alert-ui', UIAlert);
16
16
 
@@ -178,3 +178,187 @@ describe('alert-ui — full corpus shape (the §34 scenario)', () => {
178
178
  expect(content.querySelector('a').getAttribute('href')).toBe('/foo');
179
179
  });
180
180
  });
181
+
182
+ describe('alert-ui — SPEC-006 dunning pattern', () => {
183
+ beforeEach(() => { document.body.innerHTML = ''; });
184
+
185
+ it('formats amount via Intl.NumberFormat with currency', async () => {
186
+ const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
187
+ due-at="2026-05-20" card-last4="4242"
188
+ reason="declined" variant="danger"></alert-ui>`);
189
+ await tick();
190
+ const amt = a.querySelector('[data-dunning-amount]');
191
+ expect(amt).not.toBeNull();
192
+ // en-US default formatting (jsdom): "$24.50"
193
+ expect(amt.textContent).toMatch(/\$24\.50|USD\s*24\.50/);
194
+ });
195
+
196
+ it('formats due date via Intl.DateTimeFormat', async () => {
197
+ const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
198
+ due-at="2026-05-20T00:00:00Z"
199
+ reason="declined" variant="danger"></alert-ui>`);
200
+ await tick();
201
+ const meta = a.querySelector('[data-dunning-meta]');
202
+ expect(meta).not.toBeNull();
203
+ // Expect month name + day + year somewhere in the meta
204
+ expect(meta.textContent).toMatch(/May/);
205
+ expect(meta.textContent).toMatch(/2026/);
206
+ });
207
+
208
+ it('stamps the cardLast4 metadata', async () => {
209
+ const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
210
+ due-at="2026-05-20" card-last4="4242"
211
+ reason="declined" variant="danger"></alert-ui>`);
212
+ await tick();
213
+ const meta = a.querySelector('[data-dunning-meta]');
214
+ expect(meta.textContent).toContain('ending 4242');
215
+ });
216
+
217
+ it('stamps x-circle icon for reason="declined"', async () => {
218
+ const a = mount(`<alert-ui pattern="dunning" reason="declined" variant="danger"
219
+ amount="10" currency="USD"></alert-ui>`);
220
+ await tick();
221
+ const icon = a.querySelector('[slot="leading"]');
222
+ expect(icon).not.toBeNull();
223
+ expect(icon.getAttribute('name')).toBe('x-circle');
224
+ });
225
+
226
+ it('stamps clock icon for reason="expired"', async () => {
227
+ const a = mount(`<alert-ui pattern="dunning" reason="expired" variant="danger"
228
+ amount="10" currency="USD"></alert-ui>`);
229
+ await tick();
230
+ const icon = a.querySelector('[slot="leading"]');
231
+ expect(icon.getAttribute('name')).toBe('clock');
232
+ });
233
+
234
+ it('dispatches dunning-action event on descendant [data-dunning-action] click', async () => {
235
+ const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
236
+ due-at="2026-05-20" reason="declined" variant="danger">
237
+ <button-ui slot="actions" data-dunning-action="update" text="Update"></button-ui>
238
+ <button-ui slot="actions" data-dunning-action="retry" text="Retry"></button-ui>
239
+ </alert-ui>`);
240
+ await tick();
241
+
242
+ const received = [];
243
+ a.addEventListener('dunning-action', (e) => received.push(e.detail));
244
+
245
+ const updateBtn = a.querySelector('[data-dunning-action="update"]');
246
+ updateBtn.dispatchEvent(new Event('press', { bubbles: true }));
247
+ await tick();
248
+
249
+ expect(received.length).toBe(1);
250
+ expect(received[0].action).toBe('update');
251
+ expect(received[0].amount).toBe(24.5);
252
+ expect(received[0].currency).toBe('USD');
253
+ expect(received[0].dueAt).toBe('2026-05-20');
254
+ });
255
+
256
+ it('dispatches dunning-action with action="retry" when retry button is pressed', async () => {
257
+ const a = mount(`<alert-ui pattern="dunning" amount="10" currency="USD"
258
+ due-at="2026-05-20" reason="declined" variant="danger">
259
+ <button-ui slot="actions" data-dunning-action="retry" text="Retry"></button-ui>
260
+ </alert-ui>`);
261
+ await tick();
262
+
263
+ const received = [];
264
+ a.addEventListener('dunning-action', (e) => received.push(e.detail));
265
+
266
+ const retryBtn = a.querySelector('[data-dunning-action="retry"]');
267
+ retryBtn.dispatchEvent(new Event('press', { bubbles: true }));
268
+ await tick();
269
+
270
+ expect(received.length).toBe(1);
271
+ expect(received[0].action).toBe('retry');
272
+ });
273
+
274
+ it('re-formats amount when the [amount] attribute changes', async () => {
275
+ const a = mount(`<alert-ui pattern="dunning" amount="10" currency="USD"
276
+ due-at="2026-05-20" reason="declined" variant="danger"></alert-ui>`);
277
+ await tick();
278
+ let amt = a.querySelector('[data-dunning-amount]');
279
+ expect(amt.textContent).toMatch(/10/);
280
+
281
+ a.amount = 99.99;
282
+ await tick();
283
+ amt = a.querySelector('[data-dunning-amount]');
284
+ expect(amt.textContent).toMatch(/99\.99/);
285
+ });
286
+
287
+ it('inherits role="alert" for variant="danger" in dunning mode', async () => {
288
+ const a = mount(`<alert-ui pattern="dunning" variant="danger" amount="10" currency="USD"
289
+ due-at="2026-05-20" reason="declined"></alert-ui>`);
290
+ await tick();
291
+ expect(a.getAttribute('role')).toBe('alert');
292
+ });
293
+
294
+ it('inherits role="alert" for variant="warning" (grace period)', async () => {
295
+ const a = mount(`<alert-ui pattern="dunning" variant="warning" amount="10" currency="USD"
296
+ due-at="2026-06-01"></alert-ui>`);
297
+ await tick();
298
+ // warning maps to role=alert (same as danger via #updateRole)
299
+ expect(a.getAttribute('role')).toBe('alert');
300
+ });
301
+
302
+ it('grace-period (warning) uses "due" prefix in date meta, not "failed"', async () => {
303
+ const a = mount(`<alert-ui pattern="dunning" variant="warning" amount="10" currency="USD"
304
+ due-at="2026-06-01"></alert-ui>`);
305
+ await tick();
306
+ const meta = a.querySelector('[data-dunning-meta]');
307
+ expect(meta.textContent).toContain('due');
308
+ });
309
+
310
+ it('default title for declined is "Payment failed"', async () => {
311
+ const a = mount(`<alert-ui pattern="dunning" reason="declined" variant="danger"
312
+ amount="10" currency="USD"></alert-ui>`);
313
+ await tick();
314
+ const titleEl = a.querySelector('[data-dunning-title]');
315
+ expect(titleEl.textContent).toBe('Payment failed');
316
+ });
317
+
318
+ it('default title for expired is "Card expired"', async () => {
319
+ const a = mount(`<alert-ui pattern="dunning" reason="expired" variant="danger"
320
+ amount="10" currency="USD"></alert-ui>`);
321
+ await tick();
322
+ const titleEl = a.querySelector('[data-dunning-title]');
323
+ expect(titleEl.textContent).toBe('Card expired');
324
+ });
325
+
326
+ it('explicit [title] overrides the per-reason default', async () => {
327
+ const a = mount(`<alert-ui pattern="dunning" reason="declined" title="Custom message"
328
+ amount="10" currency="USD" variant="danger"></alert-ui>`);
329
+ await tick();
330
+ const titleEl = a.querySelector('[data-dunning-title]');
331
+ expect(titleEl.textContent).toBe('Custom message');
332
+ });
333
+
334
+ it('explicit [icon] overrides the per-reason icon map', async () => {
335
+ const a = mount(`<alert-ui pattern="dunning" reason="declined" icon="credit-card"
336
+ amount="10" currency="USD" variant="danger"></alert-ui>`);
337
+ await tick();
338
+ const icon = a.querySelector('[slot="leading"]');
339
+ expect(icon.getAttribute('name')).toBe('credit-card');
340
+ });
341
+
342
+ it('composes aria-label from title + amount + meta', async () => {
343
+ const a = mount(`<alert-ui pattern="dunning" reason="declined" amount="24.50"
344
+ currency="USD" due-at="2026-05-20" card-last4="4242"
345
+ variant="danger"></alert-ui>`);
346
+ await tick();
347
+ const aria = a.getAttribute('aria-label');
348
+ expect(aria).toContain('Payment failed');
349
+ expect(aria).toMatch(/24\.50/);
350
+ expect(aria).toMatch(/4242/);
351
+ });
352
+
353
+ it('does not stamp dunning content when pattern is empty (standard mode)', async () => {
354
+ const a = mount(`<alert-ui variant="danger" amount="24.50" currency="USD"
355
+ due-at="2026-05-20" reason="declined"
356
+ text="standard message"></alert-ui>`);
357
+ await tick();
358
+ // No dunning markers — uses the standard text-only render path
359
+ expect(a.querySelector('[data-dunning-amount]')).toBeNull();
360
+ expect(a.querySelector('[data-dunning-title]')).toBeNull();
361
+ const content = a.querySelector(':scope > [slot="content"]');
362
+ expect(content.textContent.trim()).toBe('standard message');
363
+ });
364
+ });
@@ -69,9 +69,73 @@ props:
69
69
  - danger
70
70
  - muted
71
71
  - neutral
72
+ pattern:
73
+ description: >-
74
+ Domain pattern mode. Default is empty (standard alert). Setting
75
+ `pattern="dunning"` switches to billing dunning render mode —
76
+ stamps a formatted amount + due-date + decline-reason from props
77
+ and re-dispatches descendant `[data-dunning-action]` button
78
+ clicks as a unified `dunning-action` event. Spec: SPEC-006.
79
+ type: string
80
+ default: ""
81
+ enum:
82
+ - ""
83
+ - dunning
84
+ amount:
85
+ description: >-
86
+ Dunning mode only — the unpaid amount as a decimal major-unit
87
+ number (`24.50`, not minor units like `2450`). Formatted via
88
+ `Intl.NumberFormat` using [currency] and [lang]. Ignored when
89
+ [pattern] is not `dunning`.
90
+ type: number
91
+ default: 0
92
+ currency:
93
+ description: >-
94
+ Dunning mode only — ISO 4217 currency code driving the
95
+ `Intl.NumberFormat` style="currency" rendering. Defaults to
96
+ `USD`.
97
+ type: string
98
+ default: USD
99
+ dueAt:
100
+ description: >-
101
+ Dunning mode only — ISO 8601 due / failed-at timestamp.
102
+ Formatted via `Intl.DateTimeFormat`. Ignored when [pattern] is
103
+ not `dunning`. Attribute spelling: `due-at`.
104
+ type: string
105
+ default: ""
106
+ attribute: due-at
107
+ cardLast4:
108
+ description: >-
109
+ Dunning mode only — last 4 digits of the declined card.
110
+ Rendered as "ending 4242". Ignored when [pattern] is not
111
+ `dunning`. Attribute spelling: `card-last4`.
112
+ type: string
113
+ default: ""
114
+ attribute: card-last4
115
+ reason:
116
+ description: >-
117
+ Dunning mode only — decline reason. Drives the leading icon
118
+ (declined → `x-circle`, expired → `clock`, insufficient →
119
+ `wallet`, network → `wifi-slash`). One of: `declined`,
120
+ `expired`, `insufficient`, `network`, or empty.
121
+ type: string
122
+ default: ""
123
+ enum:
124
+ - ""
125
+ - declined
126
+ - expired
127
+ - insufficient
128
+ - network
72
129
  events:
73
130
  close:
74
131
  description: Fired when the close button is clicked
132
+ dunning-action:
133
+ description: >-
134
+ Dunning pattern only — fired when a descendant button with
135
+ `[data-dunning-action]` is clicked. Detail shape `{ action,
136
+ amount, currency, dueAt }`; the action string is the value of
137
+ the `data-dunning-action` attribute (typically `"update"` or
138
+ `"retry"`).
75
139
  slots:
76
140
  content:
77
141
  description: >-
@@ -94,6 +158,12 @@ slots:
94
158
  Optional trailing action button (e.g. "Refresh", "Status page").
95
159
  Right-aligned in the flex layout. See system-banners pattern for
96
160
  examples.
161
+ actions:
162
+ description: >-
163
+ Dunning pattern only — action button stack (typically two
164
+ `<button-ui>` with `data-dunning-action="update"` and
165
+ `data-dunning-action="retry"`). Sits below the formatted message
166
+ in the col layout when `pattern="dunning"`.
97
167
  states:
98
168
  - name: idle
99
169
  description: Default, ready for interaction.
@@ -107,7 +177,19 @@ a2ui:
107
177
  reason: 'Ephemeral vs persistent boundary.'
108
178
  - rule: 'For modal-style critical alerts use <modal-ui> with alert content.'
109
179
  reason: 'Inline vs modal.'
110
- anti_patterns: []
180
+ - rule: 'Billing dunning / payment-failed notices use pattern="dunning" + amount + currency + dueAt props, NOT inlined into title/description strings.'
181
+ reason: 'Intl-formatting + unified telemetry via dunning-action event.'
182
+ - rule: 'pattern="dunning" SHOULD use variant="danger" (default) or variant="warning" (grace period); never variant="info" or "success".'
183
+ reason: 'Dunning is always a failure surface.'
184
+ - rule: 'When pattern="dunning", slot at least one button-ui in slot="actions" with data-dunning-action ("update" or "retry").'
185
+ reason: 'No actions = no resolution path.'
186
+ anti_patterns:
187
+ - wrong: '<alert-ui pattern="dunning" variant="info">'
188
+ why: 'Dunning is always a failure surface; info mis-signals severity.'
189
+ fix: '<alert-ui pattern="dunning" variant="danger">'
190
+ - wrong: '<alert-ui pattern="dunning" title="Payment failed $24.50">'
191
+ why: 'Amount baked into title defeats Intl formatting + telemetry.'
192
+ fix: '<alert-ui pattern="dunning" amount="24.50" currency="USD" due-at="2026-05-20">'
111
193
  examples:
112
194
  - name: alert-banner
113
195
  description: Single alert component with icon and dismiss action.
@@ -164,6 +246,37 @@ examples:
164
246
  "variant": "primary"
165
247
  }
166
248
  ]
249
+ - name: dunning-payment-failed
250
+ description: Billing dunning banner — declined card with retry + update-card actions (SPEC-006).
251
+ a2ui: >-
252
+ [
253
+ {
254
+ "id": "root",
255
+ "component": "Alert",
256
+ "pattern": "dunning",
257
+ "variant": "danger",
258
+ "amount": 24.5,
259
+ "currency": "USD",
260
+ "dueAt": "2026-05-20T00:00:00Z",
261
+ "cardLast4": "4242",
262
+ "reason": "declined",
263
+ "children": ["btn-update", "btn-retry"]
264
+ },
265
+ {
266
+ "id": "btn-update",
267
+ "component": "Button",
268
+ "text": "Update payment method",
269
+ "variant": "primary",
270
+ "slot": "actions"
271
+ },
272
+ {
273
+ "id": "btn-retry",
274
+ "component": "Button",
275
+ "text": "Retry charge",
276
+ "variant": "outline",
277
+ "slot": "actions"
278
+ }
279
+ ]
167
280
  keywords:
168
281
  - alert
169
282
  - inbox
@@ -1,9 +1,9 @@
1
1
  # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
2
  #
3
3
  # §176 (v0.5.5): authored to close the §175 baseline-orphan class. The
4
- # component already existed as a sibling class in the parent's class.js
4
+ # component already existed as a sibling class in the parent's avatar.class.js
5
5
  # + was registered alongside the parent (e.g. UIList + UIListItem both
6
- # from list/class.js). The catalog just lacked its own entry. With the
6
+ # from list/list.class.js). The catalog just lacked its own entry. With the
7
7
  # §172 sibling-yaml scanner, this file gets picked up next to the parent
8
8
  # yaml.
9
9
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIAvatar, UIAvatarGroup } from './class.js';
13
+ import { UIAvatar, UIAvatarGroup } from './avatar.class.js';
14
14
 
15
15
  defineIfFree('avatar-ui', UIAvatar);
16
16
  defineIfFree('avatar-group-ui', UIAvatarGroup);
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIBadge } from './class.js';
13
+ import { UIBadge } from './badge.class.js';
14
14
 
15
15
  defineIfFree('badge-ui', UIBadge);
16
16
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIBlock } from './class.js';
13
+ import { UIBlock } from './block.class.js';
14
14
 
15
15
  defineIfFree('block-ui', UIBlock);
16
16
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIBreadcrumb } from './class.js';
13
+ import { UIBreadcrumb } from './breadcrumb.class.js';
14
14
 
15
15
  defineIfFree('breadcrumb-ui', UIBreadcrumb);
16
16
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIButton } from './class.js';
13
+ import { UIButton } from './button.class.js';
14
14
 
15
15
  defineIfFree('button-ui', UIButton);
16
16
 
@@ -31,6 +31,16 @@
31
31
  "type": "string",
32
32
  "default": ""
33
33
  },
34
+ "rangeEnd": {
35
+ "description": "End of a date range (ISO YYYY-MM-DD). See `rangeStart` for the full contract.",
36
+ "type": "string",
37
+ "default": ""
38
+ },
39
+ "rangeStart": {
40
+ "description": "Start of a date range (ISO YYYY-MM-DD). When both rangeStart and rangeEnd are set + ordered, day cells strictly between the endpoints get `[data-in-range]` stamped for visual continuity. Used by `<date-range-picker-ui>` which pushes the same from/to onto both calendar panes. Endpoints themselves render via the `value` prop's `[data-selected]` state.",
41
+ "type": "string",
42
+ "default": ""
43
+ },
34
44
  "readonly": {
35
45
  "description": "Blocks selection but allows month navigation.",
36
46
  "type": "boolean",
@@ -65,6 +65,14 @@ export class UICalendarGrid extends UIElement {
65
65
  max: { type: String, default: '', reflect: true },
66
66
  disabled: { type: Boolean, default: false, reflect: true },
67
67
  readonly: { type: Boolean, default: false, reflect: true },
68
+ // Range-aware highlighting — when both rangeStart and rangeEnd are
69
+ // set, day cells whose date falls strictly between them get
70
+ // `[data-in-range]` stamped on them (the endpoints themselves get
71
+ // `[data-selected]` via the `value` prop). Consumers like
72
+ // <date-range-picker-ui> push the same from/to onto both panes so
73
+ // each independently lights up its in-range cells.
74
+ rangeStart: { type: String, default: '', reflect: true, attribute: 'range-start' },
75
+ rangeEnd: { type: String, default: '', reflect: true, attribute: 'range-end' },
68
76
  };
69
77
 
70
78
  // No html`` template — we render imperatively via #renderCalendar so we can
@@ -142,6 +150,11 @@ export class UICalendarGrid extends UIElement {
142
150
  const selected = parseISO(this.value);
143
151
  const minDate = parseISO(this.min);
144
152
  const maxDate = parseISO(this.max);
153
+ // Range endpoints — only honor when BOTH are set + valid + ordered.
154
+ const rs = parseISO(this.rangeStart);
155
+ const re = parseISO(this.rangeEnd);
156
+ const rangeFrom = (rs && re && rs <= re) ? rs : null;
157
+ const rangeTo = (rs && re && rs <= re) ? re : null;
145
158
 
146
159
  // Header — Prev / Title / Next
147
160
  let h = `<div data-cal-header>
@@ -174,15 +187,28 @@ export class UICalendarGrid extends UIElement {
174
187
  const isSelected = selected && sameDay(date, selected);
175
188
  const isDisabled = (minDate && date < minDate) || (maxDate && date > maxDate);
176
189
  const isFocused = this.#focusedDay === d;
190
+ // In-range: strictly between rangeStart and rangeEnd (endpoints
191
+ // themselves render as `[data-selected]` via the `value` prop;
192
+ // marking them in-range too would double-up the styling).
193
+ const isInRange = !!(rangeFrom && rangeTo
194
+ && date > rangeFrom && date < rangeTo);
195
+ // Range endpoint flags — let CSS render the start/end cell with
196
+ // half-pill caps so the in-range strip reads as a continuous
197
+ // band when displayed next to the in-range cells.
198
+ const isRangeStart = !!(rangeFrom && sameDay(date, rangeFrom));
199
+ const isRangeEnd = !!(rangeTo && sameDay(date, rangeTo));
177
200
 
178
201
  const attrs = [
179
202
  'type="button"',
180
203
  'data-cal-day',
181
204
  `data-date="${iso}"`,
182
- isToday ? 'data-today' : '',
183
- isSelected ? 'data-selected' : '',
184
- isDisabled ? 'disabled' : '',
185
- isFocused ? 'data-focused' : '',
205
+ isToday ? 'data-today' : '',
206
+ isSelected ? 'data-selected' : '',
207
+ isInRange ? 'data-in-range' : '',
208
+ isRangeStart ? 'data-range-start' : '',
209
+ isRangeEnd ? 'data-range-end' : '',
210
+ isDisabled ? 'disabled' : '',
211
+ isFocused ? 'data-focused' : '',
186
212
  `tabindex="${isFocused ? '0' : '-1'}"`,
187
213
  ].filter(Boolean).join(' ');
188
214
 
@@ -52,6 +52,12 @@
52
52
  --calendar-grid-day-fg-hover-default: var(--a-fg);
53
53
  --calendar-grid-day-bg-selected-default: var(--a-accent);
54
54
  --calendar-grid-day-fg-selected-default: var(--a-accent-fg);
55
+ /* In-range cells (strictly between rangeStart and rangeEnd) — muted
56
+ accent strip so the endpoints (full --a-accent fill) read as the
57
+ primary selection while the in-range band reads as secondary
58
+ context. Stylable independently of `selected`. */
59
+ --calendar-grid-day-bg-in-range-default: var(--a-accent-muted);
60
+ --calendar-grid-day-fg-in-range-default: var(--a-fg);
55
61
  --calendar-grid-day-fg-outside-default: var(--a-fg-muted);
56
62
  --calendar-grid-day-fg-disabled-default: var(--a-ui-text-disabled);
57
63
  --calendar-grid-day-today-color-default: var(--a-accent);
@@ -193,6 +199,20 @@
193
199
  color: var(--calendar-grid-day-fg-selected, var(--calendar-grid-day-fg-selected-default));
194
200
  }
195
201
 
202
+ /* ── State: in-range ──
203
+ Cells strictly between rangeStart and rangeEnd get the muted accent
204
+ strip. Endpoints themselves render as `[data-selected]` (full accent
205
+ fill); the in-range middle is visually subordinate. `:not([data-selected])`
206
+ guard prevents the muted fill from overriding the endpoint cells in
207
+ consumer code that sets both `value` AND `range-start`/`range-end`. */
208
+ [data-cal-day][data-in-range]:not([data-selected]) {
209
+ background: var(--calendar-grid-day-bg-in-range, var(--calendar-grid-day-bg-in-range-default));
210
+ color: var(--calendar-grid-day-fg-in-range, var(--calendar-grid-day-fg-in-range-default));
211
+ }
212
+ [data-cal-day][data-in-range]:not([data-selected]):not([disabled]):hover {
213
+ background: var(--calendar-grid-day-bg-in-range, var(--calendar-grid-day-bg-in-range-default));
214
+ }
215
+
196
216
  /* ── State: today (unselected) ── */
197
217
  [data-cal-day][data-today]:not([data-selected]) {
198
218
  position: relative;
@@ -22,6 +22,10 @@ export class UICalendarGrid extends UIElement {
22
22
  max: string;
23
23
  /** Minimum selectable date in ISO format (YYYY-MM-DD). */
24
24
  min: string;
25
+ /** End of a date range (ISO YYYY-MM-DD). See `rangeStart` for the full contract. */
26
+ rangeEnd: string;
27
+ /** Start of a date range (ISO YYYY-MM-DD). When both rangeStart and rangeEnd are set + ordered, day cells strictly between the endpoints get `[data-in-range]` stamped for visual continuity. Used by `<date-range-picker-ui>` which pushes the same from/to onto both calendar panes. Endpoints themselves render via the `value` prop's `[data-selected]` state. */
28
+ rangeStart: string;
25
29
  /** Blocks selection but allows month navigation. */
26
30
  readonly: boolean;
27
31
  /** Selected date in ISO format (YYYY-MM-DD). */
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UICalendarGrid } from './class.js';
13
+ import { UICalendarGrid } from './calendar-grid.class.js';
14
14
 
15
15
  defineIfFree('calendar-grid-ui', UICalendarGrid);
16
16
 
@@ -42,6 +42,26 @@ props:
42
42
  type: boolean
43
43
  default: false
44
44
  reflect: true
45
+ rangeStart:
46
+ description: >-
47
+ Start of a date range (ISO YYYY-MM-DD). When both rangeStart and
48
+ rangeEnd are set + ordered, day cells strictly between the
49
+ endpoints get `[data-in-range]` stamped for visual continuity.
50
+ Used by `<date-range-picker-ui>` which pushes the same from/to
51
+ onto both calendar panes. Endpoints themselves render via the
52
+ `value` prop's `[data-selected]` state.
53
+ type: string
54
+ default: ''
55
+ reflect: true
56
+ attribute: range-start
57
+ rangeEnd:
58
+ description: >-
59
+ End of a date range (ISO YYYY-MM-DD). See `rangeStart` for the
60
+ full contract.
61
+ type: string
62
+ default: ''
63
+ reflect: true
64
+ attribute: range-end
45
65
  events:
46
66
  change:
47
67
  description: User selected a date. detail.value is the ISO string.
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UICalendarPicker } from './class.js';
13
+ import { UICalendarPicker } from './calendar-picker.class.js';
14
14
 
15
15
  defineIfFree('calendar-picker-ui', UICalendarPicker);
16
16
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UICard } from './class.js';
13
+ import { UICard } from './card.class.js';
14
14
 
15
15
  defineIfFree('card-ui', UICard);
16
16
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIChart } from './class.js';
13
+ import { UIChart } from './chart.class.js';
14
14
 
15
15
  defineIfFree('chart-ui', UIChart);
16
16
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIChartLegend } from './class.js';
13
+ import { UIChartLegend } from './chart-legend.class.js';
14
14
 
15
15
  defineIfFree('chart-legend-ui', UIChartLegend);
16
16