@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
@@ -0,0 +1,81 @@
1
+ import { renderToStaticMarkup } from "react-dom/server"
2
+ import { describe, expect, it } from "vitest"
3
+ import { Check } from "lucide-react"
4
+ import { ListFooter } from "../components/ListFooter.js"
5
+ import { ICON_STROKE } from "../lib/icon-stroke.js"
6
+ import { LocaleProvider, useLocale } from "../locale/locale-provider.js"
7
+ import { useHostBridge } from "./host-bridge.js"
8
+ import { WidgetFixtureHost } from "./widget-fixture.js"
9
+
10
+ /**
11
+ * The harness gives a widget what `McpAppView` gives it in a host, so a
12
+ * preview in the widget playground shows what a user sees: the toolkit's
13
+ * strings in the host locale (`globals.locale`), the host theme on the
14
+ * bridge (`globals.theme`), the CI icon stroke and a toast stack.
15
+ */
16
+
17
+ /** A widget made of toolkit chrome: the list footer's text follows the toolkit locale. */
18
+ function Footer() {
19
+ return <ListFooter shown={8} total={1234} hasMore onLoadMore={() => {}} noun="Aufgaben" />
20
+ }
21
+
22
+ const render = (globals?: Record<string, unknown>, widget = Footer) =>
23
+ renderToStaticMarkup(<WidgetFixtureHost widget={widget} globals={globals} />)
24
+
25
+ describe("WidgetFixtureHost host scope", () => {
26
+ it("renders the toolkit's strings and number format in globals.locale", () => {
27
+ const html = render({ locale: "de-DE" })
28
+ expect(html).toContain("Zeigt 8 von 1.234 Aufgaben")
29
+ expect(html).toContain("Mehr laden")
30
+ })
31
+
32
+ it("defaults to English and falls back to it for a blank locale", () => {
33
+ for (const globals of [undefined, { locale: "" }, { locale: 42 }]) {
34
+ const html = render(globals)
35
+ expect(html).toContain("Showing 8 of 1,234 Aufgaben")
36
+ expect(html).toContain("Load more")
37
+ }
38
+ })
39
+
40
+ it("lets a LocaleProvider around the harness win over globals.locale", () => {
41
+ const html = renderToStaticMarkup(
42
+ <LocaleProvider locale="de">
43
+ <WidgetFixtureHost widget={Footer} globals={{ locale: "en-US" }} />
44
+ </LocaleProvider>,
45
+ )
46
+ expect(html).toContain("Mehr laden")
47
+ })
48
+
49
+ it("leaves useLocale() of the widget alone, like McpAppView does", () => {
50
+ function ShowsLocale() {
51
+ return <p>{useLocale()}</p>
52
+ }
53
+ expect(render({ locale: "de-DE" }, ShowsLocale)).toContain("<p>en</p>")
54
+ })
55
+
56
+ it("reports globals.theme on the bridge", () => {
57
+ function ShowsTheme() {
58
+ return <p>{useHostBridge().theme}</p>
59
+ }
60
+ expect(render({ theme: "dark" }, ShowsTheme)).toContain("<p>dark</p>")
61
+ expect(render(undefined, ShowsTheme)).toContain("<p>light</p>")
62
+ })
63
+
64
+ it("draws Lucide icons at the view's stroke", () => {
65
+ expect(render(undefined, () => <Check />)).toContain(`stroke-width="${ICON_STROKE}"`)
66
+ })
67
+
68
+ it("mounts the toast stack as the view's one polite live region", () => {
69
+ // Like McpAppView's: fixed attributes, no role and no name, so the first
70
+ // toast lands in a region the browser already tracks. Its words (the
71
+ // dismiss label in the host locale) need a toast, so effects:
72
+ // widget-fixture.effects.test.tsx.
73
+ const html = render()
74
+ const viewport = /<div data-slot="toast-viewport"[^>]*>/.exec(html)?.[0]
75
+ expect(viewport).toBeDefined()
76
+ expect(viewport).toContain('aria-live="polite"')
77
+ expect(viewport).toContain('aria-atomic="false"')
78
+ expect(viewport).not.toContain("aria-label")
79
+ expect(viewport).not.toContain("role=")
80
+ })
81
+ })
@@ -1,5 +1,9 @@
1
1
  import { createContext, useContext, useMemo, type ComponentType, type ReactNode } from "react"
