@opencosmos/ui 1.3.1

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 (260) hide show
  1. package/.claude/CLAUDE.md +239 -0
  2. package/README.md +161 -0
  3. package/dist/cli.mjs +151 -0
  4. package/dist/dates.d.mts +20 -0
  5. package/dist/dates.d.ts +20 -0
  6. package/dist/dates.js +240 -0
  7. package/dist/dates.js.map +1 -0
  8. package/dist/dates.mjs +203 -0
  9. package/dist/dates.mjs.map +1 -0
  10. package/dist/dnd.d.mts +126 -0
  11. package/dist/dnd.d.ts +126 -0
  12. package/dist/dnd.js +274 -0
  13. package/dist/dnd.js.map +1 -0
  14. package/dist/dnd.mjs +250 -0
  15. package/dist/dnd.mjs.map +1 -0
  16. package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
  17. package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
  18. package/dist/forms.d.mts +38 -0
  19. package/dist/forms.d.ts +38 -0
  20. package/dist/forms.js +198 -0
  21. package/dist/forms.js.map +1 -0
  22. package/dist/forms.mjs +159 -0
  23. package/dist/forms.mjs.map +1 -0
  24. package/dist/hooks-1b8WaQf1.d.mts +225 -0
  25. package/dist/hooks-CKW8vE9H.d.ts +225 -0
  26. package/dist/hooks.d.mts +3 -0
  27. package/dist/hooks.d.ts +3 -0
  28. package/dist/hooks.js +971 -0
  29. package/dist/hooks.js.map +1 -0
  30. package/dist/hooks.mjs +943 -0
  31. package/dist/hooks.mjs.map +1 -0
  32. package/dist/index-DscTIrZ2.d.mts +29 -0
  33. package/dist/index-DscTIrZ2.d.ts +29 -0
  34. package/dist/index.d.mts +3382 -0
  35. package/dist/index.d.ts +3382 -0
  36. package/dist/index.js +15146 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +14802 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/providers-CXPDMsl7.d.mts +30 -0
  41. package/dist/providers-Dn_Msjvz.d.ts +30 -0
  42. package/dist/providers.d.mts +3 -0
  43. package/dist/providers.d.ts +3 -0
  44. package/dist/providers.js +1885 -0
  45. package/dist/providers.js.map +1 -0
  46. package/dist/providers.mjs +1859 -0
  47. package/dist/providers.mjs.map +1 -0
  48. package/dist/tables.d.mts +10 -0
  49. package/dist/tables.d.ts +10 -0
  50. package/dist/tables.js +248 -0
  51. package/dist/tables.js.map +1 -0
  52. package/dist/tables.mjs +218 -0
  53. package/dist/tables.mjs.map +1 -0
  54. package/dist/tokens.d.mts +1065 -0
  55. package/dist/tokens.d.ts +1065 -0
  56. package/dist/tokens.js +2637 -0
  57. package/dist/tokens.js.map +1 -0
  58. package/dist/tokens.mjs +2555 -0
  59. package/dist/tokens.mjs.map +1 -0
  60. package/dist/utils-CIIM7dAC.d.ts +986 -0
  61. package/dist/utils-Cs04sxth.d.mts +986 -0
  62. package/dist/utils.d.mts +4 -0
  63. package/dist/utils.d.ts +4 -0
  64. package/dist/utils.js +874 -0
  65. package/dist/utils.js.map +1 -0
  66. package/dist/utils.mjs +806 -0
  67. package/dist/utils.mjs.map +1 -0
  68. package/dist/validation-Bj1ye-v_.d.mts +114 -0
  69. package/dist/validation-Bj1ye-v_.d.ts +114 -0
  70. package/dist/webgl.d.mts +104 -0
  71. package/dist/webgl.d.ts +104 -0
  72. package/dist/webgl.js +226 -0
  73. package/dist/webgl.js.map +1 -0
  74. package/dist/webgl.mjs +195 -0
  75. package/dist/webgl.mjs.map +1 -0
  76. package/package.json +267 -0
  77. package/src/cli.ts +206 -0
  78. package/src/component-registry.ts +183 -0
  79. package/src/components/actions/Button.test.tsx +61 -0
  80. package/src/components/actions/Button.tsx +70 -0
  81. package/src/components/actions/Link.tsx +78 -0
  82. package/src/components/actions/Magnetic.tsx +68 -0
  83. package/src/components/actions/Toggle.test.tsx +40 -0
  84. package/src/components/actions/Toggle.tsx +47 -0
  85. package/src/components/actions/ToggleGroup.tsx +70 -0
  86. package/src/components/actions/index.ts +5 -0
  87. package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
  88. package/src/components/backgrounds/OrbBackground.tsx +424 -0
  89. package/src/components/backgrounds/WarpBackground.tsx +358 -0
  90. package/src/components/backgrounds/index.ts +3 -0
  91. package/src/components/blocks/Hero.tsx +142 -0
  92. package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
  93. package/src/components/cursor/SplashCursor.tsx +1315 -0
  94. package/src/components/cursor/TargetCursor.tsx +187 -0
  95. package/src/components/cursor/index.ts +2 -0
  96. package/src/components/data-display/AspectImage.tsx +73 -0
  97. package/src/components/data-display/Avatar.test.tsx +35 -0
  98. package/src/components/data-display/Avatar.tsx +55 -0
  99. package/src/components/data-display/Badge.test.tsx +43 -0
  100. package/src/components/data-display/Badge.tsx +84 -0
  101. package/src/components/data-display/Brand.tsx +123 -0
  102. package/src/components/data-display/Calendar.tsx +70 -0
  103. package/src/components/data-display/Card.test.tsx +92 -0
  104. package/src/components/data-display/Card.tsx +115 -0
  105. package/src/components/data-display/Code.tsx +210 -0
  106. package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
  107. package/src/components/data-display/DataTable.tsx +119 -0
  108. package/src/components/data-display/DescriptionList.tsx +41 -0
  109. package/src/components/data-display/GitHubIcon.tsx +44 -0
  110. package/src/components/data-display/Heading.test.tsx +36 -0
  111. package/src/components/data-display/Heading.tsx +83 -0
  112. package/src/components/data-display/StatCard.tsx +195 -0
  113. package/src/components/data-display/Table.tsx +133 -0
  114. package/src/components/data-display/Text.test.tsx +48 -0
  115. package/src/components/data-display/Text.tsx +144 -0
  116. package/src/components/data-display/Timeline.tsx +194 -0
  117. package/src/components/data-display/TreeView.tsx +226 -0
  118. package/src/components/data-display/Typewriter.tsx +119 -0
  119. package/src/components/data-display/VariableWeightText.tsx +130 -0
  120. package/src/components/data-display/index.ts +19 -0
  121. package/src/components/feedback/Alert.test.tsx +44 -0
  122. package/src/components/feedback/Alert.tsx +65 -0
  123. package/src/components/feedback/EmptyState.tsx +113 -0
  124. package/src/components/feedback/Progress.test.tsx +60 -0
  125. package/src/components/feedback/Progress.tsx +30 -0
  126. package/src/components/feedback/ProgressBar.tsx +158 -0
  127. package/src/components/feedback/Skeleton.test.tsx +39 -0
  128. package/src/components/feedback/Skeleton.tsx +45 -0
  129. package/src/components/feedback/Sonner.tsx +28 -0
  130. package/src/components/feedback/Spinner.test.tsx +33 -0
  131. package/src/components/feedback/Spinner.tsx +99 -0
  132. package/src/components/feedback/Stepper.tsx +307 -0
  133. package/src/components/feedback/Toast/Toast.tsx +243 -0
  134. package/src/components/feedback/Toast/index.ts +2 -0
  135. package/src/components/feedback/index.ts +9 -0
  136. package/src/components/forms/Checkbox.test.tsx +40 -0
  137. package/src/components/forms/Checkbox.tsx +31 -0
  138. package/src/components/forms/ColorPicker.tsx +118 -0
  139. package/src/components/forms/Combobox.tsx +96 -0
  140. package/src/components/forms/DragDrop.tsx +440 -0
  141. package/src/components/forms/FileUpload.tsx +252 -0
  142. package/src/components/forms/FilterButton.tsx +65 -0
  143. package/src/components/forms/Form.tsx +197 -0
  144. package/src/components/forms/Input.test.tsx +46 -0
  145. package/src/components/forms/Input.tsx +43 -0
  146. package/src/components/forms/InputOTP.tsx +81 -0
  147. package/src/components/forms/Label.test.tsx +20 -0
  148. package/src/components/forms/Label.tsx +25 -0
  149. package/src/components/forms/RadioGroup.tsx +51 -0
  150. package/src/components/forms/SearchBar.tsx +215 -0
  151. package/src/components/forms/Select.test.tsx +118 -0
  152. package/src/components/forms/Select.tsx +274 -0
  153. package/src/components/forms/Slider.tsx +29 -0
  154. package/src/components/forms/Switch.test.tsx +76 -0
  155. package/src/components/forms/Switch.tsx +30 -0
  156. package/src/components/forms/TextField.tsx +152 -0
  157. package/src/components/forms/Textarea.test.tsx +41 -0
  158. package/src/components/forms/Textarea.tsx +29 -0
  159. package/src/components/forms/ThemeSwitcher.tsx +290 -0
  160. package/src/components/forms/ThemeToggle.tsx +151 -0
  161. package/src/components/forms/index.ts +19 -0
  162. package/src/components/layout/Accordion.test.tsx +66 -0
  163. package/src/components/layout/Accordion.tsx +64 -0
  164. package/src/components/layout/AspectRatio.tsx +7 -0
  165. package/src/components/layout/Carousel.tsx +277 -0
  166. package/src/components/layout/Collapsible.test.tsx +40 -0
  167. package/src/components/layout/Collapsible.tsx +31 -0
  168. package/src/components/layout/Container.test.tsx +45 -0
  169. package/src/components/layout/Container.tsx +99 -0
  170. package/src/components/layout/CustomizerPanel.tsx +400 -0
  171. package/src/components/layout/DatePicker.tsx +57 -0
  172. package/src/components/layout/Footer/Footer.tsx +175 -0
  173. package/src/components/layout/Footer/index.ts +2 -0
  174. package/src/components/layout/GlassSurface.tsx +82 -0
  175. package/src/components/layout/Grid.test.tsx +31 -0
  176. package/src/components/layout/Grid.tsx +130 -0
  177. package/src/components/layout/Header/Header.tsx +450 -0
  178. package/src/components/layout/Header/index.ts +2 -0
  179. package/src/components/layout/PageLayout.tsx +180 -0
  180. package/src/components/layout/PageTemplate.tsx +158 -0
  181. package/src/components/layout/Resizable.tsx +48 -0
  182. package/src/components/layout/ScrollArea.tsx +53 -0
  183. package/src/components/layout/Separator.test.tsx +28 -0
  184. package/src/components/layout/Separator.tsx +29 -0
  185. package/src/components/layout/Sidebar.tsx +171 -0
  186. package/src/components/layout/Stack.test.tsx +41 -0
  187. package/src/components/layout/Stack.tsx +89 -0
  188. package/src/components/layout/glass-surface.css +60 -0
  189. package/src/components/layout/index.ts +18 -0
  190. package/src/components/motion/AnimatedBeam.tsx +159 -0
  191. package/src/components/navigation/Breadcrumb.test.tsx +57 -0
  192. package/src/components/navigation/Breadcrumb.tsx +119 -0
  193. package/src/components/navigation/Breadcrumbs.tsx +221 -0
  194. package/src/components/navigation/Command.tsx +159 -0
  195. package/src/components/navigation/Menubar.tsx +115 -0
  196. package/src/components/navigation/NavLink.tsx +55 -0
  197. package/src/components/navigation/NavigationMenu.tsx +125 -0
  198. package/src/components/navigation/Pagination.tsx +121 -0
  199. package/src/components/navigation/SecondaryNav.tsx +100 -0
  200. package/src/components/navigation/Tabs.test.tsx +47 -0
  201. package/src/components/navigation/Tabs.tsx +60 -0
  202. package/src/components/navigation/TertiaryNav.tsx +90 -0
  203. package/src/components/navigation/index.ts +10 -0
  204. package/src/components/overlays/AlertDialog.test.tsx +69 -0
  205. package/src/components/overlays/AlertDialog.tsx +166 -0
  206. package/src/components/overlays/ContextMenu.tsx +243 -0
  207. package/src/components/overlays/Dialog.test.tsx +79 -0
  208. package/src/components/overlays/Dialog.tsx +158 -0
  209. package/src/components/overlays/Drawer.tsx +128 -0
  210. package/src/components/overlays/Dropdown.tsx +253 -0
  211. package/src/components/overlays/DropdownMenu.tsx +242 -0
  212. package/src/components/overlays/HoverCard.tsx +32 -0
  213. package/src/components/overlays/Modal.tsx +250 -0
  214. package/src/components/overlays/NotificationCenter.tsx +364 -0
  215. package/src/components/overlays/Popover.test.tsx +40 -0
  216. package/src/components/overlays/Popover.tsx +46 -0
  217. package/src/components/overlays/Sheet.tsx +163 -0
  218. package/src/components/overlays/Tooltip.test.tsx +33 -0
  219. package/src/components/overlays/Tooltip.tsx +32 -0
  220. package/src/components/overlays/index.ts +12 -0
  221. package/src/dates.ts +2 -0
  222. package/src/dnd.ts +1 -0
  223. package/src/forms.ts +1 -0
  224. package/src/globals.css +187 -0
  225. package/src/hooks/index.ts +6 -0
  226. package/src/hooks/useForm.ts +247 -0
  227. package/src/hooks/useMotionPreference.test.ts +102 -0
  228. package/src/hooks/useMotionPreference.ts +78 -0
  229. package/src/hooks/useTheme.ts +58 -0
  230. package/src/hooks.ts +9 -0
  231. package/src/index.ts +168 -0
  232. package/src/lib/animations.ts +356 -0
  233. package/src/lib/breadcrumbs.ts +94 -0
  234. package/src/lib/colors.ts +493 -0
  235. package/src/lib/store/customizer.ts +482 -0
  236. package/src/lib/store/index.ts +3 -0
  237. package/src/lib/store/theme.ts +55 -0
  238. package/src/lib/syntax-parser/index.ts +50 -0
  239. package/src/lib/syntax-parser/patterns.ts +64 -0
  240. package/src/lib/syntax-parser/tokenizer.ts +117 -0
  241. package/src/lib/syntax-parser/types.ts +27 -0
  242. package/src/lib/utils.ts +6 -0
  243. package/src/lib/validation.ts +204 -0
  244. package/src/lib/webgl/Color.ts +11 -0
  245. package/src/lib/webgl/Mesh.ts +41 -0
  246. package/src/lib/webgl/Program.ts +118 -0
  247. package/src/lib/webgl/Renderer.ts +51 -0
  248. package/src/lib/webgl/Triangle.ts +27 -0
  249. package/src/lib/webgl/Vec3.ts +18 -0
  250. package/src/lib/webgl/index.ts +13 -0
  251. package/src/nativewind-env.d.ts +1 -0
  252. package/src/providers/ThemeProvider.tsx +461 -0
  253. package/src/providers/index.ts +1 -0
  254. package/src/providers.ts +7 -0
  255. package/src/tables.ts +1 -0
  256. package/src/test/setup.ts +39 -0
  257. package/src/theme.css +158 -0
  258. package/src/tokens.ts +7 -0
  259. package/src/utils.ts +12 -0
  260. package/src/webgl.ts +1 -0
