@miragon/mcp-toolkit-ui 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/README.md +36 -0
  2. package/dist/app/adapt-data-widget.d.ts +33 -0
  3. package/dist/app/adapt-data-widget.d.ts.map +1 -0
  4. package/dist/app/adapt-data-widget.js +35 -0
  5. package/dist/app/adapt-data-widget.js.map +1 -0
  6. package/dist/app/builder/CatalogueSheet.d.ts +13 -0
  7. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -0
  8. package/dist/app/builder/CatalogueSheet.js +33 -0
  9. package/dist/app/builder/CatalogueSheet.js.map +1 -0
  10. package/dist/app/builder/PipelineStrip.d.ts +22 -0
  11. package/dist/app/builder/PipelineStrip.d.ts.map +1 -0
  12. package/dist/app/builder/PipelineStrip.js +31 -0
  13. package/dist/app/builder/PipelineStrip.js.map +1 -0
  14. package/dist/app/builder/PreviewPane.d.ts +13 -0
  15. package/dist/app/builder/PreviewPane.d.ts.map +1 -0
  16. package/dist/app/builder/PreviewPane.js +21 -0
  17. package/dist/app/builder/PreviewPane.js.map +1 -0
  18. package/dist/app/builder/SaveDialog.d.ts +14 -0
  19. package/dist/app/builder/SaveDialog.d.ts.map +1 -0
  20. package/dist/app/builder/SaveDialog.js +14 -0
  21. package/dist/app/builder/SaveDialog.js.map +1 -0
  22. package/dist/app/builder/Toolbar.d.ts +17 -0
  23. package/dist/app/builder/Toolbar.d.ts.map +1 -0
  24. package/dist/app/builder/Toolbar.js +15 -0
  25. package/dist/app/builder/Toolbar.js.map +1 -0
  26. package/dist/app/builder/Workspace.d.ts +28 -0
  27. package/dist/app/builder/Workspace.d.ts.map +1 -0
  28. package/dist/app/builder/Workspace.js +117 -0
  29. package/dist/app/builder/Workspace.js.map +1 -0
  30. package/dist/app/builder/builder-model.d.ts +86 -0
  31. package/dist/app/builder/builder-model.d.ts.map +1 -0
  32. package/dist/app/builder/builder-model.js +174 -0
  33. package/dist/app/builder/builder-model.js.map +1 -0
  34. package/dist/app/builder/builder-reducer.d.ts +71 -0
  35. package/dist/app/builder/builder-reducer.d.ts.map +1 -0
  36. package/dist/app/builder/builder-reducer.js +201 -0
  37. package/dist/app/builder/builder-reducer.js.map +1 -0
  38. package/dist/app/builder/labels.d.ts +78 -0
  39. package/dist/app/builder/labels.d.ts.map +1 -0
  40. package/dist/app/builder/labels.js +71 -0
  41. package/dist/app/builder/labels.js.map +1 -0
  42. package/dist/app/builder/status-chip.d.ts +10 -0
  43. package/dist/app/builder/status-chip.d.ts.map +1 -0
  44. package/dist/app/builder/status-chip.js +16 -0
  45. package/dist/app/builder/status-chip.js.map +1 -0
  46. package/dist/app/host-bridge.d.ts +211 -0
  47. package/dist/app/host-bridge.d.ts.map +1 -0
  48. package/dist/app/host-bridge.js +247 -0
  49. package/dist/app/host-bridge.js.map +1 -0
  50. package/dist/app/index.d.ts +16 -0
  51. package/dist/app/index.d.ts.map +1 -0
  52. package/dist/app/index.js +18 -0
  53. package/dist/app/index.js.map +1 -0
  54. package/dist/app/layout-builder.d.ts +48 -0
  55. package/dist/app/layout-builder.d.ts.map +1 -0
  56. package/dist/app/layout-builder.js +418 -0
  57. package/dist/app/layout-builder.js.map +1 -0
  58. package/dist/app/mcp-app-view.d.ts +43 -0
  59. package/dist/app/mcp-app-view.d.ts.map +1 -0
  60. package/dist/app/mcp-app-view.js +169 -0
  61. package/dist/app/mcp-app-view.js.map +1 -0
  62. package/dist/app/mcp-toolkit-app.d.ts +18 -0
  63. package/dist/app/mcp-toolkit-app.d.ts.map +1 -0
  64. package/dist/app/mcp-toolkit-app.js +22 -0
  65. package/dist/app/mcp-toolkit-app.js.map +1 -0
  66. package/dist/app/use-host-actions.d.ts +57 -0
  67. package/dist/app/use-host-actions.d.ts.map +1 -0
  68. package/dist/app/use-host-actions.js +42 -0
  69. package/dist/app/use-host-actions.js.map +1 -0
  70. package/dist/app/widget-fixture.d.ts +180 -0
  71. package/dist/app/widget-fixture.d.ts.map +1 -0
  72. package/dist/app/widget-fixture.js +273 -0
  73. package/dist/app/widget-fixture.js.map +1 -0
  74. package/dist/app/widget-props-sheet.d.ts +20 -0
  75. package/dist/app/widget-props-sheet.d.ts.map +1 -0
  76. package/dist/app/widget-props-sheet.js +87 -0
  77. package/dist/app/widget-props-sheet.js.map +1 -0
  78. package/dist/app/widget-renderer.d.ts +87 -0
  79. package/dist/app/widget-renderer.d.ts.map +1 -0
  80. package/dist/app/widget-renderer.js +105 -0
  81. package/dist/app/widget-renderer.js.map +1 -0
  82. package/dist/components/DrillButton.d.ts +22 -0
  83. package/dist/components/DrillButton.d.ts.map +1 -0
  84. package/dist/components/DrillButton.js +13 -0
  85. package/dist/components/DrillButton.js.map +1 -0
  86. package/dist/components/FilterBar.d.ts +28 -0
  87. package/dist/components/FilterBar.d.ts.map +1 -0
  88. package/dist/components/FilterBar.js +12 -0
  89. package/dist/components/FilterBar.js.map +1 -0
  90. package/dist/components/GridLayout.d.ts +26 -0
  91. package/dist/components/GridLayout.d.ts.map +1 -0
  92. package/dist/components/GridLayout.js +31 -0
  93. package/dist/components/GridLayout.js.map +1 -0
  94. package/dist/components/GroupCard.d.ts +21 -0
  95. package/dist/components/GroupCard.d.ts.map +1 -0
  96. package/dist/components/GroupCard.js +24 -0
  97. package/dist/components/GroupCard.js.map +1 -0
  98. package/dist/components/KpiGrid.d.ts +43 -0
  99. package/dist/components/KpiGrid.d.ts.map +1 -0
  100. package/dist/components/KpiGrid.js +35 -0
  101. package/dist/components/KpiGrid.js.map +1 -0
  102. package/dist/components/ListFooter.d.ts +22 -0
  103. package/dist/components/ListFooter.d.ts.map +1 -0
  104. package/dist/components/ListFooter.js +13 -0
  105. package/dist/components/ListFooter.js.map +1 -0
  106. package/dist/components/Pills.d.ts +25 -0
  107. package/dist/components/Pills.d.ts.map +1 -0
  108. package/dist/components/Pills.js +17 -0
  109. package/dist/components/Pills.js.map +1 -0
  110. package/dist/components/SectionHeading.d.ts +16 -0
  111. package/dist/components/SectionHeading.d.ts.map +1 -0
  112. package/dist/components/SectionHeading.js +10 -0
  113. package/dist/components/SectionHeading.js.map +1 -0
  114. package/dist/components/WidgetHeader.d.ts +21 -0
  115. package/dist/components/WidgetHeader.d.ts.map +1 -0
  116. package/dist/components/WidgetHeader.js +11 -0
  117. package/dist/components/WidgetHeader.js.map +1 -0
  118. package/dist/hooks/index.d.ts +11 -0
  119. package/dist/hooks/index.d.ts.map +1 -0
  120. package/dist/hooks/index.js +7 -0
  121. package/dist/hooks/index.js.map +1 -0
  122. package/dist/hooks/use-debounced-value.d.ts +7 -0
  123. package/dist/hooks/use-debounced-value.d.ts.map +1 -0
  124. package/dist/hooks/use-debounced-value.js +15 -0
  125. package/dist/hooks/use-debounced-value.js.map +1 -0
  126. package/dist/hooks/use-mobile.d.ts +6 -0
  127. package/dist/hooks/use-mobile.d.ts.map +1 -0
  128. package/dist/hooks/use-mobile.js +19 -0
  129. package/dist/hooks/use-mobile.js.map +1 -0
  130. package/dist/hooks/use-tool-query.d.ts +21 -0
  131. package/dist/hooks/use-tool-query.d.ts.map +1 -0
  132. package/dist/hooks/use-tool-query.js +47 -0
  133. package/dist/hooks/use-tool-query.js.map +1 -0
  134. package/dist/hooks/use-tool-result-recovery.d.ts +54 -0
  135. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -0
  136. package/dist/hooks/use-tool-result-recovery.js +93 -0
  137. package/dist/hooks/use-tool-result-recovery.js.map +1 -0
  138. package/dist/hooks/use-view-data.d.ts +19 -0
  139. package/dist/hooks/use-view-data.d.ts.map +1 -0
  140. package/dist/hooks/use-view-data.js +18 -0
  141. package/dist/hooks/use-view-data.js.map +1 -0
  142. package/dist/hooks/use-view-tool-query.d.ts +18 -0
  143. package/dist/hooks/use-view-tool-query.d.ts.map +1 -0
  144. package/dist/hooks/use-view-tool-query.js +29 -0
  145. package/dist/hooks/use-view-tool-query.js.map +1 -0
  146. package/dist/index.d.ts +47 -0
  147. package/dist/index.d.ts.map +1 -0
  148. package/dist/index.js +56 -0
  149. package/dist/index.js.map +1 -0
  150. package/dist/lib/parse-tool-result.d.ts +39 -0
  151. package/dist/lib/parse-tool-result.d.ts.map +1 -0
  152. package/dist/lib/parse-tool-result.js +66 -0
  153. package/dist/lib/parse-tool-result.js.map +1 -0
  154. package/dist/lib/tone-utils.d.ts +18 -0
  155. package/dist/lib/tone-utils.d.ts.map +1 -0
  156. package/dist/lib/tone-utils.js +25 -0
  157. package/dist/lib/tone-utils.js.map +1 -0
  158. package/dist/lib/utils.d.ts +3 -0
  159. package/dist/lib/utils.d.ts.map +1 -0
  160. package/dist/lib/utils.js +6 -0
  161. package/dist/lib/utils.js.map +1 -0
  162. package/dist/locale/index.d.ts +3 -0
  163. package/dist/locale/index.d.ts.map +1 -0
  164. package/dist/locale/index.js +7 -0
  165. package/dist/locale/index.js.map +1 -0
  166. package/dist/locale/locale-provider.d.ts +50 -0
  167. package/dist/locale/locale-provider.d.ts.map +1 -0
  168. package/dist/locale/locale-provider.js +47 -0
  169. package/dist/locale/locale-provider.js.map +1 -0
  170. package/dist/primitives/alert.d.ts +10 -0
  171. package/dist/primitives/alert.d.ts.map +1 -0
  172. package/dist/primitives/alert.js +25 -0
  173. package/dist/primitives/alert.js.map +1 -0
  174. package/dist/primitives/badge.d.ts +8 -0
  175. package/dist/primitives/badge.d.ts.map +1 -0
  176. package/dist/primitives/badge.js +34 -0
  177. package/dist/primitives/badge.js.map +1 -0
  178. package/dist/primitives/button.d.ts +11 -0
  179. package/dist/primitives/button.d.ts.map +1 -0
  180. package/dist/primitives/button.js +36 -0
  181. package/dist/primitives/button.js.map +1 -0
  182. package/dist/primitives/card.d.ts +12 -0
  183. package/dist/primitives/card.d.ts.map +1 -0
  184. package/dist/primitives/card.js +25 -0
  185. package/dist/primitives/card.js.map +1 -0
  186. package/dist/primitives/dialog.d.ts +18 -0
  187. package/dist/primitives/dialog.d.ts.map +1 -0
  188. package/dist/primitives/dialog.js +38 -0
  189. package/dist/primitives/dialog.js.map +1 -0
  190. package/dist/primitives/input.d.ts +4 -0
  191. package/dist/primitives/input.d.ts.map +1 -0
  192. package/dist/primitives/input.js +7 -0
  193. package/dist/primitives/input.js.map +1 -0
  194. package/dist/primitives/scroll-area.d.ts +6 -0
  195. package/dist/primitives/scroll-area.d.ts.map +1 -0
  196. package/dist/primitives/scroll-area.js +11 -0
  197. package/dist/primitives/scroll-area.js.map +1 -0
  198. package/dist/primitives/select.d.ts +16 -0
  199. package/dist/primitives/select.d.ts.map +1 -0
  200. package/dist/primitives/select.js +38 -0
  201. package/dist/primitives/select.js.map +1 -0
  202. package/dist/primitives/separator.d.ts +4 -0
  203. package/dist/primitives/separator.d.ts.map +1 -0
  204. package/dist/primitives/separator.js +9 -0
  205. package/dist/primitives/separator.js.map +1 -0
  206. package/dist/primitives/sheet.d.ts +15 -0
  207. package/dist/primitives/sheet.d.ts.map +1 -0
  208. package/dist/primitives/sheet.js +41 -0
  209. package/dist/primitives/sheet.js.map +1 -0
  210. package/dist/primitives/skeleton.d.ts +3 -0
  211. package/dist/primitives/skeleton.d.ts.map +1 -0
  212. package/dist/primitives/skeleton.js +7 -0
  213. package/dist/primitives/skeleton.js.map +1 -0
  214. package/dist/primitives/switch.d.ts +5 -0
  215. package/dist/primitives/switch.d.ts.map +1 -0
  216. package/dist/primitives/switch.js +8 -0
  217. package/dist/primitives/switch.js.map +1 -0
  218. package/dist/primitives/table.d.ts +11 -0
  219. package/dist/primitives/table.d.ts.map +1 -0
  220. package/dist/primitives/table.js +28 -0
  221. package/dist/primitives/table.js.map +1 -0
  222. package/dist/primitives/tabs.d.ts +12 -0
  223. package/dist/primitives/tabs.d.ts.map +1 -0
  224. package/dist/primitives/tabs.js +29 -0
  225. package/dist/primitives/tabs.js.map +1 -0
  226. package/dist/providers/query-provider.d.ts +11 -0
  227. package/dist/providers/query-provider.d.ts.map +1 -0
  228. package/dist/providers/query-provider.js +20 -0
  229. package/dist/providers/query-provider.js.map +1 -0
  230. package/dist/theme/create-theme.d.ts +130 -0
  231. package/dist/theme/create-theme.d.ts.map +1 -0
  232. package/dist/theme/create-theme.js +65 -0
  233. package/dist/theme/create-theme.js.map +1 -0
  234. package/dist/theme/index.d.ts +7 -0
  235. package/dist/theme/index.d.ts.map +1 -0
  236. package/dist/theme/index.js +7 -0
  237. package/dist/theme/index.js.map +1 -0
  238. package/dist/theme/presets.d.ts +25 -0
  239. package/dist/theme/presets.d.ts.map +1 -0
  240. package/dist/theme/presets.js +68 -0
  241. package/dist/theme/presets.js.map +1 -0
  242. package/dist/theme/theme-provider.d.ts +69 -0
  243. package/dist/theme/theme-provider.d.ts.map +1 -0
  244. package/dist/theme/theme-provider.js +79 -0
  245. package/dist/theme/theme-provider.js.map +1 -0
  246. package/package.json +71 -0
  247. package/src/app/adapt-data-widget.tsx +51 -0
  248. package/src/app/builder/CatalogueSheet.tsx +230 -0
  249. package/src/app/builder/PipelineStrip.tsx +260 -0
  250. package/src/app/builder/PreviewPane.tsx +72 -0
  251. package/src/app/builder/SaveDialog.tsx +95 -0
  252. package/src/app/builder/Toolbar.tsx +95 -0
  253. package/src/app/builder/Workspace.tsx +483 -0
  254. package/src/app/builder/builder-model.test.ts +351 -0
  255. package/src/app/builder/builder-model.ts +213 -0
  256. package/src/app/builder/builder-reducer.test.ts +441 -0
  257. package/src/app/builder/builder-reducer.ts +289 -0
  258. package/src/app/builder/labels.ts +139 -0
  259. package/src/app/builder/status-chip.tsx +37 -0
  260. package/src/app/host-bridge.test.ts +206 -0
  261. package/src/app/host-bridge.tsx +371 -0
  262. package/src/app/index.ts +51 -0
  263. package/src/app/layout-builder.tsx +872 -0
  264. package/src/app/mcp-app-view.tsx +367 -0
  265. package/src/app/mcp-toolkit-app.tsx +25 -0
  266. package/src/app/use-host-actions.test.ts +16 -0
  267. package/src/app/use-host-actions.tsx +88 -0
  268. package/src/app/widget-fixture.test.ts +108 -0
  269. package/src/app/widget-fixture.tsx +441 -0
  270. package/src/app/widget-props-sheet.tsx +285 -0
  271. package/src/app/widget-renderer.test.ts +42 -0
  272. package/src/app/widget-renderer.tsx +200 -0
  273. package/src/components/DrillButton.tsx +37 -0
  274. package/src/components/FilterBar.tsx +68 -0
  275. package/src/components/GridLayout.tsx +49 -0
  276. package/src/components/GroupCard.tsx +52 -0
  277. package/src/components/KpiGrid.tsx +134 -0
  278. package/src/components/ListFooter.tsx +48 -0
  279. package/src/components/Pills.tsx +45 -0
  280. package/src/components/SectionHeading.tsx +26 -0
  281. package/src/components/WidgetHeader.tsx +51 -0
  282. package/src/globals.css +168 -0
  283. package/src/hooks/index.ts +17 -0
  284. package/src/hooks/use-debounced-value.ts +15 -0
  285. package/src/hooks/use-mobile.ts +24 -0
  286. package/src/hooks/use-tool-query.ts +63 -0
  287. package/src/hooks/use-tool-result-recovery.test.ts +65 -0
  288. package/src/hooks/use-tool-result-recovery.ts +144 -0
  289. package/src/hooks/use-view-data.ts +31 -0
  290. package/src/hooks/use-view-tool-query.ts +38 -0
  291. package/src/index.ts +134 -0
  292. package/src/lib/parse-tool-result.test.ts +139 -0
  293. package/src/lib/parse-tool-result.ts +89 -0
  294. package/src/lib/tone-utils.test.ts +35 -0
  295. package/src/lib/tone-utils.ts +38 -0
  296. package/src/lib/utils.ts +6 -0
  297. package/src/locale/index.ts +7 -0
  298. package/src/locale/locale-provider.tsx +81 -0
  299. package/src/primitives/alert.tsx +60 -0
  300. package/src/primitives/badge.tsx +49 -0
  301. package/src/primitives/button.tsx +62 -0
  302. package/src/primitives/card.tsx +92 -0
  303. package/src/primitives/dialog.tsx +144 -0
  304. package/src/primitives/input.tsx +21 -0
  305. package/src/primitives/scroll-area.tsx +54 -0
  306. package/src/primitives/select.tsx +173 -0
  307. package/src/primitives/separator.tsx +21 -0
  308. package/src/primitives/sheet.tsx +134 -0
  309. package/src/primitives/skeleton.tsx +13 -0
  310. package/src/primitives/switch.tsx +24 -0
  311. package/src/primitives/table.tsx +87 -0
  312. package/src/primitives/tabs.tsx +79 -0
  313. package/src/providers/query-provider.tsx +38 -0
  314. package/src/theme/create-theme.test.ts +108 -0
  315. package/src/theme/create-theme.ts +158 -0
  316. package/src/theme/index.ts +9 -0
  317. package/src/theme/presets.ts +82 -0
  318. package/src/theme/theme-provider.tsx +152 -0
  319. package/src/ui-catalog.test.ts +112 -0
  320. package/ui-catalog.json +1280 -0
