@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,483 @@
1
+ import * as React from "react"
2
+ import { useControllable } from "@/lib/controllable"
3
+
4
+ /**
5
+ * A small headless table engine: column definitions, sorting, global and
6
+ * per-column filtering with faceted counts, pagination, row selection, and
7
+ * column visibility. No external dependencies.
8
+ */
9
+
10
+ /* ------------------------------- Definitions ------------------------------- */
11
+
12
+ export type SortingState = { id: string; desc: boolean }[]
13
+ export type ColumnFiltersState = { id: string; value: unknown }[]
14
+ export type PaginationState = { pageIndex: number; pageSize: number }
15
+ export type RowSelectionState = Record<string, boolean>
16
+ export type VisibilityState = Record<string, boolean>
17
+
18
+ export type TableState = {
19
+ sorting: SortingState
20
+ columnFilters: ColumnFiltersState
21
+ globalFilter: string
22
+ pagination: PaginationState
23
+ rowSelection: RowSelectionState
24
+ columnVisibility: VisibilityState
25
+ }
26
+
27
+ export type Updater<T> = T | ((previous: T) => T)
28
+ export type OnChange<T> = (updater: Updater<T>) => void
29
+
30
+ export type FilterFn<TData> = (row: Row<TData>, columnId: string, filterValue: unknown) => boolean
31
+
32
+ export type SortingFn<TData> = (a: Row<TData>, b: Row<TData>, columnId: string) => number
33
+
34
+ export type ColumnDef<TData, TValue = unknown> = {
35
+ /** Required when no `accessorKey` is given. */
36
+ id?: string
37
+ /** Read the value from this property of the row. */
38
+ accessorKey?: keyof TData & string
39
+ /** Compute the value from the row. */
40
+ accessorFn?: (row: TData, index: number) => TValue
41
+ /** Human label (view options, headers); falls back to the column id. */
42
+ label?: string
43
+ enableSorting?: boolean
44
+ enableHiding?: boolean
45
+ enableGlobalFilter?: boolean
46
+ filterFn?: FilterFn<TData>
47
+ sortingFn?: SortingFn<TData>
48
+ }
49
+
50
+ /* ---------------------------------- Runtime -------------------------------- */
51
+
52
+ export interface Column<TData, TValue = unknown> {
53
+ id: string
54
+ label?: string
55
+ columnDef: ColumnDef<TData, TValue>
56
+ accessorFn?: (row: TData, index: number) => TValue
57
+ getCanSort: () => boolean
58
+ getIsSorted: () => false | "asc" | "desc"
59
+ toggleSorting: (desc?: boolean) => void
60
+ clearSorting: () => void
61
+ getCanHide: () => boolean
62
+ getIsVisible: () => boolean
63
+ toggleVisibility: (visible?: boolean) => void
64
+ getCanFilter: () => boolean
65
+ getFilterValue: () => unknown
66
+ setFilterValue: (value: unknown) => void
67
+ getFacetedUniqueValues: () => Map<unknown, number>
68
+ }
69
+
70
+ export interface Row<TData> {
71
+ id: string
72
+ index: number
73
+ original: TData
74
+ getValue: <TValue = unknown>(columnId: string) => TValue
75
+ getIsSelected: () => boolean
76
+ getCanSelect: () => boolean
77
+ toggleSelected: (selected?: boolean) => void
78
+ }
79
+
80
+ export interface RowModel<TData> {
81
+ rows: Row<TData>[]
82
+ }
83
+
84
+ export interface Table<TData> {
85
+ options: TableOptions<TData>
86
+ getState: () => TableState
87
+ getAllColumns: () => Column<TData>[]
88
+ getVisibleColumns: () => Column<TData>[]
89
+ getColumn: (id: string) => Column<TData> | undefined
90
+ /** All rows before filtering. */
91
+ getCoreRowModel: () => RowModel<TData>
92
+ /** Rows after global and column filters. */
93
+ getFilteredRowModel: () => RowModel<TData>
94
+ /** Filtered and sorted rows. */
95
+ getSortedRowModel: () => RowModel<TData>
96
+ /** The current page of sorted, filtered rows. */
97
+ getRowModel: () => RowModel<TData>
98
+ getFilteredSelectedRowModel: () => RowModel<TData>
99
+ getSelectedRowModel: () => RowModel<TData>
100
+ // sorting
101
+ setSorting: OnChange<SortingState>
102
+ resetSorting: () => void
103
+ // filtering
104
+ setGlobalFilter: (value: string) => void
105
+ setColumnFilters: OnChange<ColumnFiltersState>
106
+ resetColumnFilters: () => void
107
+ // pagination
108
+ setPagination: OnChange<PaginationState>
109
+ setPageIndex: (index: number) => void
110
+ setPageSize: (size: number) => void
111
+ getPageCount: () => number
112
+ getCanPreviousPage: () => boolean
113
+ getCanNextPage: () => boolean
114
+ previousPage: () => void
115
+ nextPage: () => void
116
+ // selection
117
+ setRowSelection: OnChange<RowSelectionState>
118
+ resetRowSelection: () => void
119
+ getIsAllPageRowsSelected: () => boolean
120
+ getIsSomePageRowsSelected: () => boolean
121
+ toggleAllPageRowsSelected: (selected?: boolean) => void
122
+ getIsAllRowsSelected: () => boolean
123
+ toggleAllRowsSelected: (selected?: boolean) => void
124
+ // visibility
125
+ setColumnVisibility: OnChange<VisibilityState>
126
+ resetColumnVisibility: () => void
127
+ }
128
+
129
+ export type TableOptions<TData> = {
130
+ data: TData[]
131
+ columns: ColumnDef<TData, any>[] // eslint-disable-line @typescript-eslint/no-explicit-any
132
+ getRowId?: (row: TData, index: number) => string
133
+ enableRowSelection?: boolean | ((row: Row<TData>) => boolean)
134
+ /** Runs against every row when `globalFilter` is set. Defaults to a case-insensitive `includes` over globally filterable columns. */
135
+ globalFilterFn?: FilterFn<TData>
136
+ /** Uncontrolled initial values. */
137
+ initialState?: Partial<TableState>
138
+ /** Controlled values; pair each with its `on*Change` handler. */
139
+ state?: Partial<TableState>
140
+ onSortingChange?: OnChange<SortingState>
141
+ onColumnFiltersChange?: OnChange<ColumnFiltersState>
142
+ onGlobalFilterChange?: OnChange<string>
143
+ onPaginationChange?: OnChange<PaginationState>
144
+ onRowSelectionChange?: OnChange<RowSelectionState>
145
+ onColumnVisibilityChange?: OnChange<VisibilityState>
146
+ /** Keep the page index inside the page count as filters change. Defaults to true. */
147
+ autoResetPageIndex?: boolean
148
+ }
149
+
150
+ /* ----------------------------------- Helpers -------------------------------- */
151
+
152
+ function defaultCompare(a: unknown, b: unknown) {
153
+ if (a == null && b == null) return 0
154
+ if (a == null) return 1
155
+ if (b == null) return -1
156
+ if (typeof a === "number" && typeof b === "number") return a - b
157
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()
158
+ return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" })
159
+ }
160
+
161
+ function defaultColumnFilter<TData>(row: Row<TData>, columnId: string, filterValue: unknown) {
162
+ const value = row.getValue(columnId)
163
+ if (Array.isArray(filterValue)) return filterValue.includes(value)
164
+ if (filterValue == null || filterValue === "") return true
165
+ return String(value ?? "")
166
+ .toLowerCase()
167
+ .includes(String(filterValue).toLowerCase())
168
+ }
169
+
170
+ /* ------------------------------------ Hook ---------------------------------- */
171
+
172
+ export function useTable<TData>(options: TableOptions<TData>): Table<TData> {
173
+ const {
174
+ data,
175
+ columns: columnDefs,
176
+ getRowId,
177
+ enableRowSelection = true,
178
+ globalFilterFn,
179
+ initialState,
180
+ state: controlled,
181
+ autoResetPageIndex = true,
182
+ } = options
183
+
184
+ const [sorting, setSorting] = useControllable(
185
+ controlled?.sorting,
186
+ initialState?.sorting ?? [],
187
+ options.onSortingChange
188
+ )
189
+ const [columnFilters, setColumnFilters] = useControllable(
190
+ controlled?.columnFilters,
191
+ initialState?.columnFilters ?? [],
192
+ options.onColumnFiltersChange
193
+ )
194
+ const [globalFilter, setGlobalFilterState] = useControllable(
195
+ controlled?.globalFilter,
196
+ initialState?.globalFilter ?? "",
197
+ options.onGlobalFilterChange
198
+ )
199
+ const [pagination, setPagination] = useControllable(
200
+ controlled?.pagination,
201
+ initialState?.pagination ?? { pageIndex: 0, pageSize: 10 },
202
+ options.onPaginationChange
203
+ )
204
+ const [rowSelection, setRowSelection] = useControllable(
205
+ controlled?.rowSelection,
206
+ initialState?.rowSelection ?? {},
207
+ options.onRowSelectionChange
208
+ )
209
+ const [columnVisibility, setColumnVisibility] = useControllable(
210
+ controlled?.columnVisibility,
211
+ initialState?.columnVisibility ?? {},
212
+ options.onColumnVisibilityChange
213
+ )
214
+
215
+ const state: TableState = React.useMemo(
216
+ () => ({ sorting, columnFilters, globalFilter, pagination, rowSelection, columnVisibility }),
217
+ [sorting, columnFilters, globalFilter, pagination, rowSelection, columnVisibility]
218
+ )
219
+
220
+ // Keep a mutable reference so column and row methods always see fresh state
221
+ // without rebuilding every object on each render.
222
+ const tableRef = React.useRef<Table<TData>>(null!)
223
+
224
+ /* Columns */
225
+ const columns = React.useMemo<Column<TData>[]>(() => {
226
+ return columnDefs.map((def) => {
227
+ const id = def.id ?? def.accessorKey
228
+ if (!id) throw new Error("Every column needs an `id` or an `accessorKey`.")
229
+ const accessorFn =
230
+ def.accessorFn ??
231
+ (def.accessorKey ? (row: TData) => (row as Record<string, unknown>)[def.accessorKey as string] : undefined)
232
+ const column: Column<TData> = {
233
+ id,
234
+ columnDef: def,
235
+
236
+ label: def.label,
237
+ accessorFn: accessorFn as Column<TData>["accessorFn"],
238
+ getCanSort: () => (def.enableSorting ?? true) && !!accessorFn,
239
+ getIsSorted: () => {
240
+ const entry = tableRef.current.getState().sorting.find((s) => s.id === id)
241
+ return entry ? (entry.desc ? "desc" : "asc") : false
242
+ },
243
+ toggleSorting: (desc) => {
244
+ tableRef.current.setSorting((prev) => {
245
+ const current = prev.find((s) => s.id === id)
246
+ const nextDesc = desc ?? (current ? !current.desc : false)
247
+ return [{ id, desc: nextDesc }]
248
+ })
249
+ },
250
+ clearSorting: () => tableRef.current.setSorting((prev) => prev.filter((s) => s.id !== id)),
251
+ getCanHide: () => def.enableHiding ?? true,
252
+ getIsVisible: () => tableRef.current.getState().columnVisibility[id] !== false,
253
+ toggleVisibility: (visible) => {
254
+ tableRef.current.setColumnVisibility((prev) => ({
255
+ ...prev,
256
+ [id]: visible ?? prev[id] === false,
257
+ }))
258
+ },
259
+ getCanFilter: () => !!accessorFn,
260
+ getFilterValue: () => tableRef.current.getState().columnFilters.find((f) => f.id === id)?.value,
261
+ setFilterValue: (value) => {
262
+ tableRef.current.setColumnFilters((prev) => {
263
+ const rest = prev.filter((f) => f.id !== id)
264
+ const isEmpty =
265
+ value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0)
266
+ return isEmpty ? rest : [...rest, { id, value }]
267
+ })
268
+ },
269
+ getFacetedUniqueValues: () => facetsRef.current.get(id) ?? new Map(),
270
+ }
271
+ return column
272
+ })
273
+ }, [columnDefs])
274
+
275
+ const columnById = React.useMemo(() => new Map(columns.map((c) => [c.id, c])), [columns])
276
+
277
+ /* Rows */
278
+ const coreRows = React.useMemo<Row<TData>[]>(() => {
279
+ return data.map((original, index) => {
280
+ const id = getRowId ? getRowId(original, index) : String(index)
281
+ const valueCache = new Map<string, unknown>()
282
+ const row: Row<TData> = {
283
+ id,
284
+ index,
285
+ original,
286
+ getValue: <TValue>(columnId: string) => {
287
+ if (valueCache.has(columnId)) return valueCache.get(columnId) as TValue
288
+ const column = columnById.get(columnId)
289
+ const value = column?.accessorFn ? column.accessorFn(original, index) : undefined
290
+ valueCache.set(columnId, value)
291
+ return value as TValue
292
+ },
293
+ getIsSelected: () => !!tableRef.current.getState().rowSelection[id],
294
+ getCanSelect: () => (typeof enableRowSelection === "function" ? enableRowSelection(row) : enableRowSelection),
295
+ toggleSelected: (selected) => {
296
+ if (!row.getCanSelect()) return
297
+ tableRef.current.setRowSelection((prev) => {
298
+ const next = { ...prev }
299
+ const value = selected ?? !prev[id]
300
+ if (value) next[id] = true
301
+ else delete next[id]
302
+ return next
303
+ })
304
+ },
305
+ }
306
+ return row
307
+ })
308
+ }, [data, getRowId, columnById, enableRowSelection])
309
+
310
+ /* Filtering */
311
+ const globallyFilteredRows = React.useMemo(() => {
312
+ if (!globalFilter) return coreRows
313
+ const fn: FilterFn<TData> =
314
+ globalFilterFn ??
315
+ ((row, _id, value) => {
316
+ const search = String(value).toLowerCase()
317
+ return columns.some((column) => {
318
+ if (!column.accessorFn || column.columnDef.enableGlobalFilter === false) return false
319
+ return String(row.getValue(column.id) ?? "")
320
+ .toLowerCase()
321
+ .includes(search)
322
+ })
323
+ })
324
+ return coreRows.filter((row) => fn(row, "__global__", globalFilter))
325
+ }, [coreRows, globalFilter, globalFilterFn, columns])
326
+
327
+ const filteredRows = React.useMemo(() => {
328
+ let rows = globallyFilteredRows
329
+ for (const filter of columnFilters) {
330
+ const column = columnById.get(filter.id)
331
+ if (!column) continue
332
+ const fn = column.columnDef.filterFn ?? defaultColumnFilter
333
+ rows = rows.filter((row) => fn(row, filter.id, filter.value))
334
+ }
335
+ return rows
336
+ }, [globallyFilteredRows, columnFilters, columnById])
337
+
338
+ /* Facets: unique value counts per column, over rows filtered by everything except that column. */
339
+ const facetsRef = React.useRef(new Map<string, Map<unknown, number>>())
340
+ facetsRef.current = React.useMemo(() => {
341
+ const result = new Map<string, Map<unknown, number>>()
342
+ for (const column of columns) {
343
+ if (!column.accessorFn) continue
344
+ let rows = globallyFilteredRows
345
+ for (const filter of columnFilters) {
346
+ if (filter.id === column.id) continue
347
+ const other = columnById.get(filter.id)
348
+ if (!other) continue
349
+ const fn = other.columnDef.filterFn ?? defaultColumnFilter
350
+ rows = rows.filter((row) => fn(row, filter.id, filter.value))
351
+ }
352
+ const counts = new Map<unknown, number>()
353
+ for (const row of rows) {
354
+ const value = row.getValue(column.id)
355
+ counts.set(value, (counts.get(value) ?? 0) + 1)
356
+ }
357
+ result.set(column.id, counts)
358
+ }
359
+ return result
360
+ }, [columns, globallyFilteredRows, columnFilters, columnById])
361
+
362
+ /* Sorting */
363
+ const sortedRows = React.useMemo(() => {
364
+ if (sorting.length === 0) return filteredRows
365
+ const rows = [...filteredRows]
366
+ rows.sort((a, b) => {
367
+ for (const { id, desc } of sorting) {
368
+ const column = columnById.get(id)
369
+ if (!column) continue
370
+ const compare = column.columnDef.sortingFn
371
+ ? column.columnDef.sortingFn(a, b, id)
372
+ : defaultCompare(a.getValue(id), b.getValue(id))
373
+ if (compare !== 0) return desc ? -compare : compare
374
+ }
375
+ return a.index - b.index
376
+ })
377
+ return rows
378
+ }, [filteredRows, sorting, columnById])
379
+
380
+ /* Pagination */
381
+ const pageCount = Math.max(1, Math.ceil(sortedRows.length / pagination.pageSize))
382
+ const pageRows = React.useMemo(() => {
383
+ const start = pagination.pageIndex * pagination.pageSize
384
+ return sortedRows.slice(start, start + pagination.pageSize)
385
+ }, [sortedRows, pagination])
386
+
387
+ React.useEffect(() => {
388
+ if (!autoResetPageIndex) return
389
+ if (pagination.pageIndex > pageCount - 1) {
390
+ setPagination((prev) => ({ ...prev, pageIndex: Math.max(0, pageCount - 1) }))
391
+ }
392
+ }, [autoResetPageIndex, pageCount, pagination.pageIndex, setPagination])
393
+
394
+ /* Table */
395
+ const table = React.useMemo<Table<TData>>(() => {
396
+ const t: Table<TData> = {
397
+ options,
398
+ getState: () => state,
399
+ getAllColumns: () => columns,
400
+ getVisibleColumns: () => columns.filter((c) => c.getIsVisible()),
401
+ getColumn: (id) => columnById.get(id),
402
+ getCoreRowModel: () => ({ rows: coreRows }),
403
+ getFilteredRowModel: () => ({ rows: filteredRows }),
404
+ getSortedRowModel: () => ({ rows: sortedRows }),
405
+ getRowModel: () => ({ rows: pageRows }),
406
+ getFilteredSelectedRowModel: () => ({
407
+ rows: filteredRows.filter((r) => r.getIsSelected()),
408
+ }),
409
+ getSelectedRowModel: () => ({ rows: coreRows.filter((r) => r.getIsSelected()) }),
410
+ setSorting,
411
+ resetSorting: () => setSorting([]),
412
+ setGlobalFilter: (value) => {
413
+ setGlobalFilterState(value)
414
+ if (autoResetPageIndex) setPagination((p) => ({ ...p, pageIndex: 0 }))
415
+ },
416
+ setColumnFilters: (updater) => {
417
+ setColumnFilters(updater)
418
+ if (autoResetPageIndex) setPagination((p) => ({ ...p, pageIndex: 0 }))
419
+ },
420
+ resetColumnFilters: () => setColumnFilters([]),
421
+ setPagination,
422
+ setPageIndex: (index) => setPagination((p) => ({ ...p, pageIndex: Math.min(Math.max(0, index), pageCount - 1) })),
423
+ setPageSize: (size) =>
424
+ setPagination((p) => ({ pageSize: size, pageIndex: Math.floor((p.pageIndex * p.pageSize) / size) })),
425
+ getPageCount: () => pageCount,
426
+ getCanPreviousPage: () => pagination.pageIndex > 0,
427
+ getCanNextPage: () => pagination.pageIndex < pageCount - 1,
428
+ previousPage: () => t.setPageIndex(pagination.pageIndex - 1),
429
+ nextPage: () => t.setPageIndex(pagination.pageIndex + 1),
430
+ setRowSelection,
431
+ resetRowSelection: () => setRowSelection({}),
432
+ getIsAllPageRowsSelected: () =>
433
+ pageRows.length > 0 && pageRows.every((r) => !r.getCanSelect() || r.getIsSelected()),
434
+ getIsSomePageRowsSelected: () => pageRows.some((r) => r.getIsSelected()) && !t.getIsAllPageRowsSelected(),
435
+ toggleAllPageRowsSelected: (selected) => {
436
+ const value = selected ?? !t.getIsAllPageRowsSelected()
437
+ setRowSelection((prev) => {
438
+ const next = { ...prev }
439
+ for (const row of pageRows) {
440
+ if (!row.getCanSelect()) continue
441
+ if (value) next[row.id] = true
442
+ else delete next[row.id]
443
+ }
444
+ return next
445
+ })
446
+ },
447
+ getIsAllRowsSelected: () =>
448
+ filteredRows.length > 0 && filteredRows.every((r) => !r.getCanSelect() || r.getIsSelected()),
449
+ toggleAllRowsSelected: (selected) => {
450
+ const value = selected ?? !t.getIsAllRowsSelected()
451
+ setRowSelection(() => {
452
+ const next: RowSelectionState = {}
453
+ if (value) for (const row of filteredRows) if (row.getCanSelect()) next[row.id] = true
454
+ return next
455
+ })
456
+ },
457
+ setColumnVisibility,
458
+ resetColumnVisibility: () => setColumnVisibility({}),
459
+ }
460
+ return t
461
+ }, [
462
+ options,
463
+ state,
464
+ columns,
465
+ columnById,
466
+ coreRows,
467
+ filteredRows,
468
+ sortedRows,
469
+ pageRows,
470
+ pageCount,
471
+ pagination,
472
+ autoResetPageIndex,
473
+ setSorting,
474
+ setGlobalFilterState,
475
+ setColumnFilters,
476
+ setPagination,
477
+ setRowSelection,
478
+ setColumnVisibility,
479
+ ])
480
+
481
+ tableRef.current = table
482
+ return table
483
+ }
@@ -0,0 +1,37 @@
1
+ const units: [Intl.RelativeTimeFormatUnit, number][] = [
2
+ ["year", 365 * 86_400_000],
3
+ ["month", 30 * 86_400_000],
4
+ ["week", 7 * 86_400_000],
5
+ ["day", 86_400_000],
6
+ ["hour", 3_600_000],
7
+ ["minute", 60_000],
8
+ ]
9
+
10
+ /** "3 minutes ago", "yesterday", "in 2 weeks"; "just now" under a minute. */
11
+ export function formatRelativeTime(date: Date, options: { now?: Date; locale?: string } = {}) {
12
+ const now = options.now ?? new Date()
13
+ const diff = date.getTime() - now.getTime()
14
+ if (Math.abs(diff) < 60_000) return "just now"
15
+ const rtf = new Intl.RelativeTimeFormat(options.locale, { numeric: "auto" })
16
+ for (const [unit, ms] of units) {
17
+ if (Math.abs(diff) >= ms) return rtf.format(Math.round(diff / ms), unit)
18
+ }
19
+ return "just now"
20
+ }
21
+
22
+ /** Compact past-only form for dense lists: "now", "5m", "3h", "2d", "4w", "3mo", "1y". */
23
+ export function formatCompactAge(date: Date, now = new Date()) {
24
+ const s = Math.max(0, Math.round((now.getTime() - date.getTime()) / 1000))
25
+ if (s < 60) return "now"
26
+ const m = Math.round(s / 60)
27
+ if (m < 60) return `${m}m`
28
+ const h = Math.round(m / 60)
29
+ if (h < 24) return `${h}h`
30
+ const d = Math.round(h / 24)
31
+ if (d < 7) return `${d}d`
32
+ const w = Math.round(d / 7)
33
+ if (w < 5) return `${w}w`
34
+ const mo = Math.round(d / 30)
35
+ if (mo < 12) return `${mo}mo`
36
+ return `${Math.round(d / 365)}y`
37
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }