@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,700 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ark } from "@ark-ui/react"
5
+ import {
6
+ CheckIcon,
7
+ ChevronLeftIcon,
8
+ ChevronRightIcon,
9
+ ChevronsLeftIcon,
10
+ ChevronsRightIcon,
11
+ SearchIcon,
12
+ } from "lucide-react"
13
+ import { useControllable } from "@/lib/controllable"
14
+ import { cn } from "@/lib/utils"
15
+ import { LiveRegion, useLiveRegion } from "@/components/ui/live-region"
16
+ import { Button } from "@/components/ui/button"
17
+ import { Checkbox } from "@/components/ui/checkbox"
18
+ import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"
19
+
20
+ /* -------------------------------- context -------------------------------- */
21
+
22
+ type TransferListSide = "source" | "target"
23
+ type TransferListDirection = "right" | "left"
24
+
25
+ type TransferListItemBase = { value: string; label: string; disabled?: boolean }
26
+
27
+ type TransferListValueChangeDetails<T> = {
28
+ /** Values now on the target side, in catalog order. */
29
+ value: string[]
30
+ items: T[]
31
+ /** What just moved and where. */
32
+ moved: T[]
33
+ direction: TransferListDirection
34
+ }
35
+
36
+ type TransferListContextValue<T = TransferListItemBase> = {
37
+ items: T[]
38
+ itemToValue: (item: T) => string
39
+ itemToString: (item: T) => string
40
+ itemDisabled: (item: T) => boolean
41
+ value: string[]
42
+ /** Items on each side, in catalog order. */
43
+ sides: Record<TransferListSide, T[]>
44
+ /** Visible (searched) items on each side. */
45
+ visible: Record<TransferListSide, T[]>
46
+ query: Record<TransferListSide, string>
47
+ setQuery: (side: TransferListSide, query: string) => void
48
+ selected: Record<TransferListSide, string[]>
49
+ toggle: (side: TransferListSide, value: string, force?: boolean) => void
50
+ selectRange: (side: TransferListSide, from: string, to: string) => void
51
+ selectAll: (side: TransferListSide, checked: boolean) => void
52
+ /** Move the selected (or given) values across. */
53
+ move: (direction: TransferListDirection, values?: string[]) => void
54
+ moveAll: (direction: TransferListDirection) => void
55
+ disabled: boolean
56
+ announcement: string
57
+ focused: Record<TransferListSide, string | null>
58
+ setFocused: (side: TransferListSide, value: string | null) => void
59
+ }
60
+
61
+ const TransferListContext = React.createContext<TransferListContextValue | null>(null)
62
+ const TransferListSideContext = React.createContext<TransferListSide | null>(null)
63
+
64
+ function useTransferList<T = TransferListItemBase>() {
65
+ const ctx = React.useContext(TransferListContext)
66
+ if (!ctx) throw new Error("TransferList parts must be used inside <TransferList>")
67
+ return ctx as unknown as TransferListContextValue<T>
68
+ }
69
+
70
+ function useTransferListSide() {
71
+ const side = React.useContext(TransferListSideContext)
72
+ if (!side) throw new Error("This part must be used inside <TransferListPanel>")
73
+ return side
74
+ }
75
+
76
+ const sideOf = (direction: TransferListDirection): TransferListSide => (direction === "right" ? "source" : "target")
77
+
78
+ /* ---------------------------------- root --------------------------------- */
79
+
80
+ type TransferListProps<T> = Omit<React.ComponentProps<"div">, "defaultValue" | "onChange"> & {
81
+ items: T[]
82
+ itemToValue?: (item: T) => string
83
+ itemToString?: (item: T) => string
84
+ itemDisabled?: (item: T) => boolean
85
+ /** Values on the target side. Kept in catalog order. */
86
+ value?: string[]
87
+ defaultValue?: string[]
88
+ onValueChange?: (details: TransferListValueChangeDetails<T>) => void
89
+ /** Case-insensitive label match by default. */
90
+ filter?: (item: T, query: string) => boolean
91
+ disabled?: boolean
92
+ /** Labels for the live region: "Moved 3 items to {target}". */
93
+ titles?: Partial<Record<TransferListSide, string>>
94
+ }
95
+
96
+ function TransferList<T = TransferListItemBase>({
97
+ items,
98
+ itemToValue = (item) => (item as TransferListItemBase).value,
99
+ itemToString = (item) => (item as TransferListItemBase).label,
100
+ itemDisabled = (item) => !!(item as TransferListItemBase).disabled,
101
+ value: valueProp,
102
+ defaultValue = [],
103
+ onValueChange,
104
+ filter,
105
+ disabled = false,
106
+ titles,
107
+ className,
108
+ children,
109
+ ...props
110
+ }: TransferListProps<T>) {
111
+ const order = React.useMemo(() => new Map(items.map((item, i) => [itemToValue(item), i])), [items, itemToValue])
112
+ const sortValues = React.useCallback(
113
+ (values: string[]) =>
114
+ Array.from(new Set(values))
115
+ .filter((v) => order.has(v))
116
+ .sort((a, b) => order.get(a)! - order.get(b)!),
117
+ [order]
118
+ )
119
+ const [value, setValueState] = useControllable<string[]>(valueProp, defaultValue)
120
+ const [query, setQueryState] = React.useState<Record<TransferListSide, string>>({ source: "", target: "" })
121
+ const [selected, setSelected] = React.useState<Record<TransferListSide, string[]>>({ source: [], target: [] })
122
+ const [focused, setFocusedState] = React.useState<Record<TransferListSide, string | null>>({
123
+ source: null,
124
+ target: null,
125
+ })
126
+ const { message: announcement, announce: setAnnouncement } = useLiveRegion()
127
+
128
+ const sides = React.useMemo(() => {
129
+ const target = new Set(value)
130
+ return {
131
+ source: items.filter((item) => !target.has(itemToValue(item))),
132
+ target: items.filter((item) => target.has(itemToValue(item))),
133
+ }
134
+ }, [items, value, itemToValue])
135
+
136
+ const matches = React.useCallback(
137
+ (item: T, q: string) =>
138
+ filter ? filter(item, q) : itemToString(item).toLowerCase().includes(q.trim().toLowerCase()),
139
+ [filter, itemToString]
140
+ )
141
+ const visible = React.useMemo(
142
+ () => ({
143
+ source: query.source.trim() ? sides.source.filter((i) => matches(i, query.source)) : sides.source,
144
+ target: query.target.trim() ? sides.target.filter((i) => matches(i, query.target)) : sides.target,
145
+ }),
146
+ [sides, query, matches]
147
+ )
148
+
149
+ const setQuery = React.useCallback((side: TransferListSide, q: string) => {
150
+ setQueryState((prev) => ({ ...prev, [side]: q }))
151
+ }, [])
152
+ const setFocused = React.useCallback((side: TransferListSide, v: string | null) => {
153
+ setFocusedState((prev) => (prev[side] === v ? prev : { ...prev, [side]: v }))
154
+ }, [])
155
+
156
+ const toggle = React.useCallback((side: TransferListSide, v: string, force?: boolean) => {
157
+ setSelected((prev) => {
158
+ const has = prev[side].includes(v)
159
+ const next = force ?? !has
160
+ if (next === has) return prev
161
+ return { ...prev, [side]: next ? [...prev[side], v] : prev[side].filter((x) => x !== v) }
162
+ })
163
+ }, [])
164
+
165
+ const selectRange = React.useCallback(
166
+ (side: TransferListSide, from: string, to: string) => {
167
+ const list = visible[side].filter((i) => !itemDisabled(i)).map(itemToValue)
168
+ const a = list.indexOf(from)
169
+ const b = list.indexOf(to)
170
+ if (a < 0 || b < 0) return
171
+ const range = list.slice(Math.min(a, b), Math.max(a, b) + 1)
172
+ setSelected((prev) => ({ ...prev, [side]: Array.from(new Set([...prev[side], ...range])) }))
173
+ },
174
+ [visible, itemDisabled, itemToValue]
175
+ )
176
+
177
+ const selectAll = React.useCallback(
178
+ (side: TransferListSide, checked: boolean) => {
179
+ const list = visible[side].filter((i) => !itemDisabled(i)).map(itemToValue)
180
+ setSelected((prev) => ({
181
+ ...prev,
182
+ [side]: checked ? Array.from(new Set([...prev[side], ...list])) : prev[side].filter((v) => !list.includes(v)),
183
+ }))
184
+ },
185
+ [visible, itemDisabled, itemToValue]
186
+ )
187
+
188
+ const move = React.useCallback(
189
+ (direction: TransferListDirection, values?: string[]) => {
190
+ if (disabled) return
191
+ const from = sideOf(direction)
192
+ const allowed = new Set(sides[from].filter((i) => !itemDisabled(i)).map(itemToValue))
193
+ const moving = (values ?? selected[from]).filter((v) => allowed.has(v))
194
+ if (moving.length === 0) return
195
+ const next = sortValues(direction === "right" ? [...value, ...moving] : value.filter((v) => !moving.includes(v)))
196
+ setValueState(next)
197
+ setSelected((prev) => ({ ...prev, [from]: prev[from].filter((v) => !moving.includes(v)) }))
198
+ const movedItems = items.filter((item) => moving.includes(itemToValue(item)))
199
+ const to = direction === "right" ? "target" : "source"
200
+ const title = titles?.[to] ?? (to === "target" ? "the selected list" : "the available list")
201
+ setAnnouncement(
202
+ movedItems.length === 1
203
+ ? `Moved ${itemToString(movedItems[0])} to ${title}.`
204
+ : `Moved ${movedItems.length} items to ${title}.`
205
+ )
206
+ onValueChange?.({
207
+ value: next,
208
+ items: items.filter((i) => next.includes(itemToValue(i))),
209
+ moved: movedItems,
210
+ direction,
211
+ })
212
+ },
213
+ [
214
+ disabled,
215
+ sides,
216
+ selected,
217
+ value,
218
+ items,
219
+ itemToValue,
220
+ itemToString,
221
+ itemDisabled,
222
+ sortValues,
223
+ setValueState,
224
+ onValueChange,
225
+ titles,
226
+ setAnnouncement,
227
+ ]
228
+ )
229
+
230
+ const moveAll = React.useCallback(
231
+ (direction: TransferListDirection) => {
232
+ const from = sideOf(direction)
233
+ move(direction, visible[from].map(itemToValue))
234
+ },
235
+ [move, visible, itemToValue]
236
+ )
237
+
238
+ const ctx: TransferListContextValue<T> = {
239
+ items,
240
+ itemToValue,
241
+ itemToString,
242
+ itemDisabled,
243
+ value,
244
+ sides,
245
+ visible,
246
+ query,
247
+ setQuery,
248
+ selected,
249
+ toggle,
250
+ selectRange,
251
+ selectAll,
252
+ move,
253
+ moveAll,
254
+ disabled,
255
+ announcement,
256
+ focused,
257
+ setFocused,
258
+ }
259
+
260
+ return (
261
+ <TransferListContext.Provider value={ctx as unknown as TransferListContextValue}>
262
+ <div
263
+ data-slot="transfer-list"
264
+ data-disabled={disabled ? "" : undefined}
265
+ className={cn("grid grid-cols-1 gap-2 sm:grid-cols-[1fr_auto_1fr] sm:items-stretch", className)}
266
+ {...props}
267
+ >
268
+ {children}
269
+ <LiveRegion data-slot="transfer-list-live-region" message={announcement} />
270
+ </div>
271
+ </TransferListContext.Provider>
272
+ )
273
+ }
274
+
275
+ /* ---------------------------------- panel -------------------------------- */
276
+
277
+ function TransferListPanel({ side, className, ...props }: React.ComponentProps<"div"> & { side: TransferListSide }) {
278
+ const ctx = useTransferList()
279
+ return (
280
+ <TransferListSideContext.Provider value={side}>
281
+ <div
282
+ data-slot="transfer-list-panel"
283
+ data-side={side}
284
+ data-disabled={ctx.disabled ? "" : undefined}
285
+ className={cn(
286
+ "flex min-h-64 min-w-0 flex-col overflow-hidden rounded-lg border border-input bg-background text-sm dark:bg-input/30 data-disabled:opacity-50",
287
+ className
288
+ )}
289
+ {...props}
290
+ />
291
+ </TransferListSideContext.Provider>
292
+ )
293
+ }
294
+
295
+ function TransferListPanelHeader({ className, ...props }: React.ComponentProps<"div">) {
296
+ return (
297
+ <div
298
+ data-slot="transfer-list-panel-header"
299
+ className={cn("flex h-9 shrink-0 items-center gap-2 border-b px-2.5", className)}
300
+ {...props}
301
+ />
302
+ )
303
+ }
304
+
305
+ function TransferListPanelTitle({ className, ...props }: React.ComponentProps<"span">) {
306
+ return (
307
+ <span data-slot="transfer-list-panel-title" className={cn("flex-1 truncate font-medium", className)} {...props} />
308
+ )
309
+ }
310
+
311
+ /** "selected / total" for the side; `children` overrides with `(selected, total) => ReactNode`. */
312
+ function TransferListPanelCount({
313
+ className,
314
+ children,
315
+ ...props
316
+ }: Omit<React.ComponentProps<"span">, "children"> & {
317
+ children?: (selected: number, total: number) => React.ReactNode
318
+ }) {
319
+ const ctx = useTransferList()
320
+ const side = useTransferListSide()
321
+ const total = ctx.sides[side].length
322
+ const count = ctx.selected[side].length
323
+ return (
324
+ <span
325
+ data-slot="transfer-list-panel-count"
326
+ className={cn("text-xs text-muted-foreground tabular-nums", className)}
327
+ {...props}
328
+ >
329
+ {children ? children(count, total) : count > 0 ? `${count} / ${total}` : total}
330
+ </span>
331
+ )
332
+ }
333
+
334
+ /** Header checkbox that selects every visible enabled item on this side. */
335
+ function TransferListSelectAll({
336
+ className,
337
+ ...props
338
+ }: Omit<React.ComponentProps<typeof Checkbox>, "checked" | "onCheckedChange">) {
339
+ const ctx = useTransferList()
340
+ const side = useTransferListSide()
341
+ const enabled = ctx.visible[side].filter((i) => !ctx.itemDisabled(i)).map(ctx.itemToValue)
342
+ const count = enabled.filter((v) => ctx.selected[side].includes(v)).length
343
+ const checked = enabled.length > 0 && count === enabled.length ? true : count > 0 ? "indeterminate" : false
344
+ return (
345
+ <Checkbox
346
+ data-slot="transfer-list-select-all"
347
+ aria-label={`Select all ${side === "source" ? "available" : "selected"} items`}
348
+ checked={checked}
349
+ disabled={ctx.disabled || enabled.length === 0}
350
+ onCheckedChange={({ checked: next }) => ctx.selectAll(side, next === true)}
351
+ className={className}
352
+ {...props}
353
+ />
354
+ )
355
+ }
356
+
357
+ function TransferListSearch({
358
+ className,
359
+ placeholder = "Search…",
360
+ ...props
361
+ }: Omit<React.ComponentProps<typeof InputGroupInput>, "value" | "onChange">) {
362
+ const ctx = useTransferList()
363
+ const side = useTransferListSide()
364
+ return (
365
+ <InputGroup
366
+ data-slot="transfer-list-search"
367
+ className={cn("h-8 shrink-0 rounded-none border-0 border-b shadow-none has-focus-visible:ring-0", className)}
368
+ >
369
+ <InputGroupAddon>
370
+ <SearchIcon />
371
+ </InputGroupAddon>
372
+ <InputGroupInput
373
+ role="searchbox"
374
+ placeholder={placeholder}
375
+ value={ctx.query[side]}
376
+ disabled={ctx.disabled}
377
+ onChange={(event) => ctx.setQuery(side, event.target.value)}
378
+ {...props}
379
+ />
380
+ </InputGroup>
381
+ )
382
+ }
383
+
384
+ /* ---------------------------------- items -------------------------------- */
385
+
386
+ const TransferListRenderContext = React.createContext<((item: unknown) => React.ReactNode) | undefined>(undefined)
387
+
388
+ /**
389
+ * The listbox for one side. Roving focus; ↑/↓ move, Space toggles, Shift+↑/↓ extends,
390
+ * ⌘/Ctrl+A selects all, Enter (or double-click) moves the focused item across.
391
+ */
392
+ function TransferListItems<T = TransferListItemBase>({
393
+ className,
394
+ children,
395
+ onKeyDown,
396
+ ...props
397
+ }: Omit<React.ComponentProps<"div">, "children"> & { children?: (item: T) => React.ReactNode }) {
398
+ const ctx = useTransferList<T>()
399
+ const side = useTransferListSide()
400
+ const ref = React.useRef<HTMLDivElement>(null)
401
+ const list = ctx.visible[side]
402
+ const values = list.map(ctx.itemToValue)
403
+ const focused = ctx.focused[side]
404
+ const active = focused && values.includes(focused) ? focused : (values[0] ?? null)
405
+
406
+ const focusValue = (v: string) => {
407
+ ctx.setFocused(side, v)
408
+ ref.current?.querySelector<HTMLElement>(`[data-slot=transfer-list-item][data-value="${CSS.escape(v)}"]`)?.focus()
409
+ }
410
+
411
+ return (
412
+ <TransferListRenderContext.Provider value={children as ((item: unknown) => React.ReactNode) | undefined}>
413
+ <div
414
+ ref={ref}
415
+ data-slot="transfer-list-items"
416
+ role="listbox"
417
+ aria-multiselectable
418
+ aria-label={side === "source" ? "Available" : "Selected"}
419
+ data-active-value={active ?? undefined}
420
+ className={cn("flex min-h-0 flex-1 flex-col gap-px overflow-y-auto p-1 outline-none", className)}
421
+ onKeyDown={(event) => {
422
+ onKeyDown?.(event)
423
+ if (event.defaultPrevented || ctx.disabled) return
424
+ const enabledValues = list.filter((i) => !ctx.itemDisabled(i)).map(ctx.itemToValue)
425
+ const index = active ? values.indexOf(active) : -1
426
+ const step = (delta: number) => {
427
+ const next = values[Math.max(0, Math.min(values.length - 1, index + delta))]
428
+ if (!next) return
429
+ if (event.shiftKey && active && enabledValues.includes(active)) ctx.selectRange(side, active, next)
430
+ focusValue(next)
431
+ }
432
+ switch (event.key) {
433
+ case "ArrowDown":
434
+ event.preventDefault()
435
+ step(1)
436
+ break
437
+ case "ArrowUp":
438
+ event.preventDefault()
439
+ step(-1)
440
+ break
441
+ case "Home":
442
+ event.preventDefault()
443
+ if (values[0]) focusValue(values[0])
444
+ break
445
+ case "End":
446
+ event.preventDefault()
447
+ if (values.length) focusValue(values[values.length - 1])
448
+ break
449
+ case " ":
450
+ event.preventDefault()
451
+ if (active && enabledValues.includes(active)) ctx.toggle(side, active)
452
+ break
453
+ case "Enter":
454
+ event.preventDefault()
455
+ if (active && enabledValues.includes(active)) {
456
+ const next = values[index + 1] ?? values[index - 1] ?? null
457
+ ctx.move(side === "source" ? "right" : "left", [active])
458
+ ctx.setFocused(side, next)
459
+ if (next) queueMicrotask(() => focusValue(next))
460
+ }
461
+ break
462
+ case "a":
463
+ case "A":
464
+ if (event.metaKey || event.ctrlKey) {
465
+ event.preventDefault()
466
+ ctx.selectAll(side, true)
467
+ }
468
+ break
469
+ }
470
+ }}
471
+ {...props}
472
+ >
473
+ {list.map((item) => {
474
+ const v = ctx.itemToValue(item)
475
+ return <TransferListItem key={v} item={item} tabIndex={v === active ? 0 : -1} />
476
+ })}
477
+ </div>
478
+ </TransferListRenderContext.Provider>
479
+ )
480
+ }
481
+
482
+ function TransferListItem<T = TransferListItemBase>({
483
+ item,
484
+ className,
485
+ children,
486
+ ...props
487
+ }: Omit<React.ComponentProps<"div">, "children"> & { item: T; children?: React.ReactNode }) {
488
+ const ctx = useTransferList<T>()
489
+ const side = useTransferListSide()
490
+ const render = React.useContext(TransferListRenderContext)
491
+ const value = ctx.itemToValue(item)
492
+ const disabled = ctx.disabled || ctx.itemDisabled(item)
493
+ const selected = ctx.selected[side].includes(value)
494
+ return (
495
+ <div
496
+ data-slot="transfer-list-item"
497
+ role="option"
498
+ aria-selected={selected}
499
+ aria-disabled={disabled || undefined}
500
+ data-value={value}
501
+ data-selected={selected ? "" : undefined}
502
+ data-disabled={disabled ? "" : undefined}
503
+ className={cn(
504
+ "flex h-7 cursor-default items-center gap-2 rounded-md px-2 outline-none select-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset data-selected:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
505
+ className
506
+ )}
507
+ onFocus={() => ctx.setFocused(side, value)}
508
+ onClick={(event) => {
509
+ if (disabled) return
510
+ const anchor = ctx.focused[side]
511
+ if (event.shiftKey && anchor && anchor !== value) ctx.selectRange(side, anchor, value)
512
+ else ctx.toggle(side, value)
513
+ ctx.setFocused(side, value)
514
+ }}
515
+ onDoubleClick={() => {
516
+ if (!disabled) ctx.move(side === "source" ? "right" : "left", [value])
517
+ }}
518
+ {...props}
519
+ >
520
+ {children ?? (
521
+ <>
522
+ <TransferListItemIndicator />
523
+ <TransferListItemText>{render ? render(item) : ctx.itemToString(item)}</TransferListItemText>
524
+ </>
525
+ )}
526
+ </div>
527
+ )
528
+ }
529
+
530
+ /** Checkbox-styled indicator that follows the item's `data-selected`. */
531
+ function TransferListItemIndicator({ className, children, ...props }: React.ComponentProps<"span">) {
532
+ return (
533
+ <span
534
+ aria-hidden
535
+ data-slot="transfer-list-item-indicator"
536
+ className={cn(
537
+ "flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input text-primary-foreground in-data-selected:border-primary in-data-selected:bg-primary not-in-data-selected:**:data-check:hidden dark:bg-input/30 dark:in-data-selected:bg-primary [&_svg]:size-3",
538
+ className
539
+ )}
540
+ {...props}
541
+ >
542
+ {children ?? <CheckIcon data-check="" />}
543
+ </span>
544
+ )
545
+ }
546
+
547
+ function TransferListItemText({ className, ...props }: React.ComponentProps<"span">) {
548
+ return <span data-slot="transfer-list-item-text" className={cn("flex-1 truncate", className)} {...props} />
549
+ }
550
+
551
+ function TransferListEmpty({ className, children, ...props }: React.ComponentProps<"div">) {
552
+ const ctx = useTransferList()
553
+ const side = useTransferListSide()
554
+ if (ctx.visible[side].length > 0) return null
555
+ const searching = ctx.query[side].trim().length > 0
556
+ return (
557
+ <div
558
+ data-slot="transfer-list-empty"
559
+ className={cn(
560
+ "flex flex-1 items-center justify-center px-4 py-6 text-center text-sm text-muted-foreground",
561
+ className
562
+ )}
563
+ {...props}
564
+ >
565
+ {children ?? (searching ? "No matches" : "Nothing here")}
566
+ </div>
567
+ )
568
+ }
569
+
570
+ /* -------------------------------- controls ------------------------------- */
571
+
572
+ function TransferListControls({ className, ...props }: React.ComponentProps<"div">) {
573
+ return (
574
+ <div
575
+ data-slot="transfer-list-controls"
576
+ className={cn("flex items-center justify-center gap-1 sm:flex-col", className)}
577
+ {...props}
578
+ />
579
+ )
580
+ }
581
+
582
+ const directionIcon = {
583
+ right: <ChevronRightIcon />,
584
+ left: <ChevronLeftIcon />,
585
+ }
586
+ const directionAllIcon = {
587
+ right: <ChevronsRightIcon />,
588
+ left: <ChevronsLeftIcon />,
589
+ }
590
+
591
+ /** Moves the selected items in `direction` ("right" = source → target). Polymorphic via `asChild`. */
592
+ function TransferListMoveTrigger({
593
+ direction,
594
+ asChild,
595
+ className,
596
+ children,
597
+ ...props
598
+ }: React.ComponentProps<typeof Button> & { direction: TransferListDirection; asChild?: boolean }) {
599
+ const ctx = useTransferList()
600
+ const from = sideOf(direction)
601
+ const count = ctx.selected[from].filter((v) => ctx.sides[from].some((i) => ctx.itemToValue(i) === v)).length
602
+ const disabled = ctx.disabled || count === 0
603
+ if (asChild) {
604
+ return (
605
+ <ark.button
606
+ asChild
607
+ data-slot="transfer-list-move-trigger"
608
+ data-direction={direction}
609
+ disabled={disabled}
610
+ onClick={() => ctx.move(direction)}
611
+ {...(props as React.ComponentProps<typeof ark.button>)}
612
+ >
613
+ {children}
614
+ </ark.button>
615
+ )
616
+ }
617
+ return (
618
+ <Button
619
+ type="button"
620
+ variant="outline"
621
+ size="icon-sm"
622
+ data-slot="transfer-list-move-trigger"
623
+ data-direction={direction}
624
+ aria-label={direction === "right" ? `Move ${count} selected right` : `Move ${count} selected left`}
625
+ disabled={disabled}
626
+ onClick={() => ctx.move(direction)}
627
+ className={cn("rotate-90 sm:rotate-0", className)}
628
+ {...props}
629
+ >
630
+ {children ?? directionIcon[direction]}
631
+ </Button>
632
+ )
633
+ }
634
+
635
+ /** Moves every visible item in `direction`. Polymorphic via `asChild`. */
636
+ function TransferListMoveAllTrigger({
637
+ direction,
638
+ asChild,
639
+ className,
640
+ children,
641
+ ...props
642
+ }: React.ComponentProps<typeof Button> & { direction: TransferListDirection; asChild?: boolean }) {
643
+ const ctx = useTransferList()
644
+ const from = sideOf(direction)
645
+ const disabled = ctx.disabled || ctx.visible[from].every((i) => ctx.itemDisabled(i))
646
+ if (asChild) {
647
+ return (
648
+ <ark.button
649
+ asChild
650
+ data-slot="transfer-list-move-all-trigger"
651
+ data-direction={direction}
652
+ disabled={disabled}
653
+ onClick={() => ctx.moveAll(direction)}
654
+ {...(props as React.ComponentProps<typeof ark.button>)}
655
+ >
656
+ {children}
657
+ </ark.button>
658
+ )
659
+ }
660
+ return (
661
+ <Button
662
+ type="button"
663
+ variant="outline"
664
+ size="icon-sm"
665
+ data-slot="transfer-list-move-all-trigger"
666
+ data-direction={direction}
667
+ aria-label={direction === "right" ? "Move all right" : "Move all left"}
668
+ disabled={disabled}
669
+ onClick={() => ctx.moveAll(direction)}
670
+ className={cn("rotate-90 sm:rotate-0", className)}
671
+ {...props}
672
+ >
673
+ {children ?? directionAllIcon[direction]}
674
+ </Button>
675
+ )
676
+ }
677
+
678
+ export {
679
+ TransferList,
680
+ TransferListControls,
681
+ TransferListEmpty,
682
+ TransferListItem,
683
+ TransferListItemIndicator,
684
+ TransferListItemText,
685
+ TransferListItems,
686
+ TransferListMoveAllTrigger,
687
+ TransferListMoveTrigger,
688
+ TransferListPanel,
689
+ TransferListPanelCount,
690
+ TransferListPanelHeader,
691
+ TransferListPanelTitle,
692
+ TransferListSearch,
693
+ TransferListSelectAll,
694
+ useTransferList,
695
+ type TransferListDirection,
696
+ type TransferListItemBase,
697
+ type TransferListProps,
698
+ type TransferListSide,
699
+ type TransferListValueChangeDetails,
700
+ }