@miragon/mcp-toolkit-ui 2.6.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 (511) hide show
  1. package/README.md +14 -5
  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.d.ts +7 -2
  7. package/dist/app/builder/BuilderBanners.d.ts.map +1 -1
  8. package/dist/app/builder/BuilderBanners.js +8 -3
  9. package/dist/app/builder/BuilderBanners.js.map +1 -1
  10. package/dist/app/builder/CanvasRow.d.ts +4 -2
  11. package/dist/app/builder/CanvasRow.d.ts.map +1 -1
  12. package/dist/app/builder/CanvasRow.js +29 -17
  13. package/dist/app/builder/CanvasRow.js.map +1 -1
  14. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -1
  15. package/dist/app/builder/CatalogueSheet.js +14 -6
  16. package/dist/app/builder/CatalogueSheet.js.map +1 -1
  17. package/dist/app/builder/CellPropsSheet.d.ts +7 -3
  18. package/dist/app/builder/CellPropsSheet.d.ts.map +1 -1
  19. package/dist/app/builder/CellPropsSheet.js +10 -3
  20. package/dist/app/builder/CellPropsSheet.js.map +1 -1
  21. package/dist/app/builder/DiscardDialog.d.ts +18 -0
  22. package/dist/app/builder/DiscardDialog.d.ts.map +1 -0
  23. package/dist/app/builder/DiscardDialog.js +18 -0
  24. package/dist/app/builder/DiscardDialog.js.map +1 -0
  25. package/dist/app/builder/EmptyHint.d.ts +4 -0
  26. package/dist/app/builder/EmptyHint.d.ts.map +1 -1
  27. package/dist/app/builder/EmptyHint.js +6 -1
  28. package/dist/app/builder/EmptyHint.js.map +1 -1
  29. package/dist/app/builder/LayoutSurface.d.ts +33 -9
  30. package/dist/app/builder/LayoutSurface.d.ts.map +1 -1
  31. package/dist/app/builder/LayoutSurface.js +74 -23
  32. package/dist/app/builder/LayoutSurface.js.map +1 -1
  33. package/dist/app/builder/PipelineStrip.d.ts +5 -2
  34. package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
  35. package/dist/app/builder/PipelineStrip.js +33 -15
  36. package/dist/app/builder/PipelineStrip.js.map +1 -1
  37. package/dist/app/builder/SaveDialog.d.ts +9 -1
  38. package/dist/app/builder/SaveDialog.d.ts.map +1 -1
  39. package/dist/app/builder/SaveDialog.js +9 -3
  40. package/dist/app/builder/SaveDialog.js.map +1 -1
  41. package/dist/app/builder/Toolbar.d.ts +14 -4
  42. package/dist/app/builder/Toolbar.d.ts.map +1 -1
  43. package/dist/app/builder/Toolbar.js +24 -7
  44. package/dist/app/builder/Toolbar.js.map +1 -1
  45. package/dist/app/builder/Workspace.d.ts +23 -3
  46. package/dist/app/builder/Workspace.d.ts.map +1 -1
  47. package/dist/app/builder/Workspace.js +58 -8
  48. package/dist/app/builder/Workspace.js.map +1 -1
  49. package/dist/app/builder/builder-model.d.ts +146 -3
  50. package/dist/app/builder/builder-model.d.ts.map +1 -1
  51. package/dist/app/builder/builder-model.js +196 -5
  52. package/dist/app/builder/builder-model.js.map +1 -1
  53. package/dist/app/builder/labels.d.ts +13 -62
  54. package/dist/app/builder/labels.d.ts.map +1 -1
  55. package/dist/app/builder/labels.js +9 -58
  56. package/dist/app/builder/labels.js.map +1 -1
  57. package/dist/app/builder/reveal.d.ts +10 -0
  58. package/dist/app/builder/reveal.d.ts.map +1 -0
  59. package/dist/app/builder/reveal.js +10 -0
  60. package/dist/app/builder/reveal.js.map +1 -0
  61. package/dist/app/builder/status-chip.d.ts.map +1 -1
  62. package/dist/app/builder/status-chip.js +3 -2
  63. package/dist/app/builder/status-chip.js.map +1 -1
  64. package/dist/app/builder/use-builder-catalogue.d.ts +5 -1
  65. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
  66. package/dist/app/builder/use-builder-catalogue.js +50 -17
  67. package/dist/app/builder/use-builder-catalogue.js.map +1 -1
  68. package/dist/app/builder/use-builder-session.d.ts +30 -0
  69. package/dist/app/builder/use-builder-session.d.ts.map +1 -0
  70. package/dist/app/builder/use-builder-session.js +58 -0
  71. package/dist/app/builder/use-builder-session.js.map +1 -0
  72. package/dist/app/builder/use-display-resolver.d.ts +17 -0
  73. package/dist/app/builder/use-display-resolver.d.ts.map +1 -0
  74. package/dist/app/builder/use-display-resolver.js +16 -0
  75. package/dist/app/builder/use-display-resolver.js.map +1 -0
  76. package/dist/app/builder/use-save-dashboard.d.ts +3 -1
  77. package/dist/app/builder/use-save-dashboard.d.ts.map +1 -1
  78. package/dist/app/builder/use-save-dashboard.js +3 -1
  79. package/dist/app/builder/use-save-dashboard.js.map +1 -1
  80. package/dist/app/index.d.ts +8 -3
  81. package/dist/app/index.d.ts.map +1 -1
  82. package/dist/app/index.js +8 -2
  83. package/dist/app/index.js.map +1 -1
  84. package/dist/app/layout-builder.d.ts +18 -25
  85. package/dist/app/layout-builder.d.ts.map +1 -1
  86. package/dist/app/layout-builder.js +84 -78
  87. package/dist/app/layout-builder.js.map +1 -1
  88. package/dist/app/mcp-app-view-chrome.d.ts +103 -20
  89. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
  90. package/dist/app/mcp-app-view-chrome.js +125 -40
  91. package/dist/app/mcp-app-view-chrome.js.map +1 -1
  92. package/dist/app/mcp-app-view-states.d.ts +23 -0
  93. package/dist/app/mcp-app-view-states.d.ts.map +1 -0
  94. package/dist/app/mcp-app-view-states.js +55 -0
  95. package/dist/app/mcp-app-view-states.js.map +1 -0
  96. package/dist/app/mcp-app-view.d.ts +72 -5
  97. package/dist/app/mcp-app-view.d.ts.map +1 -1
  98. package/dist/app/mcp-app-view.js +241 -141
  99. package/dist/app/mcp-app-view.js.map +1 -1
  100. package/dist/app/mcp-toolkit-app.d.ts +8 -2
  101. package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
  102. package/dist/app/mcp-toolkit-app.js +33 -4
  103. package/dist/app/mcp-toolkit-app.js.map +1 -1
  104. package/dist/app/mcp-use-host-bridge.d.ts +15 -6
  105. package/dist/app/mcp-use-host-bridge.d.ts.map +1 -1
  106. package/dist/app/mcp-use-host-bridge.js +51 -22
  107. package/dist/app/mcp-use-host-bridge.js.map +1 -1
  108. package/dist/app/model-context.d.ts +71 -0
  109. package/dist/app/model-context.d.ts.map +1 -0
  110. package/dist/app/model-context.js +101 -0
  111. package/dist/app/model-context.js.map +1 -0
  112. package/dist/app/use-build-mode-focus.d.ts +33 -0
  113. package/dist/app/use-build-mode-focus.d.ts.map +1 -0
  114. package/dist/app/use-build-mode-focus.js +59 -0
  115. package/dist/app/use-build-mode-focus.js.map +1 -0
  116. package/dist/app/use-view-actions.d.ts +31 -0
  117. package/dist/app/use-view-actions.d.ts.map +1 -0
  118. package/dist/app/use-view-actions.js +81 -0
  119. package/dist/app/use-view-actions.js.map +1 -0
  120. package/dist/app/use-view-lifecycle.d.ts +42 -0
  121. package/dist/app/use-view-lifecycle.d.ts.map +1 -0
  122. package/dist/app/use-view-lifecycle.js +81 -0
  123. package/dist/app/use-view-lifecycle.js.map +1 -0
  124. package/dist/app/view-lifecycle.d.ts +185 -0
  125. package/dist/app/view-lifecycle.d.ts.map +1 -0
  126. package/dist/app/view-lifecycle.js +207 -0
  127. package/dist/app/view-lifecycle.js.map +1 -0
  128. package/dist/app/widget-fixture.d.ts +10 -3
  129. package/dist/app/widget-fixture.d.ts.map +1 -1
  130. package/dist/app/widget-fixture.js +21 -8
  131. package/dist/app/widget-fixture.js.map +1 -1
  132. package/dist/app/widget-props-sheet.d.ts +6 -10
  133. package/dist/app/widget-props-sheet.d.ts.map +1 -1
  134. package/dist/app/widget-props-sheet.js +25 -20
  135. package/dist/app/widget-props-sheet.js.map +1 -1
  136. package/dist/app/widget-renderer.d.ts +41 -1
  137. package/dist/app/widget-renderer.d.ts.map +1 -1
  138. package/dist/app/widget-renderer.js +82 -31
  139. package/dist/app/widget-renderer.js.map +1 -1
  140. package/dist/components/DrillButton.d.ts +9 -6
  141. package/dist/components/DrillButton.d.ts.map +1 -1
  142. package/dist/components/DrillButton.js +9 -6
  143. package/dist/components/DrillButton.js.map +1 -1
  144. package/dist/components/EmptyState.d.ts +33 -0
  145. package/dist/components/EmptyState.d.ts.map +1 -0
  146. package/dist/components/EmptyState.js +19 -0
  147. package/dist/components/EmptyState.js.map +1 -0
  148. package/dist/components/ErrorState.d.ts +53 -0
  149. package/dist/components/ErrorState.d.ts.map +1 -0
  150. package/dist/components/ErrorState.js +77 -0
  151. package/dist/components/ErrorState.js.map +1 -0
  152. package/dist/components/FilterBar.d.ts +16 -5
  153. package/dist/components/FilterBar.d.ts.map +1 -1
  154. package/dist/components/FilterBar.js +13 -5
  155. package/dist/components/FilterBar.js.map +1 -1
  156. package/dist/components/GroupCard.d.ts.map +1 -1
  157. package/dist/components/GroupCard.js +2 -1
  158. package/dist/components/GroupCard.js.map +1 -1
  159. package/dist/components/KpiGrid.d.ts +70 -18
  160. package/dist/components/KpiGrid.d.ts.map +1 -1
  161. package/dist/components/KpiGrid.js +112 -23
  162. package/dist/components/KpiGrid.js.map +1 -1
  163. package/dist/components/ListFooter.d.ts +32 -4
  164. package/dist/components/ListFooter.d.ts.map +1 -1
  165. package/dist/components/ListFooter.js +35 -6
  166. package/dist/components/ListFooter.js.map +1 -1
  167. package/dist/components/PagedListFooter.d.ts +57 -0
  168. package/dist/components/PagedListFooter.d.ts.map +1 -0
  169. package/dist/components/PagedListFooter.js +27 -0
  170. package/dist/components/PagedListFooter.js.map +1 -0
  171. package/dist/components/Pills.d.ts +36 -12
  172. package/dist/components/Pills.d.ts.map +1 -1
  173. package/dist/components/Pills.js +29 -8
  174. package/dist/components/Pills.js.map +1 -1
  175. package/dist/components/SectionHeading.d.ts +3 -1
  176. package/dist/components/SectionHeading.d.ts.map +1 -1
  177. package/dist/components/SectionHeading.js +6 -2
  178. package/dist/components/SectionHeading.js.map +1 -1
  179. package/dist/components/Toast.d.ts +43 -0
  180. package/dist/components/Toast.d.ts.map +1 -0
  181. package/dist/components/Toast.js +106 -0
  182. package/dist/components/Toast.js.map +1 -0
  183. package/dist/components/WidgetHeader.d.ts +34 -10
  184. package/dist/components/WidgetHeader.d.ts.map +1 -1
  185. package/dist/components/WidgetHeader.js +11 -6
  186. package/dist/components/WidgetHeader.js.map +1 -1
  187. package/dist/hooks/index.d.ts +4 -4
  188. package/dist/hooks/index.d.ts.map +1 -1
  189. package/dist/hooks/index.js +1 -1
  190. package/dist/hooks/index.js.map +1 -1
  191. package/dist/hooks/tool-result-recovery-model.d.ts +155 -0
  192. package/dist/hooks/tool-result-recovery-model.d.ts.map +1 -0
  193. package/dist/hooks/tool-result-recovery-model.js +273 -0
  194. package/dist/hooks/tool-result-recovery-model.js.map +1 -0
  195. package/dist/hooks/use-tool-query.d.ts +56 -14
  196. package/dist/hooks/use-tool-query.d.ts.map +1 -1
  197. package/dist/hooks/use-tool-query.js +102 -25
  198. package/dist/hooks/use-tool-query.js.map +1 -1
  199. package/dist/hooks/use-tool-result-recovery.d.ts +61 -44
  200. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
  201. package/dist/hooks/use-tool-result-recovery.js +62 -110
  202. package/dist/hooks/use-tool-result-recovery.js.map +1 -1
  203. package/dist/hooks/use-view-data.d.ts +55 -7
  204. package/dist/hooks/use-view-data.d.ts.map +1 -1
  205. package/dist/hooks/use-view-data.js +81 -11
  206. package/dist/hooks/use-view-data.js.map +1 -1
  207. package/dist/hooks/use-view-tool-query.d.ts +12 -7
  208. package/dist/hooks/use-view-tool-query.d.ts.map +1 -1
  209. package/dist/hooks/use-view-tool-query.js +15 -14
  210. package/dist/hooks/use-view-tool-query.js.map +1 -1
  211. package/dist/index.d.ts +27 -13
  212. package/dist/index.d.ts.map +1 -1
  213. package/dist/index.js +29 -7
  214. package/dist/index.js.map +1 -1
  215. package/dist/lib/focus-return.d.ts +61 -0
  216. package/dist/lib/focus-return.d.ts.map +1 -0
  217. package/dist/lib/focus-return.js +46 -0
  218. package/dist/lib/focus-return.js.map +1 -0
  219. package/dist/lib/focus-ring.d.ts +18 -0
  220. package/dist/lib/focus-ring.d.ts.map +1 -0
  221. package/dist/lib/focus-ring.js +18 -0
  222. package/dist/lib/focus-ring.js.map +1 -0
  223. package/dist/lib/icon-stroke.d.ts +12 -0
  224. package/dist/lib/icon-stroke.d.ts.map +1 -0
  225. package/dist/lib/icon-stroke.js +12 -0
  226. package/dist/lib/icon-stroke.js.map +1 -0
  227. package/dist/lib/parse-tool-result.d.ts +23 -1
  228. package/dist/lib/parse-tool-result.d.ts.map +1 -1
  229. package/dist/lib/parse-tool-result.js +38 -2
  230. package/dist/lib/parse-tool-result.js.map +1 -1
  231. package/dist/lib/toast-model.d.ts +86 -0
  232. package/dist/lib/toast-model.d.ts.map +1 -0
  233. package/dist/lib/toast-model.js +102 -0
  234. package/dist/lib/toast-model.js.map +1 -0
  235. package/dist/lib/tone-utils.d.ts +95 -9
  236. package/dist/lib/tone-utils.d.ts.map +1 -1
  237. package/dist/lib/tone-utils.js +100 -9
  238. package/dist/lib/tone-utils.js.map +1 -1
  239. package/dist/lib/tool-query.d.ts +107 -0
  240. package/dist/lib/tool-query.d.ts.map +1 -0
  241. package/dist/lib/tool-query.js +182 -0
  242. package/dist/lib/tool-query.js.map +1 -0
  243. package/dist/lib/tool-result-observer.d.ts +108 -0
  244. package/dist/lib/tool-result-observer.d.ts.map +1 -0
  245. package/dist/lib/tool-result-observer.js +158 -0
  246. package/dist/lib/tool-result-observer.js.map +1 -0
  247. package/dist/lib/typography.d.ts +37 -0
  248. package/dist/lib/typography.d.ts.map +1 -0
  249. package/dist/lib/typography.js +41 -0
  250. package/dist/lib/typography.js.map +1 -0
  251. package/dist/lib/utils.d.ts.map +1 -1
  252. package/dist/lib/utils.js +11 -1
  253. package/dist/lib/utils.js.map +1 -1
  254. package/dist/locale/index.d.ts +3 -1
  255. package/dist/locale/index.d.ts.map +1 -1
  256. package/dist/locale/index.js +2 -1
  257. package/dist/locale/index.js.map +1 -1
  258. package/dist/locale/locale-provider.d.ts +42 -1
  259. package/dist/locale/locale-provider.d.ts.map +1 -1
  260. package/dist/locale/locale-provider.js +44 -3
  261. package/dist/locale/locale-provider.js.map +1 -1
  262. package/dist/locale/message-types.d.ts +336 -0
  263. package/dist/locale/message-types.d.ts.map +1 -0
  264. package/dist/locale/message-types.js +7 -0
  265. package/dist/locale/message-types.js.map +1 -0
  266. package/dist/locale/messages-de.d.ts +9 -0
  267. package/dist/locale/messages-de.d.ts.map +1 -0
  268. package/dist/locale/messages-de.js +185 -0
  269. package/dist/locale/messages-de.js.map +1 -0
  270. package/dist/locale/messages-en.d.ts +8 -0
  271. package/dist/locale/messages-en.d.ts.map +1 -0
  272. package/dist/locale/messages-en.js +184 -0
  273. package/dist/locale/messages-en.js.map +1 -0
  274. package/dist/locale/toolkit-messages.d.ts +57 -0
  275. package/dist/locale/toolkit-messages.d.ts.map +1 -0
  276. package/dist/locale/toolkit-messages.js +103 -0
  277. package/dist/locale/toolkit-messages.js.map +1 -0
  278. package/dist/primitives/alert.d.ts +1 -1
  279. package/dist/primitives/alert.d.ts.map +1 -1
  280. package/dist/primitives/alert.js +6 -2
  281. package/dist/primitives/alert.js.map +1 -1
  282. package/dist/primitives/badge.d.ts +1 -1
  283. package/dist/primitives/badge.d.ts.map +1 -1
  284. package/dist/primitives/badge.js +19 -6
  285. package/dist/primitives/badge.js.map +1 -1
  286. package/dist/primitives/button.d.ts +1 -1
  287. package/dist/primitives/button.d.ts.map +1 -1
  288. package/dist/primitives/button.js +12 -8
  289. package/dist/primitives/button.js.map +1 -1
  290. package/dist/primitives/card.js +4 -4
  291. package/dist/primitives/card.js.map +1 -1
  292. package/dist/primitives/dialog.d.ts +7 -3
  293. package/dist/primitives/dialog.d.ts.map +1 -1
  294. package/dist/primitives/dialog.js +22 -8
  295. package/dist/primitives/dialog.js.map +1 -1
  296. package/dist/primitives/input.d.ts.map +1 -1
  297. package/dist/primitives/input.js +4 -1
  298. package/dist/primitives/input.js.map +1 -1
  299. package/dist/primitives/scroll-area.js +1 -1
  300. package/dist/primitives/scroll-area.js.map +1 -1
  301. package/dist/primitives/select.d.ts.map +1 -1
  302. package/dist/primitives/select.js +6 -3
  303. package/dist/primitives/select.js.map +1 -1
  304. package/dist/primitives/sheet.d.ts +3 -1
  305. package/dist/primitives/sheet.d.ts.map +1 -1
  306. package/dist/primitives/sheet.js +17 -7
  307. package/dist/primitives/sheet.js.map +1 -1
  308. package/dist/primitives/skeleton.js +1 -1
  309. package/dist/primitives/skeleton.js.map +1 -1
  310. package/dist/primitives/switch.d.ts.map +1 -1
  311. package/dist/primitives/switch.js +2 -1
  312. package/dist/primitives/switch.js.map +1 -1
  313. package/dist/primitives/tabs.d.ts.map +1 -1
  314. package/dist/primitives/tabs.js +5 -3
  315. package/dist/primitives/tabs.js.map +1 -1
  316. package/dist/providers/query-provider.d.ts +32 -1
  317. package/dist/providers/query-provider.d.ts.map +1 -1
  318. package/dist/providers/query-provider.js +53 -4
  319. package/dist/providers/query-provider.js.map +1 -1
  320. package/dist/render-cases-primitives.js +1 -1
  321. package/dist/render-cases-primitives.js.map +1 -1
  322. package/dist/render-cases.d.ts.map +1 -1
  323. package/dist/render-cases.js +48 -6
  324. package/dist/render-cases.js.map +1 -1
  325. package/dist/render-effects.d.ts +42 -0
  326. package/dist/render-effects.d.ts.map +1 -0
  327. package/dist/render-effects.js +81 -0
  328. package/dist/render-effects.js.map +1 -0
  329. package/dist/render-scan.d.ts +25 -0
  330. package/dist/render-scan.d.ts.map +1 -0
  331. package/dist/render-scan.js +66 -0
  332. package/dist/render-scan.js.map +1 -0
  333. package/dist/theme/cd-tokens.d.ts +66 -0
  334. package/dist/theme/cd-tokens.d.ts.map +1 -0
  335. package/dist/theme/cd-tokens.js +64 -0
  336. package/dist/theme/cd-tokens.js.map +1 -0
  337. package/dist/theme/create-theme.d.ts +214 -39
  338. package/dist/theme/create-theme.d.ts.map +1 -1
  339. package/dist/theme/create-theme.js +204 -13
  340. package/dist/theme/create-theme.js.map +1 -1
  341. package/dist/theme/fonts.d.ts +38 -0
  342. package/dist/theme/fonts.d.ts.map +1 -0
  343. package/dist/theme/fonts.js +56 -0
  344. package/dist/theme/fonts.js.map +1 -0
  345. package/dist/theme/host-fonts.d.ts +39 -0
  346. package/dist/theme/host-fonts.d.ts.map +1 -0
  347. package/dist/theme/host-fonts.js +44 -0
  348. package/dist/theme/host-fonts.js.map +1 -0
  349. package/dist/theme/index.d.ts +8 -1
  350. package/dist/theme/index.d.ts.map +1 -1
  351. package/dist/theme/index.js +5 -1
  352. package/dist/theme/index.js.map +1 -1
  353. package/dist/theme/portal-container.d.ts +16 -0
  354. package/dist/theme/portal-container.d.ts.map +1 -0
  355. package/dist/theme/portal-container.js +19 -0
  356. package/dist/theme/portal-container.js.map +1 -0
  357. package/dist/theme/presets.d.ts +24 -9
  358. package/dist/theme/presets.d.ts.map +1 -1
  359. package/dist/theme/presets.js +44 -11
  360. package/dist/theme/presets.js.map +1 -1
  361. package/dist/theme/theme-provider.d.ts +27 -9
  362. package/dist/theme/theme-provider.d.ts.map +1 -1
  363. package/dist/theme/theme-provider.js +42 -20
  364. package/dist/theme/theme-provider.js.map +1 -1
  365. package/package.json +11 -5
  366. package/src/app/adapt-data-widget.tsx +2 -42
  367. package/src/app/builder/BuilderBanners.tsx +26 -13
  368. package/src/app/builder/CanvasRow.tsx +79 -32
  369. package/src/app/builder/CatalogueSheet.tsx +58 -35
  370. package/src/app/builder/CellPropsSheet.tsx +17 -2
  371. package/src/app/builder/DiscardDialog.tsx +59 -0
  372. package/src/app/builder/EmptyHint.tsx +16 -11
  373. package/src/app/builder/LayoutSurface.tsx +207 -67
  374. package/src/app/builder/PipelineStrip.tsx +265 -156
  375. package/src/app/builder/SaveDialog.tsx +25 -14
  376. package/src/app/builder/Toolbar.tsx +154 -57
  377. package/src/app/builder/Workspace.tsx +226 -56
  378. package/src/app/builder/builder-feedback.test.tsx +75 -0
  379. package/src/app/builder/builder-model.test.ts +5 -0
  380. package/src/app/builder/builder-model.ts +346 -6
  381. package/src/app/builder/builder-model.ux.test.ts +456 -0
  382. package/src/app/builder/builder-ux.test.tsx +844 -0
  383. package/src/app/builder/labels.ts +15 -121
  384. package/src/app/builder/reveal.ts +10 -0
  385. package/src/app/builder/status-chip.tsx +8 -4
  386. package/src/app/builder/use-builder-catalogue.ts +62 -19
  387. package/src/app/builder/use-builder-session.ts +68 -0
  388. package/src/app/builder/use-display-resolver.ts +24 -0
  389. package/src/app/builder/use-save-dashboard.ts +5 -0
  390. package/src/app/index.ts +20 -3
  391. package/src/app/layout-builder.tsx +193 -186
  392. package/src/app/mcp-app-view-chrome.test.tsx +487 -14
  393. package/src/app/mcp-app-view-chrome.tsx +328 -100
  394. package/src/app/mcp-app-view-states.tsx +140 -0
  395. package/src/app/mcp-app-view.effects.test.tsx +292 -0
  396. package/src/app/mcp-app-view.test.tsx +478 -0
  397. package/src/app/mcp-app-view.tsx +447 -190
  398. package/src/app/mcp-toolkit-app.test.tsx +40 -0
  399. package/src/app/mcp-toolkit-app.tsx +39 -8
  400. package/src/app/mcp-use-host-bridge.test.tsx +180 -0
  401. package/src/app/mcp-use-host-bridge.tsx +67 -24
  402. package/src/app/model-context.test.tsx +116 -0
  403. package/src/app/model-context.tsx +125 -0
  404. package/src/app/use-build-mode-focus.ts +62 -0
  405. package/src/app/use-view-actions.ts +89 -0
  406. package/src/app/use-view-lifecycle.ts +122 -0
  407. package/src/app/view-lifecycle.test.ts +443 -0
  408. package/src/app/view-lifecycle.ts +334 -0
  409. package/src/app/widget-fixture.effects.test.tsx +51 -0
  410. package/src/app/widget-fixture.render.test.tsx +81 -0
  411. package/src/app/widget-fixture.tsx +34 -11
  412. package/src/app/widget-props-sheet.tsx +67 -39
  413. package/src/app/widget-renderer.render.test.tsx +131 -0
  414. package/src/app/widget-renderer.tsx +136 -38
  415. package/src/components/DrillButton.tsx +24 -10
  416. package/src/components/EmptyState.tsx +83 -0
  417. package/src/components/ErrorState.tsx +204 -0
  418. package/src/components/FilterBar.test.tsx +82 -0
  419. package/src/components/FilterBar.tsx +56 -27
  420. package/src/components/GroupCard.tsx +3 -2
  421. package/src/components/KpiGrid.test.tsx +279 -0
  422. package/src/components/KpiGrid.tsx +329 -91
  423. package/src/components/ListFooter.test.tsx +116 -0
  424. package/src/components/ListFooter.tsx +110 -12
  425. package/src/components/PagedListFooter.test.tsx +140 -0
  426. package/src/components/PagedListFooter.tsx +107 -0
  427. package/src/components/Pills.test.tsx +77 -0
  428. package/src/components/Pills.tsx +78 -16
  429. package/src/components/SectionHeading.tsx +9 -3
  430. package/src/components/Toast.tsx +206 -0
  431. package/src/components/WidgetHeader.test.tsx +78 -0
  432. package/src/components/WidgetHeader.tsx +63 -16
  433. package/src/components/feedback-states.test.tsx +314 -0
  434. package/src/components/tone-figures.test.tsx +58 -0
  435. package/src/fonts.css +19 -0
  436. package/src/globals.css +9 -165
  437. package/src/hooks/index.ts +6 -3
  438. package/src/hooks/tool-result-recovery-model.test.ts +744 -0
  439. package/src/hooks/tool-result-recovery-model.ts +418 -0
  440. package/src/hooks/use-tool-query.effects.test.tsx +205 -0
  441. package/src/hooks/use-tool-query.ts +156 -33
  442. package/src/hooks/use-tool-result-recovery.effects.test.tsx +141 -0
  443. package/src/hooks/use-tool-result-recovery.test.ts +28 -5
  444. package/src/hooks/use-tool-result-recovery.ts +101 -131
  445. package/src/hooks/use-view-data.test.tsx +169 -0
  446. package/src/hooks/use-view-data.ts +131 -10
  447. package/src/hooks/use-view-tool-query.ts +25 -19
  448. package/src/i18n-coverage.test.tsx +425 -0
  449. package/src/index.ts +102 -13
  450. package/src/lib/focus-return.test.ts +288 -0
  451. package/src/lib/focus-return.ts +96 -0
  452. package/src/lib/focus-ring.ts +20 -0
  453. package/src/lib/icon-stroke.test.ts +59 -0
  454. package/src/lib/icon-stroke.ts +13 -0
  455. package/src/lib/parse-tool-result.test.ts +46 -1
  456. package/src/lib/parse-tool-result.ts +40 -2
  457. package/src/lib/toast-model.test.ts +233 -0
  458. package/src/lib/toast-model.ts +165 -0
  459. package/src/lib/tone-utils.test.ts +125 -16
  460. package/src/lib/tone-utils.ts +144 -15
  461. package/src/lib/tool-query.test.ts +569 -0
  462. package/src/lib/tool-query.ts +253 -0
  463. package/src/lib/tool-result-observer.test.ts +296 -0
  464. package/src/lib/tool-result-observer.ts +211 -0
  465. package/src/lib/typography.test.ts +45 -0
  466. package/src/lib/typography.ts +47 -0
  467. package/src/lib/utils.ts +12 -1
  468. package/src/locale/index.ts +14 -1
  469. package/src/locale/locale-provider.test.tsx +87 -0
  470. package/src/locale/locale-provider.tsx +90 -3
  471. package/src/locale/message-types.ts +337 -0
  472. package/src/locale/messages-de.ts +198 -0
  473. package/src/locale/messages-en.ts +196 -0
  474. package/src/locale/toolkit-messages.test.ts +349 -0
  475. package/src/locale/toolkit-messages.ts +143 -0
  476. package/src/no-hardcoded-copy.test.ts +432 -0
  477. package/src/primitives/alert.tsx +7 -2
  478. package/src/primitives/badge.tsx +19 -7
  479. package/src/primitives/button.tsx +13 -8
  480. package/src/primitives/card.tsx +4 -4
  481. package/src/primitives/dialog.tsx +42 -8
  482. package/src/primitives/input.tsx +5 -3
  483. package/src/primitives/scroll-area.tsx +1 -1
  484. package/src/primitives/select.tsx +7 -4
  485. package/src/primitives/sheet.tsx +33 -6
  486. package/src/primitives/skeleton.tsx +1 -1
  487. package/src/primitives/switch.tsx +4 -2
  488. package/src/primitives/tabs.tsx +9 -6
  489. package/src/providers/query-provider.test.tsx +154 -0
  490. package/src/providers/query-provider.tsx +66 -5
  491. package/src/render-cases-primitives.tsx +1 -1
  492. package/src/render-cases.tsx +82 -6
  493. package/src/render-effects.tsx +96 -0
  494. package/src/render-scan.ts +75 -0
  495. package/src/theme/cd-tokens.generated.css +48 -0
  496. package/src/theme/cd-tokens.test.ts +109 -0
  497. package/src/theme/cd-tokens.ts +67 -0
  498. package/src/theme/create-theme.test.ts +253 -5
  499. package/src/theme/create-theme.ts +341 -41
  500. package/src/theme/fonts.test.ts +85 -0
  501. package/src/theme/fonts.ts +72 -0
  502. package/src/theme/host-fonts.test.ts +135 -0
  503. package/src/theme/host-fonts.ts +52 -0
  504. package/src/theme/index.ts +15 -1
  505. package/src/theme/portal-container.ts +20 -0
  506. package/src/theme/presets.ts +44 -11
  507. package/src/theme/theme-css.test.ts +619 -0
  508. package/src/theme/theme-provider.test.tsx +82 -0
  509. package/src/theme/theme-provider.tsx +60 -24
  510. package/src/theme.css +344 -0
  511. package/ui-catalog.json +783 -49
