@adia-ai/web-components 0.6.33 → 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 (391) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/color/index.js +1 -1
  3. package/components/accordion/accordion-item.yaml +2 -2
  4. package/components/accordion/accordion.css +2 -2
  5. package/components/accordion/accordion.js +1 -1
  6. package/components/action-list/action-item.yaml +2 -2
  7. package/components/action-list/action-list.css +2 -2
  8. package/components/action-list/action-list.js +1 -1
  9. package/components/agent-artifact/{class.js → agent-artifact.class.js} +1 -1
  10. package/components/agent-artifact/agent-artifact.css +31 -31
  11. package/components/agent-artifact/agent-artifact.js +1 -1
  12. package/components/agent-feedback-bar/agent-feedback-bar.css +10 -10
  13. package/components/agent-feedback-bar/agent-feedback-bar.js +1 -1
  14. package/components/agent-questions/agent-questions.css +57 -57
  15. package/components/agent-questions/agent-questions.js +1 -1
  16. package/components/agent-reasoning/agent-reasoning.css +62 -62
  17. package/components/agent-reasoning/agent-reasoning.js +1 -1
  18. package/components/agent-suggestions/agent-suggestions.css +4 -4
  19. package/components/agent-suggestions/agent-suggestions.js +1 -1
  20. package/components/agent-trace/agent-trace.css +53 -53
  21. package/components/alert/alert.a2ui.json +64 -1
  22. package/components/alert/{class.js → alert.class.js} +189 -2
  23. package/components/alert/alert.css +119 -41
  24. package/components/alert/alert.d.ts +14 -0
  25. package/components/alert/alert.js +1 -1
  26. package/components/alert/alert.test.js +184 -0
  27. package/components/alert/alert.yaml +114 -1
  28. package/components/avatar/avatar-group.yaml +2 -2
  29. package/components/avatar/avatar.css +27 -27
  30. package/components/avatar/avatar.js +1 -1
  31. package/components/badge/badge.css +27 -27
  32. package/components/badge/badge.js +1 -1
  33. package/components/block/block.css +16 -16
  34. package/components/block/block.js +1 -1
  35. package/components/breadcrumb/breadcrumb.css +23 -23
  36. package/components/breadcrumb/breadcrumb.js +1 -1
  37. package/components/button/button.css +101 -91
  38. package/components/button/button.js +1 -1
  39. package/components/calendar-grid/calendar-grid.a2ui.json +146 -0
  40. package/components/calendar-grid/calendar-grid.class.js +326 -0
  41. package/components/calendar-grid/calendar-grid.css +246 -0
  42. package/components/calendar-grid/calendar-grid.d.ts +41 -0
  43. package/components/calendar-grid/calendar-grid.js +17 -0
  44. package/components/calendar-grid/calendar-grid.yaml +136 -0
  45. package/components/calendar-picker/calendar-picker.css +139 -139
  46. package/components/calendar-picker/calendar-picker.js +1 -1
  47. package/components/canvas/canvas.css +12 -12
  48. package/components/card/card.css +83 -83
  49. package/components/card/card.js +1 -1
  50. package/components/chart/chart.css +224 -224
  51. package/components/chart/chart.js +1 -1
  52. package/components/chart-legend/chart-legend.css +26 -26
  53. package/components/chart-legend/chart-legend.js +1 -1
  54. package/components/chat-thread/chat-input.a2ui.json +1 -1
  55. package/components/chat-thread/chat-input.js +6 -1
  56. package/components/chat-thread/chat-input.yaml +4 -1
  57. package/components/chat-thread/chat-thread.js +1 -1
  58. package/components/check/check.css +40 -40
  59. package/components/check/check.js +1 -1
  60. package/components/code/code.css +125 -125
  61. package/components/code/code.js +1 -1
  62. package/components/col/col.css +15 -15
  63. package/components/col/col.js +1 -1
  64. package/components/color-input/color-input.js +1 -1
  65. package/components/color-picker/color-picker.css +55 -55
  66. package/components/color-picker/color-picker.js +1 -1
  67. package/components/combobox/combobox.a2ui.json +363 -0
  68. package/components/combobox/combobox.class.js +861 -0
  69. package/components/combobox/combobox.css +244 -0
  70. package/components/combobox/combobox.d.ts +113 -0
  71. package/components/combobox/combobox.examples.md +59 -0
  72. package/components/combobox/combobox.js +17 -0
  73. package/components/combobox/combobox.test.js +181 -0
  74. package/components/combobox/combobox.yaml +369 -0
  75. package/components/command/command.css +90 -90
  76. package/components/command/command.js +1 -1
  77. package/components/date-range-picker/date-range-picker.a2ui.json +300 -0
  78. package/components/date-range-picker/date-range-picker.class.js +791 -0
  79. package/components/date-range-picker/date-range-picker.css +224 -0
  80. package/components/date-range-picker/date-range-picker.d.ts +82 -0
  81. package/components/date-range-picker/date-range-picker.examples.md +37 -0
  82. package/components/date-range-picker/date-range-picker.js +17 -0
  83. package/components/date-range-picker/date-range-picker.test.js +387 -0
  84. package/components/date-range-picker/date-range-picker.yaml +285 -0
  85. package/components/datetime-picker/datetime-picker.a2ui.json +334 -0
  86. package/components/datetime-picker/datetime-picker.class.js +706 -0
  87. package/components/datetime-picker/datetime-picker.css +150 -0
  88. package/components/datetime-picker/datetime-picker.d.ts +86 -0
  89. package/components/datetime-picker/datetime-picker.examples.md +46 -0
  90. package/components/datetime-picker/datetime-picker.js +17 -0
  91. package/components/datetime-picker/datetime-picker.test.js +454 -0
  92. package/components/datetime-picker/datetime-picker.yaml +332 -0
  93. package/components/demo-toggle/demo-toggle.css +27 -27
  94. package/components/demo-toggle/demo-toggle.js +1 -1
  95. package/components/description-list/description-list.css +18 -18
  96. package/components/description-list/description-list.js +1 -1
  97. package/components/divider/divider.css +24 -24
  98. package/components/divider/divider.js +1 -1
  99. package/components/drawer/drawer.js +1 -1
  100. package/components/embed/embed.css +6 -6
  101. package/components/embed/embed.js +1 -1
  102. package/components/empty-state/empty-state.css +27 -27
  103. package/components/empty-state/empty-state.js +1 -1
  104. package/components/feed/feed.css +12 -12
  105. package/components/feed/feed.js +1 -1
  106. package/components/field/field.css +28 -28
  107. package/components/field/field.js +1 -1
  108. package/components/field/field.test.js +1 -1
  109. package/components/fields/fields.css +5 -5
  110. package/components/fields/fields.js +1 -1
  111. package/components/grid/grid.css +5 -5
  112. package/components/grid/grid.js +1 -1
  113. package/components/heatmap/heatmap.css +63 -63
  114. package/components/heatmap/heatmap.js +1 -1
  115. package/components/icon/icon.css +12 -12
  116. package/components/icon/icon.js +1 -1
  117. package/components/image/image.css +14 -14
  118. package/components/image/image.js +1 -1
  119. package/components/index.js +11 -0
  120. package/components/inline-message/inline-message.a2ui.json +143 -0
  121. package/components/inline-message/inline-message.class.js +169 -0
  122. package/components/inline-message/inline-message.css +75 -0
  123. package/components/inline-message/inline-message.d.ts +31 -0
  124. package/components/inline-message/inline-message.examples.md +19 -0
  125. package/components/inline-message/inline-message.js +17 -0
  126. package/components/inline-message/inline-message.test.js +203 -0
  127. package/components/inline-message/inline-message.yaml +205 -0
  128. package/components/input/input.css +67 -67
  129. package/components/input/input.js +1 -1
  130. package/components/input/input.yaml +5 -4
  131. package/components/inspector/inspector.css +6 -6
  132. package/components/inspector/inspector.js +1 -1
  133. package/components/integration-card/integration-card.a2ui.json +268 -0
  134. package/components/integration-card/integration-card.class.js +410 -0
  135. package/components/integration-card/integration-card.css +169 -0
  136. package/components/integration-card/integration-card.d.ts +63 -0
  137. package/components/integration-card/integration-card.examples.md +41 -0
  138. package/components/integration-card/integration-card.js +17 -0
  139. package/components/integration-card/integration-card.test.js +306 -0
  140. package/components/integration-card/integration-card.yaml +280 -0
  141. package/components/kbd/kbd.css +32 -32
  142. package/components/kbd/kbd.js +1 -1
  143. package/components/link/link.css +12 -12
  144. package/components/link/link.js +1 -1
  145. package/components/list/list-item.yaml +2 -2
  146. package/components/list/list.css +8 -8
  147. package/components/list/list.js +1 -1
  148. package/components/list-window/list-window.a2ui.json +277 -0
  149. package/components/list-window/list-window.class.js +688 -0
  150. package/components/list-window/list-window.css +124 -0
  151. package/components/list-window/list-window.d.ts +84 -0
  152. package/components/list-window/list-window.examples.md +73 -0
  153. package/components/list-window/list-window.js +17 -0
  154. package/components/list-window/list-window.test.js +303 -0
  155. package/components/list-window/list-window.yaml +270 -0
  156. package/components/loading-overlay/loading-overlay.a2ui.json +176 -0
  157. package/components/loading-overlay/loading-overlay.class.js +203 -0
  158. package/components/loading-overlay/loading-overlay.css +81 -0
  159. package/components/loading-overlay/loading-overlay.d.ts +24 -0
  160. package/components/loading-overlay/loading-overlay.examples.md +50 -0
  161. package/components/loading-overlay/loading-overlay.js +17 -0
  162. package/components/loading-overlay/loading-overlay.test.js +257 -0
  163. package/components/loading-overlay/loading-overlay.yaml +260 -0
  164. package/components/menu/menu-divider.yaml +1 -1
  165. package/components/menu/menu-item.yaml +1 -1
  166. package/components/menu/menu.a2ui.json +3 -0
  167. package/components/menu/menu.css +8 -8
  168. package/components/menu/menu.js +1 -1
  169. package/components/menu/menu.yaml +7 -0
  170. package/components/modal/{class.js → modal.class.js} +12 -1
  171. package/components/modal/modal.css +54 -44
  172. package/components/modal/modal.js +1 -1
  173. package/components/nav/nav.css +40 -40
  174. package/components/nav/nav.js +1 -1
  175. package/components/nav-group/nav-group.css +52 -52
  176. package/components/nav-group/nav-group.js +1 -1
  177. package/components/nav-item/nav-item.css +44 -44
  178. package/components/nav-item/nav-item.js +1 -1
  179. package/components/noodles/noodles.css +31 -31
  180. package/components/noodles/noodles.js +1 -1
  181. package/components/option-card/option-card.css +69 -69
  182. package/components/option-card/option-card.js +1 -1
  183. package/components/otp-input/otp-input.css +30 -30
  184. package/components/otp-input/otp-input.js +1 -1
  185. package/components/page/page.css +18 -18
  186. package/components/page/page.js +1 -1
  187. package/components/pagination/pagination.css +61 -61
  188. package/components/pagination/pagination.js +1 -1
  189. package/components/pane/pane.css +57 -57
  190. package/components/pane/pane.js +1 -1
  191. package/components/pipeline-status/pipeline-status.css +65 -65
  192. package/components/pipeline-status/pipeline-status.js +1 -1
  193. package/components/popover/popover.a2ui.json +8 -1
  194. package/components/popover/popover.css +17 -17
  195. package/components/popover/popover.js +1 -1
  196. package/components/popover/popover.yaml +14 -1
  197. package/components/progress/progress.css +23 -23
  198. package/components/progress/progress.js +1 -1
  199. package/components/progress-row/progress-row.css +17 -17
  200. package/components/progress-row/progress-row.js +1 -1
  201. package/components/radio/radio.css +39 -39
  202. package/components/radio/radio.js +1 -1
  203. package/components/range/range.css +55 -55
  204. package/components/range/range.js +1 -1
  205. package/components/rating/rating.css +28 -28
  206. package/components/rating/rating.js +1 -1
  207. package/components/richtext/richtext.css +133 -133
  208. package/components/richtext/richtext.js +1 -1
  209. package/components/row/row.css +19 -19
  210. package/components/row/row.js +1 -1
  211. package/components/search/search.css +5 -5
  212. package/components/search/search.js +1 -1
  213. package/components/segment/segment.css +24 -24
  214. package/components/segment/segment.js +1 -1
  215. package/components/segmented/segmented.css +25 -25
  216. package/components/segmented/segmented.js +1 -1
  217. package/components/select/select.a2ui.json +58 -4
  218. package/components/select/{class.js → select.class.js} +415 -6
  219. package/components/select/select.css +242 -84
  220. package/components/select/select.d.ts +31 -1
  221. package/components/select/select.js +1 -1
  222. package/components/select/select.test.js +202 -0
  223. package/components/select/select.yaml +126 -5
  224. package/components/skeleton/skeleton.css +14 -14
  225. package/components/skeleton/skeleton.js +1 -1
  226. package/components/slider/slider.css +46 -46
  227. package/components/slider/slider.js +1 -1
  228. package/components/spinner/spinner.a2ui.json +198 -0
  229. package/components/spinner/spinner.class.js +99 -0
  230. package/components/spinner/spinner.css +221 -0
  231. package/components/spinner/spinner.d.ts +26 -0
  232. package/components/spinner/spinner.examples.md +26 -0
  233. package/components/spinner/spinner.js +17 -0
  234. package/components/spinner/spinner.test.js +272 -0
  235. package/components/spinner/spinner.yaml +238 -0
  236. package/components/stack/stack.css +11 -11
  237. package/components/stack/stack.js +1 -1
  238. package/components/stat/stat.css +25 -25
  239. package/components/step-progress/step-progress.css +20 -20
  240. package/components/step-progress/step-progress.js +1 -1
  241. package/components/stepper/stepper-item.yaml +1 -1
  242. package/components/stepper/stepper.css +29 -29
  243. package/components/stepper/stepper.js +1 -1
  244. package/components/stream/stream.css +12 -12
  245. package/components/stream/stream.js +1 -1
  246. package/components/swatch/swatch.css +68 -68
  247. package/components/swatch/swatch.js +1 -1
  248. package/components/swiper/swiper.css +57 -57
  249. package/components/swiper/swiper.js +1 -1
  250. package/components/switch/switch.css +52 -52
  251. package/components/switch/switch.js +1 -1
  252. package/components/table/table.css +163 -163
  253. package/components/table/table.js +1 -1
  254. package/components/table-toolbar/{class.js → table-toolbar.class.js} +1 -1
  255. package/components/table-toolbar/table-toolbar.css +32 -32
  256. package/components/table-toolbar/table-toolbar.js +1 -1
  257. package/components/tabs/tab.yaml +2 -2
  258. package/components/tabs/tabs.css +51 -51
  259. package/components/tabs/tabs.js +1 -1
  260. package/components/tag/tag.css +48 -48
  261. package/components/tag/tag.js +1 -1
  262. package/components/tags-input/tags-input.a2ui.json +337 -0
  263. package/components/tags-input/tags-input.class.js +776 -0
  264. package/components/tags-input/tags-input.css +201 -0
  265. package/components/tags-input/tags-input.d.ts +120 -0
  266. package/components/tags-input/tags-input.examples.md +92 -0
  267. package/components/tags-input/tags-input.js +17 -0
  268. package/components/tags-input/tags-input.test.js +368 -0
  269. package/components/tags-input/tags-input.yaml +367 -0
  270. package/components/text/text.css +44 -44
  271. package/components/text/text.js +1 -1
  272. package/components/textarea/textarea.a2ui.json +1 -1
  273. package/components/textarea/textarea.css +46 -46
  274. package/components/textarea/textarea.js +1 -1
  275. package/components/textarea/textarea.yaml +11 -8
  276. package/components/time-picker/time-picker.a2ui.json +267 -0
  277. package/components/time-picker/time-picker.class.js +693 -0
  278. package/components/time-picker/time-picker.css +122 -0
  279. package/components/time-picker/time-picker.d.ts +75 -0
  280. package/components/time-picker/time-picker.examples.md +35 -0
  281. package/components/time-picker/time-picker.js +17 -0
  282. package/components/time-picker/time-picker.test.js +287 -0
  283. package/components/time-picker/time-picker.yaml +256 -0
  284. package/components/timeline/timeline-item.yaml +2 -2
  285. package/components/timeline/{class.js → timeline.class.js} +1 -1
  286. package/components/timeline/timeline.css +50 -50
  287. package/components/timeline/timeline.js +1 -1
  288. package/components/toast/toast.css +58 -58
  289. package/components/toast/toast.js +1 -1
  290. package/components/toggle-group/toggle-group.css +6 -6
  291. package/components/toggle-group/toggle-group.js +1 -1
  292. package/components/toggle-group/toggle-option.yaml +1 -1
  293. package/components/toggle-scheme/toggle-scheme.css +2 -2
  294. package/components/toggle-scheme/toggle-scheme.js +1 -1
  295. package/components/toolbar/toolbar-group.yaml +1 -1
  296. package/components/toolbar/toolbar.css +17 -17
  297. package/components/toolbar/toolbar.js +1 -1
  298. package/components/tooltip/tooltip.css +2 -2
  299. package/components/tooltip/tooltip.js +1 -1
  300. package/components/tree/tree-item.yaml +1 -1
  301. package/components/tree/tree.css +37 -37
  302. package/components/tree/tree.js +1 -1
  303. package/components/upload/upload.css +49 -49
  304. package/components/upload/upload.js +1 -1
  305. package/dist/web-components.min.css +1 -1
  306. package/dist/web-components.min.js +146 -87
  307. package/package.json +3 -3
  308. package/styles/components.css +11 -0
  309. /package/components/accordion/{class.js → accordion.class.js} +0 -0
  310. /package/components/action-list/{class.js → action-list.class.js} +0 -0
  311. /package/components/agent-feedback-bar/{class.js → agent-feedback-bar.class.js} +0 -0
  312. /package/components/agent-questions/{class.js → agent-questions.class.js} +0 -0
  313. /package/components/agent-reasoning/{class.js → agent-reasoning.class.js} +0 -0
  314. /package/components/agent-suggestions/{class.js → agent-suggestions.class.js} +0 -0
  315. /package/components/avatar/{class.js → avatar.class.js} +0 -0
  316. /package/components/badge/{class.js → badge.class.js} +0 -0
  317. /package/components/block/{class.js → block.class.js} +0 -0
  318. /package/components/breadcrumb/{class.js → breadcrumb.class.js} +0 -0
  319. /package/components/button/{class.js → button.class.js} +0 -0
  320. /package/components/calendar-picker/{class.js → calendar-picker.class.js} +0 -0
  321. /package/components/card/{class.js → card.class.js} +0 -0
  322. /package/components/chart/{class.js → chart.class.js} +0 -0
  323. /package/components/chart-legend/{class.js → chart-legend.class.js} +0 -0
  324. /package/components/chat-thread/{class.js → chat-thread.class.js} +0 -0
  325. /package/components/check/{class.js → check.class.js} +0 -0
  326. /package/components/code/{class.js → code.class.js} +0 -0
  327. /package/components/col/{class.js → col.class.js} +0 -0
  328. /package/components/color-input/{class.js → color-input.class.js} +0 -0
  329. /package/components/color-picker/{class.js → color-picker.class.js} +0 -0
  330. /package/components/command/{class.js → command.class.js} +0 -0
  331. /package/components/demo-toggle/{class.js → demo-toggle.class.js} +0 -0
  332. /package/components/description-list/{class.js → description-list.class.js} +0 -0
  333. /package/components/divider/{class.js → divider.class.js} +0 -0
  334. /package/components/drawer/{class.js → drawer.class.js} +0 -0
  335. /package/components/embed/{class.js → embed.class.js} +0 -0
  336. /package/components/empty-state/{class.js → empty-state.class.js} +0 -0
  337. /package/components/feed/{class.js → feed.class.js} +0 -0
  338. /package/components/field/{class.js → field.class.js} +0 -0
  339. /package/components/fields/{class.js → fields.class.js} +0 -0
  340. /package/components/grid/{class.js → grid.class.js} +0 -0
  341. /package/components/heatmap/{class.js → heatmap.class.js} +0 -0
  342. /package/components/icon/{class.js → icon.class.js} +0 -0
  343. /package/components/image/{class.js → image.class.js} +0 -0
  344. /package/components/input/{class.js → input.class.js} +0 -0
  345. /package/components/inspector/{class.js → inspector.class.js} +0 -0
  346. /package/components/kbd/{class.js → kbd.class.js} +0 -0
  347. /package/components/link/{class.js → link.class.js} +0 -0
  348. /package/components/list/{class.js → list.class.js} +0 -0
  349. /package/components/menu/{class.js → menu.class.js} +0 -0
  350. /package/components/nav/{class.js → nav.class.js} +0 -0
  351. /package/components/nav-group/{class.js → nav-group.class.js} +0 -0
  352. /package/components/nav-item/{class.js → nav-item.class.js} +0 -0
  353. /package/components/noodles/{class.js → noodles.class.js} +0 -0
  354. /package/components/option-card/{class.js → option-card.class.js} +0 -0
  355. /package/components/otp-input/{class.js → otp-input.class.js} +0 -0
  356. /package/components/page/{class.js → page.class.js} +0 -0
  357. /package/components/pagination/{class.js → pagination.class.js} +0 -0
  358. /package/components/pane/{class.js → pane.class.js} +0 -0
  359. /package/components/pipeline-status/{class.js → pipeline-status.class.js} +0 -0
  360. /package/components/popover/{class.js → popover.class.js} +0 -0
  361. /package/components/progress/{class.js → progress.class.js} +0 -0
  362. /package/components/progress-row/{class.js → progress-row.class.js} +0 -0
  363. /package/components/radio/{class.js → radio.class.js} +0 -0
  364. /package/components/range/{class.js → range.class.js} +0 -0
  365. /package/components/rating/{class.js → rating.class.js} +0 -0
  366. /package/components/richtext/{class.js → richtext.class.js} +0 -0
  367. /package/components/row/{class.js → row.class.js} +0 -0
  368. /package/components/search/{class.js → search.class.js} +0 -0
  369. /package/components/segment/{class.js → segment.class.js} +0 -0
  370. /package/components/segmented/{class.js → segmented.class.js} +0 -0
  371. /package/components/skeleton/{class.js → skeleton.class.js} +0 -0
  372. /package/components/slider/{class.js → slider.class.js} +0 -0
  373. /package/components/stack/{class.js → stack.class.js} +0 -0
  374. /package/components/step-progress/{class.js → step-progress.class.js} +0 -0
  375. /package/components/stepper/{class.js → stepper.class.js} +0 -0
  376. /package/components/stream/{class.js → stream.class.js} +0 -0
  377. /package/components/swatch/{class.js → swatch.class.js} +0 -0
  378. /package/components/swiper/{class.js → swiper.class.js} +0 -0
  379. /package/components/switch/{class.js → switch.class.js} +0 -0
  380. /package/components/table/{class.js → table.class.js} +0 -0
  381. /package/components/tabs/{class.js → tabs.class.js} +0 -0
  382. /package/components/tag/{class.js → tag.class.js} +0 -0
  383. /package/components/text/{class.js → text.class.js} +0 -0
  384. /package/components/textarea/{class.js → textarea.class.js} +0 -0
  385. /package/components/toast/{class.js → toast.class.js} +0 -0
  386. /package/components/toggle-group/{class.js → toggle-group.class.js} +0 -0
  387. /package/components/toggle-scheme/{class.js → toggle-scheme.class.js} +0 -0
  388. /package/components/toolbar/{class.js → toolbar.class.js} +0 -0
  389. /package/components/tooltip/{class.js → tooltip.class.js} +0 -0
  390. /package/components/tree/{class.js → tree.class.js} +0 -0
  391. /package/components/upload/{class.js → upload.class.js} +0 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * spinner-ui tests — covers SPEC-001 § Verification.
