@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.6.35] — 2026-05-24
4
+
5
+ ### Fixed — `<popover-ui>` + `<menu-ui>` yaml `slots:` blocks declare the canonical `trigger` / `content` vocabulary (FB-62)
6
+
7
+ - **CSS positioned slots that the yaml said didn't exist.** `popover.yaml` pre-patch line 43: `slots: {}` (empty). But `popover.css` positions `[slot="trigger"]` and `[slot="content"]` across 7 selectors (open/closed states + first/last-child margin guards), and the yaml's own a2ui rule already states "popover-ui wraps a focusable trigger (slot=\"trigger\") + arbitrary interactive content (slot=\"content\")". Parallel case in `menu.yaml`: `slots:` declared only `default`, but the a2ui rule said "MUST have exactly one child with slot=\"trigger\"". Three sources said the slots existed, one source (the structured `slots:` SoT) said they didn't. Surfaced as 6 INFO findings in the adia-ui-kit v3.4.X recipe-staleness audit; suppressed via `KNOWN_PARENT_SLOT_DRIFT`.
8
+ - **Fix**: `popover.yaml` `slots:` now declares `trigger` (focusable element that opens the popover; MUST be focusable for keyboard accessibility — bare `<span>`/`<div>` won't work) and `content` (the body, hoisted to top-layer via `:popover-open`). `menu.yaml` `slots:` adds `trigger` (Required — exactly one child must have `slot="trigger"`; without it the menu cannot open). Pure yaml backfill — no JS, no CSS, no API change. Sidecar `.a2ui.json` files regenerated. Kit's `KNOWN_PARENT_SLOT_DRIFT` allowlist entries can be removed next cycle. Files: `components/popover/popover.{yaml,a2ui.json}`, `components/menu/menu.{yaml,a2ui.json}`.
9
+
10
+ ### Fixed — chat-input + textarea + input substrate prose accurately documents Enter→submit (FB-63)
11
+
12
+ - **`submit-on-enter` was a documentation phantom across the substrate, and the prose around it carried two adjacent lies.** Three input primitives all dispatch a bubbling `submit` event on Enter (without Shift) unconditionally: `textarea.class.js:97-106`, `input.class.js:713-727`, and `chat-input.js` (which delegates to the inner `<textarea-ui>`'s submit event at line 147). None of them observe a `[submit-on-enter]` attribute — yet the substrate's own demos, yamls, and a2ui rules used `<chat-input-ui submit-on-enter>` as canonical authoring shape. Two adjacent lies discovered in the same cluster: (1) `textarea.yaml` description + a2ui rule + `textarea.examples.html` claimed "Enter inserts a newline — textarea-ui does NOT emit a `submit` event" — false per `textarea.class.js:97-106`. (2) `input.yaml` + `input.examples.html` claimed "only chat-input-ui reflects the [submit-on-enter] attribute" — false, NONE of them reflect it.
13
+ - **Fix**: `submit-on-enter` stripped from `patterns/chat-shell/chat-shell.examples.html` (1 occurrence). `chat-input.yaml` event description + `chat-input.js` docstring now state the truth — Enter→submit is unconditional, delegated from inner `<textarea-ui>`, no opt-in/opt-out. `textarea.yaml` description (line 10-18) + a2ui rule (line 119-123) + `textarea.examples.html:253` corrected: "Enter (without Shift) dispatches a bubbling `submit` event; Shift+Enter inserts a newline. Unconditional — no opt-in/opt-out attribute." `input.yaml` + `input.examples.html` adjacent prose: stripped "only chat-input-ui reflects [submit-on-enter]" claim; replaced with accurate "the plain `<input-ui>` primitive ALSO fires `submit` on Enter (unconditional, no opt-in attribute); `<chat-input-ui>` simply builds on that semantic." Companion strip in `@adia-ai/web-modules` chat-cluster (see its changelog). Sweep-grep `submit-on-enter` in authored sources now returns only the two truth-note phrasings (both say "no `[submit-on-enter]` opt-in/opt-out"). Sidecar `.a2ui.json` files regenerated; corpus catalog + rules text rebuilt. Files: `components/chat-thread/chat-input.{yaml,js,a2ui.json}`, `components/textarea/textarea.{yaml,examples.html,a2ui.json}`, `components/input/input.{yaml,examples.html,a2ui.json}`, `patterns/chat-shell/chat-shell.examples.html`.
14
+
15
+ ### Fixed — `<spinner-ui variant="dots">` dot spacing now even (was lopsided)
16
+
17
+ - **The pre-fix dots layout used `::before` + `::after` pseudo-elements** as the outer two dots and a `box-shadow` on `::before` to paint the middle dot. Three breakages: (1) the box-shadow middle dot doesn't participate in the host's flex-gap math, so the gap between dot1↔dot2 and dot2↔dot3 was unequal (the screenshot showed two dots close together on the left, then one isolated on the right). (2) The box-shadow dot couldn't carry its own `@keyframes` animation, so the middle stayed at default scale while the outer two bounced — a "middle is anchored" look. (3) The author comment explicitly acknowledged the hack ("simpler, more robust shape is to overlay three identical dot pseudos via a flexbox container of pseudo + two child pseudos isn't possible without a stamped child"). Fix: `class.js` `#syncDots()` now stamps three real `<span data-spinner-dot=N>` children when `variant="dots"`. Flex-gap distributes the row evenly; each child gets `animation-delay: 0 / T/3 / 2T/3` so the bounce reads as a left-to-right wave. Reduced-motion + `[paused]` selectors updated to include `> [data-spinner-dot]`. Files: `components/spinner/class.js`, `components/spinner/spinner.css`, `components/spinner/spinner.test.js`.
18
+
19
+ ### Added — `<spinner-ui variant="knight">` knight-rider sliding-bar variant
20
+
21
+ - **NEW `knight` variant** — a horizontal track with a thumb that slides back-and-forth via `animation-direction: alternate`. Reads as a determinate-looking bar but is indeterminate by intent (no progress value; same loading semantics as the other variants). Tokens added: `--spinner-bar-track-width` (default `4rem`), `--spinner-bar-track-height` (default `var(--spinner-stroke)` — visual continuity with arc/ring), `--spinner-bar-thumb-ratio` (default `0.3` — thumb is 30% of track). Keyframe math: `translateX((1 / ratio - 1) * 100%)` of thumb-relative width = `(track - thumb)` px regardless of track-width override. Reduced-motion path hides the thumb and falls through to the shared ellipsis fallback. yaml `variant` enum + description updated. Files: `components/spinner/spinner.css`, `components/spinner/spinner.yaml`, `components/spinner/spinner.examples.html`.
22
+
23
+ ### Added — `<integration-card-ui>` demo page now ships a live CRUD drawer wiring
24
+
25
+ - **`integration-card.examples.html` previously showed cards only as static visual showcases** — clicking `Connect` / `Configure` dispatched the `connect` / `configure` events into the void (no consumer wiring on the docs page). Added a "Wired to drawer (CRUD)" section at the top with three cards (`slack` / `github` / `linear`) bound to a shared `<drawer-ui>` form. NEW `integration-card.examples.js` setup module: listens for bubbled events at the grid level, opens the drawer in `connect` or `configure` mode, persists token to an in-memory store keyed by `provider`, and on Save / Disconnect mutates the originating card's `status` attribute. Demonstrates the full Create / Update / Delete loop expected of consumers per SPEC-062 §134-136. Sitemap entry gains `"setup": ".../integration-card.examples.js"` so the docs router auto-imports the module on route load (same pattern as `agent-feedback-bar`, etc.). Files: `components/integration-card/integration-card.examples.html`, `components/integration-card/integration-card.examples.js` (NEW), `site/sitemap.json`.
26
+
27
+ ### Changed — `<date-range-picker-ui>` trigger surface mirrors `<select-ui>` / `<calendar-picker-ui>`
28
+
29
+ - **`date-range-picker.css` had only `:scope > [slot="trigger"] { min-width: 14em }`** — the entire trigger chrome relied on `<button-ui variant="outline">`'s defaults, which produced an off-pattern surface vs. the rest of the input/picker family. Trigger now styled with the canonical contract: flex row, `min-height: var(--a-size)`, `padding: 0 var(--a-ui-px)`, `border-radius: var(--a-radius)`, `border: 1px solid var(--a-ui-border)`, `background: var(--a-ui-bg)`, `justify-content: space-between` so leading icon + label sit start, trailing caret sits end. Adds canonical `--date-range-picker-trigger-{height,px,gap,radius,bg,bg-hover,fg,border,border-hover,focus-ring}` token shelf — same tokens `<calendar-picker-ui>` already exposes, so consumers can override either family-side. Hover/focus-visible rules attached for parity. Files: `components/date-range-picker/date-range-picker.css`.
30
+
31
+ ### Fixed — `<spinner-ui>` rotational cadence (was 300ms, now 0.8s)
32
+
33
+ - **`--spinner-duration-default` retargeted from `var(--a-duration-slow)` (300 ms — interaction-duration scale, tuned for hover/focus transitions) to `0.8s`** — canonical loader cadence. A full revolution at 300 ms reads as a frantic blur; 0.8 s gives smooth, perceptible motion that signals "loading" without alarming the eye. The `--a-duration-*` token family (120 / 250 / 300 ms) is now correctly reserved for state transitions; spinner uses its own rotational tempo. Files: `components/spinner/spinner.css`.
34
+
35
+ ### Fixed — `<modal-ui>` header no longer reserves a phantom flex slot for empty `text=`
36
+
37
+ - **`modal.css`'s `:scope [slot="header"]::before { content: attr(text); flex: 1 }` rule generated an empty-content pseudo-element with `flex: 1` whenever the host's `text` attr was empty or unset.** That phantom flex slot claimed the row's free space and pushed any slotted header content (e.g., `<confirm-dialog-ui>`'s stamped `<text-ui part="title">` + auto-stamped close button) to the trailing edge — the title rendered tight against the X close button instead of left-aligned. Same drift class as the `tag-ui` / `badge-ui` / `segment-ui` / `button-ui` phantom-gap arc (April–May 2026).
38
+ - **Fix (defense-in-depth)**: CSS gate the `::before` on `[text]:not([text=""])` so the pseudo isn't generated when text is absent or empty; class.js no longer writes `text=""` on the header when `this.text` is falsy. Either side alone would fix the symptom; both prevent recurrence from independent edit paths. Visible effect: `<confirm-dialog-ui>` now renders "Save changes?" left-aligned with X on the right, matching the canonical header pattern. Files: `components/modal/modal.css`, `components/modal/class.js`.
39
+
40
+ ### Added — `<calendar-grid-ui>` `range-start` / `range-end` props for in-range cell highlighting
41
+
42
+ - **`<calendar-grid-ui>` gains two new reflected string props — `range-start` and `range-end` (ISO `YYYY-MM-DD`).** When both are set + ordered, day cells strictly between the endpoints get `[data-in-range]` stamped on them. New CSS state styles in-range cells with `--calendar-grid-day-bg-in-range` (defaults to `--a-accent-muted` — the muted accent strip) and `--calendar-grid-day-fg-in-range`. The endpoints themselves continue rendering via the `value` prop's `[data-selected]` state (full `--a-accent` fill); the in-range middle is visually subordinate. Per-day cells also get `data-range-start` / `data-range-end` markers for future shape work (rounded caps, etc.).
43
+ - **`<date-range-picker-ui>` now wires this contract**: in `render()`, the effective from/to (pending click-state OR committed value) is pushed onto BOTH calendar-grid panes via `range-start` / `range-end` attributes, so each pane independently lights up its in-range cells. Closes the "selected start + end but no fill between" trap. Files: `components/calendar-grid/calendar-grid.css`, `components/calendar-grid/class.js`, `components/calendar-grid/calendar-grid.yaml`, `components/date-range-picker/class.js`.
44
+
3
45
  ## [0.6.34] — 2026-05-23
4
46
 
5
47
  ### Added — Wave 1 + Wave 2: 10 P1 components shipped from SPEC pilot
package/color/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * §301 (v0.5.12, FEEDBACK-29 re-bucket from v0.6.0). Pure functions; no
7
7
  * DOM/runtime side-effects. Pre-§301 these helpers lived inline at
8
- * `components/color-picker/class.js:34-121`. Extracted into a shared
8
+ * `components/color-picker/color-picker.class.js:34-121`. Extracted into a shared
9
9
  * subpath so consumers (Tokens Studio, custom palette tools) don't
10
10
  * re-implement OKLCH math.
11
11
  *
@@ -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 accordion.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 { UIAccordion, UIAccordionItem } from './class.js';
13
+ import { UIAccordion, UIAccordionItem } from './accordion.class.js';
14
14
 
15
15
  defineIfFree('accordion-ui', UIAccordion);
16
16
  defineIfFree('accordion-item-ui', UIAccordionItem);
@@ -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 action-list.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 { UIActionList, UIActionItem } from './class.js';
13
+ import { UIActionList, UIActionItem } from './action-list.class.js';
14
14
 
15
15
  defineIfFree('action-list-ui', UIActionList);
16
16
  defineIfFree('action-item-ui', UIActionItem);
@@ -59,7 +59,7 @@ export class UIAgentArtifact extends UIElement {
59
59
  };
60
60
 
61
61
  // §205 (v0.5.7): dynamic chevron icons stamped on collapse/expand state
62
- // transition (class.js:119+188). Per FEEDBACK-16 §1 + §209 slot-11 ternary-
62
+ // transition (agent-artifact.class.js:119+188). Per FEEDBACK-16 §1 + §209 slot-11 ternary-
63
63
  // walker discovery. Note: `this.icon` consumer-supplied — not declared here.
64
64
  static requiredIcons = ['caret-right', 'caret-down'];
65
65
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIAgentArtifact } from './class.js';
13
+ import { UIAgentArtifact } from './agent-artifact.class.js';
14
14
 