@@ -0,0 +1,440 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext } from 'react';
4
+ import {
5
+ DndContext,
6
+ closestCenter,
7
+ KeyboardSensor,
8
+ PointerSensor,
9
+ useSensor,
10
+ useSensors,
11
+ DragEndEvent,
12
+ DragOverlay,
13
+ DragStartEvent,
14
+ } from '@dnd-kit/core';
15
+ import {
16
+ arrayMove,
17
+ SortableContext,
18
+ sortableKeyboardCoordinates,
19
+ useSortable,
20
+ verticalListSortingStrategy,
21
+ rectSortingStrategy,
22
+ SortingStrategy,
23
+ } from '@dnd-kit/sortable';
24
+ import { CSS } from '@dnd-kit/utilities';
25
+ import { GripVertical } from 'lucide-react';
26
+ import { cn } from '../../lib/utils';
27
+
28
+ /**
29
+ * Item interface for drag and drop lists
30
+ */
31
+ export interface DragDropItem {
32
+ id: string;
33
+ [key: string]: any;
34
+ }
35
+
36
+ /**
37
+ * Context for sharing drag handle listeners
38
+ */
39
+ interface DragHandleContextValue {
40
+ attributes: any;
41
+ listeners: any;
42
+ setActivatorNodeRef?: (element: HTMLElement | null) => void;
43
+ }
44
+
45
+ const DragHandleContext = createContext<DragHandleContextValue | null>(null);
46
+
47
+ /**
48
+ * Props for DragDropList component
49
+ */
50
+ export interface DragDropListProps<T extends DragDropItem> {
51
+ /**
52
+ * Array of items to display
53
+ */
54
+ items: T[];
55
+ /**
56
+ * Callback fired when items are reordered
57
+ */
58
+ onReorder: (items: T[]) => void;
59
+ /**
60
+ * Render function for each item
61
+ */
62
+ renderItem: (item: T, isDragging: boolean) => React.ReactNode;
63
+ /**
64
+ * Enable drag handle mode (only drag from handle)
65
+ * @default false
66
+ */
67
+ withHandle?: boolean;
68
+ /**
69
+ * Custom class name for the list container
70
+ */
71
+ className?: string;
72
+ /**
73
+ * Custom class name for list items
74
+ */
75
+ itemClassName?: string;
76
+ /**
77
+ * Sorting strategy for the drag & drop behavior
78
+ * - verticalListSortingStrategy: For vertical lists (default)
79
+ * - rectSortingStrategy: For grid layouts
80
+ * @default verticalListSortingStrategy
81
+ */
82
+ strategy?: SortingStrategy;
83
+ }
84
+
85
+ /**
86
+ * Props for sortable item
87
+ */
88
+ interface SortableItemProps {
89
+ id: string;
90
+ children: React.ReactNode;
91
+ withHandle?: boolean;
92
+ className?: string;
93
+ }
94
+
95
+ /**
96
+ * Internal sortable item component
97
+ */
98
+ function SortableItem({ id, children, withHandle, className }: SortableItemProps) {
99
+ const {
100
+ attributes,
101
+ listeners,
102
+ setNodeRef,
103
+ setActivatorNodeRef,
104
+ transform,
105
+ transition,
106
+ isDragging,
107
+ } = useSortable({ id });
108
+
109
+ const style = {
110
+ transform: CSS.Transform.toString(transform),
111
+ transition,
112
+ opacity: isDragging ? 0.5 : 1,
113
+ };
114
+
115
+ // If using handle, don't attach listeners to the item
116
+ const itemProps = withHandle ? {} : { ...listeners, ...attributes };
117
+
118
+ const content = withHandle ? (
119
+ <DragHandleContext.Provider value={{ attributes, listeners, setActivatorNodeRef }}>
120
+ {children}
121
+ </DragHandleContext.Provider>
122
+ ) : (
123
+ children
124
+ );
125
+
126
+ return (
127
+ <div
128
+ ref={setNodeRef}
129
+ style={style}
130
+ className={cn(
131
+ 'relative transition-opacity',
132
+ isDragging && 'z-50',
133
+ className
134
+ )}
135
+ {...itemProps}
136
+ >
137
+ {content}
138
+ </div>
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Drag handle component for manual drag control
144
+ * Must be used within a DragDropList with withHandle={true}
145
+ */
146
+ export interface DragDropHandleProps {
147
+ /**
148
+ * Custom class name
149
+ */
150
+ className?: string;
151
+ /**
152
+ * Handle icon
153
+ */
154
+ icon?: React.ReactNode;
155
+ }
156
+
157
+ export function DragDropHandle({ className, icon }: DragDropHandleProps) {
158
+ const context = useContext(DragHandleContext);
159
+
160
+ if (!context) {
161
+ console.warn('DragDropHandle must be used within a DragDropList with withHandle={true}');
162
+ return null;
163
+ }
164
+
165
+ const { attributes, listeners, setActivatorNodeRef } = context;
166
+
167
+ return (
168
+ <div
169
+ ref={setActivatorNodeRef}
170
+ {...attributes}
171
+ {...listeners}
172
+ className={cn(
173
+ 'flex items-center justify-center cursor-grab active:cursor-grabbing',
174
+ 'text-[var(--color-text-secondary)] hover:text-[var(--color-foreground)]',
175
+ 'transition-colors',
176
+ className
177
+ )}
178
+ >
179
+ {icon || <GripVertical className="w-4 h-4" />}
180
+ </div>
181
+ );
182
+ }
183
+
184
+ /**
185
+ * DragDropList - Sortable list component with drag and drop functionality
186
+ *
187
+ * @example
188
+ * ```tsx
189
+ * const [items, setItems] = useState([
190
+ * { id: '1', name: 'Item 1' },
191
+ * { id: '2', name: 'Item 2' },
192
+ * ]);
193
+ *
194
+ * <DragDropList
195
+ * items={items}
196
+ * onReorder={setItems}
197
+ * renderItem={(item) => (
198
+ * <div className="p-4 bg-surface rounded">{item.name}</div>
199
+ * )}
200
+ * />
201
+ * ```
202
+ */
203
+ export function DragDropList<T extends DragDropItem>({
204
+ items,
205
+ onReorder,
206
+ renderItem,
207
+ withHandle = false,
208
+ className,
209
+ itemClassName,
210
+ strategy = verticalListSortingStrategy,
211
+ }: DragDropListProps<T>) {
212
+ const [activeId, setActiveId] = React.useState<string | null>(null);
213
+
214
+ // Configure sensors for touch/mouse/keyboard support
215
+ const sensors = useSensors(
216
+ useSensor(PointerSensor, {
217
+ activationConstraint: {
218
+ distance: 5, // Prevents accidental drags on mobile
219
+ },
220
+ }),
221
+ useSensor(KeyboardSensor, {
222
+ coordinateGetter: sortableKeyboardCoordinates,
223
+ })
224
+ );
225
+
226
+ const handleDragStart = (event: DragStartEvent) => {
227
+ setActiveId(event.active.id as string);
228
+ };
229
+
230
+ const handleDragEnd = (event: DragEndEvent) => {
231
+ const { active, over } = event;
232
+
233
+ if (over && active.id !== over.id) {
234
+ const oldIndex = items.findIndex((item) => item.id === active.id);
235
+ const newIndex = items.findIndex((item) => item.id === over.id);
236
+ onReorder(arrayMove(items, oldIndex, newIndex));
237
+ }
238
+
239
+ setActiveId(null);
240
+ };
241
+
242
+ const activeItem = items.find((item) => item.id === activeId);
243
+
244
+ return (
245
+ <DndContext
246
+ sensors={sensors}
247
+ collisionDetection={closestCenter}
248
+ onDragStart={handleDragStart}
249
+ onDragEnd={handleDragEnd}
250
+ >
251
+ <SortableContext items={items} strategy={strategy}>
252
+ <div className={cn(strategy === verticalListSortingStrategy && 'space-y-2', className)}>
253
+ {items.map((item) => (
254
+ <SortableItem
255
+ key={item.id}
256
+ id={item.id}
257
+ withHandle={withHandle}
258
+ className={itemClassName}
259
+ >
260
+ {renderItem(item, item.id === activeId)}
261
+ </SortableItem>
262
+ ))}
263
+ </div>
264
+ </SortableContext>
265
+
266
+ <DragOverlay>
267
+ {activeId && activeItem ? (
268
+ <div className="opacity-80 shadow-lg">
269
+ {renderItem(activeItem, true)}
270
+ </div>
271
+ ) : null}
272
+ </DragOverlay>
273
+ </DndContext>
274
+ );
275
+ }
276
+
277
+ /**
278
+ * Props for DragDropTable component
279
+ */
280
+ export interface DragDropTableProps<T extends DragDropItem> {
281
+ /**
282
+ * Array of items to display
283
+ */
284
+ items: T[];
285
+ /**
286
+ * Callback fired when items are reordered
287
+ */
288
+ onReorder: (items: T[]) => void;
289
+ /**
290
+ * Table columns configuration
291
+ */
292
+ columns: {
293
+ key: string;
294
+ header: string;
295
+ render?: (item: T) => React.ReactNode;
296
+ }[];
297
+ /**
298
+ * Custom class name for the table
299
+ */
300
+ className?: string;
301
+ }
302
+
303
+ /**
304
+ * DragDropTable - Sortable table with draggable rows
305
+ *
306
+ * @example
307
+ * ```tsx
308
+ * <DragDropTable
309
+ * items={data}
310
+ * onReorder={setData}
311
+ * columns={[
312
+ * { key: 'name', header: 'Name' },
313
+ * { key: 'email', header: 'Email' },
314
+ * ]}
315
+ * />
316
+ * ```
317
+ */
318
+ export function DragDropTable<T extends DragDropItem>({
319
+ items,
320
+ onReorder,
321
+ columns,
322
+ className,
323
+ }: DragDropTableProps<T>) {
324
+ const [activeId, setActiveId] = React.useState<string | null>(null);
325
+
326
+ const sensors = useSensors(
327
+ useSensor(PointerSensor, {
328
+ activationConstraint: {
329
+ distance: 5,
330
+ },
331
+ }),
332
+ useSensor(KeyboardSensor, {
333
+ coordinateGetter: sortableKeyboardCoordinates,
334
+ })
335
+ );
336
+
337
+ const handleDragStart = (event: DragStartEvent) => {
338
+ setActiveId(event.active.id as string);
339
+ };
340
+
341
+ const handleDragEnd = (event: DragEndEvent) => {
342
+ const { active, over } = event;
343
+
344
+ if (over && active.id !== over.id) {
345
+ const oldIndex = items.findIndex((item) => item.id === active.id);
346
+ const newIndex = items.findIndex((item) => item.id === over.id);
347
+ onReorder(arrayMove(items, oldIndex, newIndex));
348
+ }
349
+
350
+ setActiveId(null);
351
+ };
352
+
353
+ return (
354
+ <DndContext
355
+ sensors={sensors}
356
+ collisionDetection={closestCenter}
357
+ onDragStart={handleDragStart}
358
+ onDragEnd={handleDragEnd}
359
+ >
360
+ <div className={cn('overflow-x-auto', className)}>
361
+ <table className="w-full border-collapse">
362
+ <thead>
363
+ <tr className="border-b border-[var(--color-border)]">
364
+ <th className="w-12"></th>
365
+ {columns.map((column) => (
366
+ <th
367
+ key={column.key}
368
+ className="text-left p-3 text-sm font-medium text-[var(--color-text-secondary)]"
369
+ >
370
+ {column.header}
371
+ </th>
372
+ ))}
373
+ </tr>
374
+ </thead>
375
+ <tbody>
376
+ <SortableContext items={items} strategy={verticalListSortingStrategy}>
377
+ {items.map((item) => (
378
+ <TableRow
379
+ key={item.id}
380
+ item={item}
381
+ columns={columns}
382
+ isDragging={item.id === activeId}
383
+ />
384
+ ))}
385
+ </SortableContext>
386
+ </tbody>
387
+ </table>
388
+ </div>
389
+ </DndContext>
390
+ );
391
+ }
392
+
393
+ interface TableRowProps<T extends DragDropItem> {
394
+ item: T;
395
+ columns: { key: string; header: string; render?: (item: T) => React.ReactNode }[];
396
+ isDragging: boolean;
397
+ }
398
+
399
+ function TableRow<T extends DragDropItem>({ item, columns, isDragging }: TableRowProps<T>) {
400
+ const {
401
+ attributes,
402
+ listeners,
403
+ setNodeRef,
404
+ transform,
405
+ transition,
406
+ } = useSortable({ id: item.id });
407
+
408
+ const style = {
409
+ transform: CSS.Transform.toString(transform),
410
+ transition,
411
+ opacity: isDragging ? 0.5 : 1,
412
+ };
413
+
414
+ return (
415
+ <tr
416
+ ref={setNodeRef}
417
+ style={style}
418
+ className={cn(
419
+ 'border-b border-[var(--color-border)] hover:bg-[var(--color-hover)]',
420
+ 'transition-colors',
421
+ isDragging && 'bg-[var(--color-active)]'
422
+ )}
423
+ >
424
+ <td className="p-3">
425
+ <div
426
+ {...attributes}
427
+ {...listeners}
428
+ className="cursor-grab active:cursor-grabbing text-[var(--color-text-secondary)] hover:text-[var(--color-foreground)] transition-colors"
429
+ >
430
+ <GripVertical className="w-4 h-4" />
431
+ </div>
432
+ </td>
433
+ {columns.map((column) => (
434
+ <td key={column.key} className="p-3 text-sm">
435
+ {column.render ? column.render(item) : (item as any)[column.key]}
436
+ </td>
437
+ ))}
438
+ </tr>
439
+ );
440
+ }
@@ -0,0 +1,252 @@
1
+ 'use client'
2
+
3
+ import * as React from "react"
4
+ import { useDropzone, type Accept, type FileRejection } from "react-dropzone"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+ import { cn } from "../../lib/utils"
7
+
8
+ const fileUploadZoneVariants = cva(
9
+ "relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed transition-colors cursor-pointer",
10
+ {
11
+ variants: {
12
+ state: {
13
+ idle: "border-border bg-muted/30 hover:border-primary/50 hover:bg-muted/50",
14
+ active: "border-primary bg-primary/5",
15
+ reject: "border-destructive bg-destructive/5",
16
+ disabled: "border-border/50 bg-muted/20 cursor-not-allowed opacity-60",
17
+ },
18
+ size: {
19
+ sm: "px-4 py-6 gap-1",
20
+ default: "px-6 py-10 gap-2",
21
+ lg: "px-8 py-14 gap-3",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ state: "idle",
26
+ size: "default",
27
+ },
28
+ }
29
+ )
30
+
31
+ export interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onDrop'> {
32
+ /** Accepted file types (MIME types) */
33
+ accept?: Accept
34
+ /** Max file size in bytes */
35
+ maxSize?: number
36
+ /** Max number of files */
37
+ maxFiles?: number
38
+ /** Allow multiple file selection */
39
+ multiple?: boolean
40
+ /** Disabled state */
41
+ disabled?: boolean
42
+ /** Called when valid files are dropped/selected */
43
+ onFilesSelected?: (files: File[]) => void
44
+ /** Called when files are rejected */
45
+ onFilesRejected?: (rejections: FileRejection[]) => void
46
+ /** Label text */
47
+ label?: string
48
+ /** Description text shown in the drop zone */
49
+ description?: string
50
+ /** Size variant */
51
+ size?: "sm" | "default" | "lg"
52
+ }
53
+
54
+ const UploadIcon = () => (
55
+ <svg
56
+ xmlns="http://www.w3.org/2000/svg"
57
+ width="24"
58
+ height="24"
59
+ viewBox="0 0 24 24"
60
+ fill="none"
61
+ stroke="currentColor"
62
+ strokeWidth="2"
63
+ strokeLinecap="round"
64
+ strokeLinejoin="round"
65
+ aria-hidden="true"
66
+ className="text-foreground-secondary"
67
+ >
68
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
69
+ <polyline points="17 8 12 3 7 8" />
70
+ <line x1="12" y1="3" x2="12" y2="15" />
71
+ </svg>
72
+ )
73
+
74
+ const FileIcon = () => (
75
+ <svg
76
+ xmlns="http://www.w3.org/2000/svg"
77
+ width="16"
78
+ height="16"
79
+ viewBox="0 0 24 24"
80
+ fill="none"
81
+ stroke="currentColor"
82
+ strokeWidth="2"
83
+ strokeLinecap="round"
84
+ strokeLinejoin="round"
85
+ aria-hidden="true"
86
+ >
87
+ <path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
88
+ <path d="M14 2v4a2 2 0 0 0 2 2h4" />
89
+ </svg>
90
+ )
91
+
92
+ const XIcon = () => (
93
+ <svg
94
+ xmlns="http://www.w3.org/2000/svg"
95
+ width="14"
96
+ height="14"
97
+ viewBox="0 0 24 24"
98
+ fill="none"
99
+ stroke="currentColor"
100
+ strokeWidth="2"
101
+ strokeLinecap="round"
102
+ strokeLinejoin="round"
103
+ aria-hidden="true"
104
+ >
105
+ <line x1="18" y1="6" x2="6" y2="18" />
106
+ <line x1="6" y1="6" x2="18" y2="18" />
107
+ </svg>
108
+ )
109
+
110
+ function formatFileSize(bytes: number): string {
111
+ if (bytes === 0) return "0 B"
112
+ const k = 1024
113
+ const sizes = ["B", "KB", "MB", "GB"]
114
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
115
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
116
+ }
117
+
118
+ function FileUpload({
119
+ className,
120
+ accept,
121
+ maxSize,
122
+ maxFiles = 0,
123
+ multiple = false,
124
+ disabled = false,
125
+ onFilesSelected,
126
+ onFilesRejected,
127
+ label,
128
+ description,
129
+ size = "default",
130
+ ...props
131
+ }: FileUploadProps) {
132
+ const [files, setFiles] = React.useState<File[]>([])
133
+ const [errors, setErrors] = React.useState<string[]>([])
134
+
135
+ const onDrop = React.useCallback(
136
+ (acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
137
+ if (acceptedFiles.length > 0) {
138
+ const newFiles = multiple ? [...files, ...acceptedFiles] : acceptedFiles
139
+ setFiles(newFiles)
140
+ onFilesSelected?.(newFiles)
141
+ }
142
+ if (rejectedFiles.length > 0) {
143
+ const errorMessages = rejectedFiles.flatMap((rejection) =>
144
+ rejection.errors.map((err) => `${rejection.file.name}: ${err.message}`)
145
+ )
146
+ setErrors(errorMessages)
147
+ onFilesRejected?.(rejectedFiles)
148
+ } else {
149
+ setErrors([])
150
+ }
151
+ },
152
+ [files, multiple, onFilesSelected, onFilesRejected]
153
+ )
154
+
155
+ const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({
156
+ onDrop,
157
+ accept,
158
+ maxSize,
159
+ maxFiles: maxFiles > 0 ? maxFiles : undefined,
160
+ multiple,
161
+ disabled,
162
+ })
163
+
164
+ const removeFile = (index: number) => {
165
+ const newFiles = files.filter((_, i) => i !== index)
166
+ setFiles(newFiles)
167
+ onFilesSelected?.(newFiles)
168
+ }
169
+
170
+ const state = disabled
171
+ ? "disabled"
172
+ : isDragReject
173
+ ? "reject"
174
+ : isDragActive
175
+ ? "active"
176
+ : "idle"
177
+
178
+ return (
179
+ <div data-slot="file-upload" className={cn("space-y-3", className)} {...props}>
180
+ {label && (
181
+ <p className="text-sm font-medium text-foreground">{label}</p>
182
+ )}
183
+
184
+ <div
185
+ {...getRootProps()}
186
+ className={cn(fileUploadZoneVariants({ state, size }))}
187
+ role="button"
188
+ aria-label={label ?? "Upload files"}
189
+ >
190
+ <input {...getInputProps()} />
191
+ <UploadIcon />
192
+ <p className="text-sm font-medium text-foreground">
193
+ {isDragActive
194
+ ? "Drop files here"
195
+ : "Drag & drop files here, or click to browse"}
196
+ </p>
197
+ {description && (
198
+ <p className="text-xs text-foreground-secondary">{description}</p>
199
+ )}
200
+ {maxSize && (
201
+ <p className="text-xs text-foreground-secondary">
202
+ Max size: {formatFileSize(maxSize)}
203
+ </p>
204
+ )}
205
+ </div>
206
+
207
+ {/* Error messages */}
208
+ {errors.length > 0 && (
209
+ <div className="space-y-1" role="alert">
210
+ {errors.map((error, i) => (
211
+ <p key={i} className="text-sm text-destructive">{error}</p>
212
+ ))}
213
+ </div>
214
+ )}
215
+
216
+ {/* File list */}
217
+ {files.length > 0 && (
218
+ <ul data-slot="file-upload-list" className="space-y-2">
219
+ {files.map((file, index) => (
220
+ <li
221
+ key={`${file.name}-${index}`}
222
+ className="flex items-center gap-3 rounded-lg border border-border bg-surface p-3"
223
+ >
224
+ <FileIcon />
225
+ <div className="flex-1 min-w-0">
226
+ <p className="text-sm font-medium text-foreground truncate">
227
+ {file.name}
228
+ </p>
229
+ <p className="text-xs text-foreground-secondary">
230
+ {formatFileSize(file.size)}
231
+ </p>
232
+ </div>
233
+ <button
234
+ type="button"
235
+ onClick={(e) => {
236
+ e.stopPropagation()
237
+ removeFile(index)
238
+ }}
239
+ className="shrink-0 rounded-xs p-1 text-foreground-secondary hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
240
+ aria-label={`Remove ${file.name}`}
241
+ >
242
+ <XIcon />
243
+ </button>
244
+ </li>
245
+ ))}
246
+ </ul>
247
+ )}
248
+ </div>
249
+ )
250
+ }
251
+
252
+ export { FileUpload, fileUploadZoneVariants }