@miragon/mcp-toolkit-ui 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (511) hide show
  1. package/README.md +14 -5
  2. package/dist/app/adapt-data-widget.d.ts +1 -22
  3. package/dist/app/adapt-data-widget.d.ts.map +1 -1
  4. package/dist/app/adapt-data-widget.js +1 -35
  5. package/dist/app/adapt-data-widget.js.map +1 -1
  6. package/dist/app/builder/BuilderBanners.d.ts +7 -2
  7. package/dist/app/builder/BuilderBanners.d.ts.map +1 -1
  8. package/dist/app/builder/BuilderBanners.js +8 -3
  9. package/dist/app/builder/BuilderBanners.js.map +1 -1
  10. package/dist/app/builder/CanvasRow.d.ts +4 -2
  11. package/dist/app/builder/CanvasRow.d.ts.map +1 -1
  12. package/dist/app/builder/CanvasRow.js +29 -17
  13. package/dist/app/builder/CanvasRow.js.map +1 -1
  14. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -1
  15. package/dist/app/builder/CatalogueSheet.js +14 -6
  16. package/dist/app/builder/CatalogueSheet.js.map +1 -1
  17. package/dist/app/builder/CellPropsSheet.d.ts +7 -3
  18. package/dist/app/builder/CellPropsSheet.d.ts.map +1 -1
  19. package/dist/app/builder/CellPropsSheet.js +10 -3
  20. package/dist/app/builder/CellPropsSheet.js.map +1 -1
  21. package/dist/app/builder/DiscardDialog.d.ts +18 -0
  22. package/dist/app/builder/DiscardDialog.d.ts.map +1 -0
  23. package/dist/app/builder/DiscardDialog.js +18 -0
  24. package/dist/app/builder/DiscardDialog.js.map +1 -0
  25. package/dist/app/builder/EmptyHint.d.ts +4 -0
  26. package/dist/app/builder/EmptyHint.d.ts.map +1 -1
  27. package/dist/app/builder/EmptyHint.js +6 -1
  28. package/dist/app/builder/EmptyHint.js.map +1 -1
  29. package/dist/app/builder/LayoutSurface.d.ts +33 -9
  30. package/dist/app/builder/LayoutSurface.d.ts.map +1 -1
  31. package/dist/app/builder/LayoutSurface.js +74 -23
  32. package/dist/app/builder/LayoutSurface.js.map +1 -1
  33. package/dist/app/builder/PipelineStrip.d.ts +5 -2
  34. package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
  35. package/dist/app/builder/PipelineStrip.js +33 -15
  36. package/dist/app/builder/PipelineStrip.js.map +1 -1
  37. package/dist/app/builder/SaveDialog.d.ts +9 -1
  38. package/dist/app/builder/SaveDialog.d.ts.map +1 -1
  39. package/dist/app/builder/SaveDialog.js +9 -3
  40. package/dist/app/builder/SaveDialog.js.map +1 -1
  41. package/dist/app/builder/Toolbar.d.ts +14 -4
  42. package/dist/app/builder/Toolbar.d.ts.map +1 -1
  43. package/dist/app/builder/Toolbar.js +24 -7
  44. package/dist/app/builder/Toolbar.js.map +1 -1
  45. package/dist/app/builder/Workspace.d.ts +23 -3
  46. package/dist/app/builder/Workspace.d.ts.map +1 -1
  47. package/dist/app/builder/Workspace.js +58 -8
  48. package/dist/app/builder/Workspace.js.map +1 -1
  49. package/dist/app/builder/builder-model.d.ts +146 -3
  50. package/dist/app/builder/builder-model.d.ts.map +1 -1
  51. package/dist/app/builder/builder-model.js +196 -5
  52. package/dist/app/builder/builder-model.js.map +1 -1
  53. package/dist/app/builder/labels.d.ts +13 -62
  54. package/dist/app/builder/labels.d.ts.map +1 -1
  55. package/dist/app/builder/labels.js +9 -58
  56. package/dist/app/builder/labels.js.map +1 -1
  57. package/dist/app/builder/reveal.d.ts +10 -0
  58. package/dist/app/builder/reveal.d.ts.map +1 -0
  59. package/dist/app/builder/reveal.js +10 -0
  60. package/dist/app/builder/reveal.js.map +1 -0
  61. package/dist/app/builder/status-chip.d.ts.map +1 -1
  62. package/dist/app/builder/status-chip.js +3 -2
  63. package/dist/app/builder/status-chip.js.map +1 -1
  64. package/dist/app/builder/use-builder-catalogue.d.ts +5 -1
  65. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
  66. package/dist/app/builder/use-builder-catalogue.js +50 -17
  67. package/dist/app/builder/use-builder-catalogue.js.map +1 -1
  68. package/dist/app/builder/use-builder-session.d.ts +30 -0
  69. package/dist/app/builder/use-builder-session.d.ts.map +1 -0
  70. package/dist/app/builder/use-builder-session.js +58 -0
  71. package/dist/app/builder/use-builder-session.js.map +1 -0
  72. package/dist/app/builder/use-display-resolver.d.ts +17 -0
  73. package/dist/app/builder/use-display-resolver.d.ts.map +1 -0
  74. package/dist/app/builder/use-display-resolver.js +16 -0
  75. package/dist/app/builder/use-display-resolver.js.map +1 -0
  76. package/dist/app/builder/use-save-dashboard.d.ts +3 -1
  77. package/dist/app/builder/use-save-dashboard.d.ts.map +1 -1
  78. package/dist/app/builder/use-save-dashboard.js +3 -1
  79. package/dist/app/builder/use-save-dashboard.js.map +1 -1
  80. package/dist/app/index.d.ts +8 -3
  81. package/dist/app/index.d.ts.map +1 -1
  82. package/dist/app/index.js +8 -2
  83. package/dist/app/index.js.map +1 -1
  84. package/dist/app/layout-builder.d.ts +18 -25
  85. package/dist/app/layout-builder.d.ts.map +1 -1
  86. package/dist/app/layout-builder.js +84 -78
  87. package/dist/app/layout-builder.js.map +1 -1
  88. package/dist/app/mcp-app-view-chrome.d.ts +103 -20
  89. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
  90. package/dist/app/mcp-app-view-chrome.js +125 -40
  91. package/dist/app/mcp-app-view-chrome.js.map +1 -1
  92. package/dist/app/mcp-app-view-states.d.ts +23 -0
  93. package/dist/app/mcp-app-view-states.d.ts.map +1 -0
  94. package/dist/app/mcp-app-view-states.js +55 -0
  95. package/dist/app/mcp-app-view-states.js.map +1 -0
  96. package/dist/app/mcp-app-view.d.ts +72 -5
  97. package/dist/app/mcp-app-view.d.ts.map +1 -1
  98. package/dist/app/mcp-app-view.js +241 -141
  99. package/dist/app/mcp-app-view.js.map +1 -1
  100. package/dist/app/mcp-toolkit-app.d.ts +8 -2
  101. package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
  102. package/dist/app/mcp-toolkit-app.js +33 -4
  103. package/dist/app/mcp-toolkit-app.js.map +1 -1
  104. package/dist/app/mcp-use-host-bridge.d.ts +15 -6
  105. package/dist/app/mcp-use-host-bridge.d.ts.map +1 -1
  106. package/dist/app/mcp-use-host-bridge.js +51 -22
  107. package/dist/app/mcp-use-host-bridge.js.map +1 -1
  108. package/dist/app/model-context.d.ts +71 -0
  109. package/dist/app/model-context.d.ts.map +1 -0
  110. package/dist/app/model-context.js +101 -0
  111. package/dist/app/model-context.js.map +1 -0
  112. package/dist/app/use-build-mode-focus.d.ts +33 -0
  113. package/dist/app/use-build-mode-focus.d.ts.map +1 -0
  114. package/dist/app/use-build-mode-focus.js +59 -0
  115. package/dist/app/use-build-mode-focus.js.map +1 -0
  116. package/dist/app/use-view-actions.d.ts +31 -0
  117. package/dist/app/use-view-actions.d.ts.map +1 -0
  118. package/dist/app/use-view-actions.js +81 -0
  119. package/dist/app/use-view-actions.js.map +1 -0
  120. package/dist/app/use-view-lifecycle.d.ts +42 -0
  121. package/dist/app/use-view-lifecycle.d.ts.map +1 -0
  122. package/dist/app/use-view-lifecycle.js +81 -0
  123. package/dist/app/use-view-lifecycle.js.map +1 -0
  124. package/dist/app/view-lifecycle.d.ts +185 -0
  125. package/dist/app/view-lifecycle.d.ts.map +1 -0
  126. package/dist/app/view-lifecycle.js +207 -0
  127. package/dist/app/view-lifecycle.js.map +1 -0
  128. package/dist/app/widget-fixture.d.ts +10 -3
  129. package/dist/app/widget-fixture.d.ts.map +1 -1
  130. package/dist/app/widget-fixture.js +21 -8
  131. package/dist/app/widget-fixture.js.map +1 -1
  132. package/dist/app/widget-props-sheet.d.ts +6 -10
  133. package/dist/app/widget-props-sheet.d.ts.map +1 -1
  134. package/dist/app/widget-props-sheet.js +25 -20
  135. package/dist/app/widget-props-sheet.js.map +1 -1
  136. package/dist/app/widget-renderer.d.ts +41 -1
  137. package/dist/app/widget-renderer.d.ts.map +1 -1
  138. package/dist/app/widget-renderer.js +82 -31
  139. package/dist/app/widget-renderer.js.map +1 -1
  140. package/dist/components/DrillButton.d.ts +9 -6
  141. package/dist/components/DrillButton.d.ts.map +1 -1
  142. package/dist/components/DrillButton.js +9 -6
  143. package/dist/components/DrillButton.js.map +1 -1
  144. package/dist/components/EmptyState.d.ts +33 -0
  145. package/dist/components/EmptyState.d.ts.map +1 -0
  146. package/dist/components/EmptyState.js +19 -0
  147. package/dist/components/EmptyState.js.map +1 -0
  148. package/dist/components/ErrorState.d.ts +53 -0
  149. package/dist/components/ErrorState.d.ts.map +1 -0
  150. package/dist/components/ErrorState.js +77 -0
  151. package/dist/components/ErrorState.js.map +1 -0
  152. package/dist/components/FilterBar.d.ts +16 -5
  153. package/dist/components/FilterBar.d.ts.map +1 -1
  154. package/dist/components/FilterBar.js +13 -5
  155. package/dist/components/FilterBar.js.map +1 -1
  156. package/dist/components/GroupCard.d.ts.map +1 -1
  157. package/dist/components/GroupCard.js +2 -1
  158. package/dist/components/GroupCard.js.map +1 -1
  159. package/dist/components/KpiGrid.d.ts +70 -18
  160. package/dist/components/KpiGrid.d.ts.map +1 -1
  161. package/dist/components/KpiGrid.js +112 -23
  162. package/dist/components/KpiGrid.js.map +1 -1
  163. package/dist/components/ListFooter.d.ts +32 -4
  164. package/dist/components/ListFooter.d.ts.map +1 -1
  165. package/dist/components/ListFooter.js +35 -6
  166. package/dist/components/ListFooter.js.map +1 -1
  167. package/dist/components/PagedListFooter.d.ts +57 -0
  168. package/dist/components/PagedListFooter.d.ts.map +1 -0
  169. package/dist/components/PagedListFooter.js +27 -0
  170. package/dist/components/PagedListFooter.js.map +1 -0
  171. package/dist/components/Pills.d.ts +36 -12
  172. package/dist/components/Pills.d.ts.map +1 -1
  173. package/dist/components/Pills.js +29 -8
  174. package/dist/components/Pills.js.map +1 -1
  175. package/dist/components/SectionHeading.d.ts +3 -1
  176. package/dist/components/SectionHeading.d.ts.map +1 -1
  177. package/dist/components/SectionHeading.js +6 -2
  178. package/dist/components/SectionHeading.js.map +1 -1
  179. package/dist/components/Toast.d.ts +43 -0
  180. package/dist/components/Toast.d.ts.map +1 -0
  181. package/dist/components/Toast.js +106 -0
  182. package/dist/components/Toast.js.map +1 -0
  183. package/dist/components/WidgetHeader.d.ts +34 -10
  184. package/dist/components/WidgetHeader.d.ts.map +1 -1
  185. package/dist/components/WidgetHeader.js +11 -6
  186. package/dist/components/WidgetHeader.js.map +1 -1
  187. package/dist/hooks/index.d.ts +4 -4
  188. package/dist/hooks/index.d.ts.map +1 -1
  189. package/dist/hooks/index.js +1 -1
  190. package/dist/hooks/index.js.map +1 -1
  191. package/dist/hooks/tool-result-recovery-model.d.ts +155 -0
  192. package/dist/hooks/tool-result-recovery-model.d.ts.map +1 -0
  193. package/dist/hooks/tool-result-recovery-model.js +273 -0
  194. package/dist/hooks/tool-result-recovery-model.js.map +1 -0
  195. package/dist/hooks/use-tool-query.d.ts +56 -14
  196. package/dist/hooks/use-tool-query.d.ts.map +1 -1
  197. package/dist/hooks/use-tool-query.js +102 -25
  198. package/dist/hooks/use-tool-query.js.map +1 -1
  199. package/dist/hooks/use-tool-result-recovery.d.ts +61 -44
  200. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
  201. package/dist/hooks/use-tool-result-recovery.js +62 -110
  202. package/dist/hooks/use-tool-result-recovery.js.map +1 -1
  203. package/dist/hooks/use-view-data.d.ts +55 -7
  204. package/dist/hooks/use-view-data.d.ts.map +1 -1
  205. package/dist/hooks/use-view-data.js +81 -11
  206. package/dist/hooks/use-view-data.js.map +1 -1
  207. package/dist/hooks/use-view-tool-query.d.ts +12 -7
  208. package/dist/hooks/use-view-tool-query.d.ts.map +1 -1
  209. package/dist/hooks/use-view-tool-query.js +15 -14
  210. package/dist/hooks/use-view-tool-query.js.map +1 -1
  211. package/dist/index.d.ts +27 -13
  212. package/dist/index.d.ts.map +1 -1
  213. package/dist/index.js +29 -7
  214. package/dist/index.js.map +1 -1
  215. package/dist/lib/focus-return.d.ts +61 -0
  216. package/dist/lib/focus-return.d.ts.map +1 -0
  217. package/dist/lib/focus-return.js +46 -0
  218. package/dist/lib/focus-return.js.map +1 -0
  219. package/dist/lib/focus-ring.d.ts +18 -0
  220. package/dist/lib/focus-ring.d.ts.map +1 -0
  221. package/dist/lib/focus-ring.js +18 -0
  222. package/dist/lib/focus-ring.js.map +1 -0
  223. package/dist/lib/icon-stroke.d.ts +12 -0
  224. package/dist/lib/icon-stroke.d.ts.map +1 -0
  225. package/dist/lib/icon-stroke.js +12 -0
  226. package/dist/lib/icon-stroke.js.map +1 -0
  227. package/dist/lib/parse-tool-result.d.ts +23 -1
  228. package/dist/lib/parse-tool-result.d.ts.map +1 -1
  229. package/dist/lib/parse-tool-result.js +38 -2
  230. package/dist/lib/parse-tool-result.js.map +1 -1
  231. package/dist/lib/toast-model.d.ts +86 -0
  232. package/dist/lib/toast-model.d.ts.map +1 -0
  233. package/dist/lib/toast-model.js +102 -0
  234. package/dist/lib/toast-model.js.map +1 -0
  235. package/dist/lib/tone-utils.d.ts +95 -9
  236. package/dist/lib/tone-utils.d.ts.map +1 -1
  237. package/dist/lib/tone-utils.js +100 -9
  238. package/dist/lib/tone-utils.js.map +1 -1
  239. package/dist/lib/tool-query.d.ts +107 -0
  240. package/dist/lib/tool-query.d.ts.map +1 -0
  241. package/dist/lib/tool-query.js +182 -0
  242. package/dist/lib/tool-query.js.map +1 -0
  243. package/dist/lib/tool-result-observer.d.ts +108 -0
  244. package/dist/lib/tool-result-observer.d.ts.map +1 -0
  245. package/dist/lib/tool-result-observer.js +158 -0
  246. package/dist/lib/tool-result-observer.js.map +1 -0
  247. package/dist/lib/typography.d.ts +37 -0
  248. package/dist/lib/typography.d.ts.map +1 -0
  249. package/dist/lib/typography.js +41 -0
  250. package/dist/lib/typography.js.map +1 -0
  251. package/dist/lib/utils.d.ts.map +1 -1
  252. package/dist/lib/utils.js +11 -1
  253. package/dist/lib/utils.js.map +1 -1
  254. package/dist/locale/index.d.ts +3 -1
  255. package/dist/locale/index.d.ts.map +1 -1
  256. package/dist/locale/index.js +2 -1
  257. package/dist/locale/index.js.map +1 -1
  258. package/dist/locale/locale-provider.d.ts +42 -1
  259. package/dist/locale/locale-provider.d.ts.map +1 -1
  260. package/dist/locale/locale-provider.js +44 -3
  261. package/dist/locale/locale-provider.js.map +1 -1
  262. package/dist/locale/message-types.d.ts +336 -0
  263. package/dist/locale/message-types.d.ts.map +1 -0
  264. package/dist/locale/message-types.js +7 -0
  265. package/dist/locale/message-types.js.map +1 -0
  266. package/dist/locale/messages-de.d.ts +9 -0
  267. package/dist/locale/messages-de.d.ts.map +1 -0
  268. package/dist/locale/messages-de.js +185 -0
  269. package/dist/locale/messages-de.js.map +1 -0
  270. package/dist/locale/messages-en.d.ts +8 -0
  271. package/dist/locale/messages-en.d.ts.map +1 -0
  272. package/dist/locale/messages-en.js +184 -0
  273. package/dist/locale/messages-en.js.map +1 -0
  274. package/dist/locale/toolkit-messages.d.ts +57 -0
  275. package/dist/locale/toolkit-messages.d.ts.map +1 -0
  276. package/dist/locale/toolkit-messages.js +103 -0
  277. package/dist/locale/toolkit-messages.js.map +1 -0
  278. package/dist/primitives/alert.d.ts +1 -1
  279. package/dist/primitives/alert.d.ts.map +1 -1
  280. package/dist/primitives/alert.js +6 -2
  281. package/dist/primitives/alert.js.map +1 -1
  282. package/dist/primitives/badge.d.ts +1 -1
  283. package/dist/primitives/badge.d.ts.map +1 -1
  284. package/dist/primitives/badge.js +19 -6
  285. package/dist/primitives/badge.js.map +1 -1
  286. package/dist/primitives/button.d.ts +1 -1
  287. package/dist/primitives/button.d.ts.map +1 -1
  288. package/dist/primitives/button.js +12 -8
  289. package/dist/primitives/button.js.map +1 -1
  290. package/dist/primitives/card.js +4 -4
  291. package/dist/primitives/card.js.map +1 -1
  292. package/dist/primitives/dialog.d.ts +7 -3
  293. package/dist/primitives/dialog.d.ts.map +1 -1
  294. package/dist/primitives/dialog.js +22 -8
  295. package/dist/primitives/dialog.js.map +1 -1
  296. package/dist/primitives/input.d.ts.map +1 -1
  297. package/dist/primitives/input.js +4 -1
  298. package/dist/primitives/input.js.map +1 -1
  299. package/dist/primitives/scroll-area.js +1 -1
  300. package/dist/primitives/scroll-area.js.map +1 -1
  301. package/dist/primitives/select.d.ts.map +1 -1
  302. package/dist/primitives/select.js +6 -3
  303. package/dist/primitives/select.js.map +1 -1
  304. package/dist/primitives/sheet.d.ts +3 -1
  305. package/dist/primitives/sheet.d.ts.map +1 -1
  306. package/dist/primitives/sheet.js +17 -7
  307. package/dist/primitives/sheet.js.map +1 -1
  308. package/dist/primitives/skeleton.js +1 -1
  309. package/dist/primitives/skeleton.js.map +1 -1
  310. package/dist/primitives/switch.d.ts.map +1 -1
  311. package/dist/primitives/switch.js +2 -1
  312. package/dist/primitives/switch.js.map +1 -1
  313. package/dist/primitives/tabs.d.ts.map +1 -1
  314. package/dist/primitives/tabs.js +5 -3
  315. package/dist/primitives/tabs.js.map +1 -1
  316. package/dist/providers/query-provider.d.ts +32 -1
  317. package/dist/providers/query-provider.d.ts.map +1 -1
  318. package/dist/providers/query-provider.js +53 -4
  319. package/dist/providers/query-provider.js.map +1 -1
  320. package/dist/render-cases-primitives.js +1 -1
  321. package/dist/render-cases-primitives.js.map +1 -1
  322. package/dist/render-cases.d.ts.map +1 -1
  323. package/dist/render-cases.js +48 -6
  324. package/dist/render-cases.js.map +1 -1
  325. package/dist/render-effects.d.ts +42 -0
  326. package/dist/render-effects.d.ts.map +1 -0
  327. package/dist/render-effects.js +81 -0
  328. package/dist/render-effects.js.map +1 -0
  329. package/dist/render-scan.d.ts +25 -0
  330. package/dist/render-scan.d.ts.map +1 -0
  331. package/dist/render-scan.js +66 -0
  332. package/dist/render-scan.js.map +1 -0
  333. package/dist/theme/cd-tokens.d.ts +66 -0
  334. package/dist/theme/cd-tokens.d.ts.map +1 -0
  335. package/dist/theme/cd-tokens.js +64 -0
  336. package/dist/theme/cd-tokens.js.map +1 -0
  337. package/dist/theme/create-theme.d.ts +214 -39
  338. package/dist/theme/create-theme.d.ts.map +1 -1
  339. package/dist/theme/create-theme.js +204 -13
  340. package/dist/theme/create-theme.js.map +1 -1
  341. package/dist/theme/fonts.d.ts +38 -0
  342. package/dist/theme/fonts.d.ts.map +1 -0
  343. package/dist/theme/fonts.js +56 -0
  344. package/dist/theme/fonts.js.map +1 -0
  345. package/dist/theme/host-fonts.d.ts +39 -0
  346. package/dist/theme/host-fonts.d.ts.map +1 -0
  347. package/dist/theme/host-fonts.js +44 -0
  348. package/dist/theme/host-fonts.js.map +1 -0
  349. package/dist/theme/index.d.ts +8 -1
  350. package/dist/theme/index.d.ts.map +1 -1
  351. package/dist/theme/index.js +5 -1
  352. package/dist/theme/index.js.map +1 -1
  353. package/dist/theme/portal-container.d.ts +16 -0
  354. package/dist/theme/portal-container.d.ts.map +1 -0
  355. package/dist/theme/portal-container.js +19 -0
  356. package/dist/theme/portal-container.js.map +1 -0
  357. package/dist/theme/presets.d.ts +24 -9
  358. package/dist/theme/presets.d.ts.map +1 -1
  359. package/dist/theme/presets.js +44 -11
  360. package/dist/theme/presets.js.map +1 -1
  361. package/dist/theme/theme-provider.d.ts +27 -9
  362. package/dist/theme/theme-provider.d.ts.map +1 -1
  363. package/dist/theme/theme-provider.js +42 -20
  364. package/dist/theme/theme-provider.js.map +1 -1
  365. package/package.json +11 -5
  366. package/src/app/adapt-data-widget.tsx +2 -42
  367. package/src/app/builder/BuilderBanners.tsx +26 -13
  368. package/src/app/builder/CanvasRow.tsx +79 -32
  369. package/src/app/builder/CatalogueSheet.tsx +58 -35
  370. package/src/app/builder/CellPropsSheet.tsx +17 -2
  371. package/src/app/builder/DiscardDialog.tsx +59 -0
  372. package/src/app/builder/EmptyHint.tsx +16 -11
  373. package/src/app/builder/LayoutSurface.tsx +207 -67
  374. package/src/app/builder/PipelineStrip.tsx +265 -156
  375. package/src/app/builder/SaveDialog.tsx +25 -14
  376. package/src/app/builder/Toolbar.tsx +154 -57
  377. package/src/app/builder/Workspace.tsx +226 -56
  378. package/src/app/builder/builder-feedback.test.tsx +75 -0
  379. package/src/app/builder/builder-model.test.ts +5 -0
  380. package/src/app/builder/builder-model.ts +346 -6
  381. package/src/app/builder/builder-model.ux.test.ts +456 -0
  382. package/src/app/builder/builder-ux.test.tsx +844 -0
  383. package/src/app/builder/labels.ts +15 -121
  384. package/src/app/builder/reveal.ts +10 -0
  385. package/src/app/builder/status-chip.tsx +8 -4
  386. package/src/app/builder/use-builder-catalogue.ts +62 -19
  387. package/src/app/builder/use-builder-session.ts +68 -0
  388. package/src/app/builder/use-display-resolver.ts +24 -0
  389. package/src/app/builder/use-save-dashboard.ts +5 -0
  390. package/src/app/index.ts +20 -3
  391. package/src/app/layout-builder.tsx +193 -186
  392. package/src/app/mcp-app-view-chrome.test.tsx +487 -14
  393. package/src/app/mcp-app-view-chrome.tsx +328 -100
  394. package/src/app/mcp-app-view-states.tsx +140 -0
  395. package/src/app/mcp-app-view.effects.test.tsx +292 -0
  396. package/src/app/mcp-app-view.test.tsx +478 -0
  397. package/src/app/mcp-app-view.tsx +447 -190
  398. package/src/app/mcp-toolkit-app.test.tsx +40 -0
  399. package/src/app/mcp-toolkit-app.tsx +39 -8
  400. package/src/app/mcp-use-host-bridge.test.tsx +180 -0
  401. package/src/app/mcp-use-host-bridge.tsx +67 -24
  402. package/src/app/model-context.test.tsx +116 -0
  403. package/src/app/model-context.tsx +125 -0
  404. package/src/app/use-build-mode-focus.ts +62 -0
  405. package/src/app/use-view-actions.ts +89 -0
  406. package/src/app/use-view-lifecycle.ts +122 -0
  407. package/src/app/view-lifecycle.test.ts +443 -0
  408. package/src/app/view-lifecycle.ts +334 -0
  409. package/src/app/widget-fixture.effects.test.tsx +51 -0
  410. package/src/app/widget-fixture.render.test.tsx +81 -0
  411. package/src/app/widget-fixture.tsx +34 -11
  412. package/src/app/widget-props-sheet.tsx +67 -39
  413. package/src/app/widget-renderer.render.test.tsx +131 -0
  414. package/src/app/widget-renderer.tsx +136 -38
  415. package/src/components/DrillButton.tsx +24 -10
  416. package/src/components/EmptyState.tsx +83 -0
  417. package/src/components/ErrorState.tsx +204 -0
  418. package/src/components/FilterBar.test.tsx +82 -0
  419. package/src/components/FilterBar.tsx +56 -27
  420. package/src/components/GroupCard.tsx +3 -2
  421. package/src/components/KpiGrid.test.tsx +279 -0
  422. package/src/components/KpiGrid.tsx +329 -91
  423. package/src/components/ListFooter.test.tsx +116 -0
  424. package/src/components/ListFooter.tsx +110 -12
  425. package/src/components/PagedListFooter.test.tsx +140 -0
  426. package/src/components/PagedListFooter.tsx +107 -0
  427. package/src/components/Pills.test.tsx +77 -0
  428. package/src/components/Pills.tsx +78 -16
  429. package/src/components/SectionHeading.tsx +9 -3
  430. package/src/components/Toast.tsx +206 -0
  431. package/src/components/WidgetHeader.test.tsx +78 -0
  432. package/src/components/WidgetHeader.tsx +63 -16
  433. package/src/components/feedback-states.test.tsx +314 -0
  434. package/src/components/tone-figures.test.tsx +58 -0
  435. package/src/fonts.css +19 -0
  436. package/src/globals.css +9 -165
  437. package/src/hooks/index.ts +6 -3
  438. package/src/hooks/tool-result-recovery-model.test.ts +744 -0
  439. package/src/hooks/tool-result-recovery-model.ts +418 -0
  440. package/src/hooks/use-tool-query.effects.test.tsx +205 -0
  441. package/src/hooks/use-tool-query.ts +156 -33
  442. package/src/hooks/use-tool-result-recovery.effects.test.tsx +141 -0
  443. package/src/hooks/use-tool-result-recovery.test.ts +28 -5
  444. package/src/hooks/use-tool-result-recovery.ts +101 -131
  445. package/src/hooks/use-view-data.test.tsx +169 -0
  446. package/src/hooks/use-view-data.ts +131 -10
  447. package/src/hooks/use-view-tool-query.ts +25 -19
  448. package/src/i18n-coverage.test.tsx +425 -0
  449. package/src/index.ts +102 -13
  450. package/src/lib/focus-return.test.ts +288 -0
  451. package/src/lib/focus-return.ts +96 -0
  452. package/src/lib/focus-ring.ts +20 -0
  453. package/src/lib/icon-stroke.test.ts +59 -0
  454. package/src/lib/icon-stroke.ts +13 -0
  455. package/src/lib/parse-tool-result.test.ts +46 -1
  456. package/src/lib/parse-tool-result.ts +40 -2
  457. package/src/lib/toast-model.test.ts +233 -0
  458. package/src/lib/toast-model.ts +165 -0
  459. package/src/lib/tone-utils.test.ts +125 -16
  460. package/src/lib/tone-utils.ts +144 -15
  461. package/src/lib/tool-query.test.ts +569 -0
  462. package/src/lib/tool-query.ts +253 -0
  463. package/src/lib/tool-result-observer.test.ts +296 -0
  464. package/src/lib/tool-result-observer.ts +211 -0
  465. package/src/lib/typography.test.ts +45 -0
  466. package/src/lib/typography.ts +47 -0
  467. package/src/lib/utils.ts +12 -1
  468. package/src/locale/index.ts +14 -1
  469. package/src/locale/locale-provider.test.tsx +87 -0
  470. package/src/locale/locale-provider.tsx +90 -3
  471. package/src/locale/message-types.ts +337 -0
  472. package/src/locale/messages-de.ts +198 -0
  473. package/src/locale/messages-en.ts +196 -0
  474. package/src/locale/toolkit-messages.test.ts +349 -0
  475. package/src/locale/toolkit-messages.ts +143 -0
  476. package/src/no-hardcoded-copy.test.ts +432 -0
  477. package/src/primitives/alert.tsx +7 -2
  478. package/src/primitives/badge.tsx +19 -7
  479. package/src/primitives/button.tsx +13 -8
  480. package/src/primitives/card.tsx +4 -4
  481. package/src/primitives/dialog.tsx +42 -8
  482. package/src/primitives/input.tsx +5 -3
  483. package/src/primitives/scroll-area.tsx +1 -1
  484. package/src/primitives/select.tsx +7 -4
  485. package/src/primitives/sheet.tsx +33 -6
  486. package/src/primitives/skeleton.tsx +1 -1
  487. package/src/primitives/switch.tsx +4 -2
  488. package/src/primitives/tabs.tsx +9 -6
  489. package/src/providers/query-provider.test.tsx +154 -0
  490. package/src/providers/query-provider.tsx +66 -5
  491. package/src/render-cases-primitives.tsx +1 -1
  492. package/src/render-cases.tsx +82 -6
  493. package/src/render-effects.tsx +96 -0
  494. package/src/render-scan.ts +75 -0
  495. package/src/theme/cd-tokens.generated.css +48 -0
  496. package/src/theme/cd-tokens.test.ts +109 -0
  497. package/src/theme/cd-tokens.ts +67 -0
  498. package/src/theme/create-theme.test.ts +253 -5
  499. package/src/theme/create-theme.ts +341 -41
  500. package/src/theme/fonts.test.ts +85 -0
  501. package/src/theme/fonts.ts +72 -0
  502. package/src/theme/host-fonts.test.ts +135 -0
  503. package/src/theme/host-fonts.ts +52 -0
  504. package/src/theme/index.ts +15 -1
  505. package/src/theme/portal-container.ts +20 -0
  506. package/src/theme/presets.ts +44 -11
  507. package/src/theme/theme-css.test.ts +619 -0
  508. package/src/theme/theme-provider.test.tsx +82 -0
  509. package/src/theme/theme-provider.tsx +60 -24
  510. package/src/theme.css +344 -0
  511. package/ui-catalog.json +783 -49