15
15
  defineIfFree('agent-artifact-ui', UIAgentArtifact);
16
16
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIAgentFeedbackBar } from './class.js';
13
+ import { UIAgentFeedbackBar } from './agent-feedback-bar.class.js';
14
14
 
15
15
  defineIfFree('agent-feedback-bar-ui', UIAgentFeedbackBar);
16
16
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIAgentQuestions } from './class.js';
13
+ import { UIAgentQuestions } from './agent-questions.class.js';
14
14
 
15
15
  defineIfFree('agent-questions-ui', UIAgentQuestions);
16
16
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIAgentReasoning } from './class.js';
13
+ import { UIAgentReasoning } from './agent-reasoning.class.js';
14
14
 
15
15
  defineIfFree('agent-reasoning-ui', UIAgentReasoning);
16
16
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIAgentSuggestions } from './class.js';
13
+ import { UIAgentSuggestions } from './agent-suggestions.class.js';
14
14
 
15
15
  defineIfFree('agent-suggestions-ui', UIAgentSuggestions);
16
16
 
@@ -23,6 +23,16 @@
23
23
  "type": "string",
24
24
  "default": ""
25
25
  },
26
+ "amount": {
27
+ "description": "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`.",
28
+ "type": "number",
29
+ "default": 0
30
+ },
31
+ "cardLast4": {
32
+ "description": "Dunning mode only — last 4 digits of the declined card. Rendered as \"ending 4242\". Ignored when [pattern] is not `dunning`. Attribute spelling: `card-last4`.",
33
+ "type": "string",
34
+ "default": ""
35
+ },
26
36
  "closable": {
27
37
  "description": "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).",
28
38
  "type": "boolean",
@@ -31,16 +41,47 @@
31
41
  "component": {
32
42
  "const": "Alert"
33
43
  },
44
+ "currency": {
45
+ "description": "Dunning mode only — ISO 4217 currency code driving the `Intl.NumberFormat` style=\"currency\" rendering. Defaults to `USD`.",
46
+ "type": "string",
47
+ "default": "USD"
48
+ },
34
49
  "dismissible": {
35
50
  "description": "Public alias for [closable] — same semantics. Both attributes render the close button. Use whichever spelling matches your authoring style.",
36
51
  "type": "boolean",
37
52
  "default": false
38
53
  },
54
+ "dueAt": {
55
+ "description": "Dunning mode only — ISO 8601 due / failed-at timestamp. Formatted via `Intl.DateTimeFormat`. Ignored when [pattern] is not `dunning`. Attribute spelling: `due-at`.",
56
+ "type": "string",
57
+ "default": ""
58
+ },
39
59
  "icon": {
40
60
  "description": "Icon identifier displayed before the message content",
41
61
  "type": "string",
42
62
  "default": ""
43
63
  },
64
+ "pattern": {
65
+ "description": "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.",
66
+ "type": "string",
67
+ "enum": [
68
+ "",
69
+ "dunning"
70
+ ],
71
+ "default": ""
72
+ },
73
+ "reason": {
74
+ "description": "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.",
75
+ "type": "string",
76
+ "enum": [
77
+ "",
78
+ "declined",
79
+ "expired",
80
+ "insufficient",
81
+ "network"
82
+ ],
83
+ "default": ""
84
+ },
44
85
  "text": {
45
86
  "description": "Single-line alert message. For two-line \"headline + body\" alerts, use [title] + [description] instead. For rich content (links, formatting), use the [slot=\"content\"] slot.",
46
87
  "type": "string",
@@ -66,12 +107,26 @@
66
107
  ],
67
108
  "unevaluatedProperties": false,
68
109
  "x-adiaui": {
69
- "anti_patterns": [],
110
+ "anti_patterns": [
111
+ {
112
+ "fix": "<alert-ui pattern=\"dunning\" variant=\"danger\">",
113
+ "why": "Dunning is always a failure surface; info mis-signals severity.",
114
+ "wrong": "<alert-ui pattern=\"dunning\" variant=\"info\">"
115
+ },
116
+ {
117
+ "fix": "<alert-ui pattern=\"dunning\" amount=\"24.50\" currency=\"USD\" due-at=\"2026-05-20\">",
118
+ "why": "Amount baked into title defeats Intl formatting + telemetry.",
119
+ "wrong": "<alert-ui pattern=\"dunning\" title=\"Payment failed $24.50\">"
120
+ }
121
+ ],
70
122
  "category": "container",
71
123
  "composes": [],
72
124
  "events": {
73
125
  "close": {
74
126
  "description": "Fired when the close button is clicked"
127
+ },
128
+ "dunning-action": {
129
+ "description": "Dunning pattern only — fired when a descendant button with `[data-dunning-action]` is clicked. Detail shape `{ action, amount, currency, dueAt }`; the action string is the value of the `data-dunning-action` attribute (typically `\"update\"` or `\"retry\"`)."
75
130
  }
76
131
  },
77
132
  "examples": [
@@ -84,6 +139,11 @@
84
139
  "description": "Card with error alert and a retry button for error state display.",
85
140
  "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"Card\",\n \"children\": [\n \"sec\",\n \"ftr\"\n ]\n },\n {\n \"id\": \"sec\",\n \"component\": \"Section\",\n \"children\": [\n \"alert\"\n ]\n },\n {\n \"id\": \"alert\",\n \"component\": \"Alert\",\n \"variant\": \"error\",\n \"title\": \"Something went wrong\",\n \"description\": \"We encountered an error while loading the data. Please try again.\"\n },\n {\n \"id\": \"ftr\",\n \"component\": \"Footer\",\n \"children\": [\n \"retry\"\n ]\n },\n {\n \"id\": \"retry\",\n \"component\": \"Button\",\n \"text\": \"Retry\",\n \"icon\": \"refresh\",\n \"variant\": \"primary\"\n }\n]",
86
141
  "name": "error-state"
142
+ },
143
+ {
144
+ "description": "Billing dunning banner — declined card with retry + update-card actions (SPEC-006).",
145
+ "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"Alert\",\n \"pattern\": \"dunning\",\n \"variant\": \"danger\",\n \"amount\": 24.5,\n \"currency\": \"USD\",\n \"dueAt\": \"2026-05-20T00:00:00Z\",\n \"cardLast4\": \"4242\",\n \"reason\": \"declined\",\n \"children\": [\"btn-update\", \"btn-retry\"]\n },\n {\n \"id\": \"btn-update\",\n \"component\": \"Button\",\n \"text\": \"Update payment method\",\n \"variant\": \"primary\",\n \"slot\": \"actions\"\n },\n {\n \"id\": \"btn-retry\",\n \"component\": \"Button\",\n \"text\": \"Retry charge\",\n \"variant\": \"outline\",\n \"slot\": \"actions\"\n }\n]",
146
+ "name": "dunning-payment-failed"
87
147
  }
88
148
  ],
89
149
  "keywords": [
@@ -112,6 +172,9 @@
112
172
  "action": {
113
173
  "description": "Optional trailing action button (e.g. \"Refresh\", \"Status page\"). Right-aligned in the flex layout. See system-banners pattern for examples."
114
174
  },
175
+ "actions": {
176
+ "description": "Dunning pattern only — action button stack (typically two `<button-ui>` with `data-dunning-action=\"update\"` and `data-dunning-action=\"retry\"`). Sits below the formatted message in the col layout when `pattern=\"dunning\"`."
177
+ },
115
178
  "close": {
116
179
  "description": "Close button. Stamped automatically when `closable` is set; the stamped element is `<button-ui slot=\"close\" icon=\"x\" variant=\"ghost\" size=\"sm\">`. Override by passing a custom `slot=\"close\"` button."
117
180
  },
@@ -50,6 +50,17 @@ export class UIAlert extends UIElement {
50
50
  closable: { type: Boolean, default: false, reflect: true },
51
51
  dismissible: { type: Boolean, default: false, reflect: true },
52
52
  icon: { type: String, default: '', reflect: true },
53
+ /* SPEC-006 dunning pattern \u2014 billing payment-failed mode. When
54
+ `pattern="dunning"`, the alert stamps an Intl-formatted amount +
55
+ due-date + decline-reason from the props below and re-dispatches
56
+ descendant `[data-dunning-action]` button clicks as a unified
57
+ `dunning-action` event. See SPEC-006 for the full rationale. */
58
+ pattern: { type: String, default: '', reflect: true },
59
+ amount: { type: Number, default: 0, reflect: true },
60
+ currency: { type: String, default: 'USD', reflect: true },
61
+ dueAt: { type: String, default: '', reflect: true, attribute: 'due-at' },
62
+ cardLast4: { type: String, default: '', reflect: true, attribute: 'card-last4' },
63
+ reason: { type: String, default: '', reflect: true },
53
64
  };
54
65
 
55
66
  static parts = {
@@ -62,6 +73,15 @@ export class UIAlert extends UIElement {
62
73
  close: '<button-ui slot="close" icon="x" variant="ghost" size="sm" aria-label="Close"></button-ui>',
63
74
  };
64
75
 
76
+ /* SPEC-006 \u2014 decline-reason \u2192 leading icon map. Used when
77
+ `pattern="dunning"` and no explicit [icon] override is set. */
78
+ static #DUNNING_ICONS = {
79
+ declined: 'x-circle',
80
+ expired: 'clock',
81
+ insufficient: 'wallet',
82
+ network: 'wifi-slash',
83
+ };
84
+
65
85
  static template = () => null;
66
86
 
67
87
  /**
@@ -100,7 +120,28 @@ export class UIAlert extends UIElement {
100
120
  }
101
121
 
102
122
  #onPress = (e) => {
103
- if (e.target.closest('[slot="close"]')) this.#close();
123
+ if (e.target.closest('[slot="close"]')) {
124
+ this.#close();
125
+ return;
126
+ }
127
+ // SPEC-006 — dunning action delegation. Re-dispatch as a unified
128
+ // `dunning-action` event so downstream telemetry only listens once.
129
+ if (this.pattern === 'dunning') {
130
+ const trigger = e.target.closest('[data-dunning-action]');
131
+ if (trigger && this.contains(trigger)) {
132
+ const action = trigger.getAttribute('data-dunning-action') || '';
133
+ this.dispatchEvent(new CustomEvent('dunning-action', {
134
+ bubbles: true,
135
+ composed: true,
136
+ detail: {
137
+ action,
138
+ amount: this.amount,
139
+ currency: this.currency,
140
+ dueAt: this.dueAt,
141
+ },
142
+ }));
143
+ }
144
+ }
104
145
  };
105
146
 
106
147
  connected() {
@@ -108,7 +149,7 @@ export class UIAlert extends UIElement {
108
149
  this.#updateRole();
109
150
 
110
151
  // Stamp default DOM if nothing was provided
111
- if (this.icon) this.ensure('leading');
152
+ if (this.icon || (this.pattern === 'dunning' && this.reason)) this.ensure('leading');
112
153
  this.ensure('content');
113
154
  if (this.closable) this.ensure('close');
114
155
 
@@ -120,6 +161,15 @@ export class UIAlert extends UIElement {
120
161
  }
121
162
 
122
163
  render() {
164
+ // SPEC-006 — dunning pattern: branch into a separate render path
165
+ // that stamps Intl-formatted amount + due-date + reason into the
166
+ // content slot. The standard alert render path below is skipped.
167
+ if (this.pattern === 'dunning') {
168
+ this.#renderDunning();
169
+ this.#updateRole();
170
+ return;
171
+ }
172
+
123
173
  // Icon
124
174
  if (this.icon) {
125
175
  const leading = this.ensure('leading');
@@ -182,6 +232,143 @@ export class UIAlert extends UIElement {
182
232
  this.#updateRole();
183
233
  }
184
234
 
235
+ /**
236
+ * SPEC-006 — dunning render path.
237
+ *
238
+ * Stamps the content slot with a bolded title (per-reason copy) +
239
+ * a metadata line ("Card ending 4242 · declined May 20, 2026") +
240
+ * an Intl-formatted amount. The leading icon is selected from
241
+ * `#DUNNING_ICONS` keyed by the [reason] attribute (or [icon] if
242
+ * the consumer explicitly overrode).
243
+ *
244
+ * Re-runs on every render — formatting is stable + idempotent so a
245
+ * re-stamp produces the same DOM. We rebuild the content subtree
246
+ * each render to keep the auto-stamp markers from drifting between
247
+ * the dunning path and the standard path if [pattern] is toggled
248
+ * at runtime.
249
+ */
250
+ #renderDunning() {
251
+ // Leading icon — explicit [icon] beats the reason map
252
+ const iconName = this.icon || UIAlert.#DUNNING_ICONS[this.reason] || 'x-circle';
253
+ const leading = this.ensure('leading');
254
+ if (leading) leading.setAttribute('name', iconName);
255
+
256
+ // Content
257
+ const content = this.ensure('content');
258
+ if (!content) return;
259
+ content.setAttribute('data-alert-auto', 'dunning');
260
+ content.replaceChildren();
261
+
262
+ // Title line — explicit [title] beats the per-reason default
263
+ const titleText = this.title || this.#defaultDunningTitle();
264
+ if (titleText) {
265
+ const strong = document.createElement('strong');
266
+ strong.setAttribute('data-dunning-title', '');
267
+ strong.textContent = titleText;
268
+ content.appendChild(strong);
269
+ }
270
+
271
+ // Body line — formatted amount + metadata. We put amount first
272
+ // (it's the most-scanned datum) followed by the meta string.
273
+ const amountText = this.#formatAmount();
274
+ const metaText = this.#dunningMeta();
275
+ if (amountText || metaText) {
276
+ content.appendChild(document.createTextNode(' '));
277
+ if (amountText) {
278
+ const amt = document.createElement('span');
279
+ amt.setAttribute('data-dunning-amount', '');
280
+ amt.textContent = amountText;
281
+ content.appendChild(amt);
282
+ }
283
+ if (amountText && metaText) {
284
+ content.appendChild(document.createTextNode(' '));
285
+ }
286
+ if (metaText) {
287
+ const meta = document.createElement('span');
288
+ meta.setAttribute('data-dunning-meta', '');
289
+ meta.textContent = metaText;
290
+ content.appendChild(meta);
291
+ }
292
+ }
293
+
294
+ // Composed accessible name — title + amount + meta in DOM order
295
+ const aria = [titleText, amountText, metaText].filter(Boolean).join('. ');
296
+ if (aria) this.setAttribute('aria-label', aria);
297
+
298
+ // Close button — dismissible-on-dunning is advisory-not-blocked
299
+ // (SPEC-006 OD-002 lean A). Pass through.
300
+ if (this.closable || this.dismissible) {
301
+ this.ensure('close');
302
+ } else {
303
+ this.drop('close');
304
+ }
305
+ }
306
+
307
+ #defaultDunningTitle() {
308
+ switch (this.reason) {
309
+ case 'expired': return 'Card expired';
310
+ case 'insufficient': return 'Insufficient funds';
311
+ case 'network': return 'Network error';
312
+ case 'declined': return 'Payment failed';
313
+ default: return this.variant === 'warning' ? 'Payment due soon' : 'Payment failed';
314
+ }
315
+ }
316
+
317
+ #formatAmount() {
318
+ if (!this.amount && this.amount !== 0) return '';
319
+ if (this.amount === 0) return '';
320
+ try {
321
+ const lang = this.getAttribute('lang') || this.lang || undefined;
322
+ return new Intl.NumberFormat(lang, {
323
+ style: 'currency',
324
+ currency: this.currency || 'USD',
325
+ }).format(this.amount);
326
+ } catch {
327
+ // Fallback if Intl rejects the currency code
328
+ return `${this.currency || ''} ${this.amount}`.trim();
329
+ }
330
+ }
331
+
332
+ #formatDueDate() {
333
+ if (!this.dueAt) return '';
334
+ const d = new Date(this.dueAt);
335
+ if (Number.isNaN(d.getTime())) return '';
336
+ try {
337
+ const lang = this.getAttribute('lang') || this.lang || undefined;
338
+ return new Intl.DateTimeFormat(lang, {
339
+ year: 'numeric',
340
+ month: 'short',
341
+ day: 'numeric',
342
+ }).format(d);
343
+ } catch {
344
+ return d.toDateString();
345
+ }
346
+ }
347
+
348
+ #dunningMeta() {
349
+ const parts = [];
350
+ if (this.cardLast4) parts.push(`Card ending ${this.cardLast4}`);
351
+ if (this.reason && this.reason !== 'declined') {
352
+ parts.push(this.#reasonLabel());
353
+ }
354
+ const date = this.#formatDueDate();
355
+ if (date) {
356
+ const prefix = this.variant === 'warning' ? 'due' : 'failed';
357
+ parts.push(`${prefix} ${date}`);
358
+ }
359
+ return parts.join(' · ');
360
+ }
361
+
362
+ #reasonLabel() {
363
+ switch (this.reason) {
364
+ case 'expired': return 'expired';
365
+ case 'insufficient': return 'insufficient funds';
366
+ case 'network': return 'network error';
367
+ case 'declined': return 'declined';
368
+ default: return '';
369
+ }
370
+ }
371
+
185
372
  #updateRole() {
