@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.
Files changed (77) hide show
  1. package/dist/app/builder/BuilderBanners.d.ts +19 -0
  2. package/dist/app/builder/BuilderBanners.d.ts.map +1 -0
  3. package/dist/app/builder/BuilderBanners.js +22 -0
  4. package/dist/app/builder/BuilderBanners.js.map +1 -0
  5. package/dist/app/builder/CanvasRow.d.ts +26 -0
  6. package/dist/app/builder/CanvasRow.d.ts.map +1 -0
  7. package/dist/app/builder/CanvasRow.js +98 -0
  8. package/dist/app/builder/CanvasRow.js.map +1 -0
  9. package/dist/app/builder/CellPropsSheet.d.ts +19 -0
  10. package/dist/app/builder/CellPropsSheet.d.ts.map +1 -0
  11. package/dist/app/builder/CellPropsSheet.js +28 -0
  12. package/dist/app/builder/CellPropsSheet.js.map +1 -0
  13. package/dist/app/builder/EmptyHint.d.ts +6 -0
  14. package/dist/app/builder/EmptyHint.d.ts.map +1 -0
  15. package/dist/app/builder/EmptyHint.js +10 -0
  16. package/dist/app/builder/EmptyHint.js.map +1 -0
  17. package/dist/app/builder/LayoutSurface.d.ts +27 -0
  18. package/dist/app/builder/LayoutSurface.d.ts.map +1 -0
  19. package/dist/app/builder/LayoutSurface.js +45 -0
  20. package/dist/app/builder/LayoutSurface.js.map +1 -0
  21. package/dist/app/builder/Workspace.d.ts +9 -3
  22. package/dist/app/builder/Workspace.d.ts.map +1 -1
  23. package/dist/app/builder/Workspace.js +3 -97
  24. package/dist/app/builder/Workspace.js.map +1 -1
  25. package/dist/app/builder/builder-reducer.d.ts.map +1 -1
  26. package/dist/app/builder/builder-reducer.js +176 -164
  27. package/dist/app/builder/builder-reducer.js.map +1 -1
  28. package/dist/app/builder/use-builder-catalogue.d.ts +33 -0
  29. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -0
  30. package/dist/app/builder/use-builder-catalogue.js +155 -0
  31. package/dist/app/builder/use-builder-catalogue.js.map +1 -0
  32. package/dist/app/builder/use-key-step-entries.d.ts +22 -0
  33. package/dist/app/builder/use-key-step-entries.d.ts.map +1 -0
  34. package/dist/app/builder/use-key-step-entries.js +46 -0
  35. package/dist/app/builder/use-key-step-entries.js.map +1 -0
  36. package/dist/app/builder/use-save-dashboard.d.ts +33 -0
  37. package/dist/app/builder/use-save-dashboard.d.ts.map +1 -0
  38. package/dist/app/builder/use-save-dashboard.js +78 -0
  39. package/dist/app/builder/use-save-dashboard.js.map +1 -0
  40. package/dist/app/layout-builder.d.ts.map +1 -1
  41. package/dist/app/layout-builder.js +46 -277
  42. package/dist/app/layout-builder.js.map +1 -1
  43. package/dist/app/mcp-app-view-chrome.d.ts +77 -0
  44. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -0
  45. package/dist/app/mcp-app-view-chrome.js +53 -0
  46. package/dist/app/mcp-app-view-chrome.js.map +1 -0
  47. package/dist/app/mcp-app-view.d.ts +3 -9
  48. package/dist/app/mcp-app-view.d.ts.map +1 -1
  49. package/dist/app/mcp-app-view.js +4 -31
  50. package/dist/app/mcp-app-view.js.map +1 -1
  51. package/dist/render-cases-primitives.d.ts +17 -0
  52. package/dist/render-cases-primitives.d.ts.map +1 -0
  53. package/dist/render-cases-primitives.js +88 -0
  54. package/dist/render-cases-primitives.js.map +1 -0
  55. package/dist/render-cases.d.ts +4 -0
  56. package/dist/render-cases.d.ts.map +1 -0
  57. package/dist/render-cases.js +140 -0
  58. package/dist/render-cases.js.map +1 -0
  59. package/package.json +10 -5
  60. package/src/app/builder/BuilderBanners.tsx +61 -0
  61. package/src/app/builder/CanvasRow.tsx +316 -0
  62. package/src/app/builder/CellPropsSheet.tsx +47 -0
  63. package/src/app/builder/EmptyHint.tsx +29 -0
  64. package/src/app/builder/LayoutSurface.tsx +140 -0
  65. package/src/app/builder/Workspace.tsx +33 -342
  66. package/src/app/builder/builder-reducer.ts +204 -170
  67. package/src/app/builder/use-builder-catalogue.ts +187 -0
  68. package/src/app/builder/use-key-step-entries.ts +60 -0
  69. package/src/app/builder/use-save-dashboard.ts +97 -0
  70. package/src/app/layout-builder.tsx +92 -497
  71. package/src/app/mcp-app-view-chrome.tsx +204 -0
  72. package/src/app/mcp-app-view.tsx +32 -140
  73. package/src/lib/parse-tool-result.property.test.ts +90 -0
  74. package/src/render-cases-primitives.tsx +255 -0
  75. package/src/render-cases.tsx +239 -0
  76. package/src/render-smoke.test.tsx +100 -0
  77. 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, useCallback, useRef } from "react"
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
- }