@@ -16,6 +16,10 @@ import {
16
16
  SheetTitle,
17
17
  } from "../primitives/sheet.js"
18
18
  import { Switch } from "../primitives/switch.js"
19
+ import { useToolkitMessages } from "../locale/locale-provider.js"
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"
19
23
 
20
24
  /**
21
25
  * Tiny JSON-Schema-driven editor for a layout cell's `props`. The
@@ -29,6 +33,12 @@ import { Switch } from "../primitives/switch.js"
29
33
  * - Nested objects/arrays/$ref/anyOf are not supported — the field
30
34
  * shows an "edit JSON manually" hint so authors know to fall back.
31
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
+ *
32
42
  * The sheet writes a clean `Record<string, unknown>` (empty values
33
43
  * stripped) and reports `undefined` when the user clears every field, so
34
44
  * the cell's `props` field stays absent rather than `{}` — matches the
@@ -37,6 +47,7 @@ import { Switch } from "../primitives/switch.js"
37
47
 
38
48
  interface JsonSchemaProperty {
39
49
  type?: string
50
+ title?: string
40
51
  description?: string
41
52
  enum?: unknown[]
42
53
  default?: unknown
@@ -48,25 +59,8 @@ interface JsonSchemaObject {
48
59
  required?: string[]
49
60
  }
50
61
 
51
- export interface WidgetPropsSheetLabels {
52
- title?: string
53
- description?: string
54
- apply?: string
55
- cancel?: string
56
- reset?: string
57
- noProps?: string
58
- unsetOption?: string
59
- }
60
-
61
- const DEFAULT_LABELS: Required<WidgetPropsSheetLabels> = {
62
- title: "Configure widget",
63
- description: "Set per-instance props for this widget.",
64
- apply: "Apply",
65
- cancel: "Cancel",
66
- reset: "Reset",
67
- noProps: "This widget takes no per-instance props.",
68
- unsetOption: "(unset)",
69
- }
62
+ /** Override strings of the sheet; each defaults to the locale's `propsSheet` catalog. */
63
+ export type WidgetPropsSheetLabels = Partial<PropsSheetMessages>
70
64
 