2
+ import { LucideProvider } from "lucide-react"
2
3
  import type { PipelineContext, StepResult, WidgetProps } from "@miragon/mcp-toolkit-core"
4
+ import { ToastProvider } from "../components/Toast.js"
5
+ import { ICON_STROKE } from "../lib/icon-stroke.js"
6
+ import { ToolkitLocaleFallback } from "../locale/locale-provider.js"
3
7
  import { AppQueryProvider } from "../providers/query-provider.js"
4
8
  import { HostBridgeProvider, createStandaloneHostBridge } from "./host-bridge.js"
5
9
 
@@ -143,10 +147,10 @@ export function useFixtureHost(): FixtureHostValue | null {
143
147
  }
144
148
 
145
149
  /**
146
- * Default host-context values the harness reports. Only `theme` is read since
147
- * the mcp-use 2.x move (the other host globals were consumed by the 1.x
148
- * `window.openai` shim); the object keeps its shape so existing playground
149
- * setups that spread overrides keep compiling.
150
+ * Default host-context values the harness reports. `theme` and `locale` are
151
+ * read since the mcp-use 2.x move (the other host globals were consumed by
152
+ * the 1.x `window.openai` shim); the object keeps its shape so existing
153
+ * playground setups that spread overrides keep compiling.
150
154
  */