3
+ *
4
+ * The component is a CSS-only animation (no stamped DOM, no JS timers).
5
+ * Tests fall in two categories:
6
+ *
7
+ * 1. Behavioral — props reflect to host attributes, defaults apply,
8
+ * ARIA wiring is in place, element is not a focus target.
9
+ * 2. CSS source — happy-dom doesn't resolve @scope rules through
10
+ * getComputedStyle(), so the size-ladder, tone-mapping, and
11
+ * reduced-motion fallback are validated against the CSS source
12
+ * directly (same recipe as tag-ui / description-list / text).
13
+ */
14
+
15
+ import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
16
+ import { readFileSync } from 'node:fs';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { dirname, resolve } from 'node:path';
19
+
20
+ const HERE = dirname(fileURLToPath(import.meta.url));
21
+ const SPINNER_CSS = readFileSync(resolve(HERE, 'spinner.css'), 'utf8');
22
+
23
+ const tick = () => new Promise((r) => queueMicrotask(r));
24
+
25
+ beforeAll(async () => {
26
+ await import('./spinner.js');
27
+ });
28
+
29
+ function mount(html) {
30
+ const wrap = document.createElement('div');
31
+ wrap.innerHTML = html;
32
+ document.body.appendChild(wrap);
33
+ return wrap.firstElementChild;
34
+ }
35
+
36
+ // ── 1. Defaults + property reflection ──────────────────────────────────
37
+
38
+ describe('spinner-ui — defaults', () => {
39
+ beforeEach(() => { document.body.innerHTML = ''; });
40
+
41
+ it('renders without props using documented defaults', async () => {
42
+ const s = mount('<spinner-ui></spinner-ui>');
43
+ await tick();
44
+ expect(s.size).toBe('md');
45
+ expect(s.variant).toBe('arc');
46
+ expect(s.tone).toBe('current');
47
+ expect(s.paused).toBe(false);
48
+ expect(s.label).toBe('Loading');
49
+ });
50
+
51
+ it('sets role="progressbar" on the host', async () => {
52
+ const s = mount('<spinner-ui></spinner-ui>');
53
+ await tick();
54
+ expect(s.getAttribute('role')).toBe('progressbar');
55
+ });
56
+
57
+ it('sets aria-busy="true" on the host', async () => {
58
+ const s = mount('<spinner-ui></spinner-ui>');
59
+ await tick();
60
+ expect(s.getAttribute('aria-busy')).toBe('true');
61
+ });
62
+
63
+ it('sets aria-valuetext to the default label', async () => {
64
+ const s = mount('<spinner-ui></spinner-ui>');
65
+ await tick();
66
+ expect(s.getAttribute('aria-valuetext')).toBe('Loading');
67
+ });
68
+ });
69
+
70
+ // ── 2. Reflected props ─────────────────────────────────────────────────
71
+
72
+ describe('spinner-ui — reflected attributes', () => {
73
+ beforeEach(() => { document.body.innerHTML = ''; });
74
+
75
+ it('reflects size="sm" / "lg" to the host attribute', async () => {
76
+ const sm = mount('<spinner-ui size="sm"></spinner-ui>');
77
+ await tick();
78
+ expect(sm.getAttribute('size')).toBe('sm');
79
+
80
+ const lg = mount('<spinner-ui size="lg"></spinner-ui>');
81
+ await tick();
82
+ expect(lg.getAttribute('size')).toBe('lg');
83
+ });
84
+
85
+ it('reflects variant="ring" / "dots" to the host attribute', async () => {
86
+ const ring = mount('<spinner-ui variant="ring"></spinner-ui>');
87
+ await tick();
88
+ expect(ring.getAttribute('variant')).toBe('ring');
89
+
90
+ const dots = mount('<spinner-ui variant="dots"></spinner-ui>');
91
+ await tick();
92
+ expect(dots.getAttribute('variant')).toBe('dots');
93
+ });
94
+
95
+ it('reflects tone="subtle" / "accent" / "inverse" to the host attribute', async () => {
96
+ const subtle = mount('<spinner-ui tone="subtle"></spinner-ui>');
97
+ const accent = mount('<spinner-ui tone="accent"></spinner-ui>');
98
+ const inverse = mount('<spinner-ui tone="inverse"></spinner-ui>');
99
+ await tick();
100
+ expect(subtle.getAttribute('tone')).toBe('subtle');
101
+ expect(accent.getAttribute('tone')).toBe('accent');
102
+ expect(inverse.getAttribute('tone')).toBe('inverse');
103
+ });
104
+
105
+ it('reflects [paused] as a boolean attribute', async () => {
106
+ const s = mount('<spinner-ui paused></spinner-ui>');
107
+ await tick();
108
+ expect(s.hasAttribute('paused')).toBe(true);
109
+ expect(s.paused).toBe(true);
110
+ });
111
+ });
112
+
113
+ // ── 3. Accessibility wiring ────────────────────────────────────────────
114
+
115
+ describe('spinner-ui — accessibility', () => {
116
+ beforeEach(() => { document.body.innerHTML = ''; });
117
+
118
+ it('mirrors a custom [label] to aria-valuetext', async () => {
119
+ const s = mount('<spinner-ui label="Saving"></spinner-ui>');
120
+ await tick();
121
+ expect(s.getAttribute('aria-valuetext')).toBe('Saving');
122
+ });
123
+
124
+ it('updates aria-valuetext when label is set programmatically', async () => {
125
+ const s = mount('<spinner-ui></spinner-ui>');
126
+ await tick();
127
+ s.label = 'Uploading';
128
+ await tick();
129
+ expect(s.getAttribute('aria-valuetext')).toBe('Uploading');
130
+ });
131
+
132
+ it('is NOT a focus target (no tabindex set, body tabIndex chain skips it)', async () => {
133
+ const s = mount('<spinner-ui></spinner-ui>');
134
+ await tick();
135
+ expect(s.hasAttribute('tabindex')).toBe(false);
136
+ // tabIndex defaults to -1 for non-focusable elements in happy-dom
137
+ expect(s.tabIndex).toBeLessThan(0);
138
+ });
139
+
140
+ it('does not stamp any internal DOM (static template = null)', async () => {
141
+ const s = mount('<spinner-ui></spinner-ui>');
142
+ await tick();
143
+ // The visual is entirely pseudo-element-driven; the host has no
144
+ // element children of its own.
145
+ expect(s.children.length).toBe(0);
146
+ });
147
+
148
+ it('preserves user-supplied role / aria-busy when set in markup', async () => {
149
+ const s = mount('<spinner-ui role="status" aria-busy="false"></spinner-ui>');
150
+ await tick();
151
+ // connected() only sets the default ARIA when absent
152
+ expect(s.getAttribute('role')).toBe('status');
153
+ expect(s.getAttribute('aria-busy')).toBe('false');
154
+ });
155
+ });
156
+
157
+ // ── 4. CSS source contract ─────────────────────────────────────────────
158
+ // happy-dom doesn't resolve @scope rules through getComputedStyle, so we
159
+ // validate the CSS source shape directly. Same recipe as tag-ui.
160
+
161
+ describe('spinner-ui — CSS source contract', () => {
162
+ it('opens with the canonical @scope (spinner-ui) block', () => {
163
+ expect(SPINNER_CSS).toMatch(/^@scope \(spinner-ui\)/);
164
+ });
165
+
166
+ it('declares both the :where(:scope) token block and a :scope base block', () => {
167
+ expect(SPINNER_CSS).toMatch(/:where\(:scope\)\s*\{/);
168
+ expect(SPINNER_CSS).toMatch(/^\s*:scope\s*\{/m);
169
+ });
170
+
171
+ it('declares all 7 component tokens', () => {
172
+ for (const tok of [
173
+ '--spinner-size',
174
+ '--spinner-color',
175
+ '--spinner-stroke',
176
+ '--spinner-duration',
177
+ '--spinner-track-opacity',
178
+ '--spinner-dot-size',
179
+ '--spinner-dot-gap',
180
+ ]) {
181
+ expect(SPINNER_CSS).toContain(tok);
182
+ }
183
+ });
184
+
185
+ it('size="sm" / "md" / "lg" all override --spinner-size-default', () => {
186
+ // The two-token convention (--{prop}-default for defaults +
187
+ // var(--{prop}, var(--{prop}-default)) at use sites) means size
188
+ // variants flip the -default token; consumer overrides write to
189
+ // the bare --{prop} which wins via the var() fallback chain.
190
+ expect(SPINNER_CSS).toMatch(/:scope\[size="sm"\][^}]*--spinner-size-default:\s*0\.875rem/);
191
+ expect(SPINNER_CSS).toMatch(/:scope\[size="md"\][^}]*--spinner-size-default:\s*1rem/);
192
+ expect(SPINNER_CSS).toMatch(/:scope\[size="lg"\][^}]*--spinner-size-default:\s*1\.25rem/);
193
+ });
194
+
195
+ it('tone="subtle" / "accent" / "inverse" override --spinner-color-default via semantic tokens', () => {
196
+ expect(SPINNER_CSS).toMatch(/:scope\[tone="subtle"\][^}]*--spinner-color-default:\s*var\(--a-fg-subtle\)/);
197
+ expect(SPINNER_CSS).toMatch(/:scope\[tone="accent"\][^}]*--spinner-color-default:\s*var\(--a-accent-strong\)/);
198
+ expect(SPINNER_CSS).toMatch(/:scope\[tone="inverse"\][^}]*--spinner-color-default:\s*var\(--a-chrome-light\)/);
199
+ });
200
+
201
+ it('arc variant uses a rotating quarter-circle border on ::before', () => {
202
+ expect(SPINNER_CSS).toMatch(/:scope\[variant="arc"\]::before/);
203
+ // The arc colors three borders transparent + the top currentColor
204
+ expect(SPINNER_CSS).toMatch(/border-color:\s*currentColor\s+transparent\s+transparent\s+transparent/);
205
+ expect(SPINNER_CSS).toMatch(/animation:\s*spinner-ui-rotate/);
206
+ });
207
+
208
+ it('ring variant uses a full ring with one rotating colored segment', () => {
209
+ expect(SPINNER_CSS).toMatch(/:scope\[variant="ring"\]::before/);
210
+ expect(SPINNER_CSS).toMatch(/border-top-color:\s*currentColor/);
211
+ });
212
+
213
+ it('dots variant uses 3 stamped [data-spinner-dot] children with bounce keyframes', () => {
214
+ // Pre-fix: ::before painted dot1 + a box-shadow middle dot, ::after
215
+ // painted dot3. Spacing was uneven (box-shadow doesn't participate
216
+ // in flex-gap math) and the middle dot couldn't animate. Post-fix
217
+ // (spinner.class.js #syncDots): three real <span data-spinner-dot=N>
218
+ // children stamped on connect; flex-gap distributes the row evenly
219
+ // and each child carries an independent animation-delay so the
220
+ // bounce reads as a left-to-right wave.
221
+ expect(SPINNER_CSS).toMatch(/:scope\[variant="dots"\]\s*>\s*\[data-spinner-dot\]/);
222
+ expect(SPINNER_CSS).toMatch(/:scope\[variant="dots"\]\s*>\s*\[data-spinner-dot="1"\]/);
223
+ expect(SPINNER_CSS).toMatch(/:scope\[variant="dots"\]\s*>\s*\[data-spinner-dot="2"\]/);
224
+ expect(SPINNER_CSS).toMatch(/:scope\[variant="dots"\]\s*>\s*\[data-spinner-dot="3"\]/);
225
+ expect(SPINNER_CSS).toMatch(/animation:\s*spinner-ui-bounce/);
226
+ });
227
+
228
+ it('knight variant uses a sliding ::before thumb with knight keyframes', () => {
229
+ expect(SPINNER_CSS).toMatch(/:scope\[variant="knight"\]\s*\{/);
230
+ expect(SPINNER_CSS).toMatch(/:scope\[variant="knight"\]::before/);
231
+ expect(SPINNER_CSS).toMatch(/animation:\s*spinner-ui-knight/);
232
+ // Keyframe math: translateX((1/ratio - 1) * 100%) sweeps the thumb
233
+ // from x=0 to x=(track - thumb) px, expressed in thumb-relative %.
234
+ expect(SPINNER_CSS).toMatch(/@keyframes\s+spinner-ui-knight/);
235
+ });
236
+
237
+ it('knight stamps NO data-spinner-dot children (pseudo-element paint only)', async () => {
238
+ const k = mount('<spinner-ui variant="knight"></spinner-ui>');
239
+ await tick();
240
+ expect(k.querySelectorAll(':scope > [data-spinner-dot]').length).toBe(0);
241
+ });
242
+
243
+ it('dots stamps exactly 3 data-spinner-dot children on connect', async () => {
244
+ const d = mount('<spinner-ui variant="dots"></spinner-ui>');
245
+ await tick();
246
+ const dots = d.querySelectorAll(':scope > [data-spinner-dot]');
247
+ expect(dots.length).toBe(3);
248
+ expect(dots[0].getAttribute('data-spinner-dot')).toBe('1');
249
+ expect(dots[1].getAttribute('data-spinner-dot')).toBe('2');
250
+ expect(dots[2].getAttribute('data-spinner-dot')).toBe('3');
251
+ });
252
+
253
+ it('paused state freezes the animation via animation-play-state', () => {
254
+ expect(SPINNER_CSS).toMatch(/:scope\[paused\][^}]*animation-play-state:\s*paused/);
255
+ });
256
+
257
+ it('reduced-motion media query replaces the animation with a static ellipsis', () => {
258
+ expect(SPINNER_CSS).toMatch(/@media\s*\(prefers-reduced-motion:\s*reduce\)/);
259
+ expect(SPINNER_CSS).toMatch(/content:\s*"…"/);
260
+ });
261
+
262
+ it('uses semantic --a-* tokens for color tones; no raw hex / rgb / oklch in the file', () => {
263
+ // Lints adjacent to the token contract: no raw colors in component CSS.
264
+ expect(SPINNER_CSS).not.toMatch(/#[0-9a-fA-F]{3,8}\b/);
265
+ expect(SPINNER_CSS).not.toMatch(/\brgb\s*\(/);
266
+ expect(SPINNER_CSS).not.toMatch(/\boklch\s*\(/);
267
+ });
268
+
269
+ it('declares a linear rotation timing function (load-bearing for smooth spin)', () => {
270
+ expect(SPINNER_CSS).toMatch(/spinner-ui-rotate[^;]*linear/);
271
+ });
272
+ });
@@ -0,0 +1,238 @@
1
+ # Hand-authored per SPEC-001 (docs/specs/implementation-ready/SPEC-001-spinner-loader.md).
2
+ # Edit this file; run `npm run build:components` to regenerate spinner.a2ui.json.
3
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
4
+ name: UISpinner
5
+ tag: spinner-ui
6
+ status: stable
7
+ component: Spinner
8
+ category: feedback
9
+ version: 1
10
+ description: >-
11
+ Circular animated indicator for indeterminate loading. Renders a rotating
12
+ arc, full ring, or three bouncing dots inside a sized box; the animation
13
+ runs while the element is in the DOM and `[paused]` is unset. Fills the
14
+ circular-spinner gap left by <skeleton-ui> (rectangular placeholder) and
15
+ <progress-ui> (linear determinate bar) — use <spinner-ui> when the wait
16
+ duration is unknown and the shape of the eventual content is irregular
17
+ or the region is too small for a placeholder block.
18
+ props:
19
+ size:
20
+ description: Diameter — matches icon-ui's ladder (sm 14px, md 16px, lg 20px).
21
+ type: string
22
+ default: md
23
+ enum:
24
+ - sm
25
+ - md
26
+ - lg
27
+ reflect: true
28
+ variant:
29
+ description: >-
30
+ Visual flavor —
31
+ `arc` (rotating quarter-circle, the default glyph spinner),
32
+ `ring` (full ring with one colored segment rotating around it),
33
+ `dots` (three bouncing dots — animated in a left-to-right wave),
34
+ `knight` (horizontal "knight-rider" bar — a sliding thumb that
35
+ bounces back-and-forth across a track; widest variant, reads as
36
+ a determinate-looking bar but is indeterminate by intent).
37
+ type: string
38
+ default: arc
39
+ enum:
40
+ - arc
41
+ - ring
42
+ - dots
43
+ - knight
44
+ reflect: true
45
+ tone:
46
+ description: Color tone — `current` inherits parent text color (matches button label), `accent` uses brand accent, `subtle` is muted, `inverse` flips for on-accent surfaces.
47
+ type: string
48
+ default: current
49
+ enum:
50
+ - current
51
+ - accent
52
+ - subtle
53
+ - inverse
54
+ reflect: true
55
+ paused:
56
+ description: Pause the animation in-place. Useful for screenshot tests and explicit-control flows.
57
+ type: boolean
58
+ default: false
59
+ reflect: true
60
+ label:
61
+ description: Accessible operation name surfaced via `aria-valuetext`. Override for context-specific labels ("Saving", "Uploading").
62
+ type: string
63
+ default: Loading
64
+ events: {}
65
+ slots: {}
66
+ states:
67
+ - name: running
68
+ description: Default; animation active.
69
+ - name: paused
70
+ description: Animation frozen at the current frame.
71
+ attribute: paused
72
+ - name: reduced
73
+ description: Triggered by prefers-reduced-motion. Animation replaced with a static ellipsis. Detected via CSS, not JS.
74
+ traits: []
75
+ tokens:
76
+ --spinner-size:
77
+ description: Diameter of the spinner box.
78
+ default: 1rem
79
+ --spinner-color:
80
+ description: Color of the active arc / ring / dots. Defaults to currentColor so a tone-driven cascade resolves naturally.
81
+ default: currentColor
82
+ --spinner-stroke:
83
+ description: Border thickness for the arc / ring variants.
84
+ default: 2px
85
+ --spinner-duration:
86
+ description: One full rotation duration.
87
+ default: var(--a-duration-slow)
88
+ --spinner-track-opacity:
89
+ description: Opacity for the non-rotating ring track (variant=ring only).
90
+ default: '0.25'
91
+ a2ui:
92
+ rules:
93
+ - >-
94
+ Use <Spinner> for INDETERMINATE loading where the duration is
95
+ unknown. For determinate progress (a known fraction complete), use
96
+ <Progress> (linear) instead. For known-shape placeholder loading,
97
+ use <Skeleton>.
98
+ - >-
99
+ When a Spinner is inside a Button, set tone="current" so it
100
+ matches the button label color, and disable the button while the
101
+ operation is in progress.
102
+ - >-
103
+ When overriding [label], use a present-progressive verb form
104
+ ("Loading", "Saving", "Uploading"). Never use "Spin" or "Wait" —
105
+ they describe the visual, not the operation.
106
+ - >-
107
+ Do not nest <Spinner> inside <Skeleton>; they are siblings (two
108
+ different loading idioms), not parent/child.
109
+ - >-
110
+ Do not stack multiple sibling <Spinner>s in the same viewport
111
+ region. Use one parent-level Spinner instead — multiple spinners
112
+ add visual noise without extra information.
113
+ anti_patterns:
114
+ - wrong: |
115
+ {"component": "Spinner", "label": "Spin", "value": 0.42}
116
+ why: |
117
+ Spinner is INDETERMINATE only. `value` and any quantitative
118
+ progress field belongs on Progress, not Spinner. Also "Spin" is
119
+ not a valid operation label.
120
+ fix: |
121
+ {"component": "Progress", "value": 42, "max": 100}
122
+ - wrong: |
123
+ {"component": "Skeleton", "variant": "circle", "animation": "rotate"}
124
+ why: |
125
+ Skeleton is a placeholder; rotation is not part of its contract.
126
+ A rotating circle is a Spinner.
127
+ fix: |
128
+ {"component": "Spinner", "size": "md"}
129
+ - wrong: |
130
+ {"component": "Card", "children": [
131
+ {"component": "Spinner"},
132
+ {"component": "Spinner"},
133
+ {"component": "Spinner"}
134
+ ]}
135
+ why: |
136
+ Multiple sibling spinners in one region produce visual noise
137
+ without extra information. Use one parent-level Spinner.
138
+ fix: |
139
+ {"component": "Card", "children": [
140
+ {"component": "Spinner", "size": "lg"}
141
+ ]}
142
+ examples:
143
+ - name: button-saving
144
+ description: Loading button — primary action with a saving spinner. The button is disabled while the operation is in progress; the spinner matches the button label color via tone="current".
145
+ a2ui: >-
146
+ [
147
+ {
148
+ "id": "btn-save",
149
+ "component": "Button",
150
+ "text": "Saving",
151
+ "variant": "primary",
152
+ "disabled": true,
153
+ "children": ["sp-1"]
154
+ },
155
+ {
156
+ "id": "sp-1",
157
+ "component": "Spinner",
158
+ "size": "sm",
159
+ "tone": "current",
160
+ "label": "Saving"
161
+ }
162
+ ]
163
+ - name: centered-card-loading
164
+ description: Standalone centered spinner inside a card while body content fetches.
165
+ a2ui: >-
166
+ [
167
+ {
168
+ "id": "card",
169
+ "component": "Card",
170
+ "children": ["row"]
171
+ },
172
+ {
173
+ "id": "row",
174
+ "component": "Row",
175
+ "justify": "center",
176
+ "align": "center",
177
+ "children": ["sp"]
178
+ },
179
+ {
180
+ "id": "sp",
181
+ "component": "Spinner",
182
+ "size": "lg",
183
+ "tone": "subtle",
184
+ "label": "Loading dashboard"
185
+ }
186
+ ]
187
+ - name: typing-indicator
188
+ description: Three bouncing dots — good for chat / typing indicators.
189
+ a2ui: >-
190
+ [
191
+ {
192
+ "id": "sp",
193
+ "component": "Spinner",
194
+ "variant": "dots",
195
+ "tone": "subtle",
196
+ "label": "Assistant is typing"
197
+ }
198
+ ]
199
+ keywords:
200
+ - spinner
201
+ - loader
202
+ - loading
203
+ - indeterminate
204
+ - progress
205
+ - busy
206
+ - feedback
207
+ - circular
208
+ synonyms:
209
+ spinner:
210
+ - spinner
211
+ - loader
212
+ - progress
213
+ loader:
214
+ - spinner
215
+ - loading
216
+ - progress
217
+ loading:
218
+ - spinner
219
+ - loading
220
+ - progress
221
+ - skeleton
222
+ busy:
223
+ - spinner
224
+ - loading
225
+ indeterminate:
226
+ - spinner
227
+ - progress
228
+ saving:
229
+ - spinner
230
+ - loading
231
+ uploading:
232
+ - spinner
233
+ - loading
234
+ related:
235
+ - Progress
236
+ - Skeleton
237
+ - Button
238
+ - EmptyState
@@ -1,12 +1,12 @@
1
1
  @scope (stack-ui) {
2
2
  :where(:scope) {
3
- --stack-align: center;
3
+ --stack-align-default: center;
4
4
  }
5
5
 
6
6
  :scope {
7
7
  box-sizing: border-box;
8
8
  display: grid;
9
- place-items: var(--stack-align);
9
+ place-items: var(--stack-align, var(--stack-align-default));
10
10
  /* Universal [padding] / [margin] opt-in — see tokens.css for scale. */
11
11
  padding: var(--a-padding, 0);
12
12
  margin: var(--a-margin, 0);
@@ -19,13 +19,13 @@
19
19
  }
20
20
 
21
21
  /* Alignment */
22
- :scope[align="center"] { --stack-align: center; }
23
- :scope[align="top-left"] { --stack-align: start start; }
24
- :scope[align="top-right"] { --stack-align: start end; }
25
- :scope[align="bottom-left"] { --stack-align: end start; }
26
- :scope[align="bottom-right"] { --stack-align: end end; }
27
- :scope[align="top"] { --stack-align: start center; }
28
- :scope[align="bottom"] { --stack-align: end center; }
29
- :scope[align="left"] { --stack-align: center start; }
30
- :scope[align="right"] { --stack-align: center end; }
22
+ :scope[align="center"] { --stack-align-default: center; }
23
+ :scope[align="top-left"] { --stack-align-default: start start; }
24
+ :scope[align="top-right"] { --stack-align-default: start end; }
25
+ :scope[align="bottom-left"] { --stack-align-default: end start; }
26
+ :scope[align="bottom-right"] { --stack-align-default: end end; }
27
+ :scope[align="top"] { --stack-align-default: start center; }
28
+ :scope[align="bottom"] { --stack-align-default: end center; }
29
+ :scope[align="left"] { --stack-align-default: center start; }
30
+ :scope[align="right"] { --stack-align-default: center end; }
31
31
  }
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIStack } from './class.js';
13
+ import { UIStack } from './stack.class.js';
14
14
 
