@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,289 @@
1
+ /**
2
+ * Pure reducer for the LayoutBuilder draft state.
3
+ *
4
+ * Replaces the `setDraft((prev) => …)` closures that previously lived in
5
+ * `layout-builder.tsx`, plus the `setState`-inside-an-updater anti-pattern
6
+ * those closures used to keep `activeTabIndex` / `focusedRowIndex` in sync
7
+ * with structural draft changes. Those index updates are now part of each
8
+ * action's pure result, so the whole thing is testable without React.
9
+ */
10
+ import type { RowDef } from "@miragon/mcp-toolkit-core"
11
+ import { emptyRows, type DraftLayout, type DraftTab } from "./builder-model.js"
12
+
13
+ // -------------------------------------------------------------------------- //
14
+ // State
15
+ // -------------------------------------------------------------------------- //
16
+
17
+ /**
18
+ * The draft plus the UI indices that are coupled to draft mutations.
19
+ * `editingTabIdx` is the inline-rename target; it is cleared whenever a
20
+ * structural tab change could invalidate it.
21
+ */
22
+ export interface BuilderDraftState {
23
+ draft: DraftLayout
24
+ activeTabIndex: number
25
+ focusedRowIndex: number
26
+ editingTabIdx: number | null
27
+ /**
28
+ * Monotonic counter bumped whenever a `renameTab` action is rejected
29
+ * (duplicate label). The UI watches it to surface feedback; a counter
30
+ * rather than a boolean so two consecutive identical rejections still
31
+ * register as a change.
32
+ */
33
+ renameRejectedAt: number
34
+ }
35
+
36
+ export function initBuilderState(draft: DraftLayout): BuilderDraftState {
37
+ return {
38
+ draft,
39
+ activeTabIndex: 0,
40
+ focusedRowIndex: 0,
41
+ editingTabIdx: null,
42
+ renameRejectedAt: 0,
43
+ }
44
+ }
45
+
46
+ // -------------------------------------------------------------------------- //
47
+ // Actions
48
+ // -------------------------------------------------------------------------- //
49
+
50
+ export type BuilderAction =
51
+ | { type: "addWidgetToRow"; widgetId: string; defaultSpan: number; targetRowIdx?: number }
52
+ | { type: "addRow" }
53
+ | { type: "removeRow"; rowIdx: number }
54
+ | { type: "moveRow"; rowIdx: number; direction: -1 | 1 }
55
+ | { type: "setCellSpan"; rowIdx: number; cellIdx: number; span: number }
56
+ | { type: "removeCell"; rowIdx: number; cellIdx: number }
57
+ | {
58
+ type: "setCellProps"
59
+ rowIdx: number
60
+ cellIdx: number
61
+ next: Record<string, unknown> | undefined
62
+ }
63
+ | { type: "setFocusedRow"; rowIdx: number }
64
+ | { type: "selectTab"; tabIdx: number }
65
+ | { type: "startEditingTab"; tabIdx: number }
66
+ | { type: "stopEditingTab" }
67
+ | { type: "addTab" }
68
+ | { type: "renameTab"; tabIdx: number; label: string }
69
+ | { type: "removeTab"; tabIdx: number }
70
+
71
+ // -------------------------------------------------------------------------- //
72
+ // Helpers
73
+ // -------------------------------------------------------------------------- //
74
+
75
+ /** The rows surface the active tab (or the flat draft) currently exposes. */
76
+ function activeRowsOf(draft: DraftLayout, activeTabIndex: number): RowDef[] {
77
+ return draft.kind === "tabs" ? (draft.tabs[activeTabIndex]?.rows ?? []) : draft.rows
78
+ }
79
+
80
+ /** Clamp a focused-row index into the bounds of `rows`, never below 0. */
81
+ function clampRowIndex(rowIndex: number, rowCount: number): number {
82
+ return Math.max(0, Math.min(rowIndex, rowCount - 1))
83
+ }
84
+
85
+ /**
86
+ * Applies a pure rows-transform to whichever surface is active (the flat
87
+ * rows or the active tab's rows) and returns the new draft.
88
+ */
89
+ function mapActiveRows(
90
+ draft: DraftLayout,
91
+ activeTabIndex: number,
92
+ fn: (rows: RowDef[]) => RowDef[],
93
+ ): DraftLayout {
94
+ if (draft.kind === "rows") return { kind: "rows", rows: fn(draft.rows) }
95
+ const tabs = draft.tabs.map((t, idx) => (idx === activeTabIndex ? { ...t, rows: fn(t.rows) } : t))
96
+ return { kind: "tabs", tabs }
97
+ }
98
+
99
+ // -------------------------------------------------------------------------- //
100
+ // Reducer
101
+ // -------------------------------------------------------------------------- //
102
+
103
+ export function draftReducer(state: BuilderDraftState, action: BuilderAction): BuilderDraftState {
104
+ switch (action.type) {
105
+ case "addWidgetToRow": {
106
+ const rows = activeRowsOf(state.draft, state.activeTabIndex)
107
+ const safeLen = rows.length > 0 ? rows.length : 1
108
+ const idx = Math.min(Math.max(action.targetRowIdx ?? state.focusedRowIndex, 0), safeLen - 1)
109
+ const draft = mapActiveRows(state.draft, state.activeTabIndex, (rs) => {
110
+ const safeRows = rs.length > 0 ? rs : emptyRows()
111
+ return safeRows.map((r, i) =>
112
+ i === idx
113
+ ? { row: [...r.row, { widget: action.widgetId, span: action.defaultSpan }] }
114
+ : r,
115
+ )
116
+ })
117
+ return { ...state, draft, focusedRowIndex: idx }
118
+ }
119
+
120
+ case "addRow": {
121
+ let newIndex = state.focusedRowIndex
122
+ const draft = mapActiveRows(state.draft, state.activeTabIndex, (rows) => {
123
+ const next = [...rows, { row: [] }]
124
+ newIndex = next.length - 1
125
+ return next
126
+ })
127
+ return { ...state, draft, focusedRowIndex: newIndex }
128
+ }
129
+
130
+ case "removeRow": {
131
+ const draft = mapActiveRows(state.draft, state.activeTabIndex, (rows) => {
132
+ const next = rows.filter((_, i) => i !== action.rowIdx)
133
+ return next.length > 0 ? next : emptyRows()
134
+ })
135
+ // Matches the previous behaviour: decrement, floored at 0. Finding [9]
136
+ // clamping is scoped to tab switch / removeTab, so this stays as-is.
137
+ return { ...state, draft, focusedRowIndex: Math.max(0, state.focusedRowIndex - 1) }
138
+ }
139
+
140
+ case "moveRow": {
141
+ const target = action.rowIdx + action.direction
142
+ const rows = activeRowsOf(state.draft, state.activeTabIndex)
143
+ if (target < 0 || target >= rows.length) return state
144
+ const draft = mapActiveRows(state.draft, state.activeTabIndex, (rs) => {
145
+ const next = [...rs]
146
+ const [moved] = next.splice(action.rowIdx, 1)
147
+ if (!moved) return rs
148
+ next.splice(target, 0, moved)
149
+ return next
150
+ })
151
+ return { ...state, draft, focusedRowIndex: target }
152
+ }
153
+
154
+ case "setCellSpan": {
155
+ const clamped = Math.max(1, Math.min(12, action.span))
156
+ const draft = mapActiveRows(state.draft, state.activeTabIndex, (rows) =>
157
+ rows.map((r, i) =>
158
+ i === action.rowIdx
159
+ ? { row: r.row.map((c, j) => (j === action.cellIdx ? { ...c, span: clamped } : c)) }
160
+ : r,
161
+ ),
162
+ )
163
+ return { ...state, draft }
164
+ }
165
+
166
+ case "removeCell": {
167
+ const draft = mapActiveRows(state.draft, state.activeTabIndex, (rows) =>
168
+ rows.map((r, i) =>
169
+ i === action.rowIdx ? { row: r.row.filter((_, j) => j !== action.cellIdx) } : r,
170
+ ),
171
+ )
172
+ return { ...state, draft }
173
+ }
174
+
175
+ case "setCellProps": {
176
+ const draft = mapActiveRows(state.draft, state.activeTabIndex, (rows) =>
177
+ rows.map((r, i) =>
178
+ i === action.rowIdx
179
+ ? {
180
+ row: r.row.map((c, j) => {
181
+ if (j !== action.cellIdx) return c
182
+ if (action.next === undefined) {
183
+ const rest: typeof c = { widget: c.widget }
184
+ if (c.span !== undefined) rest.span = c.span
185
+ return rest
186
+ }
187
+ return { ...c, props: action.next }
188
+ }),
189
+ }
190
+ : r,
191
+ ),
192
+ )
193
+ return { ...state, draft }
194
+ }
195
+
196
+ case "setFocusedRow":
197
+ return { ...state, focusedRowIndex: Math.max(0, action.rowIdx) }
198
+
199
+ case "selectTab": {
200
+ if (state.draft.kind !== "tabs") return state
201
+ if (action.tabIdx < 0 || action.tabIdx >= state.draft.tabs.length) return state
202
+ const rows = state.draft.tabs[action.tabIdx]?.rows ?? []
203
+ return {
204
+ ...state,
205
+ activeTabIndex: action.tabIdx,
206
+ focusedRowIndex: clampRowIndex(state.focusedRowIndex, rows.length),
207
+ }
208
+ }
209
+
210
+ case "startEditingTab":
211
+ return { ...state, editingTabIdx: action.tabIdx }
212
+
213
+ case "stopEditingTab":
214
+ return { ...state, editingTabIdx: null }
215
+
216
+ case "addTab": {
217
+ if (state.draft.kind === "tabs") {
218
+ const label = `Tab ${state.draft.tabs.length + 1}`
219
+ const nextTabs: DraftTab[] = [...state.draft.tabs, { label, rows: emptyRows() }]
220
+ return {
221
+ ...state,
222
+ draft: { kind: "tabs", tabs: nextTabs },
223
+ activeTabIndex: nextTabs.length - 1,
224
+ }
225
+ }
226
+ const seededTabs: DraftTab[] = [
227
+ { label: "Tab 1", rows: state.draft.rows },
228
+ { label: "Tab 2", rows: emptyRows() },
229
+ ]
230
+ return { ...state, draft: { kind: "tabs", tabs: seededTabs }, activeTabIndex: 1 }
231
+ }
232
+
233
+ case "renameTab": {
234
+ const trimmed = action.label.trim()
235
+ if (!trimmed) return state
236
+ if (state.draft.kind !== "tabs") return state
237
+ // Reject duplicates — Tabs keys by label, collisions break the
238
+ // component. Signal the rejection so the UI can give feedback; the
239
+ // old label stays put.
240
+ if (state.draft.tabs.some((t, i) => i !== action.tabIdx && t.label === trimmed)) {
241
+ return { ...state, renameRejectedAt: state.renameRejectedAt + 1 }
242
+ }
243
+ const tabs = state.draft.tabs.map((t, i) =>
244
+ i === action.tabIdx ? { ...t, label: trimmed } : t,
245
+ )
246
+ return { ...state, draft: { kind: "tabs", tabs } }
247
+ }
248
+
249
+ case "removeTab": {
250
+ if (state.draft.kind !== "tabs") return state
251
+ const tabs = state.draft.tabs.filter((_, i) => i !== action.tabIdx)
252
+ const activeTabIndex = clampRowIndex(state.activeTabIndex, tabs.length)
253
+ if (tabs.length === 0) {
254
+ const draft: DraftLayout = { kind: "rows", rows: emptyRows() }
255
+ return {
256
+ ...state,
257
+ draft,
258
+ activeTabIndex: 0,
259
+ editingTabIdx: null,
260
+ focusedRowIndex: clampRowIndex(state.focusedRowIndex, draft.rows.length),
261
+ }
262
+ }
263
+ if (tabs.length === 1 && tabs[0]) {
264
+ const draft: DraftLayout = { kind: "rows", rows: tabs[0].rows }
265
+ return {
266
+ ...state,
267
+ draft,
268
+ activeTabIndex: 0,
269
+ editingTabIdx: null,
270
+ focusedRowIndex: clampRowIndex(state.focusedRowIndex, draft.rows.length),
271
+ }
272
+ }
273
+ const rows = tabs[activeTabIndex]?.rows ?? []
274
+ return {
275
+ ...state,
276
+ draft: { kind: "tabs", tabs },
277
+ activeTabIndex,
278
+ editingTabIdx: null,
279
+ focusedRowIndex: clampRowIndex(state.focusedRowIndex, rows.length),
280
+ }
281
+ }
282
+
283
+ default: {
284
+ // Exhaustiveness guard: a new action type without a case is a type error.
285
+ const _never: never = action
286
+ return _never
287
+ }
288
+ }
289
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Shared label contract for the visual LayoutBuilder.
3
+ *
4
+ * Extracted from `layout-builder.tsx` so the presentational sub-components
5
+ * (Toolbar, PipelineStrip, Workspace, CatalogueSheet, SaveDialog, PreviewPane)
6
+ * can depend on the labels without importing the orchestrating container —
7
+ * which would create an import cycle. React-free and `mcp-use`-free.
8
+ */
9
+
10
+ export interface LayoutBuilderLabels {
11
+ title?: string
12
+ builderBadge?: string
13
+ previewBadge?: string
14
+ catalogue?: string
15
+ save?: string
16
+ done?: string
17
+ viewLayoutTab?: string
18
+ viewPipelineTab?: string
19
+ viewPreviewTab?: string
20
+ saveDialogTitle?: string
21
+ saveDialogDescription?: string
22
+ saveNameLabel?: string
23
+ saveDescriptionLabel?: string
24
+ saveConfirm?: string
25
+ cancel?: string
26
+ pipelineHeader?: string
27
+ keysHeader?: string
28
+ stepsHeader?: string
29
+ keyName?: string
30
+ keyValue?: string
31
+ stepContextId?: string
32
+ stepPickerLabel?: string
33
+ addKey?: string
34
+ addStep?: string
35
+ emptyKeys?: string
36
+ emptySteps?: string
37
+ statusUpToDate?: string
38
+ statusPending?: string
39
+ statusRefreshing?: string
40
+ paletteHeader?: string
41
+ paletteEmpty?: string
42
+ canvasEmpty?: string
43
+ canvasDropHint?: string
44
+ addRow?: string
45
+ addTab?: string
46
+ removeRow?: string
47
+ renameTab?: string
48
+ removeTab?: string
49
+ tabsLabel?: string
50
+ catalogueDescription?: string
51
+ catalogueKeys?: string
52
+ catalogueSteps?: string
53
+ catalogueUnreachable?: string
54
+ catalogueUnreachableEmpty?: string
55
+ catalogueProducedBy?: string
56
+ catalogueConsumedBy?: string
57
+ catalogueNeedsKeys?: string
58
+ cataloguePickKey?: string
59
+ catalogueAddProducingStep?: string
60
+ catalogueLive?: string
61
+ catalogueMissing?: string
62
+ /** Banner shown when the catalogue fetch fails (prefixes the reason). */
63
+ catalogueError?: string
64
+ /** Heading for the pipeline-validation warning banner. */
65
+ validationWarning?: string
66
+ /** Inline message when a save fails (prefixes the reason). */
67
+ saveError?: string
68
+ /** Inline message when a tab rename is rejected (duplicate label). */
69
+ renameDuplicate?: string
70
+ }
71
+
72
+ export const DEFAULT_LABELS: Required<LayoutBuilderLabels> = {
73
+ title: "View Builder",
74
+ builderBadge: "Builder",
75
+ previewBadge: "Preview",
76
+ catalogue: "Catalogue",
77
+ save: "Save",
78
+ done: "Done",
79
+ viewLayoutTab: "Layout",
80
+ viewPipelineTab: "Pipeline",
81
+ viewPreviewTab: "Preview",
82
+ saveDialogTitle: "Save dashboard",
83
+ saveDialogDescription:
84
+ "Persists the current layout along with the keys and steps that populate it.",
85
+ saveNameLabel: "Name",
86
+ saveDescriptionLabel: "Description",
87
+ saveConfirm: "Save dashboard",
88
+ cancel: "Cancel",
89
+ pipelineHeader: "Pipeline",
90
+ keysHeader: "Keys",
91
+ stepsHeader: "Steps",
92
+ keyName: "key",
93
+ keyValue: 'value, "literal", true, {…}',
94
+ stepContextId: "ctx id",
95
+ stepPickerLabel: "pick a step",
96
+ addKey: "Add key",
97
+ addStep: "Add step",
98
+ emptyKeys: "No keys seeded.",
99
+ emptySteps: "No steps configured.",
100
+ statusUpToDate: "Up to date",
101
+ statusPending: "Applying soon…",
102
+ statusRefreshing: "Applying…",
103
+ paletteHeader: "Palette",
104
+ paletteEmpty: "No widgets reachable yet — seed a key or add a step.",
105
+ canvasEmpty: "Drag a widget here, or click one in the palette.",
106
+ canvasDropHint: "Drop to add",
107
+ addRow: "Add row",
108
+ addTab: "Add tab",
109
+ removeRow: "Remove row",
110
+ renameTab: "Rename tab",
111
+ removeTab: "Remove tab",
112
+ tabsLabel: "Tabs",
113
+ catalogueDescription:
114
+ "Every key and widget the framework knows about, whether or not it's reachable right now.",
115
+ catalogueKeys: "Keys",
116
+ catalogueSteps: "Steps",
117
+ catalogueUnreachable: "Unreachable widgets",
118
+ catalogueUnreachableEmpty: "Every registered widget is reachable.",
119
+ catalogueProducedBy: "produced by",
120
+ catalogueConsumedBy: "consumed by",
121
+ catalogueNeedsKeys: "needs",
122
+ cataloguePickKey: "Add as key",
123
+ catalogueAddProducingStep: "Add producing step",
124
+ catalogueLive: "live",
125
+ catalogueMissing: "missing",
126
+ catalogueError: "Couldn't refresh the builder catalogue",
127
+ validationWarning: "This pipeline won't render as configured",
128
+ saveError: "Couldn't save",
129
+ renameDuplicate: "A tab with that name already exists.",
130
+ }
131
+
132
+ /**
133
+ * MIME type used to carry a widget id across the palette → canvas drag.
134
+ * Shared between the palette items and the canvas drop targets.
135
+ */
136
+ export const WIDGET_DRAG_MIME = "application/x-mcp-widget-id"
137
+
138
+ /** Refresh status for the auto-apply chip and the pipeline strip. */
139
+ export type RefreshStatus = "idle" | "pending" | "refreshing"
@@ -0,0 +1,37 @@
1
+ import { CircleDot, Loader2 } from "lucide-react"
2
+ import type { LayoutBuilderLabels, RefreshStatus } from "./labels.js"
3
+
4
+ /**
5
+ * Compact auto-apply status chip. Shared by the Toolbar and the
6
+ * PipelineStrip so both surfaces show the same refresh state.
7
+ */
8
+ export function StatusChip({
9
+ status,
10
+ L,
11
+ }: {
12
+ status: RefreshStatus
13
+ L: Required<LayoutBuilderLabels>
14
+ }) {
15
+ if (status === "idle") {
16
+ return (
17
+ <span className="text-muted-foreground inline-flex items-center gap-1.5">
18
+ <CircleDot className="size-3" />
19
+ {L.statusUpToDate}
20
+ </span>
21
+ )
22
+ }
23
+ if (status === "pending") {
24
+ return (
25
+ <span className="inline-flex items-center gap-1.5 text-amber-600 dark:text-amber-400">
26
+ <span className="size-1.5 animate-pulse rounded-full bg-amber-500" />
27
+ {L.statusPending}
28
+ </span>
29
+ )
30
+ }
31
+ return (
32
+ <span className="text-primary inline-flex items-center gap-1.5">
33
+ <Loader2 className="size-3 animate-spin" />
34
+ {L.statusRefreshing}
35
+ </span>
36
+ )
37
+ }
@@ -0,0 +1,206 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest"
2
+ import {
3
+ createChatGptHostBridge,
4
+ createStandaloneHostBridge,
5
+ type OpenAiAppsSdk,
6
+ } from "./host-bridge.js"
7
+
8
+ afterEach(() => {
9
+ vi.restoreAllMocks()
10
+ })
11
+
12
+ describe("createChatGptHostBridge — no window.openai (defensive guards)", () => {
13
+ // Passing `null` exercises the fully-absent SDK path without touching window.
14
+ it("rejects callTool with a clear error instead of throwing synchronously", async () => {
15
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
16
+ const bridge = createChatGptHostBridge(null)
17
+
18
+ await expect(bridge.callTool("do_thing", {})).rejects.toThrow(
19
+ /OpenAI Apps SDK callTool is unavailable.*do_thing/,
20
+ )
21
+ expect(warn).toHaveBeenCalled()
22
+ })
23
+
24
+ it("sendFollowup is a logged no-op (no throw) when the SDK is absent", () => {
25
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
26
+ const bridge = createChatGptHostBridge(null)
27
+
28
+ expect(() => bridge.sendFollowup("hello")).not.toThrow()
29
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("sendFollowUpMessage unavailable"))
30
+ })
31
+
32
+ it("setModelContext is a logged no-op (no throw) when the SDK is absent", () => {
33
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
34
+ const bridge = createChatGptHostBridge(null)
35
+
36
+ expect(() => bridge.setModelContext?.("ctx")).not.toThrow()
37
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("setWidgetState unavailable"))
38
+ })
39
+
40
+ it("getWidgetData returns null when the SDK is absent", () => {
41
+ const bridge = createChatGptHostBridge(null)
42
+ expect(bridge.getWidgetData()).toBeNull()
43
+ })
44
+
45
+ it("leaves theme undefined when the SDK reports none", () => {
46
+ const bridge = createChatGptHostBridge(null)
47
+ expect(bridge.theme).toBeUndefined()
48
+ })
49
+ })
50
+
51
+ describe("createChatGptHostBridge — with a stubbed SDK", () => {
52
+ it("routes callTool through window.openai.callTool", async () => {
53
+ const sdk: OpenAiAppsSdk = {
54
+ callTool: vi.fn().mockResolvedValue({ structuredContent: { ok: true } }),
55
+ }
56
+ const bridge = createChatGptHostBridge(sdk)
57
+
58
+ const res = await bridge.callTool("get_thing", { id: 1 })
59
+
60
+ expect(sdk.callTool).toHaveBeenCalledWith("get_thing", { id: 1 })
61
+ expect(res).toEqual({ structuredContent: { ok: true } })
62
+ })
63
+
64
+ it("calls sendFollowUpMessage with the { prompt } shape", () => {
65
+ const sendFollowUpMessage = vi.fn().mockReturnValue(undefined)
66
+ const bridge = createChatGptHostBridge({ sendFollowUpMessage })
67
+
68
+ bridge.sendFollowup("show details")
69
+
70
+ expect(sendFollowUpMessage).toHaveBeenCalledWith({ prompt: "show details" })
71
+ })
72
+
73
+ it("falls back to the bare-string signature when { prompt } throws", () => {
74
+ const calls: unknown[] = []
75
+ const sendFollowUpMessage = vi.fn((arg: { prompt: string } | string) => {
76
+ calls.push(arg)
77
+ // Reject the object form to force the string fallback.
78
+ if (typeof arg !== "string") throw new Error("object form unsupported")
79
+ })
80
+ const bridge = createChatGptHostBridge({ sendFollowUpMessage })
81
+
82
+ bridge.sendFollowup("hi")
83
+
84
+ expect(calls).toEqual([{ prompt: "hi" }, "hi"])
85
+ })
86
+
87
+ it("accepts the alternate sendFollowupMessage spelling", () => {
88
+ const sendFollowupMessage = vi.fn()
89
+ const bridge = createChatGptHostBridge({ sendFollowupMessage })
90
+
91
+ bridge.sendFollowup("hi")
92
+
93
+ expect(sendFollowupMessage).toHaveBeenCalledWith({ prompt: "hi" })
94
+ })
95
+
96
+ it("opens external links via openExternal with the { href } shape", () => {
97
+ const openExternal = vi.fn()
98
+ const bridge = createChatGptHostBridge({ openExternal })
99
+
100
+ bridge.openExternal("https://example.com")
101
+
102
+ expect(openExternal).toHaveBeenCalledWith({ href: "https://example.com" })
103
+ })
104
+
105
+ it("prefers toolOutput over toolResponseMetadata for widget data", () => {
106
+ const bridge = createChatGptHostBridge({
107
+ toolOutput: { a: 1 },
108
+ toolResponseMetadata: { b: 2 },
109
+ })
110
+ expect(bridge.getWidgetData()).toEqual({ a: 1 })
111
+ })
112
+
113
+ it("falls back to toolResponseMetadata when toolOutput is absent", () => {
114
+ const bridge = createChatGptHostBridge({ toolResponseMetadata: { b: 2 } })
115
+ expect(bridge.getWidgetData()).toEqual({ b: 2 })
116
+ })
117
+
118
+ it("normalizes the theme to light | dark | undefined", () => {
119
+ expect(createChatGptHostBridge({ theme: "dark" }).theme).toBe("dark")
120
+ expect(createChatGptHostBridge({ theme: "light" }).theme).toBe("light")
121
+ expect(createChatGptHostBridge({ theme: "sepia" }).theme).toBeUndefined()
122
+ })
123
+ })
124
+
125
+ describe("createStandaloneHostBridge", () => {
126
+ it("routes callTool through the injected function", async () => {
127
+ const callTool = vi.fn().mockResolvedValue({ structuredContent: { id: "x" } })
128
+ const bridge = createStandaloneHostBridge({ callTool })
129
+
130
+ const res = await bridge.callTool("find", { q: "a" })
131
+
132
+ expect(callTool).toHaveBeenCalledWith("find", { q: "a" })
133
+ expect(res).toEqual({ structuredContent: { id: "x" } })
134
+ })
135
+
136
+ it("reads static getData", () => {
137
+ const bridge = createStandaloneHostBridge({
138
+ callTool: vi.fn(),
139
+ getData: { name: "Acme" },
140
+ })
141
+ expect(bridge.getWidgetData()).toEqual({ name: "Acme" })
142
+ })
143
+
144
+ it("re-reads getData when it is a getter", () => {
145
+ let n = 0
146
+ const bridge = createStandaloneHostBridge({
147
+ callTool: vi.fn(),
148
+ getData: () => ({ n: ++n }),
149
+ })
150
+ expect(bridge.getWidgetData()).toEqual({ n: 1 })
151
+ expect(bridge.getWidgetData()).toEqual({ n: 2 })
152
+ })
153
+
154
+ it("returns null from getWidgetData when no getData is supplied", () => {
155
+ const bridge = createStandaloneHostBridge({ callTool: vi.fn() })
156
+ expect(bridge.getWidgetData()).toBeNull()
157
+ })
158
+
159
+ it("invokes onFollowup when supplied", () => {
160
+ const onFollowup = vi.fn()
161
+ const bridge = createStandaloneHostBridge({ callTool: vi.fn(), onFollowup })
162
+
163
+ bridge.sendFollowup("analyse this")
164
+
165
+ expect(onFollowup).toHaveBeenCalledWith("analyse this")
166
+ })
167
+
168
+ it("logs and drops the prompt when no onFollowup is supplied", () => {
169
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
170
+ const bridge = createStandaloneHostBridge({ callTool: vi.fn() })
171
+
172
+ expect(() => bridge.sendFollowup("dropped")).not.toThrow()
173
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("no onFollowup handler"))
174
+ })
175
+
176
+ it("invokes onOpenExternal when supplied", () => {
177
+ const onOpenExternal = vi.fn()
178
+ const bridge = createStandaloneHostBridge({ callTool: vi.fn(), onOpenExternal })
179
+
180
+ bridge.openExternal("https://x.test")
181
+
182
+ expect(onOpenExternal).toHaveBeenCalledWith("https://x.test")
183
+ })
184
+
185
+ it("routes setModelContext to onModelContext when supplied", () => {
186
+ const onModelContext = vi.fn()
187
+ const bridge = createStandaloneHostBridge({ callTool: vi.fn(), onModelContext })
188
+
189
+ bridge.setModelContext?.("selected row 3")
190
+
191
+ expect(onModelContext).toHaveBeenCalledWith("selected row 3")
192
+ })
193
+
194
+ it("logs and drops model context when no onModelContext is supplied", () => {
195
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
196
+ const bridge = createStandaloneHostBridge({ callTool: vi.fn() })
197
+
198
+ expect(() => bridge.setModelContext?.("ctx")).not.toThrow()
199
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("no onModelContext handler"))
200
+ })
201
+
202
+ it("passes through the configured theme", () => {
203
+ expect(createStandaloneHostBridge({ callTool: vi.fn(), theme: "dark" }).theme).toBe("dark")
204
+ expect(createStandaloneHostBridge({ callTool: vi.fn() }).theme).toBeUndefined()
205
+ })
206
+ })