@miragon/mcp-toolkit-ui 2.7.0 → 2.8.1

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 (388) 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 +14 -6
  122. package/dist/components/DrillButton.d.ts.map +1 -1
  123. package/dist/components/DrillButton.js +14 -7
  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.test.tsx +48 -0
  319. package/src/components/DrillButton.tsx +26 -11
  320. package/src/components/EmptyState.tsx +6 -4
  321. package/src/components/ErrorState.tsx +31 -9
  322. package/src/components/FilterBar.test.tsx +82 -0
  323. package/src/components/FilterBar.tsx +52 -26
  324. package/src/components/KpiGrid.test.tsx +279 -0
  325. package/src/components/KpiGrid.tsx +297 -90
  326. package/src/components/ListFooter.test.tsx +116 -0
  327. package/src/components/ListFooter.tsx +102 -15
  328. package/src/components/PagedListFooter.test.tsx +140 -0
  329. package/src/components/PagedListFooter.tsx +107 -0
  330. package/src/components/Pills.test.tsx +77 -0
  331. package/src/components/Pills.tsx +63 -10
  332. package/src/components/SectionHeading.tsx +9 -3
  333. package/src/components/Toast.tsx +37 -19
  334. package/src/components/WidgetHeader.test.tsx +78 -0
  335. package/src/components/WidgetHeader.tsx +63 -16
  336. package/src/components/feedback-states.test.tsx +98 -9
  337. package/src/components/tone-figures.test.tsx +8 -4
  338. package/src/hooks/index.ts +6 -3
  339. package/src/hooks/tool-result-recovery-model.test.ts +744 -0
  340. package/src/hooks/tool-result-recovery-model.ts +418 -0
  341. package/src/hooks/use-tool-query.effects.test.tsx +205 -0
  342. package/src/hooks/use-tool-query.ts +156 -33
  343. package/src/hooks/use-tool-result-recovery.effects.test.tsx +141 -0
  344. package/src/hooks/use-tool-result-recovery.test.ts +3 -4
  345. package/src/hooks/use-tool-result-recovery.ts +84 -142
  346. package/src/hooks/use-view-data.test.tsx +118 -9
  347. package/src/hooks/use-view-data.ts +128 -18
  348. package/src/hooks/use-view-tool-query.ts +25 -19
  349. package/src/i18n-coverage.test.tsx +104 -2
  350. package/src/index.ts +48 -11
  351. package/src/lib/focus-return.test.ts +288 -0
  352. package/src/lib/focus-return.ts +96 -0
  353. package/src/lib/parse-tool-result.test.ts +46 -1
  354. package/src/lib/parse-tool-result.ts +40 -2
  355. package/src/lib/toast-model.test.ts +166 -1
  356. package/src/lib/toast-model.ts +104 -2
  357. package/src/lib/tone-utils.test.ts +86 -10
  358. package/src/lib/tone-utils.ts +89 -8
  359. package/src/lib/tool-query.test.ts +569 -0
  360. package/src/lib/tool-query.ts +253 -0
  361. package/src/lib/tool-result-observer.test.ts +296 -0
  362. package/src/lib/tool-result-observer.ts +211 -0
  363. package/src/lib/typography.test.ts +45 -0
  364. package/src/lib/typography.ts +47 -0
  365. package/src/lib/utils.ts +12 -1
  366. package/src/locale/index.ts +1 -0
  367. package/src/locale/message-types.ts +118 -2
  368. package/src/locale/messages-de.ts +49 -10
  369. package/src/locale/messages-en.ts +49 -10
  370. package/src/locale/toolkit-messages.test.ts +133 -0
  371. package/src/locale/toolkit-messages.ts +63 -2
  372. package/src/primitives/alert.tsx +3 -0
  373. package/src/primitives/badge.tsx +9 -4
  374. package/src/primitives/button.tsx +1 -1
  375. package/src/primitives/dialog.tsx +10 -1
  376. package/src/primitives/select.tsx +1 -1
  377. package/src/primitives/sheet.tsx +7 -0
  378. package/src/providers/query-provider.test.tsx +154 -0
  379. package/src/providers/query-provider.tsx +66 -5
  380. package/src/render-cases-primitives.tsx +1 -1
  381. package/src/render-cases.tsx +38 -6
  382. package/src/render-effects.tsx +96 -0
  383. package/src/theme/cd-tokens.test.ts +3 -1
  384. package/src/theme/create-theme.test.ts +10 -0
  385. package/src/theme/create-theme.ts +28 -0
  386. package/src/theme/theme-css.test.ts +51 -0
  387. package/src/theme.css +31 -1
  388. package/ui-catalog.json +448 -41
