@miragon/mcp-toolkit-ui 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +1 -1
  2. package/dist/app/builder/BuilderBanners.d.ts +19 -0
  3. package/dist/app/builder/BuilderBanners.d.ts.map +1 -0
  4. package/dist/app/builder/BuilderBanners.js +22 -0
  5. package/dist/app/builder/BuilderBanners.js.map +1 -0
  6. package/dist/app/builder/CanvasRow.d.ts +26 -0
  7. package/dist/app/builder/CanvasRow.d.ts.map +1 -0
  8. package/dist/app/builder/CanvasRow.js +98 -0
  9. package/dist/app/builder/CanvasRow.js.map +1 -0
  10. package/dist/app/builder/CellPropsSheet.d.ts +19 -0
  11. package/dist/app/builder/CellPropsSheet.d.ts.map +1 -0
  12. package/dist/app/builder/CellPropsSheet.js +28 -0
  13. package/dist/app/builder/CellPropsSheet.js.map +1 -0
  14. package/dist/app/builder/EmptyHint.d.ts +6 -0
  15. package/dist/app/builder/EmptyHint.d.ts.map +1 -0
  16. package/dist/app/builder/EmptyHint.js +10 -0
  17. package/dist/app/builder/EmptyHint.js.map +1 -0
  18. package/dist/app/builder/LayoutSurface.d.ts +27 -0
  19. package/dist/app/builder/LayoutSurface.d.ts.map +1 -0
  20. package/dist/app/builder/LayoutSurface.js +45 -0
  21. package/dist/app/builder/LayoutSurface.js.map +1 -0
  22. package/dist/app/builder/Workspace.d.ts +9 -3
  23. package/dist/app/builder/Workspace.d.ts.map +1 -1
  24. package/dist/app/builder/Workspace.js +3 -97
  25. package/dist/app/builder/Workspace.js.map +1 -1
  26. package/dist/app/builder/builder-reducer.d.ts.map +1 -1
  27. package/dist/app/builder/builder-reducer.js +176 -164
  28. package/dist/app/builder/builder-reducer.js.map +1 -1
  29. package/dist/app/builder/use-builder-catalogue.d.ts +33 -0
  30. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -0
  31. package/dist/app/builder/use-builder-catalogue.js +155 -0
  32. package/dist/app/builder/use-builder-catalogue.js.map +1 -0
  33. package/dist/app/builder/use-key-step-entries.d.ts +22 -0
  34. package/dist/app/builder/use-key-step-entries.d.ts.map +1 -0
  35. package/dist/app/builder/use-key-step-entries.js +46 -0
  36. package/dist/app/builder/use-key-step-entries.js.map +1 -0
  37. package/dist/app/builder/use-save-dashboard.d.ts +33 -0
  38. package/dist/app/builder/use-save-dashboard.d.ts.map +1 -0
  39. package/dist/app/builder/use-save-dashboard.js +78 -0
  40. package/dist/app/builder/use-save-dashboard.js.map +1 -0
  41. package/dist/app/host-bridge.d.ts +4 -4
  42. package/dist/app/host-bridge.js +2 -2
  43. package/dist/app/layout-builder.d.ts.map +1 -1
  44. package/dist/app/layout-builder.js +46 -277
  45. package/dist/app/layout-builder.js.map +1 -1
  46. package/dist/app/mcp-app-view-chrome.d.ts +77 -0
  47. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -0
  48. package/dist/app/mcp-app-view-chrome.js +53 -0
  49. package/dist/app/mcp-app-view-chrome.js.map +1 -0
  50. package/dist/app/mcp-app-view.d.ts +3 -9
  51. package/dist/app/mcp-app-view.d.ts.map +1 -1
  52. package/dist/app/mcp-app-view.js +4 -31
  53. package/dist/app/mcp-app-view.js.map +1 -1
  54. package/dist/render-cases-primitives.d.ts +17 -0
  55. package/dist/render-cases-primitives.d.ts.map +1 -0
  56. package/dist/render-cases-primitives.js +88 -0
  57. package/dist/render-cases-primitives.js.map +1 -0
  58. package/dist/render-cases.d.ts +4 -0
  59. package/dist/render-cases.d.ts.map +1 -0
  60. package/dist/render-cases.js +140 -0
  61. package/dist/render-cases.js.map +1 -0
  62. package/package.json +12 -7
  63. package/src/app/builder/BuilderBanners.tsx +61 -0
  64. package/src/app/builder/CanvasRow.tsx +316 -0
  65. package/src/app/builder/CellPropsSheet.tsx +47 -0
  66. package/src/app/builder/EmptyHint.tsx +29 -0
  67. package/src/app/builder/LayoutSurface.tsx +140 -0
  68. package/src/app/builder/Workspace.tsx +33 -342
  69. package/src/app/builder/builder-reducer.ts +204 -170
  70. package/src/app/builder/use-builder-catalogue.ts +187 -0
  71. package/src/app/builder/use-key-step-entries.ts +60 -0
  72. package/src/app/builder/use-save-dashboard.ts +97 -0
  73. package/src/app/host-bridge.tsx +4 -4
  74. package/src/app/layout-builder.tsx +92 -497
  75. package/src/app/mcp-app-view-chrome.tsx +204 -0
  76. package/src/app/mcp-app-view.tsx +32 -140
  77. package/src/lib/parse-tool-result.property.test.ts +90 -0
  78. package/src/render-cases-primitives.tsx +255 -0
  79. package/src/render-cases.tsx +239 -0
  80. package/src/render-smoke.test.tsx +100 -0
  81. package/src/ui-catalog.test.ts +52 -0
  82. package/ui-catalog.json +1 -1
