@djangocfg/ui-core 1.0.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/README.md +135 -0
- package/package.json +111 -0
- package/src/components/accordion.tsx +56 -0
- package/src/components/alert-dialog.tsx +142 -0
- package/src/components/alert.tsx +59 -0
- package/src/components/aspect-ratio.tsx +7 -0
- package/src/components/avatar.tsx +50 -0
- package/src/components/badge.tsx +36 -0
- package/src/components/button-group.tsx +85 -0
- package/src/components/button.tsx +111 -0
- package/src/components/calendar.tsx +213 -0
- package/src/components/card.tsx +76 -0
- package/src/components/carousel.tsx +261 -0
- package/src/components/chart.tsx +369 -0
- package/src/components/checkbox.tsx +29 -0
- package/src/components/collapsible.tsx +11 -0
- package/src/components/combobox.tsx +182 -0
- package/src/components/command.tsx +170 -0
- package/src/components/context-menu.tsx +200 -0
- package/src/components/copy.tsx +144 -0
- package/src/components/dialog.tsx +122 -0
- package/src/components/drawer.tsx +137 -0
- package/src/components/empty.tsx +104 -0
- package/src/components/field.tsx +244 -0
- package/src/components/form.tsx +178 -0
- package/src/components/hover-card.tsx +29 -0
- package/src/components/image-with-fallback.tsx +170 -0
- package/src/components/index.ts +86 -0
- package/src/components/input-group.tsx +170 -0
- package/src/components/input-otp.tsx +81 -0
- package/src/components/input.tsx +22 -0
- package/src/components/item.tsx +195 -0
- package/src/components/kbd.tsx +28 -0
- package/src/components/label.tsx +26 -0
- package/src/components/multi-select.tsx +222 -0
- package/src/components/og-image.tsx +47 -0
- package/src/components/popover.tsx +33 -0
- package/src/components/portal.tsx +106 -0
- package/src/components/preloader.tsx +250 -0
- package/src/components/progress.tsx +28 -0
- package/src/components/radio-group.tsx +43 -0
- package/src/components/resizable.tsx +111 -0
- package/src/components/scroll-area.tsx +102 -0
- package/src/components/section.tsx +58 -0
- package/src/components/select.tsx +158 -0
- package/src/components/separator.tsx +31 -0
- package/src/components/sheet.tsx +140 -0
- package/src/components/skeleton.tsx +15 -0
- package/src/components/slider.tsx +28 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/sticky.tsx +117 -0
- package/src/components/switch.tsx +29 -0
- package/src/components/table.tsx +120 -0
- package/src/components/tabs.tsx +238 -0
- package/src/components/textarea.tsx +22 -0
- package/src/components/toast.tsx +129 -0
- package/src/components/toaster.tsx +41 -0
- package/src/components/toggle-group.tsx +61 -0
- package/src/components/toggle.tsx +45 -0
- package/src/components/token-icon.tsx +156 -0
- package/src/components/tooltip-provider-safe.tsx +43 -0
- package/src/components/tooltip.tsx +32 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/useCopy.ts +41 -0
- package/src/hooks/useCountdown.ts +73 -0
- package/src/hooks/useDebounce.ts +25 -0
- package/src/hooks/useDebouncedCallback.ts +58 -0
- package/src/hooks/useDebugTools.ts +52 -0
- package/src/hooks/useEventsBus.ts +53 -0
- package/src/hooks/useImageLoader.ts +95 -0
- package/src/hooks/useMediaQuery.ts +40 -0
- package/src/hooks/useMobile.tsx +22 -0
- package/src/hooks/useToast.ts +194 -0
- package/src/index.ts +14 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/og-image.ts +151 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/base.css +20 -0
- package/src/styles/globals.css +12 -0
- package/src/styles/index.css +25 -0
- package/src/styles/sources.css +11 -0
- package/src/styles/theme/animations.css +65 -0
- package/src/styles/theme/dark.css +49 -0
- package/src/styles/theme/light.css +50 -0
- package/src/styles/theme/tokens.css +134 -0
- package/src/styles/theme.css +22 -0
- package/src/styles/utilities.css +187 -0
- package/src/types/index.ts +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preloader Component
|
|
3
|
+
*
|
|
4
|
+
* Universal loading indicator component with multiple variants
|
|
5
|
+
* Supports inline, fullscreen, and custom layouts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { Loader2 } from 'lucide-react';
|
|
12
|
+
import { cn } from '../lib/utils';
|
|
13
|
+
import { Spinner } from './spinner';
|
|
14
|
+
|
|
15
|
+
export interface PreloaderProps {
|
|
16
|
+
/**
|
|
17
|
+
* Loading text to display
|
|
18
|
+
* @default undefined (no text)
|
|
19
|
+
*/
|
|
20
|
+
text?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Additional description/subtitle text
|
|
24
|
+
*/
|
|
25
|
+
description?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Size variant
|
|
29
|
+
* @default 'md'
|
|
30
|
+
*/
|
|
31
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Variant: inline (fits container) or fullscreen (fixed overlay)
|
|
35
|
+
* @default 'inline'
|
|
36
|
+
*/
|
|
37
|
+
variant?: 'inline' | 'fullscreen';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Show backdrop (only for fullscreen variant)
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
backdrop?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Backdrop opacity (0-100, only for fullscreen with backdrop)
|
|
47
|
+
* @default 80
|
|
48
|
+
*/
|
|
49
|
+
backdropOpacity?: number;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Additional CSS classes
|
|
53
|
+
*/
|
|
54
|
+
className?: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Spinner className override
|
|
58
|
+
*/
|
|
59
|
+
spinnerClassName?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const sizeMap = {
|
|
63
|
+
sm: 'h-4 w-4',
|
|
64
|
+
md: 'h-6 w-6',
|
|
65
|
+
lg: 'h-8 w-8',
|
|
66
|
+
xl: 'h-12 w-12',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Preloader - Universal loading indicator
|
|
71
|
+
*
|
|
72
|
+
* Features:
|
|
73
|
+
* - Multiple size variants (sm, md, lg, xl)
|
|
74
|
+
* - Inline or fullscreen variants
|
|
75
|
+
* - Optional text and description
|
|
76
|
+
* - Optional backdrop for fullscreen
|
|
77
|
+
* - Accessible (ARIA labels)
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```tsx
|
|
81
|
+
* // Inline loading
|
|
82
|
+
* <Preloader text="Loading..." />
|
|
83
|
+
*
|
|
84
|
+
* // Fullscreen with backdrop
|
|
85
|
+
* <Preloader variant="fullscreen" text="Loading data..." />
|
|
86
|
+
*
|
|
87
|
+
* // Custom size
|
|
88
|
+
* <Preloader size="lg" text="Processing..." />
|
|
89
|
+
*
|
|
90
|
+
* // Without text
|
|
91
|
+
* <Preloader />
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function Preloader({
|
|
95
|
+
text,
|
|
96
|
+
description,
|
|
97
|
+
size = 'md',
|
|
98
|
+
variant = 'inline',
|
|
99
|
+
backdrop = true,
|
|
100
|
+
backdropOpacity = 80,
|
|
101
|
+
className,
|
|
102
|
+
spinnerClassName,
|
|
103
|
+
}: PreloaderProps) {
|
|
104
|
+
const spinnerSize = sizeMap[size];
|
|
105
|
+
|
|
106
|
+
// Fullscreen variant
|
|
107
|
+
if (variant === 'fullscreen') {
|
|
108
|
+
const backdropClasses = backdrop
|
|
109
|
+
? backdropOpacity >= 90
|
|
110
|
+
? 'bg-background/95'
|
|
111
|
+
: backdropOpacity >= 70
|
|
112
|
+
? 'bg-background/90'
|
|
113
|
+
: backdropOpacity >= 50
|
|
114
|
+
? 'bg-background/80'
|
|
115
|
+
: 'bg-background/60'
|
|
116
|
+
: 'bg-transparent';
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
className={cn(
|
|
121
|
+
'fixed inset-0 z-50 flex items-center justify-center',
|
|
122
|
+
backdropClasses,
|
|
123
|
+
className
|
|
124
|
+
)}
|
|
125
|
+
role="status"
|
|
126
|
+
aria-label={text || 'Loading'}
|
|
127
|
+
aria-live="polite"
|
|
128
|
+
>
|
|
129
|
+
<div className="flex flex-col items-center gap-4 animate-in fade-in duration-300">
|
|
130
|
+
{/* Spinner */}
|
|
131
|
+
<Loader2
|
|
132
|
+
className={cn('animate-spin text-primary', spinnerSize, spinnerClassName)}
|
|
133
|
+
aria-hidden="true"
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
{/* Text content */}
|
|
137
|
+
{(text || description) && (
|
|
138
|
+
<div className="flex flex-col items-center gap-2 text-center">
|
|
139
|
+
{text && (
|
|
140
|
+
<p className="text-base font-medium text-foreground">{text}</p>
|
|
141
|
+
)}
|
|
142
|
+
{description && (
|
|
143
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
144
|
+
)}
|
|
145
|
+
{/* Animated dots */}
|
|
146
|
+
{text && (
|
|
147
|
+
<div className="flex gap-1 mt-1">
|
|
148
|
+
<span
|
|
149
|
+
className="h-1.5 w-1.5 animate-bounce rounded-full bg-primary"
|
|
150
|
+
style={{ animationDelay: '0ms' }}
|
|
151
|
+
/>
|
|
152
|
+
<span
|
|
153
|
+
className="h-1.5 w-1.5 animate-bounce rounded-full bg-primary"
|
|
154
|
+
style={{ animationDelay: '150ms' }}
|
|
155
|
+
/>
|
|
156
|
+
<span
|
|
157
|
+
className="h-1.5 w-1.5 animate-bounce rounded-full bg-primary"
|
|
158
|
+
style={{ animationDelay: '300ms' }}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Inline variant
|
|
170
|
+
return (
|
|
171
|
+
<div
|
|
172
|
+
className={cn(
|
|
173
|
+
'flex items-center justify-center',
|
|
174
|
+
text || description ? 'gap-3' : '',
|
|
175
|
+
className
|
|
176
|
+
)}
|
|
177
|
+
role="status"
|
|
178
|
+
aria-label={text || 'Loading'}
|
|
179
|
+
aria-live="polite"
|
|
180
|
+
>
|
|
181
|
+
<Loader2
|
|
182
|
+
className={cn('animate-spin text-primary', spinnerSize, spinnerClassName)}
|
|
183
|
+
aria-hidden="true"
|
|
184
|
+
/>
|
|
185
|
+
{(text || description) && (
|
|
186
|
+
<div className="flex flex-col gap-1">
|
|
187
|
+
{text && (
|
|
188
|
+
<p className="text-sm font-medium text-foreground">{text}</p>
|
|
189
|
+
)}
|
|
190
|
+
{description && (
|
|
191
|
+
<p className="text-xs text-muted-foreground">{description}</p>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* PreloaderSkeleton - Loading skeleton variant
|
|
201
|
+
* Useful for content placeholders
|
|
202
|
+
*/
|
|
203
|
+
export interface PreloaderSkeletonProps {
|
|
204
|
+
/**
|
|
205
|
+
* Number of skeleton lines
|
|
206
|
+
* @default 3
|
|
207
|
+
*/
|
|
208
|
+
lines?: number;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Show avatar skeleton
|
|
212
|
+
* @default false
|
|
213
|
+
*/
|
|
214
|
+
showAvatar?: boolean;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Additional CSS classes
|
|
218
|
+
*/
|
|
219
|
+
className?: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function PreloaderSkeleton({
|
|
223
|
+
lines = 3,
|
|
224
|
+
showAvatar = false,
|
|
225
|
+
className,
|
|
226
|
+
}: PreloaderSkeletonProps) {
|
|
227
|
+
return (
|
|
228
|
+
<div className={cn('space-y-3', className)}>
|
|
229
|
+
{showAvatar && (
|
|
230
|
+
<div className="flex items-center gap-3">
|
|
231
|
+
<div className="h-10 w-10 rounded-full bg-muted animate-pulse" />
|
|
232
|
+
<div className="space-y-2 flex-1">
|
|
233
|
+
<div className="h-4 w-24 rounded bg-muted animate-pulse" />
|
|
234
|
+
<div className="h-3 w-32 rounded bg-muted animate-pulse" />
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
{Array.from({ length: lines }).map((_, i) => (
|
|
239
|
+
<div
|
|
240
|
+
key={i}
|
|
241
|
+
className={cn(
|
|
242
|
+
'h-4 rounded bg-muted animate-pulse',
|
|
243
|
+
i === lines - 1 ? 'w-3/4' : 'w-full'
|
|
244
|
+
)}
|
|
245
|
+
/>
|
|
246
|
+
))}
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
const Progress = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof ProgressPrimitive.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
|
11
|
+
>(({ className, value, ...props }, ref) => (
|
|
12
|
+
<ProgressPrimitive.Root
|
|
13
|
+
ref={ref}
|
|
14
|
+
className={cn(
|
|
15
|
+
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<ProgressPrimitive.Indicator
|
|
21
|
+
className="h-full w-full flex-1 bg-primary transition-all"
|
|
22
|
+
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
23
|
+
/>
|
|
24
|
+
</ProgressPrimitive.Root>
|
|
25
|
+
))
|
|
26
|
+
Progress.displayName = ProgressPrimitive.Root.displayName
|
|
27
|
+
|
|
28
|
+
export { Progress }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
import { DotFilledIcon } from "@radix-ui/react-icons"
|
|
7
|
+
|
|
8
|
+
const RadioGroup = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
|
11
|
+
>(({ className, ...props }, ref) => {
|
|
12
|
+
return (
|
|
13
|
+
<RadioGroupPrimitive.Root
|
|
14
|
+
className={cn("grid gap-2", className)}
|
|
15
|
+
{...props}
|
|
16
|
+
ref={ref}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
|
21
|
+
|
|
22
|
+
const RadioGroupItem = React.forwardRef<
|
|
23
|
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
24
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> & { key?: React.Key }
|
|
25
|
+
>(({ className, ...props }, ref) => {
|
|
26
|
+
return (
|
|
27
|
+
<RadioGroupPrimitive.Item
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(
|
|
30
|
+
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
36
|
+
<DotFilledIcon className="h-3.5 w-3.5 fill-primary" />
|
|
37
|
+
</RadioGroupPrimitive.Indicator>
|
|
38
|
+
</RadioGroupPrimitive.Item>
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
|
42
|
+
|
|
43
|
+
export { RadioGroup, RadioGroupItem }
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import * as ResizablePrimitive from "react-resizable-panels"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
import { DragHandleDots2Icon } from "@radix-ui/react-icons"
|
|
7
|
+
|
|
8
|
+
const ResizablePanelGroup = ({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
|
12
|
+
<ResizablePrimitive.PanelGroup
|
|
13
|
+
className={cn(
|
|
14
|
+
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const ResizablePanel = ResizablePrimitive.Panel
|
|
22
|
+
|
|
23
|
+
// Size variants for the handle hit area
|
|
24
|
+
const sizeVariants = {
|
|
25
|
+
sm: { hitArea: 4, indicator: 4 }, // 4px hit area, 4px indicator height
|
|
26
|
+
md: { hitArea: 8, indicator: 8 }, // 8px hit area, 8px indicator height
|
|
27
|
+
lg: { hitArea: 12, indicator: 12 }, // 12px hit area, 12px indicator height
|
|
28
|
+
} as const
|
|
29
|
+
|
|
30
|
+
export interface ResizableHandleProps
|
|
31
|
+
extends React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> {
|
|
32
|
+
/** Show classic dots handle icon */
|
|
33
|
+
withHandle?: boolean
|
|
34
|
+
/** Size variant - controls hit area and indicator size */
|
|
35
|
+
size?: keyof typeof sizeVariants
|
|
36
|
+
/** Show visual indicator on hover */
|
|
37
|
+
showIndicator?: boolean
|
|
38
|
+
/** Custom indicator height in pixels (overrides size variant) */
|
|
39
|
+
indicatorHeight?: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ResizableHandle = ({
|
|
43
|
+
withHandle,
|
|
44
|
+
size = "md",
|
|
45
|
+
showIndicator = true,
|
|
46
|
+
indicatorHeight,
|
|
47
|
+
className,
|
|
48
|
+
...props
|
|
49
|
+
}: ResizableHandleProps) => {
|
|
50
|
+
const variant = sizeVariants[size]
|
|
51
|
+
const hitAreaPx = variant.hitArea
|
|
52
|
+
const indicatorPx = indicatorHeight ?? variant.indicator * 4 // 32px default for md
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<ResizablePrimitive.PanelResizeHandle
|
|
56
|
+
className={cn(
|
|
57
|
+
"group/resize relative flex items-center justify-center",
|
|
58
|
+
"bg-border transition-colors",
|
|
59
|
+
"hover:bg-primary/30 active:bg-primary/50",
|
|
60
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
61
|
+
// Horizontal (default)
|
|
62
|
+
"w-px",
|
|
63
|
+
// Vertical
|
|
64
|
+
"data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full",
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
style={{
|
|
68
|
+
// Expand hit area without changing visual width
|
|
69
|
+
// Using padding to increase clickable area
|
|
70
|
+
}}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
{/* Expanded hit area - invisible but clickable */}
|
|
74
|
+
<div
|
|
75
|
+
className={cn(
|
|
76
|
+
"absolute inset-y-0 cursor-ew-resize z-10",
|
|
77
|
+
"data-[panel-group-direction=vertical]:inset-x-0 data-[panel-group-direction=vertical]:inset-y-auto",
|
|
78
|
+
"data-[panel-group-direction=vertical]:cursor-ns-resize"
|
|
79
|
+
)}
|
|
80
|
+
style={{
|
|
81
|
+
left: -hitAreaPx / 2,
|
|
82
|
+
right: -hitAreaPx / 2,
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
{/* Visual indicator - appears on hover */}
|
|
87
|
+
{showIndicator && (
|
|
88
|
+
<div
|
|
89
|
+
className={cn(
|
|
90
|
+
"absolute rounded-full bg-border opacity-0 transition-opacity",
|
|
91
|
+
"group-hover/resize:opacity-100 group-active/resize:bg-primary/50",
|
|
92
|
+
// Horizontal
|
|
93
|
+
"left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-1",
|
|
94
|
+
// Vertical
|
|
95
|
+
"data-[panel-group-direction=vertical]:w-auto data-[panel-group-direction=vertical]:h-1",
|
|
96
|
+
)}
|
|
97
|
+
style={{ height: indicatorPx }}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{/* Classic dots handle */}
|
|
102
|
+
{withHandle && (
|
|
103
|
+
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
|
104
|
+
<DragHandleDots2Icon className="h-2.5 w-2.5" />
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</ResizablePrimitive.PanelResizeHandle>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ScrollArea imperative handle for programmatic control
|
|
10
|
+
*/
|
|
11
|
+
export interface ScrollAreaHandle {
|
|
12
|
+
/** Scroll to bottom of the content */
|
|
13
|
+
scrollToBottom: (behavior?: ScrollBehavior) => void;
|
|
14
|
+
/** Scroll to top of the content */
|
|
15
|
+
scrollToTop: (behavior?: ScrollBehavior) => void;
|
|
16
|
+
/** Scroll to a specific position */
|
|
17
|
+
scrollTo: (options: ScrollToOptions) => void;
|
|
18
|
+
/** Get the viewport element */
|
|
19
|
+
getViewport: () => HTMLDivElement | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ScrollAreaProps
|
|
23
|
+
extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
|
|
24
|
+
/** Ref to access the viewport element directly */
|
|
25
|
+
viewportRef?: React.RefObject<HTMLDivElement>;
|
|
26
|
+
/** Additional className for the viewport */
|
|
27
|
+
viewportClassName?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ScrollArea = React.forwardRef<ScrollAreaHandle, ScrollAreaProps>(
|
|
31
|
+
({ className, children, viewportRef, viewportClassName, ...props }, ref) => {
|
|
32
|
+
const internalViewportRef = React.useRef<HTMLDivElement>(null);
|
|
33
|
+
const actualViewportRef = viewportRef || internalViewportRef;
|
|
34
|
+
|
|
35
|
+
// Expose imperative methods
|
|
36
|
+
React.useImperativeHandle(ref, () => ({
|
|
37
|
+
scrollToBottom: (behavior: ScrollBehavior = 'smooth') => {
|
|
38
|
+
const viewport = actualViewportRef.current;
|
|
39
|
+
if (viewport) {
|
|
40
|
+
viewport.scrollTo({
|
|
41
|
+
top: viewport.scrollHeight,
|
|
42
|
+
behavior,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
scrollToTop: (behavior: ScrollBehavior = 'smooth') => {
|
|
47
|
+
const viewport = actualViewportRef.current;
|
|
48
|
+
if (viewport) {
|
|
49
|
+
viewport.scrollTo({
|
|
50
|
+
top: 0,
|
|
51
|
+
behavior,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
scrollTo: (options: ScrollToOptions) => {
|
|
56
|
+
actualViewportRef.current?.scrollTo(options);
|
|
57
|
+
},
|
|
58
|
+
getViewport: () => actualViewportRef.current,
|
|
59
|
+
}), [actualViewportRef]);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ScrollAreaPrimitive.Root
|
|
63
|
+
className={cn("relative overflow-hidden", className)}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
<ScrollAreaPrimitive.Viewport
|
|
67
|
+
ref={actualViewportRef as React.RefObject<HTMLDivElement>}
|
|
68
|
+
className={cn("h-full w-full rounded-[inherit]", viewportClassName)}
|
|
69
|
+
>
|
|
70
|
+
{children}
|
|
71
|
+
</ScrollAreaPrimitive.Viewport>
|
|
72
|
+
<ScrollBar />
|
|
73
|
+
<ScrollAreaPrimitive.Corner />
|
|
74
|
+
</ScrollAreaPrimitive.Root>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
ScrollArea.displayName = "ScrollArea"
|
|
79
|
+
|
|
80
|
+
const ScrollBar = React.forwardRef<
|
|
81
|
+
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
82
|
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
83
|
+
>(({ className, orientation = "vertical", ...props }, ref) => (
|
|
84
|
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
85
|
+
ref={ref}
|
|
86
|
+
orientation={orientation}
|
|
87
|
+
className={cn(
|
|
88
|
+
"flex touch-none select-none transition-colors",
|
|
89
|
+
orientation === "vertical" &&
|
|
90
|
+
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
|
91
|
+
orientation === "horizontal" &&
|
|
92
|
+
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
|
93
|
+
className
|
|
94
|
+
)}
|
|
95
|
+
{...props}
|
|
96
|
+
>
|
|
97
|
+
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
|
98
|
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
99
|
+
))
|
|
100
|
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
|
101
|
+
|
|
102
|
+
export { ScrollArea, ScrollBar }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../lib/utils';
|
|
4
|
+
|
|
5
|
+
interface SectionProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
variant?: "default" | "dark" | "gradient" | "card";
|
|
9
|
+
size?: "sm" | "md" | "lg" | "xl";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Section = ({
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
variant = "default",
|
|
16
|
+
size = "lg"
|
|
17
|
+
}: SectionProps) => {
|
|
18
|
+
const paddingClasses = {
|
|
19
|
+
sm: "py-12",
|
|
20
|
+
md: "py-16",
|
|
21
|
+
lg: "py-24",
|
|
22
|
+
xl: "py-32"
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const variantClasses = {
|
|
26
|
+
default: "bg-background",
|
|
27
|
+
dark: "bg-card relative",
|
|
28
|
+
gradient: "gradient-hero relative overflow-hidden",
|
|
29
|
+
card: "gradient-card relative"
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<section className={cn('section-padding', variantClasses[variant], className)}>
|
|
34
|
+
<div className="w-full px-4 sm:px-6 lg:px-8">
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
</section>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface SectionHeaderProps {
|
|
42
|
+
title: string;
|
|
43
|
+
subtitle?: string;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const SectionHeader = ({ title, subtitle, className }: SectionHeaderProps) => (
|
|
48
|
+
<div className={cn("text-center mb-16 animate-fade-in", className)}>
|
|
49
|
+
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-foreground mb-6">
|
|
50
|
+
{title}
|
|
51
|
+
</h2>
|
|
52
|
+
{subtitle && (
|
|
53
|
+
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
|
54
|
+
{subtitle}
|
|
55
|
+
</p>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|