15
15
  defineIfFree('stack-ui', UIStack);
16
16
 
@@ -2,19 +2,19 @@
2
2
  :where(:scope) {
3
3
  /* ── Tokens ──
4
4
  Use size-responsive tokens so stat shrinks when parent card has size="sm". */
5
- --stat-value-size: var(--a-title-size);
6
- --stat-value-weight: var(--a-weight-bold);
7
- --stat-value-fg: var(--a-fg-strong);
8
- --stat-label-size: var(--a-ui-size);
9
- --stat-label-fg: var(--a-fg);
10
- --stat-change-size: var(--a-ui-size);
11
- --stat-up-fg: var(--a-success-bg);
12
- --stat-down-fg: var(--a-danger-bg);
13
- --stat-icon-fg: var(--a-fg-muted);
5
+ --stat-value-size-default: var(--a-title-size);
6
+ --stat-value-weight-default: var(--a-weight-bold);
7
+ --stat-value-fg-default: var(--a-fg-strong);
8
+ --stat-label-size-default: var(--a-ui-size);
9
+ --stat-label-fg-default: var(--a-fg);
10
+ --stat-change-size-default: var(--a-ui-size);
11
+ --stat-up-fg-default: var(--a-success-bg);
12
+ --stat-down-fg-default: var(--a-danger-bg);
13
+ --stat-icon-fg-default: var(--a-fg-muted);
14
14
 
15
15
  /* ── Spacing ── */
16
- --stat-column-gap: var(--a-gap);
17
- --stat-row-gap: var(--a-gap-sm);
16
+ --stat-column-gap-default: var(--a-gap);
17
+ --stat-row-gap-default: var(--a-gap-sm);
18
18
  text-align: start; /* §text-align-reset — blocks inheritance from centered ancestors */
19
19
  }
