@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.
Files changed (88) hide show
  1. package/README.md +135 -0
  2. package/package.json +111 -0
  3. package/src/components/accordion.tsx +56 -0
  4. package/src/components/alert-dialog.tsx +142 -0
  5. package/src/components/alert.tsx +59 -0
  6. package/src/components/aspect-ratio.tsx +7 -0
  7. package/src/components/avatar.tsx +50 -0
  8. package/src/components/badge.tsx +36 -0
  9. package/src/components/button-group.tsx +85 -0
  10. package/src/components/button.tsx +111 -0
  11. package/src/components/calendar.tsx +213 -0
  12. package/src/components/card.tsx +76 -0
  13. package/src/components/carousel.tsx +261 -0
  14. package/src/components/chart.tsx +369 -0
  15. package/src/components/checkbox.tsx +29 -0
  16. package/src/components/collapsible.tsx +11 -0
  17. package/src/components/combobox.tsx +182 -0
  18. package/src/components/command.tsx +170 -0
  19. package/src/components/context-menu.tsx +200 -0
  20. package/src/components/copy.tsx +144 -0
  21. package/src/components/dialog.tsx +122 -0
  22. package/src/components/drawer.tsx +137 -0
  23. package/src/components/empty.tsx +104 -0
  24. package/src/components/field.tsx +244 -0
  25. package/src/components/form.tsx +178 -0
  26. package/src/components/hover-card.tsx +29 -0
  27. package/src/components/image-with-fallback.tsx +170 -0
  28. package/src/components/index.ts +86 -0
  29. package/src/components/input-group.tsx +170 -0
  30. package/src/components/input-otp.tsx +81 -0
  31. package/src/components/input.tsx +22 -0
  32. package/src/components/item.tsx +195 -0
  33. package/src/components/kbd.tsx +28 -0
  34. package/src/components/label.tsx +26 -0
  35. package/src/components/multi-select.tsx +222 -0
  36. package/src/components/og-image.tsx +47 -0
  37. package/src/components/popover.tsx +33 -0
  38. package/src/components/portal.tsx +106 -0
  39. package/src/components/preloader.tsx +250 -0
  40. package/src/components/progress.tsx +28 -0
  41. package/src/components/radio-group.tsx +43 -0
  42. package/src/components/resizable.tsx +111 -0
  43. package/src/components/scroll-area.tsx +102 -0
  44. package/src/components/section.tsx +58 -0
  45. package/src/components/select.tsx +158 -0
  46. package/src/components/separator.tsx +31 -0
  47. package/src/components/sheet.tsx +140 -0
  48. package/src/components/skeleton.tsx +15 -0
  49. package/src/components/slider.tsx +28 -0
  50. package/src/components/spinner.tsx +16 -0
  51. package/src/components/sticky.tsx +117 -0
  52. package/src/components/switch.tsx +29 -0
  53. package/src/components/table.tsx +120 -0
  54. package/src/components/tabs.tsx +238 -0
  55. package/src/components/textarea.tsx +22 -0
  56. package/src/components/toast.tsx +129 -0
  57. package/src/components/toaster.tsx +41 -0
  58. package/src/components/toggle-group.tsx +61 -0
  59. package/src/components/toggle.tsx +45 -0
  60. package/src/components/token-icon.tsx +156 -0
  61. package/src/components/tooltip-provider-safe.tsx +43 -0
  62. package/src/components/tooltip.tsx +32 -0
  63. package/src/hooks/index.ts +15 -0
  64. package/src/hooks/useCopy.ts +41 -0
  65. package/src/hooks/useCountdown.ts +73 -0
  66. package/src/hooks/useDebounce.ts +25 -0
  67. package/src/hooks/useDebouncedCallback.ts +58 -0
  68. package/src/hooks/useDebugTools.ts +52 -0
  69. package/src/hooks/useEventsBus.ts +53 -0
  70. package/src/hooks/useImageLoader.ts +95 -0
  71. package/src/hooks/useMediaQuery.ts +40 -0
  72. package/src/hooks/useMobile.tsx +22 -0
  73. package/src/hooks/useToast.ts +194 -0
  74. package/src/index.ts +14 -0
  75. package/src/lib/index.ts +2 -0
  76. package/src/lib/og-image.ts +151 -0
  77. package/src/lib/utils.ts +6 -0
  78. package/src/styles/base.css +20 -0
  79. package/src/styles/globals.css +12 -0
  80. package/src/styles/index.css +25 -0
  81. package/src/styles/sources.css +11 -0
  82. package/src/styles/theme/animations.css +65 -0
  83. package/src/styles/theme/dark.css +49 -0
  84. package/src/styles/theme/light.css +50 -0
  85. package/src/styles/theme/tokens.css +134 -0
  86. package/src/styles/theme.css +22 -0
  87. package/src/styles/utilities.css +187 -0
  88. 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
+ );