@@ -0,0 +1,285 @@
1
+ import { useState, type ReactNode } from "react"
2
+ import { Button } from "../primitives/button.js"
3
+ import { Input } from "../primitives/input.js"
4
+ import {
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ } from "../primitives/select.js"
11
+ import {
12
+ Sheet,
13
+ SheetContent,
14
+ SheetDescription,
15
+ SheetHeader,
16
+ SheetTitle,
17
+ } from "../primitives/sheet.js"
18
+ import { Switch } from "../primitives/switch.js"
19
+
20
+ /**
21
+ * Tiny JSON-Schema-driven editor for a layout cell's `props`. The
22
+ * LayoutBuilder opens this sheet when the user clicks "Configure" on a
23
+ * cell whose widget advertises a `propsSchema` in the framework manifest.
24
+ *
25
+ * Scope (v1):
26
+ * - Object-root schemas only.
27
+ * - Top-level properties: `string`, `number`, `integer`, `boolean`.
28
+ * - String enums render as a Select.
29
+ * - Nested objects/arrays/$ref/anyOf are not supported — the field
30
+ * shows an "edit JSON manually" hint so authors know to fall back.
31
+ *
32
+ * The sheet writes a clean `Record<string, unknown>` (empty values
33
+ * stripped) and reports `undefined` when the user clears every field, so
34
+ * the cell's `props` field stays absent rather than `{}` — matches the
35
+ * "no props" wire format used elsewhere.
36
+ */
37
+
38
+ interface JsonSchemaProperty {
39
+ type?: string
40
+ description?: string
41
+ enum?: unknown[]
42
+ default?: unknown
43
+ }
44
+
45
+ interface JsonSchemaObject {
46
+ type?: string
47
+ properties?: Record<string, JsonSchemaProperty>
48
+ required?: string[]
49
+ }
50
+
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
+ }
70
+
71
+ const UNSET_SENTINEL = "__unset__"
72
+
73
+ export interface WidgetPropsSheetProps {
74
+ open: boolean
75
+ onOpenChange: (open: boolean) => void
76
+ widgetId: string
77
+ schema: Record<string, unknown> | undefined
78
+ value: Record<string, unknown> | undefined
79
+ onApply: (next: Record<string, unknown> | undefined) => void
80
+ labels?: WidgetPropsSheetLabels
81
+ }
82
+
83
+ export function WidgetPropsSheet({
84
+ open,
85
+ onOpenChange,
86
+ widgetId,
87
+ schema,
88
+ value,
89
+ onApply,
90
+ labels,
91
+ }: WidgetPropsSheetProps) {
92
+ const L = { ...DEFAULT_LABELS, ...labels }
93
+ const obj = (schema ?? {}) as JsonSchemaObject
94
+ const [draft, setDraft] = useState<Record<string, unknown>>(value ?? {})
95
+
96
+ // Reset the form to the saved value on the open→true transition so a
97
+ // user who closes the sheet without applying gets their unsaved edits
98
+ // discarded next time. Render-time state update with a "prev" tracker
99
+ // is the React-recommended alternative to a setState-in-effect.
100
+ const [prevOpen, setPrevOpen] = useState(open)
101
+ if (open !== prevOpen) {
102
+ setPrevOpen(open)
103
+ if (open) setDraft(value ?? {})
104
+ }
105
+
106
+ const properties = obj.properties ?? {}
107
+ const propEntries = Object.entries(properties)
108
+
109
+ const handleApply = () => {
110
+ const cleaned: Record<string, unknown> = {}
111
+ for (const [k, v] of Object.entries(draft)) {
112
+ if (v === "" || v === undefined || v === null) continue
113
+ cleaned[k] = v
114
+ }
115
+ onApply(Object.keys(cleaned).length > 0 ? cleaned : undefined)
116
+ onOpenChange(false)
117
+ }
118
+
119
+ return (
120
+ <Sheet open={open} onOpenChange={onOpenChange}>
121
+ <SheetContent className="w-[400px] sm:max-w-md">
122
+ <SheetHeader>
123
+ <SheetTitle>{L.title}</SheetTitle>
124
+ <SheetDescription>
125
+ <span className="bg-muted rounded px-1 font-mono text-xs">{widgetId}</span> —{" "}
126
+ {L.description}
127
+ </SheetDescription>
128
+ </SheetHeader>
129
+ <div className="flex flex-col gap-4 px-4 py-4">
130
+ {propEntries.length === 0 ? (
131
+ <p className="text-muted-foreground text-sm">{L.noProps}</p>
132
+ ) : (
133
+ propEntries.map(([key, prop]) => (
134
+ <PropertyField
135
+ key={key}
136
+ name={key}
137
+ prop={prop}
138
+ value={draft[key]}
139
+ unsetLabel={L.unsetOption}
140
+ onChange={(v) =>
141
+ setDraft((prev) => {
142
+ if (v === undefined) {
143
+ const rest: Record<string, unknown> = {}
144
+ for (const [k, val] of Object.entries(prev)) {
145
+ if (k !== key) rest[k] = val
146
+ }
147
+ return rest
148
+ }
149
+ return { ...prev, [key]: v }
150
+ })
151
+ }
152
+ />
153
+ ))
154
+ )}
155
+ </div>
156
+ <div className="flex items-center justify-end gap-2 border-t px-4 py-3">
157
+ <Button variant="ghost" onClick={() => setDraft({})}>
158
+ {L.reset}
159
+ </Button>
160
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
161
+ {L.cancel}
162
+ </Button>
163
+ <Button onClick={handleApply}>{L.apply}</Button>
164
+ </div>
165
+ </SheetContent>
166
+ </Sheet>
167
+ )
168
+ }
169
+
170
+ function PropertyField({
171
+ name,
172
+ prop,
173
+ value,
174
+ unsetLabel,
175
+ onChange,
176
+ }: {
177
+ name: string
178
+ prop: JsonSchemaProperty
179
+ value: unknown
180
+ unsetLabel: string
181
+ onChange: (v: unknown) => void
182
+ }) {
183
+ const id = `prop-${name}`
184
+
185
+ if (prop.type === "string" && Array.isArray(prop.enum)) {
186
+ const options = prop.enum.filter((v): v is string => typeof v === "string")
187
+ const current = typeof value === "string" && value !== "" ? value : UNSET_SENTINEL
188
+ return (
189
+ <FieldShell id={id} name={name} description={prop.description}>
190
+ <Select
191
+ value={current}
192
+ onValueChange={(v) => onChange(v === UNSET_SENTINEL ? undefined : v)}
193
+ >
194
+ <SelectTrigger id={id}>
195
+ <SelectValue placeholder={unsetLabel} />
196
+ </SelectTrigger>
197
+ <SelectContent>
198
+ <SelectItem value={UNSET_SENTINEL}>{unsetLabel}</SelectItem>
199
+ {options.map((opt) => (
200
+ <SelectItem key={opt} value={opt}>
201
+ {opt}
202
+ </SelectItem>
203
+ ))}
204
+ </SelectContent>
205
+ </Select>
206
+ </FieldShell>
207
+ )
208
+ }
209
+
210
+ if (prop.type === "string") {
211
+ const current = typeof value === "string" ? value : ""
212
+ return (
213
+ <FieldShell id={id} name={name} description={prop.description}>
214
+ <Input
215
+ id={id}
216
+ value={current}
217
+ onChange={(e) => onChange(e.target.value === "" ? undefined : e.target.value)}
218
+ />
219
+ </FieldShell>
220
+ )
221
+ }
222
+
223
+ if (prop.type === "number" || prop.type === "integer") {
224
+ const current = typeof value === "number" ? String(value) : ""
225
+ return (
226
+ <FieldShell id={id} name={name} description={prop.description}>
227
+ <Input
228
+ id={id}
229
+ type="number"
230
+ value={current}
231
+ onChange={(e) => {
232
+ const raw = e.target.value
233
+ if (raw === "") return onChange(undefined)
234
+ const parsed =
235
+ prop.type === "integer" ? Number.parseInt(raw, 10) : Number.parseFloat(raw)
236
+ if (Number.isFinite(parsed)) onChange(parsed)
237
+ }}
238
+ />
239
+ </FieldShell>
240
+ )
241
+ }
242
+
243
+ if (prop.type === "boolean") {
244
+ const current = value === true
245
+ return (
246
+ <FieldShell id={id} name={name} description={prop.description}>
247
+ <Switch
248
+ id={id}
249
+ checked={current}
250
+ onCheckedChange={(checked) => onChange(checked || undefined)}
251
+ />
252
+ </FieldShell>
253
+ )
254
+ }
255
+
256
+ 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.
260
+ </p>
261
+ </FieldShell>
262
+ )
263
+ }
264
+
265
+ function FieldShell({
266
+ id,
267
+ name,
268
+ description,
269
+ children,
270
+ }: {
271
+ id: string
272
+ name: string
273
+ description?: string
274
+ children: ReactNode
275
+ }) {
276
+ return (
277
+ <div className="flex flex-col gap-1.5">
278
+ <label htmlFor={id} className="text-sm font-medium">
279
+ {name}
280
+ </label>
281
+ {children}
282
+ {description ? <p className="text-muted-foreground text-xs">{description}</p> : null}
283
+ </div>
284
+ )
285
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import type { PipelineContext } from "@miragon/mcp-toolkit-core"
3
+ import { contextHasNoData } from "./widget-renderer.js"
4
+
5
+ const ctx = (over: Partial<PipelineContext> = {}): PipelineContext => ({
6
+ steps: {},
7
+ keys: {},
8
+ errors: [],
9
+ ...over,
10
+ })
11
+
12
+ describe("contextHasNoData", () => {
13
+ it("is true for an empty context (no steps, no keys)", () => {
14
+ expect(contextHasNoData(ctx())).toBe(true)
15
+ })
16
+
17
+ it("is true even when there are errors but no data", () => {
18
+ expect(contextHasNoData(ctx({ errors: [{ stepId: "x", reason: "boom" }] }))).toBe(true)
19
+ })
20
+
21
+ it("is false when keys are present", () => {
22
+ expect(contextHasNoData(ctx({ keys: { "demo:id": "INV-1" } }))).toBe(false)
23
+ })
24
+
25
+ it("is false when step data is present", () => {
26
+ expect(
27
+ contextHasNoData(
28
+ ctx({
29
+ steps: {
30
+ result: {
31
+ data: { total: 1 },
32
+ keys: {},
33
+ _app: "demo",
34
+ _step: "result",
35
+ _dataType: "demo:data",
36
+ },
37
+ },
38
+ }),
39
+ ),
40
+ ).toBe(false)
41
+ })
42
+ })
@@ -0,0 +1,200 @@
1
+ import { Component, type ComponentType, type ReactNode, type ErrorInfo } from "react"
2
+ import type { LayoutConfig, PipelineContext, RowDef, WidgetProps } from "@miragon/mcp-toolkit-core"
3
+ import { normalizeLayout } from "@miragon/mcp-toolkit-core"
4
+ import { GridLayout, GridItem } from "../components/GridLayout.js"
5
+ import { Card, CardContent } from "../primitives/card.js"
6
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "../primitives/tabs.js"
7
+
8
+ export type WidgetComponent = ComponentType<WidgetProps>
9
+
10
+ /**
11
+ * Catches render-time crashes from a single widget so one broken widget
12
+ * can't take down the entire iframe view (or the builder canvas/preview).
13
+ *
14
+ * Exported so the visual builder's canvas + preview render sites reuse the
15
+ * same isolation the rendered `WidgetRenderer` already had — every place a
16
+ * widget component is rendered goes through {@link RenderedWidget}, which
17
+ * wraps the component in this boundary.
18
+ */
19
+ export class WidgetErrorBoundary extends Component<
20
+ { widgetId: string; children: ReactNode },
21
+ { error: Error | null }
22
+ > {
23
+ override state: { error: Error | null } = { error: null }
24
+ static getDerivedStateFromError(error: Error) {
25
+ return { error }
26
+ }
27
+ override componentDidCatch(error: Error, info: ErrorInfo) {
28
+ console.error(`Widget ${this.props.widgetId} crashed:`, error, info)
29
+ }
30
+ override render() {
31
+ if (this.state.error) {
32
+ 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>
38
+ )
39
+ }
40
+ return this.props.children
41
+ }
42
+ }
43
+
44
+ /**
45
+ * True when the pipeline context carries no widget data at all — no step
46
+ * results and no keys. A widget rendered against an empty context typically
47
+ * paints an empty shell (Finding [4]); callers use this to swap in a
48
+ * "data appears after refresh" hint instead.
49
+ *
50
+ * Intentionally coarse (per-view, not per-widget): the toolkit can't know
51
+ * which slice each widget reads without running its adapter, so it only
52
+ * degrades when there is provably nothing for any widget to show.
53
+ */
54
+ export function contextHasNoData(context: PipelineContext): boolean {
55
+ return Object.keys(context.steps).length === 0 && Object.keys(context.keys).length === 0
56
+ }
57
+
58
+ /**
59
+ * The single render site for a layout cell's widget, shared across the
60
+ * rendered view (`RowsRenderer`), the builder canvas, and the builder
61
+ * preview so all three isolate crashes identically:
62
+ * - missing component → "not bundled" fallback,
63
+ * - render crash → caught by {@link WidgetErrorBoundary} (Finding [1]),
64
+ * - (builder only, opt-in) empty pipeline context → a dezent
65
+ * "no data yet" hint (Finding [4]).
66
+ *
67
+ * The empty-state hint is gated behind `showEmptyHint` and defaults off so
68
+ * the shipped rendered view is unchanged: self-fetching control widgets
69
+ * there render correctly against an empty key-only context. The builder's
70
+ * canvas/preview opt in because their live context is empty until the first
71
+ * catalogue refresh resolves, and an empty widget shell reads as broken.
72
+ *
73
+ * `notBundledClassName` lets each surface match its own chrome (the builder
74
+ * uses denser padding/text than the rendered view).
75
+ */
76
+ export function RenderedWidget({
77
+ widget: Widget,
78
+ widgetId,
79
+ props,
80
+ cellProps,
81
+ showEmptyHint = false,
82
+ notBundledClassName = "text-muted-foreground rounded border border-dashed p-4 text-center text-xs",
83
+ }: {
84
+ widget: WidgetComponent | undefined
85
+ widgetId: string
86
+ props: WidgetProps
87
+ cellProps?: Record<string, unknown>
88
+ showEmptyHint?: boolean
89
+ notBundledClassName?: string
90
+ }) {
91
+ if (!Widget) {
92
+ return <div className={notBundledClassName}>Widget "{widgetId}" not bundled.</div>
93
+ }
94
+ 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
+ )
100
+ }
101
+ return (
102
+ <WidgetErrorBoundary widgetId={widgetId}>
103
+ <Widget {...props} widgetProps={cellProps} />
104
+ </WidgetErrorBoundary>
105
+ )
106
+ }
107
+
108
+ export interface WidgetRendererProps {
109
+ layout: LayoutConfig
110
+ keys: Record<string, unknown>
111
+ stepData?: Record<
112
+ string,
113
+ { data: unknown; keys: Record<string, unknown>; _app: string; _dataType: string }
114
+ >
115
+ errors: { stepId: string; reason: string }[]
116
+ /**
117
+ * Map of widget ID to React component. Consumers provide this so that the
118
+ * renderer can dispatch layout entries like `{widget: "camunda7:incident-panel"}`
119
+ * to the concrete component. The map is usually aggregated from each module's
120
+ * widget exports at the consumer's `main.tsx` entry point.
121
+ */
122
+ widgets: Record<string, WidgetComponent>
123
+ }
124
+
125
+ function RowsRenderer({
126
+ rows,
127
+ props,
128
+ widgets,
129
+ }: {
130
+ rows: RowDef[]
131
+ props: WidgetProps
132
+ widgets: Record<string, WidgetComponent>
133
+ }) {
134
+ return (
135
+ <div className="flex flex-col gap-4">
136
+ {rows.map((row, rowIdx) => (
137
+ <GridLayout key={rowIdx}>
138
+ {row.row.map((cell, cellIdx) => {
139
+ 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.
148
+ return (
149
+ <GridItem key={`${cell.widget}-${cellIdx}`} span={cell.span ?? 12}>
150
+ <RenderedWidget
151
+ widget={WidgetComponent}
152
+ widgetId={cell.widget}
153
+ props={props}
154
+ cellProps={cell.props}
155
+ />
156
+ </GridItem>
157
+ )
158
+ })}
159
+ </GridLayout>
160
+ ))}
161
+ </div>
162
+ )
163
+ }
164
+
165
+ export function WidgetRenderer({ layout, keys, stepData, errors, widgets }: WidgetRendererProps) {
166
+ 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
+ }
171
+ }
172
+ const pipelineContext: PipelineContext = {
173
+ steps,
174
+ keys,
175
+ errors,
176
+ }
177
+ const widgetProps: WidgetProps = { keys, context: pipelineContext }
178
+ const normalized = normalizeLayout(layout)
179
+
180
+ if ("tabs" in normalized) {
181
+ return (
182
+ <Tabs defaultValue={normalized.tabs[0]?.label}>
183
+ <TabsList>
184
+ {normalized.tabs.map((tab) => (
185
+ <TabsTrigger key={tab.label} value={tab.label}>
186
+ {tab.label}
187
+ </TabsTrigger>
188
+ ))}
189
+ </TabsList>
190
+ {normalized.tabs.map((tab) => (
191
+ <TabsContent key={tab.label} value={tab.label}>
192
+ <RowsRenderer rows={tab.rows} props={widgetProps} widgets={widgets} />
193
+ </TabsContent>
194
+ ))}
195
+ </Tabs>
196
+ )
197
+ }
198
+
199
+ return <RowsRenderer rows={normalized.rows} props={widgetProps} widgets={widgets} />
200
+ }
@@ -0,0 +1,37 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ export interface DrillButtonProps {
4
+ /** Button label. */
5
+ children: ReactNode
6
+ /** Drill/navigation handler — wire to in-widget navigation (e.g. `showWidget`). */
7
+ onDrill: () => void
8
+ /** Optional leading glyph (e.g. a domain icon). The trailing → is always rendered. */
9
+ icon?: ReactNode
10
+ /** `sm` for table rows, `md` for surface headers. Default `"sm"`. */
11
+ size?: "sm" | "md"
12
+ /** Accessible label when the visible text is terse. */
13
+ ariaLabel?: string
14
+ }
15
+
16
+ /**
17
+ * The single, deterministic in-widget navigation control: a neutral outline
18
+ * button with a trailing → that drills into a detail/sub-view. Use it for
19
+ * every in-app drill (row "Open", header nav) so they read identically and stay
20
+ * visually quieter than primary/agentic actions. Decoupled from any host — pass
21
+ * an `onDrill` callback (e.g. wrapping `useHostActions().showWidget`).
22
+ */
23
+ export function DrillButton({ children, onDrill, icon, size = "sm", ariaLabel }: DrillButtonProps) {
24
+ const pad = size === "md" ? "px-3 py-1.5 text-sm" : "px-2.5 py-1 text-xs"
25
+ return (
26
+ <button
27
+ type="button"
28
+ onClick={onDrill}
29
+ aria-label={ariaLabel}
30
+ className={`border-border text-foreground hover:bg-muted focus-visible:ring-ring inline-flex items-center gap-1 rounded-md border font-medium transition-colors outline-none focus-visible:ring-2 ${pad}`}
31
+ >
32
+ {icon && <span aria-hidden="true">{icon}</span>}
33
+ {children}
34
+ <span aria-hidden="true">→</span>
35
+ </button>
36
+ )
37
+ }
@@ -0,0 +1,68 @@
1
+ import { Input } from "../primitives/input.js"
2
+
3
+ export interface FilterChip {
4
+ /** Stable id passed back to `onChipToggle`. */
5
+ id: string
6
+ /** Visible chip label. */
7
+ label: string
8
+ /** Optional count rendered after the label. */
9
+ count?: number | string
10
+ /** Whether the chip is currently active. */
11
+ active?: boolean
12
+ }
13
+
14
+ export interface FilterBarProps {
15
+ /** Current search text (controlled). */
16
+ search: string
17
+ /** Placeholder for the search input. Default `"Filter…"`. */
18
+ searchPlaceholder?: string
19
+ /** Called with the new search text on every keystroke. */
20
+ onSearchChange: (next: string) => void
21
+ /** Filter chips rendered to the right of the search input. */
22
+ chips: FilterChip[]
23
+ /** Called with a chip id when it is toggled. */
24
+ onChipToggle: (id: string) => void
25
+ }
26
+
27
+ /**
28
+ * Search input + toggleable chip row for filtering a list/table of widget
29
+ * items. Pair with `useDebouncedValue` to avoid re-querying on every keystroke.
30
+ */
31
+ export function FilterBar({
32
+ search,
33
+ searchPlaceholder = "Filter…",
34
+ onSearchChange,
35
+ chips,
36
+ onChipToggle,
37
+ }: FilterBarProps) {
38
+ return (
39
+ <div className="flex flex-wrap items-center gap-2">
40
+ <Input
41
+ type="search"
42
+ value={search}
43
+ onChange={(e) => onSearchChange(e.target.value)}
44
+ placeholder={searchPlaceholder}
45
+ className="h-9 min-w-[220px] flex-1 text-sm"
46
+ />
47
+ <div className="flex flex-wrap gap-1.5">
48
+ {chips.map((chip) => (
49
+ <button
50
+ type="button"
51
+ key={chip.id}
52
+ onClick={() => onChipToggle(chip.id)}
53
+ className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors ${
54
+ chip.active
55
+ ? "bg-accent border-border text-accent-foreground font-semibold"
56
+ : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground"
57
+ }`}
58
+ >
59
+ {chip.label}
60
+ {chip.count !== undefined && chip.count !== "" && (
61
+ <span className="tabular-nums opacity-60">{chip.count}</span>
62
+ )}
63
+ </button>
64
+ ))}
65
+ </div>
66
+ </div>
67
+ )
68
+ }
@@ -0,0 +1,49 @@
1
+ import * as React from "react"
2
+ import { cn } from "../lib/utils.js"
3
+
4
+ export interface GridLayoutProps {
5
+ /** `GridItem` children to lay out across the 12 columns. */
6
+ children: React.ReactNode
7
+ /** Extra classes merged onto the grid container (e.g. to change the gap). */
8
+ className?: string
9
+ }
10
+
11
+ /**
12
+ * 12-column responsive grid container. Wrap `GridItem`s in it to compose
13
+ * dashboard rows; this is the same grid `WidgetRenderer` uses internally.
14
+ */
15
+ export function GridLayout({ children, className }: GridLayoutProps) {
16
+ return <div className={cn("grid grid-cols-12 gap-4", className)}>{children}</div>
17
+ }
18
+
19
+ export interface GridItemProps {
20
+ /** Column span from 1–12. Defaults to a full-width `12`. */
21
+ span?: number
22
+ /** Cell content. */
23
+ children: React.ReactNode
24
+ /** Extra classes merged onto the cell. */
25
+ className?: string
26
+ }
27
+
28
+ const spanClasses: Record<number, string> = {
29
+ 1: "col-span-1",
30
+ 2: "col-span-2",
31
+ 3: "col-span-3",
32
+ 4: "col-span-4",
33
+ 5: "col-span-5",
34
+ 6: "col-span-6",
35
+ 7: "col-span-7",
36
+ 8: "col-span-8",
37
+ 9: "col-span-9",
38
+ 10: "col-span-10",
39
+ 11: "col-span-11",
40
+ 12: "col-span-12",
41
+ }
42
+
43
+ /**
44
+ * A cell inside a `GridLayout`, spanning `span` of the 12 columns. Out-of-range
45
+ * spans fall back to full width.
46
+ */
47
+ export function GridItem({ span = 12, children, className }: GridItemProps) {
48
+ return <div className={cn(spanClasses[span] ?? "col-span-12", className)}>{children}</div>
49
+ }