@miragon/mcp-toolkit-ui 2.7.0 → 2.8.0

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 (387) hide show
  1. package/README.md +7 -4
  2. package/dist/app/adapt-data-widget.d.ts +1 -22
  3. package/dist/app/adapt-data-widget.d.ts.map +1 -1
  4. package/dist/app/adapt-data-widget.js +1 -35
  5. package/dist/app/adapt-data-widget.js.map +1 -1
  6. package/dist/app/builder/BuilderBanners.js +1 -1
  7. package/dist/app/builder/BuilderBanners.js.map +1 -1
  8. package/dist/app/builder/CanvasRow.d.ts +4 -2
  9. package/dist/app/builder/CanvasRow.d.ts.map +1 -1
  10. package/dist/app/builder/CanvasRow.js +21 -7
  11. package/dist/app/builder/CanvasRow.js.map +1 -1
  12. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -1
  13. package/dist/app/builder/CatalogueSheet.js +11 -4
  14. package/dist/app/builder/CatalogueSheet.js.map +1 -1
  15. package/dist/app/builder/CellPropsSheet.d.ts +7 -3
  16. package/dist/app/builder/CellPropsSheet.d.ts.map +1 -1
  17. package/dist/app/builder/CellPropsSheet.js +10 -3
  18. package/dist/app/builder/CellPropsSheet.js.map +1 -1
  19. package/dist/app/builder/DiscardDialog.d.ts +18 -0
  20. package/dist/app/builder/DiscardDialog.d.ts.map +1 -0
  21. package/dist/app/builder/DiscardDialog.js +18 -0
  22. package/dist/app/builder/DiscardDialog.js.map +1 -0
  23. package/dist/app/builder/LayoutSurface.d.ts +33 -9
  24. package/dist/app/builder/LayoutSurface.d.ts.map +1 -1
  25. package/dist/app/builder/LayoutSurface.js +74 -23
  26. package/dist/app/builder/LayoutSurface.js.map +1 -1
  27. package/dist/app/builder/PipelineStrip.d.ts +2 -1
  28. package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
  29. package/dist/app/builder/PipelineStrip.js +26 -13
  30. package/dist/app/builder/PipelineStrip.js.map +1 -1
  31. package/dist/app/builder/SaveDialog.d.ts +9 -1
  32. package/dist/app/builder/SaveDialog.d.ts.map +1 -1
  33. package/dist/app/builder/SaveDialog.js +8 -2
  34. package/dist/app/builder/SaveDialog.js.map +1 -1
  35. package/dist/app/builder/Toolbar.d.ts +12 -4
  36. package/dist/app/builder/Toolbar.d.ts.map +1 -1
  37. package/dist/app/builder/Toolbar.js +18 -7
  38. package/dist/app/builder/Toolbar.js.map +1 -1
  39. package/dist/app/builder/Workspace.d.ts +23 -3
  40. package/dist/app/builder/Workspace.d.ts.map +1 -1
  41. package/dist/app/builder/Workspace.js +58 -8
  42. package/dist/app/builder/Workspace.js.map +1 -1
  43. package/dist/app/builder/builder-model.d.ts +143 -1
  44. package/dist/app/builder/builder-model.d.ts.map +1 -1
  45. package/dist/app/builder/builder-model.js +191 -1
  46. package/dist/app/builder/builder-model.js.map +1 -1
  47. package/dist/app/builder/reveal.d.ts +10 -0
  48. package/dist/app/builder/reveal.d.ts.map +1 -0
  49. package/dist/app/builder/reveal.js +10 -0
  50. package/dist/app/builder/reveal.js.map +1 -0
  51. package/dist/app/builder/use-builder-catalogue.d.ts +4 -1
  52. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
  53. package/dist/app/builder/use-builder-catalogue.js +44 -17
  54. package/dist/app/builder/use-builder-catalogue.js.map +1 -1
  55. package/dist/app/builder/use-builder-session.d.ts +30 -0
  56. package/dist/app/builder/use-builder-session.d.ts.map +1 -0
  57. package/dist/app/builder/use-builder-session.js +58 -0
  58. package/dist/app/builder/use-builder-session.js.map +1 -0
  59. package/dist/app/builder/use-display-resolver.d.ts +17 -0
  60. package/dist/app/builder/use-display-resolver.d.ts.map +1 -0
  61. package/dist/app/builder/use-display-resolver.js +16 -0
  62. package/dist/app/builder/use-display-resolver.js.map +1 -0
  63. package/dist/app/index.d.ts +8 -3
  64. package/dist/app/index.d.ts.map +1 -1
  65. package/dist/app/index.js +8 -2
  66. package/dist/app/index.js.map +1 -1
  67. package/dist/app/layout-builder.d.ts +13 -25
  68. package/dist/app/layout-builder.d.ts.map +1 -1
  69. package/dist/app/layout-builder.js +75 -81
  70. package/dist/app/layout-builder.js.map +1 -1
  71. package/dist/app/mcp-app-view-chrome.d.ts +12 -3
  72. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
  73. package/dist/app/mcp-app-view-chrome.js +21 -5
  74. package/dist/app/mcp-app-view-chrome.js.map +1 -1
  75. package/dist/app/mcp-app-view-states.d.ts +11 -3
  76. package/dist/app/mcp-app-view-states.d.ts.map +1 -1
  77. package/dist/app/mcp-app-view-states.js +29 -8
  78. package/dist/app/mcp-app-view-states.js.map +1 -1
  79. package/dist/app/mcp-app-view.d.ts +33 -1
  80. package/dist/app/mcp-app-view.d.ts.map +1 -1
  81. package/dist/app/mcp-app-view.js +181 -127
  82. package/dist/app/mcp-app-view.js.map +1 -1
  83. package/dist/app/mcp-toolkit-app.d.ts +6 -1
  84. package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
  85. package/dist/app/mcp-toolkit-app.js +8 -1
  86. package/dist/app/mcp-toolkit-app.js.map +1 -1
  87. package/dist/app/mcp-use-host-bridge.d.ts +15 -6
  88. package/dist/app/mcp-use-host-bridge.d.ts.map +1 -1
  89. package/dist/app/mcp-use-host-bridge.js +51 -22
  90. package/dist/app/mcp-use-host-bridge.js.map +1 -1
  91. package/dist/app/model-context.d.ts +71 -0
  92. package/dist/app/model-context.d.ts.map +1 -0
  93. package/dist/app/model-context.js +101 -0
  94. package/dist/app/model-context.js.map +1 -0
  95. package/dist/app/use-build-mode-focus.d.ts +33 -0
  96. package/dist/app/use-build-mode-focus.d.ts.map +1 -0
  97. package/dist/app/use-build-mode-focus.js +59 -0
  98. package/dist/app/use-build-mode-focus.js.map +1 -0
  99. package/dist/app/use-view-actions.d.ts +31 -0
  100. package/dist/app/use-view-actions.d.ts.map +1 -0
  101. package/dist/app/use-view-actions.js +81 -0
  102. package/dist/app/use-view-actions.js.map +1 -0
  103. package/dist/app/use-view-lifecycle.d.ts +4 -0
  104. package/dist/app/use-view-lifecycle.d.ts.map +1 -1
  105. package/dist/app/use-view-lifecycle.js +2 -0
  106. package/dist/app/use-view-lifecycle.js.map +1 -1
  107. package/dist/app/view-lifecycle.d.ts +80 -4
  108. package/dist/app/view-lifecycle.d.ts.map +1 -1
  109. package/dist/app/view-lifecycle.js +90 -3
  110. package/dist/app/view-lifecycle.js.map +1 -1
  111. package/dist/app/widget-fixture.d.ts +10 -3
  112. package/dist/app/widget-fixture.d.ts.map +1 -1
  113. package/dist/app/widget-fixture.js +21 -8
  114. package/dist/app/widget-fixture.js.map +1 -1
  115. package/dist/app/widget-props-sheet.d.ts +3 -1
  116. package/dist/app/widget-props-sheet.d.ts.map +1 -1
  117. package/dist/app/widget-props-sheet.js +22 -10
  118. package/dist/app/widget-props-sheet.js.map +1 -1
  119. package/dist/app/widget-renderer.js +1 -1
  120. package/dist/app/widget-renderer.js.map +1 -1
  121. package/dist/components/DrillButton.d.ts +9 -6
  122. package/dist/components/DrillButton.d.ts.map +1 -1
  123. package/dist/components/DrillButton.js +8 -6
  124. package/dist/components/DrillButton.js.map +1 -1
  125. package/dist/components/EmptyState.d.ts.map +1 -1
  126. package/dist/components/EmptyState.js +4 -2
  127. package/dist/components/EmptyState.js.map +1 -1
  128. package/dist/components/ErrorState.d.ts +5 -2
  129. package/dist/components/ErrorState.d.ts.map +1 -1
  130. package/dist/components/ErrorState.js +25 -8
  131. package/dist/components/ErrorState.js.map +1 -1
  132. package/dist/components/FilterBar.d.ts +15 -4
  133. package/dist/components/FilterBar.d.ts.map +1 -1
  134. package/dist/components/FilterBar.js +11 -6
  135. package/dist/components/FilterBar.js.map +1 -1
  136. package/dist/components/KpiGrid.d.ts +66 -21
  137. package/dist/components/KpiGrid.d.ts.map +1 -1
  138. package/dist/components/KpiGrid.js +101 -34
  139. package/dist/components/KpiGrid.js.map +1 -1
  140. package/dist/components/ListFooter.d.ts +31 -4
  141. package/dist/components/ListFooter.d.ts.map +1 -1
  142. package/dist/components/ListFooter.js +30 -11
  143. package/dist/components/ListFooter.js.map +1 -1
  144. package/dist/components/PagedListFooter.d.ts +57 -0
  145. package/dist/components/PagedListFooter.d.ts.map +1 -0
  146. package/dist/components/PagedListFooter.js +27 -0
  147. package/dist/components/PagedListFooter.js.map +1 -0
  148. package/dist/components/Pills.d.ts +27 -7
  149. package/dist/components/Pills.d.ts.map +1 -1
  150. package/dist/components/Pills.js +21 -6
  151. package/dist/components/Pills.js.map +1 -1
  152. package/dist/components/SectionHeading.d.ts +3 -1
  153. package/dist/components/SectionHeading.d.ts.map +1 -1
  154. package/dist/components/SectionHeading.js +6 -2
  155. package/dist/components/SectionHeading.js.map +1 -1
  156. package/dist/components/Toast.d.ts +9 -3
  157. package/dist/components/Toast.d.ts.map +1 -1
  158. package/dist/components/Toast.js +31 -16
  159. package/dist/components/Toast.js.map +1 -1
  160. package/dist/components/WidgetHeader.d.ts +34 -10
  161. package/dist/components/WidgetHeader.d.ts.map +1 -1
  162. package/dist/components/WidgetHeader.js +11 -6
  163. package/dist/components/WidgetHeader.js.map +1 -1
  164. package/dist/hooks/index.d.ts +4 -4
  165. package/dist/hooks/index.d.ts.map +1 -1
  166. package/dist/hooks/index.js +1 -1
  167. package/dist/hooks/index.js.map +1 -1
  168. package/dist/hooks/tool-result-recovery-model.d.ts +155 -0
  169. package/dist/hooks/tool-result-recovery-model.d.ts.map +1 -0
  170. package/dist/hooks/tool-result-recovery-model.js +273 -0
  171. package/dist/hooks/tool-result-recovery-model.js.map +1 -0
  172. package/dist/hooks/use-tool-query.d.ts +56 -14
  173. package/dist/hooks/use-tool-query.d.ts.map +1 -1
  174. package/dist/hooks/use-tool-query.js +102 -25
  175. package/dist/hooks/use-tool-query.js.map +1 -1
  176. package/dist/hooks/use-tool-result-recovery.d.ts +47 -52
  177. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
  178. package/dist/hooks/use-tool-result-recovery.js +58 -120
  179. package/dist/hooks/use-tool-result-recovery.js.map +1 -1
  180. package/dist/hooks/use-view-data.d.ts +52 -10
  181. package/dist/hooks/use-view-data.d.ts.map +1 -1
  182. package/dist/hooks/use-view-data.js +81 -16
  183. package/dist/hooks/use-view-data.js.map +1 -1
  184. package/dist/hooks/use-view-tool-query.d.ts +12 -7
  185. package/dist/hooks/use-view-tool-query.d.ts.map +1 -1
  186. package/dist/hooks/use-view-tool-query.js +15 -14
  187. package/dist/hooks/use-view-tool-query.js.map +1 -1
  188. package/dist/index.d.ts +17 -10
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +18 -6
  191. package/dist/index.js.map +1 -1
  192. package/dist/lib/focus-return.d.ts +61 -0
  193. package/dist/lib/focus-return.d.ts.map +1 -0
  194. package/dist/lib/focus-return.js +46 -0
  195. package/dist/lib/focus-return.js.map +1 -0
  196. package/dist/lib/parse-tool-result.d.ts +23 -1
  197. package/dist/lib/parse-tool-result.d.ts.map +1 -1
  198. package/dist/lib/parse-tool-result.js +38 -2
  199. package/dist/lib/parse-tool-result.js.map +1 -1
  200. package/dist/lib/toast-model.d.ts +41 -1
  201. package/dist/lib/toast-model.d.ts.map +1 -1
  202. package/dist/lib/toast-model.js +67 -1
  203. package/dist/lib/toast-model.js.map +1 -1
  204. package/dist/lib/tone-utils.d.ts +58 -6
  205. package/dist/lib/tone-utils.d.ts.map +1 -1
  206. package/dist/lib/tone-utils.js +66 -4
  207. package/dist/lib/tone-utils.js.map +1 -1
  208. package/dist/lib/tool-query.d.ts +107 -0
  209. package/dist/lib/tool-query.d.ts.map +1 -0
  210. package/dist/lib/tool-query.js +182 -0
  211. package/dist/lib/tool-query.js.map +1 -0
  212. package/dist/lib/tool-result-observer.d.ts +108 -0
  213. package/dist/lib/tool-result-observer.d.ts.map +1 -0
  214. package/dist/lib/tool-result-observer.js +158 -0
  215. package/dist/lib/tool-result-observer.js.map +1 -0
  216. package/dist/lib/typography.d.ts +37 -0
  217. package/dist/lib/typography.d.ts.map +1 -0
  218. package/dist/lib/typography.js +41 -0
  219. package/dist/lib/typography.js.map +1 -0
  220. package/dist/lib/utils.d.ts.map +1 -1
  221. package/dist/lib/utils.js +11 -1
  222. package/dist/lib/utils.js.map +1 -1
  223. package/dist/locale/index.d.ts +1 -1
  224. package/dist/locale/index.d.ts.map +1 -1
  225. package/dist/locale/message-types.d.ts +117 -4
  226. package/dist/locale/message-types.d.ts.map +1 -1
  227. package/dist/locale/messages-de.d.ts.map +1 -1
  228. package/dist/locale/messages-de.js +44 -10
  229. package/dist/locale/messages-de.js.map +1 -1
  230. package/dist/locale/messages-en.d.ts.map +1 -1
  231. package/dist/locale/messages-en.js +44 -10
  232. package/dist/locale/messages-en.js.map +1 -1
  233. package/dist/locale/toolkit-messages.d.ts +19 -1
  234. package/dist/locale/toolkit-messages.d.ts.map +1 -1
  235. package/dist/locale/toolkit-messages.js +54 -1
  236. package/dist/locale/toolkit-messages.js.map +1 -1
  237. package/dist/primitives/alert.d.ts +1 -1
  238. package/dist/primitives/alert.d.ts.map +1 -1
  239. package/dist/primitives/alert.js +2 -0
  240. package/dist/primitives/alert.js.map +1 -1
  241. package/dist/primitives/badge.d.ts +1 -1
  242. package/dist/primitives/badge.d.ts.map +1 -1
  243. package/dist/primitives/badge.js +9 -4
  244. package/dist/primitives/badge.js.map +1 -1
  245. package/dist/primitives/button.d.ts +1 -1
  246. package/dist/primitives/button.js +1 -1
  247. package/dist/primitives/button.js.map +1 -1
  248. package/dist/primitives/dialog.d.ts +1 -1
  249. package/dist/primitives/dialog.d.ts.map +1 -1
  250. package/dist/primitives/dialog.js +10 -3
  251. package/dist/primitives/dialog.js.map +1 -1
  252. package/dist/primitives/select.js +1 -1
  253. package/dist/primitives/select.js.map +1 -1
  254. package/dist/primitives/sheet.d.ts +1 -1
  255. package/dist/primitives/sheet.d.ts.map +1 -1
  256. package/dist/primitives/sheet.js +6 -2
  257. package/dist/primitives/sheet.js.map +1 -1
  258. package/dist/providers/query-provider.d.ts +32 -1
  259. package/dist/providers/query-provider.d.ts.map +1 -1
  260. package/dist/providers/query-provider.js +53 -4
  261. package/dist/providers/query-provider.js.map +1 -1
  262. package/dist/render-cases-primitives.js +1 -1
  263. package/dist/render-cases-primitives.js.map +1 -1
  264. package/dist/render-cases.d.ts.map +1 -1
  265. package/dist/render-cases.js +30 -6
  266. package/dist/render-cases.js.map +1 -1
  267. package/dist/render-effects.d.ts +42 -0
  268. package/dist/render-effects.d.ts.map +1 -0
  269. package/dist/render-effects.js +81 -0
  270. package/dist/render-effects.js.map +1 -0
  271. package/dist/theme/create-theme.d.ts +19 -1
  272. package/dist/theme/create-theme.d.ts.map +1 -1
  273. package/dist/theme/create-theme.js +15 -0
  274. package/dist/theme/create-theme.js.map +1 -1
  275. package/package.json +5 -4
  276. package/src/app/adapt-data-widget.tsx +2 -42
  277. package/src/app/builder/BuilderBanners.tsx +1 -1
  278. package/src/app/builder/CanvasRow.tsx +63 -17
  279. package/src/app/builder/CatalogueSheet.tsx +50 -29
  280. package/src/app/builder/CellPropsSheet.tsx +17 -2
  281. package/src/app/builder/DiscardDialog.tsx +59 -0
  282. package/src/app/builder/LayoutSurface.tsx +206 -66
  283. package/src/app/builder/PipelineStrip.tsx +253 -153
  284. package/src/app/builder/SaveDialog.tsx +14 -2
  285. package/src/app/builder/Toolbar.tsx +144 -59
  286. package/src/app/builder/Workspace.tsx +226 -58
  287. package/src/app/builder/builder-model.ts +341 -2
  288. package/src/app/builder/builder-model.ux.test.ts +456 -0
  289. package/src/app/builder/builder-ux.test.tsx +844 -0
  290. package/src/app/builder/reveal.ts +10 -0
  291. package/src/app/builder/use-builder-catalogue.ts +56 -19
  292. package/src/app/builder/use-builder-session.ts +68 -0
  293. package/src/app/builder/use-display-resolver.ts +24 -0
  294. package/src/app/index.ts +12 -2
  295. package/src/app/layout-builder.tsx +178 -191
  296. package/src/app/mcp-app-view-chrome.test.tsx +141 -1
  297. package/src/app/mcp-app-view-chrome.tsx +56 -6
  298. package/src/app/mcp-app-view-states.tsx +51 -11
  299. package/src/app/mcp-app-view.effects.test.tsx +292 -0
  300. package/src/app/mcp-app-view.test.tsx +222 -2
  301. package/src/app/mcp-app-view.tsx +267 -117
  302. package/src/app/mcp-toolkit-app.test.tsx +40 -0
  303. package/src/app/mcp-toolkit-app.tsx +8 -1
  304. package/src/app/mcp-use-host-bridge.test.tsx +180 -0
  305. package/src/app/mcp-use-host-bridge.tsx +67 -24
  306. package/src/app/model-context.test.tsx +116 -0
  307. package/src/app/model-context.tsx +125 -0
  308. package/src/app/use-build-mode-focus.ts +62 -0
  309. package/src/app/use-view-actions.ts +89 -0
  310. package/src/app/use-view-lifecycle.ts +6 -0
  311. package/src/app/view-lifecycle.test.ts +216 -0
  312. package/src/app/view-lifecycle.ts +165 -5
  313. package/src/app/widget-fixture.effects.test.tsx +51 -0
  314. package/src/app/widget-fixture.render.test.tsx +81 -0
  315. package/src/app/widget-fixture.tsx +34 -11
  316. package/src/app/widget-props-sheet.tsx +57 -16
  317. package/src/app/widget-renderer.tsx +1 -1
  318. package/src/components/DrillButton.tsx +23 -10
  319. package/src/components/EmptyState.tsx +6 -4
  320. package/src/components/ErrorState.tsx +31 -9
  321. package/src/components/FilterBar.test.tsx +82 -0
  322. package/src/components/FilterBar.tsx +52 -26
  323. package/src/components/KpiGrid.test.tsx +279 -0
  324. package/src/components/KpiGrid.tsx +297 -90
  325. package/src/components/ListFooter.test.tsx +116 -0
  326. package/src/components/ListFooter.tsx +102 -15
  327. package/src/components/PagedListFooter.test.tsx +140 -0
  328. package/src/components/PagedListFooter.tsx +107 -0
  329. package/src/components/Pills.test.tsx +77 -0
  330. package/src/components/Pills.tsx +63 -10
  331. package/src/components/SectionHeading.tsx +9 -3
  332. package/src/components/Toast.tsx +37 -19
  333. package/src/components/WidgetHeader.test.tsx +78 -0
  334. package/src/components/WidgetHeader.tsx +63 -16
  335. package/src/components/feedback-states.test.tsx +98 -9
  336. package/src/components/tone-figures.test.tsx +8 -4
  337. package/src/hooks/index.ts +6 -3
  338. package/src/hooks/tool-result-recovery-model.test.ts +744 -0
  339. package/src/hooks/tool-result-recovery-model.ts +418 -0
  340. package/src/hooks/use-tool-query.effects.test.tsx +205 -0
  341. package/src/hooks/use-tool-query.ts +156 -33
  342. package/src/hooks/use-tool-result-recovery.effects.test.tsx +141 -0
  343. package/src/hooks/use-tool-result-recovery.test.ts +3 -4
  344. package/src/hooks/use-tool-result-recovery.ts +84 -142
  345. package/src/hooks/use-view-data.test.tsx +118 -9
  346. package/src/hooks/use-view-data.ts +128 -18
  347. package/src/hooks/use-view-tool-query.ts +25 -19
  348. package/src/i18n-coverage.test.tsx +104 -2
  349. package/src/index.ts +48 -11
  350. package/src/lib/focus-return.test.ts +288 -0
  351. package/src/lib/focus-return.ts +96 -0
  352. package/src/lib/parse-tool-result.test.ts +46 -1
  353. package/src/lib/parse-tool-result.ts +40 -2
  354. package/src/lib/toast-model.test.ts +166 -1
  355. package/src/lib/toast-model.ts +104 -2
  356. package/src/lib/tone-utils.test.ts +86 -10
  357. package/src/lib/tone-utils.ts +89 -8
  358. package/src/lib/tool-query.test.ts +569 -0
  359. package/src/lib/tool-query.ts +253 -0
  360. package/src/lib/tool-result-observer.test.ts +296 -0
  361. package/src/lib/tool-result-observer.ts +211 -0
  362. package/src/lib/typography.test.ts +45 -0
  363. package/src/lib/typography.ts +47 -0
  364. package/src/lib/utils.ts +12 -1
  365. package/src/locale/index.ts +1 -0
  366. package/src/locale/message-types.ts +118 -2
  367. package/src/locale/messages-de.ts +49 -10
  368. package/src/locale/messages-en.ts +49 -10
  369. package/src/locale/toolkit-messages.test.ts +133 -0
  370. package/src/locale/toolkit-messages.ts +63 -2
  371. package/src/primitives/alert.tsx +3 -0
  372. package/src/primitives/badge.tsx +9 -4
  373. package/src/primitives/button.tsx +1 -1
  374. package/src/primitives/dialog.tsx +10 -1
  375. package/src/primitives/select.tsx +1 -1
  376. package/src/primitives/sheet.tsx +7 -0
  377. package/src/providers/query-provider.test.tsx +154 -0
  378. package/src/providers/query-provider.tsx +66 -5
  379. package/src/render-cases-primitives.tsx +1 -1
  380. package/src/render-cases.tsx +38 -6
  381. package/src/render-effects.tsx +96 -0
  382. package/src/theme/cd-tokens.test.ts +3 -1
  383. package/src/theme/create-theme.test.ts +10 -0
  384. package/src/theme/create-theme.ts +28 -0
  385. package/src/theme/theme-css.test.ts +51 -0
  386. package/src/theme.css +31 -1
  387. package/ui-catalog.json +448 -41