@@ -0,0 +1,140 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Button, CountPill, DrillButton, FilterBar, GridItem, GridLayout, GroupCard, KpiGrid, ListFooter, LivePill, LocaleProvider, SectionHeading, ThemeProvider, themePresets, useLocale, WidgetHeader, } from "./index.js";
3
+ import { createStandaloneHostBridge, HostBridgeProvider, useHostBridge } from "./app/host-bridge.js";
4
+ import { HostModelContext } from "./app/adapt-data-widget.js";
5
+ import { WidgetFixtureHost } from "./app/widget-fixture.js";
6
+ import { PRIMITIVE_RENDER_CASES } from "./render-cases-primitives.js";
7
+ /**
8
+ * SSR render cases for every catalogued component/composite (FITNESS.md phase
9
+ * 5c). DATA module, not a test: `render-smoke.test.tsx` renders each case via
10
+ * `react-dom/server`'s `renderToStaticMarkup` (jsdom is deliberately renounced
11
+ * — SSR is the house render path) and enforces completeness against
12
+ * `ui-catalog.json` ∪ `ratchets/render-allowlist.json` (shrink-only).
13
+ *
14
+ * Each case mounts the component with minimal realistic props and, where the
15
+ * markup can carry one, a distinctive `marker` string the test asserts on.
16
+ * The primitive cases live in `render-cases-primitives.tsx` (file-length
17
+ * budget); this module aggregates them with the composed/host cases.
18
+ */
19
+ const noop = () => { };
20
+ /** Static bridge for the host-bridge cases — theme "dark" is the probed marker. */
21
+ const probeBridge = createStandaloneHostBridge({
22
+ callTool: () => Promise.resolve({}),
23
+ theme: "dark",
24
+ });
25
+ /** Proves HostBridgeProvider actually provides: reads the bridge from context. */
26
+ function BridgeThemeProbe() {
27
+ const bridge = useHostBridge();
28
+ return _jsxs("span", { children: ["RC:HostBridgeProvider ", bridge.theme ?? "unset"] });
29
+ }
30
+ /** Proves LocaleProvider actually provides: reads the locale from context. */
31
+ function LocaleProbe() {
32
+ const locale = useLocale();
33
+ return _jsxs("span", { children: ["RC:LocaleProvider ", locale] });
34
+ }
35
+ /** Minimal raw `({ data })` widget for the WidgetFixtureHost case. */
36
+ function FixtureProbeWidget({ data }) {
37
+ return _jsxs("div", { children: ["RC:WidgetFixtureHost ", data?.label ?? "no fixture"] });
38
+ }
39
+ export const RENDER_CASES = [
40
+ ...PRIMITIVE_RENDER_CASES,
41
+ // ── Composed components ──
42
+ {
43
+ name: "GridLayout",
44
+ marker: "RC:GridLayout",
45
+ render: () => (_jsxs(GridLayout, { children: [_jsx(GridItem, { span: 8, children: _jsx("div", { children: "RC:GridLayout" }) }), _jsx(GridItem, { span: 4, children: _jsx("div", { children: "side tile" }) })] })),
46
+ },
47
+ {
48
+ name: "GridItem",
49
+ marker: "RC:GridItem",
50
+ render: () => (_jsx(GridLayout, { children: _jsx(GridItem, { span: 6, children: _jsx("div", { children: "RC:GridItem" }) }) })),
51
+ },
52
+ {
53
+ name: "KpiGrid",
54
+ marker: "RC:KpiGrid",
55
+ render: () => (_jsx(KpiGrid, { boxed: true, header: { label: "RC:KpiGrid", badge: "live" }, cells: [
56
+ {
57
+ label: "Open",
58
+ value: 7,
59
+ tone: "danger",
60
+ trend: "+2 vs yesterday",
61
+ trendDirection: "up",
62
+ },
63
+ { label: "Done", value: 12, fraction: "/19", tone: "success" },
64
+ { label: "Total", value: 19, onClick: noop, ariaLabel: "Show all" },
65
+ ] })),
66
+ },
67
+ {
68
+ name: "LivePill",
69
+ marker: "RC:LivePill",
70
+ render: () => _jsx(LivePill, { tone: "success", children: "RC:LivePill" }),
71
+ },
72
+ {
73
+ name: "CountPill",
74
+ marker: "RC:CountPill",
75
+ render: () => _jsx(CountPill, { tone: "warning", children: "RC:CountPill" }),
76
+ },
77
+ {
78
+ name: "SectionHeading",
79
+ marker: "RC:SectionHeading",
80
+ render: () => _jsx(SectionHeading, { title: "RC:SectionHeading", hint: "3 shown" }),
81
+ },
82
+ {
83
+ name: "WidgetHeader",
84
+ marker: "RC:WidgetHeader",
85
+ render: () => (_jsx(WidgetHeader, { icon: "\u2713", iconTone: "info", title: "RC:WidgetHeader", sub: _jsx(LivePill, { children: "Live" }), actions: _jsx(Button, { size: "sm", variant: "ghost", onClick: noop, children: "Refresh" }) })),
86
+ },
87
+ {
88
+ name: "GroupCard",
89
+ marker: "RC:GroupCard",
90
+ render: () => (_jsx(GroupCard, { expanded: true, onToggle: noop, summary: _jsx("div", { children: "RC:GroupCard" }), children: _jsx("div", { children: "Expanded body row" }) })),
91
+ },
92
+ {
93
+ // The search marker lands in the input's value attribute.
94
+ name: "FilterBar",
95
+ marker: "RC:FilterBar",
96
+ render: () => (_jsx(FilterBar, { search: "RC:FilterBar", searchPlaceholder: "Filter tasks\u2026", onSearchChange: noop, chips: [
97
+ { id: "high", label: "high", count: 2, active: true },
98
+ { id: "low", label: "low" },
99
+ ], onChipToggle: noop })),
100
+ },
101
+ {
102
+ name: "DrillButton",
103
+ marker: "RC:DrillButton",
104
+ render: () => (_jsx(DrillButton, { onDrill: noop, icon: "\u2197", size: "sm", ariaLabel: "Open order o-1001", children: "RC:DrillButton" })),
105
+ },
106
+ {
107
+ name: "ListFooter",
108
+ marker: "of 42 tasks",
109
+ render: () => _jsx(ListFooter, { shown: 8, total: 42, hasMore: true, onLoadMore: noop, noun: "tasks" }),
110
+ },
111
+ // ── Host seam / providers ──
112
+ {
113
+ name: "HostBridgeProvider",
114
+ marker: "RC:HostBridgeProvider dark",
115
+ render: () => (_jsx(HostBridgeProvider, { bridge: probeBridge, children: _jsx(BridgeThemeProbe, {}) })),
116
+ },
117
+ {
118
+ // Outside an mcp-use view HostModelContext reports through the bridge in
119
+ // an effect (a no-op in SSR) and must still render its children.
120
+ name: "HostModelContext",
121
+ marker: "RC:HostModelContext",
122
+ render: () => (_jsx(HostBridgeProvider, { bridge: probeBridge, children: _jsx(HostModelContext, { content: "Viewing 3 open incidents", children: _jsx("div", { children: "RC:HostModelContext" }) }) })),
123
+ },
124
+ {
125
+ name: "ThemeProvider",
126
+ marker: "RC:ThemeProvider",
127
+ render: () => (_jsx(ThemeProvider, { theme: themePresets.miragon, mode: "dark", children: _jsx("div", { children: "RC:ThemeProvider" }) })),
128
+ },
129
+ {
130
+ name: "LocaleProvider",
131
+ marker: "RC:LocaleProvider de",
132
+ render: () => (_jsx(LocaleProvider, { locale: "de", children: _jsx(LocaleProbe, {}) })),
133
+ },
134
+ {
135
+ name: "WidgetFixtureHost",
136
+ marker: "RC:WidgetFixtureHost fixture-data",
137
+ render: () => (_jsx(WidgetFixtureHost, { widget: FixtureProbeWidget, data: { label: "fixture-data" } })),
138
+ },
139
+ ];
140
+ //# sourceMappingURL=render-cases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-cases.js","sourceRoot":"","sources":["../src/render-cases.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miragon/mcp-toolkit-ui",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "React UI for MCP servers: shadcn primitives, composite components, TanStack Query hooks, and the MCP App shell (McpAppView + WidgetRenderer).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,33 +38,37 @@
38
38
  "access": "public"