20
20
 
@@ -31,8 +31,8 @@
31
31
  "label icon"
32
32
  "value value"
33
33
  "change change";
34
- column-gap: var(--stat-column-gap);
35
- row-gap: var(--stat-row-gap);
34
+ column-gap: var(--stat-column-gap, var(--stat-column-gap-default));
35
+ row-gap: var(--stat-row-gap, var(--stat-row-gap-default));
36
36
  align-items: baseline;
37
37
  }
38
38
 
@@ -71,8 +71,8 @@
71
71
  /* ── Label (eyebrow) ── */
72
72
  [slot="label"] {
73
73
  grid-area: label;
74
- font-size: var(--stat-label-size);
75
- color: var(--stat-label-fg);
74
+ font-size: var(--stat-label-size, var(--stat-label-size-default));
75
+ color: var(--stat-label-fg, var(--stat-label-fg-default));
76
76
  line-height: 1.4;
77
77
  min-width: 0;
78
78
  overflow: hidden;
@@ -86,7 +86,7 @@
86
86
  display: inline-flex;
87
87
  align-items: center;
88
88
  gap: 0.25em;
89
- font-size: var(--stat-change-size);
89
+ font-size: var(--stat-change-size, var(--stat-change-size-default));
90
90
  line-height: 1;
91
91
  justify-self: start;
92
92
  }