186
373
  const role = (this.variant === 'danger' || this.variant === 'warning') ? 'alert' : 'status';
187
374
  this.setAttribute('role', role);
@@ -15,6 +15,12 @@
15
15
  /* ── Typography ── */
16
16
  --alert-font-default: var(--a-ui-size);
17
17
  --alert-line-height-default: var(--a-ui-line-height, 1.5);
18
+
19
+ /* ── SPEC-006 dunning pattern tokens ── */
20
+ --alert-amount-font-default: var(--a-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
21
+ --alert-meta-fg-default: var(--a-fg-muted);
22
+ --alert-actions-gap-default: var(--a-space-2);
23
+ --alert-actions-mt-default: var(--a-space-2);
18
24
  }
19
25
 
20
26
  :scope {
@@ -115,4 +121,76 @@
115
121
  flex-shrink: 0;
116
122
  min-height: calc(var(--alert-font, var(--alert-font-default)) * var(--alert-line-height, var(--alert-line-height-default)));
117
123
  }
124
+
125
+ /* ──────────────────────────────────────────────────────────────
126
+ SPEC-006 — Dunning / Payment-Failed pattern
127
+ ──────────────────────────────────────────────────────────────
128
+ `pattern="dunning"` switches the alert into a billing notice
129
+ layout. The default alert is a single-row flex container; the
130
+ dunning mode keeps the leading icon and close button in a
131
+ "header" row but stacks the content + actions vertically below.
132
+
133
+ We achieve this by switching the host to `flex-wrap: wrap` and
134
+ assigning `order` per slot. The standard alert children authored
135
+ in arbitrary DOM order (buttons before content is the common
136
+ consumer mistake) are visually re-ordered by these rules so the
137
+ final layout always reads icon → content → actions → close. */
138
+
139
+ :scope[pattern="dunning"] {
140
+ flex-wrap: wrap;
141
+ row-gap: var(--alert-actions-mt, var(--alert-actions-mt-default));
142
+ /* Use align-items: flex-start so the leading icon hugs the top
143
+ of the message regardless of how tall the content + actions
144
+ column grows. */
145
+ align-items: flex-start;
146
+ }
147
+
148
+ /* Visual order — icon first, then content, then actions, then
149
+ close. Source DOM order is irrelevant under these rules. The
150
+ `order` integers leave room (10/20/30/40) for future expansion
151
+ without renumbering. */
152
+ :scope[pattern="dunning"] [slot="leading"] { order: 10; }
153
+ :scope[pattern="dunning"] [slot="content"] { order: 20; }
154
+ :scope[pattern="dunning"] [slot="actions"] { order: 30; }
155
+ :scope[pattern="dunning"] [slot="close"] { order: 40; }
156
+
157
+ :scope[pattern="dunning"] [data-dunning-amount] {
158
+ font-family: var(--alert-amount-font, var(--alert-amount-font-default));
159
+ font-variant-numeric: tabular-nums;
160
+ font-weight: 600;
161
+ /* Inherit color from the variant; tabular-nums + monospace make
162
+ the amount the scannable focal datum. */
163
+ }
164
+
165
+ :scope[pattern="dunning"] [data-dunning-meta] {
166
+ color: var(--alert-meta-fg, var(--alert-meta-fg-default));
167
+ font-size: calc(var(--alert-font, var(--alert-font-default)) * 0.9375);
168
+ }
169
+
170
+ :scope[pattern="dunning"] [data-dunning-title] {
171
+ display: inline-block;
172
+ margin-inline-end: var(--a-space-1, 0.25rem);
173
+ }
174
+
175
+ /* Actions slot — sits below the content as a row of buttons.
176
+ `flex-basis: 100%` combined with the `order: 30` rule pushes
177
+ the actions onto their own row after the content row. The
178
+ padding-inline-start indents the buttons under the message
179
+ column (so they left-align with the body text, not the leading
180
+ icon). */
181
+ :scope[pattern="dunning"] [slot="actions"] {
182
+ flex-basis: 100%;
183
+ display: flex;
184
+ flex-wrap: wrap;
185
+ gap: var(--alert-actions-gap, var(--alert-actions-gap-default));
186
+ padding-inline-start: calc(var(--alert-font, var(--alert-font-default)) + var(--alert-gap, var(--alert-gap-default)));
187
+ }
188
+
189
+ /* Lenient — when a bare button-ui carries data-dunning-action but
190
+ no explicit slot=actions, give it the same visual order as the
191
+ actions group so authoring oversights don't break layout. */
192
+ :scope[pattern="dunning"] > [data-dunning-action]:not([slot]) {
193
+ order: 30;
194
+ flex-basis: 100%;
195
+ }
118
196
  }