@@ -0,0 +1,82 @@
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 } from "../render-scan.js"
6
+ import { FOCUS_RING } from "../lib/focus-ring.js"
7
+ import { TONE_BORDER, TONE_SOFT } from "../lib/tone-utils.js"
8
+ import { FilterBar } from "./FilterBar.js"
9
+
10
+ const noop = (): void => {}
11
+ const classOf = (tag: string | undefined) => attribute(tag ?? "", "class") ?? ""
12
+
13
+ function render(props: Partial<Parameters<typeof FilterBar>[0]> = {}, locale = "en") {
14
+ return renderToStaticMarkup(
15
+ <LocaleProvider locale={locale}>
16
+ <FilterBar
17
+ search=""
18
+ onSearchChange={noop}
19
+ chips={[
20
+ { id: "open", label: "Open", count: 3, active: true },
21
+ { id: "done", label: "Done" },
22
+ ]}
23
+ onChipToggle={noop}
24
+ {...props}
25
+ />
26
+ </LocaleProvider>,
27
+ )
28
+ }
29
+
30
+ describe("FilterBar: a labelled search and toggle chips", () => {
31
+ it("labels the search input in the locale (a placeholder is no label)", () => {
32
+ const [input] = openingTags(render(), "input")
33
+ expect(attribute(input ?? "", "type")).toBe("search")
34
+ expect(attribute(input ?? "", "aria-label")).toBe(TOOLKIT_MESSAGES.en.filterBar.searchLabel)
35
+ expect(attribute(input ?? "", "placeholder")).toBe("Filter…")
36
+ const [german] = openingTags(render({}, "de-DE"), "input")
37
+ expect(attribute(german ?? "", "aria-label")).toBe("Filtern")
38
+ expect(attribute(german ?? "", "placeholder")).toBe("Filtern…")
39
+ })
40
+
41
+ it("takes the caller's search label and placeholder", () => {
42
+ const [input] = openingTags(
43
+ render({ searchAriaLabel: "Filter tasks", searchPlaceholder: "Filter tasks…" }),
44
+ "input",
45
+ )
46
+ expect(attribute(input ?? "", "aria-label")).toBe("Filter tasks")
47
+ expect(attribute(input ?? "", "placeholder")).toBe("Filter tasks…")
48
+ })
49
+
50
+ it("groups the chips under a name and marks each one's state with aria-pressed", () => {
51
+ const html = render()
52
+ const group = openingTags(html, "div").find((t) => attribute(t, "role") === "group")
53
+ expect(attribute(group ?? "", "aria-label")).toBe("Filters")
54
+ expect(
55
+ attribute(openingTags(render({ chipsAriaLabel: "Priority" }), "div")[1] ?? "", "aria-label"),
56
+ ).toBe("Priority")
57
+ const [open, done] = openingTags(html, "button")
58
+ expect(attribute(open ?? "", "aria-pressed")).toBe("true")
59
+ expect(attribute(done ?? "", "aria-pressed")).toBe("false")
60
+ for (const chip of [open, done]) {
61
+ expect(attribute(chip ?? "", "type")).toBe("button")
62
+ expect(classOf(chip)).toContain(FOCUS_RING)
63
+ expect(classOf(chip)).toContain("text-small")
64
+ }
65
+ })
66
+
67
+ it("shows the active chip as a selection: info tint and edge around foreground text", () => {
68
+ const [open, done] = openingTags(render(), "button")
69
+ expect(classOf(open)).toContain(TONE_SOFT.info)
70
+ expect(classOf(open)).toContain(TONE_BORDER.info)
71
+ expect(classOf(open)).not.toMatch(/text-info|text-accent-foreground/)
72
+ expect(classOf(done)).toContain("text-muted-foreground")
73
+ expect(classOf(done)).not.toContain("bg-info-soft")
74
+ })
75
+
76
+ it("renders no empty group without chips and merges a className", () => {
77
+ const html = render({ chips: [], className: "mb-4" })
78
+ expect(html).not.toContain('role="group"')
79
+ expect(classOf(openingTags(html, "div")[0])).toContain("mb-4")
80
+ expect(html).not.toMatch(/text-xs|text-\[\d+px\]/)
81
+ })
82
+ })
@@ -1,15 +1,17 @@
1
1
  import { FOCUS_RING } from "../lib/focus-ring.js"