@@ -94,9 +94,9 @@
94
94
  /* ── Value ── */
95
95
  [slot="value"] {
96
96
  grid-column: 1 / -1;
97
- font-size: var(--stat-value-size);
98
- font-weight: var(--stat-value-weight);
99
- color: var(--stat-value-fg);
97
+ font-size: var(--stat-value-size, var(--stat-value-size-default));
98
+ font-weight: var(--stat-value-weight, var(--stat-value-weight-default));
99
+ color: var(--stat-value-fg, var(--stat-value-fg-default));
100
100
  line-height: 1.2;
101
101
  min-width: 0;
102
102
  overflow: hidden;
@@ -107,29 +107,29 @@
107
107
  /* Trend arrows via ::before */
108
108
  :scope[trend="up"] [slot="change"]::before {
109
109
  content: "\25B2";
110
- color: var(--stat-up-fg);
110
+ color: var(--stat-up-fg, var(--stat-up-fg-default));
111
111
  }
112
112
  :scope[trend="up"] [slot="change"] {
113
- color: var(--stat-up-fg);
113
+ color: var(--stat-up-fg, var(--stat-up-fg-default));
114
114
  }
115
115
 
116
116
  :scope[trend="down"] [slot="change"]::before {
117
117
  content: "\25BC";
118
- color: var(--stat-down-fg);
118
+ color: var(--stat-down-fg, var(--stat-down-fg-default));
119
119
  }
120
120
  :scope[trend="down"] [slot="change"] {
121
- color: var(--stat-down-fg);
121
+ color: var(--stat-down-fg, var(--stat-down-fg-default));
122
122
  }
123
123
 
124
124
  :scope[trend="neutral"] [slot="change"] {
125
- color: var(--stat-label-fg);
125
+ color: var(--stat-label-fg, var(--stat-label-fg-default));
126
126
  }
127
127
 
128
128
  /* ── Icon ── */
129
129
  [slot="icon"] {
130
130
  grid-area: icon;
131
131
  align-self: start;
132
- color: var(--stat-icon-fg);
132
+ color: var(--stat-icon-fg, var(--stat-icon-fg-default));
133
133
  --a-icon-size: 1.25rem;
134
134
  }
135
135