@@ -1,5 +1,7 @@
1
1
  import { FOCUS_RING } from "../lib/focus-ring.js"
2
+ import { cn } from "../lib/utils.js"
2
3
  import { useToolkitLocale, useToolkitMessages } from "../locale/locale-provider.js"
4
+ import type { ListFooterMessages } from "../locale/message-types.js"
3
5
  import { formatCount } from "../locale/toolkit-messages.js"
4
6
 
5
7
  export interface ListFooterProps {
@@ -15,14 +17,40 @@ export interface ListFooterProps {
15
17
  onLoadMore: () => void
16
18
  /** Plural noun for the count, e.g. `"instances"`. Default: the locale's word for rows. */
17
19
  noun?: string
20
+ /**
21
+ * A new first page is in flight while these rows stay on screen (a search,
22
+ * a filter, a refetch): the status line adds a visible "Updating…".
23
+ */
24
+ refreshing?: boolean
25
+ /**
26
+ * The rows on screen are the PREVIOUS result (a changed search or filter is
27
+ * in flight or failed): the count and "Load more" hide, so the old count
28
+ * never reads as the new search's answer.
29
+ */
30
+ stale?: boolean
31
+ /** Override strings of the active catalog's `listFooter` section. */
32
+ labels?: Partial<ListFooterMessages>
33
+ /**
34
+ * Make the count line a polite live region (`role="status"`). Default
35
+ * `true`. Set `false` only where the caller already announces the count in
36
+ * a live region of its own, so a screen reader hears it once.
37
+ */
38
+ announce?: boolean
39
+ className?: string
18
40
  }
19
41
 
20
42
  /**
21
43
  * Footer for a paginated list: an honest "Showing X of Y" total plus an
22
44
  * explicit "Load more" control. Deliberately not infinite scroll — the user
23
- * gets the total, a reachable footer, and control over fetching. Renders
24
- * nothing when `total` is 0. Text and number format follow the active locale
25
- * (`LocaleProvider`, else the host's inside `McpAppView`).
45
+ * gets the total, a reachable footer, and control over fetching. The count
46
+ * is a polite live region (`role="status"`), so a keyboard or screen-reader
47
+ * user hears the new total after "Load more" or a filter change; while a
48
+ * first page is in flight it also says "Updating…". With nothing to count and
49
+ * nothing in flight it shows nothing, but the live region stays mounted
50
+ * (visually hidden) and says "No entries": a search that finds nothing is
51
+ * announced too, and the region is never re-created with its news already in
52
+ * it (a screen reader stays silent then). Text and number format follow the
53
+ * active locale (`LocaleProvider`, else the host's inside `McpAppView`).
26
54
  */
27
55
  export function ListFooter({
28
56
  shown,
@@ -31,20 +59,40 @@ export function ListFooter({
31
59
  loadingMore = false,
32
60
  onLoadMore,
33
61
  noun,
62
+ refreshing = false,
63
+ stale = false,
64
+ labels,
65
+ announce = true,
66
+ className,
34
67
  }: ListFooterProps) {
35
- const locale = useToolkitLocale()
36
- const { listFooter } = useToolkitMessages()
37
- if (total <= 0) return null
68
+ const listFooter = { ...useToolkitMessages().listFooter, ...labels }
69
+ const counted = total > 0 && !stale
70
+ const visible = counted || refreshing
71
+ // Without a live region of its own there is nothing to keep mounted.
72
+ if (!visible && !announce) return null
38
73
  return (
39
- <div className="text-muted-foreground flex items-center justify-between gap-3 px-1 py-2 text-xs">
40
- <span>
41
- {listFooter.showing({
42
- shown: formatCount(shown, locale),
43
- total: formatCount(total, locale),
44
- noun: noun ?? listFooter.defaultNoun,
45
- })}
46
- </span>
47
- {hasMore && (
74
+ <div
75
+ data-slot="list-footer"
76
+ // The same element tree in both states, so React keeps the live region.
77
+ className={visible ? cn(FOOTER_CLASS, className) : "sr-only"}
78
+ >
79
+ <p
80
+ role={announce ? "status" : undefined}
81
+ data-slot="list-footer-status"
82
+ className="flex flex-wrap items-center gap-x-2"
83
+ >
84
+ <StatusLine
85
+ messages={listFooter}
86
+ shown={shown}
87
+ total={total}
88
+ noun={noun}
89
+ counted={counted}
90
+ refreshing={refreshing}
91
+ // Zero rows, not a stale previous result (its failure the caller reports).
92
+ empty={!visible && !stale}
93
+ />
94
+ </p>
95
+ {counted && hasMore && (
48
96
  <button
49
97
  type="button"
50
98
  onClick={onLoadMore}
@@ -57,3 +105,42 @@ export function ListFooter({
57
105
  </div>
58
106
  )
59
107
  }
108
+
109
+ const FOOTER_CLASS =
110
+ "text-small text-muted-foreground flex items-center justify-between gap-3 px-1 py-2"
111
+
112
+ /** What the live region says: the count, "Updating…", or "No entries" at zero rows. */
113
+ function StatusLine({
114
+ messages,
115
+ shown,
116
+ total,
117
+ noun,
118
+ counted,
119
+ refreshing,
120
+ empty,
121
+ }: {
122
+ messages: ListFooterMessages
123
+ shown: number
124
+ total: number
125
+ noun: string | undefined
126
+ counted: boolean
127
+ refreshing: boolean
128
+ empty: boolean
129
+ }) {
130
+ const locale = useToolkitLocale()
131
+ return (
132
+ <>
133
+ {empty && <span>{messages.noResults}</span>}
134
+ {counted && (
135
+ <span>
136
+ {messages.showing({
137
+ shown: formatCount(shown, locale),
138
+ total: formatCount(total, locale),
139
+ noun: noun ?? messages.defaultNoun,
140
+ })}
141
+ </span>
142
+ )}
143
+ {refreshing && <span data-slot="list-footer-updating">{messages.updating}</span>}
144
+ </>
145
+ )
146
+ }
@@ -0,0 +1,140 @@
1
+ import { renderToStaticMarkup } from "react-dom/server"
2
+ import { describe, expect, it } from "vitest"
3
+ import { LocaleProvider } from "../locale/locale-provider.js"
4
+ import { TOOLKIT_MESSAGES } from "../locale/toolkit-messages.js"
5
+ import { attribute, openingTags, userFacingTexts } from "../render-scan.js"
6
+ import { PagedListFooter, type PagedListState } from "./PagedListFooter.js"
7
+
8
+ /**
9
+ * The paged-list tail on its SSR markup (the house render path): a page-0
10
+ * failure over the previous rows and a load-more failure stay two separate
11
+ * alerts with their own retries; the count is a live region that says
12
+ * "Updating…" while page 0 is in flight and hides while the rows are stale.
13
+ */
14
+
15
+ const noop = (): void => {}
16
+ const loadMore = (): void => {}
17
+ const retry = (): void => {}
18
+
19
+ /** A settled page 0: two of three rows on screen, nothing in flight. */
20
+ function paged(overrides: Partial<PagedListState> = {}): PagedListState {
21
+ return {
22
+ items: ["a", "b"],
23
+ firstPage: {},
24
+ total: 3,
25
+ hasMore: true,
26
+ loadMore,
27
+ retry,
28
+ ...overrides,
29
+ }
30
+ }
31
+
32
+ function render(state: PagedListState, locale = "en") {
33
+ return renderToStaticMarkup(
34
+ <LocaleProvider locale={locale}>
35
+ <PagedListFooter paged={state} noun="items" />
36
+ </LocaleProvider>,
37
+ )
38
+ }
39
+
40
+ const alerts = (html: string) =>
41
+ openingTags(html, "div").filter((t) => attribute(t, "role") === "alert")
42
+ const statusOf = (html: string) =>
43
+ /<p role="status"[^>]*>([\s\S]*?)<\/p>/
44
+ .exec(html)?.[1]
45
+ ?.replace(/<[^>]*>/g, " ")
46
+ .replace(/\s+/g, " ")
47
+ .trim()
48
+
49
+ describe("PagedListFooter — settled and in-flight states", () => {
50
+ it("a settled page: the count in the live region, Load more, no alert", () => {
51
+ const html = render(paged())
52
+ expect(statusOf(html)).toBe("Showing 2 of 3 items")
53
+ expect(alerts(html)).toHaveLength(0)
54
+ expect(html).toContain("Load more</button>")
55
+ })
56
+
57
+ it("a search that finds nothing: the live region stays and says so, nothing visible", () => {
58
+ const html = render(paged({ items: [], total: 0, hasMore: false }))
59
+ expect(statusOf(html)).toBe("No entries")
60
+ expect(html.match(/role="status"/g)).toHaveLength(1)
61
+ const footer = openingTags(html, "div").find((t) => /data-slot="list-footer"/.test(t)) ?? ""
62
+ expect(attribute(footer, "class")).toBe("sr-only")
63
+ expect(statusOf(render(paged({ items: [], total: 0, hasMore: false }), "de"))).toBe(
64
+ TOOLKIT_MESSAGES.de.listFooter.noResults,
65
+ )
66
+ })
67
+
68
+ it("a new search in flight: a visible Updating…, no count beside the previous rows", () => {
69
+ const html = render(paged({ stale: true, refreshing: true }))
70
+ expect(statusOf(html)).toBe("Updating…")
71
+ expect(html).not.toMatch(/Showing/)
72
+ expect(html).not.toContain("Load more")
73
+ })
74
+
75
+ it("a same-filter refetch: Updating… and the count of THIS filter stay", () => {
76
+ expect(statusOf(render(paged({ refreshing: true })))).toBe("Showing 2 of 3 items Updating…")
77
+ })
78
+ })
79
+
80
+ describe("PagedListFooter — two failures, two retries", () => {
81
+ const failed = new Error("[503] engine down")
82
+
83
+ it("a failed search over the previous rows: a warning alert whose retry re-runs page 0", () => {
84
+ const html = render(paged({ stale: true, error: failed }))
85
+ const [alert] = alerts(html)
86
+ expect(attribute(alert ?? "", "data-tone")).toBe("warning")
87
+ const texts = userFacingTexts(html).map((t) => t.value)
88
+ expect(texts).toContain(TOOLKIT_MESSAGES.en.listFooter.refreshFailed)
89
+ expect(texts).toContain(TOOLKIT_MESSAGES.en.listFooter.refreshFailedHint)
90
+ // The raw reason sits under Details, never in the sentence.
91
+ expect(html).toMatch(/<summary[^>]*>Details<\/summary><pre[^>]*>\[503\] engine down<\/pre>/)
92
+ expect(html).toContain("Try again</button>")
93
+ expect(html).not.toMatch(/Showing/)
94
+ })
95
+
96
+ it("leaves a page-0 failure WITHOUT rows on screen to the caller's loading guard", () => {
97
+ expect(alerts(render(paged({ firstPage: null, error: failed })))).toHaveLength(0)
98
+ expect(alerts(render(paged({ firstPage: undefined, error: failed })))).toHaveLength(0)
99
+ })
100
+
101
+ it("a failed load-more: a danger alert next to a still-current count", () => {
102
+ const html = render(paged({ loadMoreError: failed }))
103
+ const [alert] = alerts(html)
104
+ expect(attribute(alert ?? "", "data-tone")).toBe("danger")
105
+ expect(userFacingTexts(html).map((t) => t.value)).toContain(
106
+ TOOLKIT_MESSAGES.en.listFooter.loadMoreFailed,
107
+ )
108
+ expect(statusOf(html)).toBe("Showing 2 of 3 items")
109
+ })
110
+
111
+ it("keeps both failures apart, each with its own retry, in German", () => {
112
+ const html = render(paged({ stale: true, error: failed, loadMoreError: failed }), "de")
113
+ expect(alerts(html).map((a) => attribute(a, "data-tone"))).toEqual(["warning", "danger"])
114
+ const texts = userFacingTexts(html).map((t) => t.value)
115
+ expect(texts).toContain("Die Liste konnte nicht aktualisiert werden")
116
+ expect(texts).toContain("Weitere Einträge konnten nicht geladen werden")
117
+ expect(openingTags(html, "button")).toHaveLength(2)
118
+ expect(html.match(/Erneut versuchen<\/button>/g)).toHaveLength(2)
119
+ })
120
+
121
+ it("offers no retry it cannot run, and takes the caller's labels", () => {
122
+ // A paged state without a page-0 retry callback.
123
+ const noRetryState = paged({ error: failed })
124
+ delete noRetryState.retry
125
+ const withoutRetry = render(noRetryState)
126
+ expect(alerts(withoutRetry)).toHaveLength(1)
127
+ // Only Load more is a button: the alert offers no retry without paged.retry.
128
+ expect(openingTags(withoutRetry, "button")).toHaveLength(1)
129
+ expect(withoutRetry).not.toContain("Try again</button>")
130
+ const custom = renderToStaticMarkup(
131
+ <PagedListFooter
132
+ paged={paged({ loadMoreError: failed, loadMore: noop })}
133
+ labels={{ loadMoreFailed: "More tasks didn't load" }}
134
+ retryLabel="Load again"
135
+ />,
136
+ )
137
+ expect(userFacingTexts(custom).map((t) => t.value)).toContain("More tasks didn't load")
138
+ expect(custom).toContain("Load again</button>")
139
+ })
140
+ })
@@ -0,0 +1,107 @@
1
+ import { useToolkitMessages } from "../locale/locale-provider.js"
2
+ import type { ListFooterMessages } from "../locale/message-types.js"
3
+ import { ErrorState } from "./ErrorState.js"
4
+ import { ListFooter } from "./ListFooter.js"
5
+
6
+ /**
7
+ * What {@link PagedListFooter} reads from a paged list — structural, so a
8
+ * paged-query hook's result (`items`, `total`, `loadMore`, …) passes as is.
9
+ */
10
+ export interface PagedListState {
11
+ /** The rows on screen (all loaded pages); its length is the "shown" count. */
12
+ items: readonly unknown[]
13
+ /** The server's total for the rows on screen. */
14
+ total: number
15
+ /** Whether another page exists. */
16
+ hasMore: boolean
17
+ /** Fetch the next page (also the retry of a failed load-more). */
18
+ loadMore: () => void
19
+ /** A next page is in flight. */
20
+ loadingMore?: boolean
21
+ /** A first page is in flight while rows are on screen (a search, a filter, a refetch). */
22
+ refreshing?: boolean
23
+ /** The rows on screen are the previous result: a changed search or filter is in flight or failed. */
24
+ stale?: boolean
25
+ /**
26
+ * The first page that is on screen, if any. A first-page {@link error} only
27
+ * belongs in the footer while one is shown; without rows on screen it is
28
+ * the load error the caller's loading guard renders.
29
+ */
30
+ firstPage?: unknown
31
+ /** The first-page failure (see {@link firstPage}). */
32
+ error?: Error | null
33
+ /** Re-run the first page — the retry of {@link error}. */
34
+ retry?: () => void
35
+ /** The last load-more failure; {@link loadMore} retries it, the rows above stay current. */
36
+ loadMoreError?: Error | null
37
+ }
38
+
39
+ export interface PagedListFooterProps {
40
+ /** The paged list's state. */
41
+ paged: PagedListState
42
+ /** Plural noun for "Showing X of Y {noun}". Default: the locale's word for rows. */
43
+ noun?: string
44
+ /** Override strings of the active catalog's `listFooter` section. */
45
+ labels?: Partial<ListFooterMessages>
46
+ /** Retry button label of both error lines. Default: the locale's "Try again". */
47
+ retryLabel?: string
48
+ }
49
+
50
+ /** The raw reason of a failure, for the "Details" disclosure (never the sentence). */
51
+ function rawReason(error: Error): string {
52
+ return error.message || error.name
53
+ }
54
+
55
+ /**
56
+ * The tail of a paged list: its failures and the honest {@link ListFooter}.
57
+ * Two failures stay apart because their retries differ — a failed first page
58
+ * over the rows still on screen ("You're seeing the previous result", its
59
+ * retry re-runs page 0) and a failed load-more ("The entries above are
60
+ * current", its retry fetches the next offset). Each is an inline
61
+ * `ErrorState` (role="alert") with the raw reason under "Details". The
62
+ * footer's count line is a polite live region that says "Updating…" while a
63
+ * first page is in flight, hides the count while the rows are stale, and
64
+ * stays mounted at zero rows ("No entries", visually hidden), so a search
65
+ * that finds nothing is announced.
66
+ */
67
+ export function PagedListFooter({ paged, noun, labels, retryLabel }: PagedListFooterProps) {
68
+ const catalog = useToolkitMessages().listFooter
69
+ const L = { ...catalog, ...labels }
70
+ const refreshError = paged.firstPage != null ? (paged.error ?? null) : null
71
+ return (
72
+ <div data-slot="paged-list-footer" className="flex flex-col gap-2">
73
+ {refreshError && (
74
+ <ErrorState
75
+ variant="inline"
76
+ tone="warning"
77
+ title={L.refreshFailed}
78
+ description={L.refreshFailedHint}
79
+ details={rawReason(refreshError)}
80
+ onRetry={paged.retry}
81
+ retryLabel={retryLabel}
82
+ />
83
+ )}
84
+ {paged.loadMoreError && (
85
+ <ErrorState
86
+ variant="inline"
87
+ title={L.loadMoreFailed}
88
+ description={L.loadMoreFailedHint}
89
+ details={rawReason(paged.loadMoreError)}
90
+ onRetry={paged.loadMore}
91
+ retryLabel={retryLabel}
92
+ />
93
+ )}
94
+ <ListFooter
95
+ shown={paged.items.length}
96
+ total={paged.total}
97
+ hasMore={paged.hasMore}
98
+ loadingMore={paged.loadingMore}
99
+ onLoadMore={paged.loadMore}
100
+ noun={noun}
101
+ refreshing={paged.refreshing}
102
+ stale={paged.stale}
103
+ labels={labels}
104
+ />
105
+ </div>
106
+ )
107
+ }
@@ -0,0 +1,77 @@
1
+ import { renderToStaticMarkup } from "react-dom/server"
2
+ import { describe, expect, it } from "vitest"
3
+ import { attribute, openingTags, userFacingTexts } from "../render-scan.js"
4
+ import { TONE_BORDER, TONE_DOT, TONE_SOFT, TONE_VARIANTS } from "../lib/tone-utils.js"
5
+ import { CountPill, LivePill, StatusBadge } from "./Pills.js"
6
+
7
+ const classOf = (tag: string | undefined) => attribute(tag ?? "", "class") ?? ""
8
+
9
+ describe("StatusBadge: the state in words, marked in its tone", () => {
10
+ it.each(TONE_VARIANTS)("%s: tint, edge and a decorative dot next to foreground text", (tone) => {
11
+ const html = renderToStaticMarkup(<StatusBadge tone={tone}>Suspended</StatusBadge>)
12
+ const [badge, dot] = openingTags(html, "span")
13
+ expect(attribute(badge ?? "", "data-tone")).toBe(tone)
14
+ expect(classOf(badge)).toContain(TONE_SOFT[tone])
15
+ expect(classOf(badge)).toContain(TONE_BORDER[tone])
16
+ expect(classOf(badge)).toContain("text-small")
17
+ expect(attribute(dot ?? "", "aria-hidden")).toBe("true")
18
+ expect(classOf(dot)).toContain(TONE_DOT[tone])
19
+ // The words carry the state; no tone colour reaches them.
20
+ expect(html).not.toMatch(/\btext-(?:success|warning|danger|info)(?:-ink)?\b/)
21
+ expect(userFacingTexts(html).map((t) => t.value)).toEqual(["Suspended"])
22
+ })
23
+
24
+ it("maps a retired tone name onto the vocabulary (critical → danger)", () => {
25
+ const html = renderToStaticMarkup(<StatusBadge tone="critical">Open</StatusBadge>)
26
+ expect(attribute(openingTags(html, "span")[0] ?? "", "data-tone")).toBe("danger")
27
+ })
28
+
29
+ it("requires its text: colour is never the only signal", () => {
30
+ // @ts-expect-error — children are mandatory
31
+ const bare = <StatusBadge tone="danger" />
32
+ // @ts-expect-error — null is no text either
33
+ const nothing = <StatusBadge tone="danger">{null}</StatusBadge>
34
+ expect(bare.props).not.toHaveProperty("children")
35
+ expect(nothing.props).toHaveProperty("children", null)
36
+ const mixed = renderToStaticMarkup(
37
+ <StatusBadge tone="info">
38
+ {3} open {"incidents"}
39
+ </StatusBadge>,
40
+ )
41
+ expect(mixed.replace(/<!-- -->/g, "")).toContain("3 open incidents")
42
+ })
43
+ })
44
+
45
+ describe("LivePill and CountPill", () => {
46
+ it("LivePill pulses only without reduced motion and defaults to the locale's Live", () => {
47
+ const html = renderToStaticMarkup(<LivePill />)
48
+ const dot = openingTags(html, "span")[1] ?? ""
49
+ expect(classOf(dot)).toContain("motion-safe:animate-pulse")
50
+ expect(classOf(dot).split(/\s+/)).not.toContain("animate-pulse")
51
+ expect(html).toContain(">Live</span>")
52
+ expect(classOf(openingTags(html, "span")[0])).toContain(TONE_SOFT.info)
53
+ })
54
+
55
+ it("CountPill keeps a neutral count in the foreground on the muted tint", () => {
56
+ const html = renderToStaticMarkup(<CountPill>7</CountPill>)
57
+ expect(classOf(openingTags(html, "span")[0])).toContain("bg-muted text-foreground")
58
+ expect(html).not.toContain("text-muted-foreground")
59
+ })
60
+
61
+ it("both take a className and resolve tone aliases", () => {
62
+ const live = renderToStaticMarkup(
63
+ <LivePill tone="ok" className="ml-2">
64
+ On
65
+ </LivePill>,
66
+ )
67
+ expect(classOf(openingTags(live, "span")[0])).toContain(TONE_SOFT.success)
68
+ expect(classOf(openingTags(live, "span")[0])).toContain("ml-2")
69
+ const count = renderToStaticMarkup(
70
+ <CountPill tone="error" className="ml-auto">
71
+ 3
72
+ </CountPill>,
73
+ )
74
+ expect(classOf(openingTags(count, "span")[0])).toContain(TONE_BORDER.danger)
75
+ expect(classOf(openingTags(count, "span")[0])).toContain("ml-auto")
76
+ })
77
+ })
@@ -1,12 +1,14 @@
1
- import type { ReactNode } from "react"
1
+ import type { ReactElement, ReactNode } from "react"
2
2
  import { useToolkitMessages } from "../locale/locale-provider.js"
3
- import { TONE_BORDER, TONE_DOT, TONE_SOFT, type ToneVariant } from "../lib/tone-utils.js"
3
+ import { resolveTone, TONE_BORDER, TONE_DOT, TONE_SOFT, type ToneName } from "../lib/tone-utils.js"
4
+ import { cn } from "../lib/utils.js"
4
5
 
5
6
  export interface LivePillProps {
6
7
  /** Status tone driving the dot, tint and border. Default `"info"`. */
7
- tone?: ToneVariant
8
+ tone?: ToneName
8
9
  /** Pill label. Defaults to the locale's "Live". */
9
10
  children?: ReactNode
11
+ className?: string
10
12
  }
11
13
 
12
14
  /**
@@ -15,38 +17,89 @@ export interface LivePillProps {
15
17
  * text in the foreground), so it inherits the active theme. The pulse stops
16
18
  * under `prefers-reduced-motion`.
17
19
  */
18
- export function LivePill({ tone = "info", children }: LivePillProps) {
20
+ export function LivePill({ tone = "info", children, className }: LivePillProps) {
19
21
  const messages = useToolkitMessages()
22
+ const t = resolveTone(tone, "info")
20
23
  return (
21
24
  <span
22
- className={`inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 text-xs font-semibold ${TONE_SOFT[tone]} ${TONE_BORDER[tone]}`}
25
+ className={cn(
26
+ "text-small inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 font-semibold",
27
+ TONE_SOFT[t],
28
+ TONE_BORDER[t],
29
+ className,
30
+ )}
23
31
  >
24
32
  <span
25
33
  aria-hidden="true"
26
- className={`size-1.5 animate-pulse rounded-full ${TONE_DOT[tone]}`}
34
+ className={cn("size-1.5 rounded-full motion-safe:animate-pulse", TONE_DOT[t])}
27
35
  />
28
36
  {children ?? messages.pills.live}
29
37
  </span>
30
38
  )
31
39
  }
32
40
 
41
+ /** The words a {@link StatusBadge} must carry: text, a number or an element — never nothing. */
42
+ export type StatusBadgeText = string | number | ReactElement | (string | number | ReactElement)[]
43
+
44
+ export interface StatusBadgeProps {
45
+ /** The state, in words ("Suspended", "3 open incidents"). Required: colour is never the only signal. */
46
+ children: StatusBadgeText
47
+ /** The state's tone: tint, edge and dot. */
48
+ tone: ToneName
49
+ className?: string
50
+ }
51
+
52
+ /**
53
+ * A state in words, marked in its tone: tint, edge and a static dot next to
54
+ * foreground text (CI §3.3: the colour marks the state, the words say it).
55
+ * For a detail-page header (`WidgetHeader`'s `badge`) or a table cell. The
56
+ * text is required — a badge without words would make colour the only
57
+ * signal. The pulsing dot is {@link LivePill}'s live-data semantic.
58
+ */
59
+ export function StatusBadge({ children, tone, className }: StatusBadgeProps) {
60
+ const t = resolveTone(tone)
61
+ return (
62
+ <span
63
+ data-slot="status-badge"
64
+ data-tone={t}
65
+ className={cn(
66
+ "text-small inline-flex w-fit items-center gap-1.5 rounded-sm border px-2.5 py-0.5 font-semibold",
67
+ TONE_SOFT[t],
68
+ TONE_BORDER[t],
69
+ className,
70
+ )}
71
+ >
72
+ <span aria-hidden="true" className={cn("size-1.5 shrink-0 rounded-full", TONE_DOT[t])} />
73
+ {children}
74
+ </span>
75
+ )
76
+ }
77
+
33
78
  export interface CountPillProps {
34
79
  /** Status tone driving the tint and border. Default `"neutral"`. */
35
- tone?: ToneVariant
80
+ tone?: ToneName
36
81
  /** The count/value to display (tabular numbers). */
37
82
  children: ReactNode
83
+ className?: string
38
84
  }
39
85
 
40
86
  /**
41
87
  * Compact count badge — tabular numbers on a tinted background with a border
42
88
  * in the tone. Use as the right-aligned indicator on a list/group row (e.g. a
43
89
  * per-group item count). The number stays in the foreground in every tone
44
- * (no coloured digits): the tint and the border carry the tone.
90
+ * (no coloured digits): the tint and the border carry the tone. A count
91
+ * without a state stays `neutral`.
45
92
  */
46
- export function CountPill({ tone = "neutral", children }: CountPillProps) {
93
+ export function CountPill({ tone = "neutral", children, className }: CountPillProps) {
94
+ const t = resolveTone(tone)
47
95
  return (
48
96
  <span
49
- className={`inline-flex min-w-[2.5rem] items-center justify-center rounded-sm border px-2.5 py-0.5 text-sm font-semibold tabular-nums ${TONE_SOFT[tone]} ${TONE_BORDER[tone]}`}
97
+ className={cn(
98
+ "text-dense inline-flex min-w-10 items-center justify-center rounded-sm border px-2.5 py-0.5 font-semibold tabular-nums",
99
+ TONE_SOFT[t],
100
+ TONE_BORDER[t],
101
+ className,
102
+ )}
50
103
  >
51
104
  {children}
52
105
  </span>
@@ -1,4 +1,6 @@
1
1
  import type { ReactNode } from "react"
2
+ import { MICRO_LABEL } from "../lib/typography.js"
3
+ import { cn } from "../lib/utils.js"
2
4
 
3
5
  export interface SectionHeadingProps {
4
6
  /** The section title. */
@@ -12,14 +14,18 @@ export interface SectionHeadingProps {
12
14
  /**
13
15
  * Section title row — a small semibold heading with an optional muted hint or
14
16
  * control on the right. Use to label a block inside a widget body (e.g. a list
15
- * or sub-table) without the visual weight of a full card header.
17
+ * or sub-table) without the visual weight of a full card header. The title is
18
+ * a caption ({@link MICRO_LABEL}: 13 px semibold, normal case); a section that
19
+ * needs a real heading takes `text-section` (21 px) instead.
16
20
  */
17
21
  export function SectionHeading({ title, hint, trailing }: SectionHeadingProps) {
18
22
  return (
19
- <div className="text-muted-foreground mb-3 flex items-center justify-between text-sm font-semibold">
23
+ <div
24
+ className={cn("text-muted-foreground mb-3 flex items-center justify-between", MICRO_LABEL)}
25
+ >
20
26
  <span>{title}</span>
21
27
  {(hint || trailing) && (
22
- <span className="text-muted-foreground text-xs font-medium">{trailing ?? hint}</span>
28
+ <span className="text-muted-foreground text-small font-medium">{trailing ?? hint}</span>
23
29
  )}
24
30
  </div>
25
31
  )