@@ -1,133 +1,371 @@
1
- import type { ReactNode } from "react"
2
- import { TONE_TEXT, type ToneVariant } from "../lib/tone-utils.js"
1
+ import { useId, type ReactNode } from "react"
2
+ import { ChevronRight, Minus, TrendingDown, TrendingUp, type LucideIcon } from "lucide-react"
3
+ import { FOCUS_RING_INSET, FOCUS_RING } from "../lib/focus-ring.js"
4
+ import {
5
+ resolveTone,
6
+ TONE_BORDER,
7
+ TONE_DOT,
8
+ TONE_ICON,
9
+ TONE_TINT,
10
+ type ToneName,
11
+ type ToneVariant,
12
+ } from "../lib/tone-utils.js"
13
+ import { MICRO_LABEL } from "../lib/typography.js"
14
+ import { cn } from "../lib/utils.js"
15
+ import { Skeleton } from "../primitives/skeleton.js"
3
16
 
4
- /** Trend direction → tone for the small trend line under a value. */
5
- const TREND_TONE: Record<"up" | "down" | "flat", string> = {
6
- up: "text-danger",
7
- down: "text-success",
8
- flat: "text-muted-foreground",
17
+ type TrendDirection = "up" | "down" | "flat"
18
+
19
+ /**
20
+ * Direction → tone when no `trendTone` is set (incident-biased: up reads as
21
+ * worse). The tone shows ONLY as the direction icon's colour or a dot; the
22
+ * trend words stay neutral (no coloured digits, CI §3.3).
23
+ */
24
+ const TREND_TONE: Record<TrendDirection, ToneVariant> = {
25
+ up: "danger",
26
+ down: "success",
27
+ flat: "neutral",
28
+ }
29
+
30
+ const TREND_ICON: Record<TrendDirection, LucideIcon> = {
31
+ up: TrendingUp,
32
+ down: TrendingDown,
33
+ flat: Minus,
9
34
  }
10
35
 
11
36
  export interface KpiCell {
12
- /** Caption shown above the value. */
37
+ /** Caption shown above the value. Part of the cell's accessible name. */
13
38
  label: ReactNode
14
39
  /** The headline figure. Pre-format numbers for locale/currency. */
15
40
  value: ReactNode
16
41
  /** Small fraction shown next to the value, e.g. ` /14`. */
17
42
  fraction?: ReactNode
18
- /** Optional trend line under the value (e.g. `"+12% vs last week"`). */
43
+ /** Optional trend line under the value (e.g. `"+12% vs last week"`); its words stay neutral. */
19
44
  trend?: ReactNode
20
- /** Colours the trend line. `up` reads as bad (danger), `down` as good (success). */
21
- trendDirection?: "up" | "down" | "flat"
22
- /** Tone applied to the value text (e.g. `"danger"` for a breached metric). */
23
- tone?: ToneVariant
45
+ /**
46
+ * Draws a Lucide direction icon (TrendingUp / TrendingDown / Minus) before
47
+ * the trend line, in the trend's tone: `up` reads as bad (danger), `down`
48
+ * as good (success) unless {@link KpiCell.trendTone} says otherwise.
49
+ */
50
+ trendDirection?: TrendDirection
51
+ /**
52
+ * Explicit tone of the trend — overrides the direction's default. Shown as
53
+ * the direction icon's colour, or as a dot when there is no direction.
54
+ */
55
+ trendTone?: ToneName
56
+ /**
57
+ * The state the figure stands for (e.g. `"danger"` for a breached metric),
58
+ * shown as a dot next to the label (strip) or as the card's tint and edge
59
+ * (`variant="soft"`). The figure itself stays in the foreground colour.
60
+ */
61
+ tone?: ToneName
24
62
  /** When set, the cell renders as a button — turns a metric into a nav entry point. */
25
63
  onClick?: () => void
26
- /** Accessible label for the clickable cell (recommended when `onClick` is set). */
64
+ /**
65
+ * What activating the cell does ("Opens the incidents dashboard"). Read as
66
+ * the button's DESCRIPTION (`aria-describedby`), so its accessible name
67
+ * stays the visible label and value (WCAG 2.5.3, label in name).
68
+ */
69
+ actionHint?: string
70
+ /**
71
+ * @deprecated Replaces the accessible name with text a voice-control user
72
+ * cannot see (WCAG 2.5.3). Say what the click does in
73
+ * {@link KpiCell.actionHint}; the visible label and value name the cell.
74
+ */
27
75
  ariaLabel?: string
28
76
  }
29
77
 
78
+ /**
79
+ * Columns per cell count. 4+ cells fall back to 2 columns in narrow frames
80
+ * (inline chat views are often under 500 px, where five cells side by side
81
+ * leave ~40 px each); 6 steps through 3.
82
+ */
30
83
  const COL_CLASS: Record<number, string> = {
31
84
  1: "grid-cols-1",
32
85
  2: "grid-cols-2",
33
86
  3: "grid-cols-3",
34
- 4: "grid-cols-4",
35
- 5: "grid-cols-5",
36
- 6: "grid-cols-6",
87
+ 4: "grid-cols-2 sm:grid-cols-4",
88
+ 5: "grid-cols-2 sm:grid-cols-5",
89
+ 6: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-6",
90
+ }
91
+
92
+ /** The responsive column classes for `count` cells (clamped to 1–6). */
93
+ export function kpiColumns(count: number): string {
94
+ const cols = Math.min(Math.max(Math.trunc(count) || 1, 1), 6)
95
+ return COL_CLASS[cols] ?? "grid-cols-1"
37
96
  }
38
97
 
39
98
  export interface KpiGridHeader {
40
- /** Group label, rendered uppercase in the strip header. */
99
+ /** Group label of the strip header (13 px semibold, normal case). */
41
100
  label: ReactNode
42
101
  /** Optional muted badge to the right of the label. */
43
102
  badge?: ReactNode
44
103
  }
45
104
 
46
105
  export interface KpiGridProps {
47
- /** The metric cells, flowed as columns (1–6, clamped). */
106
+ /** The metric cells, flowed as columns (1–6; more wrap onto further rows). */
48
107
  cells: KpiCell[]
49
108
  /** Wrap the strip in a bordered card with an optional group `header`. Default `false`. */
50
109
  boxed?: boolean
51
- /** Group header shown above the strip when `boxed`. */
110
+ /** Group header above the strip (strip variant), usually with `boxed`. */
52
111
  header?: KpiGridHeader
112
+ /**
113
+ * `"strip"` (default): one bordered strip, the tone as a dot next to the
114
+ * label. `"soft"`: one card per cell in its tone's tint and edge (`boxed`
115
+ * and `header` don't apply).
116
+ */
117
+ variant?: "strip" | "soft"
118
+ /** Names the group of metrics (`role="group"`); omit when a heading right above names it. */
119
+ ariaLabel?: string
120
+ className?: string
121
+ }
122
+
123
+ /** A small status dot; decorative — the label next to it carries the meaning. */
124
+ function ToneDot({ tone, className }: { tone: ToneVariant; className: string }) {
125
+ return (
126
+ <span
127
+ aria-hidden="true"
128
+ data-tone={tone}
129
+ className={cn("shrink-0 rounded-full", className, TONE_DOT[tone])}
130
+ />
131
+ )
53
132
  }
54
133
 
55
134
  /**
56
- * Bordered strip of KPI cells — the canonical way to show a row of headline
57
- * metrics in a widget (replaces the single-metric KPICard). Cells flow as
58
- * columns (1–6) and can be made clickable to drill into a detail view. Use
59
- * `boxed` + `header` for a labelled metric block; unboxed (default) renders the
60
- * lighter `border-y` strip.
135
+ * The trend line: neutral words (the foreground next to a state, quiet
136
+ * without one) and the state beside them as a direction icon or a dot in the
137
+ * tone — never as coloured digits.
61
138
  */
62
- export function KpiGrid({ cells, boxed = false, header }: KpiGridProps) {
63
- const cols = Math.min(Math.max(cells.length, 1), 6)
64
- const colClass = COL_CLASS[cols] ?? "grid-cols-4"
65
- const wrapperClass = boxed ? "border-border overflow-hidden rounded-lg border" : ""
66
- const stripClass = boxed ? "" : "border-border border-y"
139
+ function TrendLine({ cell }: { cell: KpiCell }) {
140
+ const direction = cell.trendDirection
141
+ const tone =
142
+ cell.trendTone !== undefined
143
+ ? resolveTone(cell.trendTone)
144
+ : direction
145
+ ? TREND_TONE[direction]
146
+ : "neutral"
147
+ const marked = tone !== "neutral"
148
+ const Icon = direction ? TREND_ICON[direction] : undefined
67
149
  return (
68
- <div className={wrapperClass}>
69
- {header && (
70
- <div className="border-border bg-muted text-muted-foreground flex items-center gap-2 border-b px-4 py-2 text-[11px] font-semibold tracking-wide uppercase">
71
- <span>{header.label}</span>
72
- {header.badge && (
73
- <span className="border-border bg-card text-muted-foreground rounded border px-1.5 py-0 text-[10px] font-medium normal-case">
74
- {header.badge}
75
- </span>
76
- )}
77
- </div>
150
+ <div data-slot="kpi-trend" className="text-small mt-1.5 flex items-center gap-1.5">
151
+ {Icon ? (
152
+ <Icon aria-hidden="true" className={cn("size-4 shrink-0", TONE_ICON[tone])} />
153
+ ) : (
154
+ marked && <ToneDot tone={tone} className="size-1.5" />
155
+ )}
156
+ <span className={marked ? "text-foreground" : "text-muted-foreground"}>{cell.trend}</span>
157
+ </div>
158
+ )
159
+ }
160
+
161
+ /** Label, figure, fraction and trend — shared by the strip and soft variants. */
162
+ function KpiCellBody({ cell, variant }: { cell: KpiCell; variant: "strip" | "soft" }) {
163
+ const tone = resolveTone(cell.tone)
164
+ return (
165
+ <>
166
+ <div
167
+ className={cn(
168
+ "text-small flex items-center justify-between gap-2 font-medium",
169
+ variant === "strip" ? "text-muted-foreground" : "text-foreground",
170
+ )}
171
+ >
172
+ <span className="inline-flex min-w-0 items-center gap-1.5">
173
+ {variant === "strip" && tone !== "neutral" && <ToneDot tone={tone} className="size-2" />}
174
+ <span className="min-w-0 break-words">{cell.label}</span>
175
+ </span>
176
+ {cell.onClick && <ChevronRight aria-hidden="true" className="size-4 shrink-0" />}
177
+ </div>
178
+ <div className="text-title text-foreground mt-1.5 leading-none tracking-tight break-words tabular-nums">
179
+ {cell.value}
180
+ {cell.fraction && (
181
+ <span className="text-dense text-muted-foreground ml-0.5 font-normal">
182
+ {cell.fraction}
183
+ </span>
184
+ )}
185
+ </div>
186
+ {cell.trend && <TrendLine cell={cell} />}
187
+ </>
188
+ )
189
+ }
190
+
191
+ /** One cell: a button when clickable (named by its visible text), else a plain box. */
192
+ function KpiCellView({
193
+ cell,
194
+ variant,
195
+ className,
196
+ }: {
197
+ cell: KpiCell
198
+ variant: "strip" | "soft"
199
+ className: string
200
+ }) {
201
+ const hintId = useId()
202
+ const body = <KpiCellBody cell={cell} variant={variant} />
203
+ const tone = cell.tone === undefined ? undefined : resolveTone(cell.tone)
204
+ if (!cell.onClick) {
205
+ return (
206
+ <div data-tone={tone} className={className}>
207
+ {body}
208
+ </div>
209
+ )
210
+ }
211
+ return (
212
+ <button
213
+ type="button"
214
+ onClick={cell.onClick}
215
+ aria-label={cell.ariaLabel}
216
+ aria-describedby={cell.actionHint ? hintId : undefined}
217
+ data-tone={tone}
218
+ className={cn(
219
+ "cursor-pointer text-left transition-colors",
220
+ variant === "strip" ? cn("hover:bg-muted", FOCUS_RING_INSET) : FOCUS_RING,
221
+ className,
222
+ )}
223
+ >
224
+ {body}
225
+ {/* Referenced by aria-describedby; `hidden` keeps it out of the name and the page. */}
226
+ {cell.actionHint && (
227
+ <span id={hintId} hidden>
228
+ {cell.actionHint}
229
+ </span>
230
+ )}
231
+ </button>
232
+ )
233
+ }
234
+
235
+ /** A soft card: the tone's tint and edge around neutral text. */
236
+ function softCardClass(tone: ToneName | undefined): string {
237
+ const t = resolveTone(tone)
238
+ return cn("text-foreground rounded-lg border p-4", TONE_TINT[t], TONE_BORDER[t])
239
+ }
240
+
241
+ /**
242
+ * The strip's dividers. Each cell draws its own right and bottom rule, and the
243
+ * grid reaches 1 px past its clipping frame, so the rules on the outer edges
244
+ * fall away at any column count. An empty track in a short last row (5 cells
245
+ * in 2 columns, 7 in 6) keeps the card colour: a `gap-px` grid on a
246
+ * `bg-border` container would paint it as a solid block of the divider colour.
247
+ */
248
+ const STRIP_FRAME = "bg-card overflow-hidden"
249
+ const STRIP_GRID = "-mr-px -mb-px grid"
250
+ const STRIP_CELL = "border-border min-w-0 border-r border-b px-5 py-4"
251
+
252
+ function KpiStripHeader({ header }: { header: KpiGridHeader }) {
253
+ return (
254
+ <div
255
+ className={cn(
256
+ "border-border bg-muted text-muted-foreground flex items-center gap-2 border-b px-4 py-2",
257
+ MICRO_LABEL,
78
258
  )}
79
- <div className={`grid ${colClass} ${stripClass}`}>
80
- {cells.map((cell, idx) => {
81
- const borderClass = idx < cells.length - 1 ? "border-border border-r" : ""
82
- const content = (
83
- <>
84
- <div className="text-muted-foreground flex items-center justify-between gap-2 text-xs font-medium">
85
- <span>{cell.label}</span>
86
- {cell.onClick && (
87
- <span aria-hidden="true" className="text-sm leading-none">
88
- ›
89
- </span>
90
- )}
91
- </div>
92
- <div
93
- className={`mt-1.5 text-2xl leading-none font-bold tracking-tight tabular-nums ${
94
- (cell.tone && TONE_TEXT[cell.tone]) || "text-foreground"
95
- }`}
96
- >
97
- {cell.value}
98
- {cell.fraction && (
99
- <span className="text-muted-foreground ml-0.5 text-sm font-normal">
100
- {cell.fraction}
101
- </span>
102
- )}
103
- </div>
104
- {cell.trend && (
105
- <div
106
- className={`mt-1.5 text-xs ${
107
- cell.trendDirection ? TREND_TONE[cell.trendDirection] : "text-muted-foreground"
108
- }`}
109
- >
110
- {cell.trend}
111
- </div>
112
- )}
113
- </>
114
- )
115
- return cell.onClick ? (
116
- <button
117
- key={idx}
118
- type="button"
119
- onClick={cell.onClick}
120
- aria-label={cell.ariaLabel}
121
- className={`hover:bg-muted focus-visible:ring-ring cursor-pointer px-5 py-4 text-left transition-colors outline-none focus-visible:ring-2 focus-visible:ring-inset ${borderClass}`}
122
- >
123
- {content}
124
- </button>
125
- ) : (
126
- <div key={idx} className={`px-5 py-4 ${borderClass}`}>
127
- {content}
259
+ >
260
+ <span>{header.label}</span>
261
+ {header.badge && (
262
+ <span className="border-border bg-card text-muted-foreground text-small rounded-sm border px-1.5 font-medium">
263
+ {header.badge}
264
+ </span>
265
+ )}
266
+ </div>
267
+ )
268
+ }
269
+
270
+ /**
271
+ * A row of headline metrics — the canonical way to show KPIs in a widget
272
+ * (replaces the single-metric KPICard). Cells flow as columns (1–6), falling
273
+ * back to 2 columns in narrow frames. A cell's `tone` shows as a dot next to
274
+ * its label (or as a soft card's tint and edge); figures stay in the
275
+ * foreground colour. A clickable cell is a button whose accessible name is
276
+ * its visible label and value; `actionHint` describes what the click does.
277
+ */
278
+ export function KpiGrid({
279
+ cells,
280
+ boxed = false,
281
+ header,
282
+ variant = "strip",
283
+ ariaLabel,
284
+ className,
285
+ }: KpiGridProps) {
286
+ const colClass = kpiColumns(cells.length)
287
+ const group = ariaLabel ? { role: "group" as const, "aria-label": ariaLabel } : {}
288
+ if (variant === "soft") {
289
+ return (
290
+ <div {...group} data-slot="kpi-grid" className={cn("grid gap-4", colClass, className)}>
291
+ {cells.map((cell, idx) => (
292
+ <KpiCellView
293
+ key={idx}
294
+ cell={cell}
295
+ variant="soft"
296
+ className={cn("min-w-0", softCardClass(cell.tone))}
297
+ />
298
+ ))}
299
+ </div>
300
+ )
301
+ }
302
+ return (
303
+ <div
304
+ {...group}
305
+ data-slot="kpi-grid"
306
+ className={cn(boxed && "border-border overflow-hidden rounded-lg border", className)}
307
+ >
308
+ {header && <KpiStripHeader header={header} />}
309
+ <div data-slot="kpi-strip" className={cn(STRIP_FRAME, !boxed && "border-border border-y")}>
310
+ <div className={cn(STRIP_GRID, colClass)}>
311
+ {cells.map((cell, idx) => (
312
+ <KpiCellView key={idx} cell={cell} variant="strip" className={STRIP_CELL} />
313
+ ))}
314
+ </div>
315
+ </div>
316
+ </div>
317
+ )
318
+ }
319
+
320
+ export interface KpiGridSkeletonProps {
321
+ /** How many cells the loaded grid will have. */
322
+ cells: number
323
+ /** The loaded grid's variant. Default `"strip"`. */
324
+ variant?: "strip" | "soft"
325
+ /** The loaded grid's `boxed`. Default `false`. */
326
+ boxed?: boolean
327
+ className?: string
328
+ }
329
+
330
+ /**
331
+ * Loading placeholder with {@link KpiGrid}'s geometry (same wrapper, grid and
332
+ * cell padding in both variants), so the layout doesn't jump when the data
333
+ * arrives. `aria-busy`; pair it with a status line if the wait is long.
334
+ */
335
+ export function KpiGridSkeleton({
336
+ cells,
337
+ variant = "strip",
338
+ boxed = false,
339
+ className,
340
+ }: KpiGridSkeletonProps) {
341
+ const colClass = kpiColumns(cells)
342
+ const count = Math.max(Math.trunc(cells) || 0, 0)
343
+ if (variant === "soft") {
344
+ return (
345
+ <div aria-busy="true" className={cn("grid gap-4", colClass, className)}>
346
+ {Array.from({ length: count }, (_, idx) => (
347
+ <div key={idx} className="bg-muted border-border rounded-lg border p-4">
348
+ <Skeleton className="h-4 w-20" />
349
+ <Skeleton className="mt-2 h-7 w-14" />
350
+ </div>
351
+ ))}
352
+ </div>
353
+ )
354
+ }
355
+ return (
356
+ <div
357
+ aria-busy="true"
358
+ className={cn(boxed && "border-border overflow-hidden rounded-lg border", className)}
359
+ >
360
+ <div data-slot="kpi-strip" className={cn(STRIP_FRAME, !boxed && "border-border border-y")}>
361
+ <div className={cn(STRIP_GRID, colClass)}>
362
+ {Array.from({ length: count }, (_, idx) => (
363
+ <div key={idx} className={STRIP_CELL}>
364
+ <Skeleton className="h-4 w-16" />
365
+ <Skeleton className="mt-2.5 h-7 w-14" />
128
366
  </div>
129
- )
130
- })}
367
+ ))}
368
+ </div>
131
369
  </div>
132
370
  </div>
133
371
  )
@@ -0,0 +1,116 @@
1
+ import { renderToStaticMarkup } from "react-dom/server"
2
+ import { describe, expect, it } from "vitest"
3
+ import { LocaleProvider } from "../locale/locale-provider.js"
4
+ import { attribute, openingTags } from "../render-scan.js"
5
+ import { FOCUS_RING } from "../lib/focus-ring.js"
6
+ import { ListFooter, type ListFooterProps } from "./ListFooter.js"
7
+
8
+ const noop = (): void => {}
9
+
10
+ function render(props: Partial<ListFooterProps> = {}, locale = "en") {
11
+ return renderToStaticMarkup(
12
+ <LocaleProvider locale={locale}>
13
+ <ListFooter shown={8} total={42} hasMore onLoadMore={noop} noun="tasks" {...props} />
14
+ </LocaleProvider>,
15
+ )
16
+ }
17
+
18
+ /** The text of the live region (the one `role="status"` element). */
19
+ function statusText(html: string): string | undefined {
20
+ const match = /<p role="status"[^>]*>([\s\S]*?)<\/p>/.exec(html)
21
+ return match?.[1]
22
+ ?.replace(/<[^>]*>/g, " ")
23
+ .replace(/\s+/g, " ")
24
+ .trim()
25
+ }
26
+
27
+ /** The class of the footer's root (`sr-only` while nothing is visible). */
28
+ function footerClass(html: string): string | undefined {
29
+ return attribute(openingTags(html, "div")[0] ?? "", "class")
30
+ }
31
+
32
+ describe("ListFooter: an honest count in a polite live region", () => {
33
+ it("puts the count in ONE status region and Load more outside it", () => {
34
+ const html = render()
35
+ expect(html.match(/role="status"/g)).toHaveLength(1)
36
+ expect(statusText(html)).toBe("Showing 8 of 42 tasks")
37
+ const [button] = openingTags(html, "button")
38
+ expect(attribute(button ?? "", "type")).toBe("button")
39
+ expect(attribute(button ?? "", "class")).toContain(FOCUS_RING)
40
+ expect(html.indexOf("<button")).toBeGreaterThan(html.indexOf("</p>"))
41
+ expect(html).toContain("text-small")
42
+ expect(html).not.toContain("text-xs")
43
+ })
44
+
45
+ it("says Updating… next to the count while a same-filter refetch runs", () => {
46
+ expect(statusText(render({ refreshing: true }))).toBe("Showing 8 of 42 tasks Updating…")
47
+ expect(statusText(render({ refreshing: true }, "de"))).toBe(
48
+ "Zeigt 8 von 42 tasks Wird aktualisiert…",
49
+ )
50
+ })
51
+
52
+ it("hides the count and Load more while the rows are the previous result", () => {
53
+ const inFlight = render({ stale: true, refreshing: true })
54
+ expect(statusText(inFlight)).toBe("Updating…")
55
+ expect(inFlight).not.toContain("<button")
56
+ // A failed search: the caller's alert reports it, the region stays, empty.
57
+ const failed = render({ stale: true })
58
+ expect(footerClass(failed)).toBe("sr-only")
59
+ expect(statusText(failed)).toBe("")
60
+ expect(failed).not.toContain("<button")
61
+ })
62
+
63
+ it("shows nothing at zero rows, but keeps the live region and says so to screen readers", () => {
64
+ const empty = render({ total: 0, hasMore: false })
65
+ expect(footerClass(empty)).toBe("sr-only")
66
+ expect(statusText(empty)).toBe("No entries")
67
+ expect(statusText(render({ total: 0, hasMore: false }, "de"))).toBe("Keine Einträge")
68
+ expect(empty).not.toContain("<button")
69
+ expect(statusText(render({ total: 0, refreshing: true }))).toBe("Updating…")
70
+ expect(footerClass(render({ total: 0, refreshing: true }))).toContain("flex")
71
+ })
72
+
73
+ it("keeps the SAME live region from a count to zero rows, so a search that finds nothing is announced", () => {
74
+ // React keeps an element whose type and position stay put; a region that
75
+ // unmounts and comes back with its news already in it is not announced.
76
+ const path = (html: string) =>
77
+ [...html.matchAll(/<(div|p)\b[^>]*?(data-slot="[^"]*")/g)].map((m) => `${m[1]}[${m[2]}]`)
78
+ const states = [
79
+ render(),
80
+ render({ refreshing: true }),
81
+ render({ total: 0, hasMore: false }),
82
+ render({ stale: true }),
83
+ ]
84
+ for (const html of states) {
85
+ expect(path(html)).toEqual([
86
+ 'div[data-slot="list-footer"]',
87
+ 'p[data-slot="list-footer-status"]',
88
+ ])
89
+ expect(html.match(/role="status"/g)).toHaveLength(1)
90
+ }
91
+ expect(states.map(statusText)).toEqual([
92
+ "Showing 8 of 42 tasks",
93
+ "Showing 8 of 42 tasks Updating…",
94
+ "No entries",
95
+ "",
96
+ ])
97
+ })
98
+
99
+ it("leaves the announcing to a caller that owns a live region (announce={false})", () => {
100
+ const html = render({ announce: false, refreshing: true })
101
+ expect(html).not.toContain('role="status"')
102
+ expect(html).toMatch(/<p data-slot="list-footer-status"[^>]*>.*Showing 8 of 42 tasks/)
103
+ expect(html).toContain("Updating…")
104
+ // Nothing visible and no region to keep: nothing at all.
105
+ expect(render({ announce: false, total: 0 })).toBe("")
106
+ expect(render({ announce: false, stale: true })).toBe("")
107
+ })
108
+
109
+ it("disables Load more while a page is in flight, and takes label overrides", () => {
110
+ const html = render({ loadingMore: true, labels: { loadingMore: "Fetching…" } })
111
+ const [button] = openingTags(html, "button")
112
+ expect(attribute(button ?? "", "disabled")).toBe("")
113
+ expect(html).toContain("Fetching…</button>")
114
+ expect(render({ hasMore: false })).not.toContain("<button")
115
+ })
116
+ })