71
65
  const UNSET_SENTINEL = "__unset__"
72
66
 
@@ -74,6 +68,8 @@ export interface WidgetPropsSheetProps {
74
68
  open: boolean
75
69
  onOpenChange: (open: boolean) => void
76
70
  widgetId: string
71
+ /** The widget's human name (its title); the id then shows as a quiet hint under it. */
72
+ widgetTitle?: string
77
73
  schema: Record<string, unknown> | undefined
78
74
  value: Record<string, unknown> | undefined
79
75
  onApply: (next: Record<string, unknown> | undefined) => void
@@ -84,12 +80,15 @@ export function WidgetPropsSheet({
84
80
  open,
85
81
  onOpenChange,
86
82
  widgetId,
83
+ widgetTitle,
87
84
  schema,
88
85
  value,
89
86
  onApply,
90
87
  labels,
91
88
  }: WidgetPropsSheetProps) {
92
- const L = { ...DEFAULT_LABELS, ...labels }
89
+ const messages = useToolkitMessages()
90
+ const { locale, resolveKey } = useDisplayResolver()
91
+ const L = { ...messages.propsSheet, ...labels }
93
92
  const obj = (schema ?? {}) as JsonSchemaObject
94
93
  const [draft, setDraft] = useState<Record<string, unknown>>(value ?? {})
95
94
 
@@ -121,9 +120,10 @@ export function WidgetPropsSheet({
121
120
  <SheetContent className="w-[400px] sm:max-w-md">
122
121
  <SheetHeader>
123
122
  <SheetTitle>{L.title}</SheetTitle>
124
- <SheetDescription>
125
- <span className="bg-muted rounded px-1 font-mono text-xs">{widgetId}</span> —{" "}
126
- {L.description}
123
+ <SheetDescription className="flex flex-col items-start gap-1">
124
+ <span>{L.description}</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>
127
127
  </SheetDescription>
128
128
  </SheetHeader>
129
129
  <div className="flex flex-col gap-4 px-4 py-4">
@@ -134,9 +134,11 @@ export function WidgetPropsSheet({
134
134
  <PropertyField
135
135
  key={key}
136
136
  name={key}
137
+ text={propFieldText(key, prop as Record<string, unknown>, locale, resolveKey)}
137
138
  prop={prop}
138
139
  value={draft[key]}
139
140
  unsetLabel={L.unsetOption}
141
+ unsupportedLabel={L.unsupportedType}
140
142
  onChange={(v) =>
141
143
  setDraft((prev) => {
142
144
  if (v === undefined) {
@@ -169,15 +171,19 @@ export function WidgetPropsSheet({
169
171
 
170
172
  function PropertyField({
171
173
  name,
174
+ text,
172
175
  prop,
173
176
  value,
174
177
  unsetLabel,
178
+ unsupportedLabel,
175
179
  onChange,
176
180
  }: {
177
181
  name: string
182
+ text: PropFieldText
178
183
  prop: JsonSchemaProperty
179
184
  value: unknown
180
185
  unsetLabel: string
186
+ unsupportedLabel: (type: string) => string
181
187
  onChange: (v: unknown) => void
182
188
  }) {
183
189
  const id = `prop-${name}`
@@ -186,12 +192,12 @@ function PropertyField({
186
192
  const options = prop.enum.filter((v): v is string => typeof v === "string")
187
193
  const current = typeof value === "string" && value !== "" ? value : UNSET_SENTINEL
188
194
  return (
189
- <FieldShell id={id} name={name} description={prop.description}>
195
+ <FieldShell id={id} text={text}>
190
196
  <Select
191
197
  value={current}
192
198
  onValueChange={(v) => onChange(v === UNSET_SENTINEL ? undefined : v)}
193
199
  >
194
- <SelectTrigger id={id}>
200
+ <SelectTrigger id={id} aria-describedby={helpId(id, text)}>
195
201
  <SelectValue placeholder={unsetLabel} />
196
202
  </SelectTrigger>
197
203
  <SelectContent>
@@ -210,9 +216,10 @@ function PropertyField({
210
216
  if (prop.type === "string") {
211
217
  const current = typeof value === "string" ? value : ""
212
218
  return (
213
- <FieldShell id={id} name={name} description={prop.description}>
219
+ <FieldShell id={id} text={text}>
214
220
  <Input
215
221
  id={id}
222
+ aria-describedby={helpId(id, text)}
216
223
  value={current}
217
224
  onChange={(e) => onChange(e.target.value === "" ? undefined : e.target.value)}
218
225
  />
@@ -223,9 +230,10 @@ function PropertyField({
223
230
  if (prop.type === "number" || prop.type === "integer") {
224
231
  const current = typeof value === "number" ? String(value) : ""
225
232
  return (
226
- <FieldShell id={id} name={name} description={prop.description}>
233
+ <FieldShell id={id} text={text}>
227
234
  <Input
228
235
  id={id}
236
+ aria-describedby={helpId(id, text)}
229
237
  type="number"
230
238
  value={current}
231
239
  onChange={(e) => {
@@ -243,9 +251,10 @@ function PropertyField({
243
251
  if (prop.type === "boolean") {
244
252
  const current = value === true
245
253
  return (
246
- <FieldShell id={id} name={name} description={prop.description}>
254
+ <FieldShell id={id} text={text}>
247
255
  <Switch
248
256
  id={id}
257
+ aria-describedby={helpId(id, text)}
249
258
  checked={current}
250
259
  onCheckedChange={(checked) => onChange(checked || undefined)}
251
260
  />
@@ -254,32 +263,51 @@ function PropertyField({
254
263
  }
255
264
 
256
265
  return (
257
- <FieldShell id={id} name={name} description={prop.description}>
258
- <p className="text-muted-foreground text-xs italic">
259
- Unsupported type "{prop.type ?? "(none)"}" — edit raw JSON to set this field.
266
+ <FieldShell id={id} text={text}>
267
+ <p className="text-muted-foreground text-small italic">
268
+ {unsupportedLabel(prop.type ?? "?")}
260
269
  </p>
261
270
  </FieldShell>
262
271
  )
263
272
  }
264
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
+ */
265
284
  function FieldShell({
266
285
  id,
267
- name,
268
- description,
286
+ text,
269
287
  children,
270
288
  }: {
271
289
  id: string
272
- name: string
273
- description?: string
290
+ text: PropFieldText
274
291
  children: ReactNode
275
292
  }) {
276
293
  return (
277
294
  <div className="flex flex-col gap-1.5">
278
- <label htmlFor={id} className="text-sm font-medium">
279
- {name}
280
- </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>
281
305
  {children}
282
- {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}
283
311
  </div>
284
312
  )
285
313
  }
@@ -0,0 +1,131 @@
1
+ import { renderToStaticMarkup } from "react-dom/server"
2
+ import { describe, expect, it, vi } from "vitest"
3
+ import type { WidgetProps } from "@miragon/mcp-toolkit-core"
4
+ import { LocaleProvider } from "../locale/locale-provider.js"
5
+ import { TOOLKIT_MESSAGES } from "../locale/toolkit-messages.js"
6
+ import { decodeEntities } from "../render-scan.js"
7
+ import {
8
+ buildPipelineContext,
9
+ RenderedWidget,
10
+ resetKeysChanged,
11
+ WidgetCrashFallback,
12
+ WidgetErrorBoundary,
13
+ WidgetRenderer,
14
+ } from "./widget-renderer.js"
15
+
16
+ const de = TOOLKIT_MESSAGES.de.renderer
17
+
18
+ function Hello({ keys }: WidgetProps) {
19
+ return <p>hello {typeof keys.who === "string" ? keys.who : ""}</p>
20
+ }
21
+
22
+ describe("WidgetRenderer fallbacks (toolkit#176 N24, U19)", () => {
23
+ it("an unknown widget id renders a localized 'not available' note instead of vanishing", () => {
24
+ const html = renderToStaticMarkup(
25
+ <LocaleProvider locale="de">
26
+ <WidgetRenderer
27
+ layout={{ rows: [{ row: [{ widget: "demo:hello" }, { widget: "demo:gone" }] }] }}
28
+ keys={{ who: "ada" }}
29
+ errors={[]}
30
+ widgets={{ "demo:hello": Hello }}
31
+ />
32
+ </LocaleProvider>,
33
+ )
34
+ expect(html).toContain("hello ada")
35
+ expect(decodeEntities(html)).toContain(de.widgetUnavailable)
36
+ expect(html).toContain('data-widget-id="demo:gone"')
37
+ })
38
+
39
+ it("the crash fallback names what happened, keeps the raw error under Details, offers a retry", () => {
40
+ const html = decodeEntities(
41
+ renderToStaticMarkup(
42
+ <LocaleProvider locale="de">
43
+ <WidgetCrashFallback widgetId="demo:x" error={new Error("kaputt")} onRetry={() => {}} />
44
+ </LocaleProvider>,
45
+ ),
46
+ )
47
+ expect(html).toContain('role="alert"')
48
+ expect(html).toContain(de.widgetCrashed)
49
+ expect(html).toContain("demo:x: kaputt")
50
+ expect(html).toContain(`${TOOLKIT_MESSAGES.de.common.retry}</button>`)
51
+ })
52
+
53
+ it("the builder's empty-context hint is localized", () => {
54
+ const html = renderToStaticMarkup(
55
+ <LocaleProvider locale="de">
56
+ <RenderedWidget
57
+ widget={Hello}
58
+ widgetId="demo:hello"
59
+ props={{ keys: {}, context: { steps: {}, keys: {}, errors: [] } }}
60
+ showEmptyHint
61
+ />
62
+ </LocaleProvider>,
63
+ )
64
+ expect(html).toContain(TOOLKIT_MESSAGES.de.renderer.noDataYet)
65
+ })
66
+ })
67
+
68
+ describe("WidgetErrorBoundary reset", () => {
69
+ it("resets a latched crash when its reset keys change, not on an unrelated update", () => {
70
+ const keysA = [{ a: 1 }]
71
+ const boundary = new WidgetErrorBoundary({ widgetId: "w", children: null, resetKeys: keysA })
72
+ const setState = vi.fn()
73
+ boundary.setState = setState
74
+ boundary.state = { error: new Error("boom") }
75
+
76
+ // Same keys (identity) → stays failed.
77
+ boundary.componentDidUpdate({ resetKeys: keysA })
78
+ expect(setState).not.toHaveBeenCalled()
79
+
80
+ // New data → a fresh render.
81
+ boundary.componentDidUpdate({ resetKeys: [{ a: 1 }] })
82
+ expect(setState).toHaveBeenCalledWith({ error: null })
83
+ })
84
+
85
+ it("does nothing while healthy", () => {
86
+ const boundary = new WidgetErrorBoundary({ widgetId: "w", children: null, resetKeys: [1] })
87
+ const setState = vi.fn()
88
+ boundary.setState = setState
89
+ boundary.componentDidUpdate({ resetKeys: [2] })
90
+ expect(setState).not.toHaveBeenCalled()
91
+ })
92
+
93
+ it("its retry clears the error", () => {
94
+ const boundary = new WidgetErrorBoundary({ widgetId: "w", children: null })
95
+ const setState = vi.fn()
96
+ boundary.setState = setState
97
+ boundary.reset()
98
+ expect(setState).toHaveBeenCalledWith({ error: null })
99
+ })
100
+ })
101
+
102
+ describe("resetKeysChanged", () => {
103
+ it("compares by length and entry identity", () => {
104
+ const a = { x: 1 }
105
+ expect(resetKeysChanged([a], [a])).toBe(false)
106
+ expect(resetKeysChanged(undefined, undefined)).toBe(false)
107
+ const same = [a]
108
+ expect(resetKeysChanged(same, same)).toBe(false)
109
+ expect(resetKeysChanged([a], [{ x: 1 }])).toBe(true)
110
+ expect(resetKeysChanged([a], [a, a])).toBe(true)
111
+ expect(resetKeysChanged(undefined, [a])).toBe(true)
112
+ expect(resetKeysChanged([a], undefined)).toBe(true)
113
+ expect(resetKeysChanged([Number.NaN], [Number.NaN])).toBe(false)
114
+ })
115
+ })
116
+
117
+ describe("buildPipelineContext", () => {
118
+ it("tags each step with its id and passes keys and errors through", () => {
119
+ const keys = { k: 1 }
120
+ const errors = [{ stepId: "s2", reason: "x" }]
121
+ const ctx = buildPipelineContext(
122
+ keys,
123
+ { s1: { data: 1, keys: {}, _app: "a", _dataType: "a:t" } },
124
+ errors,
125
+ )
126
+ expect(ctx.steps.s1).toEqual({ data: 1, keys: {}, _app: "a", _dataType: "a:t", _step: "s1" })
127
+ expect(ctx.keys).toBe(keys)
128
+ expect(ctx.errors).toBe(errors)
129
+ expect(buildPipelineContext(keys, undefined, []).steps).toEqual({})
130
+ })
131
+ })
@@ -1,46 +1,136 @@
1
- import { Component, type ComponentType, type ReactNode, type ErrorInfo } from "react"
1
+ import {
2
+ Component,
3
+ createRef,
4
+ useMemo,
5
+ type ComponentType,
6
+ type ReactNode,
7
+ type ErrorInfo,
8
+ } from "react"
2
9
  import type { LayoutConfig, PipelineContext, RowDef, WidgetProps } from "@miragon/mcp-toolkit-core"
3
10
  import { normalizeLayout } from "@miragon/mcp-toolkit-core"
11
+ import { ErrorState } from "../components/ErrorState.js"
4
12
  import { GridLayout, GridItem } from "../components/GridLayout.js"
5
- import { Card, CardContent } from "../primitives/card.js"
13
+ import { useToolkitMessages } from "../locale/locale-provider.js"
6
14
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "../primitives/tabs.js"
7
15
 
8
16
  export type WidgetComponent = ComponentType<WidgetProps>
9
17
 
18
+ /**
19
+ * True when two reset-key lists differ (length or any entry by identity).
20
+ * Exported for tests: it decides when a crashed widget gets a fresh render.
21
+ */
22
+ export function resetKeysChanged(
23
+ prev: readonly unknown[] | undefined,
24
+ next: readonly unknown[] | undefined,
25
+ ): boolean {
26
+ if (prev === next) return false
27
+ if (!prev || !next || prev.length !== next.length) return true
28
+ return prev.some((value, idx) => !Object.is(value, next[idx]))
29
+ }
30
+
10
31
  /**
11
32
  * Catches render-time crashes from a single widget so one broken widget
12
33
  * can't take down the entire iframe view (or the builder canvas/preview).
13
34
  *
35
+ * The fallback is localized, names what happened, keeps the raw error under
36
+ * "Details" and offers a retry. The boundary resets itself when its
37
+ * `resetKeys` change, so new data (a refresh, a new tool result, a builder
38
+ * edit) gets a fresh render instead of a latched failure (toolkit#176 N24).
39
+ * When a retry crashes again, keyboard focus moves to the new retry button
40
+ * instead of falling back to the document (the pressed one unmounted). After
41
+ * a retry that renders, the widget's own markup is unknown here, so focus
42
+ * returns to the document.
43
+ *
14
44
  * Exported so the visual builder's canvas + preview render sites reuse the
15
45
  * same isolation the rendered `WidgetRenderer` already had — every place a
16
46
  * widget component is rendered goes through {@link RenderedWidget}, which
17
47
  * wraps the component in this boundary.
18
48
  */
19
49
  export class WidgetErrorBoundary extends Component<
20
- { widgetId: string; children: ReactNode },
50
+ { widgetId: string; children: ReactNode; resetKeys?: readonly unknown[] },
21
51
  { error: Error | null }
22
52
  > {
23
53
  override state: { error: Error | null } = { error: null }
54
+ /** Set by the user's retry; consumed by the next update. */
55
+ private retryFocusOwed = false
56
+ private readonly fallbackRef = createRef<HTMLDivElement>()
24
57
  static getDerivedStateFromError(error: Error) {
25
58
  return { error }
26
59
  }
27
60
  override componentDidCatch(error: Error, info: ErrorInfo) {
28
61
  console.error(`Widget ${this.props.widgetId} crashed:`, error, info)
29
62
  }
63
+ override componentDidUpdate(prevProps: { resetKeys?: readonly unknown[] }) {
64
+ if (this.retryFocusOwed) {
65
+ this.retryFocusOwed = false
66
+ if (this.state.error) this.fallbackRef.current?.querySelector<HTMLElement>("button")?.focus()
67
+ }
68
+ if (this.state.error && resetKeysChanged(prevProps.resetKeys, this.props.resetKeys)) {
69
+ this.reset()
70
+ }
71
+ }
72
+ reset = () => {
73
+ this.setState({ error: null })
74
+ }
75
+ /** The fallback's retry: a reset that keeps keyboard focus if the widget crashes again. */
76
+ retry = () => {
77
+ this.retryFocusOwed = true
78
+ this.reset()
79
+ }
30
80
  override render() {
31
81
  if (this.state.error) {
32
82
  return (
33
- <Card>
34
- <CardContent className="text-muted-foreground py-3 text-sm">
35
- Widget "{this.props.widgetId}" failed to render.
36
- </CardContent>
37
- </Card>
83
+ <div ref={this.fallbackRef} data-slot="widget-crash">
84
+ <WidgetCrashFallback
85
+ widgetId={this.props.widgetId}
86
+ error={this.state.error}
87
+ onRetry={this.retry}
88
+ />
89
+ </div>
38
90
  )
39
91
  }
40
92
  return this.props.children
41
93
  }
42
94
  }
43
95
 
96
+ /** The localized crash fallback of {@link WidgetErrorBoundary}. */
97
+ export function WidgetCrashFallback({
98
+ widgetId,
99
+ error,
100
+ onRetry,
101
+ }: {
102
+ widgetId: string
103
+ error: Error
104
+ onRetry: () => void
105
+ }) {
106
+ const messages = useToolkitMessages()
107
+ return (
108
+ <ErrorState
109
+ variant="inline"
110
+ title={messages.renderer.widgetCrashed}
111
+ details={`${widgetId}: ${error.message}`}
112
+ onRetry={onRetry}
113
+ />
114
+ )
115
+ }
116
+
117
+ /** A layout cell names a widget the bundle does not contain: say so, quietly. */
118
+ export function UnavailableWidget({
119
+ widgetId,
120
+ className = "text-muted-foreground rounded-md border border-dashed p-4 text-sm",
121
+ }: {
122
+ widgetId: string
123
+ className?: string
124
+ }) {
125
+ const messages = useToolkitMessages()
126
+ return (
127
+ <div data-slot="widget-unavailable" data-widget-id={widgetId} className={className}>
128
+ <p>{messages.renderer.widgetUnavailable}</p>
129
+ <p className="text-small mt-1 font-mono break-all">{widgetId}</p>
130
+ </div>
131
+ )
132
+ }
133
+
44
134
  /**
45
135
  * True when the pipeline context carries no widget data at all — no step
46
136
  * results and no keys. A widget rendered against an empty context typically
@@ -59,7 +149,7 @@ export function contextHasNoData(context: PipelineContext): boolean {
59
149
  * The single render site for a layout cell's widget, shared across the
60
150
  * rendered view (`RowsRenderer`), the builder canvas, and the builder
61
151
  * preview so all three isolate crashes identically:
62
- * - missing component → "not bundled" fallback,
152
+ * - missing component → a localized "not available in this version" note,
63
153
  * - render crash → caught by {@link WidgetErrorBoundary} (Finding [1]),
64
154
  * - (builder only, opt-in) empty pipeline context → a dezent
65
155
  * "no data yet" hint (Finding [4]).
@@ -79,7 +169,7 @@ export function RenderedWidget({
79
169
  props,
80
170
  cellProps,
81
171
  showEmptyHint = false,
82
- notBundledClassName = "text-muted-foreground rounded border border-dashed p-4 text-center text-xs",
172
+ notBundledClassName,
83
173
  }: {
84
174
  widget: WidgetComponent | undefined
85
175
  widgetId: string
@@ -88,23 +178,26 @@ export function RenderedWidget({
88
178
  showEmptyHint?: boolean
89
179
  notBundledClassName?: string
90
180
  }) {
91
- if (!Widget) {
92
- return <div className={notBundledClassName}>Widget "{widgetId}" not bundled.</div>
93
- }
181
+ if (!Widget) return <UnavailableWidget widgetId={widgetId} className={notBundledClassName} />
94
182
  if (showEmptyHint && contextHasNoData(props.context)) {
95
- return (
96
- <div className={notBundledClassName}>
97
- No data for this widget yet — it appears after a refresh.
98
- </div>
99
- )
183
+ return <NoDataYet className={notBundledClassName} />
100
184
  }
101
185
  return (
102
- <WidgetErrorBoundary widgetId={widgetId}>
186
+ <WidgetErrorBoundary widgetId={widgetId} resetKeys={[props.context, props.keys, cellProps]}>
103
187
  <Widget {...props} widgetProps={cellProps} />
104
188
  </WidgetErrorBoundary>
105
189
  )
106
190
  }
107
191
 
192
+ function NoDataYet({
193
+ className = "text-muted-foreground rounded-md border border-dashed p-4 text-sm",
194
+ }: {
195
+ className?: string
196
+ }) {
197
+ const messages = useToolkitMessages()
198
+ return <div className={className}>{messages.renderer.noDataYet}</div>
199
+ }
200
+
108
201
  export interface WidgetRendererProps {
109
202
  layout: LayoutConfig
110
203
  keys: Record<string, unknown>
@@ -137,14 +230,11 @@ function RowsRenderer({
137
230
  <GridLayout key={rowIdx}>
138
231
  {row.row.map((cell, cellIdx) => {
139
232
  const WidgetComponent = widgets[cell.widget]
140
- // Keep the rendered view's original behaviour of skipping cells
141
- // whose component isn't bundled (no placeholder in the shipped
142
- // view), while still routing the resolvable ones through the
143
- // shared `RenderedWidget` for error-boundary + empty-state.
144
- if (!WidgetComponent) return null
145
- // Use a composite key so the same widget can appear in multiple
146
- // cells of one row (e.g. side-by-side scoped dashboards) without
147
- // colliding on React's reconciliation key.
233
+ // An unknown widget id renders the localized "not available"
234
+ // note in its cell instead of vanishing (an empty page reads as
235
+ // broken). The composite key lets the same widget appear in
236
+ // several cells of one row (e.g. side-by-side scoped dashboards)
237
+ // without colliding on React's reconciliation key.
148
238
  return (
149
239
  <GridItem key={`${cell.widget}-${cellIdx}`} span={cell.span ?? 12}>
150
240
  <RenderedWidget
@@ -162,19 +252,27 @@ function RowsRenderer({
162
252
  )
163
253
  }
164
254
 
165
- export function WidgetRenderer({ layout, keys, stepData, errors, widgets }: WidgetRendererProps) {
255
+ /**
256
+ * The pipeline context widgets read. `WidgetRenderer` rebuilds it only when
257
+ * the view data changes — its identity is the error boundaries' reset key.
258
+ */
259
+ export function buildPipelineContext(
260
+ keys: Record<string, unknown>,
261
+ stepData: WidgetRendererProps["stepData"],
262
+ errors: WidgetRendererProps["errors"],
263
+ ): PipelineContext {
166
264
  const steps: Record<string, PipelineContext["steps"][string]> = {}
167
- if (stepData) {
168
- for (const [id, result] of Object.entries(stepData)) {
169
- steps[id] = { ...result, _step: id }
170
- }
265
+ for (const [id, result] of Object.entries(stepData ?? {})) {
266
+ steps[id] = { ...result, _step: id }
171
267
  }
172
- const pipelineContext: PipelineContext = {
173
- steps,
174
- keys,
175
- errors,
176
- }
177
- const widgetProps: WidgetProps = { keys, context: pipelineContext }
268
+ return { steps, keys, errors }
269
+ }
270
+
271
+ export function WidgetRenderer({ layout, keys, stepData, errors, widgets }: WidgetRendererProps) {
272
+ const widgetProps = useMemo<WidgetProps>(
273
+ () => ({ keys, context: buildPipelineContext(keys, stepData, errors) }),
274
+ [keys, stepData, errors],
275
+ )
178
276
  const normalized = normalizeLayout(layout)
179
277
 
180
278
  if ("tabs" in normalized) {