@multicomma/arkcn 0.1.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 (237) hide show
  1. package/README.md +78 -0
  2. package/bin/arkcn.mjs +78 -0
  3. package/cli/commands.mjs +251 -0
  4. package/cli/project.mjs +168 -0
  5. package/cli/registry.mjs +50 -0
  6. package/cli/ui.mjs +46 -0
  7. package/mcp/server.mjs +149 -0
  8. package/package.json +201 -0
  9. package/registry/base.css +680 -0
  10. package/registry/index.json +1833 -0
  11. package/registry/items/accordion.json +18 -0
  12. package/registry/items/activity-feed.json +18 -0
  13. package/registry/items/alert-dialog.json +18 -0
  14. package/registry/items/alert.json +17 -0
  15. package/registry/items/angle-slider.json +17 -0
  16. package/registry/items/app-shell.json +28 -0
  17. package/registry/items/aspect-ratio.json +13 -0
  18. package/registry/items/attachment.json +19 -0
  19. package/registry/items/avatar.json +17 -0
  20. package/registry/items/badge.json +18 -0
  21. package/registry/items/breadcrumb.json +18 -0
  22. package/registry/items/bubble.json +18 -0
  23. package/registry/items/button-group.json +19 -0
  24. package/registry/items/button.json +18 -0
  25. package/registry/items/calendar.json +19 -0
  26. package/registry/items/canvas.json +24 -0
  27. package/registry/items/card.json +15 -0
  28. package/registry/items/carousel.json +19 -0
  29. package/registry/items/cascader.json +23 -0
  30. package/registry/items/chart.json +17 -0
  31. package/registry/items/chat.json +23 -0
  32. package/registry/items/checkbox.json +18 -0
  33. package/registry/items/clipboard.json +19 -0
  34. package/registry/items/collapsible.json +15 -0
  35. package/registry/items/color-picker.json +19 -0
  36. package/registry/items/combobox.json +20 -0
  37. package/registry/items/command.json +21 -0
  38. package/registry/items/comment-thread.json +25 -0
  39. package/registry/items/context-menu.json +18 -0
  40. package/registry/items/controllable.json +14 -0
  41. package/registry/items/data-grid.json +24 -0
  42. package/registry/items/data-table-adapter.json +14 -0
  43. package/registry/items/data-table.json +32 -0
  44. package/registry/items/date-input.json +17 -0
  45. package/registry/items/dialog.json +19 -0
  46. package/registry/items/direction.json +13 -0
  47. package/registry/items/drag-preview.json +16 -0
  48. package/registry/items/drawer.json +18 -0
  49. package/registry/items/dropdown-menu.json +18 -0
  50. package/registry/items/editable.json +17 -0
  51. package/registry/items/empty.json +17 -0
  52. package/registry/items/field.json +19 -0
  53. package/registry/items/file-upload.json +19 -0
  54. package/registry/items/floating-panel.json +19 -0
  55. package/registry/items/floating-toolbar.json +16 -0
  56. package/registry/items/form.json +14 -0
  57. package/registry/items/format.json +15 -0
  58. package/registry/items/gantt.json +22 -0
  59. package/registry/items/history.json +14 -0
  60. package/registry/items/hotkeys.json +18 -0
  61. package/registry/items/hover-card.json +17 -0
  62. package/registry/items/image-cropper.json +17 -0
  63. package/registry/items/input-group.json +20 -0
  64. package/registry/items/input-otp.json +18 -0
  65. package/registry/items/input.json +15 -0
  66. package/registry/items/item.json +19 -0
  67. package/registry/items/json-tree-view.json +17 -0
  68. package/registry/items/kanban.json +26 -0
  69. package/registry/items/kbd.json +15 -0
  70. package/registry/items/label.json +15 -0
  71. package/registry/items/live-region.json +16 -0
  72. package/registry/items/marker.json +18 -0
  73. package/registry/items/marquee.json +17 -0
  74. package/registry/items/menubar.json +19 -0
  75. package/registry/items/message.json +15 -0
  76. package/registry/items/navigation-menu.json +19 -0
  77. package/registry/items/node-graph.json +22 -0
  78. package/registry/items/number-input.json +18 -0
  79. package/registry/items/pagination.json +18 -0
  80. package/registry/items/password-input.json +18 -0
  81. package/registry/items/popconfirm.json +20 -0
  82. package/registry/items/popover.json +18 -0
  83. package/registry/items/progress.json +17 -0
  84. package/registry/items/qr-code.json +17 -0
  85. package/registry/items/query-builder.json +23 -0
  86. package/registry/items/radio-group.json +17 -0
  87. package/registry/items/rating-group.json +18 -0
  88. package/registry/items/resizable.json +17 -0
  89. package/registry/items/rich-text-editor.json +37 -0
  90. package/registry/items/row-layout.json +15 -0
  91. package/registry/items/scheduler.json +23 -0
  92. package/registry/items/scroll-area.json +17 -0
  93. package/registry/items/segment-group.json +17 -0
  94. package/registry/items/select.json +19 -0
  95. package/registry/items/separator.json +15 -0
  96. package/registry/items/sheet.json +19 -0
  97. package/registry/items/sidebar.json +27 -0
  98. package/registry/items/signature-pad.json +19 -0
  99. package/registry/items/skeleton.json +15 -0
  100. package/registry/items/slider.json +17 -0
  101. package/registry/items/sonner.json +17 -0
  102. package/registry/items/spinner.json +17 -0
  103. package/registry/items/steps.json +19 -0
  104. package/registry/items/swap.json +17 -0
  105. package/registry/items/switch.json +17 -0
  106. package/registry/items/table.json +16 -0
  107. package/registry/items/tabs.json +18 -0
  108. package/registry/items/tags-input.json +18 -0
  109. package/registry/items/textarea.json +15 -0
  110. package/registry/items/time.json +14 -0
  111. package/registry/items/timer.json +18 -0
  112. package/registry/items/toc.json +17 -0
  113. package/registry/items/toggle-group.json +19 -0
  114. package/registry/items/toggle.json +18 -0
  115. package/registry/items/tooltip.json +17 -0
  116. package/registry/items/tour.json +19 -0
  117. package/registry/items/transfer-list.json +24 -0
  118. package/registry/items/tree-select.json +24 -0
  119. package/registry/items/tree-view.json +18 -0
  120. package/registry/items/use-mobile.json +13 -0
  121. package/registry/items/utils.json +17 -0
  122. package/registry/items/virtual-list.json +16 -0
  123. package/src/components/ui/accordion.tsx +111 -0
  124. package/src/components/ui/activity-feed.tsx +228 -0
  125. package/src/components/ui/alert-dialog.tsx +204 -0
  126. package/src/components/ui/alert.tsx +55 -0
  127. package/src/components/ui/angle-slider.tsx +117 -0
  128. package/src/components/ui/app-shell.tsx +985 -0
  129. package/src/components/ui/aspect-ratio.tsx +35 -0
  130. package/src/components/ui/attachment.tsx +193 -0
  131. package/src/components/ui/avatar.tsx +94 -0
  132. package/src/components/ui/badge.tsx +45 -0
  133. package/src/components/ui/breadcrumb.tsx +93 -0
  134. package/src/components/ui/bubble.tsx +119 -0
  135. package/src/components/ui/button-group.tsx +79 -0
  136. package/src/components/ui/button.tsx +65 -0
  137. package/src/components/ui/calendar.tsx +172 -0
  138. package/src/components/ui/canvas.tsx +723 -0
  139. package/src/components/ui/card.tsx +69 -0
  140. package/src/components/ui/carousel.tsx +228 -0
  141. package/src/components/ui/cascader.tsx +754 -0
  142. package/src/components/ui/chart.tsx +303 -0
  143. package/src/components/ui/chat.tsx +544 -0
  144. package/src/components/ui/checkbox.tsx +94 -0
  145. package/src/components/ui/clipboard.tsx +94 -0
  146. package/src/components/ui/collapsible.tsx +32 -0
  147. package/src/components/ui/color-picker.tsx +441 -0
  148. package/src/components/ui/combobox.tsx +400 -0
  149. package/src/components/ui/command.tsx +292 -0
  150. package/src/components/ui/comment-thread.tsx +923 -0
  151. package/src/components/ui/context-menu.tsx +331 -0
  152. package/src/components/ui/data-grid.tsx +1227 -0
  153. package/src/components/ui/data-table.tsx +993 -0
  154. package/src/components/ui/date-input.tsx +103 -0
  155. package/src/components/ui/dialog.tsx +159 -0
  156. package/src/components/ui/direction.tsx +32 -0
  157. package/src/components/ui/drawer.tsx +200 -0
  158. package/src/components/ui/dropdown-menu.tsx +329 -0
  159. package/src/components/ui/editable.tsx +90 -0
  160. package/src/components/ui/empty.tsx +86 -0
  161. package/src/components/ui/field.tsx +218 -0
  162. package/src/components/ui/file-upload.tsx +204 -0
  163. package/src/components/ui/floating-panel.tsx +199 -0
  164. package/src/components/ui/floating-toolbar.tsx +79 -0
  165. package/src/components/ui/format.tsx +22 -0
  166. package/src/components/ui/gantt.tsx +950 -0
  167. package/src/components/ui/hotkeys.tsx +340 -0
  168. package/src/components/ui/hover-card.tsx +83 -0
  169. package/src/components/ui/image-cropper.tsx +100 -0
  170. package/src/components/ui/input-group.tsx +133 -0
  171. package/src/components/ui/input-otp.tsx +82 -0
  172. package/src/components/ui/input.tsx +18 -0
  173. package/src/components/ui/item.tsx +174 -0
  174. package/src/components/ui/json-tree-view.tsx +37 -0
  175. package/src/components/ui/kanban.tsx +884 -0
  176. package/src/components/ui/kbd.tsx +20 -0
  177. package/src/components/ui/label.tsx +30 -0
  178. package/src/components/ui/live-region.tsx +54 -0
  179. package/src/components/ui/marker.tsx +66 -0
  180. package/src/components/ui/marquee.tsx +61 -0
  181. package/src/components/ui/menubar.tsx +456 -0
  182. package/src/components/ui/message.tsx +74 -0
  183. package/src/components/ui/navigation-menu.tsx +202 -0
  184. package/src/components/ui/node-graph.tsx +1723 -0
  185. package/src/components/ui/number-input.tsx +148 -0
  186. package/src/components/ui/pagination.tsx +88 -0
  187. package/src/components/ui/password-input.tsx +104 -0
  188. package/src/components/ui/popconfirm.tsx +149 -0
  189. package/src/components/ui/popover.tsx +126 -0
  190. package/src/components/ui/progress.tsx +114 -0
  191. package/src/components/ui/qr-code.tsx +51 -0
  192. package/src/components/ui/query-builder.tsx +938 -0
  193. package/src/components/ui/radio-group.tsx +108 -0
  194. package/src/components/ui/rating-group.tsx +91 -0
  195. package/src/components/ui/resizable.tsx +41 -0
  196. package/src/components/ui/rich-text-editor.tsx +845 -0
  197. package/src/components/ui/scheduler.tsx +1164 -0
  198. package/src/components/ui/scroll-area.tsx +82 -0
  199. package/src/components/ui/segment-group.tsx +95 -0
  200. package/src/components/ui/select.tsx +354 -0
  201. package/src/components/ui/separator.tsx +32 -0
  202. package/src/components/ui/sheet.tsx +130 -0
  203. package/src/components/ui/sidebar.tsx +665 -0
  204. package/src/components/ui/signature-pad.tsx +112 -0
  205. package/src/components/ui/skeleton.tsx +7 -0
  206. package/src/components/ui/slider.tsx +183 -0
  207. package/src/components/ui/sonner.tsx +37 -0
  208. package/src/components/ui/spinner.tsx +16 -0
  209. package/src/components/ui/steps.tsx +152 -0
  210. package/src/components/ui/swap.tsx +33 -0
  211. package/src/components/ui/switch.tsx +83 -0
  212. package/src/components/ui/table.tsx +72 -0
  213. package/src/components/ui/tabs.tsx +100 -0
  214. package/src/components/ui/tags-input.tsx +153 -0
  215. package/src/components/ui/textarea.tsx +17 -0
  216. package/src/components/ui/timer.tsx +67 -0
  217. package/src/components/ui/toc.tsx +64 -0
  218. package/src/components/ui/toggle-group.tsx +89 -0
  219. package/src/components/ui/toggle.tsx +46 -0
  220. package/src/components/ui/tooltip.tsx +113 -0
  221. package/src/components/ui/tour.tsx +168 -0
  222. package/src/components/ui/transfer-list.tsx +700 -0
  223. package/src/components/ui/tree-select.tsx +638 -0
  224. package/src/components/ui/tree-view.tsx +217 -0
  225. package/src/components/ui/virtual-list.tsx +343 -0
  226. package/src/hooks/use-mobile.ts +19 -0
  227. package/src/lib/controllable.ts +31 -0
  228. package/src/lib/data-table-adapter.ts +67 -0
  229. package/src/lib/drag-preview.ts +39 -0
  230. package/src/lib/form.ts +39 -0
  231. package/src/lib/history.ts +55 -0
  232. package/src/lib/row-layout.ts +81 -0
  233. package/src/lib/table.ts +483 -0
  234. package/src/lib/time.ts +37 -0
  235. package/src/lib/utils.ts +6 -0
  236. package/src/styles/arkcn.css +764 -0
  237. package/src/styles/index.css +3 -0