2
+ import { TONE_BORDER, TONE_SOFT } from "../lib/tone-utils.js"
3
+ import { cn } from "../lib/utils.js"
2
4
  import { useToolkitMessages } from "../locale/locale-provider.js"
3
5
  import { Input } from "../primitives/input.js"
4
6
 
5
7
  export interface FilterChip {
6
8
  /** Stable id passed back to `onChipToggle`. */
7
9
  id: string
8
- /** Visible chip label. */
10
+ /** Visible chip label (its accessible name, with the count). */
9
11
  label: string
10
12
  /** Optional count rendered after the label. */
11
13
  count?: number | string
12
- /** Whether the chip is currently active. */
14
+ /** Whether the chip is currently active (`aria-pressed`). */
13
15
  active?: boolean
14
16
  }
15
17
 
@@ -18,54 +20,78 @@ export interface FilterBarProps {
18
20
  search: string
19
21
  /** Placeholder for the search input. Default: the locale's "Filter…". */
20
22
  searchPlaceholder?: string
23
+ /**
24
+ * Accessible name of the search input. Default: the locale's "Filter" — a
25
+ * placeholder is no label (it disappears while you type).
26
+ */
27
+ searchAriaLabel?: string
21
28
  /** Called with the new search text on every keystroke. */
22
29
  onSearchChange: (next: string) => void
23
30
  /** Filter chips rendered to the right of the search input. */
24
31
  chips: FilterChip[]
25
32
  /** Called with a chip id when it is toggled. */
26
33
  onChipToggle: (id: string) => void
34
+ /** Names the chip row (`role="group"`). Default: the locale's "Filters". */
35
+ chipsAriaLabel?: string
36
+ className?: string
27
37
  }
28
38
 
29
39
  /**
30
40
  * Search input + toggleable chip row for filtering a list/table of widget
31
- * items. Pair with `useDebouncedValue` to avoid re-querying on every keystroke.
41
+ * items. The input is labelled; every chip is a toggle button whose state is
42
+ * `aria-pressed`. An active chip is a selection in the CI pattern: the info
43
+ * tint and edge around foreground text. Pair with `useDebouncedValue` to
44
+ * avoid re-querying on every keystroke.
32
45
  */
33
46
  export function FilterBar({
34
47
  search,
35
48
  searchPlaceholder,
49
+ searchAriaLabel,
36
50
  onSearchChange,
37
51
  chips,
38
52
  onChipToggle,
53
+ chipsAriaLabel,
54
+ className,
39
55
  }: FilterBarProps) {
40
- const messages = useToolkitMessages()
56
+ const { filterBar } = useToolkitMessages()
41
57
  return (
42
- <div className="flex flex-wrap items-center gap-2">
58
+ <div data-slot="filter-bar" className={cn("flex flex-wrap items-center gap-2", className)}>
43
59
  <Input
44
60
  type="search"
45
61
  value={search}
46
62
  onChange={(e) => onSearchChange(e.target.value)}
47
- placeholder={searchPlaceholder ?? messages.filterBar.searchPlaceholder}
48
- className="h-9 min-w-[220px] flex-1 text-sm"
63
+ placeholder={searchPlaceholder ?? filterBar.searchPlaceholder}
64
+ aria-label={searchAriaLabel ?? filterBar.searchLabel}
65
+ className="text-dense md:text-dense h-9 min-w-[220px] flex-1"
49
66
  />
50
- <div className="flex flex-wrap gap-1.5">
51
- {chips.map((chip) => (
52
- <button
53
- type="button"
54
- key={chip.id}
55
- onClick={() => onChipToggle(chip.id)}
56
- className={`inline-flex items-center gap-1.5 rounded-sm border px-2.5 py-1 text-xs font-medium transition-colors ${FOCUS_RING} ${
57
- chip.active
58
- ? "bg-accent border-focus text-accent-foreground font-semibold"
59
- : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground"
60
- }`}
61
- >
62
- {chip.label}
63
- {chip.count !== undefined && chip.count !== "" && (
64
- <span className="font-normal tabular-nums">{chip.count}</span>
65
- )}
66
- </button>
67
- ))}
68
- </div>
67
+ {chips.length > 0 && (
68
+ <div
69
+ role="group"
70
+ aria-label={chipsAriaLabel ?? filterBar.chipsLabel}
71
+ className="flex flex-wrap gap-1.5"
72
+ >
73
+ {chips.map((chip) => (
74
+ <button
75
+ type="button"
76
+ key={chip.id}
77
+ onClick={() => onChipToggle(chip.id)}
78
+ aria-pressed={Boolean(chip.active)}
79
+ className={cn(
80
+ "text-small inline-flex items-center gap-1.5 rounded-sm border px-2.5 py-1 font-medium transition-colors",
81
+ FOCUS_RING,
82
+ chip.active
83
+ ? cn(TONE_SOFT.info, TONE_BORDER.info, "font-semibold")
84
+ : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground",
85
+ )}
86
+ >
87
+ {chip.label}
88
+ {chip.count !== undefined && chip.count !== "" && (
89
+ <span className="font-normal tabular-nums">{chip.count}</span>
90
+ )}
91
+ </button>
92
+ ))}
93
+ </div>
94
+ )}
69
95
  </div>
70
96
  )
71
97
  }