151
155
  const DEFAULT_GLOBALS = {
152
156
  theme: "light",
@@ -192,9 +196,12 @@ export interface WidgetFixtureHostProps {
192
196
  */
193
197
  tools?: Record<string, FixtureToolEntry>
194
198
  /**
195
- * Host context overrides, merged over {@link DEFAULT_GLOBALS}. Since the
196
- * mcp-use 2.x move only `theme` influences the simulated host; the other
197
- * keys are accepted for compatibility with existing playground setups.
199
+ * Host context overrides, merged over {@link DEFAULT_GLOBALS}. Two keys
200
+ * influence the simulated host: `theme` (`"light"` | `"dark"`, reported by
201
+ * `useHostBridge().theme`) and `locale` (a BCP 47 tag: the toolkit's own
202
+ * strings and number format follow it, exactly as under `McpAppView`; a
203
+ * `LocaleProvider` around or inside the widget still wins). The other keys
204
+ * are accepted for compatibility with existing playground setups.
198
205
  */
199
206
  globals?: Record<string, unknown>
200
207
  /**
@@ -247,6 +254,10 @@ export function buildFixtureWidgetProps(
247
254
  * would (adapted widgets report through this bridge outside a real view).
248
255
  * - **The toolkit `AppQueryProvider`** — so widgets that fetch via `useToolQuery`
249
256
  * (e.g. generated `use*` hooks) resolve against the same registry.
257
+ * - **What `McpAppView` gives a widget** — the toolkit's strings in the host
258
+ * locale (`globals.locale`, as `McpAppView` does with the host's), the
259
+ * toast stack (`useToast()` shows real toasts) and the icon stroke of the
260
+ * view (2.5), so the preview matches what a host renders.
250
261
  *
251
262
  * ### Boundaries
252
263
  * The harness simulates the **host-portable pattern** (`useHostBridge`,
@@ -282,10 +293,14 @@ export function WidgetFixtureHost({
282
293
  // The standalone host bridge is the harness's single simulation source: it
283
294
  // logs every verb through the `HostActionLog` channel for the playground's
284
295
  // activity log. Depends on the callback props directly — a fresh Provider
285
- // value on callback change is fine for a dev harness. Only `globals.theme`
286
- // still influences the simulated host (the remaining globals fed the 1.x
287
- // `window.openai` shim).
296
+ // value on callback change is fine for a dev harness. `globals.theme` and
297
+ // `globals.locale` influence the simulated host (the remaining globals fed
298
+ // the 1.x `window.openai` shim).
288
299
  const bridgeTheme = (globals?.theme ?? DEFAULT_GLOBALS.theme) === "dark" ? "dark" : "light"
300
+ const hostLocale =
301
+ typeof globals?.locale === "string" && globals.locale.trim() !== ""
302
+ ? globals.locale
303
+ : DEFAULT_GLOBALS.locale
289
304
  const bridge = useMemo(
290
305
  () =>
291
306
  createStandaloneHostBridge({
@@ -308,11 +323,19 @@ export function WidgetFixtureHost({
308
323
  // widgets and raw `({ data })` widgets read their fixture from one mount.
309
324
  const RenderWidget = Widget as ComponentType<Record<string, unknown>>
310
325
 
326
+ // The same scope `McpAppView` puts around a widget: the toolkit's strings
327
+ // in the host locale, the CI icon stroke and a toast stack.
311
328
  return (
312
329
  <FixtureHostContext.Provider value={value}>
313
330
  <HostBridgeProvider bridge={bridge}>
314
331
  <AppQueryProvider callTool={callTool}>
315
- <RenderWidget {...widgetProps} data={data} />
332
+ <ToolkitLocaleFallback locale={hostLocale}>
333
+ <LucideProvider strokeWidth={ICON_STROKE}>
334
+ <ToastProvider>
335
+ <RenderWidget {...widgetProps} data={data} />
336
+ </ToastProvider>
337
+ </LucideProvider>
338
+ </ToolkitLocaleFallback>
316
339
  </AppQueryProvider>
317
340
  </HostBridgeProvider>
318
341
  </FixtureHostContext.Provider>
@@ -18,6 +18,8 @@ import {
18
18
  import { Switch } from "../primitives/switch.js"
19
19
  import { useToolkitMessages } from "../locale/locale-provider.js"
20
20
  import type { PropsSheetMessages } from "../locale/toolkit-messages.js"
21
+ import { propFieldText, type PropFieldText } from "./builder/builder-model.js"
22
+ import { useDisplayResolver } from "./builder/use-display-resolver.js"
21
23
 
22
24
  /**
23
25
  * Tiny JSON-Schema-driven editor for a layout cell's `props`. The
@@ -31,6 +33,12 @@ import type { PropsSheetMessages } from "../locale/toolkit-messages.js"
31
33
  * - Nested objects/arrays/$ref/anyOf are not supported — the field
32
34
  * shows an "edit JSON manually" hint so authors know to fall back.
33
35
  *
36
+ * Each field is labelled in user words: the property's `x-display` title
37
+ * (`PROPS_SCHEMA_DISPLAY_KEYWORD`, a key or per-locale text), else its JSON
38
+ * Schema `title`, else its name; the raw property name then follows as a
39
+ * quiet hint, and the help line is the `x-display` summary or the
40
+ * `description`.
41
+ *
34
42
  * The sheet writes a clean `Record<string, unknown>` (empty values
35
43
  * stripped) and reports `undefined` when the user clears every field, so
36
44
  * the cell's `props` field stays absent rather than `{}` — matches the
@@ -39,6 +47,7 @@ import type { PropsSheetMessages } from "../locale/toolkit-messages.js"
39
47
 
40
48
  interface JsonSchemaProperty {
41
49
  type?: string
50
+ title?: string
42
51
  description?: string
43
52
  enum?: unknown[]
44
53
  default?: unknown
@@ -59,6 +68,8 @@ export interface WidgetPropsSheetProps {
59
68
  open: boolean
60
69
  onOpenChange: (open: boolean) => void
61
70
  widgetId: string
71
+ /** The widget's human name (its title); the id then shows as a quiet hint under it. */
72
+ widgetTitle?: string
62
73
  schema: Record<string, unknown> | undefined
63
74
  value: Record<string, unknown> | undefined
64
75
  onApply: (next: Record<string, unknown> | undefined) => void
@@ -69,12 +80,14 @@ export function WidgetPropsSheet({
69
80
  open,
70
81
  onOpenChange,
71
82
  widgetId,
83
+ widgetTitle,
72
84
  schema,
73
85
  value,
74
86
  onApply,
75
87
  labels,
76
88
  }: WidgetPropsSheetProps) {
77
89
  const messages = useToolkitMessages()
90
+ const { locale, resolveKey } = useDisplayResolver()
78
91
  const L = { ...messages.propsSheet, ...labels }
79
92
  const obj = (schema ?? {}) as JsonSchemaObject
80
93
  const [draft, setDraft] = useState<Record<string, unknown>>(value ?? {})
@@ -109,7 +122,8 @@ export function WidgetPropsSheet({
109
122
  <SheetTitle>{L.title}</SheetTitle>
110
123
  <SheetDescription className="flex flex-col items-start gap-1">
111
124
  <span>{L.description}</span>
112
- <span className="bg-muted rounded px-1 font-mono text-xs">{widgetId}</span>
125
+ {widgetTitle && <span className="text-foreground font-medium">{widgetTitle}</span>}
126
+ <span className="bg-muted text-small rounded px-1 font-mono">{widgetId}</span>
113
127
  </SheetDescription>
114
128
  </SheetHeader>
115
129
  <div className="flex flex-col gap-4 px-4 py-4">
@@ -120,6 +134,7 @@ export function WidgetPropsSheet({
120
134
  <PropertyField
121
135
  key={key}
122
136
  name={key}
137
+ text={propFieldText(key, prop as Record<string, unknown>, locale, resolveKey)}
123
138
  prop={prop}
124
139
  value={draft[key]}
125
140
  unsetLabel={L.unsetOption}
@@ -156,6 +171,7 @@ export function WidgetPropsSheet({
156
171
 
157
172
  function PropertyField({
158
173
  name,
174
+ text,
159
175
  prop,
160
176
  value,
161
177
  unsetLabel,
@@ -163,6 +179,7 @@ function PropertyField({
163
179
  onChange,
164
180
  }: {
165
181
  name: string
182
+ text: PropFieldText
166
183
  prop: JsonSchemaProperty
167
184
  value: unknown
168
185
  unsetLabel: string
@@ -175,12 +192,12 @@ function PropertyField({
175
192
  const options = prop.enum.filter((v): v is string => typeof v === "string")
176
193
  const current = typeof value === "string" && value !== "" ? value : UNSET_SENTINEL
177
194
  return (
178
- <FieldShell id={id} name={name} description={prop.description}>
195
+ <FieldShell id={id} text={text}>
179
196
  <Select
180
197
  value={current}
181
198
  onValueChange={(v) => onChange(v === UNSET_SENTINEL ? undefined : v)}
182
199
  >
183
- <SelectTrigger id={id}>
200
+ <SelectTrigger id={id} aria-describedby={helpId(id, text)}>
184
201
  <SelectValue placeholder={unsetLabel} />
185
202
  </SelectTrigger>
186
203
  <SelectContent>
@@ -199,9 +216,10 @@ function PropertyField({
199
216
  if (prop.type === "string") {
200
217
  const current = typeof value === "string" ? value : ""
201
218
  return (
202
- <FieldShell id={id} name={name} description={prop.description}>
219
+ <FieldShell id={id} text={text}>
203
220
  <Input
204
221
  id={id}
222
+ aria-describedby={helpId(id, text)}
205
223
  value={current}
206
224
  onChange={(e) => onChange(e.target.value === "" ? undefined : e.target.value)}
207
225
  />
@@ -212,9 +230,10 @@ function PropertyField({
212
230
  if (prop.type === "number" || prop.type === "integer") {
213
231
  const current = typeof value === "number" ? String(value) : ""
214
232
  return (
215
- <FieldShell id={id} name={name} description={prop.description}>
233
+ <FieldShell id={id} text={text}>
216
234
  <Input
217
235
  id={id}
236
+ aria-describedby={helpId(id, text)}
218
237
  type="number"
219
238
  value={current}
220
239
  onChange={(e) => {
@@ -232,9 +251,10 @@ function PropertyField({
232
251
  if (prop.type === "boolean") {
233
252
  const current = value === true
234
253
  return (
235
- <FieldShell id={id} name={name} description={prop.description}>
254
+ <FieldShell id={id} text={text}>
236
255
  <Switch
237
256
  id={id}
257
+ aria-describedby={helpId(id, text)}
238
258
  checked={current}
239
259
  onCheckedChange={(checked) => onChange(checked || undefined)}
240
260
  />
@@ -243,30 +263,51 @@ function PropertyField({
243
263
  }
244
264
 
245
265
  return (
246
- <FieldShell id={id} name={name} description={prop.description}>
247
- <p className="text-muted-foreground text-xs italic">{unsupportedLabel(prop.type ?? "?")}</p>
266
+ <FieldShell id={id} text={text}>
267
+ <p className="text-muted-foreground text-small italic">
268
+ {unsupportedLabel(prop.type ?? "?")}
269
+ </p>
248
270
  </FieldShell>
249
271
  )
250
272
  }
251
273
 
274
+ /** The id of a field's help line, when it has one (its `aria-describedby`). */
275
+ function helpId(id: string, text: PropFieldText): string | undefined {
276
+ return text.help ? `${id}-help` : undefined
277
+ }
278
+
279
+ /**
280
+ * Label (the human title), the raw property name as a quiet hint beside it
281
+ * (outside the label, so the accessible name stays the title), the control,
282
+ * and the help line it is described by.
283
+ */
252
284
  function FieldShell({
253
285
  id,
254
- name,
255
- description,
286
+ text,
256
287
  children,
257
288
  }: {
258
289
  id: string
259
- name: string
260
- description?: string
290
+ text: PropFieldText
261
291
  children: ReactNode
262
292
  }) {
263
293
  return (
264
294
  <div className="flex flex-col gap-1.5">
265
- <label htmlFor={id} className="text-sm font-medium">
266
- {name}
267
- </label>
295
+ <div className="flex flex-wrap items-baseline gap-x-2">
296
+ <label htmlFor={id} className="text-sm font-medium">
297
+ {text.label}
298
+ </label>
299
+ {text.hint && (
300
+ <code data-slot="prop-name" className="text-muted-foreground text-small font-mono">
301
+ {text.hint}
302
+ </code>
303
+ )}
304
+ </div>
268
305
  {children}
269
- {description ? <p className="text-muted-foreground text-xs">{description}</p> : null}
306
+ {text.help ? (
307
+ <p id={helpId(id, text)} className="text-muted-foreground text-small">
308
+ {text.help}
309
+ </p>
310
+ ) : null}
270
311
  </div>
271
312
  )
272
313
  }
@@ -126,7 +126,7 @@ export function UnavailableWidget({
126
126
  return (
127
127
  <div data-slot="widget-unavailable" data-widget-id={widgetId} className={className}>
128
128
  <p>{messages.renderer.widgetUnavailable}</p>
129
- <p className="mt-1 font-mono text-xs break-all">{widgetId}</p>
129
+ <p className="text-small mt-1 font-mono break-all">{widgetId}</p>
130
130
  </div>
131
131
  )
132
132
  }
@@ -1,38 +1,51 @@
1
1
  import type { ReactNode } from "react"
2
+ import { ArrowRight } from "lucide-react"
2
3
  import { FOCUS_RING } from "../lib/focus-ring.js"
4
+ import { cn } from "../lib/utils.js"
3
5
 
4
6
  export interface DrillButtonProps {
5
7
  /** Button label. */
6
8
  children: ReactNode
7
9
  /** Drill/navigation handler — wire to in-widget navigation (e.g. `showWidget`). */
8
10
  onDrill: () => void
9
- /** Optional leading glyph (e.g. a domain icon). The trailing → is always rendered. */
11
+ /**
12
+ * Optional leading icon — a Lucide icon of the domain, never a typed glyph.
13
+ * Decorative (`aria-hidden`). The trailing arrow is always rendered.
14
+ */
10
15
  icon?: ReactNode
11
16
  /** `sm` for table rows, `md` for surface headers. Default `"sm"`. */
12
17
  size?: "sm" | "md"
13
- /** Accessible label when the visible text is terse. */
18
+ /** Accessible label when the visible text is terse; keep the visible text in it (WCAG 2.5.3). */
14
19
  ariaLabel?: string
15
20
  }
16
21
 
17
22
  /**
18
23
  * The single, deterministic in-widget navigation control: a neutral outline
19
- * button with a trailing → that drills into a detail/sub-view. Use it for
20
- * every in-app drill (row "Open", header nav) so they read identically and stay
21
- * visually quieter than primary/agentic actions. Decoupled from any host — pass
22
- * an `onDrill` callback (e.g. wrapping `useHostActions().showWidget`).
24
+ * button with a trailing Lucide arrow that drills into a detail/sub-view. Use
25
+ * it for every in-app drill (row "Open", header nav) so they read identically
26
+ * and stay visually quieter than primary/agentic actions. Decoupled from any
27
+ * host — pass an `onDrill` callback (e.g. wrapping `useHostActions().showWidget`).
23
28
  */
24
29
  export function DrillButton({ children, onDrill, icon, size = "sm", ariaLabel }: DrillButtonProps) {
25
- const pad = size === "md" ? "px-3 py-1.5 text-sm" : "px-2.5 py-1 text-xs"
30
+ const pad = size === "md" ? "px-3 py-1.5 text-dense" : "px-2.5 py-1 text-small"
26
31
  return (
27
32
  <button
28
33
  type="button"
29
34
  onClick={onDrill}
30
35
  aria-label={ariaLabel}
31
- className={`border-border text-foreground hover:border-focus hover:bg-accent hover:text-accent-foreground inline-flex items-center gap-1 rounded-md border font-medium transition-colors ${FOCUS_RING} ${pad}`}
36
+ className={cn(
37
+ "border-border text-foreground hover:border-focus hover:bg-accent hover:text-accent-foreground inline-flex items-center gap-1 rounded-md border font-medium transition-colors [&_svg:not([class*='size-'])]:size-4",
38
+ FOCUS_RING,
39
+ pad,
40
+ )}
32
41
  >
33
- {icon && <span aria-hidden="true">{icon}</span>}
42
+ {icon && (
43
+ <span aria-hidden="true" className="inline-flex">
44
+ {icon}
45
+ </span>
46
+ )}
34
47
  {children}
35
- <span aria-hidden="true">→</span>
48
+ <ArrowRight aria-hidden="true" className="size-4 shrink-0" />
36
49
  </button>
37
50
  )
38
51
  }
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react"
2
- import { TONE_ICON } from "../lib/tone-utils.js"
2
+ import { resolveTone, TONE_ICON } from "../lib/tone-utils.js"
3
3
  import { cn } from "../lib/utils.js"
4
4
 
5
5
  /** Heading element for an empty/error state title (the surface decides the outline level). */
@@ -50,6 +50,8 @@ export function EmptyState({
50
50
  titleAs: Title = "h3",
51
51
  className,
52
52
  }: EmptyStateProps) {
53
+ // An untyped tone resolves (`ok` → success); anything else stays quiet.
54
+ const iconTone = resolveTone(tone) === "success" ? "success" : "neutral"
53
55
  return (
54
56
  <div
55
57
  data-slot="empty-state"
@@ -60,15 +62,15 @@ export function EmptyState({
60
62
  aria-hidden="true"
61
63
  className={cn(
62
64
  "mb-1 flex items-center justify-center [&_svg:not([class*='size-'])]:size-6",
63
- ICON_TONE[tone],
65
+ ICON_TONE[iconTone],
64
66
  )}
65
67
  >
66
68
  {icon}
67
69
  </div>
68
70
  )}
69
- <Title className="text-foreground text-base font-semibold">{title}</Title>
71
+ <Title className="text-foreground text-body font-semibold">{title}</Title>
70
72
  {description && (
71
- <p className="text-muted-foreground max-w-prose text-sm text-balance">{description}</p>
73
+ <p className="text-muted-foreground text-body max-w-prose text-balance">{description}</p>
72
74
  )}
73
75
  {(action || secondaryAction) && (
74
76
  <div className="mt-2 flex flex-wrap items-center justify-center gap-2">
@@ -3,7 +3,7 @@ import { CircleAlert, Info, RefreshCw, TriangleAlert, type LucideIcon } from "lu
3
3
  import { useToolkitMessages } from "../locale/locale-provider.js"
4
4
  import { Button } from "../primitives/button.js"
5
5
  import { FOCUS_RING } from "../lib/focus-ring.js"
6
- import { TONE_BORDER, TONE_ICON, TONE_SOFT } from "../lib/tone-utils.js"
6
+ import { resolveTone, TONE_BORDER, TONE_ICON, TONE_SOFT } from "../lib/tone-utils.js"
7
7
  import { cn } from "../lib/utils.js"
8
8
  import type { StateHeadingLevel } from "./EmptyState.js"
9
9
 
@@ -28,7 +28,10 @@ export interface ErrorStateProps {
28
28
  retryLabel?: string
29
29
  /** Further actions after the retry button (e.g. a link to another view). */
30
30
  actions?: ReactNode
31
- /** Default `"danger"`. `"warning"` for partial failures, `"info"` for notices. */
31
+ /**
32
+ * Default `"danger"`. `"warning"` for partial failures, `"info"` for
33
+ * notices. An untyped retired name still resolves (`error` → `danger`).
34
+ */
32
35
  tone?: ErrorStateTone
33
36
  /** Replace the tone's default Lucide icon. */
34
37
  icon?: LucideIcon
@@ -54,19 +57,36 @@ const STATE_ICON: Record<ErrorStateTone, LucideIcon> = {
54
57
  info: Info,
55
58
  }
56
59
 
57
- /** Per-variant geometry: `block` replaces a surface, `inline` sits above content. */
60
+ /**
61
+ * The state's tone from whatever arrives: its three tones as is, a retired
62
+ * name through `resolveTone` (`error` → `danger`, `notice` → `info`), any
63
+ * other tone or unknown name as the default `danger`. A tone from untyped
64
+ * data (plain JS, the wire) never crashes the view on a missing icon.
65
+ */
66
+ function stateTone(name: unknown): ErrorStateTone {
67
+ const tone = resolveTone(name, "danger")
68
+ return tone === "warning" || tone === "info" ? tone : "danger"
69
+ }
70
+
71
+ /**
72
+ * Per-variant geometry: `block` replaces a surface and reads as running text
73
+ * (16 px), `inline` is the compact alert box above content that still renders
74
+ * (14 px, the dense-controls exception of the type scale).
75
+ */
58
76
  const VARIANT = {
59
77
  block: {
60
78
  box: "gap-4 p-4",
61
79
  icon: "size-5",
62
- title: "text-base",
80
+ title: "text-body",
81
+ description: "text-body",
63
82
  defaultTitleAs: "h2",
64
83
  retry: "default",
65
84
  },
66
85
  inline: {
67
- box: "gap-3 px-4 py-3 text-sm",
86
+ box: "gap-3 px-4 py-3 text-dense",
68
87
  icon: "size-4",
69
- title: "text-sm",
88
+ title: "text-dense",
89
+ description: "text-dense",
70
90
  defaultTitleAs: "p",
71
91
  retry: "outline",
72
92
  },
@@ -76,6 +96,7 @@ const VARIANT = {
76
96
  box: string
77
97
  icon: string
78
98
  title: string
99
+ description: string
79
100
  defaultTitleAs: StateHeadingLevel
80
101
  retry: "default" | "outline"
81
102
  }
@@ -95,13 +116,14 @@ export function ErrorState({
95
116
  onRetry,
96
117
  retryLabel,
97
118
  actions,
98
- tone = "danger",
119
+ tone: toneProp,
99
120
  icon,
100
121
  variant = "block",
101
122
  role = "alert",
102
123
  titleAs,
103
124
  className,
104
125
  }: ErrorStateProps) {
126
+ const tone = stateTone(toneProp)
105
127
  const style = VARIANT[variant]
106
128
  const Icon = icon ?? STATE_ICON[tone]
107
129
  const Title = titleAs ?? style.defaultTitleAs
@@ -125,7 +147,7 @@ export function ErrorState({
125
147
  <Icon aria-hidden="true" className={cn("mt-0.5 shrink-0", style.icon, TONE_ICON[tone])} />
126
148
  <div className="flex min-w-0 flex-1 flex-col gap-1">
127
149
  <Title className={cn("font-semibold", style.title)}>{title}</Title>
128
- {description && <div className="text-sm break-words">{description}</div>}
150
+ {description && <div className={cn("break-words", style.description)}>{description}</div>}
129
151
  {details && <ErrorDetails label={detailsLabel}>{details}</ErrorDetails>}
130
152
  <ErrorActions
131
153
  onRetry={onRetry}
@@ -146,7 +168,7 @@ export function ErrorState({
146
168
  function ErrorDetails({ label, children }: { label: string | undefined; children: ReactNode }) {
147
169
  const messages = useToolkitMessages()
148
170
  return (
149
- <details className="text-xs">
171
+ <details className="text-small">
150
172
  <summary className={`w-fit cursor-pointer rounded-sm py-0.5 select-none ${FOCUS_RING}`}>
151
173
  {label ?? messages.common.details}
152
174
  </summary>
@@ -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
+ })