@@ -0,0 +1,723 @@
1
+ import * as React from "react"
2
+ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"
3
+ import { draggable, dropTargetForElements, monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
4
+ import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"
5
+ import { attachClosestEdge, extractClosestEdge, type Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"
6
+ import { ark } from "@ark-ui/react"
7
+ import { GripVerticalIcon } from "lucide-react"
8
+ import { cloneDragPreview } from "@/lib/drag-preview"
9
+ import { cn } from "@/lib/utils"
10
+ import { LiveRegion, useLiveRegion } from "@/components/ui/live-region"
11
+
12
+ /**
13
+ * Canvas — a compositional, data-agnostic drag-and-drop layout surface on
14
+ * Pragmatic drag and drop. Items come from a palette (`CanvasPaletteItem`,
15
+ * carrying arbitrary `data`) or are existing nodes (`CanvasNode`). Nodes are
16
+ * drop targets on all four edges; the empty area is a target too. The canvas
17
+ * reports every drop as an intent (`onDrop`) and leaves layout state to the
18
+ * consumer, which renders rows/nodes from it. `CanvasResizeHandle` reports
19
+ * pointer resizes between siblings as a fraction of the row width.
20
+ */
21
+
22
+ type CanvasSource = { type: "palette"; data: unknown } | { type: "node"; id: string }
23
+ type CanvasTarget = { type: "node"; id: string; edge: Edge } | { type: "area" }
24
+ type CanvasDropDetails = { source: CanvasSource; target: CanvasTarget }
25
+
26
+ type DragData =
27
+ { instanceId: symbol; type: "palette"; data: unknown } | { instanceId: symbol; type: "node"; id: string }
28
+
29
+ const NODE_TARGET = "canvas-node-target"
30
+ const AREA_TARGET = "canvas-area-target"
31
+
32
+ type CanvasDirection = "up" | "down" | "left" | "right"
33
+
34
+ type CanvasContextValue = {
35
+ instanceId: symbol
36
+ dragging: { type: "palette" | "node"; id?: string } | null
37
+ /** Keyboard "pick up" state for nodes; arrows emit drop intents. */
38
+ grabbed: string | null
39
+ grab: (nodeId: string | null) => void
40
+ moveByKey: (nodeId: string, direction: CanvasDirection) => void
41
+ /** Keyboard activation of a palette item: drops it onto the area. */
42
+ dropFromPalette: (data: unknown) => void
43
+ }
44
+
45
+ /** Focus the first selector that matches, after React has committed. */
46
+ const focusLater = (...selectors: string[]) =>
47
+ requestAnimationFrame(() =>
48
+ requestAnimationFrame(() => {
49
+ for (const selector of selectors) {
50
+ const el = document.querySelector<HTMLElement>(selector)
51
+ if (el) {
52
+ el.focus()
53
+ return
54
+ }
55
+ }
56
+ })
57
+ )
58
+
59
+ const cssEscape = (value: string) => (typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value)
60
+
61
+ const CanvasContext = React.createContext<CanvasContextValue | null>(null)
62
+
63
+ function useCanvas() {
64
+ const ctx = React.useContext(CanvasContext)
65
+ if (!ctx) throw new Error("Canvas parts must be used within <Canvas>")
66
+ return ctx
67
+ }
68
+
69
+ const isOwnData = (instanceId: symbol, data: Record<string | symbol, unknown>): data is DragData =>
70
+ data.instanceId === instanceId && (data.type === "palette" || data.type === "node")
71
+
72
+ function Canvas({
73
+ onDrop,
74
+ className,
75
+ children,
76
+ ...props
77
+ }: Omit<React.ComponentProps<"div">, "onDrop"> & { onDrop?: (details: CanvasDropDetails) => void }) {
78
+ const [instanceId] = React.useState(() => Symbol("canvas"))
79
+ const [dragging, setDragging] = React.useState<CanvasContextValue["dragging"]>(null)
80
+ const [grabbed, setGrabbed] = React.useState<string | null>(null)
81
+ const { message: announcement, announce: setAnnouncement } = useLiveRegion()
82
+ const ref = React.useRef<HTMLDivElement>(null)
83
+ const onDropRef = React.useRef(onDrop)
84
+ React.useEffect(() => {
85
+ onDropRef.current = onDrop
86
+ })
87
+
88
+ const grab = React.useCallback(
89
+ (nodeId: string | null) => {
90
+ setGrabbed(nodeId)
91
+ setAnnouncement(
92
+ nodeId
93
+ ? `Picked up ${nodeId}. Arrow up or down moves it to its own row, left or right moves it beside a neighbour. Space or Enter drops, Escape cancels.`
94
+ : "Dropped."
95
+ )
96
+ },
97
+ [setAnnouncement]
98
+ )
99
+
100
+ const moveByKey = React.useCallback(
101
+ (nodeId: string, direction: CanvasDirection) => {
102
+ const root = ref.current
103
+ const node = root?.querySelector<HTMLElement>(`[data-slot=canvas-node][data-value="${cssEscape(nodeId)}"]`)
104
+ const row = node?.closest<HTMLElement>("[data-slot=canvas-row]")
105
+ if (!root || !node || !row) return
106
+ const siblings = Array.from(row.querySelectorAll<HTMLElement>(":scope > [data-slot=canvas-node]"))
107
+ const index = siblings.indexOf(node)
108
+ let target: CanvasTarget | null = null
109
+ if (direction === "up") {
110
+ const first = siblings[0]
111
+ // Already alone at the top of its row: hop above the previous row instead.
112
+ if (siblings.length === 1) {
113
+ const rows = Array.from(root.querySelectorAll<HTMLElement>("[data-slot=canvas-row]"))
114
+ const prev = rows[rows.indexOf(row) - 1]?.querySelector<HTMLElement>("[data-slot=canvas-node]")
115
+ if (!prev) return
116
+ target = { type: "node", id: prev.dataset.value ?? "", edge: "top" }
117
+ } else target = { type: "node", id: first.dataset.value ?? "", edge: "top" }
118
+ } else if (direction === "down") {
119
+ const last = siblings[siblings.length - 1]
120
+ if (siblings.length === 1) {
121
+ const rows = Array.from(root.querySelectorAll<HTMLElement>("[data-slot=canvas-row]"))
122
+ const next = rows[rows.indexOf(row) + 1]?.querySelector<HTMLElement>("[data-slot=canvas-node]")
123
+ if (!next) return
124
+ target = { type: "node", id: next.dataset.value ?? "", edge: "bottom" }
125
+ } else target = { type: "node", id: last.dataset.value ?? "", edge: "bottom" }
126
+ } else {
127
+ const neighbour = siblings[direction === "left" ? index - 1 : index + 1]
128
+ if (!neighbour) return
129
+ target = { type: "node", id: neighbour.dataset.value ?? "", edge: direction }
130
+ }
131
+ // A vertical drop on the node itself means "pull me out into a new row"; horizontal is a no-op.
132
+ if (target.type === "node" && target.id === nodeId && (target.edge === "left" || target.edge === "right")) return
133
+ onDropRef.current?.({ source: { type: "node", id: nodeId }, target })
134
+ setAnnouncement(`Moved ${nodeId} ${direction}.`)
135
+ const nodeSelector = `[data-slot=canvas-node][data-value="${cssEscape(nodeId)}"]`
136
+ focusLater(`${nodeSelector} [data-slot=canvas-node-handle]`, nodeSelector)
137
+ },
138
+ [setAnnouncement]
139
+ )
140
+
141
+ const dropFromPalette = React.useCallback(
142
+ (data: unknown) => {
143
+ onDropRef.current?.({ source: { type: "palette", data }, target: { type: "area" } })
144
+ setAnnouncement("Added to the canvas.")
145
+ },
146
+ [setAnnouncement]
147
+ )
148
+
149
+ React.useEffect(
150
+ () =>
151
+ monitorForElements({
152
+ canMonitor: ({ source }) => isOwnData(instanceId, source.data),
153
+ onDragStart: ({ source }) => {
154
+ const data = source.data as DragData
155
+ setDragging(data.type === "node" ? { type: "node", id: data.id } : { type: "palette" })
156
+ },
157
+ onDrop: ({ source, location }) => {
158
+ setDragging(null)
159
+ const data = source.data as DragData
160
+ const targets = location.current.dropTargets
161
+ const nodeTarget = targets.find((t) => t.data[NODE_TARGET] === true)
162
+ const areaTarget = targets.find((t) => t.data[AREA_TARGET] === true)
163
+ const src: CanvasSource =
164
+ data.type === "node" ? { type: "node", id: data.id } : { type: "palette", data: data.data }
165
+ if (nodeTarget) {
166
+ const id = String(nodeTarget.data.id)
167
+ if (src.type === "node" && src.id === id) return
168
+ const edge = extractClosestEdge(nodeTarget.data) ?? "bottom"
169
+ onDropRef.current?.({ source: src, target: { type: "node", id, edge } })
170
+ } else if (areaTarget) {
171
+ onDropRef.current?.({ source: src, target: { type: "area" } })
172
+ }
173
+ },
174
+ }),
175
+ [instanceId]
176
+ )
177
+
178
+ const ctx = React.useMemo(
179
+ () => ({ instanceId, dragging, grabbed, grab, moveByKey, dropFromPalette }),
180
+ [instanceId, dragging, grabbed, grab, moveByKey, dropFromPalette]
181
+ )
182
+ return (
183
+ <CanvasContext.Provider value={ctx}>
184
+ <div
185
+ ref={ref}
186
+ data-slot="canvas"
187
+ data-dragging={dragging ? dragging.type : undefined}
188
+ data-grabbed={grabbed ? "" : undefined}
189
+ className={cn("flex min-h-0 flex-1 flex-col gap-4 lg:flex-row", className)}
190
+ {...props}
191
+ >
192
+ {children}
193
+ <LiveRegion data-slot="canvas-live-region" message={announcement} />
194
+ </div>
195
+ </CanvasContext.Provider>
196
+ )
197
+ }
198
+
199
+ /** Keyboard handling shared by nodes and their handles. */
200
+ function useCanvasKeyboard(nodeId: string) {
201
+ const { grabbed, grab, moveByKey } = useCanvas()
202
+ const isGrabbed = grabbed === nodeId
203
+ const onKeyDown = (event: React.KeyboardEvent) => {
204
+ const key = event.key
205
+ if (key === " " || key === "Enter") {
206
+ event.preventDefault()
207
+ grab(isGrabbed ? null : nodeId)
208
+ return
209
+ }
210
+ if (!isGrabbed) return
211
+ if (key === "Escape") {
212
+ event.preventDefault()
213
+ grab(null)
214
+ return
215
+ }
216
+ const direction = { ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right" }[key] as
217
+ CanvasDirection | undefined
218
+ if (!direction) return
219
+ event.preventDefault()
220
+ moveByKey(nodeId, direction)
221
+ }
222
+ return { isGrabbed, onKeyDown, onBlur: () => isGrabbed && grab(null) }
223
+ }
224
+
225
+ /* -------------------------------------------------------------------------- */
226
+ /* Palette */
227
+ /* -------------------------------------------------------------------------- */
228
+
229
+ function CanvasPalette({ className, ...props }: React.ComponentProps<"div">) {
230
+ return (
231
+ <div
232
+ data-slot="canvas-palette"
233
+ className={cn(
234
+ "flex shrink-0 flex-row flex-wrap gap-1 lg:w-56 lg:flex-col lg:flex-nowrap lg:overflow-y-auto",
235
+ className
236
+ )}
237
+ {...props}
238
+ />
239
+ )
240
+ }
241
+
242
+ /** A draggable source. `data` is handed back untouched in `onDrop`. */
243
+ function CanvasPaletteItem({ data, className, onKeyDown, ...props }: React.ComponentProps<"div"> & { data: unknown }) {
244
+ const { instanceId, dropFromPalette } = useCanvas()
245
+ const ref = React.useRef<HTMLDivElement>(null)
246
+ const dataRef = React.useRef(data)
247
+ React.useEffect(() => {
248
+ dataRef.current = data
249
+ })
250
+ const [isDragging, setIsDragging] = React.useState(false)
251
+
252
+ React.useEffect(() => {
253
+ const el = ref.current
254
+ if (!el) return
255
+ return draggable({
256
+ element: el,
257
+ getInitialData: (): DragData => ({ instanceId, type: "palette", data: dataRef.current }),
258
+ onGenerateDragPreview: (args) => cloneDragPreview(args),
259
+ onDragStart: () => setIsDragging(true),
260
+ onDrop: () => setIsDragging(false),
261
+ })
262
+ }, [instanceId])
263
+
264
+ return (
265
+ <div
266
+ ref={ref}
267
+ data-slot="canvas-palette-item"
268
+ data-dragging={isDragging ? "" : undefined}
269
+ tabIndex={0}
270
+ role="button"
271
+ aria-roledescription="draggable palette item"
272
+ onKeyDown={(event) => {
273
+ onKeyDown?.(event)
274
+ if (event.defaultPrevented) return
275
+ if (event.key === " " || event.key === "Enter") {
276
+ event.preventDefault()
277
+ dropFromPalette(dataRef.current)
278
+ }
279
+ }}
280
+ className={cn(
281
+ "flex cursor-grab items-center gap-2 rounded-lg border bg-card px-2.5 py-2 text-sm shadow-xs transition-colors outline-none select-none hover:bg-accent focus-visible:ring-3 focus-visible:ring-ring/50 active:cursor-grabbing data-dragging:opacity-50 [&_svg]:size-4 [&_svg]:text-muted-foreground",
282
+ className
283
+ )}
284
+ {...props}
285
+ />
286
+ )
287
+ }
288
+
289
+ /* -------------------------------------------------------------------------- */
290
+ /* Area + rows */
291
+ /* -------------------------------------------------------------------------- */
292
+
293
+ /** The drop surface. Dropping here (not on a node) reports `{ type: "area" }`. */
294
+ function CanvasArea({ className, ...props }: React.ComponentProps<"div">) {
295
+ const { instanceId } = useCanvas()
296
+ const ref = React.useRef<HTMLDivElement>(null)
297
+ const [isOver, setIsOver] = React.useState(false)
298
+
299
+ React.useEffect(() => {
300
+ const el = ref.current
301
+ if (!el) return
302
+ return combine(
303
+ dropTargetForElements({
304
+ element: el,
305
+ canDrop: ({ source }) => isOwnData(instanceId, source.data),
306
+ getData: () => ({ [AREA_TARGET]: true }),
307
+ onDragEnter: ({ self, location }) => setIsOver(location.current.dropTargets[0]?.element === self.element),
308
+ onDrag: ({ self, location }) => {
309
+ const over = location.current.dropTargets[0]?.element === self.element
310
+ setIsOver((prev) => (prev === over ? prev : over))
311
+ },
312
+ onDragLeave: () => setIsOver(false),
313
+ onDrop: () => setIsOver(false),
314
+ }),
315
+ autoScrollForElements({ element: el, canScroll: ({ source }) => isOwnData(instanceId, source.data) })
316
+ )
317
+ }, [instanceId])
318
+
319
+ return (
320
+ <div
321
+ ref={ref}
322
+ data-slot="canvas-area"
323
+ data-over={isOver ? "" : undefined}
324
+ className={cn(
325
+ "flex min-h-40 min-w-0 flex-1 flex-col gap-3 overflow-y-auto rounded-xl border border-dashed bg-muted/30 p-4 transition-colors data-over:border-primary/50 data-over:bg-primary/5",
326
+ className
327
+ )}
328
+ {...props}
329
+ />
330
+ )
331
+ }
332
+
333
+ function CanvasEmpty({ className, ...props }: React.ComponentProps<"div">) {
334
+ return (
335
+ <div
336
+ data-slot="canvas-empty"
337
+ className={cn(
338
+ "flex flex-1 flex-col items-center justify-center gap-1 py-16 text-center text-sm text-muted-foreground [&_svg]:mb-1 [&_svg]:size-6",
339
+ className
340
+ )}
341
+ {...props}
342
+ />
343
+ )
344
+ }
345
+
346
+ type RowContextValue = {
347
+ ref: React.RefObject<HTMLDivElement | null>
348
+ /** True while a `CanvasResizeHandle` in this row is being dragged. */
349
+ resizing: boolean
350
+ setResizing: (resizing: boolean) => void
351
+ }
352
+ const RowContext = React.createContext<RowContextValue | null>(null)
353
+
354
+ /** A horizontal row of nodes. Node widths are `flex-grow` weights. */
355
+ function CanvasRow({ className, ...props }: React.ComponentProps<"div">) {
356
+ const ref = React.useRef<HTMLDivElement>(null)
357
+ const [resizing, setResizing] = React.useState(false)
358
+ const ctx = React.useMemo(() => ({ ref, resizing, setResizing }), [resizing])
359
+ return (
360
+ <RowContext.Provider value={ctx}>
361
+ <div
362
+ ref={ref}
363
+ data-slot="canvas-row"
364
+ data-resizing={resizing ? "" : undefined}
365
+ className={cn("flex items-stretch gap-3", className)}
366
+ {...props}
367
+ />
368
+ </RowContext.Provider>
369
+ )
370
+ }
371
+
372
+ /* -------------------------------------------------------------------------- */
373
+ /* Node */
374
+ /* -------------------------------------------------------------------------- */
375
+
376
+ type NodeContextValue = { handleRef: React.RefObject<HTMLElement | null>; isDragging: boolean; value: string }
377
+ const NodeContext = React.createContext<NodeContextValue | null>(null)
378
+
379
+ function CanvasNode({
380
+ value,
381
+ width = 1,
382
+ draggable: isDraggable = true,
383
+ selected,
384
+ className,
385
+ children,
386
+ style,
387
+ onKeyDown,
388
+ onBlur,
389
+ ...props
390
+ }: React.ComponentProps<"div"> & {
391
+ value: string
392
+ /** Relative width within the row (flex-grow weight). */
393
+ width?: number
394
+ draggable?: boolean
395
+ selected?: boolean
396
+ }) {
397
+ const { instanceId } = useCanvas()
398
+ const keyboard = useCanvasKeyboard(value)
399
+ const row = React.useContext(RowContext)
400
+ const ref = React.useRef<HTMLDivElement>(null)
401
+ const handleRef = React.useRef<HTMLElement | null>(null)
402
+ const [isDragging, setIsDragging] = React.useState(false)
403
+ const [closestEdge, setClosestEdge] = React.useState<Edge | null>(null)
404
+ const [percent, setPercent] = React.useState<number | null>(null)
405
+ const resizing = row?.resizing ?? false
406
+
407
+ // While the row is being resized, show this node's share of the row.
408
+ React.useLayoutEffect(() => {
409
+ const el = ref.current
410
+ const rowEl = row?.ref.current
411
+ if (!resizing || !el || !rowEl) {
412
+ setPercent(null)
413
+ return
414
+ }
415
+ const siblings = Array.from(rowEl.querySelectorAll<HTMLElement>(":scope > [data-slot=canvas-node]"))
416
+ const total = siblings.reduce((sum, n) => sum + n.getBoundingClientRect().width, 0)
417
+ setPercent(total ? Math.round((el.getBoundingClientRect().width / total) * 100) : null)
418
+ }, [resizing, width, row])
419
+
420
+ React.useEffect(() => {
421
+ const el = ref.current
422
+ if (!el) return
423
+ const cleanups = [
424
+ dropTargetForElements({
425
+ element: el,
426
+ canDrop: ({ source }) =>
427
+ isOwnData(instanceId, source.data) && !(source.data.type === "node" && source.data.id === value),
428
+ getIsSticky: () => true,
429
+ getData: ({ input, element }) =>
430
+ attachClosestEdge(
431
+ { [NODE_TARGET]: true, id: value },
432
+ { element, input, allowedEdges: ["top", "bottom", "left", "right"] }
433
+ ),
434
+ onDragEnter: ({ self }) => setClosestEdge(extractClosestEdge(self.data)),
435
+ onDrag: ({ self }) => {
436
+ const edge = extractClosestEdge(self.data)
437
+ setClosestEdge((prev) => (prev === edge ? prev : edge))
438
+ },
439
+ onDragLeave: () => setClosestEdge(null),
440
+ onDrop: () => setClosestEdge(null),
441
+ }),
442
+ ]
443
+ if (isDraggable) {
444
+ cleanups.push(
445
+ draggable({
446
+ element: el,
447
+ dragHandle: handleRef.current ?? undefined,
448
+ getInitialData: (): DragData => ({ instanceId, type: "node", id: value }),
449
+ onGenerateDragPreview: (args) => cloneDragPreview(args),
450
+ onDragStart: () => setIsDragging(true),
451
+ onDrop: () => setIsDragging(false),
452
+ })
453
+ )
454
+ }
455
+ return combine(...cleanups)
456
+ }, [instanceId, value, isDraggable])
457
+
458
+ const [hasHandle, setHasHandle] = React.useState(false)
459
+ React.useEffect(() => setHasHandle(!!handleRef.current), [])
460
+ const ctx = React.useMemo(() => ({ handleRef, isDragging, value }), [isDragging, value])
461
+ return (
462
+ <NodeContext.Provider value={ctx}>
463
+ <div
464
+ ref={ref}
465
+ data-slot="canvas-node"
466
+ data-value={value}
467
+ data-dragging={isDragging ? "" : undefined}
468
+ data-edge={closestEdge ?? undefined}
469
+ data-selected={selected ? "" : undefined}
470
+ data-grabbed={keyboard.isGrabbed ? "" : undefined}
471
+ tabIndex={isDraggable && !hasHandle ? 0 : undefined}
472
+ aria-roledescription={isDraggable && !hasHandle ? "draggable item" : undefined}
473
+ onKeyDown={(event) => {
474
+ onKeyDown?.(event)
475
+ if (!event.defaultPrevented && !hasHandle && event.target === event.currentTarget) keyboard.onKeyDown(event)
476
+ }}
477
+ onBlur={(event) => {
478
+ onBlur?.(event)
479
+ if (!hasHandle && event.target === event.currentTarget) keyboard.onBlur()
480
+ }}
481
+ style={{ flexGrow: width, flexBasis: 0, ...style }}
482
+ className={cn(
483
+ "group/canvas-node relative flex min-w-0 flex-col rounded-lg border bg-card p-3 text-sm shadow-xs transition-[opacity,box-shadow,border-color] outline-none hover:border-foreground/20 focus-visible:ring-3 focus-visible:ring-ring/50 data-dragging:opacity-40 data-grabbed:border-primary data-grabbed:ring-3 data-grabbed:ring-primary/30 data-selected:border-primary data-selected:ring-3 data-selected:ring-primary/20",
484
+ className
485
+ )}
486
+ {...props}
487
+ >
488
+ {children}
489
+ {closestEdge && <CanvasDropIndicator edge={closestEdge} />}
490
+ {percent !== null && <CanvasNodeOverlay>{percent}%</CanvasNodeOverlay>}
491
+ </div>
492
+ </NodeContext.Provider>
493
+ )
494
+ }
495
+
496
+ /** Optional explicit drag handle; without it the whole node is the handle. */
497
+ function CanvasNodeHandle({
498
+ className,
499
+ children,
500
+ onKeyDown,
501
+ onBlur,
502
+ asChild,
503
+ ...props
504
+ }: React.ComponentProps<typeof ark.button>) {
505
+ const ctx = React.useContext(NodeContext)
506
+ const keyboard = useCanvasKeyboard(ctx?.value ?? "")
507
+ return (
508
+ <ark.button
509
+ asChild={asChild}
510
+ ref={(el: HTMLElement | null) => {
511
+ if (ctx) ctx.handleRef.current = el
512
+ }}
513
+ type="button"
514
+ data-slot="canvas-node-handle"
515
+ data-grabbed={keyboard.isGrabbed ? "" : undefined}
516
+ aria-label="Drag"
517
+ aria-pressed={keyboard.isGrabbed}
518
+ onKeyDown={(event) => {
519
+ onKeyDown?.(event)
520
+ if (!event.defaultPrevented) {
521
+ keyboard.onKeyDown(event)
522
+ event.stopPropagation()
523
+ }
524
+ }}
525
+ onBlur={(event) => {
526
+ onBlur?.(event)
527
+ keyboard.onBlur()
528
+ }}
529
+ className={cn(
530
+ "flex size-6 cursor-grab items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 active:cursor-grabbing data-grabbed:bg-primary data-grabbed:text-primary-foreground [&_svg]:size-4",
531
+ className
532
+ )}
533
+ {...props}
534
+ >
535
+ {asChild ? children : (children ?? <GripVerticalIcon />)}
536
+ </ark.button>
537
+ )
538
+ }
539
+
540
+ function CanvasNodeHeader({ className, ...props }: React.ComponentProps<"div">) {
541
+ return <div data-slot="canvas-node-header" className={cn("mb-2 flex items-center gap-1", className)} {...props} />
542
+ }
543
+
544
+ function CanvasNodeTitle({ className, ...props }: React.ComponentProps<"span">) {
545
+ return (
546
+ <span
547
+ data-slot="canvas-node-title"
548
+ className={cn("min-w-0 flex-1 truncate text-xs font-medium text-muted-foreground", className)}
549
+ {...props}
550
+ />
551
+ )
552
+ }
553
+
554
+ /** A blurring overlay with a centered pill, e.g. the node's width share while resizing. */
555
+ function CanvasNodeOverlay({ className, children, ...props }: React.ComponentProps<"div">) {
556
+ return (
557
+ <div
558
+ data-slot="canvas-node-overlay"
559
+ aria-hidden
560
+ className={cn(
561
+ "pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-[inherit] bg-background/50 backdrop-blur-xs",
562
+ className
563
+ )}
564
+ {...props}
565
+ >
566
+ <span
567
+ data-slot="canvas-node-overlay-pill"
568
+ className="rounded-full bg-foreground px-2.5 py-1 text-xs font-semibold text-background tabular-nums shadow-md"
569
+ >
570
+ {children}
571
+ </span>
572
+ </div>
573
+ )
574
+ }
575
+
576
+ /** Actions revealed on hover/selection. */
577
+ function CanvasNodeActions({ className, ...props }: React.ComponentProps<"div">) {
578
+ return (
579
+ <div
580
+ data-slot="canvas-node-actions"
581
+ className={cn(
582
+ "ms-auto flex items-center gap-0.5 opacity-0 transition-opacity group-focus-within/canvas-node:opacity-100 group-hover/canvas-node:opacity-100 group-data-selected/canvas-node:opacity-100",
583
+ className
584
+ )}
585
+ {...props}
586
+ />
587
+ )
588
+ }
589
+
590
+ /* -------------------------------------------------------------------------- */
591
+ /* Resize handle */
592
+ /* -------------------------------------------------------------------------- */
593
+
594
+ /**
595
+ * Place between two nodes in a `CanvasRow`. Pointer drags call `onResize`
596
+ * with the horizontal movement as a fraction of the row width (signed).
597
+ */
598
+ function CanvasResizeHandle({
599
+ onResize,
600
+ onResizeStart,
601
+ onResizeEnd,
602
+ className,
603
+ ...props
604
+ }: Omit<React.ComponentProps<"div">, "onResize"> & {
605
+ onResize: (deltaFraction: number) => void
606
+ onResizeStart?: () => void
607
+ onResizeEnd?: () => void
608
+ }) {
609
+ const row = React.useContext(RowContext)
610
+ const [active, setActive] = React.useState(false)
611
+ const handlers = React.useRef({ onResize, onResizeStart, onResizeEnd })
612
+ React.useEffect(() => {
613
+ handlers.current = { onResize, onResizeStart, onResizeEnd }
614
+ })
615
+
616
+ const onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
617
+ if (event.button !== 0) return
618
+ const rowEl = row?.ref.current
619
+ if (!rowEl) return
620
+ event.preventDefault()
621
+ const target = event.currentTarget
622
+ try {
623
+ target.setPointerCapture(event.pointerId)
624
+ } catch {
625
+ // Synthetic pointers can't be captured; moves still arrive via bubbling.
626
+ }
627
+ let lastX = event.clientX
628
+ setActive(true)
629
+ row.setResizing(true)
630
+ handlers.current.onResizeStart?.()
631
+ const move = (e: PointerEvent) => {
632
+ const width = rowEl.getBoundingClientRect().width
633
+ if (!width) return
634
+ const delta = (e.clientX - lastX) / width
635
+ lastX = e.clientX
636
+ if (delta !== 0) handlers.current.onResize(delta)
637
+ }
638
+ const up = () => {
639
+ target.removeEventListener("pointermove", move)
640
+ target.removeEventListener("pointerup", up)
641
+ target.removeEventListener("pointercancel", up)
642
+ setActive(false)
643
+ row.setResizing(false)
644
+ handlers.current.onResizeEnd?.()
645
+ }
646
+ target.addEventListener("pointermove", move)
647
+ target.addEventListener("pointerup", up)
648
+ target.addEventListener("pointercancel", up)
649
+ }
650
+
651
+ return (
652
+ <div
653
+ role="separator"
654
+ aria-orientation="vertical"
655
+ data-slot="canvas-resize-handle"
656
+ data-active={active ? "" : undefined}
657
+ onPointerDown={onPointerDown}
658
+ className={cn(
659
+ "group/resize relative -mx-2 flex w-4 shrink-0 cursor-col-resize touch-none items-center justify-center select-none",
660
+ className
661
+ )}
662
+ {...props}
663
+ >
664
+ <span className="h-8 w-1 rounded-full bg-border transition-colors group-hover/resize:bg-primary/60 group-data-active/resize:bg-primary" />
665
+ </div>
666
+ )
667
+ }
668
+
669
+ /* -------------------------------------------------------------------------- */
670
+ /* Drop indicator */
671
+ /* -------------------------------------------------------------------------- */
672
+
673
+ function CanvasDropIndicator({
674
+ edge,
675
+ gap = "0.75rem",
676
+ className,
677
+ style,
678
+ ...props
679
+ }: React.ComponentProps<"div"> & { edge: Edge; gap?: string }) {
680
+ const horizontal = edge === "top" || edge === "bottom"
681
+ return (
682
+ <div
683
+ data-slot="canvas-drop-indicator"
684
+ data-edge={edge}
685
+ aria-hidden
686
+ style={{ ["--canvas-gap" as string]: gap, ...style }}
687
+ className={cn(
688
+ "pointer-events-none absolute z-10 rounded-full bg-primary",
689
+ "before:absolute before:size-2 before:rounded-full before:border-2 before:border-primary before:bg-background",
690
+ horizontal
691
+ ? "inset-x-0 h-0.5 before:top-1/2 before:-left-1 before:-translate-y-1/2"
692
+ : "inset-y-0 w-0.5 before:-top-1 before:left-1/2 before:-translate-x-1/2",
693
+ edge === "top" && "top-[calc(-1*var(--canvas-gap)/2-1px)]",
694
+ edge === "bottom" && "bottom-[calc(-1*var(--canvas-gap)/2-1px)]",
695
+ edge === "left" && "left-[calc(-1*var(--canvas-gap)/2-1px)]",
696
+ edge === "right" && "right-[calc(-1*var(--canvas-gap)/2-1px)]",
697
+ className
698
+ )}
699
+ {...props}
700
+ />
701
+ )
702
+ }
703
+
704
+ export {
705
+ Canvas,
706
+ CanvasArea,
707
+ CanvasDropIndicator,
708
+ CanvasEmpty,
709
+ CanvasNode,
710
+ CanvasNodeActions,
711
+ CanvasNodeHandle,
712
+ CanvasNodeHeader,
713
+ CanvasNodeOverlay,
714
+ CanvasNodeTitle,
715
+ CanvasPalette,
716
+ CanvasPaletteItem,
717
+ CanvasResizeHandle,
718
+ CanvasRow,
719
+ useCanvas,
720
+ type CanvasDropDetails,
721
+ type CanvasSource,
722
+ type CanvasTarget,
723
+ }