@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,16 @@
1
+ {
2
+ "name": "table",
3
+ "type": "lib",
4
+ "description": "A small headless table engine: column definitions, sorting, global and per-column filtering with faceted counts, pagination, row selection, and column visibility. No external dependencies.",
5
+ "dependencies": {},
6
+ "registryDependencies": [
7
+ "controllable"
8
+ ],
9
+ "files": [
10
+ {
11
+ "path": "lib/table.ts",
12
+ "content": "import * as React from \"react\"\nimport { useControllable } from \"@/lib/controllable\"\n\n/**\n * A small headless table engine: column definitions, sorting, global and\n * per-column filtering with faceted counts, pagination, row selection, and\n * column visibility. No external dependencies.\n */\n\n/* ------------------------------- Definitions ------------------------------- */\n\nexport type SortingState = { id: string; desc: boolean }[]\nexport type ColumnFiltersState = { id: string; value: unknown }[]\nexport type PaginationState = { pageIndex: number; pageSize: number }\nexport type RowSelectionState = Record<string, boolean>\nexport type VisibilityState = Record<string, boolean>\n\nexport type TableState = {\n sorting: SortingState\n columnFilters: ColumnFiltersState\n globalFilter: string\n pagination: PaginationState\n rowSelection: RowSelectionState\n columnVisibility: VisibilityState\n}\n\nexport type Updater<T> = T | ((previous: T) => T)\nexport type OnChange<T> = (updater: Updater<T>) => void\n\nexport type FilterFn<TData> = (row: Row<TData>, columnId: string, filterValue: unknown) => boolean\n\nexport type SortingFn<TData> = (a: Row<TData>, b: Row<TData>, columnId: string) => number\n\nexport type ColumnDef<TData, TValue = unknown> = {\n /** Required when no `accessorKey` is given. */\n id?: string\n /** Read the value from this property of the row. */\n accessorKey?: keyof TData & string\n /** Compute the value from the row. */\n accessorFn?: (row: TData, index: number) => TValue\n /** Human label (view options, headers); falls back to the column id. */\n label?: string\n enableSorting?: boolean\n enableHiding?: boolean\n enableGlobalFilter?: boolean\n filterFn?: FilterFn<TData>\n sortingFn?: SortingFn<TData>\n}\n\n/* ---------------------------------- Runtime -------------------------------- */\n\nexport interface Column<TData, TValue = unknown> {\n id: string\n label?: string\n columnDef: ColumnDef<TData, TValue>\n accessorFn?: (row: TData, index: number) => TValue\n getCanSort: () => boolean\n getIsSorted: () => false | \"asc\" | \"desc\"\n toggleSorting: (desc?: boolean) => void\n clearSorting: () => void\n getCanHide: () => boolean\n getIsVisible: () => boolean\n toggleVisibility: (visible?: boolean) => void\n getCanFilter: () => boolean\n getFilterValue: () => unknown\n setFilterValue: (value: unknown) => void\n getFacetedUniqueValues: () => Map<unknown, number>\n}\n\nexport interface Row<TData> {\n id: string\n index: number\n original: TData\n getValue: <TValue = unknown>(columnId: string) => TValue\n getIsSelected: () => boolean\n getCanSelect: () => boolean\n toggleSelected: (selected?: boolean) => void\n}\n\nexport interface RowModel<TData> {\n rows: Row<TData>[]\n}\n\nexport interface Table<TData> {\n options: TableOptions<TData>\n getState: () => TableState\n getAllColumns: () => Column<TData>[]\n getVisibleColumns: () => Column<TData>[]\n getColumn: (id: string) => Column<TData> | undefined\n /** All rows before filtering. */\n getCoreRowModel: () => RowModel<TData>\n /** Rows after global and column filters. */\n getFilteredRowModel: () => RowModel<TData>\n /** Filtered and sorted rows. */\n getSortedRowModel: () => RowModel<TData>\n /** The current page of sorted, filtered rows. */\n getRowModel: () => RowModel<TData>\n getFilteredSelectedRowModel: () => RowModel<TData>\n getSelectedRowModel: () => RowModel<TData>\n // sorting\n setSorting: OnChange<SortingState>\n resetSorting: () => void\n // filtering\n setGlobalFilter: (value: string) => void\n setColumnFilters: OnChange<ColumnFiltersState>\n resetColumnFilters: () => void\n // pagination\n setPagination: OnChange<PaginationState>\n setPageIndex: (index: number) => void\n setPageSize: (size: number) => void\n getPageCount: () => number\n getCanPreviousPage: () => boolean\n getCanNextPage: () => boolean\n previousPage: () => void\n nextPage: () => void\n // selection\n setRowSelection: OnChange<RowSelectionState>\n resetRowSelection: () => void\n getIsAllPageRowsSelected: () => boolean\n getIsSomePageRowsSelected: () => boolean\n toggleAllPageRowsSelected: (selected?: boolean) => void\n getIsAllRowsSelected: () => boolean\n toggleAllRowsSelected: (selected?: boolean) => void\n // visibility\n setColumnVisibility: OnChange<VisibilityState>\n resetColumnVisibility: () => void\n}\n\nexport type TableOptions<TData> = {\n data: TData[]\n columns: ColumnDef<TData, any>[] // eslint-disable-line @typescript-eslint/no-explicit-any\n getRowId?: (row: TData, index: number) => string\n enableRowSelection?: boolean | ((row: Row<TData>) => boolean)\n /** Runs against every row when `globalFilter` is set. Defaults to a case-insensitive `includes` over globally filterable columns. */\n globalFilterFn?: FilterFn<TData>\n /** Uncontrolled initial values. */\n initialState?: Partial<TableState>\n /** Controlled values; pair each with its `on*Change` handler. */\n state?: Partial<TableState>\n onSortingChange?: OnChange<SortingState>\n onColumnFiltersChange?: OnChange<ColumnFiltersState>\n onGlobalFilterChange?: OnChange<string>\n onPaginationChange?: OnChange<PaginationState>\n onRowSelectionChange?: OnChange<RowSelectionState>\n onColumnVisibilityChange?: OnChange<VisibilityState>\n /** Keep the page index inside the page count as filters change. Defaults to true. */\n autoResetPageIndex?: boolean\n}\n\n/* ----------------------------------- Helpers -------------------------------- */\n\nfunction defaultCompare(a: unknown, b: unknown) {\n if (a == null && b == null) return 0\n if (a == null) return 1\n if (b == null) return -1\n if (typeof a === \"number\" && typeof b === \"number\") return a - b\n if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()\n return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: \"base\" })\n}\n\nfunction defaultColumnFilter<TData>(row: Row<TData>, columnId: string, filterValue: unknown) {\n const value = row.getValue(columnId)\n if (Array.isArray(filterValue)) return filterValue.includes(value)\n if (filterValue == null || filterValue === \"\") return true\n return String(value ?? \"\")\n .toLowerCase()\n .includes(String(filterValue).toLowerCase())\n}\n\n/* ------------------------------------ Hook ---------------------------------- */\n\nexport function useTable<TData>(options: TableOptions<TData>): Table<TData> {\n const {\n data,\n columns: columnDefs,\n getRowId,\n enableRowSelection = true,\n globalFilterFn,\n initialState,\n state: controlled,\n autoResetPageIndex = true,\n } = options\n\n const [sorting, setSorting] = useControllable(\n controlled?.sorting,\n initialState?.sorting ?? [],\n options.onSortingChange\n )\n const [columnFilters, setColumnFilters] = useControllable(\n controlled?.columnFilters,\n initialState?.columnFilters ?? [],\n options.onColumnFiltersChange\n )\n const [globalFilter, setGlobalFilterState] = useControllable(\n controlled?.globalFilter,\n initialState?.globalFilter ?? \"\",\n options.onGlobalFilterChange\n )\n const [pagination, setPagination] = useControllable(\n controlled?.pagination,\n initialState?.pagination ?? { pageIndex: 0, pageSize: 10 },\n options.onPaginationChange\n )\n const [rowSelection, setRowSelection] = useControllable(\n controlled?.rowSelection,\n initialState?.rowSelection ?? {},\n options.onRowSelectionChange\n )\n const [columnVisibility, setColumnVisibility] = useControllable(\n controlled?.columnVisibility,\n initialState?.columnVisibility ?? {},\n options.onColumnVisibilityChange\n )\n\n const state: TableState = React.useMemo(\n () => ({ sorting, columnFilters, globalFilter, pagination, rowSelection, columnVisibility }),\n [sorting, columnFilters, globalFilter, pagination, rowSelection, columnVisibility]\n )\n\n // Keep a mutable reference so column and row methods always see fresh state\n // without rebuilding every object on each render.\n const tableRef = React.useRef<Table<TData>>(null!)\n\n /* Columns */\n const columns = React.useMemo<Column<TData>[]>(() => {\n return columnDefs.map((def) => {\n const id = def.id ?? def.accessorKey\n if (!id) throw new Error(\"Every column needs an `id` or an `accessorKey`.\")\n const accessorFn =\n def.accessorFn ??\n (def.accessorKey ? (row: TData) => (row as Record<string, unknown>)[def.accessorKey as string] : undefined)\n const column: Column<TData> = {\n id,\n columnDef: def,\n\n label: def.label,\n accessorFn: accessorFn as Column<TData>[\"accessorFn\"],\n getCanSort: () => (def.enableSorting ?? true) && !!accessorFn,\n getIsSorted: () => {\n const entry = tableRef.current.getState().sorting.find((s) => s.id === id)\n return entry ? (entry.desc ? \"desc\" : \"asc\") : false\n },\n toggleSorting: (desc) => {\n tableRef.current.setSorting((prev) => {\n const current = prev.find((s) => s.id === id)\n const nextDesc = desc ?? (current ? !current.desc : false)\n return [{ id, desc: nextDesc }]\n })\n },\n clearSorting: () => tableRef.current.setSorting((prev) => prev.filter((s) => s.id !== id)),\n getCanHide: () => def.enableHiding ?? true,\n getIsVisible: () => tableRef.current.getState().columnVisibility[id] !== false,\n toggleVisibility: (visible) => {\n tableRef.current.setColumnVisibility((prev) => ({\n ...prev,\n [id]: visible ?? prev[id] === false,\n }))\n },\n getCanFilter: () => !!accessorFn,\n getFilterValue: () => tableRef.current.getState().columnFilters.find((f) => f.id === id)?.value,\n setFilterValue: (value) => {\n tableRef.current.setColumnFilters((prev) => {\n const rest = prev.filter((f) => f.id !== id)\n const isEmpty =\n value === undefined || value === null || value === \"\" || (Array.isArray(value) && value.length === 0)\n return isEmpty ? rest : [...rest, { id, value }]\n })\n },\n getFacetedUniqueValues: () => facetsRef.current.get(id) ?? new Map(),\n }\n return column\n })\n }, [columnDefs])\n\n const columnById = React.useMemo(() => new Map(columns.map((c) => [c.id, c])), [columns])\n\n /* Rows */\n const coreRows = React.useMemo<Row<TData>[]>(() => {\n return data.map((original, index) => {\n const id = getRowId ? getRowId(original, index) : String(index)\n const valueCache = new Map<string, unknown>()\n const row: Row<TData> = {\n id,\n index,\n original,\n getValue: <TValue>(columnId: string) => {\n if (valueCache.has(columnId)) return valueCache.get(columnId) as TValue\n const column = columnById.get(columnId)\n const value = column?.accessorFn ? column.accessorFn(original, index) : undefined\n valueCache.set(columnId, value)\n return value as TValue\n },\n getIsSelected: () => !!tableRef.current.getState().rowSelection[id],\n getCanSelect: () => (typeof enableRowSelection === \"function\" ? enableRowSelection(row) : enableRowSelection),\n toggleSelected: (selected) => {\n if (!row.getCanSelect()) return\n tableRef.current.setRowSelection((prev) => {\n const next = { ...prev }\n const value = selected ?? !prev[id]\n if (value) next[id] = true\n else delete next[id]\n return next\n })\n },\n }\n return row\n })\n }, [data, getRowId, columnById, enableRowSelection])\n\n /* Filtering */\n const globallyFilteredRows = React.useMemo(() => {\n if (!globalFilter) return coreRows\n const fn: FilterFn<TData> =\n globalFilterFn ??\n ((row, _id, value) => {\n const search = String(value).toLowerCase()\n return columns.some((column) => {\n if (!column.accessorFn || column.columnDef.enableGlobalFilter === false) return false\n return String(row.getValue(column.id) ?? \"\")\n .toLowerCase()\n .includes(search)\n })\n })\n return coreRows.filter((row) => fn(row, \"__global__\", globalFilter))\n }, [coreRows, globalFilter, globalFilterFn, columns])\n\n const filteredRows = React.useMemo(() => {\n let rows = globallyFilteredRows\n for (const filter of columnFilters) {\n const column = columnById.get(filter.id)\n if (!column) continue\n const fn = column.columnDef.filterFn ?? defaultColumnFilter\n rows = rows.filter((row) => fn(row, filter.id, filter.value))\n }\n return rows\n }, [globallyFilteredRows, columnFilters, columnById])\n\n /* Facets: unique value counts per column, over rows filtered by everything except that column. */\n const facetsRef = React.useRef(new Map<string, Map<unknown, number>>())\n facetsRef.current = React.useMemo(() => {\n const result = new Map<string, Map<unknown, number>>()\n for (const column of columns) {\n if (!column.accessorFn) continue\n let rows = globallyFilteredRows\n for (const filter of columnFilters) {\n if (filter.id === column.id) continue\n const other = columnById.get(filter.id)\n if (!other) continue\n const fn = other.columnDef.filterFn ?? defaultColumnFilter\n rows = rows.filter((row) => fn(row, filter.id, filter.value))\n }\n const counts = new Map<unknown, number>()\n for (const row of rows) {\n const value = row.getValue(column.id)\n counts.set(value, (counts.get(value) ?? 0) + 1)\n }\n result.set(column.id, counts)\n }\n return result\n }, [columns, globallyFilteredRows, columnFilters, columnById])\n\n /* Sorting */\n const sortedRows = React.useMemo(() => {\n if (sorting.length === 0) return filteredRows\n const rows = [...filteredRows]\n rows.sort((a, b) => {\n for (const { id, desc } of sorting) {\n const column = columnById.get(id)\n if (!column) continue\n const compare = column.columnDef.sortingFn\n ? column.columnDef.sortingFn(a, b, id)\n : defaultCompare(a.getValue(id), b.getValue(id))\n if (compare !== 0) return desc ? -compare : compare\n }\n return a.index - b.index\n })\n return rows\n }, [filteredRows, sorting, columnById])\n\n /* Pagination */\n const pageCount = Math.max(1, Math.ceil(sortedRows.length / pagination.pageSize))\n const pageRows = React.useMemo(() => {\n const start = pagination.pageIndex * pagination.pageSize\n return sortedRows.slice(start, start + pagination.pageSize)\n }, [sortedRows, pagination])\n\n React.useEffect(() => {\n if (!autoResetPageIndex) return\n if (pagination.pageIndex > pageCount - 1) {\n setPagination((prev) => ({ ...prev, pageIndex: Math.max(0, pageCount - 1) }))\n }\n }, [autoResetPageIndex, pageCount, pagination.pageIndex, setPagination])\n\n /* Table */\n const table = React.useMemo<Table<TData>>(() => {\n const t: Table<TData> = {\n options,\n getState: () => state,\n getAllColumns: () => columns,\n getVisibleColumns: () => columns.filter((c) => c.getIsVisible()),\n getColumn: (id) => columnById.get(id),\n getCoreRowModel: () => ({ rows: coreRows }),\n getFilteredRowModel: () => ({ rows: filteredRows }),\n getSortedRowModel: () => ({ rows: sortedRows }),\n getRowModel: () => ({ rows: pageRows }),\n getFilteredSelectedRowModel: () => ({\n rows: filteredRows.filter((r) => r.getIsSelected()),\n }),\n getSelectedRowModel: () => ({ rows: coreRows.filter((r) => r.getIsSelected()) }),\n setSorting,\n resetSorting: () => setSorting([]),\n setGlobalFilter: (value) => {\n setGlobalFilterState(value)\n if (autoResetPageIndex) setPagination((p) => ({ ...p, pageIndex: 0 }))\n },\n setColumnFilters: (updater) => {\n setColumnFilters(updater)\n if (autoResetPageIndex) setPagination((p) => ({ ...p, pageIndex: 0 }))\n },\n resetColumnFilters: () => setColumnFilters([]),\n setPagination,\n setPageIndex: (index) => setPagination((p) => ({ ...p, pageIndex: Math.min(Math.max(0, index), pageCount - 1) })),\n setPageSize: (size) =>\n setPagination((p) => ({ pageSize: size, pageIndex: Math.floor((p.pageIndex * p.pageSize) / size) })),\n getPageCount: () => pageCount,\n getCanPreviousPage: () => pagination.pageIndex > 0,\n getCanNextPage: () => pagination.pageIndex < pageCount - 1,\n previousPage: () => t.setPageIndex(pagination.pageIndex - 1),\n nextPage: () => t.setPageIndex(pagination.pageIndex + 1),\n setRowSelection,\n resetRowSelection: () => setRowSelection({}),\n getIsAllPageRowsSelected: () =>\n pageRows.length > 0 && pageRows.every((r) => !r.getCanSelect() || r.getIsSelected()),\n getIsSomePageRowsSelected: () => pageRows.some((r) => r.getIsSelected()) && !t.getIsAllPageRowsSelected(),\n toggleAllPageRowsSelected: (selected) => {\n const value = selected ?? !t.getIsAllPageRowsSelected()\n setRowSelection((prev) => {\n const next = { ...prev }\n for (const row of pageRows) {\n if (!row.getCanSelect()) continue\n if (value) next[row.id] = true\n else delete next[row.id]\n }\n return next\n })\n },\n getIsAllRowsSelected: () =>\n filteredRows.length > 0 && filteredRows.every((r) => !r.getCanSelect() || r.getIsSelected()),\n toggleAllRowsSelected: (selected) => {\n const value = selected ?? !t.getIsAllRowsSelected()\n setRowSelection(() => {\n const next: RowSelectionState = {}\n if (value) for (const row of filteredRows) if (row.getCanSelect()) next[row.id] = true\n return next\n })\n },\n setColumnVisibility,\n resetColumnVisibility: () => setColumnVisibility({}),\n }\n return t\n }, [\n options,\n state,\n columns,\n columnById,\n coreRows,\n filteredRows,\n sortedRows,\n pageRows,\n pageCount,\n pagination,\n autoResetPageIndex,\n setSorting,\n setGlobalFilterState,\n setColumnFilters,\n setPagination,\n setRowSelection,\n setColumnVisibility,\n ])\n\n tableRef.current = table\n return table\n}\n"
13
+ }
14
+ ],
15
+ "docs": "## Data table\n`src/lib/table.ts` is a dependency-free headless table engine (columns, sorting, global +\ncolumn filters with faceted counts, pagination, row selection, visibility). Column config is\ndata only: `{ id | accessorKey | accessorFn, label, enableSorting, enableHiding, filterFn,\nsortingFn }`. No render functions and no markup in column definitions.\n`src/components/ui/data-table.tsx` builds compositional parts on the source-agnostic\n`DataTableInstance` contract in `src/lib/data-table-adapter.ts`: toolbar, search, faceted\nfilter, reset, view options, `DataTableTable` > `DataTableHeader` (one `DataTableHead\ncolumn=\"id\"` per column; sortable columns get the sort/hide menu with `children` as the title)\n+ `DataTableBody` (`children` is `(row) => cells`; wrap in `DataTableRow` or return a fragment\nof `DataTableCell column=\"id\"` cells), `DataTableEmpty`, select-all/select-row cells, row\nactions, pagination pieces, bulk actions. Heads and cells keyed by a hidden column render\nnothing, so visibility toggles need no extra wiring. The toolkit must not know about specific\ntable libraries (no TanStack). `src/demo/tasks/` and `src/demo/users/` are the reference\nconsumers: `columns.tsx` is pure config, `*-table.tsx` composes the markup."
16
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "tabs",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "class-variance-authority": "^0.7.1"
8
+ },
9
+ "registryDependencies": [
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/tabs.tsx",
15
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"@/lib/utils\"\nimport { Tabs as TabsPrimitive } from \"@ark-ui/react\"\n\nfunction Tabs({\n className,\n orientation = \"horizontal\",\n lazyMount = true,\n unmountOnExit = true,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n return (\n <TabsPrimitive.Root\n data-slot=\"tabs\"\n orientation={orientation}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n className={cn(\"group/tabs flex gap-2 data-horizontal:flex-col\", className)}\n {...props}\n />\n )\n}\n\nfunction TabsContext({ ...props }: React.ComponentProps<typeof TabsPrimitive.Context>) {\n return <TabsPrimitive.Context {...props} />\n}\n\nconst tabsListVariants = cva(\n \"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none\",\n {\n variants: {\n variant: {\n default: \"bg-muted\",\n line: \"gap-1 bg-transparent\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction TabsList({\n className,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n data-variant={variant}\n className={cn(tabsListVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nfunction TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n return (\n <TabsPrimitive.Trigger\n data-slot=\"tabs-trigger\"\n className={cn(\n \"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-selected:shadow-sm group-data-[variant=line]/tabs-list:data-selected:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n \"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-selected:bg-transparent dark:group-data-[variant=line]/tabs-list:data-selected:border-transparent dark:group-data-[variant=line]/tabs-list:data-selected:bg-transparent\",\n \"data-selected:bg-background data-selected:text-foreground dark:data-selected:border-input dark:data-selected:bg-input/30 dark:data-selected:text-foreground\",\n \"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:-bottom-1.25 group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-selected:after:opacity-100\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TabsIndicator({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Indicator>) {\n return (\n <TabsPrimitive.Indicator\n data-slot=\"tabs-indicator\"\n className={cn(\n \"z-0 rounded-md bg-background shadow-sm transition-[left,top,width,height] duration-200\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return (\n <TabsPrimitive.Content\n data-slot=\"tabs-content\"\n className={cn(\"flex-1 text-sm outline-none\", className)}\n {...props}\n />\n )\n}\n\nexport { Tabs, TabsContent, TabsContext, TabsIndicator, TabsList, TabsTrigger, tabsListVariants }\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "tags-input",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/tags-input.tsx",
15
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { TagsInput as TagsInputPrimitive } from \"@ark-ui/react\"\nimport { XIcon } from \"lucide-react\"\n\nfunction TagsInput({ className, ...props }: React.ComponentProps<typeof TagsInputPrimitive.Root>) {\n return (\n <TagsInputPrimitive.Root\n data-slot=\"tags-input\"\n className={cn(\"flex w-full flex-col gap-1.5\", className)}\n {...props}\n />\n )\n}\n\nfunction TagsInputContext({ ...props }: React.ComponentProps<typeof TagsInputPrimitive.Context>) {\n return <TagsInputPrimitive.Context {...props} />\n}\n\nfunction TagsInputLabel({ className, ...props }: React.ComponentProps<typeof TagsInputPrimitive.Label>) {\n return (\n <TagsInputPrimitive.Label\n data-slot=\"tags-input-label\"\n className={cn(\"text-sm font-medium select-none\", className)}\n {...props}\n />\n )\n}\n\nfunction TagsInputControl({ className, ...props }: React.ComponentProps<typeof TagsInputPrimitive.Control>) {\n return (\n <TagsInputPrimitive.Control\n data-slot=\"tags-input-control\"\n className={cn(\n \"flex min-h-8 w-full flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent px-1.5 py-1 text-sm transition-colors has-focus-visible:border-ring has-focus-visible:ring-3 has-focus-visible:ring-ring/50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 dark:bg-input/30 data-disabled:pointer-events-none data-disabled:opacity-50\",\n className\n )}\n {...props}\n />\n )\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction TagsInputInput({ className, ...props }: Omit<React.ComponentProps<typeof TagsInputPrimitive.Input>, \"id\">) {\n return (\n <TagsInputPrimitive.Input\n data-slot=\"tags-input-input\"\n className={cn(\n \"min-w-16 flex-1 bg-transparent px-1 text-sm outline-none placeholder:text-muted-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TagsInputItem({ className, ...props }: React.ComponentProps<typeof TagsInputPrimitive.Item>) {\n return <TagsInputPrimitive.Item data-slot=\"tags-input-item\" className={cn(\"inline-flex\", className)} {...props} />\n}\n\nfunction TagsInputItemContext({ ...props }: React.ComponentProps<typeof TagsInputPrimitive.ItemContext>) {\n return <TagsInputPrimitive.ItemContext {...props} />\n}\n\nfunction TagsInputItemPreview({ className, ...props }: React.ComponentProps<typeof TagsInputPrimitive.ItemPreview>) {\n return (\n <TagsInputPrimitive.ItemPreview\n data-slot=\"tags-input-item-preview\"\n className={cn(\n \"flex h-[calc(--spacing(5.25))] items-center gap-1 rounded-sm bg-muted pr-0 pl-1.5 text-xs font-medium whitespace-nowrap text-foreground data-highlighted:ring-2 data-highlighted:ring-ring/50 data-disabled:opacity-50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TagsInputItemText({ ...props }: React.ComponentProps<typeof TagsInputPrimitive.ItemText>) {\n return <TagsInputPrimitive.ItemText data-slot=\"tags-input-item-text\" {...props} />\n}\n\nfunction TagsInputItemDeleteTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof TagsInputPrimitive.ItemDeleteTrigger>) {\n return (\n <TagsInputPrimitive.ItemDeleteTrigger\n data-slot=\"tags-input-item-delete-trigger\"\n className={cn(\n \"inline-flex size-5 items-center justify-center rounded-sm opacity-50 hover:opacity-100 [&_svg]:size-3\",\n className\n )}\n {...props}\n >\n {children ?? <XIcon />}\n </TagsInputPrimitive.ItemDeleteTrigger>\n )\n}\n\nfunction TagsInputItemInput({ className, ...props }: React.ComponentProps<typeof TagsInputPrimitive.ItemInput>) {\n return (\n <TagsInputPrimitive.ItemInput\n data-slot=\"tags-input-item-input\"\n className={cn(\n \"h-[calc(--spacing(5.25))] rounded-sm bg-muted px-1.5 text-xs ring-2 ring-ring/50 outline-none\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TagsInputClearTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof TagsInputPrimitive.ClearTrigger>) {\n return (\n <TagsInputPrimitive.ClearTrigger\n data-slot=\"tags-input-clear-trigger\"\n className={cn(\n \"inline-flex size-6 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4\",\n className\n )}\n {...props}\n >\n {children ?? <XIcon />}\n </TagsInputPrimitive.ClearTrigger>\n )\n}\n\nfunction TagsInputHiddenInput({ ...props }: React.ComponentProps<typeof TagsInputPrimitive.HiddenInput>) {\n return <TagsInputPrimitive.HiddenInput {...props} />\n}\n\nexport {\n TagsInput,\n TagsInputClearTrigger,\n TagsInputContext,\n TagsInputControl,\n TagsInputHiddenInput,\n TagsInputInput,\n TagsInputItem,\n TagsInputItemContext,\n TagsInputItemDeleteTrigger,\n TagsInputItemInput,\n TagsInputItemPreview,\n TagsInputItemText,\n TagsInputLabel,\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "textarea",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {},
6
+ "registryDependencies": [
7
+ "utils"
8
+ ],
9
+ "files": [
10
+ {
11
+ "path": "components/ui/textarea.tsx",
12
+ "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n return (\n <textarea\n data-slot=\"textarea\"\n className={cn(\n \"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Textarea }\n"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "time",
3
+ "type": "lib",
4
+ "description": "`src/components/ui/activity-feed.tsx` is a compositional timeline of events on the WAI-ARIA feed pattern (`role=\"feed\"`, focusable articles, PageUp/PageDown move, Ctrl+Home/End jump): `ActivityFeed` > `ActivityFeedGroup` > `ActivityFeedGrou",
5
+ "dependencies": {},
6
+ "registryDependencies": [],
7
+ "files": [
8
+ {
9
+ "path": "lib/time.ts",
10
+ "content": "const units: [Intl.RelativeTimeFormatUnit, number][] = [\n [\"year\", 365 * 86_400_000],\n [\"month\", 30 * 86_400_000],\n [\"week\", 7 * 86_400_000],\n [\"day\", 86_400_000],\n [\"hour\", 3_600_000],\n [\"minute\", 60_000],\n]\n\n/** \"3 minutes ago\", \"yesterday\", \"in 2 weeks\"; \"just now\" under a minute. */\nexport function formatRelativeTime(date: Date, options: { now?: Date; locale?: string } = {}) {\n const now = options.now ?? new Date()\n const diff = date.getTime() - now.getTime()\n if (Math.abs(diff) < 60_000) return \"just now\"\n const rtf = new Intl.RelativeTimeFormat(options.locale, { numeric: \"auto\" })\n for (const [unit, ms] of units) {\n if (Math.abs(diff) >= ms) return rtf.format(Math.round(diff / ms), unit)\n }\n return \"just now\"\n}\n\n/** Compact past-only form for dense lists: \"now\", \"5m\", \"3h\", \"2d\", \"4w\", \"3mo\", \"1y\". */\nexport function formatCompactAge(date: Date, now = new Date()) {\n const s = Math.max(0, Math.round((now.getTime() - date.getTime()) / 1000))\n if (s < 60) return \"now\"\n const m = Math.round(s / 60)\n if (m < 60) return `${m}m`\n const h = Math.round(m / 60)\n if (h < 24) return `${h}h`\n const d = Math.round(h / 24)\n if (d < 7) return `${d}d`\n const w = Math.round(d / 7)\n if (w < 5) return `${w}w`\n const mo = Math.round(d / 30)\n if (mo < 12) return `${mo}mo`\n return `${Math.round(d / 365)}y`\n}\n"
11
+ }
12
+ ],
13
+ "docs": "## Activity feed and comments\n`src/components/ui/activity-feed.tsx` is a compositional timeline of events on the WAI-ARIA feed\npattern (`role=\"feed\"`, focusable articles, PageUp/PageDown move, Ctrl+Home/End jump):\n`ActivityFeed` > `ActivityFeedGroup` > `ActivityFeedGroupLabel` + `ActivityFeedItems` (draws the\nconnector line) > `ActivityFeedItem value=` > `ActivityFeedItemMarker` (dot by default; pass an\nicon or an Avatar) + `ActivityFeedItemContent` > `ActivityFeedItemHeader` > `ActivityFeedItemActor`,\n`ActivityFeedItemTime date=` (`<time>` with a relative label from `src/lib/time.ts`\n`formatRelativeTime`; full date in the title), `ActivityFeedItemBody`, `ActivityFeedItemActions`;\nplus `ActivityFeedLoadMoreTrigger` and `ActivityFeedEmpty`. Data, grouping and read state are the\nconsumer's.\n`src/components/ui/comment-thread.tsx` is compositional comments with replies, reactions and a\nmentions composer. `CommentThread { people, currentUserId, replyTo/onReplyToChange }` owns only\nwhich comment is being replied to or edited (`useCommentThread()` → `replyTo`, `editing`, setters).\nParts: `CommentList` > `Comment value= authorId=` (`data-own/replying/editing/depth`) >\n`CommentAvatar` (initials from `people`) + `CommentContent` > `CommentHeader` > `CommentAuthor`,\n`CommentTime date=`, `CommentMeta`; `CommentBody text=` (plain text with `@Full Name` mentions\nhighlighted as `CommentMention`, `data-self` for the current user); `CommentReactions` >\n`CommentReaction emoji= count= active=`; `CommentActions` (hover-revealed) > `CommentReplyTrigger`,\n`CommentEditTrigger`; `CommentReplies` (nested list with a guide line); `CommentEmpty`.\n`CommentComposer { people, value/onValueChange, onSubmit({ text, mentions }), onCancel }` >\n`CommentComposerReplyingTo` (render prop gets the comment id), `CommentComposerInput` (a\ncontenteditable, not a textarea: picked mentions become atomic grey chips\n`[data-slot=comment-mention-chip]` that one Backspace removes, while `value` stays plain\n`@Full Name` text and `defaultValue`/`value` text is hydrated back into chips;\n`@query` at the caret opens `CommentComposerMentionList` (a portaled Popover anchored to the input, focus stays in the textarea, so no `overflow-hidden` ancestor clips it and nothing shifts), arrows/Enter/Tab pick, Escape closes,\n⌘/Ctrl+Enter submits, `aria-activedescendant` wired), `CommentComposerFooter` >\n`CommentComposerHint`, `CommentComposerCancelTrigger`, `CommentComposerSubmitTrigger`. Mentions are\nplain `@Full Name` text; `extractMentions(text, people)` returns ids. The same composer edits in\nplace (`defaultValue`, Save). `src/demo/activity/` is the reference (`/activity`): grouped feed\nwith All/Mentions/Comments filter, unread dots, load more, and a ticket thread with nested\nreplies, reactions, edit/delete of own comments (Popconfirm), and posting that also prepends a\nfeed item."
14
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "timer",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1"
7
+ },
8
+ "registryDependencies": [
9
+ "button",
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/timer.tsx",
15
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Timer as TimerPrimitive } from \"@ark-ui/react\"\n\nimport { Button } from \"@/components/ui/button\"\n\nfunction Timer({ className, ...props }: React.ComponentProps<typeof TimerPrimitive.Root>) {\n return (\n <TimerPrimitive.Root data-slot=\"timer\" className={cn(\"flex flex-col items-center gap-3\", className)} {...props} />\n )\n}\n\nfunction TimerContext({ ...props }: React.ComponentProps<typeof TimerPrimitive.Context>) {\n return <TimerPrimitive.Context {...props} />\n}\n\nfunction TimerArea({ className, ...props }: React.ComponentProps<typeof TimerPrimitive.Area>) {\n return (\n <TimerPrimitive.Area\n data-slot=\"timer-area\"\n className={cn(\"flex items-center gap-1 font-mono text-2xl font-medium tabular-nums\", className)}\n {...props}\n />\n )\n}\n\nfunction TimerItem({ className, ...props }: React.ComponentProps<typeof TimerPrimitive.Item>) {\n return (\n <TimerPrimitive.Item\n data-slot=\"timer-item\"\n className={cn(\"flex min-w-10 items-center justify-center rounded-lg bg-muted px-2 py-1\", className)}\n {...props}\n />\n )\n}\n\nfunction TimerSeparator({ className, children, ...props }: React.ComponentProps<typeof TimerPrimitive.Separator>) {\n return (\n <TimerPrimitive.Separator data-slot=\"timer-separator\" className={cn(\"text-muted-foreground\", className)} {...props}>\n {children ?? \":\"}\n </TimerPrimitive.Separator>\n )\n}\n\nfunction TimerControl({ className, ...props }: React.ComponentProps<typeof TimerPrimitive.Control>) {\n return (\n <TimerPrimitive.Control data-slot=\"timer-control\" className={cn(\"flex items-center gap-2\", className)} {...props} />\n )\n}\n\nfunction TimerActionTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof TimerPrimitive.ActionTrigger>) {\n return (\n <TimerPrimitive.ActionTrigger data-slot=\"timer-action-trigger\" className={cn(className)} asChild {...props}>\n <Button variant=\"outline\" size=\"sm\">\n {children}\n </Button>\n </TimerPrimitive.ActionTrigger>\n )\n}\n\nexport { Timer, TimerActionTrigger, TimerArea, TimerContext, TimerControl, TimerItem, TimerSeparator }\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "toc",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1"
7
+ },
8
+ "registryDependencies": [
9
+ "utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/toc.tsx",
14
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Toc as TocPrimitive } from \"@ark-ui/react\"\n\nfunction Toc({ className, ...props }: React.ComponentProps<typeof TocPrimitive.Root>) {\n return <TocPrimitive.Root data-slot=\"toc\" className={cn(\"w-full text-sm\", className)} {...props} />\n}\n\nfunction TocContext({ ...props }: React.ComponentProps<typeof TocPrimitive.Context>) {\n return <TocPrimitive.Context {...props} />\n}\n\nfunction TocNav({ className, ...props }: React.ComponentProps<typeof TocPrimitive.Nav>) {\n return <TocPrimitive.Nav data-slot=\"toc-nav\" className={cn(\"flex flex-col gap-2\", className)} {...props} />\n}\n\nfunction TocTitle({ className, ...props }: React.ComponentProps<typeof TocPrimitive.Title>) {\n return (\n <TocPrimitive.Title\n data-slot=\"toc-title\"\n className={cn(\"text-xs font-medium text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction TocContent({ className, ...props }: React.ComponentProps<typeof TocPrimitive.Content>) {\n return <TocPrimitive.Content data-slot=\"toc-content\" className={cn(\"relative\", className)} {...props} />\n}\n\nfunction TocList({ className, ...props }: React.ComponentProps<typeof TocPrimitive.List>) {\n return <TocPrimitive.List data-slot=\"toc-list\" className={cn(\"flex flex-col border-l\", className)} {...props} />\n}\n\nfunction TocItem({ className, ...props }: React.ComponentProps<typeof TocPrimitive.Item>) {\n return <TocPrimitive.Item data-slot=\"toc-item\" className={cn(\"flex\", className)} {...props} />\n}\n\nfunction TocLink({ className, ...props }: React.ComponentProps<typeof TocPrimitive.Link>) {\n return (\n <TocPrimitive.Link\n data-slot=\"toc-link\"\n className={cn(\n \"-ml-px flex-1 border-l border-transparent py-1 pl-[calc(var(--depth,1)*(--spacing(3)))] text-muted-foreground transition-colors hover:text-foreground data-active:border-foreground data-active:text-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TocIndicator({ className, ...props }: React.ComponentProps<typeof TocPrimitive.Indicator>) {\n return (\n <TocPrimitive.Indicator\n data-slot=\"toc-indicator\"\n className={cn(\"w-px bg-foreground transition-all\", className)}\n {...props}\n />\n )\n}\n\nexport { Toc, TocContent, TocContext, TocIndicator, TocItem, TocLink, TocList, TocNav, TocTitle }\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "toggle-group",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "class-variance-authority": "^0.7.1"
8
+ },
9
+ "registryDependencies": [
10
+ "toggle",
11
+ "utils"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/toggle-group.tsx",
16
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"@/lib/utils\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@ark-ui/react\"\n\nimport { toggleVariants } from \"@/components/ui/toggle\"\n\nconst ToggleGroupVariantContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupVariantContext.Provider value={{ variant, size, spacing, orientation }}>\n {children}\n </ToggleGroupVariantContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupContext({ ...props }: React.ComponentProps<typeof ToggleGroupPrimitive.Context>) {\n return <ToggleGroupPrimitive.Context {...props} />\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupVariantContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupContext, ToggleGroupItem }\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "toggle",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "class-variance-authority": "^0.7.1"
8
+ },
9
+ "registryDependencies": [
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/toggle.tsx",
15
+ "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"@/lib/utils\"\nimport { Toggle as TogglePrimitive } from \"@ark-ui/react\"\n\nconst toggleVariants = cva(\n \"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n outline: \"border border-input bg-transparent hover:bg-muted\",\n },\n size: {\n default: \"h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n sm: \"h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5\",\n lg: \"h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nfunction Toggle({\n className,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>) {\n return (\n <TogglePrimitive.Root data-slot=\"toggle\" className={cn(toggleVariants({ variant, size, className }))} {...props} />\n )\n}\n\nfunction ToggleIndicator({ ...props }: React.ComponentProps<typeof TogglePrimitive.Indicator>) {\n return <TogglePrimitive.Indicator data-slot=\"toggle-indicator\" {...props} />\n}\n\nfunction ToggleContext({ ...props }: React.ComponentProps<typeof TogglePrimitive.Context>) {\n return <TogglePrimitive.Context {...props} />\n}\n\nexport { Toggle, ToggleContext, ToggleIndicator, toggleVariants }\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "tooltip",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1"
7
+ },
8
+ "registryDependencies": [
9
+ "utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/tooltip.tsx",
14
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Portal as PortalPrimitive, Tooltip as TooltipPrimitive } from \"@ark-ui/react\"\n\ntype TooltipProviderProps = {\n /** Delay before the tooltip opens, in ms. Maps to Ark's `openDelay`. */\n delayDuration?: number\n /** Delay before the tooltip closes, in ms. Maps to Ark's `closeDelay`. */\n closeDelay?: number\n /** When true the content cannot be hovered. Maps to Ark's `interactive: false`. */\n disableHoverableContent?: boolean\n children?: React.ReactNode\n}\n\nconst TooltipProviderContext = React.createContext<Omit<TooltipProviderProps, \"children\">>({})\n\nfunction TooltipProvider({ delayDuration = 0, closeDelay, disableHoverableContent, children }: TooltipProviderProps) {\n const value = React.useMemo(\n () => ({ delayDuration, closeDelay, disableHoverableContent }),\n [delayDuration, closeDelay, disableHoverableContent]\n )\n return <TooltipProviderContext.Provider value={value}>{children}</TooltipProviderContext.Provider>\n}\n\nfunction Tooltip({\n openDelay,\n closeDelay,\n interactive,\n positioning,\n lazyMount = true,\n unmountOnExit = true,\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n const provider = React.useContext(TooltipProviderContext)\n return (\n <TooltipPrimitive.Root\n openDelay={openDelay ?? provider.delayDuration ?? 0}\n closeDelay={closeDelay ?? provider.closeDelay}\n interactive={interactive ?? !provider.disableHoverableContent}\n positioning={{ placement: \"top\", gutter: 0, ...positioning }}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n {...props}\n />\n )\n}\n\nfunction TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />\n}\n\nfunction TooltipPortal({ ...props }: React.ComponentProps<typeof PortalPrimitive>) {\n return <PortalPrimitive {...props} />\n}\n\nfunction TooltipContext({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Context>) {\n return <TooltipPrimitive.Context {...props} />\n}\n\nfunction TooltipPositioner({ className, ...props }: React.ComponentProps<typeof TooltipPrimitive.Positioner>) {\n return (\n <TooltipPrimitive.Positioner\n data-slot=\"tooltip-positioner\"\n className={cn(\"[--z-index:50]\", className)}\n {...props}\n />\n )\n}\n\nfunction TooltipArrow({ className, ...props }: React.ComponentProps<typeof TooltipPrimitive.Arrow>) {\n return (\n <TooltipPrimitive.Arrow\n data-slot=\"tooltip-arrow\"\n className={cn(\"z-50 [--arrow-background:var(--color-foreground)] [--arrow-size:0.625rem]\", className)}\n {...props}\n >\n <TooltipPrimitive.ArrowTip data-slot=\"tooltip-arrow-tip\" className=\"rounded-xs\" />\n </TooltipPrimitive.Arrow>\n )\n}\n\nfunction TooltipContent({ className, children, ...props }: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n return (\n <TooltipPortal>\n <TooltipPositioner>\n <TooltipPrimitive.Content\n data-slot=\"tooltip-content\"\n className={cn(\n \"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n className\n )}\n {...props}\n >\n {children}\n <TooltipArrow />\n </TooltipPrimitive.Content>\n </TooltipPositioner>\n </TooltipPortal>\n )\n}\n\nexport {\n Tooltip,\n TooltipArrow,\n TooltipContent,\n TooltipContext,\n TooltipPortal,\n TooltipPositioner,\n TooltipProvider,\n TooltipTrigger,\n}\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "tour",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "utils"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/tour.tsx",
16
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Tour as TourPrimitive, Portal as PortalPrimitive, useTour } from \"@ark-ui/react\"\nimport { XIcon } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\n\nfunction Tour({ lazyMount = true, unmountOnExit = true, ...props }: React.ComponentProps<typeof TourPrimitive.Root>) {\n return <TourPrimitive.Root lazyMount={lazyMount} unmountOnExit={unmountOnExit} {...props} />\n}\n\nfunction TourContext({ ...props }: React.ComponentProps<typeof TourPrimitive.Context>) {\n return <TourPrimitive.Context {...props} />\n}\n\nfunction TourPortal({ ...props }: React.ComponentProps<typeof PortalPrimitive>) {\n return <PortalPrimitive {...props} />\n}\n\nfunction TourBackdrop({ className, ...props }: React.ComponentProps<typeof TourPrimitive.Backdrop>) {\n return (\n <TourPrimitive.Backdrop\n data-slot=\"tour-backdrop\"\n className={cn(\n \"fixed inset-0 z-50 bg-black/40 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TourSpotlight({ className, ...props }: React.ComponentProps<typeof TourPrimitive.Spotlight>) {\n return (\n <TourPrimitive.Spotlight\n data-slot=\"tour-spotlight\"\n className={cn(\n \"z-50 rounded-md ring-4 ring-background/60 transition-[left,top,width,height] duration-300\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TourPositioner({ className, ...props }: React.ComponentProps<typeof TourPrimitive.Positioner>) {\n return <TourPrimitive.Positioner data-slot=\"tour-positioner\" className={cn(\"[--z-index:50]\", className)} {...props} />\n}\n\nfunction TourContent({ className, ...props }: React.ComponentProps<typeof TourPrimitive.Content>) {\n return (\n <TourPrimitive.Content\n data-slot=\"tour-content\"\n className={cn(\n \"z-50 flex w-80 flex-col gap-3 rounded-xl bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden data-[type=dialog]:fixed data-[type=dialog]:top-1/2 data-[type=dialog]:left-1/2 data-[type=dialog]:-translate-1/2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TourArrow({ className, ...props }: React.ComponentProps<typeof TourPrimitive.Arrow>) {\n return (\n <TourPrimitive.Arrow\n data-slot=\"tour-arrow\"\n className={cn(\"[--arrow-background:var(--color-popover)] [--arrow-size:0.625rem]\", className)}\n {...props}\n >\n <TourPrimitive.ArrowTip data-slot=\"tour-arrow-tip\" className=\"border-t border-l border-foreground/10\" />\n </TourPrimitive.Arrow>\n )\n}\n\nfunction TourTitle({ className, ...props }: React.ComponentProps<typeof TourPrimitive.Title>) {\n return (\n <TourPrimitive.Title\n data-slot=\"tour-title\"\n className={cn(\"font-heading text-base font-medium\", className)}\n {...props}\n />\n )\n}\n\nfunction TourDescription({ className, ...props }: React.ComponentProps<typeof TourPrimitive.Description>) {\n return (\n <TourPrimitive.Description\n data-slot=\"tour-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction TourControl({ className, ...props }: React.ComponentProps<typeof TourPrimitive.Control>) {\n return (\n <TourPrimitive.Control\n data-slot=\"tour-control\"\n className={cn(\"flex items-center justify-between gap-2\", className)}\n {...props}\n />\n )\n}\n\nfunction TourProgressText({ className, ...props }: React.ComponentProps<typeof TourPrimitive.ProgressText>) {\n return (\n <TourPrimitive.ProgressText\n data-slot=\"tour-progress-text\"\n className={cn(\"text-xs text-muted-foreground tabular-nums\", className)}\n {...props}\n />\n )\n}\n\nfunction TourActions({ ...props }: React.ComponentProps<typeof TourPrimitive.Actions>) {\n return <TourPrimitive.Actions {...props} />\n}\n\nfunction TourActionTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof TourPrimitive.ActionTrigger>) {\n return (\n <TourPrimitive.ActionTrigger data-slot=\"tour-action-trigger\" className={cn(className)} asChild {...props}>\n <Button variant=\"outline\" size=\"sm\">\n {children}\n </Button>\n </TourPrimitive.ActionTrigger>\n )\n}\n\nfunction TourCloseTrigger({ className, children, ...props }: React.ComponentProps<typeof TourPrimitive.CloseTrigger>) {\n return (\n <TourPrimitive.CloseTrigger\n data-slot=\"tour-close-trigger\"\n className={cn(\"absolute top-2 right-2\", className)}\n asChild\n {...props}\n >\n <Button variant=\"ghost\" size=\"icon-sm\">\n {children ?? <XIcon />}\n <span className=\"sr-only\">Close</span>\n </Button>\n </TourPrimitive.CloseTrigger>\n )\n}\n\nexport {\n Tour,\n TourActionTrigger,\n TourActions,\n TourArrow,\n TourBackdrop,\n TourCloseTrigger,\n TourContent,\n TourContext,\n TourControl,\n TourDescription,\n TourPortal,\n TourPositioner,\n TourProgressText,\n TourSpotlight,\n TourTitle,\n useTour,\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "transfer-list",
3
+ "type": "ui",
4
+ "description": "`src/components/ui/transfer-list.tsx` moves items between two lists.",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "checkbox",
12
+ "controllable",
13
+ "input-group",
14
+ "live-region",
15
+ "utils"
16
+ ],
17
+ "files": [
18
+ {
19
+ "path": "components/ui/transfer-list.tsx",
20
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ark } from \"@ark-ui/react\"\nimport {\n CheckIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n ChevronsLeftIcon,\n ChevronsRightIcon,\n SearchIcon,\n} from \"lucide-react\"\nimport { useControllable } from \"@/lib/controllable\"\nimport { cn } from \"@/lib/utils\"\nimport { LiveRegion, useLiveRegion } from \"@/components/ui/live-region\"\nimport { Button } from \"@/components/ui/button\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { InputGroup, InputGroupAddon, InputGroupInput } from \"@/components/ui/input-group\"\n\n/* -------------------------------- context -------------------------------- */\n\ntype TransferListSide = \"source\" | \"target\"\ntype TransferListDirection = \"right\" | \"left\"\n\ntype TransferListItemBase = { value: string; label: string; disabled?: boolean }\n\ntype TransferListValueChangeDetails<T> = {\n /** Values now on the target side, in catalog order. */\n value: string[]\n items: T[]\n /** What just moved and where. */\n moved: T[]\n direction: TransferListDirection\n}\n\ntype TransferListContextValue<T = TransferListItemBase> = {\n items: T[]\n itemToValue: (item: T) => string\n itemToString: (item: T) => string\n itemDisabled: (item: T) => boolean\n value: string[]\n /** Items on each side, in catalog order. */\n sides: Record<TransferListSide, T[]>\n /** Visible (searched) items on each side. */\n visible: Record<TransferListSide, T[]>\n query: Record<TransferListSide, string>\n setQuery: (side: TransferListSide, query: string) => void\n selected: Record<TransferListSide, string[]>\n toggle: (side: TransferListSide, value: string, force?: boolean) => void\n selectRange: (side: TransferListSide, from: string, to: string) => void\n selectAll: (side: TransferListSide, checked: boolean) => void\n /** Move the selected (or given) values across. */\n move: (direction: TransferListDirection, values?: string[]) => void\n moveAll: (direction: TransferListDirection) => void\n disabled: boolean\n announcement: string\n focused: Record<TransferListSide, string | null>\n setFocused: (side: TransferListSide, value: string | null) => void\n}\n\nconst TransferListContext = React.createContext<TransferListContextValue | null>(null)\nconst TransferListSideContext = React.createContext<TransferListSide | null>(null)\n\nfunction useTransferList<T = TransferListItemBase>() {\n const ctx = React.useContext(TransferListContext)\n if (!ctx) throw new Error(\"TransferList parts must be used inside <TransferList>\")\n return ctx as unknown as TransferListContextValue<T>\n}\n\nfunction useTransferListSide() {\n const side = React.useContext(TransferListSideContext)\n if (!side) throw new Error(\"This part must be used inside <TransferListPanel>\")\n return side\n}\n\nconst sideOf = (direction: TransferListDirection): TransferListSide => (direction === \"right\" ? \"source\" : \"target\")\n\n/* ---------------------------------- root --------------------------------- */\n\ntype TransferListProps<T> = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n items: T[]\n itemToValue?: (item: T) => string\n itemToString?: (item: T) => string\n itemDisabled?: (item: T) => boolean\n /** Values on the target side. Kept in catalog order. */\n value?: string[]\n defaultValue?: string[]\n onValueChange?: (details: TransferListValueChangeDetails<T>) => void\n /** Case-insensitive label match by default. */\n filter?: (item: T, query: string) => boolean\n disabled?: boolean\n /** Labels for the live region: \"Moved 3 items to {target}\". */\n titles?: Partial<Record<TransferListSide, string>>\n}\n\nfunction TransferList<T = TransferListItemBase>({\n items,\n itemToValue = (item) => (item as TransferListItemBase).value,\n itemToString = (item) => (item as TransferListItemBase).label,\n itemDisabled = (item) => !!(item as TransferListItemBase).disabled,\n value: valueProp,\n defaultValue = [],\n onValueChange,\n filter,\n disabled = false,\n titles,\n className,\n children,\n ...props\n}: TransferListProps<T>) {\n const order = React.useMemo(() => new Map(items.map((item, i) => [itemToValue(item), i])), [items, itemToValue])\n const sortValues = React.useCallback(\n (values: string[]) =>\n Array.from(new Set(values))\n .filter((v) => order.has(v))\n .sort((a, b) => order.get(a)! - order.get(b)!),\n [order]\n )\n const [value, setValueState] = useControllable<string[]>(valueProp, defaultValue)\n const [query, setQueryState] = React.useState<Record<TransferListSide, string>>({ source: \"\", target: \"\" })\n const [selected, setSelected] = React.useState<Record<TransferListSide, string[]>>({ source: [], target: [] })\n const [focused, setFocusedState] = React.useState<Record<TransferListSide, string | null>>({\n source: null,\n target: null,\n })\n const { message: announcement, announce: setAnnouncement } = useLiveRegion()\n\n const sides = React.useMemo(() => {\n const target = new Set(value)\n return {\n source: items.filter((item) => !target.has(itemToValue(item))),\n target: items.filter((item) => target.has(itemToValue(item))),\n }\n }, [items, value, itemToValue])\n\n const matches = React.useCallback(\n (item: T, q: string) =>\n filter ? filter(item, q) : itemToString(item).toLowerCase().includes(q.trim().toLowerCase()),\n [filter, itemToString]\n )\n const visible = React.useMemo(\n () => ({\n source: query.source.trim() ? sides.source.filter((i) => matches(i, query.source)) : sides.source,\n target: query.target.trim() ? sides.target.filter((i) => matches(i, query.target)) : sides.target,\n }),\n [sides, query, matches]\n )\n\n const setQuery = React.useCallback((side: TransferListSide, q: string) => {\n setQueryState((prev) => ({ ...prev, [side]: q }))\n }, [])\n const setFocused = React.useCallback((side: TransferListSide, v: string | null) => {\n setFocusedState((prev) => (prev[side] === v ? prev : { ...prev, [side]: v }))\n }, [])\n\n const toggle = React.useCallback((side: TransferListSide, v: string, force?: boolean) => {\n setSelected((prev) => {\n const has = prev[side].includes(v)\n const next = force ?? !has\n if (next === has) return prev\n return { ...prev, [side]: next ? [...prev[side], v] : prev[side].filter((x) => x !== v) }\n })\n }, [])\n\n const selectRange = React.useCallback(\n (side: TransferListSide, from: string, to: string) => {\n const list = visible[side].filter((i) => !itemDisabled(i)).map(itemToValue)\n const a = list.indexOf(from)\n const b = list.indexOf(to)\n if (a < 0 || b < 0) return\n const range = list.slice(Math.min(a, b), Math.max(a, b) + 1)\n setSelected((prev) => ({ ...prev, [side]: Array.from(new Set([...prev[side], ...range])) }))\n },\n [visible, itemDisabled, itemToValue]\n )\n\n const selectAll = React.useCallback(\n (side: TransferListSide, checked: boolean) => {\n const list = visible[side].filter((i) => !itemDisabled(i)).map(itemToValue)\n setSelected((prev) => ({\n ...prev,\n [side]: checked ? Array.from(new Set([...prev[side], ...list])) : prev[side].filter((v) => !list.includes(v)),\n }))\n },\n [visible, itemDisabled, itemToValue]\n )\n\n const move = React.useCallback(\n (direction: TransferListDirection, values?: string[]) => {\n if (disabled) return\n const from = sideOf(direction)\n const allowed = new Set(sides[from].filter((i) => !itemDisabled(i)).map(itemToValue))\n const moving = (values ?? selected[from]).filter((v) => allowed.has(v))\n if (moving.length === 0) return\n const next = sortValues(direction === \"right\" ? [...value, ...moving] : value.filter((v) => !moving.includes(v)))\n setValueState(next)\n setSelected((prev) => ({ ...prev, [from]: prev[from].filter((v) => !moving.includes(v)) }))\n const movedItems = items.filter((item) => moving.includes(itemToValue(item)))\n const to = direction === \"right\" ? \"target\" : \"source\"\n const title = titles?.[to] ?? (to === \"target\" ? \"the selected list\" : \"the available list\")\n setAnnouncement(\n movedItems.length === 1\n ? `Moved ${itemToString(movedItems[0])} to ${title}.`\n : `Moved ${movedItems.length} items to ${title}.`\n )\n onValueChange?.({\n value: next,\n items: items.filter((i) => next.includes(itemToValue(i))),\n moved: movedItems,\n direction,\n })\n },\n [\n disabled,\n sides,\n selected,\n value,\n items,\n itemToValue,\n itemToString,\n itemDisabled,\n sortValues,\n setValueState,\n onValueChange,\n titles,\n setAnnouncement,\n ]\n )\n\n const moveAll = React.useCallback(\n (direction: TransferListDirection) => {\n const from = sideOf(direction)\n move(direction, visible[from].map(itemToValue))\n },\n [move, visible, itemToValue]\n )\n\n const ctx: TransferListContextValue<T> = {\n items,\n itemToValue,\n itemToString,\n itemDisabled,\n value,\n sides,\n visible,\n query,\n setQuery,\n selected,\n toggle,\n selectRange,\n selectAll,\n move,\n moveAll,\n disabled,\n announcement,\n focused,\n setFocused,\n }\n\n return (\n <TransferListContext.Provider value={ctx as unknown as TransferListContextValue}>\n <div\n data-slot=\"transfer-list\"\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\"grid grid-cols-1 gap-2 sm:grid-cols-[1fr_auto_1fr] sm:items-stretch\", className)}\n {...props}\n >\n {children}\n <LiveRegion data-slot=\"transfer-list-live-region\" message={announcement} />\n </div>\n </TransferListContext.Provider>\n )\n}\n\n/* ---------------------------------- panel -------------------------------- */\n\nfunction TransferListPanel({ side, className, ...props }: React.ComponentProps<\"div\"> & { side: TransferListSide }) {\n const ctx = useTransferList()\n return (\n <TransferListSideContext.Provider value={side}>\n <div\n data-slot=\"transfer-list-panel\"\n data-side={side}\n data-disabled={ctx.disabled ? \"\" : undefined}\n className={cn(\n \"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\",\n className\n )}\n {...props}\n />\n </TransferListSideContext.Provider>\n )\n}\n\nfunction TransferListPanelHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"transfer-list-panel-header\"\n className={cn(\"flex h-9 shrink-0 items-center gap-2 border-b px-2.5\", className)}\n {...props}\n />\n )\n}\n\nfunction TransferListPanelTitle({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span data-slot=\"transfer-list-panel-title\" className={cn(\"flex-1 truncate font-medium\", className)} {...props} />\n )\n}\n\n/** \"selected / total\" for the side; `children` overrides with `(selected, total) => ReactNode`. */\nfunction TransferListPanelCount({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"span\">, \"children\"> & {\n children?: (selected: number, total: number) => React.ReactNode\n}) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n const total = ctx.sides[side].length\n const count = ctx.selected[side].length\n return (\n <span\n data-slot=\"transfer-list-panel-count\"\n className={cn(\"text-xs text-muted-foreground tabular-nums\", className)}\n {...props}\n >\n {children ? children(count, total) : count > 0 ? `${count} / ${total}` : total}\n </span>\n )\n}\n\n/** Header checkbox that selects every visible enabled item on this side. */\nfunction TransferListSelectAll({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof Checkbox>, \"checked\" | \"onCheckedChange\">) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n const enabled = ctx.visible[side].filter((i) => !ctx.itemDisabled(i)).map(ctx.itemToValue)\n const count = enabled.filter((v) => ctx.selected[side].includes(v)).length\n const checked = enabled.length > 0 && count === enabled.length ? true : count > 0 ? \"indeterminate\" : false\n return (\n <Checkbox\n data-slot=\"transfer-list-select-all\"\n aria-label={`Select all ${side === \"source\" ? \"available\" : \"selected\"} items`}\n checked={checked}\n disabled={ctx.disabled || enabled.length === 0}\n onCheckedChange={({ checked: next }) => ctx.selectAll(side, next === true)}\n className={className}\n {...props}\n />\n )\n}\n\nfunction TransferListSearch({\n className,\n placeholder = \"Search…\",\n ...props\n}: Omit<React.ComponentProps<typeof InputGroupInput>, \"value\" | \"onChange\">) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n return (\n <InputGroup\n data-slot=\"transfer-list-search\"\n className={cn(\"h-8 shrink-0 rounded-none border-0 border-b shadow-none has-focus-visible:ring-0\", className)}\n >\n <InputGroupAddon>\n <SearchIcon />\n </InputGroupAddon>\n <InputGroupInput\n role=\"searchbox\"\n placeholder={placeholder}\n value={ctx.query[side]}\n disabled={ctx.disabled}\n onChange={(event) => ctx.setQuery(side, event.target.value)}\n {...props}\n />\n </InputGroup>\n )\n}\n\n/* ---------------------------------- items -------------------------------- */\n\nconst TransferListRenderContext = React.createContext<((item: unknown) => React.ReactNode) | undefined>(undefined)\n\n/**\n * The listbox for one side. Roving focus; ↑/↓ move, Space toggles, Shift+↑/↓ extends,\n * ⌘/Ctrl+A selects all, Enter (or double-click) moves the focused item across.\n */\nfunction TransferListItems<T = TransferListItemBase>({\n className,\n children,\n onKeyDown,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { children?: (item: T) => React.ReactNode }) {\n const ctx = useTransferList<T>()\n const side = useTransferListSide()\n const ref = React.useRef<HTMLDivElement>(null)\n const list = ctx.visible[side]\n const values = list.map(ctx.itemToValue)\n const focused = ctx.focused[side]\n const active = focused && values.includes(focused) ? focused : (values[0] ?? null)\n\n const focusValue = (v: string) => {\n ctx.setFocused(side, v)\n ref.current?.querySelector<HTMLElement>(`[data-slot=transfer-list-item][data-value=\"${CSS.escape(v)}\"]`)?.focus()\n }\n\n return (\n <TransferListRenderContext.Provider value={children as ((item: unknown) => React.ReactNode) | undefined}>\n <div\n ref={ref}\n data-slot=\"transfer-list-items\"\n role=\"listbox\"\n aria-multiselectable\n aria-label={side === \"source\" ? \"Available\" : \"Selected\"}\n data-active-value={active ?? undefined}\n className={cn(\"flex min-h-0 flex-1 flex-col gap-px overflow-y-auto p-1 outline-none\", className)}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented || ctx.disabled) return\n const enabledValues = list.filter((i) => !ctx.itemDisabled(i)).map(ctx.itemToValue)\n const index = active ? values.indexOf(active) : -1\n const step = (delta: number) => {\n const next = values[Math.max(0, Math.min(values.length - 1, index + delta))]\n if (!next) return\n if (event.shiftKey && active && enabledValues.includes(active)) ctx.selectRange(side, active, next)\n focusValue(next)\n }\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault()\n step(1)\n break\n case \"ArrowUp\":\n event.preventDefault()\n step(-1)\n break\n case \"Home\":\n event.preventDefault()\n if (values[0]) focusValue(values[0])\n break\n case \"End\":\n event.preventDefault()\n if (values.length) focusValue(values[values.length - 1])\n break\n case \" \":\n event.preventDefault()\n if (active && enabledValues.includes(active)) ctx.toggle(side, active)\n break\n case \"Enter\":\n event.preventDefault()\n if (active && enabledValues.includes(active)) {\n const next = values[index + 1] ?? values[index - 1] ?? null\n ctx.move(side === \"source\" ? \"right\" : \"left\", [active])\n ctx.setFocused(side, next)\n if (next) queueMicrotask(() => focusValue(next))\n }\n break\n case \"a\":\n case \"A\":\n if (event.metaKey || event.ctrlKey) {\n event.preventDefault()\n ctx.selectAll(side, true)\n }\n break\n }\n }}\n {...props}\n >\n {list.map((item) => {\n const v = ctx.itemToValue(item)\n return <TransferListItem key={v} item={item} tabIndex={v === active ? 0 : -1} />\n })}\n </div>\n </TransferListRenderContext.Provider>\n )\n}\n\nfunction TransferListItem<T = TransferListItemBase>({\n item,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { item: T; children?: React.ReactNode }) {\n const ctx = useTransferList<T>()\n const side = useTransferListSide()\n const render = React.useContext(TransferListRenderContext)\n const value = ctx.itemToValue(item)\n const disabled = ctx.disabled || ctx.itemDisabled(item)\n const selected = ctx.selected[side].includes(value)\n return (\n <div\n data-slot=\"transfer-list-item\"\n role=\"option\"\n aria-selected={selected}\n aria-disabled={disabled || undefined}\n data-value={value}\n data-selected={selected ? \"\" : undefined}\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\n \"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\",\n className\n )}\n onFocus={() => ctx.setFocused(side, value)}\n onClick={(event) => {\n if (disabled) return\n const anchor = ctx.focused[side]\n if (event.shiftKey && anchor && anchor !== value) ctx.selectRange(side, anchor, value)\n else ctx.toggle(side, value)\n ctx.setFocused(side, value)\n }}\n onDoubleClick={() => {\n if (!disabled) ctx.move(side === \"source\" ? \"right\" : \"left\", [value])\n }}\n {...props}\n >\n {children ?? (\n <>\n <TransferListItemIndicator />\n <TransferListItemText>{render ? render(item) : ctx.itemToString(item)}</TransferListItemText>\n </>\n )}\n </div>\n )\n}\n\n/** Checkbox-styled indicator that follows the item's `data-selected`. */\nfunction TransferListItemIndicator({ className, children, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n aria-hidden\n data-slot=\"transfer-list-item-indicator\"\n className={cn(\n \"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\",\n className\n )}\n {...props}\n >\n {children ?? <CheckIcon data-check=\"\" />}\n </span>\n )\n}\n\nfunction TransferListItemText({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span data-slot=\"transfer-list-item-text\" className={cn(\"flex-1 truncate\", className)} {...props} />\n}\n\nfunction TransferListEmpty({ className, children, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useTransferList()\n const side = useTransferListSide()\n if (ctx.visible[side].length > 0) return null\n const searching = ctx.query[side].trim().length > 0\n return (\n <div\n data-slot=\"transfer-list-empty\"\n className={cn(\n \"flex flex-1 items-center justify-center px-4 py-6 text-center text-sm text-muted-foreground\",\n className\n )}\n {...props}\n >\n {children ?? (searching ? \"No matches\" : \"Nothing here\")}\n </div>\n )\n}\n\n/* -------------------------------- controls ------------------------------- */\n\nfunction TransferListControls({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"transfer-list-controls\"\n className={cn(\"flex items-center justify-center gap-1 sm:flex-col\", className)}\n {...props}\n />\n )\n}\n\nconst directionIcon = {\n right: <ChevronRightIcon />,\n left: <ChevronLeftIcon />,\n}\nconst directionAllIcon = {\n right: <ChevronsRightIcon />,\n left: <ChevronsLeftIcon />,\n}\n\n/** Moves the selected items in `direction` (\"right\" = source → target). Polymorphic via `asChild`. */\nfunction TransferListMoveTrigger({\n direction,\n asChild,\n className,\n children,\n ...props\n}: React.ComponentProps<typeof Button> & { direction: TransferListDirection; asChild?: boolean }) {\n const ctx = useTransferList()\n const from = sideOf(direction)\n const count = ctx.selected[from].filter((v) => ctx.sides[from].some((i) => ctx.itemToValue(i) === v)).length\n const disabled = ctx.disabled || count === 0\n if (asChild) {\n return (\n <ark.button\n asChild\n data-slot=\"transfer-list-move-trigger\"\n data-direction={direction}\n disabled={disabled}\n onClick={() => ctx.move(direction)}\n {...(props as React.ComponentProps<typeof ark.button>)}\n >\n {children}\n </ark.button>\n )\n }\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n data-slot=\"transfer-list-move-trigger\"\n data-direction={direction}\n aria-label={direction === \"right\" ? `Move ${count} selected right` : `Move ${count} selected left`}\n disabled={disabled}\n onClick={() => ctx.move(direction)}\n className={cn(\"rotate-90 sm:rotate-0\", className)}\n {...props}\n >\n {children ?? directionIcon[direction]}\n </Button>\n )\n}\n\n/** Moves every visible item in `direction`. Polymorphic via `asChild`. */\nfunction TransferListMoveAllTrigger({\n direction,\n asChild,\n className,\n children,\n ...props\n}: React.ComponentProps<typeof Button> & { direction: TransferListDirection; asChild?: boolean }) {\n const ctx = useTransferList()\n const from = sideOf(direction)\n const disabled = ctx.disabled || ctx.visible[from].every((i) => ctx.itemDisabled(i))\n if (asChild) {\n return (\n <ark.button\n asChild\n data-slot=\"transfer-list-move-all-trigger\"\n data-direction={direction}\n disabled={disabled}\n onClick={() => ctx.moveAll(direction)}\n {...(props as React.ComponentProps<typeof ark.button>)}\n >\n {children}\n </ark.button>\n )\n }\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n data-slot=\"transfer-list-move-all-trigger\"\n data-direction={direction}\n aria-label={direction === \"right\" ? \"Move all right\" : \"Move all left\"}\n disabled={disabled}\n onClick={() => ctx.moveAll(direction)}\n className={cn(\"rotate-90 sm:rotate-0\", className)}\n {...props}\n >\n {children ?? directionAllIcon[direction]}\n </Button>\n )\n}\n\nexport {\n TransferList,\n TransferListControls,\n TransferListEmpty,\n TransferListItem,\n TransferListItemIndicator,\n TransferListItemText,\n TransferListItems,\n TransferListMoveAllTrigger,\n TransferListMoveTrigger,\n TransferListPanel,\n TransferListPanelCount,\n TransferListPanelHeader,\n TransferListPanelTitle,\n TransferListSearch,\n TransferListSelectAll,\n useTransferList,\n type TransferListDirection,\n type TransferListItemBase,\n type TransferListProps,\n type TransferListSide,\n type TransferListValueChangeDetails,\n}\n"
21
+ }
22
+ ],
23
+ "docs": "## Transfer list\n`src/components/ui/transfer-list.tsx` moves items between two lists. `TransferList { items,\nitemToValue, itemToString, itemDisabled, value/defaultValue/onValueChange({ value, items, moved,\ndirection }), filter, disabled, titles }` owns the per-side search, selection (ticks), and focus;\n`value` is the target side and is always kept in catalog order (moving never reorders). Parts:\n`TransferListPanel side=\"source\"|\"target\"` > `TransferListPanelHeader` > `TransferListSelectAll`\n(toolkit Checkbox over the visible enabled rows, indeterminate) + `TransferListPanelTitle` +\n`TransferListPanelCount` (\"selected / total\"), `TransferListSearch` (per side; select-all and\nmove-all respect it), `TransferListItems` (`role=\"listbox\"`, roving focus; ↑/↓, Shift+↑/↓ and\nShift+click extend, Space ticks, Enter or double-click moves the focused item across, ⌘/Ctrl+A\nticks all visible; render-prop child customises the label) > `TransferListItem item=`\n(`data-selected/disabled`) > `TransferListItemIndicator` (checkbox-styled span) +\n`TransferListItemText`, `TransferListEmpty`; `TransferListControls` > `TransferListMoveTrigger\ndirection=\"right\"|\"left\"` (selected items; disabled when nothing is ticked) and\n`TransferListMoveAllTrigger` (all visible), both polymorphic via `asChild`; a live region announces\nmoves. Disabled items never move. `/access` has the reference (permissions with group badges)."
24
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "tree-select",
3
+ "type": "ui",
4
+ "description": "Both live on Ark's `TreeCollection` (`createTreeCollection`, re-exported) and a Popover, and own only the open state, the search query, and the value (`value`/`defaultValue`/`onValueChange`, always a `string[]`); the trigger is a focusable ",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "controllable",
12
+ "input-group",
13
+ "popover",
14
+ "tree-view",
15
+ "utils"
16
+ ],
17
+ "files": [
18
+ {
19
+ "path": "components/ui/tree-select.tsx",
20
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive, createTreeCollection, type TreeCollection, type TreeNode } from \"@ark-ui/react\"\nimport { ChevronDownIcon, SearchIcon, XIcon } from \"lucide-react\"\nimport { useControllable } from \"@/lib/controllable\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { InputGroup, InputGroupAddon, InputGroupInput } from \"@/components/ui/input-group\"\nimport { PopoverContent, PopoverTrigger } from \"@/components/ui/popover\"\nimport {\n TreeView,\n TreeViewBranch,\n TreeViewBranchContent,\n TreeViewBranchControl,\n TreeViewBranchIndentGuide,\n TreeViewBranchIndicator,\n TreeViewBranchText,\n TreeViewBranchTrigger,\n TreeViewItem,\n TreeViewItemIndicator,\n TreeViewItemText,\n TreeViewNodeCheckbox,\n TreeViewNodeProvider,\n TreeViewTree,\n} from \"@/components/ui/tree-view\"\n\n/* -------------------------------- helpers -------------------------------- */\n\nconst uniq = <T,>(list: T[]) => Array.from(new Set(list))\n\n/**\n * Collapses a list of selected leaf values into the top-most nodes that are fully\n * selected, so \"Engineering\" stands in for every engineer under it.\n */\nfunction collapseValues<T extends TreeNode>(collection: TreeCollection<T>, value: string[]): T[] {\n const selected = new Set(value)\n const out: T[] = []\n const walk = (node: T) => {\n for (const child of collection.getNodeChildren(node)) {\n const v = collection.getNodeValue(child)\n if (collection.isBranchNode(child)) {\n const leaves = collection.getDescendantValues(v)\n if (leaves.length > 0 && leaves.every((l) => selected.has(l))) out.push(child)\n else walk(child)\n } else if (selected.has(v)) out.push(child)\n }\n }\n walk(collection.rootNode)\n return out\n}\n\n/* -------------------------------- context -------------------------------- */\n\ntype TreeSelectValueChangeDetails<T extends TreeNode = TreeNode> = { value: string[]; nodes: T[] }\n\ntype TreeSelectContextValue<T extends TreeNode = TreeNode> = {\n collection: TreeCollection<T>\n /** The collection narrowed by the search query (ancestors of matches kept). */\n filtered: TreeCollection<T>\n value: string[]\n setValue: (value: string[]) => void\n multiple: boolean\n selectableBranches: boolean\n closeOnSelect: boolean\n expandedValue: string[]\n setExpandedValue: (value: string[]) => void\n open: boolean\n setOpen: (open: boolean) => void\n query: string\n setQuery: (query: string) => void\n disabled: boolean\n readOnly: boolean\n invalid: boolean\n /** Toggle (multiple) or pick (single) a node, with branch semantics. */\n toggle: (node: T) => void\n /** Remove one value, or every leaf under a branch value. */\n remove: (value: string) => void\n clear: () => void\n selectedNodes: T[]\n /** Selected nodes with fully selected branches collapsed to the branch. */\n displayNodes: T[]\n contentRef: React.RefObject<HTMLDivElement | null>\n}\n\nconst TreeSelectContext = React.createContext<TreeSelectContextValue | null>(null)\n\nfunction useTreeSelect<T extends TreeNode = TreeNode>() {\n const ctx = React.useContext(TreeSelectContext)\n if (!ctx) throw new Error(\"TreeSelect parts must be used inside <TreeSelect>\")\n return ctx as unknown as TreeSelectContextValue<T>\n}\n\n/* ---------------------------------- root --------------------------------- */\n\ntype TreeSelectProps<T extends TreeNode> = Omit<\n React.ComponentProps<typeof PopoverPrimitive.Root>,\n \"open\" | \"defaultOpen\" | \"onOpenChange\" | \"children\"\n> & {\n collection: TreeCollection<T>\n value?: string[]\n defaultValue?: string[]\n onValueChange?: (details: TreeSelectValueChangeDetails<T>) => void\n /** Checkbox selection of leaves; a branch stands for all of its leaves. */\n multiple?: boolean\n /** Single mode only: allow picking a branch itself instead of expanding it. */\n selectableBranches?: boolean\n /** Close after a pick. Defaults to `!multiple`. */\n closeOnSelect?: boolean\n expandedValue?: string[]\n defaultExpandedValue?: string[]\n onExpandedChange?: (details: { expandedValue: string[] }) => void\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (details: { open: boolean }) => void\n disabled?: boolean\n readOnly?: boolean\n invalid?: boolean\n children: React.ReactNode\n}\n\nfunction TreeSelect<T extends TreeNode>({\n collection,\n value: valueProp,\n defaultValue = [],\n onValueChange,\n multiple = false,\n selectableBranches = false,\n closeOnSelect = !multiple,\n expandedValue: expandedProp,\n defaultExpandedValue = [],\n onExpandedChange,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n disabled = false,\n readOnly = false,\n invalid = false,\n positioning,\n lazyMount = true,\n unmountOnExit = true,\n children,\n ...props\n}: TreeSelectProps<T>) {\n const [value, setValueState] = useControllable<string[]>(valueProp, defaultValue, (next) =>\n onValueChange?.({ value: next, nodes: collection.findNodes(next) })\n )\n const [expandedValue, setExpandedValue] = useControllable<string[]>(expandedProp, defaultExpandedValue, (next) =>\n onExpandedChange?.({ expandedValue: next })\n )\n const [open, setOpenState] = useControllable<boolean>(openProp, defaultOpen, (next) => onOpenChange?.({ open: next }))\n const [query, setQuery] = React.useState(\"\")\n const contentRef = React.useRef<HTMLDivElement | null>(null)\n\n const setOpen = React.useCallback(\n (next: boolean) => {\n if (disabled) return\n setOpenState(next)\n if (!next) setQuery(\"\")\n },\n [disabled, setOpenState]\n )\n const setValue = React.useCallback(\n (next: string[]) => {\n if (readOnly) return\n setValueState(collection.sort(uniq(next)))\n },\n [collection, readOnly, setValueState]\n )\n\n const filtered = React.useMemo(() => {\n const q = query.trim().toLowerCase()\n if (!q) return collection\n return collection.filter((node) => collection.stringifyNode(node).toLowerCase().includes(q))\n }, [collection, query])\n\n const toggle = React.useCallback(\n (node: T) => {\n const v = collection.getNodeValue(node)\n const isBranch = collection.isBranchNode(node)\n if (multiple) {\n if (isBranch) {\n const leaves = collection.getDescendantValues(v)\n const all = leaves.length > 0 && leaves.every((l) => value.includes(l))\n setValue(all ? value.filter((x) => !leaves.includes(x)) : [...value, ...leaves])\n } else {\n setValue(value.includes(v) ? value.filter((x) => x !== v) : [...value, v])\n }\n if (closeOnSelect) setOpen(false)\n return\n }\n if (isBranch && !selectableBranches) return\n setValue([v])\n if (closeOnSelect) setOpen(false)\n },\n [collection, multiple, selectableBranches, closeOnSelect, value, setValue, setOpen]\n )\n const remove = React.useCallback(\n (v: string) => {\n const node = collection.findNode(v)\n const drop = node && collection.isBranchNode(node) ? collection.getDescendantValues(v) : [v]\n setValue(value.filter((x) => !drop.includes(x)))\n },\n [collection, value, setValue]\n )\n const clear = React.useCallback(() => setValue([]), [setValue])\n\n const selectedNodes = React.useMemo(() => collection.findNodes(value), [collection, value])\n const displayNodes = React.useMemo(\n () => (multiple ? collapseValues(collection, value) : selectedNodes),\n [collection, multiple, value, selectedNodes]\n )\n\n const ctx: TreeSelectContextValue<T> = {\n collection,\n filtered,\n value,\n setValue,\n multiple,\n selectableBranches,\n closeOnSelect,\n expandedValue,\n setExpandedValue,\n open,\n setOpen,\n query,\n setQuery,\n disabled,\n readOnly,\n invalid,\n toggle,\n remove,\n clear,\n selectedNodes,\n displayNodes,\n contentRef,\n }\n\n return (\n <TreeSelectContext.Provider value={ctx as unknown as TreeSelectContextValue}>\n <PopoverPrimitive.Root\n open={open}\n onOpenChange={({ open: next }) => setOpen(next)}\n positioning={{ placement: \"bottom-start\", gutter: 4, sameWidth: true, ...positioning }}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n {...props}\n >\n {children}\n </PopoverPrimitive.Root>\n </TreeSelectContext.Provider>\n )\n}\n\n/* -------------------------------- trigger -------------------------------- */\n\nfunction TreeSelectTrigger({\n className,\n size = \"default\",\n variant = \"default\",\n children,\n onKeyDown,\n ...props\n}: React.ComponentProps<\"div\"> & { size?: \"sm\" | \"default\"; variant?: \"default\" | \"unstyled\" }) {\n const ctx = useTreeSelect()\n return (\n <PopoverTrigger asChild>\n <div\n data-slot=\"tree-select-trigger\"\n role=\"combobox\"\n aria-haspopup=\"tree\"\n aria-expanded={ctx.open}\n aria-disabled={ctx.disabled || undefined}\n aria-invalid={ctx.invalid || undefined}\n aria-readonly={ctx.readOnly || undefined}\n tabIndex={ctx.disabled ? -1 : 0}\n data-size={size}\n data-variant={variant}\n data-state={ctx.open ? \"open\" : \"closed\"}\n data-disabled={ctx.disabled ? \"\" : undefined}\n data-invalid={ctx.invalid ? \"\" : undefined}\n data-placeholder-shown={ctx.value.length === 0 ? \"\" : undefined}\n className={cn(\n \"flex cursor-default items-center justify-between gap-1.5 text-sm outline-none select-none data-placeholder-shown:text-muted-foreground data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n variant === \"default\" &&\n \"w-full rounded-lg border border-input bg-transparent py-1 pr-2 pl-2.5 transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 data-[size=default]:min-h-8 data-[size=sm]:min-h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] dark:bg-input/30 dark:hover:bg-input/50 dark:data-invalid:border-destructive/50 dark:data-invalid:ring-destructive/40\",\n className\n )}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented || ctx.disabled) return\n if (event.key === \"Enter\" || event.key === \" \" || event.key === \"ArrowDown\") {\n event.preventDefault()\n ctx.setOpen(true)\n } else if (event.key === \"Backspace\" && ctx.multiple && ctx.value.length > 0) {\n const last = ctx.displayNodes[ctx.displayNodes.length - 1]\n if (last) ctx.remove(ctx.collection.getNodeValue(last))\n }\n }}\n {...props}\n >\n {children}\n </div>\n </PopoverTrigger>\n )\n}\n\nfunction TreeSelectIndicator({ className, children, ...props }: React.ComponentProps<\"span\">) {\n const ctx = useTreeSelect()\n return (\n <span\n data-slot=\"tree-select-indicator\"\n data-state={ctx.open ? \"open\" : \"closed\"}\n className={cn(\"ml-auto flex shrink-0 items-center text-muted-foreground\", className)}\n {...props}\n >\n {children ?? <ChevronDownIcon className=\"size-4\" />}\n </span>\n )\n}\n\n/** Selected labels as text (fully selected branches collapse to the branch), or the placeholder. */\nfunction TreeSelectValue<T extends TreeNode>({\n placeholder,\n separator = \", \",\n children,\n className,\n ...props\n}: Omit<React.ComponentProps<\"span\">, \"children\"> & {\n placeholder?: React.ReactNode\n separator?: string\n children?: (nodes: T[]) => React.ReactNode\n}) {\n const ctx = useTreeSelect<T>()\n const empty = ctx.displayNodes.length === 0\n return (\n <span\n data-slot=\"tree-select-value\"\n data-placeholder-shown={empty ? \"\" : undefined}\n className={cn(\"line-clamp-1 flex-1 text-left\", className)}\n {...props}\n >\n {empty\n ? placeholder\n : children\n ? children(ctx.displayNodes)\n : ctx.displayNodes.map((n) => ctx.collection.stringifyNode(n)).join(separator)}\n </span>\n )\n}\n\n/** Selected nodes as removable chips (multiple mode). */\nfunction TreeSelectChips<T extends TreeNode>({\n placeholder,\n children,\n className,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & {\n placeholder?: React.ReactNode\n children?: (node: T) => React.ReactNode\n}) {\n const ctx = useTreeSelect<T>()\n const empty = ctx.displayNodes.length === 0\n return (\n <div\n data-slot=\"tree-select-chips\"\n data-placeholder-shown={empty ? \"\" : undefined}\n className={cn(\"flex min-w-0 flex-1 flex-wrap items-center gap-1 text-left\", className)}\n {...props}\n >\n {empty\n ? placeholder\n : ctx.displayNodes.map((node) =>\n children ? (\n <React.Fragment key={ctx.collection.getNodeValue(node)}>{children(node)}</React.Fragment>\n ) : (\n <TreeSelectChip key={ctx.collection.getNodeValue(node)} node={node} />\n )\n )}\n </div>\n )\n}\n\nfunction TreeSelectChip<T extends TreeNode>({\n node,\n showRemove = true,\n className,\n children,\n ...props\n}: React.ComponentProps<\"span\"> & { node: T; showRemove?: boolean }) {\n const ctx = useTreeSelect<T>()\n const value = ctx.collection.getNodeValue(node)\n const label = ctx.collection.stringifyNode(node)\n return (\n <span\n data-slot=\"tree-select-chip\"\n data-value={value}\n data-branch={ctx.collection.isBranchNode(node) ? \"\" : undefined}\n className={cn(\n \"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-data-[slot=tree-select-chip-remove]:pr-0\",\n className\n )}\n {...props}\n >\n {children ?? label}\n {showRemove && !ctx.readOnly && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n data-slot=\"tree-select-chip-remove\"\n aria-label={`Remove ${label}`}\n disabled={ctx.disabled}\n className=\"-ml-1 opacity-50 hover:opacity-100\"\n onClick={(event) => {\n event.stopPropagation()\n ctx.remove(value)\n }}\n onKeyDown={(event) => event.stopPropagation()}\n >\n <XIcon className=\"pointer-events-none\" />\n </Button>\n )}\n </span>\n )\n}\n\nfunction TreeSelectClearTrigger({ className, children, ...props }: React.ComponentProps<typeof Button>) {\n const ctx = useTreeSelect()\n if (ctx.value.length === 0 || ctx.readOnly) return null\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n data-slot=\"tree-select-clear-trigger\"\n aria-label=\"Clear selection\"\n disabled={ctx.disabled}\n className={cn(\"-mr-1 text-muted-foreground hover:text-foreground\", className)}\n onClick={(event) => {\n event.stopPropagation()\n ctx.clear()\n }}\n onKeyDown={(event) => event.stopPropagation()}\n {...props}\n >\n {children ?? <XIcon />}\n </Button>\n )\n}\n\n/** Hidden inputs for native form submission. */\nfunction TreeSelectHiddenInput({\n name,\n ...props\n}: Omit<React.ComponentProps<\"input\">, \"type\" | \"value\"> & { name: string }) {\n const ctx = useTreeSelect()\n return (\n <>\n {ctx.value.map((v) => (\n <input key={v} type=\"hidden\" name={name} value={v} data-slot=\"tree-select-hidden-input\" {...props} />\n ))}\n </>\n )\n}\n\n/* -------------------------------- content -------------------------------- */\n\nfunction TreeSelectContent({ className, ...props }: React.ComponentProps<typeof PopoverContent>) {\n const ctx = useTreeSelect()\n return (\n <PopoverContent\n ref={ctx.contentRef}\n data-slot=\"tree-select-content\"\n className={cn(\"w-(--reference-width) min-w-48 gap-1 p-1\", className)}\n {...props}\n />\n )\n}\n\nfunction TreeSelectSearch({\n className,\n placeholder = \"Search…\",\n ...props\n}: Omit<React.ComponentProps<typeof InputGroupInput>, \"value\" | \"onChange\">) {\n const ctx = useTreeSelect()\n return (\n <InputGroup data-slot=\"tree-select-search\" className={cn(\"h-8 shrink-0\", className)}>\n <InputGroupAddon>\n <SearchIcon />\n </InputGroupAddon>\n <InputGroupInput\n role=\"searchbox\"\n placeholder={placeholder}\n value={ctx.query}\n onChange={(event) => ctx.setQuery(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"ArrowDown\") {\n const first = ctx.contentRef.current?.querySelector<HTMLElement>(\n \"[data-slot=tree-view-item], [data-slot=tree-view-branch-control]\"\n )\n if (first) {\n event.preventDefault()\n first.focus()\n }\n }\n }}\n {...props}\n />\n </InputGroup>\n )\n}\n\nfunction TreeSelectEmpty({ className, children, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useTreeSelect()\n if (ctx.filtered.getNodeChildren(ctx.filtered.rootNode).length > 0) return null\n return (\n <div\n data-slot=\"tree-select-empty\"\n className={cn(\"px-2 py-4 text-center text-sm text-muted-foreground\", className)}\n {...props}\n >\n {children ?? \"No results\"}\n </div>\n )\n}\n\n/* ---------------------------------- tree --------------------------------- */\n\ntype TreeSelectRenderNode<T extends TreeNode> = (node: T) => React.ReactNode\n\nconst TreeSelectRenderContext = React.createContext<TreeSelectRenderNode<TreeNode> | undefined>(undefined)\n\n/**\n * The tree inside the popup. Renders every node of the (filtered) collection;\n * pass a render-prop child to customise the row label.\n */\nfunction TreeSelectTree<T extends TreeNode>({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof TreeViewTree>, \"children\"> & { children?: TreeSelectRenderNode<T> }) {\n const ctx = useTreeSelect<T>()\n const searching = ctx.query.trim().length > 0\n const searchExpanded = React.useMemo(\n () => (searching ? ctx.filtered.getBranchValues() : null),\n [searching, ctx.filtered]\n )\n const roots = ctx.filtered.getNodeChildren(ctx.filtered.rootNode)\n if (roots.length === 0) return null\n return (\n <TreeSelectRenderContext.Provider value={children as TreeSelectRenderNode<TreeNode> | undefined}>\n <TreeView\n collection={ctx.filtered}\n selectionMode={ctx.multiple ? \"multiple\" : \"single\"}\n selectedValue={ctx.multiple ? [] : ctx.value}\n onSelectionChange={({ selectedValue }) => {\n const picked = selectedValue.find((v) => ctx.multiple || !ctx.value.includes(v)) ?? selectedValue[0]\n const node = picked != null ? ctx.filtered.findNode(picked) : undefined\n if (node) ctx.toggle(node)\n }}\n checkedValue={ctx.multiple ? ctx.value : undefined}\n onCheckedChange={({ checkedValue }) => ctx.setValue(checkedValue)}\n expandedValue={searchExpanded ?? ctx.expandedValue}\n onExpandedChange={({ expandedValue }) => {\n if (!searching) ctx.setExpandedValue(expandedValue)\n }}\n data-slot=\"tree-select-tree\"\n className=\"w-full\"\n >\n <TreeViewTree className={cn(\"max-h-72 overflow-y-auto\", className)} {...props}>\n {roots.map((node, index) => (\n <TreeSelectNode key={ctx.filtered.getNodeValue(node)} node={node} indexPath={[index]} />\n ))}\n </TreeViewTree>\n </TreeView>\n </TreeSelectRenderContext.Provider>\n )\n}\n\nfunction TreeSelectNode<T extends TreeNode>({ node, indexPath }: { node: T; indexPath: number[] }) {\n const ctx = useTreeSelect<T>()\n const render = React.useContext(TreeSelectRenderContext)\n const value = ctx.filtered.getNodeValue(node)\n const label = render ? render(node) : ctx.filtered.stringifyNode(node)\n const children = ctx.filtered.getNodeChildren(node)\n const isBranch = ctx.filtered.isBranchNode(node)\n return (\n <TreeViewNodeProvider node={node} indexPath={indexPath}>\n {isBranch ? (\n <TreeViewBranch data-slot=\"tree-select-branch\">\n <TreeViewBranchControl>\n <TreeViewBranchTrigger>\n <TreeViewBranchIndicator />\n </TreeViewBranchTrigger>\n {ctx.multiple && <TreeViewNodeCheckbox />}\n <TreeViewBranchText>{label}</TreeViewBranchText>\n </TreeViewBranchControl>\n <TreeViewBranchContent>\n <TreeViewBranchIndentGuide />\n {children.map((child, index) => (\n <TreeSelectNode key={ctx.filtered.getNodeValue(child)} node={child} indexPath={[...indexPath, index]} />\n ))}\n </TreeViewBranchContent>\n </TreeViewBranch>\n ) : (\n <TreeViewItem data-slot=\"tree-select-item\" data-value={value}>\n {ctx.multiple && <TreeViewNodeCheckbox />}\n <TreeViewItemText>{label}</TreeViewItemText>\n {!ctx.multiple && <TreeViewItemIndicator />}\n </TreeViewItem>\n )}\n </TreeViewNodeProvider>\n )\n}\n\nexport {\n TreeSelect,\n TreeSelectChip,\n TreeSelectChips,\n TreeSelectClearTrigger,\n TreeSelectContent,\n TreeSelectEmpty,\n TreeSelectHiddenInput,\n TreeSelectIndicator,\n TreeSelectNode,\n TreeSelectSearch,\n TreeSelectTree,\n TreeSelectTrigger,\n TreeSelectValue,\n collapseValues,\n createTreeCollection,\n useTreeSelect,\n type TreeCollection,\n type TreeNode,\n type TreeSelectProps,\n type TreeSelectValueChangeDetails,\n}\n"
21
+ }
22
+ ],
23
+ "docs": "## Tree select and cascader\nBoth live on Ark's `TreeCollection` (`createTreeCollection`, re-exported) and a Popover, and own\nonly the open state, the search query, and the value (`value`/`defaultValue`/`onValueChange`,\nalways a `string[]`); the trigger is a focusable `div[role=combobox]` via `PopoverTrigger asChild`\nso chips and clear buttons inside it stay real buttons. Both use `src/lib/controllable.ts`\n`useControllable(controlled, default, onChange)` (the shared controllable-state hook; prefer it\nover local copies).\n`src/components/ui/tree-select.tsx`: `TreeSelect { collection, multiple, selectableBranches,\ncloseOnSelect, expandedValue…, open…, disabled, readOnly, invalid }` > `TreeSelectTrigger`\n(`size`, `variant=\"unstyled\"`; Enter/Space/ArrowDown open, Backspace removes the last chip) >\n`TreeSelectValue` (labels joined, render-prop child gets the nodes) or `TreeSelectChips` >\n`TreeSelectChip node=` (remove button), `TreeSelectClearTrigger`, `TreeSelectIndicator`;\n`TreeSelectContent` (same width as the trigger) > `TreeSelectSearch` (filters the collection with\nancestors kept and auto-expands matches; ArrowDown moves into the tree), `TreeSelectTree`\n(renders the toolkit `TreeView` parts recursively through `TreeSelectNode`; render-prop child\ncustomises the row label), `TreeSelectEmpty`; `TreeSelectHiddenInput name=`. Multiple mode is\nArk checkbox semantics: `value` holds leaf values only, a branch toggles all its leaves, and\n`collapseValues(collection, value)` (used by the value/chips) shows a fully selected branch as one\nchip. Single mode ignores branch picks unless `selectableBranches`. `useTreeSelect()` exposes it all.\n`src/components/ui/cascader.tsx`: `Cascader { collection, changeOnSelect, expandTrigger:\nclick|hover, open…, disabled, readOnly, invalid }` > `CascaderTrigger` > `CascaderValue`\n(the path as \"A / B / C\", `separator`, render-prop child gets the path nodes), `CascaderClearTrigger`,\n`CascaderIndicator`; `CascaderContent` (owns the keyboard: ↑/↓ within a column, → opens a branch,\n← back to the parent, Home/End, Enter/Space activates) > `CascaderSearch` (matches every level,\nflat results replace the columns), `CascaderColumns` (`role=\"tree\"`, `aria-activedescendant`;\ncolumn `i + 1` lists the children of `expandedPath[i]`; render-prop child customises items) >\n`CascaderColumn depth=` > `CascaderItem node=` (`data-branch/expanded/selected/highlighted/disabled`)\n> `CascaderItemText` + `CascaderItemIndicator` (chevron on branches, check on the selected leaf),\n`CascaderSearchResults` > `CascaderSearchResult path=`, `CascaderEmpty`, `CascaderHiddenInput`.\nPicking a leaf closes; a branch opens the next column and, with `changeOnSelect`, is itself the\nvalue. Opening starts from the current value's path. `useCascader()` exposes it all.\n`src/demo/pickers/` is the reference (`/access`, \"Roles & access\"): folder picker with branches selectable and\nicons, people/teams multi-select with chips, hover-expanding location cascader with search, and a\nchange-on-select category cascader."
24
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "tree-view",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/tree-view.tsx",
15
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { TreeView as TreeViewPrimitive, createTreeCollection, type TreeCollection, type TreeNode } from \"@ark-ui/react\"\nimport { CheckIcon, ChevronRightIcon, MinusIcon } from \"lucide-react\"\n\nfunction TreeView<T extends TreeNode>({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.Root<T>>) {\n return <TreeViewPrimitive.Root data-slot=\"tree-view\" className={cn(\"w-full text-sm\", className)} {...props} />\n}\n\nfunction TreeViewContext({ ...props }: React.ComponentProps<typeof TreeViewPrimitive.Context>) {\n return <TreeViewPrimitive.Context {...props} />\n}\n\nfunction TreeViewLabel({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.Label>) {\n return (\n <TreeViewPrimitive.Label\n data-slot=\"tree-view-label\"\n className={cn(\"mb-1 text-sm font-medium select-none\", className)}\n {...props}\n />\n )\n}\n\nfunction TreeViewTree({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.Tree>) {\n return (\n <TreeViewPrimitive.Tree\n data-slot=\"tree-view-tree\"\n className={cn(\"flex flex-col gap-px outline-none\", className)}\n {...props}\n />\n )\n}\n\nfunction TreeViewNodeProvider<T>({ ...props }: React.ComponentProps<typeof TreeViewPrimitive.NodeProvider<T>>) {\n return <TreeViewPrimitive.NodeProvider {...props} />\n}\n\nfunction TreeViewNodeContext({ ...props }: React.ComponentProps<typeof TreeViewPrimitive.NodeContext>) {\n return <TreeViewPrimitive.NodeContext {...props} />\n}\n\nconst rowClassName =\n \"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\"\n\nfunction TreeViewBranch({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.Branch>) {\n return (\n <TreeViewPrimitive.Branch\n data-slot=\"tree-view-branch\"\n className={cn(\"flex flex-col gap-px\", className)}\n {...props}\n />\n )\n}\n\nfunction TreeViewBranchControl({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.BranchControl>) {\n return (\n <TreeViewPrimitive.BranchControl\n data-slot=\"tree-view-branch-control\"\n className={cn(rowClassName, className)}\n {...props}\n />\n )\n}\n\nfunction TreeViewBranchTrigger({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.BranchTrigger>) {\n return (\n <TreeViewPrimitive.BranchTrigger\n data-slot=\"tree-view-branch-trigger\"\n className={cn(\"inline-flex\", className)}\n {...props}\n />\n )\n}\n\nfunction TreeViewBranchIndicator({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof TreeViewPrimitive.BranchIndicator>) {\n return (\n <TreeViewPrimitive.BranchIndicator\n data-slot=\"tree-view-branch-indicator\"\n className={cn(\"text-muted-foreground transition-transform data-[state=open]:rotate-90\", className)}\n {...props}\n >\n {children ?? <ChevronRightIcon />}\n </TreeViewPrimitive.BranchIndicator>\n )\n}\n\nfunction TreeViewBranchText({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.BranchText>) {\n return (\n <TreeViewPrimitive.BranchText data-slot=\"tree-view-branch-text\" className={cn(\"truncate\", className)} {...props} />\n )\n}\n\nfunction TreeViewBranchContent({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.BranchContent>) {\n return (\n <TreeViewPrimitive.BranchContent\n data-slot=\"tree-view-branch-content\"\n className={cn(\"relative flex flex-col gap-px\", className)}\n {...props}\n />\n )\n}\n\nfunction TreeViewBranchIndentGuide({\n className,\n ...props\n}: React.ComponentProps<typeof TreeViewPrimitive.BranchIndentGuide>) {\n return (\n <TreeViewPrimitive.BranchIndentGuide\n data-slot=\"tree-view-branch-indent-guide\"\n className={cn(\n \"absolute inset-y-0 left-[calc(var(--depth)*(--spacing(4))+(--spacing(3)))] w-px bg-border\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TreeViewItem({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.Item>) {\n return <TreeViewPrimitive.Item data-slot=\"tree-view-item\" className={cn(rowClassName, className)} {...props} />\n}\n\nfunction TreeViewItemIndicator({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof TreeViewPrimitive.ItemIndicator>) {\n return (\n <TreeViewPrimitive.ItemIndicator\n data-slot=\"tree-view-item-indicator\"\n className={cn(\"ml-auto text-muted-foreground\", className)}\n {...props}\n >\n {children ?? <CheckIcon />}\n </TreeViewPrimitive.ItemIndicator>\n )\n}\n\nfunction TreeViewItemText({ className, ...props }: React.ComponentProps<typeof TreeViewPrimitive.ItemText>) {\n return <TreeViewPrimitive.ItemText data-slot=\"tree-view-item-text\" className={cn(\"truncate\", className)} {...props} />\n}\n\nfunction TreeViewNodeCheckbox({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof TreeViewPrimitive.NodeCheckbox>) {\n return (\n <TreeViewPrimitive.NodeCheckbox\n data-slot=\"tree-view-node-checkbox\"\n className={cn(\n \"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\",\n className\n )}\n {...props}\n >\n {children ?? (\n <TreeViewNodeCheckboxIndicator indeterminate={<MinusIcon />}>\n <CheckIcon />\n </TreeViewNodeCheckboxIndicator>\n )}\n </TreeViewPrimitive.NodeCheckbox>\n )\n}\n\nfunction TreeViewNodeCheckboxIndicator({\n ...props\n}: React.ComponentProps<typeof TreeViewPrimitive.NodeCheckboxIndicator>) {\n return <TreeViewPrimitive.NodeCheckboxIndicator data-slot=\"tree-view-node-checkbox-indicator\" {...props} />\n}\n\nfunction TreeViewNodeRenameInput({\n className,\n ...props\n}: React.ComponentProps<typeof TreeViewPrimitive.NodeRenameInput>) {\n return (\n <TreeViewPrimitive.NodeRenameInput\n data-slot=\"tree-view-node-rename-input\"\n className={cn(\n \"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\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n TreeView,\n TreeViewBranch,\n TreeViewBranchContent,\n TreeViewBranchControl,\n TreeViewBranchIndentGuide,\n TreeViewBranchIndicator,\n TreeViewBranchText,\n TreeViewBranchTrigger,\n TreeViewContext,\n TreeViewItem,\n TreeViewItemIndicator,\n TreeViewItemText,\n TreeViewLabel,\n TreeViewNodeCheckbox,\n TreeViewNodeCheckboxIndicator,\n TreeViewNodeContext,\n TreeViewNodeProvider,\n TreeViewNodeRenameInput,\n TreeViewTree,\n createTreeCollection,\n type TreeCollection,\n type TreeNode,\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "use-mobile",
3
+ "type": "hook",
4
+ "description": "",
5
+ "dependencies": {},
6
+ "registryDependencies": [],
7
+ "files": [
8
+ {
9
+ "path": "hooks/use-mobile.ts",
10
+ "content": "import * as React from \"react\"\n\nconst MOBILE_BREAKPOINT = 768\n\nexport function useIsMobile() {\n const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)\n\n React.useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)\n const onChange = () => {\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n }\n mql.addEventListener(\"change\", onChange)\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n return () => mql.removeEventListener(\"change\", onChange)\n }, [])\n\n return !!isMobile\n}\n"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "utils",
3
+ "type": "lib",
4
+ "description": "- **Feature parity** with the shadcn original: same exported names, same props surface where Ark supports it, same visual result, same variants. - **Ark's tree wins** where the structure differs (e.g.",
5
+ "dependencies": {
6
+ "clsx": "^2.1.1",
7
+ "tailwind-merge": "^3.6.0"
8
+ },
9
+ "registryDependencies": [],
10
+ "files": [
11
+ {
12
+ "path": "lib/utils.ts",
13
+ "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n"
14
+ }
15
+ ],
16
+ "docs": "## Porting rules\n- **Feature parity** with the shadcn original: same exported names, same props surface\n where Ark supports it, same visual result, same variants.\n- **Ark's tree wins** where the structure differs (e.g. `Positioner` wrappers,\n `Control`/`HiddenInput` splits, `ItemGroup`/`ItemGroupLabel`, `Backdrop` vs `Overlay`).\n Export the extra Ark parts as their own composable pieces; do not fold them away.\n- **Stay compositional.** Every part is its own exported component. No collapsing\n parts into a single \"smart\" component, no dropping sub-parts to simplify.\n- **Styling:** `cn` from `@/lib/utils` and `cva` from `class-variance-authority`.\n Style state via Ark's data attributes (`data-state`, `data-open`, `data-disabled`,\n `data-highlighted`, `data-placement`, `data-orientation`, ...). Keep `data-slot`\n attributes on every part, as shadcn does.\n- `command.tsx` runs on Ark's Listbox and `drawer.tsx` on Ark's Drawer; `cmdk`, `vaul`,\n `radix-ui`, `@base-ui/react`, and every shadcn package (`shadcn` CLI, `@shadcn/react`,\n `cn`) are removed. Do not reintroduce them. `cn` is clsx + tailwind-merge in\n `src/lib/utils.ts`. Base variants, keyframes, and utilities live in `src/index.css`\n (keyframes at the end of the `@theme inline` block); there is no `components.json`.\n- `questionnaire.tsx` and `message-scroller.tsx` were removed because they depended on\n the `@shadcn/react` runtime. Rebuild from scratch if they are wanted.\n- Components with no Ark equivalent (calendar, carousel, chart, resizable, input-otp,\n sonner/toast) keep their current libraries. Separator, Label, AspectRatio, Direction\n are plain React."
17
+ }