@miragon/mcp-toolkit-ui 1.1.0 → 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.
- package/dist/app/builder/BuilderBanners.d.ts +19 -0
- package/dist/app/builder/BuilderBanners.d.ts.map +1 -0
- package/dist/app/builder/BuilderBanners.js +22 -0
- package/dist/app/builder/BuilderBanners.js.map +1 -0
- package/dist/app/builder/CanvasRow.d.ts +26 -0
- package/dist/app/builder/CanvasRow.d.ts.map +1 -0
- package/dist/app/builder/CanvasRow.js +98 -0
- package/dist/app/builder/CanvasRow.js.map +1 -0
- package/dist/app/builder/CellPropsSheet.d.ts +19 -0
- package/dist/app/builder/CellPropsSheet.d.ts.map +1 -0
- package/dist/app/builder/CellPropsSheet.js +28 -0
- package/dist/app/builder/CellPropsSheet.js.map +1 -0
- package/dist/app/builder/EmptyHint.d.ts +6 -0
- package/dist/app/builder/EmptyHint.d.ts.map +1 -0
- package/dist/app/builder/EmptyHint.js +10 -0
- package/dist/app/builder/EmptyHint.js.map +1 -0
- package/dist/app/builder/LayoutSurface.d.ts +27 -0
- package/dist/app/builder/LayoutSurface.d.ts.map +1 -0
- package/dist/app/builder/LayoutSurface.js +45 -0
- package/dist/app/builder/LayoutSurface.js.map +1 -0
- package/dist/app/builder/Workspace.d.ts +9 -3
- package/dist/app/builder/Workspace.d.ts.map +1 -1
- package/dist/app/builder/Workspace.js +3 -97
- package/dist/app/builder/Workspace.js.map +1 -1
- package/dist/app/builder/builder-reducer.d.ts.map +1 -1
- package/dist/app/builder/builder-reducer.js +176 -164
- package/dist/app/builder/builder-reducer.js.map +1 -1
- package/dist/app/builder/use-builder-catalogue.d.ts +33 -0
- package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -0
- package/dist/app/builder/use-builder-catalogue.js +155 -0
- package/dist/app/builder/use-builder-catalogue.js.map +1 -0
- package/dist/app/builder/use-key-step-entries.d.ts +22 -0
- package/dist/app/builder/use-key-step-entries.d.ts.map +1 -0
- package/dist/app/builder/use-key-step-entries.js +46 -0
- package/dist/app/builder/use-key-step-entries.js.map +1 -0
- package/dist/app/builder/use-save-dashboard.d.ts +33 -0
- package/dist/app/builder/use-save-dashboard.d.ts.map +1 -0
- package/dist/app/builder/use-save-dashboard.js +78 -0
- package/dist/app/builder/use-save-dashboard.js.map +1 -0
- package/dist/app/layout-builder.d.ts.map +1 -1
- package/dist/app/layout-builder.js +46 -277
- package/dist/app/layout-builder.js.map +1 -1
- package/dist/app/mcp-app-view-chrome.d.ts +77 -0
- package/dist/app/mcp-app-view-chrome.d.ts.map +1 -0
- package/dist/app/mcp-app-view-chrome.js +53 -0
- package/dist/app/mcp-app-view-chrome.js.map +1 -0
- package/dist/app/mcp-app-view.d.ts +3 -9
- package/dist/app/mcp-app-view.d.ts.map +1 -1
- package/dist/app/mcp-app-view.js +4 -31
- package/dist/app/mcp-app-view.js.map +1 -1
- package/dist/render-cases-primitives.d.ts +17 -0
- package/dist/render-cases-primitives.d.ts.map +1 -0
- package/dist/render-cases-primitives.js +88 -0
- package/dist/render-cases-primitives.js.map +1 -0
- package/dist/render-cases.d.ts +4 -0
- package/dist/render-cases.d.ts.map +1 -0
- package/dist/render-cases.js +140 -0
- package/dist/render-cases.js.map +1 -0
- package/package.json +10 -5
- package/src/app/builder/BuilderBanners.tsx +61 -0
- package/src/app/builder/CanvasRow.tsx +316 -0
- package/src/app/builder/CellPropsSheet.tsx +47 -0
- package/src/app/builder/EmptyHint.tsx +29 -0
- package/src/app/builder/LayoutSurface.tsx +140 -0
- package/src/app/builder/Workspace.tsx +33 -342
- package/src/app/builder/builder-reducer.ts +204 -170
- package/src/app/builder/use-builder-catalogue.ts +187 -0
- package/src/app/builder/use-key-step-entries.ts +60 -0
- package/src/app/builder/use-save-dashboard.ts +97 -0
- package/src/app/layout-builder.tsx +92 -497
- package/src/app/mcp-app-view-chrome.tsx +204 -0
- package/src/app/mcp-app-view.tsx +32 -140
- package/src/lib/parse-tool-result.property.test.ts +90 -0
- package/src/render-cases-primitives.tsx +255 -0
- package/src/render-cases.tsx +239 -0
- package/src/render-smoke.test.tsx +100 -0
- package/src/ui-catalog.test.ts +52 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Plus, X } from "lucide-react"
|
|
2
|
+
import { Button } from "../../primitives/button.js"
|
|
3
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../primitives/tabs.js"
|
|
4
|
+
import { cn } from "../../lib/utils.js"
|
|
5
|
+
import type { DraftLayout } from "./builder-model.js"
|
|
6
|
+
import type { LayoutBuilderLabels } from "./labels.js"
|
|
7
|
+
import { Workspace, type WorkspaceProps } from "./Workspace.js"
|
|
8
|
+
|
|
9
|
+
// -------------------------------------------------------------------------- //
|
|
10
|
+
// Layout surface — single workspace, or the tab strip (with inline rename)
|
|
11
|
+
// wrapping one workspace per tab
|
|
12
|
+
// -------------------------------------------------------------------------- //
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The "Layout" view's canvas surface, extracted from `layout-builder.tsx`.
|
|
16
|
+
*
|
|
17
|
+
* Renders either a single `Workspace` (rows draft) or the tab strip with
|
|
18
|
+
* the inline-rename editor plus one `Workspace` per tab (tabs draft). All
|
|
19
|
+
* state stays in the LayoutBuilder container — rename feedback included —
|
|
20
|
+
* so unmounting this surface (e.g. switching to the Pipeline view) loses
|
|
21
|
+
* nothing. Props in, callbacks out; the shared `workspace` bag is spread
|
|
22
|
+
* into `<Workspace>` untouched so its memo semantics are unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export function LayoutSurface({
|
|
25
|
+
L,
|
|
26
|
+
draft,
|
|
27
|
+
activeTabIndex,
|
|
28
|
+
setActiveTabIndex,
|
|
29
|
+
editingTabIdx,
|
|
30
|
+
setEditingTabIdx,
|
|
31
|
+
renameRejected,
|
|
32
|
+
commitRename,
|
|
33
|
+
onAddTab,
|
|
34
|
+
onRemoveTab,
|
|
35
|
+
workspace,
|
|
36
|
+
}: {
|
|
37
|
+
L: Required<LayoutBuilderLabels>
|
|
38
|
+
draft: DraftLayout
|
|
39
|
+
activeTabIndex: number
|
|
40
|
+
setActiveTabIndex: (idx: number) => void
|
|
41
|
+
editingTabIdx: number | null
|
|
42
|
+
setEditingTabIdx: (idx: number | null) => void
|
|
43
|
+
renameRejected: boolean
|
|
44
|
+
commitRename: (tabIdx: number, label: string) => void
|
|
45
|
+
onAddTab: () => void
|
|
46
|
+
onRemoveTab: (tabIdx: number) => void
|
|
47
|
+
workspace: Omit<WorkspaceProps, "showAddTabButton" | "onAddTab">
|
|
48
|
+
}) {
|
|
49
|
+
if (draft.kind !== "tabs") {
|
|
50
|
+
return <Workspace {...workspace} showAddTabButton onAddTab={onAddTab} />
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
<Tabs
|
|
54
|
+
value={draft.tabs[activeTabIndex]?.label ?? ""}
|
|
55
|
+
onValueChange={(label) => {
|
|
56
|
+
const idx = draft.tabs.findIndex((t) => t.label === label)
|
|
57
|
+
if (idx >= 0) setActiveTabIndex(idx)
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<div className="mb-3 flex items-center gap-1">
|
|
61
|
+
<TabsList>
|
|
62
|
+
{draft.tabs.map((tab, idx) => (
|
|
63
|
+
<div key={tab.label} className="group relative inline-flex">
|
|
64
|
+
<TabsTrigger
|
|
65
|
+
value={tab.label}
|
|
66
|
+
className="pr-7"
|
|
67
|
+
onDoubleClick={() => setEditingTabIdx(idx)}
|
|
68
|
+
title="Double-click to rename"
|
|
69
|
+
>
|
|
70
|
+
{editingTabIdx === idx ? (
|
|
71
|
+
<input
|
|
72
|
+
autoFocus
|
|
73
|
+
defaultValue={tab.label}
|
|
74
|
+
aria-invalid={renameRejected}
|
|
75
|
+
onClick={(e) => e.stopPropagation()}
|
|
76
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
77
|
+
onKeyDown={(e) => {
|
|
78
|
+
if (e.key === "Enter") {
|
|
79
|
+
e.preventDefault()
|
|
80
|
+
commitRename(idx, e.currentTarget.value)
|
|
81
|
+
} else if (e.key === "Escape") {
|
|
82
|
+
e.preventDefault()
|
|
83
|
+
setEditingTabIdx(null)
|
|
84
|
+
}
|
|
85
|
+
}}
|
|
86
|
+
onBlur={(e) => {
|
|
87
|
+
commitRename(idx, e.currentTarget.value)
|
|
88
|
+
}}
|
|
89
|
+
className={cn(
|
|
90
|
+
"w-24 border-none bg-transparent p-0 text-inherit outline-none focus:ring-0",
|
|
91
|
+
renameRejected && "text-destructive",
|
|
92
|
+
)}
|
|
93
|
+
size={Math.max(tab.label.length + 1, 6)}
|
|
94
|
+
/>
|
|
95
|
+
) : (
|
|
96
|
+
<span>{tab.label}</span>
|
|
97
|
+
)}
|
|
98
|
+
</TabsTrigger>
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
aria-label={L.removeTab}
|
|
102
|
+
title={L.removeTab}
|
|
103
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
104
|
+
onClick={(e) => {
|
|
105
|
+
e.stopPropagation()
|
|
106
|
+
onRemoveTab(idx)
|
|
107
|
+
}}
|
|
108
|
+
className="text-muted-foreground/60 hover:text-foreground absolute top-1/2 right-1 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity group-hover:opacity-100 data-[state=active]:opacity-100"
|
|
109
|
+
>
|
|
110
|
+
<X className="size-3" />
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
))}
|
|
114
|
+
</TabsList>
|
|
115
|
+
<Button
|
|
116
|
+
variant="ghost"
|
|
117
|
+
size="icon-sm"
|
|
118
|
+
onClick={onAddTab}
|
|
119
|
+
aria-label={L.addTab}
|
|
120
|
+
title={L.addTab}
|
|
121
|
+
>
|
|
122
|
+
<Plus />
|
|
123
|
+
</Button>
|
|
124
|
+
<span className="text-muted-foreground/70 ml-2 hidden text-[10px] sm:inline">
|
|
125
|
+
double-click a tab to rename
|
|
126
|
+
</span>
|
|
127
|
+
</div>
|
|
128
|
+
{renameRejected && editingTabIdx !== null && (
|
|
129
|
+
<p className="text-destructive -mt-1 mb-2 text-[11px]" role="alert">
|
|
130
|
+
{L.renameDuplicate}
|
|
131
|
+
</p>
|
|
132
|
+
)}
|
|
133
|
+
{draft.tabs.map((tab, idx) => (
|
|
134
|
+
<TabsContent key={tab.label} value={tab.label} className="mt-0">
|
|
135
|
+
{idx === activeTabIndex ? <Workspace {...workspace} /> : null}
|
|
136
|
+
</TabsContent>
|
|
137
|
+
))}
|
|
138
|
+
</Tabs>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -1,34 +1,46 @@
|
|
|
1
|
-
import { memo
|
|
2
|
-
import {
|
|
3
|
-
ChevronDown,
|
|
4
|
-
ChevronUp,
|
|
5
|
-
GripVertical,
|
|
6
|
-
Plus,
|
|
7
|
-
SlidersHorizontal,
|
|
8
|
-
Trash2,
|
|
9
|
-
X,
|
|
10
|
-
} from "lucide-react"
|
|
1
|
+
import { memo } from "react"
|
|
2
|
+
import { GripVertical, Plus } from "lucide-react"
|
|
11
3
|
import type { ReachableWidget, RowDef, WidgetProps } from "@miragon/mcp-toolkit-core"
|
|
12
|
-
import { GridItem, GridLayout } from "../../components/GridLayout.js"
|
|
13
4
|
import { Badge } from "../../primitives/badge.js"
|
|
14
5
|
import { Button } from "../../primitives/button.js"
|
|
15
6
|
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"
|
|
7
|
+
import type { WidgetComponent } from "../widget-renderer.js"
|
|
25
8
|
import type { LayoutBuilderLabels } from "./labels.js"
|
|
26
9
|
import { WIDGET_DRAG_MIME } from "./labels.js"
|
|
10
|
+
import { CanvasRow } from "./CanvasRow.js"
|
|
27
11
|
|
|
28
12
|
// -------------------------------------------------------------------------- //
|
|
29
13
|
// Workspace — palette + canvas (palette sticky on lg+)
|
|
30
14
|
// -------------------------------------------------------------------------- //
|
|
31
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Props for the workspace surface. Exported so the LayoutSurface container
|
|
18
|
+
* can pass the shared prop bag through to `<Workspace {...workspace} />`
|
|
19
|
+
* without re-declaring the contract.
|
|
20
|
+
*/
|
|
21
|
+
export interface WorkspaceProps {
|
|
22
|
+
L: Required<LayoutBuilderLabels>
|
|
23
|
+
paletteByApp: [string, ReachableWidget[]][]
|
|
24
|
+
paletteCount: number
|
|
25
|
+
activeRows: RowDef[]
|
|
26
|
+
focusedRowIndex: number
|
|
27
|
+
setFocusedRowIndex: (idx: number) => void
|
|
28
|
+
dragOverRow: number | null
|
|
29
|
+
setDragOverRow: (idx: number | null) => void
|
|
30
|
+
onAddRow: () => void
|
|
31
|
+
onRemoveRow: (rowIdx: number) => void
|
|
32
|
+
onMoveRow: (rowIdx: number, direction: -1 | 1) => void
|
|
33
|
+
onSetCellSpan: (rowIdx: number, cellIdx: number, span: number) => void
|
|
34
|
+
onRemoveCell: (rowIdx: number, cellIdx: number) => void
|
|
35
|
+
onAddWidget: (widgetId: string, targetRowIdx?: number) => void
|
|
36
|
+
onConfigureCell: (rowIdx: number, cellIdx: number) => void
|
|
37
|
+
propsSchemaByWidgetId: Map<string, Record<string, unknown>>
|
|
38
|
+
widgets: Record<string, WidgetComponent>
|
|
39
|
+
widgetProps: WidgetProps
|
|
40
|
+
showAddTabButton?: boolean
|
|
41
|
+
onAddTab?: () => void
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
function WorkspaceImpl({
|
|
33
45
|
L,
|
|
34
46
|
paletteByApp,
|
|
@@ -50,28 +62,7 @@ function WorkspaceImpl({
|
|
|
50
62
|
widgetProps,
|
|
51
63
|
showAddTabButton,
|
|
52
64
|
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
|
-
}) {
|
|
65
|
+
}: WorkspaceProps) {
|
|
75
66
|
return (
|
|
76
67
|
<div className="grid grid-cols-12 gap-4">
|
|
77
68
|
<aside className="col-span-12 lg:col-span-3 xl:col-span-2">
|
|
@@ -181,303 +172,3 @@ function PaletteItemImpl({ w, onClick }: { w: ReachableWidget; onClick: () => vo
|
|
|
181
172
|
}
|
|
182
173
|
|
|
183
174
|
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
|
-
}
|