@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,483 @@
1
+ import { memo, useCallback, useRef } from "react"
2
+ import {
3
+ ChevronDown,
4
+ ChevronUp,
5
+ GripVertical,
6
+ Plus,
7
+ SlidersHorizontal,
8
+ Trash2,
9
+ X,
10
+ } from "lucide-react"
11
+ import type { ReachableWidget, RowDef, WidgetProps } from "@miragon/mcp-toolkit-core"
12
+ import { GridItem, GridLayout } from "../../components/GridLayout.js"
13
+ import { Badge } from "../../primitives/badge.js"
14
+ import { Button } from "../../primitives/button.js"
15
+ import { ScrollArea } from "../../primitives/scroll-area.js"
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from "../../primitives/select.js"
23
+ import { cn } from "../../lib/utils.js"
24
+ import { RenderedWidget, type WidgetComponent } from "../widget-renderer.js"
25
+ import type { LayoutBuilderLabels } from "./labels.js"
26
+ import { WIDGET_DRAG_MIME } from "./labels.js"
27
+
28
+ // -------------------------------------------------------------------------- //
29
+ // Workspace — palette + canvas (palette sticky on lg+)
30
+ // -------------------------------------------------------------------------- //
31
+
32
+ function WorkspaceImpl({
33
+ L,
34
+ paletteByApp,
35
+ paletteCount,
36
+ activeRows,
37
+ focusedRowIndex,
38
+ setFocusedRowIndex,
39
+ dragOverRow,
40
+ setDragOverRow,
41
+ onAddRow,
42
+ onRemoveRow,
43
+ onMoveRow,
44
+ onSetCellSpan,
45
+ onRemoveCell,
46
+ onAddWidget,
47
+ onConfigureCell,
48
+ propsSchemaByWidgetId,
49
+ widgets,
50
+ widgetProps,
51
+ showAddTabButton,
52
+ onAddTab,
53
+ }: {
54
+ L: Required<LayoutBuilderLabels>
55
+ paletteByApp: [string, ReachableWidget[]][]
56
+ paletteCount: number
57
+ activeRows: RowDef[]
58
+ focusedRowIndex: number
59
+ setFocusedRowIndex: (idx: number) => void
60
+ dragOverRow: number | null
61
+ setDragOverRow: (idx: number | null) => void
62
+ onAddRow: () => void
63
+ onRemoveRow: (rowIdx: number) => void
64
+ onMoveRow: (rowIdx: number, direction: -1 | 1) => void
65
+ onSetCellSpan: (rowIdx: number, cellIdx: number, span: number) => void
66
+ onRemoveCell: (rowIdx: number, cellIdx: number) => void
67
+ onAddWidget: (widgetId: string, targetRowIdx?: number) => void
68
+ onConfigureCell: (rowIdx: number, cellIdx: number) => void
69
+ propsSchemaByWidgetId: Map<string, Record<string, unknown>>
70
+ widgets: Record<string, WidgetComponent>
71
+ widgetProps: WidgetProps
72
+ showAddTabButton?: boolean
73
+ onAddTab?: () => void
74
+ }) {
75
+ return (
76
+ <div className="grid grid-cols-12 gap-4">
77
+ <aside className="col-span-12 lg:col-span-3 xl:col-span-2">
78
+ <div className="lg:sticky lg:top-16">
79
+ <div className="text-muted-foreground mb-2 flex items-center gap-2 text-[10px] font-semibold tracking-[0.12em] uppercase">
80
+ {L.paletteHeader}
81
+ <Badge variant="outline" className="px-1.5 text-[10px]">
82
+ {paletteCount}
83
+ </Badge>
84
+ </div>
85
+ {paletteCount === 0 ? (
86
+ <div className="text-muted-foreground rounded-md border border-dashed p-3 text-xs">
87
+ {L.paletteEmpty}
88
+ </div>
89
+ ) : (
90
+ <ScrollArea className="lg:max-h-[70vh]">
91
+ <div className="flex flex-col gap-3 pr-1">
92
+ {paletteByApp.map(([app, ws]) => (
93
+ <div key={app} className="flex flex-col gap-1">
94
+ <div className="text-muted-foreground/80 text-[10px] font-semibold tracking-wide uppercase">
95
+ {app}
96
+ </div>
97
+ {ws.map((w) => (
98
+ <PaletteItem key={w.id} w={w} onClick={() => onAddWidget(w.id)} />
99
+ ))}
100
+ </div>
101
+ ))}
102
+ </div>
103
+ </ScrollArea>
104
+ )}
105
+ {showAddTabButton && onAddTab && (
106
+ <Button
107
+ variant="ghost"
108
+ size="xs"
109
+ onClick={onAddTab}
110
+ className="text-muted-foreground hover:text-foreground mt-3"
111
+ >
112
+ <Plus /> {L.addTab}
113
+ </Button>
114
+ )}
115
+ </div>
116
+ </aside>
117
+
118
+ <section className="col-span-12 lg:col-span-9 xl:col-span-10">
119
+ <div className="flex flex-col gap-3">
120
+ {activeRows.map((row, rowIdx) => (
121
+ <CanvasRow
122
+ key={rowIdx}
123
+ L={L}
124
+ row={row}
125
+ rowIdx={rowIdx}
126
+ focused={rowIdx === focusedRowIndex}
127
+ isFirst={rowIdx === 0}
128
+ isLast={rowIdx === activeRows.length - 1}
129
+ dragOver={dragOverRow === rowIdx}
130
+ setDragOverRow={setDragOverRow}
131
+ setFocusedRowIndex={setFocusedRowIndex}
132
+ onRemoveRow={onRemoveRow}
133
+ onMoveRow={onMoveRow}
134
+ onSetCellSpan={onSetCellSpan}
135
+ onRemoveCell={onRemoveCell}
136
+ onAddWidget={onAddWidget}
137
+ onConfigureCell={onConfigureCell}
138
+ propsSchemaByWidgetId={propsSchemaByWidgetId}
139
+ widgets={widgets}
140
+ widgetProps={widgetProps}
141
+ />
142
+ ))}
143
+ <Button variant="outline" size="sm" onClick={onAddRow} className="self-start text-xs">
144
+ <Plus /> {L.addRow}
145
+ </Button>
146
+ </div>
147
+ </section>
148
+ </div>
149
+ )
150
+ }
151
+
152
+ export const Workspace = memo(WorkspaceImpl)
153
+
154
+ // -------------------------------------------------------------------------- //
155
+ // Palette item
156
+ // -------------------------------------------------------------------------- //
157
+
158
+ function PaletteItemImpl({ w, onClick }: { w: ReachableWidget; onClick: () => void }) {
159
+ return (
160
+ <button
161
+ type="button"
162
+ draggable
163
+ onDragStart={(e) => {
164
+ e.dataTransfer.setData(WIDGET_DRAG_MIME, w.id)
165
+ e.dataTransfer.effectAllowed = "copy"
166
+ }}
167
+ onClick={onClick}
168
+ className="hover:bg-accent hover:text-accent-foreground group bg-background/50 flex cursor-grab flex-col items-start gap-0.5 rounded-md border px-2 py-1.5 text-left text-xs transition-colors active:cursor-grabbing"
169
+ >
170
+ <span className="flex w-full items-center justify-between gap-2">
171
+ <span className="truncate font-mono">{w.id}</span>
172
+ <GripVertical className="text-muted-foreground/40 group-hover:text-muted-foreground size-3" />
173
+ </span>
174
+ {w.requires.length > 0 && (
175
+ <span className="text-muted-foreground/80 truncate font-mono text-[10px]">
176
+ ← {w.requires.join(", ")}
177
+ </span>
178
+ )}
179
+ </button>
180
+ )
181
+ }
182
+
183
+ const PaletteItem = memo(PaletteItemImpl)
184
+
185
+ // -------------------------------------------------------------------------- //
186
+ // Canvas row — drop target + per-cell controls + resize handle
187
+ // -------------------------------------------------------------------------- //
188
+
189
+ function CanvasRowImpl({
190
+ L,
191
+ row,
192
+ rowIdx,
193
+ focused,
194
+ isFirst,
195
+ isLast,
196
+ dragOver,
197
+ setDragOverRow,
198
+ setFocusedRowIndex,
199
+ onRemoveRow,
200
+ onMoveRow,
201
+ onSetCellSpan,
202
+ onRemoveCell,
203
+ onAddWidget,
204
+ onConfigureCell,
205
+ propsSchemaByWidgetId,
206
+ widgets,
207
+ widgetProps,
208
+ }: {
209
+ L: Required<LayoutBuilderLabels>
210
+ row: RowDef
211
+ rowIdx: number
212
+ focused: boolean
213
+ isFirst: boolean
214
+ isLast: boolean
215
+ dragOver: boolean
216
+ setDragOverRow: (idx: number | null) => void
217
+ setFocusedRowIndex: (idx: number) => void
218
+ onRemoveRow: (rowIdx: number) => void
219
+ onMoveRow: (rowIdx: number, direction: -1 | 1) => void
220
+ onSetCellSpan: (rowIdx: number, cellIdx: number, span: number) => void
221
+ onRemoveCell: (rowIdx: number, cellIdx: number) => void
222
+ onAddWidget: (widgetId: string, targetRowIdx?: number) => void
223
+ onConfigureCell: (rowIdx: number, cellIdx: number) => void
224
+ propsSchemaByWidgetId: Map<string, Record<string, unknown>>
225
+ widgets: Record<string, WidgetComponent>
226
+ widgetProps: WidgetProps
227
+ }) {
228
+ const gridRef = useRef<HTMLDivElement | null>(null)
229
+
230
+ return (
231
+ <div
232
+ onClick={() => setFocusedRowIndex(rowIdx)}
233
+ onDragOver={(e) => {
234
+ if (!e.dataTransfer.types.includes(WIDGET_DRAG_MIME)) return
235
+ e.preventDefault()
236
+ e.dataTransfer.dropEffect = "copy"
237
+ setDragOverRow(rowIdx)
238
+ }}
239
+ onDragLeave={(e) => {
240
+ if (e.currentTarget.contains(e.relatedTarget as Node)) return
241
+ setDragOverRow(null)
242
+ }}
243
+ onDrop={(e) => {
244
+ const id = e.dataTransfer.getData(WIDGET_DRAG_MIME)
245
+ setDragOverRow(null)
246
+ if (id) {
247
+ e.preventDefault()
248
+ onAddWidget(id, rowIdx)
249
+ }
250
+ }}
251
+ className={cn(
252
+ "group relative rounded-lg border-l-2 px-3 py-2 transition-colors",
253
+ focused
254
+ ? "border-l-primary bg-card"
255
+ : "bg-background hover:bg-card/60 border-l-transparent",
256
+ dragOver && "bg-primary/5 border-l-primary",
257
+ )}
258
+ >
259
+ <div className="mb-2 flex items-center justify-between">
260
+ <div className="text-muted-foreground flex items-center gap-2 text-[11px]">
261
+ <span className="font-mono">row {rowIdx + 1}</span>
262
+ <span className="bg-muted/60 rounded px-1 py-0.5 text-[10px]">
263
+ {row.row.length} {row.row.length === 1 ? "widget" : "widgets"}
264
+ </span>
265
+ </div>
266
+ <div className="opacity-0 transition-opacity group-hover:opacity-100">
267
+ <Button
268
+ variant="ghost"
269
+ size="icon-xs"
270
+ onClick={(e) => {
271
+ e.stopPropagation()
272
+ onMoveRow(rowIdx, -1)
273
+ }}
274
+ disabled={isFirst}
275
+ aria-label="Move row up"
276
+ >
277
+ <ChevronUp />
278
+ </Button>
279
+ <Button
280
+ variant="ghost"
281
+ size="icon-xs"
282
+ onClick={(e) => {
283
+ e.stopPropagation()
284
+ onMoveRow(rowIdx, 1)
285
+ }}
286
+ disabled={isLast}
287
+ aria-label="Move row down"
288
+ >
289
+ <ChevronDown />
290
+ </Button>
291
+ <Button
292
+ variant="ghost"
293
+ size="icon-xs"
294
+ onClick={(e) => {
295
+ e.stopPropagation()
296
+ onRemoveRow(rowIdx)
297
+ }}
298
+ aria-label={L.removeRow}
299
+ >
300
+ <Trash2 />
301
+ </Button>
302
+ </div>
303
+ </div>
304
+ {row.row.length === 0 ? (
305
+ <div
306
+ className={cn(
307
+ "rounded-md border border-dashed py-6 text-center text-[11px] transition-colors",
308
+ dragOver
309
+ ? "border-primary bg-primary/5 text-primary"
310
+ : "border-muted-foreground/30 text-muted-foreground",
311
+ )}
312
+ >
313
+ {dragOver ? L.canvasDropHint : L.canvasEmpty}
314
+ </div>
315
+ ) : (
316
+ <div ref={gridRef} data-row-grid>
317
+ <GridLayout>
318
+ {row.row.map((cell, cellIdx) => {
319
+ const span = cell.span ?? 12
320
+ const Widget = widgets[cell.widget]
321
+ const hasPropsSchema = propsSchemaByWidgetId.has(cell.widget)
322
+ const propCount = cell.props ? Object.keys(cell.props).length : 0
323
+ return (
324
+ <GridItem key={`${cell.widget}-${cellIdx}`} span={span}>
325
+ <div className="border-border bg-card relative overflow-hidden rounded-md border shadow-sm">
326
+ <div className="text-muted-foreground bg-muted/30 flex items-center justify-between gap-2 border-b px-2 py-1 text-[10px]">
327
+ <span className="truncate font-mono">{cell.widget}</span>
328
+ <div className="flex shrink-0 items-center gap-2">
329
+ <label className="text-muted-foreground/80 inline-flex items-center gap-0.5 font-mono">
330
+ <span className="sr-only">Column span</span>
331
+ <Select
332
+ value={String(span)}
333
+ onValueChange={(v) => onSetCellSpan(rowIdx, cellIdx, Number(v))}
334
+ >
335
+ <SelectTrigger
336
+ size="sm"
337
+ className="h-5 gap-0.5 border-none bg-transparent px-1 py-0 font-mono text-[10px] shadow-none focus:ring-0"
338
+ aria-label="Column span"
339
+ onClick={(e) => e.stopPropagation()}
340
+ >
341
+ <SelectValue />
342
+ </SelectTrigger>
343
+ <SelectContent>
344
+ {Array.from({ length: 12 }, (_, i) => i + 1).map((n) => (
345
+ <SelectItem key={n} value={String(n)} className="font-mono text-xs">
346
+ {n}
347
+ </SelectItem>
348
+ ))}
349
+ </SelectContent>
350
+ </Select>
351
+ <span aria-hidden="true">/12</span>
352
+ </label>
353
+ {hasPropsSchema && (
354
+ <Button
355
+ variant="ghost"
356
+ size="icon-xs"
357
+ onClick={(e) => {
358
+ e.stopPropagation()
359
+ onConfigureCell(rowIdx, cellIdx)
360
+ }}
361
+ aria-label="Configure widget props"
362
+ title={
363
+ propCount > 0
364
+ ? `Configure widget props (${propCount} set)`
365
+ : "Configure widget props"
366
+ }
367
+ className={cn(propCount > 0 && "text-primary")}
368
+ >
369
+ <SlidersHorizontal />
370
+ </Button>
371
+ )}
372
+ <Button
373
+ variant="ghost"
374
+ size="icon-xs"
375
+ onClick={(e) => {
376
+ e.stopPropagation()
377
+ onRemoveCell(rowIdx, cellIdx)
378
+ }}
379
+ aria-label="Remove widget"
380
+ >
381
+ <X />
382
+ </Button>
383
+ </div>
384
+ </div>
385
+ <div className="p-2">
386
+ <RenderedWidget
387
+ widget={Widget}
388
+ widgetId={cell.widget}
389
+ props={widgetProps}
390
+ cellProps={cell.props}
391
+ showEmptyHint
392
+ notBundledClassName="text-muted-foreground rounded border border-dashed p-3 text-center text-[11px]"
393
+ />
394
+ </div>
395
+ <ResizeHandle
396
+ rowIdx={rowIdx}
397
+ cellIdx={cellIdx}
398
+ span={span}
399
+ getGridEl={() => gridRef.current}
400
+ onSetCellSpan={onSetCellSpan}
401
+ />
402
+ </div>
403
+ </GridItem>
404
+ )
405
+ })}
406
+ </GridLayout>
407
+ </div>
408
+ )}
409
+ </div>
410
+ )
411
+ }
412
+
413
+ const CanvasRow = memo(CanvasRowImpl)
414
+
415
+ // -------------------------------------------------------------------------- //
416
+ // Resize handle
417
+ // -------------------------------------------------------------------------- //
418
+
419
+ function ResizeHandle({
420
+ rowIdx,
421
+ cellIdx,
422
+ span,
423
+ getGridEl,
424
+ onSetCellSpan,
425
+ }: {
426
+ rowIdx: number
427
+ cellIdx: number
428
+ span: number
429
+ getGridEl: () => HTMLElement | null
430
+ onSetCellSpan: (rowIdx: number, cellIdx: number, span: number) => void
431
+ }) {
432
+ const onPointerDown = useCallback(
433
+ (e: React.PointerEvent<HTMLDivElement>) => {
434
+ e.preventDefault()
435
+ e.stopPropagation()
436
+ const grid = getGridEl()
437
+ if (!grid) return
438
+ const rect = grid.getBoundingClientRect()
439
+ const colWidth = rect.width / 12
440
+ const startX = e.clientX
441
+ const startSpan = span
442
+ const onMove = (ev: PointerEvent) => {
443
+ const delta = ev.clientX - startX
444
+ const next = Math.round(startSpan + delta / colWidth)
445
+ onSetCellSpan(rowIdx, cellIdx, Math.max(1, Math.min(12, next)))
446
+ }
447
+ const onUp = () => {
448
+ window.removeEventListener("pointermove", onMove)
449
+ window.removeEventListener("pointerup", onUp)
450
+ }
451
+ window.addEventListener("pointermove", onMove)
452
+ window.addEventListener("pointerup", onUp)
453
+ },
454
+ [cellIdx, getGridEl, onSetCellSpan, rowIdx, span],
455
+ )
456
+
457
+ const onKeyDown = useCallback(
458
+ (e: React.KeyboardEvent<HTMLDivElement>) => {
459
+ const dir = e.key === "ArrowLeft" ? -1 : e.key === "ArrowRight" ? 1 : 0
460
+ if (dir === 0) return
461
+ e.preventDefault()
462
+ e.stopPropagation()
463
+ onSetCellSpan(rowIdx, cellIdx, Math.max(1, Math.min(12, span + dir)))
464
+ },
465
+ [cellIdx, onSetCellSpan, rowIdx, span],
466
+ )
467
+
468
+ return (
469
+ <div
470
+ onPointerDown={onPointerDown}
471
+ onClick={(e) => e.stopPropagation()}
472
+ onKeyDown={onKeyDown}
473
+ tabIndex={0}
474
+ className="hover:bg-primary/40 focus-visible:bg-primary/40 focus-visible:ring-primary/60 absolute top-0 right-0 h-full w-1.5 cursor-col-resize touch-none select-none focus-visible:ring-1 focus-visible:outline-none"
475
+ aria-label="Resize widget"
476
+ role="separator"
477
+ aria-orientation="vertical"
478
+ aria-valuenow={span}
479
+ aria-valuemin={1}
480
+ aria-valuemax={12}
481
+ />
482
+ )
483
+ }