@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.
- package/.claude/CLAUDE.md +239 -0
- package/README.md +161 -0
- package/dist/cli.mjs +151 -0
- package/dist/dates.d.mts +20 -0
- package/dist/dates.d.ts +20 -0
- package/dist/dates.js +240 -0
- package/dist/dates.js.map +1 -0
- package/dist/dates.mjs +203 -0
- package/dist/dates.mjs.map +1 -0
- package/dist/dnd.d.mts +126 -0
- package/dist/dnd.d.ts +126 -0
- package/dist/dnd.js +274 -0
- package/dist/dnd.js.map +1 -0
- package/dist/dnd.mjs +250 -0
- package/dist/dnd.mjs.map +1 -0
- package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
- package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
- package/dist/forms.d.mts +38 -0
- package/dist/forms.d.ts +38 -0
- package/dist/forms.js +198 -0
- package/dist/forms.js.map +1 -0
- package/dist/forms.mjs +159 -0
- package/dist/forms.mjs.map +1 -0
- package/dist/hooks-1b8WaQf1.d.mts +225 -0
- package/dist/hooks-CKW8vE9H.d.ts +225 -0
- package/dist/hooks.d.mts +3 -0
- package/dist/hooks.d.ts +3 -0
- package/dist/hooks.js +971 -0
- package/dist/hooks.js.map +1 -0
- package/dist/hooks.mjs +943 -0
- package/dist/hooks.mjs.map +1 -0
- package/dist/index-DscTIrZ2.d.mts +29 -0
- package/dist/index-DscTIrZ2.d.ts +29 -0
- package/dist/index.d.mts +3382 -0
- package/dist/index.d.ts +3382 -0
- package/dist/index.js +15146 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +14802 -0
- package/dist/index.mjs.map +1 -0
- package/dist/providers-CXPDMsl7.d.mts +30 -0
- package/dist/providers-Dn_Msjvz.d.ts +30 -0
- package/dist/providers.d.mts +3 -0
- package/dist/providers.d.ts +3 -0
- package/dist/providers.js +1885 -0
- package/dist/providers.js.map +1 -0
- package/dist/providers.mjs +1859 -0
- package/dist/providers.mjs.map +1 -0
- package/dist/tables.d.mts +10 -0
- package/dist/tables.d.ts +10 -0
- package/dist/tables.js +248 -0
- package/dist/tables.js.map +1 -0
- package/dist/tables.mjs +218 -0
- package/dist/tables.mjs.map +1 -0
- package/dist/tokens.d.mts +1065 -0
- package/dist/tokens.d.ts +1065 -0
- package/dist/tokens.js +2637 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tokens.mjs +2555 -0
- package/dist/tokens.mjs.map +1 -0
- package/dist/utils-CIIM7dAC.d.ts +986 -0
- package/dist/utils-Cs04sxth.d.mts +986 -0
- package/dist/utils.d.mts +4 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +874 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +806 -0
- package/dist/utils.mjs.map +1 -0
- package/dist/validation-Bj1ye-v_.d.mts +114 -0
- package/dist/validation-Bj1ye-v_.d.ts +114 -0
- package/dist/webgl.d.mts +104 -0
- package/dist/webgl.d.ts +104 -0
- package/dist/webgl.js +226 -0
- package/dist/webgl.js.map +1 -0
- package/dist/webgl.mjs +195 -0
- package/dist/webgl.mjs.map +1 -0
- package/package.json +267 -0
- package/src/cli.ts +206 -0
- package/src/component-registry.ts +183 -0
- package/src/components/actions/Button.test.tsx +61 -0
- package/src/components/actions/Button.tsx +70 -0
- package/src/components/actions/Link.tsx +78 -0
- package/src/components/actions/Magnetic.tsx +68 -0
- package/src/components/actions/Toggle.test.tsx +40 -0
- package/src/components/actions/Toggle.tsx +47 -0
- package/src/components/actions/ToggleGroup.tsx +70 -0
- package/src/components/actions/index.ts +5 -0
- package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
- package/src/components/backgrounds/OrbBackground.tsx +424 -0
- package/src/components/backgrounds/WarpBackground.tsx +358 -0
- package/src/components/backgrounds/index.ts +3 -0
- package/src/components/blocks/Hero.tsx +142 -0
- package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
- package/src/components/cursor/SplashCursor.tsx +1315 -0
- package/src/components/cursor/TargetCursor.tsx +187 -0
- package/src/components/cursor/index.ts +2 -0
- package/src/components/data-display/AspectImage.tsx +73 -0
- package/src/components/data-display/Avatar.test.tsx +35 -0
- package/src/components/data-display/Avatar.tsx +55 -0
- package/src/components/data-display/Badge.test.tsx +43 -0
- package/src/components/data-display/Badge.tsx +84 -0
- package/src/components/data-display/Brand.tsx +123 -0
- package/src/components/data-display/Calendar.tsx +70 -0
- package/src/components/data-display/Card.test.tsx +92 -0
- package/src/components/data-display/Card.tsx +115 -0
- package/src/components/data-display/Code.tsx +210 -0
- package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
- package/src/components/data-display/DataTable.tsx +119 -0
- package/src/components/data-display/DescriptionList.tsx +41 -0
- package/src/components/data-display/GitHubIcon.tsx +44 -0
- package/src/components/data-display/Heading.test.tsx +36 -0
- package/src/components/data-display/Heading.tsx +83 -0
- package/src/components/data-display/StatCard.tsx +195 -0
- package/src/components/data-display/Table.tsx +133 -0
- package/src/components/data-display/Text.test.tsx +48 -0
- package/src/components/data-display/Text.tsx +144 -0
- package/src/components/data-display/Timeline.tsx +194 -0
- package/src/components/data-display/TreeView.tsx +226 -0
- package/src/components/data-display/Typewriter.tsx +119 -0
- package/src/components/data-display/VariableWeightText.tsx +130 -0
- package/src/components/data-display/index.ts +19 -0
- package/src/components/feedback/Alert.test.tsx +44 -0
- package/src/components/feedback/Alert.tsx +65 -0
- package/src/components/feedback/EmptyState.tsx +113 -0
- package/src/components/feedback/Progress.test.tsx +60 -0
- package/src/components/feedback/Progress.tsx +30 -0
- package/src/components/feedback/ProgressBar.tsx +158 -0
- package/src/components/feedback/Skeleton.test.tsx +39 -0
- package/src/components/feedback/Skeleton.tsx +45 -0
- package/src/components/feedback/Sonner.tsx +28 -0
- package/src/components/feedback/Spinner.test.tsx +33 -0
- package/src/components/feedback/Spinner.tsx +99 -0
- package/src/components/feedback/Stepper.tsx +307 -0
- package/src/components/feedback/Toast/Toast.tsx +243 -0
- package/src/components/feedback/Toast/index.ts +2 -0
- package/src/components/feedback/index.ts +9 -0
- package/src/components/forms/Checkbox.test.tsx +40 -0
- package/src/components/forms/Checkbox.tsx +31 -0
- package/src/components/forms/ColorPicker.tsx +118 -0
- package/src/components/forms/Combobox.tsx +96 -0
- package/src/components/forms/DragDrop.tsx +440 -0
- package/src/components/forms/FileUpload.tsx +252 -0
- package/src/components/forms/FilterButton.tsx +65 -0
- package/src/components/forms/Form.tsx +197 -0
- package/src/components/forms/Input.test.tsx +46 -0
- package/src/components/forms/Input.tsx +43 -0
- package/src/components/forms/InputOTP.tsx +81 -0
- package/src/components/forms/Label.test.tsx +20 -0
- package/src/components/forms/Label.tsx +25 -0
- package/src/components/forms/RadioGroup.tsx +51 -0
- package/src/components/forms/SearchBar.tsx +215 -0
- package/src/components/forms/Select.test.tsx +118 -0
- package/src/components/forms/Select.tsx +274 -0
- package/src/components/forms/Slider.tsx +29 -0
- package/src/components/forms/Switch.test.tsx +76 -0
- package/src/components/forms/Switch.tsx +30 -0
- package/src/components/forms/TextField.tsx +152 -0
- package/src/components/forms/Textarea.test.tsx +41 -0
- package/src/components/forms/Textarea.tsx +29 -0
- package/src/components/forms/ThemeSwitcher.tsx +290 -0
- package/src/components/forms/ThemeToggle.tsx +151 -0
- package/src/components/forms/index.ts +19 -0
- package/src/components/layout/Accordion.test.tsx +66 -0
- package/src/components/layout/Accordion.tsx +64 -0
- package/src/components/layout/AspectRatio.tsx +7 -0
- package/src/components/layout/Carousel.tsx +277 -0
- package/src/components/layout/Collapsible.test.tsx +40 -0
- package/src/components/layout/Collapsible.tsx +31 -0
- package/src/components/layout/Container.test.tsx +45 -0
- package/src/components/layout/Container.tsx +99 -0
- package/src/components/layout/CustomizerPanel.tsx +400 -0
- package/src/components/layout/DatePicker.tsx +57 -0
- package/src/components/layout/Footer/Footer.tsx +175 -0
- package/src/components/layout/Footer/index.ts +2 -0
- package/src/components/layout/GlassSurface.tsx +82 -0
- package/src/components/layout/Grid.test.tsx +31 -0
- package/src/components/layout/Grid.tsx +130 -0
- package/src/components/layout/Header/Header.tsx +450 -0
- package/src/components/layout/Header/index.ts +2 -0
- package/src/components/layout/PageLayout.tsx +180 -0
- package/src/components/layout/PageTemplate.tsx +158 -0
- package/src/components/layout/Resizable.tsx +48 -0
- package/src/components/layout/ScrollArea.tsx +53 -0
- package/src/components/layout/Separator.test.tsx +28 -0
- package/src/components/layout/Separator.tsx +29 -0
- package/src/components/layout/Sidebar.tsx +171 -0
- package/src/components/layout/Stack.test.tsx +41 -0
- package/src/components/layout/Stack.tsx +89 -0
- package/src/components/layout/glass-surface.css +60 -0
- package/src/components/layout/index.ts +18 -0
- package/src/components/motion/AnimatedBeam.tsx +159 -0
- package/src/components/navigation/Breadcrumb.test.tsx +57 -0
- package/src/components/navigation/Breadcrumb.tsx +119 -0
- package/src/components/navigation/Breadcrumbs.tsx +221 -0
- package/src/components/navigation/Command.tsx +159 -0
- package/src/components/navigation/Menubar.tsx +115 -0
- package/src/components/navigation/NavLink.tsx +55 -0
- package/src/components/navigation/NavigationMenu.tsx +125 -0
- package/src/components/navigation/Pagination.tsx +121 -0
- package/src/components/navigation/SecondaryNav.tsx +100 -0
- package/src/components/navigation/Tabs.test.tsx +47 -0
- package/src/components/navigation/Tabs.tsx +60 -0
- package/src/components/navigation/TertiaryNav.tsx +90 -0
- package/src/components/navigation/index.ts +10 -0
- package/src/components/overlays/AlertDialog.test.tsx +69 -0
- package/src/components/overlays/AlertDialog.tsx +166 -0
- package/src/components/overlays/ContextMenu.tsx +243 -0
- package/src/components/overlays/Dialog.test.tsx +79 -0
- package/src/components/overlays/Dialog.tsx +158 -0
- package/src/components/overlays/Drawer.tsx +128 -0
- package/src/components/overlays/Dropdown.tsx +253 -0
- package/src/components/overlays/DropdownMenu.tsx +242 -0
- package/src/components/overlays/HoverCard.tsx +32 -0
- package/src/components/overlays/Modal.tsx +250 -0
- package/src/components/overlays/NotificationCenter.tsx +364 -0
- package/src/components/overlays/Popover.test.tsx +40 -0
- package/src/components/overlays/Popover.tsx +46 -0
- package/src/components/overlays/Sheet.tsx +163 -0
- package/src/components/overlays/Tooltip.test.tsx +33 -0
- package/src/components/overlays/Tooltip.tsx +32 -0
- package/src/components/overlays/index.ts +12 -0
- package/src/dates.ts +2 -0
- package/src/dnd.ts +1 -0
- package/src/forms.ts +1 -0
- package/src/globals.css +187 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useForm.ts +247 -0
- package/src/hooks/useMotionPreference.test.ts +102 -0
- package/src/hooks/useMotionPreference.ts +78 -0
- package/src/hooks/useTheme.ts +58 -0
- package/src/hooks.ts +9 -0
- package/src/index.ts +168 -0
- package/src/lib/animations.ts +356 -0
- package/src/lib/breadcrumbs.ts +94 -0
- package/src/lib/colors.ts +493 -0
- package/src/lib/store/customizer.ts +482 -0
- package/src/lib/store/index.ts +3 -0
- package/src/lib/store/theme.ts +55 -0
- package/src/lib/syntax-parser/index.ts +50 -0
- package/src/lib/syntax-parser/patterns.ts +64 -0
- package/src/lib/syntax-parser/tokenizer.ts +117 -0
- package/src/lib/syntax-parser/types.ts +27 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation.ts +204 -0
- package/src/lib/webgl/Color.ts +11 -0
- package/src/lib/webgl/Mesh.ts +41 -0
- package/src/lib/webgl/Program.ts +118 -0
- package/src/lib/webgl/Renderer.ts +51 -0
- package/src/lib/webgl/Triangle.ts +27 -0
- package/src/lib/webgl/Vec3.ts +18 -0
- package/src/lib/webgl/index.ts +13 -0
- package/src/nativewind-env.d.ts +1 -0
- package/src/providers/ThemeProvider.tsx +461 -0
- package/src/providers/index.ts +1 -0
- package/src/providers.ts +7 -0
- package/src/tables.ts +1 -0
- package/src/test/setup.ts +39 -0
- package/src/theme.css +158 -0
- package/src/tokens.ts +7 -0
- package/src/utils.ts +12 -0
- 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 }
|