@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,217 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@/lib/utils"
5
+ import { TreeView as TreeViewPrimitive, createTreeCollection, type TreeCollection, type TreeNode } from "@ark-ui/react"
6
+ import { CheckIcon, ChevronRightIcon, MinusIcon } from "lucide-react"
7
+
8
+ function TreeView<T extends TreeNode>({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.Root<T>>) {
9
+ return <TreeViewPrimitive.Root data-slot="tree-view" className={cn("w-full text-sm", className)} {...props} />
10
+ }
11
+
12
+ function TreeViewContext({ ...props }: React.ComponentProps<typeof TreeViewPrimitive.Context>) {
13
+ return <TreeViewPrimitive.Context {...props} />
14
+ }
15
+
16
+ function TreeViewLabel({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.Label>) {
17
+ return (
18
+ <TreeViewPrimitive.Label
19
+ data-slot="tree-view-label"
20
+ className={cn("mb-1 text-sm font-medium select-none", className)}
21
+ {...props}
22
+ />
23
+ )
24
+ }
25
+
26
+ function TreeViewTree({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.Tree>) {
27
+ return (
28
+ <TreeViewPrimitive.Tree
29
+ data-slot="tree-view-tree"
30
+ className={cn("flex flex-col gap-px outline-none", className)}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function TreeViewNodeProvider<T>({ ...props }: React.ComponentProps<typeof TreeViewPrimitive.NodeProvider<T>>) {
37
+ return <TreeViewPrimitive.NodeProvider {...props} />
38
+ }
39
+
40
+ function TreeViewNodeContext({ ...props }: React.ComponentProps<typeof TreeViewPrimitive.NodeContext>) {
41
+ return <TreeViewPrimitive.NodeContext {...props} />
42
+ }
43
+
44
+ const rowClassName =
45
+ "group/tree-row relative flex h-7 cursor-default items-center gap-1.5 rounded-md py-1 pr-2 pl-[calc(var(--depth)*--spacing(4)+--spacing(1.5))] outline-none select-none hover:bg-muted data-selected:bg-muted data-selected:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
46
+
47
+ function TreeViewBranch({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.Branch>) {
48
+ return (
49
+ <TreeViewPrimitive.Branch
50
+ data-slot="tree-view-branch"
51
+ className={cn("flex flex-col gap-px", className)}
52
+ {...props}
53
+ />
54
+ )
55
+ }
56
+
57
+ function TreeViewBranchControl({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.BranchControl>) {
58
+ return (
59
+ <TreeViewPrimitive.BranchControl
60
+ data-slot="tree-view-branch-control"
61
+ className={cn(rowClassName, className)}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+
67
+ function TreeViewBranchTrigger({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.BranchTrigger>) {
68
+ return (
69
+ <TreeViewPrimitive.BranchTrigger
70
+ data-slot="tree-view-branch-trigger"
71
+ className={cn("inline-flex", className)}
72
+ {...props}
73
+ />
74
+ )
75
+ }
76
+
77
+ function TreeViewBranchIndicator({
78
+ className,
79
+ children,
80
+ ...props
81
+ }: React.ComponentProps<typeof TreeViewPrimitive.BranchIndicator>) {
82
+ return (
83
+ <TreeViewPrimitive.BranchIndicator
84
+ data-slot="tree-view-branch-indicator"
85
+ className={cn("text-muted-foreground transition-transform data-[state=open]:rotate-90", className)}
86
+ {...props}
87
+ >
88
+ {children ?? <ChevronRightIcon />}
89
+ </TreeViewPrimitive.BranchIndicator>
90
+ )
91
+ }
92
+
93
+ function TreeViewBranchText({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.BranchText>) {
94
+ return (
95
+ <TreeViewPrimitive.BranchText data-slot="tree-view-branch-text" className={cn("truncate", className)} {...props} />
96
+ )
97
+ }
98
+
99
+ function TreeViewBranchContent({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.BranchContent>) {
100
+ return (
101
+ <TreeViewPrimitive.BranchContent
102
+ data-slot="tree-view-branch-content"
103
+ className={cn("relative flex flex-col gap-px", className)}
104
+ {...props}
105
+ />
106
+ )
107
+ }
108
+
109
+ function TreeViewBranchIndentGuide({
110
+ className,
111
+ ...props
112
+ }: React.ComponentProps<typeof TreeViewPrimitive.BranchIndentGuide>) {
113
+ return (
114
+ <TreeViewPrimitive.BranchIndentGuide
115
+ data-slot="tree-view-branch-indent-guide"
116
+ className={cn(
117
+ "absolute inset-y-0 left-[calc(var(--depth)*(--spacing(4))+(--spacing(3)))] w-px bg-border",
118
+ className
119
+ )}
120
+ {...props}
121
+ />
122
+ )
123
+ }
124
+
125
+ function TreeViewItem({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.Item>) {
126
+ return <TreeViewPrimitive.Item data-slot="tree-view-item" className={cn(rowClassName, className)} {...props} />
127
+ }
128
+
129
+ function TreeViewItemIndicator({
130
+ className,
131
+ children,
132
+ ...props
133
+ }: React.ComponentProps<typeof TreeViewPrimitive.ItemIndicator>) {
134
+ return (
135
+ <TreeViewPrimitive.ItemIndicator
136
+ data-slot="tree-view-item-indicator"
137
+ className={cn("ml-auto text-muted-foreground", className)}
138
+ {...props}
139
+ >
140
+ {children ?? <CheckIcon />}
141
+ </TreeViewPrimitive.ItemIndicator>
142
+ )
143
+ }
144
+
145
+ function TreeViewItemText({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.ItemText>) {
146
+ return <TreeViewPrimitive.ItemText data-slot="tree-view-item-text" className={cn("truncate", className)} {...props} />
147
+ }
148
+
149
+ function TreeViewNodeCheckbox({
150
+ className,
151
+ children,
152
+ ...props
153
+ }: React.ComponentProps<typeof TreeViewPrimitive.NodeCheckbox>) {
154
+ return (
155
+ <TreeViewPrimitive.NodeCheckbox
156
+ data-slot="tree-view-node-checkbox"
157
+ className={cn(
158
+ "flex size-4 shrink-0 items-center justify-center rounded-lg border border-input text-primary-foreground data-indeterminate:border-primary data-indeterminate:bg-primary data-checked:border-primary data-checked:bg-primary [&_svg]:size-3",
159
+ className
160
+ )}
161
+ {...props}
162
+ >
163
+ {children ?? (
164
+ <TreeViewNodeCheckboxIndicator indeterminate={<MinusIcon />}>
165
+ <CheckIcon />
166
+ </TreeViewNodeCheckboxIndicator>
167
+ )}
168
+ </TreeViewPrimitive.NodeCheckbox>
169
+ )
170
+ }
171
+
172
+ function TreeViewNodeCheckboxIndicator({
173
+ ...props
174
+ }: React.ComponentProps<typeof TreeViewPrimitive.NodeCheckboxIndicator>) {
175
+ return <TreeViewPrimitive.NodeCheckboxIndicator data-slot="tree-view-node-checkbox-indicator" {...props} />
176
+ }
177
+
178
+ function TreeViewNodeRenameInput({
179
+ className,
180
+ ...props
181
+ }: React.ComponentProps<typeof TreeViewPrimitive.NodeRenameInput>) {
182
+ return (
183
+ <TreeViewPrimitive.NodeRenameInput
184
+ data-slot="tree-view-node-rename-input"
185
+ className={cn(
186
+ "h-6 min-w-0 flex-1 rounded-sm border border-input bg-background px-1 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
187
+ className
188
+ )}
189
+ {...props}
190
+ />
191
+ )
192
+ }
193
+
194
+ export {
195
+ TreeView,
196
+ TreeViewBranch,
197
+ TreeViewBranchContent,
198
+ TreeViewBranchControl,
199
+ TreeViewBranchIndentGuide,
200
+ TreeViewBranchIndicator,
201
+ TreeViewBranchText,
202
+ TreeViewBranchTrigger,
203
+ TreeViewContext,
204
+ TreeViewItem,
205
+ TreeViewItemIndicator,
206
+ TreeViewItemText,
207
+ TreeViewLabel,
208
+ TreeViewNodeCheckbox,
209
+ TreeViewNodeCheckboxIndicator,
210
+ TreeViewNodeContext,
211
+ TreeViewNodeProvider,
212
+ TreeViewNodeRenameInput,
213
+ TreeViewTree,
214
+ createTreeCollection,
215
+ type TreeCollection,
216
+ type TreeNode,
217
+ }
@@ -0,0 +1,343 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ /* -------------------------------- context -------------------------------- */
7
+
8
+ type VirtualItem = {
9
+ index: number
10
+ key: React.Key
11
+ /** Offset from the top of the content, in px. */
12
+ start: number
13
+ size: number
14
+ end: number
15
+ /** False until the row's real height has been measured. */
16
+ measured: boolean
17
+ }
18
+
19
+ type ScrollAlign = "auto" | "start" | "center" | "end"
20
+
21
+ type VirtualListContextValue = {
22
+ count: number
23
+ items: VirtualItem[]
24
+ totalSize: number
25
+ /** First and last rendered index (overscan included), or null when empty. */
26
+ range: { start: number; end: number } | null
27
+ measure: (index: number, size: number) => void
28
+ scrollToIndex: (index: number, options?: { align?: ScrollAlign; behavior?: ScrollBehavior }) => void
29
+ scrollToOffset: (offset: number, behavior?: ScrollBehavior) => void
30
+ viewportRef: React.RefObject<HTMLDivElement | null>
31
+ getItemKey: (index: number) => React.Key
32
+ }
33
+
34
+ const VirtualListContext = React.createContext<VirtualListContextValue | null>(null)
35
+
36
+ function useVirtualList() {
37
+ const ctx = React.useContext(VirtualListContext)
38
+ if (!ctx) throw new Error("VirtualList parts must be used inside <VirtualList>")
39
+ return ctx
40
+ }
41
+
42
+ /* -------------------------------- helpers -------------------------------- */
43
+
44
+ /** Largest index whose start is <= offset. */
45
+ function findIndex(starts: Float64Array, count: number, offset: number) {
46
+ let lo = 0
47
+ let hi = count - 1
48
+ while (lo < hi) {
49
+ const mid = (lo + hi + 1) >> 1
50
+ if (starts[mid] <= offset) lo = mid
51
+ else hi = mid - 1
52
+ }
53
+ return lo
54
+ }
55
+
56
+ /* ---------------------------------- root --------------------------------- */
57
+
58
+ type VirtualListProps = Omit<React.ComponentProps<"div">, "children"> & {
59
+ count: number
60
+ /** Row height before measurement (a number, or per index). */
61
+ estimateSize?: number | ((index: number) => number)
62
+ /** Rows rendered beyond the visible window on each side. */
63
+ overscan?: number
64
+ gap?: number
65
+ paddingStart?: number
66
+ paddingEnd?: number
67
+ getItemKey?: (index: number) => React.Key
68
+ /** Fires when the rendered window changes. */
69
+ onRangeChange?: (range: { start: number; end: number } | null) => void
70
+ children: React.ReactNode
71
+ }
72
+
73
+ function VirtualList({
74
+ count,
75
+ estimateSize = 36,
76
+ overscan = 6,
77
+ gap = 0,
78
+ paddingStart = 0,
79
+ paddingEnd = 0,
80
+ getItemKey = (index) => index,
81
+ onRangeChange,
82
+ className,
83
+ children,
84
+ ...props
85
+ }: VirtualListProps) {
86
+ const viewportRef = React.useRef<HTMLDivElement | null>(null)
87
+ const sizes = React.useRef(new Map<number, number>())
88
+ const [version, setVersion] = React.useState(0)
89
+ const [scrollTop, setScrollTop] = React.useState(0)
90
+ const [viewportSize, setViewportSize] = React.useState(0)
91
+ const estimate = React.useCallback(
92
+ (index: number) => (typeof estimateSize === "function" ? estimateSize(index) : estimateSize),
93
+ [estimateSize]
94
+ )
95
+
96
+ // Prefix sums of row starts; recomputed when a measurement or the count changes.
97
+ const layout = React.useMemo(() => {
98
+ const starts = new Float64Array(count)
99
+ let offset = paddingStart
100
+ for (let i = 0; i < count; i++) {
101
+ starts[i] = offset
102
+ offset += (sizes.current.get(i) ?? estimate(i)) + (i < count - 1 ? gap : 0)
103
+ }
104
+ return { starts, totalSize: offset + paddingEnd }
105
+ // `version` is the measurement tick.
106
+ // eslint-disable-next-line react-hooks/exhaustive-deps
107
+ }, [count, estimate, gap, paddingStart, paddingEnd, version])
108
+
109
+ const range = React.useMemo(() => {
110
+ if (count === 0 || viewportSize === 0) return count === 0 ? null : { start: 0, end: Math.min(count - 1, overscan) }
111
+ const first = findIndex(layout.starts, count, scrollTop)
112
+ const last = findIndex(layout.starts, count, scrollTop + viewportSize)
113
+ return { start: Math.max(0, first - overscan), end: Math.min(count - 1, last + overscan) }
114
+ }, [count, viewportSize, scrollTop, layout, overscan])
115
+
116
+ const items = React.useMemo(() => {
117
+ if (!range) return []
118
+ const out: VirtualItem[] = []
119
+ for (let i = range.start; i <= range.end; i++) {
120
+ const measured = sizes.current.has(i)
121
+ const size = sizes.current.get(i) ?? estimate(i)
122
+ out.push({ index: i, key: getItemKey(i), start: layout.starts[i], size, end: layout.starts[i] + size, measured })
123
+ }
124
+ return out
125
+ // eslint-disable-next-line react-hooks/exhaustive-deps
126
+ }, [range, layout, estimate, getItemKey, version])
127
+
128
+ const onRangeRef = React.useRef(onRangeChange)
129
+ onRangeRef.current = onRangeChange
130
+ const lastRange = React.useRef<string>("")
131
+ React.useEffect(() => {
132
+ const key = range ? `${range.start}-${range.end}` : "none"
133
+ if (key === lastRange.current) return
134
+ lastRange.current = key
135
+ onRangeRef.current?.(range)
136
+ }, [range])
137
+
138
+ // Drop stale measurements when the list shrinks.
139
+ React.useEffect(() => {
140
+ let dirty = false
141
+ for (const index of sizes.current.keys()) {
142
+ if (index >= count) {
143
+ sizes.current.delete(index)
144
+ dirty = true
145
+ }
146
+ }
147
+ if (dirty) setVersion((v) => v + 1)
148
+ }, [count])
149
+
150
+ const measure = React.useCallback((index: number, size: number) => {
151
+ const prev = sizes.current.get(index)
152
+ if (prev !== undefined && Math.abs(prev - size) < 0.5) return
153
+ sizes.current.set(index, size)
154
+ setVersion((v) => v + 1)
155
+ }, [])
156
+
157
+ React.useLayoutEffect(() => {
158
+ const el = viewportRef.current
159
+ if (!el) return
160
+ const update = () => setViewportSize(el.clientHeight)
161
+ update()
162
+ const ro = new ResizeObserver(update)
163
+ ro.observe(el)
164
+ return () => ro.disconnect()
165
+ }, [])
166
+
167
+ const scrollToOffset = React.useCallback((offset: number, behavior: ScrollBehavior = "auto") => {
168
+ viewportRef.current?.scrollTo({ top: offset, behavior })
169
+ }, [])
170
+
171
+ const pending = React.useRef<{ index: number; align: ScrollAlign; tries: number } | null>(null)
172
+ const offsetFor = React.useCallback(
173
+ (index: number, align: ScrollAlign) => {
174
+ const el = viewportRef.current
175
+ if (!el) return 0
176
+ const start = layout.starts[index] ?? 0
177
+ const size = sizes.current.get(index) ?? estimate(index)
178
+ const height = el.clientHeight
179
+ let resolved = align
180
+ if (align === "auto") {
181
+ if (start < el.scrollTop) resolved = "start"
182
+ else if (start + size > el.scrollTop + height) resolved = "end"
183
+ else return el.scrollTop
184
+ }
185
+ if (resolved === "start") return start
186
+ if (resolved === "end") return start + size - height
187
+ return start + size / 2 - height / 2
188
+ },
189
+ [layout, estimate]
190
+ )
191
+
192
+ const scrollToIndex = React.useCallback(
193
+ (index: number, options?: { align?: ScrollAlign; behavior?: ScrollBehavior }) => {
194
+ const clamped = Math.max(0, Math.min(count - 1, index))
195
+ const align = options?.align ?? "auto"
196
+ pending.current = { index: clamped, align, tries: 0 }
197
+ scrollToOffset(offsetFor(clamped, align), options?.behavior)
198
+ },
199
+ [count, offsetFor, scrollToOffset]
200
+ )
201
+
202
+ // Estimated rows shift as they get measured: re-aim at the target a few times.
203
+ React.useLayoutEffect(() => {
204
+ const p = pending.current
205
+ const el = viewportRef.current
206
+ if (!p || !el) return
207
+ if (p.tries >= 4 || !sizes.current.has(p.index)) {
208
+ if (p.tries >= 4) pending.current = null
209
+ return
210
+ }
211
+ const target = offsetFor(p.index, p.align === "auto" ? "start" : p.align)
212
+ if (Math.abs(el.scrollTop - target) > 1) {
213
+ p.tries += 1
214
+ el.scrollTop = target
215
+ } else pending.current = null
216
+ })
217
+
218
+ const ctx: VirtualListContextValue = {
219
+ count,
220
+ items,
221
+ totalSize: layout.totalSize,
222
+ range,
223
+ measure,
224
+ scrollToIndex,
225
+ scrollToOffset,
226
+ viewportRef,
227
+ getItemKey,
228
+ }
229
+
230
+ return (
231
+ <VirtualListContext.Provider value={ctx}>
232
+ <VirtualListScroll onScroll={setScrollTop} />
233
+ <div data-slot="virtual-list" className={cn("flex min-h-0 flex-col", className)} {...props}>
234
+ {children}
235
+ </div>
236
+ </VirtualListContext.Provider>
237
+ )
238
+ }
239
+
240
+ /** Attaches the scroll listener once the viewport mounts (kept out of the root's render). */
241
+ function VirtualListScroll({ onScroll }: { onScroll: (top: number) => void }) {
242
+ const ctx = useVirtualList()
243
+ React.useEffect(() => {
244
+ const el = ctx.viewportRef.current
245
+ if (!el) return
246
+ const handler = () => onScroll(el.scrollTop)
247
+ el.addEventListener("scroll", handler, { passive: true })
248
+ return () => el.removeEventListener("scroll", handler)
249
+ }, [ctx.viewportRef, onScroll])
250
+ return null
251
+ }
252
+
253
+ /* --------------------------------- parts --------------------------------- */
254
+
255
+ function VirtualListViewport({ className, ...props }: React.ComponentProps<"div">) {
256
+ const ctx = useVirtualList()
257
+ return (
258
+ <div
259
+ ref={ctx.viewportRef}
260
+ data-slot="virtual-list-viewport"
261
+ className={cn("relative min-h-0 overflow-y-auto overscroll-contain", className)}
262
+ {...props}
263
+ />
264
+ )
265
+ }
266
+
267
+ /** Sized to the whole list so the scrollbar is right; rows position inside it. */
268
+ function VirtualListContent({ className, style, ...props }: React.ComponentProps<"div">) {
269
+ const ctx = useVirtualList()
270
+ return (
271
+ <div
272
+ data-slot="virtual-list-content"
273
+ className={cn("relative w-full", className)}
274
+ style={{ height: ctx.totalSize, ...style }}
275
+ {...props}
276
+ />
277
+ )
278
+ }
279
+
280
+ /** Renders the visible window. The child renders one row and must return a `VirtualListItem`. */
281
+ function VirtualListItems({ children }: { children: (item: VirtualItem) => React.ReactNode }) {
282
+ const ctx = useVirtualList()
283
+ return (
284
+ <>
285
+ {ctx.items.map((item) => (
286
+ <React.Fragment key={item.key}>{children(item)}</React.Fragment>
287
+ ))}
288
+ </>
289
+ )
290
+ }
291
+
292
+ function VirtualListItem({ index, className, style, ...props }: React.ComponentProps<"div"> & { index: number }) {
293
+ const { items, measure } = useVirtualList()
294
+ const item = items.find((i) => i.index === index)
295
+ const ref = React.useCallback(
296
+ (el: HTMLDivElement | null) => {
297
+ if (!el) return
298
+ const run = () => measure(index, el.getBoundingClientRect().height)
299
+ run()
300
+ const ro = new ResizeObserver(run)
301
+ ro.observe(el)
302
+ // React 19 ref cleanup
303
+ return () => ro.disconnect()
304
+ },
305
+ [measure, index]
306
+ )
307
+ return (
308
+ <div
309
+ ref={ref}
310
+ data-slot="virtual-list-item"
311
+ data-index={index}
312
+ className={cn("absolute inset-x-0 top-0", className)}
313
+ style={{ transform: `translateY(${item?.start ?? 0}px)`, ...style }}
314
+ {...props}
315
+ />
316
+ )
317
+ }
318
+
319
+ function VirtualListEmpty({ className, children, ...props }: React.ComponentProps<"div">) {
320
+ const ctx = useVirtualList()
321
+ if (ctx.count > 0) return null
322
+ return (
323
+ <div
324
+ data-slot="virtual-list-empty"
325
+ className={cn("flex flex-1 items-center justify-center py-8 text-sm text-muted-foreground", className)}
326
+ {...props}
327
+ >
328
+ {children ?? "Nothing to show"}
329
+ </div>
330
+ )
331
+ }
332
+
333
+ export {
334
+ VirtualList,
335
+ VirtualListContent,
336
+ VirtualListEmpty,
337
+ VirtualListItem,
338
+ VirtualListItems,
339
+ VirtualListViewport,
340
+ useVirtualList,
341
+ type VirtualItem,
342
+ type VirtualListProps,
343
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
@@ -0,0 +1,31 @@
1
+ import * as React from "react"
2
+
3
+ type Updater<T> = T | ((prev: T) => T)
4
+
5
+ /**
6
+ * Controlled-or-uncontrolled state. Pass the controlled value (or `undefined`), the
7
+ * uncontrolled default, and an optional change callback that fires in both modes.
8
+ */
9
+ export function useControllable<T>(
10
+ controlled: T | undefined,
11
+ defaultValue: T,
12
+ onChange?: (value: T) => void
13
+ ): [T, (next: Updater<T>) => void] {
14
+ const [internal, setInternal] = React.useState<T>(defaultValue)
15
+ const isControlled = controlled !== undefined
16
+ const value = isControlled ? (controlled as T) : internal
17
+ const valueRef = React.useRef(value)
18
+ valueRef.current = value
19
+ const onChangeRef = React.useRef(onChange)
20
+ onChangeRef.current = onChange
21
+ const set = React.useCallback(
22
+ (updater: Updater<T>) => {
23
+ const next = typeof updater === "function" ? (updater as (prev: T) => T)(valueRef.current) : updater
24
+ if (Object.is(next, valueRef.current)) return
25
+ if (!isControlled) setInternal(next)
26
+ onChangeRef.current?.(next)
27
+ },
28
+ [isControlled]
29
+ )
30
+ return [value, set]
31
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The contract the data table parts render against. `<DataTable>` accepts any
3
+ * object that satisfies it, so the parts are independent of where the rows,
4
+ * sorting, filtering, and paging come from.
5
+ *
6
+ * The built-in engine in `@/lib/table` satisfies it directly and is the
7
+ * default adapter. To drive the parts from another source (a server-side
8
+ * query, a different table library, a store), implement this interface over
9
+ * that source and pass the result to `<DataTable table={...}>`.
10
+ *
11
+ * Columns are data only: the adapter exposes ids, labels, sorting, visibility,
12
+ * and filtering. Headers and cells are composed in JSX on the page with
13
+ * `DataTableHead` / `DataTableCell`, keyed by column id.
14
+ */
15
+
16
+ export type DataTableSortDirection = false | "asc" | "desc"
17
+
18
+ export interface DataTableColumn<TData, TValue = unknown> {
19
+ id: string
20
+ /** Human label for view options and headers; falls back to `id`. */
21
+ label?: string
22
+ /** Present on columns that read a value from the row (used by view options). */
23
+ accessorFn?: ((row: TData, index: number) => TValue) | undefined
24
+ getCanSort: () => boolean
25
+ getIsSorted: () => DataTableSortDirection
26
+ toggleSorting: (desc?: boolean) => void
27
+ getCanHide: () => boolean
28
+ getIsVisible: () => boolean
29
+ toggleVisibility: (visible?: boolean) => void
30
+ getFilterValue: () => unknown
31
+ setFilterValue: (value: unknown) => void
32
+ getFacetedUniqueValues: () => Map<unknown, number>
33
+ }
34
+
35
+ export interface DataTableRow<TData> {
36
+ id: string
37
+ original: TData
38
+ getValue: <TValue = unknown>(columnId: string) => TValue
39
+ getIsSelected: () => boolean
40
+ toggleSelected: (selected?: boolean) => void
41
+ }
42
+
43
+ export interface DataTableInstance<TData> {
44
+ getState: () => {
45
+ globalFilter: string
46
+ columnFilters: { id: string; value: unknown }[]
47
+ pagination: { pageIndex: number; pageSize: number }
48
+ }
49
+ getAllColumns: () => DataTableColumn<TData>[]
50
+ getColumn: (id: string) => DataTableColumn<TData> | undefined
51
+ /** The rows to render (already filtered, sorted, and paginated). */
52
+ getRowModel: () => { rows: DataTableRow<TData>[] }
53
+ getFilteredSelectedRowModel: () => { rows: DataTableRow<TData>[] }
54
+ setGlobalFilter: (value: string) => void
55
+ resetColumnFilters: () => void
56
+ setPageIndex: (index: number) => void
57
+ setPageSize: (size: number) => void
58
+ getPageCount: () => number
59
+ getCanPreviousPage: () => boolean
60
+ getCanNextPage: () => boolean
61
+ previousPage: () => void
62
+ nextPage: () => void
63
+ resetRowSelection: () => void
64
+ getIsAllPageRowsSelected: () => boolean
65
+ getIsSomePageRowsSelected: () => boolean
66
+ toggleAllPageRowsSelected: (selected?: boolean) => void
67
+ }