39
39
  },
40
40
  "peerDependencies": {
41
- "mcp-use": "2.0.4",
41
+ "mcp-use": "2.1.0",
42
42
  "react": "19.2.8",
43
43
  "react-dom": "19.2.8",
44
44
  "tailwindcss": "4.3.3",
45
- "@miragon/mcp-toolkit-core": "1.0.1"
45
+ "@miragon/mcp-toolkit-core": "1.2.0"
46
46
  },
47
47
  "dependencies": {
48
- "@base-ui/react": "1.6.0",
48
+ "@base-ui/react": "1.7.0",
49
49
  "@fontsource-variable/geist": "5.3.0",
50
50
  "@tanstack/react-query": "5.101.4",
51
51
  "class-variance-authority": "0.7.1",
52
52
  "clsx": "2.1.1",
53
- "lucide-react": "0.562.0",
53
+ "lucide-react": "1.27.0",
54
54
  "radix-ui": "1.6.7",
55
55
  "tailwind-merge": "3.6.0",
56
56
  "tw-animate-css": "1.4.0"
57
57
  },
58
58
  "devDependencies": {
59
+ "@stryker-mutator/core": "9.6.1",
60
+ "@stryker-mutator/vitest-runner": "9.6.1",
59
61
  "@types/react": "19.2.18",
60
62
  "@types/react-dom": "19.2.4",
61
- "mcp-use": "2.0.4",
63
+ "@vitest/coverage-v8": "4.1.10",
64
+ "fast-check": "4.9.0",
65
+ "mcp-use": "2.1.0",
62
66
  "react": "19.2.8",
63
67
  "react-dom": "19.2.8",
64
68
  "tailwindcss": "4.3.3",
65
69
  "typescript": "6.0.3",
66
70
  "vitest": "4.1.10",
67
- "@miragon/mcp-toolkit-core": "1.0.1"
71
+ "@miragon/mcp-toolkit-core": "1.2.0"
68
72
  },