@@ -0,0 +1,279 @@
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_ICON } from "../lib/tone-utils.js"
5
+ import { KpiGrid, KpiGridSkeleton, kpiColumns } from "./KpiGrid.js"
6
+
7
+ /**
8
+ * The lifted (widget-shell) KpiGrid on its SSR markup: responsive columns,
9
+ * neutral figures with the state as a dot or a Lucide icon, and clickable
10
+ * cells whose accessible name is what you SEE (WCAG 2.5.3) while the action
11
+ * goes into the description.
12
+ */
13
+
14
+ const noop = (): void => {}
15
+ const classOf = (tag: string | undefined) => attribute(tag ?? "", "class") ?? ""
16
+
17
+ /**
18
+ * The strip's clipping frame, its grid and its cells (the grid's direct
19
+ * children: plain boxes, or buttons when clickable), read off the markup.
20
+ */
21
+ function strip(html: string) {
22
+ const start = html.indexOf('data-slot="kpi-strip"')
23
+ const tags = html.slice(start).match(/<(?:div|button)(?=[\s>])[^>]*>/g) ?? []
24
+ const [grid, ...rest] = tags
25
+ const frame = openingTags(html, "div").find((t) => /data-slot="kpi-strip"/.test(t))
26
+ return { frame, grid, cells: rest.filter((t) => /\bborder-r border-b\b/.test(t)) }
27
+ }
28
+
29
+ const kpis = (count: number) =>
30
+ Array.from({ length: count }, (_, idx) => ({ label: `KPI ${idx + 1}`, value: idx }))
31
+
32
+ describe("kpiColumns: responsive columns, clamped to 1–6", () => {
33
+ it("falls back to 2 columns in narrow frames from 4 cells on", () => {
34
+ expect(kpiColumns(1)).toBe("grid-cols-1")
35
+ expect(kpiColumns(2)).toBe("grid-cols-2")
36
+ expect(kpiColumns(3)).toBe("grid-cols-3")
37
+ expect(kpiColumns(4)).toBe("grid-cols-2 sm:grid-cols-4")
38
+ expect(kpiColumns(5)).toBe("grid-cols-2 sm:grid-cols-5")
39
+ expect(kpiColumns(6)).toBe("grid-cols-2 sm:grid-cols-3 lg:grid-cols-6")
40
+ })
41
+
42
+ it("clamps out-of-range counts instead of dropping the grid", () => {
43
+ expect(kpiColumns(0)).toBe("grid-cols-1")
44
+ expect(kpiColumns(-3)).toBe("grid-cols-1")
45
+ expect(kpiColumns(Number.NaN)).toBe("grid-cols-1")
46
+ expect(kpiColumns(9)).toBe(kpiColumns(6))
47
+ expect(kpiColumns(4.7)).toBe(kpiColumns(4))
48
+ })
49
+ })
50
+
51
+ describe("KpiGrid strip: neutral figures, the state beside them", () => {
52
+ const html = renderToStaticMarkup(
53
+ <KpiGrid
54
+ cells={[
55
+ { label: "Open incidents", value: 18, tone: "danger" },
56
+ { label: "Legacy", value: 2, tone: "critical" },
57
+ { label: "Running", value: 40, tone: "neutral" },
58
+ { label: "Plain", value: 7, fraction: "/9" },
59
+ ]}
60
+ />,
61
+ )
62
+
63
+ it("draws the tone as a decorative dot, the alias 'critical' as danger", () => {
64
+ const dots = openingTags(html, "span").filter((t) => /data-tone=/.test(t))
65
+ expect(dots.map((t) => attribute(t, "data-tone"))).toEqual(["danger", "danger"])
66
+ for (const dot of dots) {
67
+ expect(attribute(dot, "aria-hidden")).toBe("true")
68
+ expect(classOf(dot)).toContain("bg-danger")
69
+ }
70
+ })
71
+
72
+ it("keeps every figure in the foreground at the page-title size", () => {
73
+ expect(html).toMatch(/class="text-title text-foreground[^"]*tabular-nums">18</)
74
+ expect(html).not.toMatch(/text-(?:danger|warning|success|info)(?:-ink)?\b/)
75
+ })
76
+
77
+ it("flows 4 cells responsively with wrap-safe dividers", () => {
78
+ const { frame, grid, cells } = strip(html)
79
+ expect(classOf(grid)).toContain("grid-cols-2 sm:grid-cols-4")
80
+ expect(classOf(frame)).toContain("border-y")
81
+ expect(cells).toHaveLength(4)
82
+ })
83
+
84
+ it("renders no text below the 13 px floor", () => {
85
+ expect(html).not.toMatch(/text-xs|text-\[\d+px\]|uppercase/)
86
+ expect(html).toContain("text-small")
87
+ })
88
+ })
89
+
90
+ describe("KpiGrid strip: an incomplete last row keeps the card colour", () => {
91
+ // 5 cells in 2 columns (a narrow inline frame) and 7 in 6 (lg) leave empty
92
+ // tracks. Whatever shows through them is the frame's background, so the
93
+ // dividers belong on the cells and the frame is the card.
94
+ it.each([5, 7])("%i cells: dividers on every cell, nothing paints in the divider colour", (n) => {
95
+ const html = renderToStaticMarkup(<KpiGrid cells={kpis(n)} />)
96
+ const { frame, grid, cells } = strip(html)
97
+ expect(classOf(grid)).toContain(kpiColumns(n))
98
+ expect(classOf(frame)).toContain("bg-card")
99
+ // The grid reaches 1 px past the clipping frame: the outer rules fall away.
100
+ expect(classOf(frame)).toContain("overflow-hidden")
101
+ expect(classOf(grid)).toMatch(/(^| )-mr-px -mb-px grid( |$)/)
102
+ expect(cells).toHaveLength(n)
103
+ for (const cell of cells) {
104
+ expect(classOf(cell)).toContain("border-border")
105
+ expect(classOf(cell)).not.toMatch(/(^| )bg-/)
106
+ }
107
+ expect(html).not.toMatch(/\bbg-border\b|\bgap-px\b/)
108
+ })
109
+
110
+ it("clickable cells carry the same dividers under their hover surface", () => {
111
+ const html = renderToStaticMarkup(
112
+ <KpiGrid cells={kpis(5).map((cell) => ({ ...cell, onClick: noop }))} />,
113
+ )
114
+ const { cells } = strip(html)
115
+ expect(cells).toHaveLength(5)
116
+ for (const cell of cells) expect(classOf(cell)).toContain("hover:bg-muted")
117
+ })
118
+ })
119
+
120
+ describe("KpiGrid clickable cells (WCAG 2.5.3, label in name)", () => {
121
+ const html = renderToStaticMarkup(
122
+ <KpiGrid
123
+ cells={[
124
+ {
125
+ label: "Open incidents",
126
+ value: 18,
127
+ tone: "danger",
128
+ onClick: noop,
129
+ actionHint: "Opens the incidents dashboard",
130
+ },
131
+ { label: "Running", value: 40, onClick: noop },
132
+ ]}
133
+ />,
134
+ )
135
+ const [first, second] = openingTags(html, "button")
136
+
137
+ it("names a clickable cell by its visible label and value, never an aria-label", () => {
138
+ expect(attribute(first ?? "", "aria-label")).toBeUndefined()
139
+ expect(attribute(second ?? "", "aria-label")).toBeUndefined()
140
+ const button = html.slice(html.indexOf("<button"), html.indexOf("</button>"))
141
+ const visible = userFacingTexts(button.replace(/<span id="[^"]*" hidden="">[^<]*<\/span>/, ""))
142
+ expect(visible.map((t) => t.value)).toEqual(["Open incidents", "18"])
143
+ })
144
+
145
+ it("describes the action through aria-describedby on a hidden hint", () => {
146
+ const describedBy = attribute(first ?? "", "aria-describedby")
147
+ expect(describedBy).toBeTruthy()
148
+ const hint = openingTags(html, "span").find((t) => attribute(t, "id") === describedBy)
149
+ expect(hint).toBeDefined()
150
+ expect(attribute(hint ?? "", "hidden")).toBe("")
151
+ expect(html).toContain(`id="${describedBy}" hidden="">Opens the incidents dashboard</span>`)
152
+ expect(attribute(second ?? "", "aria-describedby")).toBeUndefined()
153
+ })
154
+
155
+ it("is a native button with the kit's inset focus ring and a Lucide chevron, no glyph", () => {
156
+ expect(attribute(first ?? "", "type")).toBe("button")
157
+ expect(classOf(first)).toContain("focus-visible:ring-inset")
158
+ expect(classOf(first)).toContain("outline-hidden")
159
+ expect(html).toMatch(/<svg[^>]*class="lucide lucide-chevron-right[^"]*"/)
160
+ for (const svg of openingTags(html, "svg")) expect(attribute(svg, "aria-hidden")).toBe("true")
161
+ expect(html).not.toContain("›")
162
+ })
163
+
164
+ it("still applies a legacy ariaLabel (deprecated) for existing callers", () => {
165
+ const legacy = renderToStaticMarkup(
166
+ <KpiGrid cells={[{ label: "Total", value: 19, onClick: noop, ariaLabel: "Show all" }]} />,
167
+ )
168
+ expect(attribute(openingTags(legacy, "button")[0] ?? "", "aria-label")).toBe("Show all")
169
+ })
170
+ })
171
+
172
+ describe("KpiGrid trend line: neutral words, the state as an icon or a dot", () => {
173
+ const html = renderToStaticMarkup(
174
+ <KpiGrid
175
+ cells={[
176
+ { label: "Up", value: 1, trend: "+12%", trendDirection: "up" },
177
+ { label: "Down", value: 2, trend: "-4%", trendDirection: "down" },
178
+ { label: "Override", value: 3, trend: "-3", trendDirection: "down", trendTone: "neutral" },
179
+ { label: "Dot", value: 4, trend: "+2", trendTone: "warning" },
180
+ { label: "Flat", value: 5, trend: "±0", trendDirection: "flat" },
181
+ { label: "Plain", value: 6, trend: "same" },
182
+ ]}
183
+ />,
184
+ )
185
+ const trends = html.split('data-slot="kpi-trend"').slice(1)
186
+
187
+ it("draws a Lucide direction icon in the direction's tone (up = worse)", () => {
188
+ expect(trends[0]).toMatch(/lucide-trending-up/)
189
+ expect(trends[0]).toContain(TONE_ICON.danger)
190
+ expect(trends[1]).toMatch(/lucide-trending-down/)
191
+ expect(trends[1]).toContain(TONE_ICON.success)
192
+ expect(trends[4]).toMatch(/lucide-minus/)
193
+ })
194
+
195
+ it("lets trendTone override the direction's tone, and draws a dot without a direction", () => {
196
+ expect(trends[2]).toMatch(/lucide-trending-down/)
197
+ expect(trends[2]).toContain(TONE_ICON.neutral)
198
+ expect(trends[3]).not.toContain("<svg")
199
+ expect(trends[3]).toMatch(/data-tone="warning"[^>]*class="[^"]*bg-warning/)
200
+ })
201
+
202
+ it("keeps the trend words in the foreground next to a state, quiet without one", () => {
203
+ expect(trends[0]).toMatch(/<span class="text-foreground">\+12%<\/span>/)
204
+ expect(trends[3]).toMatch(/<span class="text-foreground">\+2<\/span>/)
205
+ expect(trends[2]).toMatch(/<span class="text-muted-foreground">-3<\/span>/)
206
+ expect(trends[5]).toMatch(/<span class="text-muted-foreground">same<\/span>/)
207
+ expect(trends[5]).not.toMatch(/<svg|data-tone/)
208
+ })
209
+ })
210
+
211
+ describe("KpiGrid soft variant, group and header", () => {
212
+ it("soft: each cell a card in its tone's tint and edge around foreground text", () => {
213
+ const html = renderToStaticMarkup(
214
+ <KpiGrid
215
+ variant="soft"
216
+ cells={[
217
+ { label: "Failed", value: 4, tone: "danger", onClick: noop, actionHint: "Open" },
218
+ { label: "Total", value: 12 },
219
+ ]}
220
+ />,
221
+ )
222
+ const failed = openingTags(html, "button")[0] ?? ""
223
+ expect(classOf(failed)).toMatch(/bg-danger-soft/)
224
+ expect(classOf(failed)).toMatch(/border-danger/)
225
+ expect(classOf(failed)).toMatch(/text-foreground/)
226
+ expect(classOf(failed)).toContain("focus-visible:ring-offset-2")
227
+ const total = openingTags(html, "div").find((t) => /bg-muted/.test(t) && /rounded-lg/.test(t))
228
+ expect(classOf(total)).toContain("border-border")
229
+ expect(html).not.toMatch(/border-y/)
230
+ })
231
+
232
+ it("names the group when asked and renders the header label in normal case at 13 px", () => {
233
+ const html = renderToStaticMarkup(
234
+ <KpiGrid
235
+ boxed
236
+ ariaLabel="Health"
237
+ header={{ label: "Status", badge: "live" }}
238
+ cells={[{ label: "Up", value: 1 }]}
239
+ />,
240
+ )
241
+ const root = openingTags(html, "div")[0] ?? ""
242
+ expect(attribute(root, "role")).toBe("group")
243
+ expect(attribute(root, "aria-label")).toBe("Health")
244
+ expect(classOf(root)).toContain("rounded-lg border")
245
+ expect(html).toMatch(/text-small font-semibold[^"]*"><span>Status<\/span>/)
246
+ expect(html).toContain(">live</span>")
247
+ const unnamed = renderToStaticMarkup(<KpiGrid cells={[{ label: "Up", value: 1 }]} />)
248
+ expect(attribute(openingTags(unnamed, "div")[0] ?? "", "role")).toBeUndefined()
249
+ })
250
+ })
251
+
252
+ describe("KpiGridSkeleton mirrors the grid's geometry", () => {
253
+ it("strip: busy, one placeholder cell per KPI, the same responsive columns", () => {
254
+ const html = renderToStaticMarkup(<KpiGridSkeleton cells={4} />)
255
+ expect(attribute(openingTags(html, "div")[0] ?? "", "aria-busy")).toBe("true")
256
+ expect(strip(html).cells).toHaveLength(4)
257
+ expect(html).toContain("grid-cols-2 sm:grid-cols-4")
258
+ })
259
+
260
+ it("strip: an incomplete last row while loading has the loaded grid's frame, grid and cells", () => {
261
+ const loading = strip(renderToStaticMarkup(<KpiGridSkeleton cells={5} />))
262
+ const loaded = strip(renderToStaticMarkup(<KpiGrid cells={kpis(5)} />))
263
+ expect(classOf(loading.frame)).toBe(classOf(loaded.frame))
264
+ expect(classOf(loading.grid)).toBe(classOf(loaded.grid))
265
+ expect(loading.cells.map(classOf)).toEqual(loaded.cells.map(classOf))
266
+ // Only the placeholder bars take the border colour, never the surface behind a track.
267
+ for (const tag of [loading.frame, loading.grid, ...loading.cells]) {
268
+ expect(classOf(tag)).not.toMatch(/\bbg-border\b/)
269
+ }
270
+ })
271
+
272
+ it("soft and boxed variants keep the loaded card shapes", () => {
273
+ const soft = renderToStaticMarkup(<KpiGridSkeleton cells={3} variant="soft" />)
274
+ expect(openingTags(soft, "div").filter((t) => /rounded-lg border p-4/.test(t))).toHaveLength(3)
275
+ const boxed = renderToStaticMarkup(<KpiGridSkeleton cells={2} boxed />)
276
+ expect(classOf(openingTags(boxed, "div")[0])).toContain("rounded-lg border")
277
+ expect(renderToStaticMarkup(<KpiGridSkeleton cells={-1} />)).not.toContain("px-5 py-4")
278
+ })
279
+ })