69
73
  "engines": {
70
74
  "node": ">=22.22.2"
@@ -73,6 +77,7 @@
73
77
  "build": "tsc -p tsconfig.build.json",
74
78
  "typecheck": "tsc --noEmit",
75
79
  "test": "vitest run",
80
+ "test:coverage": "vitest run --coverage",
76
81
  "lint": "eslint src --max-warnings 0",
77
82
  "lint:fix": "eslint src --fix"
78
83
  }
@@ -0,0 +1,61 @@
1
+ import { AlertTriangle } from "lucide-react"
2
+ import type { LayoutBuilderLabels } from "./labels.js"
3
+
4
+ // -------------------------------------------------------------------------- //
5
+ // Builder banners — catalogue-fetch failure + pipeline validation warning
6
+ // -------------------------------------------------------------------------- //
7
+
8
+ /**
9
+ * Inline feedback strip under the toolbar. Surfaces two things the builder
10
+ * previously swallowed:
11
+ * - a `get-builder-catalogue` fetch failure (Finding [3]) — only console-
12
+ * logged before, so the user couldn't tell the palette was stale, and
13
+ * - pipeline validation issues (Finding [10]/[6]) the catalogue reports
14
+ * fail-soft but `render-view` would reject fail-hard.
15
+ *
16
+ * Renders nothing when there's no error and no issues, so the happy path is
17
+ * visually unchanged.
18
+ */
19
+ export function BuilderBanners({
20
+ L,
21
+ catalogueToolName,
22
+ refreshError,
23
+ validationIssues,
24
+ }: {
25
+ L: Required<LayoutBuilderLabels>
26
+ catalogueToolName: string
27
+ refreshError: string | null
28
+ validationIssues: string[]
29
+ }) {
30
+ if (!refreshError && validationIssues.length === 0) return null
31
+ return (
32
+ <div className="flex flex-col gap-2" role="status" aria-live="polite">
33
+ {refreshError && (
34
+ <div className="border-destructive/30 bg-destructive/10 text-destructive flex items-start gap-2 rounded-md border px-3 py-2 text-xs">
35
+ <AlertTriangle className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
36
+ <div className="min-w-0">
37
+ <p className="font-medium">{L.catalogueError}</p>
38
+ <p className="text-destructive/80 mt-0.5 break-words">
39
+ <span className="font-mono">{catalogueToolName}</span>: {refreshError}
40
+ </p>
41
+ </div>
42
+ </div>
43
+ )}
44
+ {validationIssues.length > 0 && (
45
+ <div className="flex items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-400">
46
+ <AlertTriangle className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
47
+ <div className="min-w-0">
48
+ <p className="font-medium">{L.validationWarning}</p>
49
+ <ul className="mt-0.5 list-inside list-disc space-y-0.5">
50
+ {validationIssues.map((issue, idx) => (
51
+ <li key={idx} className="break-words">
52
+ {issue}
53
+ </li>
54
+ ))}
55
+ </ul>
56
+ </div>
57
+ </div>
58
+ )}
59
+ </div>
60
+ )
61
+ }
@@ -0,0 +1,316 @@
1
+ import { memo, useCallback, useRef } from "react"
2
+ import { ChevronDown, ChevronUp, SlidersHorizontal, Trash2, X } from "lucide-react"
3
+ import type { RowDef, WidgetProps } from "@miragon/mcp-toolkit-core"
4
+ import { GridItem, GridLayout } from "../../components/GridLayout.js"
5
+ import { Button } from "../../primitives/button.js"
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "../../primitives/select.js"
13
+ import { cn } from "../../lib/utils.js"
14
+ import { RenderedWidget, type WidgetComponent } from "../widget-renderer.js"
15
+ import type { LayoutBuilderLabels } from "./labels.js"
16
+ import { WIDGET_DRAG_MIME } from "./labels.js"
17
+
18
+ // -------------------------------------------------------------------------- //
19
+ // Canvas row — drop target + per-cell controls + resize handle
20
+ // -------------------------------------------------------------------------- //
21
+
22
+ function CanvasRowImpl({
23
+ L,
24
+ row,
25
+ rowIdx,
26
+ focused,
27
+ isFirst,
28
+ isLast,
29
+ dragOver,
30
+ setDragOverRow,
31
+ setFocusedRowIndex,
32
+ onRemoveRow,
33
+ onMoveRow,
34
+ onSetCellSpan,
35
+ onRemoveCell,
36
+ onAddWidget,
37
+ onConfigureCell,
38
+ propsSchemaByWidgetId,
39
+ widgets,
40
+ widgetProps,
41
+ }: {
42
+ L: Required<LayoutBuilderLabels>
43
+ row: RowDef
44
+ rowIdx: number
45
+ focused: boolean
46
+ isFirst: boolean
47
+ isLast: boolean
48
+ dragOver: boolean
49
+ setDragOverRow: (idx: number | null) => void
50
+ setFocusedRowIndex: (idx: number) => void
51
+ onRemoveRow: (rowIdx: number) => void
52
+ onMoveRow: (rowIdx: number, direction: -1 | 1) => void
53
+ onSetCellSpan: (rowIdx: number, cellIdx: number, span: number) => void
54
+ onRemoveCell: (rowIdx: number, cellIdx: number) => void
55
+ onAddWidget: (widgetId: string, targetRowIdx?: number) => void
56
+ onConfigureCell: (rowIdx: number, cellIdx: number) => void
57
+ propsSchemaByWidgetId: Map<string, Record<string, unknown>>
58
+ widgets: Record<string, WidgetComponent>
59
+ widgetProps: WidgetProps
60
+ }) {
61
+ const gridRef = useRef<HTMLDivElement | null>(null)
62
+
63
+ return (
64
+ <div
65
+ onClick={() => setFocusedRowIndex(rowIdx)}
66
+ onDragOver={(e) => {
67
+ if (!e.dataTransfer.types.includes(WIDGET_DRAG_MIME)) return
68
+ e.preventDefault()
69
+ e.dataTransfer.dropEffect = "copy"
70
+ setDragOverRow(rowIdx)
71
+ }}
72
+ onDragLeave={(e) => {
73
+ if (e.currentTarget.contains(e.relatedTarget as Node)) return
74
+ setDragOverRow(null)
75
+ }}
76
+ onDrop={(e) => {
77
+ const id = e.dataTransfer.getData(WIDGET_DRAG_MIME)
78
+ setDragOverRow(null)
79
+ if (id) {
80
+ e.preventDefault()
81
+ onAddWidget(id, rowIdx)
82
+ }
83
+ }}
84
+ className={cn(
85
+ "group relative rounded-lg border-l-2 px-3 py-2 transition-colors",
86
+ focused
87
+ ? "border-l-primary bg-card"
88
+ : "bg-background hover:bg-card/60 border-l-transparent",
89
+ dragOver && "bg-primary/5 border-l-primary",
90
+ )}
91
+ >
92
+ <div className="mb-2 flex items-center justify-between">
93
+ <div className="text-muted-foreground flex items-center gap-2 text-[11px]">
94
+ <span className="font-mono">row {rowIdx + 1}</span>
95
+ <span className="bg-muted/60 rounded px-1 py-0.5 text-[10px]">
96
+ {row.row.length} {row.row.length === 1 ? "widget" : "widgets"}
97
+ </span>
98
+ </div>
99
+ <div className="opacity-0 transition-opacity group-hover:opacity-100">
100
+ <Button
101
+ variant="ghost"
102
+ size="icon-xs"
103
+ onClick={(e) => {
104
+ e.stopPropagation()
105
+ onMoveRow(rowIdx, -1)
106
+ }}
107
+ disabled={isFirst}
108
+ aria-label="Move row up"
109
+ >
110
+ <ChevronUp />
111
+ </Button>
112
+ <Button
113
+ variant="ghost"
114
+ size="icon-xs"
115
+ onClick={(e) => {
116
+ e.stopPropagation()
117
+ onMoveRow(rowIdx, 1)
118
+ }}
119
+ disabled={isLast}
120
+ aria-label="Move row down"
121
+ >
122
+ <ChevronDown />
123
+ </Button>
124
+ <Button
125
+ variant="ghost"
126
+ size="icon-xs"
127
+ onClick={(e) => {
128
+ e.stopPropagation()
129
+ onRemoveRow(rowIdx)
130
+ }}
131
+ aria-label={L.removeRow}
132
+ >
133
+ <Trash2 />
134
+ </Button>
135
+ </div>
136
+ </div>
137
+ {row.row.length === 0 ? (
138
+ <div
139
+ className={cn(
140
+ "rounded-md border border-dashed py-6 text-center text-[11px] transition-colors",
141
+ dragOver
142
+ ? "border-primary bg-primary/5 text-primary"
143
+ : "border-muted-foreground/30 text-muted-foreground",
144
+ )}
145
+ >
146
+ {dragOver ? L.canvasDropHint : L.canvasEmpty}
147
+ </div>
148
+ ) : (
149
+ <div ref={gridRef} data-row-grid>
150
+ <GridLayout>
151
+ {row.row.map((cell, cellIdx) => {
152
+ const span = cell.span ?? 12
153
+ const Widget = widgets[cell.widget]
154
+ const hasPropsSchema = propsSchemaByWidgetId.has(cell.widget)
155
+ const propCount = cell.props ? Object.keys(cell.props).length : 0
156
+ return (
157
+ <GridItem key={`${cell.widget}-${cellIdx}`} span={span}>
158
+ <div className="border-border bg-card relative overflow-hidden rounded-md border shadow-sm">
159
+ <div className="text-muted-foreground bg-muted/30 flex items-center justify-between gap-2 border-b px-2 py-1 text-[10px]">
160
+ <span className="truncate font-mono">{cell.widget}</span>
161
+ <div className="flex shrink-0 items-center gap-2">
162
+ <label className="text-muted-foreground/80 inline-flex items-center gap-0.5 font-mono">
163
+ <span className="sr-only">Column span</span>
164
+ <Select
165
+ value={String(span)}
166
+ onValueChange={(v) => onSetCellSpan(rowIdx, cellIdx, Number(v))}
167
+ >
168
+ <SelectTrigger
169
+ size="sm"
170
+ className="h-5 gap-0.5 border-none bg-transparent px-1 py-0 font-mono text-[10px] shadow-none focus:ring-0"
171
+ aria-label="Column span"
172
+ onClick={(e) => e.stopPropagation()}
173
+ >
174
+ <SelectValue />
175
+ </SelectTrigger>
176
+ <SelectContent>
177
+ {Array.from({ length: 12 }, (_, i) => i + 1).map((n) => (
178
+ <SelectItem key={n} value={String(n)} className="font-mono text-xs">
179
+ {n}
180
+ </SelectItem>
181
+ ))}
182
+ </SelectContent>
183
+ </Select>
184
+ <span aria-hidden="true">/12</span>
185
+ </label>
186
+ {hasPropsSchema && (
187
+ <Button
188
+ variant="ghost"
189
+ size="icon-xs"
190
+ onClick={(e) => {
191
+ e.stopPropagation()
192
+ onConfigureCell(rowIdx, cellIdx)
193
+ }}
194
+ aria-label="Configure widget props"
195
+ title={
196
+ propCount > 0
197
+ ? `Configure widget props (${propCount} set)`
198
+ : "Configure widget props"
199
+ }
200
+ className={cn(propCount > 0 && "text-primary")}
201
+ >
202
+ <SlidersHorizontal />
203
+ </Button>
204
+ )}
205
+ <Button
206
+ variant="ghost"
207
+ size="icon-xs"
208
+ onClick={(e) => {
209
+ e.stopPropagation()
210
+ onRemoveCell(rowIdx, cellIdx)
211
+ }}
212
+ aria-label="Remove widget"
213
+ >
214
+ <X />
215
+ </Button>
216
+ </div>
217
+ </div>
218
+ <div className="p-2">
219
+ <RenderedWidget
220
+ widget={Widget}
221
+ widgetId={cell.widget}
222
+ props={widgetProps}
223
+ cellProps={cell.props}
224
+ showEmptyHint
225
+ notBundledClassName="text-muted-foreground rounded border border-dashed p-3 text-center text-[11px]"
226
+ />
227
+ </div>
228
+ <ResizeHandle
229
+ rowIdx={rowIdx}
230
+ cellIdx={cellIdx}
231
+ span={span}
232
+ getGridEl={() => gridRef.current}
233
+ onSetCellSpan={onSetCellSpan}
234
+ />
235
+ </div>
236
+ </GridItem>
237
+ )
238
+ })}
239
+ </GridLayout>
240
+ </div>
241
+ )}
242
+ </div>
243
+ )
244
+ }
245
+
246
+ export const CanvasRow = memo(CanvasRowImpl)
247
+
248
+ // -------------------------------------------------------------------------- //
249
+ // Resize handle
250
+ // -------------------------------------------------------------------------- //
251
+
252
+ function ResizeHandle({
253
+ rowIdx,
254
+ cellIdx,
255
+ span,
256
+ getGridEl,
257
+ onSetCellSpan,
258
+ }: {
259
+ rowIdx: number
260
+ cellIdx: number
261
+ span: number
262
+ getGridEl: () => HTMLElement | null
263
+ onSetCellSpan: (rowIdx: number, cellIdx: number, span: number) => void
264
+ }) {
265
+ const onPointerDown = useCallback(
266
+ (e: React.PointerEvent<HTMLDivElement>) => {
267
+ e.preventDefault()
268
+ e.stopPropagation()
269
+ const grid = getGridEl()
270
+ if (!grid) return
271
+ const rect = grid.getBoundingClientRect()
272
+ const colWidth = rect.width / 12
273
+ const startX = e.clientX
274
+ const startSpan = span
275
+ const onMove = (ev: PointerEvent) => {
276
+ const delta = ev.clientX - startX
277
+ const next = Math.round(startSpan + delta / colWidth)
278
+ onSetCellSpan(rowIdx, cellIdx, Math.max(1, Math.min(12, next)))
279
+ }
280
+ const onUp = () => {
281
+ window.removeEventListener("pointermove", onMove)
282
+ window.removeEventListener("pointerup", onUp)
283
+ }
284
+ window.addEventListener("pointermove", onMove)
285
+ window.addEventListener("pointerup", onUp)
286
+ },
287
+ [cellIdx, getGridEl, onSetCellSpan, rowIdx, span],
288
+ )
289
+
290
+ const onKeyDown = useCallback(
291
+ (e: React.KeyboardEvent<HTMLDivElement>) => {
292
+ const dir = e.key === "ArrowLeft" ? -1 : e.key === "ArrowRight" ? 1 : 0
293
+ if (dir === 0) return
294
+ e.preventDefault()
295
+ e.stopPropagation()
296
+ onSetCellSpan(rowIdx, cellIdx, Math.max(1, Math.min(12, span + dir)))
297
+ },
298
+ [cellIdx, onSetCellSpan, rowIdx, span],
299
+ )
300
+
301
+ return (
302
+ <div
303
+ onPointerDown={onPointerDown}
304
+ onClick={(e) => e.stopPropagation()}
305
+ onKeyDown={onKeyDown}
306
+ tabIndex={0}
307
+ 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"
308
+ aria-label="Resize widget"
309
+ role="separator"
310
+ aria-orientation="vertical"
311
+ aria-valuenow={span}
312
+ aria-valuemin={1}
313
+ aria-valuemax={12}
314
+ />
315
+ )
316
+ }
@@ -0,0 +1,47 @@
1
+ import type { RowDef } from "@miragon/mcp-toolkit-core"
2
+ import { WidgetPropsSheet } from "../widget-props-sheet.js"
3
+
4
+ // -------------------------------------------------------------------------- //
5
+ // Cell props sheet — mounts the WidgetPropsSheet for the configured cell
6
+ // -------------------------------------------------------------------------- //
7
+
8
+ /**
9
+ * Mounted once at the LayoutBuilder root so that exactly one props editor
10
+ * can be open at a time across the entire workspace — including across
11
+ * tabs. `configuringCell` addresses `{ rowIdx, cellIdx }` on the currently
12
+ * active draft surface (rows or active tab's rows); renders nothing when no
13
+ * cell is being configured or the address no longer resolves.
14
+ */
15
+ export function CellPropsSheet({
16
+ configuringCell,
17
+ activeRows,
18
+ propsSchemaByWidgetId,
19
+ onApply,
20
+ onClose,
21
+ }: {
22
+ configuringCell: { rowIdx: number; cellIdx: number } | null
23
+ activeRows: RowDef[]
24
+ propsSchemaByWidgetId: Map<string, Record<string, unknown>>
25
+ onApply: (rowIdx: number, cellIdx: number, next: Record<string, unknown> | undefined) => void
26
+ onClose: () => void
27
+ }) {
28
+ if (!configuringCell) return null
29
+ const cell = activeRows[configuringCell.rowIdx]?.row[configuringCell.cellIdx]
30
+ if (!cell) return null
31
+ const schema = propsSchemaByWidgetId.get(cell.widget)
32
+ return (
33
+ <WidgetPropsSheet
34
+ open
35
+ onOpenChange={(open) => {
36
+ if (!open) onClose()
37
+ }}
38
+ widgetId={cell.widget}
39
+ schema={schema}
40
+ value={cell.props}
41
+ onApply={(next) => {
42
+ onApply(configuringCell.rowIdx, configuringCell.cellIdx, next)
43
+ onClose()
44
+ }}
45
+ />
46
+ )
47
+ }
@@ -0,0 +1,29 @@
1
+ import { Layers, SlidersHorizontal } from "lucide-react"
2
+ import { Button } from "../../primitives/button.js"
3
+ import type { LayoutBuilderLabels } from "./labels.js"
4
+
5
+ // -------------------------------------------------------------------------- //
6
+ // Empty hint — shown on the Layout view before any widget is reachable
7
+ // -------------------------------------------------------------------------- //
8
+
9
+ export function EmptyHint({
10
+ L,
11
+ onGotoPipeline,
12
+ }: {
13
+ L: Required<LayoutBuilderLabels>
14
+ onGotoPipeline: () => void
15
+ }) {
16
+ return (
17
+ <div className="border-muted-foreground/30 text-muted-foreground flex flex-col items-center gap-2 rounded-md border border-dashed p-10 text-sm">
18
+ <Layers className="size-5" />
19
+ <p>No widgets reachable yet.</p>
20
+ <p className="text-xs">
21
+ Open the <span className="font-medium">{L.viewPipelineTab}</span> tab to seed keys or add a
22
+ pipeline step — the palette unlocks as soon as a widget's contract is satisfied.
23
+ </p>
24
+ <Button variant="outline" size="sm" onClick={onGotoPipeline} className="mt-1">
25
+ <SlidersHorizontal /> Open {L.viewPipelineTab}
26
+ </Button>
27
+ </div>
28
+ )
29
+ }