@dilipod/ui 0.2.15 → 0.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 (44) hide show
  1. package/dist/components/alert-dialog.d.ts +34 -0
  2. package/dist/components/alert-dialog.d.ts.map +1 -0
  3. package/dist/components/breadcrumbs.d.ts +30 -0
  4. package/dist/components/breadcrumbs.d.ts.map +1 -0
  5. package/dist/components/date-range-picker.d.ts +36 -0
  6. package/dist/components/date-range-picker.d.ts.map +1 -0
  7. package/dist/components/pagination.d.ts +29 -0
  8. package/dist/components/pagination.d.ts.map +1 -0
  9. package/dist/components/popover.d.ts +10 -0
  10. package/dist/components/popover.d.ts.map +1 -0
  11. package/dist/components/radio-group.d.ts +17 -0
  12. package/dist/components/radio-group.d.ts.map +1 -0
  13. package/dist/components/settings-nav.d.ts +35 -0
  14. package/dist/components/settings-nav.d.ts.map +1 -0
  15. package/dist/components/skeleton.d.ts +28 -0
  16. package/dist/components/skeleton.d.ts.map +1 -0
  17. package/dist/components/step-progress.d.ts +28 -0
  18. package/dist/components/step-progress.d.ts.map +1 -0
  19. package/dist/components/switch.d.ts +15 -0
  20. package/dist/components/switch.d.ts.map +1 -0
  21. package/dist/components/tabs.d.ts +10 -0
  22. package/dist/components/tabs.d.ts.map +1 -0
  23. package/dist/components/tooltip.d.ts +17 -0
  24. package/dist/components/tooltip.d.ts.map +1 -0
  25. package/dist/index.d.ts +22 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1264 -87
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +1210 -89
  30. package/dist/index.mjs.map +1 -1
  31. package/package.json +7 -1
  32. package/src/components/alert-dialog.tsx +203 -0
  33. package/src/components/breadcrumbs.tsx +160 -0
  34. package/src/components/date-range-picker.tsx +183 -0
  35. package/src/components/pagination.tsx +220 -0
  36. package/src/components/popover.tsx +53 -0
  37. package/src/components/radio-group.tsx +125 -0
  38. package/src/components/settings-nav.tsx +137 -0
  39. package/src/components/skeleton.tsx +103 -0
  40. package/src/components/step-progress.tsx +205 -0
  41. package/src/components/switch.tsx +95 -0
  42. package/src/components/tabs.tsx +92 -0
  43. package/src/components/tooltip.tsx +78 -0
  44. package/src/index.ts +78 -0
@@ -0,0 +1,220 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { CaretLeft, CaretRight, DotsThree } from '@phosphor-icons/react'
5
+ import { cn } from '../lib/utils'
6
+ import { Button } from './button'
7
+
8
+ export interface PaginationProps {
9
+ /** Current page (1-indexed) */
10
+ currentPage: number
11
+ /** Total number of pages */
12
+ totalPages: number
13
+ /** Callback when page changes */
14
+ onPageChange: (page: number) => void
15
+ /** Number of pages to show around current page */
16
+ siblingCount?: number
17
+ /** Whether to show first/last page buttons */
18
+ showBoundaryPages?: boolean
19
+ /** Size variant */
20
+ size?: 'sm' | 'default' | 'lg'
21
+ /** Additional class name */
22
+ className?: string
23
+ }
24
+
25
+ function generatePagination(
26
+ currentPage: number,
27
+ totalPages: number,
28
+ siblingCount: number
29
+ ): (number | 'ellipsis')[] {
30
+ const totalPageNumbers = siblingCount * 2 + 5 // siblings + first + last + current + 2 ellipsis
31
+
32
+ if (totalPageNumbers >= totalPages) {
33
+ return Array.from({ length: totalPages }, (_, i) => i + 1)
34
+ }
35
+
36
+ const leftSiblingIndex = Math.max(currentPage - siblingCount, 1)
37
+ const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPages)
38
+
39
+ const showLeftDots = leftSiblingIndex > 2
40
+ const showRightDots = rightSiblingIndex < totalPages - 1
41
+
42
+ if (!showLeftDots && showRightDots) {
43
+ const leftRange = Array.from(
44
+ { length: 3 + siblingCount * 2 },
45
+ (_, i) => i + 1
46
+ )
47
+ return [...leftRange, 'ellipsis', totalPages]
48
+ }
49
+
50
+ if (showLeftDots && !showRightDots) {
51
+ const rightRange = Array.from(
52
+ { length: 3 + siblingCount * 2 },
53
+ (_, i) => totalPages - (3 + siblingCount * 2) + i + 1
54
+ )
55
+ return [1, 'ellipsis', ...rightRange]
56
+ }
57
+
58
+ const middleRange = Array.from(
59
+ { length: siblingCount * 2 + 1 },
60
+ (_, i) => leftSiblingIndex + i
61
+ )
62
+ return [1, 'ellipsis', ...middleRange, 'ellipsis', totalPages]
63
+ }
64
+
65
+ const Pagination = React.forwardRef<HTMLElement, PaginationProps>(
66
+ (
67
+ {
68
+ currentPage,
69
+ totalPages,
70
+ onPageChange,
71
+ siblingCount = 1,
72
+ showBoundaryPages = true,
73
+ size = 'default',
74
+ className,
75
+ },
76
+ ref
77
+ ) => {
78
+ const pages = generatePagination(currentPage, totalPages, siblingCount)
79
+
80
+ const buttonSize = size === 'sm' ? 'sm' : size === 'lg' ? 'lg' : 'default'
81
+ const iconSize = size === 'sm' ? 14 : size === 'lg' ? 20 : 16
82
+
83
+ if (totalPages <= 1) {
84
+ return null
85
+ }
86
+
87
+ return (
88
+ <nav
89
+ ref={ref}
90
+ role="navigation"
91
+ aria-label="Pagination"
92
+ className={cn('flex items-center gap-1', className)}
93
+ >
94
+ {/* Previous button */}
95
+ <Button
96
+ variant="outline"
97
+ size={buttonSize}
98
+ onClick={() => onPageChange(currentPage - 1)}
99
+ disabled={currentPage <= 1}
100
+ aria-label="Go to previous page"
101
+ >
102
+ <CaretLeft size={iconSize} />
103
+ </Button>
104
+
105
+ {/* Page numbers */}
106
+ <div className="flex items-center gap-1">
107
+ {pages.map((page, index) => {
108
+ if (page === 'ellipsis') {
109
+ return (
110
+ <span
111
+ key={`ellipsis-${index}`}
112
+ className="flex h-9 w-9 items-center justify-center text-muted-foreground"
113
+ >
114
+ <DotsThree size={iconSize} />
115
+ </span>
116
+ )
117
+ }
118
+
119
+ const isCurrentPage = page === currentPage
120
+ return (
121
+ <Button
122
+ key={page}
123
+ variant={isCurrentPage ? 'default' : 'outline'}
124
+ size={buttonSize}
125
+ onClick={() => onPageChange(page)}
126
+ aria-label={`Go to page ${page}`}
127
+ aria-current={isCurrentPage ? 'page' : undefined}
128
+ >
129
+ {page}
130
+ </Button>
131
+ )
132
+ })}
133
+ </div>
134
+
135
+ {/* Next button */}
136
+ <Button
137
+ variant="outline"
138
+ size={buttonSize}
139
+ onClick={() => onPageChange(currentPage + 1)}
140
+ disabled={currentPage >= totalPages}
141
+ aria-label="Go to next page"
142
+ >
143
+ <CaretRight size={iconSize} />
144
+ </Button>
145
+ </nav>
146
+ )
147
+ }
148
+ )
149
+ Pagination.displayName = 'Pagination'
150
+
151
+ // Simple variant with just prev/next
152
+ export interface SimplePaginationProps {
153
+ currentPage: number
154
+ totalPages: number
155
+ onPageChange: (page: number) => void
156
+ size?: 'sm' | 'default' | 'lg'
157
+ className?: string
158
+ showPageInfo?: boolean
159
+ }
160
+
161
+ const SimplePagination = React.forwardRef<HTMLElement, SimplePaginationProps>(
162
+ (
163
+ {
164
+ currentPage,
165
+ totalPages,
166
+ onPageChange,
167
+ size = 'default',
168
+ className,
169
+ showPageInfo = true,
170
+ },
171
+ ref
172
+ ) => {
173
+ const buttonSize = size === 'sm' ? 'sm' : size === 'lg' ? 'lg' : 'default'
174
+ const iconSize = size === 'sm' ? 14 : size === 'lg' ? 20 : 16
175
+
176
+ if (totalPages <= 1) {
177
+ return null
178
+ }
179
+
180
+ return (
181
+ <nav
182
+ ref={ref}
183
+ role="navigation"
184
+ aria-label="Pagination"
185
+ className={cn('flex items-center gap-2', className)}
186
+ >
187
+ <Button
188
+ variant="outline"
189
+ size={buttonSize}
190
+ onClick={() => onPageChange(currentPage - 1)}
191
+ disabled={currentPage <= 1}
192
+ aria-label="Go to previous page"
193
+ >
194
+ <CaretLeft size={iconSize} />
195
+ <span className="ml-1">Previous</span>
196
+ </Button>
197
+
198
+ {showPageInfo && (
199
+ <span className="text-sm text-muted-foreground px-2">
200
+ Page {currentPage} of {totalPages}
201
+ </span>
202
+ )}
203
+
204
+ <Button
205
+ variant="outline"
206
+ size={buttonSize}
207
+ onClick={() => onPageChange(currentPage + 1)}
208
+ disabled={currentPage >= totalPages}
209
+ aria-label="Go to next page"
210
+ >
211
+ <span className="mr-1">Next</span>
212
+ <CaretRight size={iconSize} />
213
+ </Button>
214
+ </nav>
215
+ )
216
+ }
217
+ )
218
+ SimplePagination.displayName = 'SimplePagination'
219
+
220
+ export { Pagination, SimplePagination }
@@ -0,0 +1,53 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as PopoverPrimitive from '@radix-ui/react-popover'
5
+ import { cn } from '../lib/utils'
6
+
7
+ const Popover = PopoverPrimitive.Root
8
+
9
+ const PopoverTrigger = PopoverPrimitive.Trigger
10
+
11
+ const PopoverAnchor = PopoverPrimitive.Anchor
12
+
13
+ const PopoverClose = PopoverPrimitive.Close
14
+
15
+ const PopoverContent = React.forwardRef<
16
+ React.ElementRef<typeof PopoverPrimitive.Content>,
17
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
18
+ >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
19
+ <PopoverPrimitive.Portal>
20
+ <PopoverPrimitive.Content
21
+ ref={ref}
22
+ align={align}
23
+ sideOffset={sideOffset}
24
+ className={cn(
25
+ 'z-50 w-72 rounded-sm border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
26
+ className
27
+ )}
28
+ {...props}
29
+ />
30
+ </PopoverPrimitive.Portal>
31
+ ))
32
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName
33
+
34
+ const PopoverArrow = React.forwardRef<
35
+ React.ElementRef<typeof PopoverPrimitive.Arrow>,
36
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Arrow>
37
+ >(({ className, ...props }, ref) => (
38
+ <PopoverPrimitive.Arrow
39
+ ref={ref}
40
+ className={cn('fill-popover', className)}
41
+ {...props}
42
+ />
43
+ ))
44
+ PopoverArrow.displayName = PopoverPrimitive.Arrow.displayName
45
+
46
+ export {
47
+ Popover,
48
+ PopoverTrigger,
49
+ PopoverContent,
50
+ PopoverAnchor,
51
+ PopoverClose,
52
+ PopoverArrow,
53
+ }
@@ -0,0 +1,125 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
5
+ import { Circle } from '@phosphor-icons/react'
6
+ import { cn } from '../lib/utils'
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>
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-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
31
+ className
32
+ )}
33
+ {...props}
34
+ >
35
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
36
+ <Circle weight="fill" className="h-2.5 w-2.5 fill-current" />
37
+ </RadioGroupPrimitive.Indicator>
38
+ </RadioGroupPrimitive.Item>
39
+ )
40
+ })
41
+ RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
42
+
43
+ // Convenience component for labeled radio items
44
+ export interface RadioGroupOptionProps
45
+ extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> {
46
+ label: string
47
+ description?: string
48
+ }
49
+
50
+ const RadioGroupOption = React.forwardRef<
51
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
52
+ RadioGroupOptionProps
53
+ >(({ label, description, className, id, ...props }, ref) => {
54
+ const optionId = id || React.useId()
55
+
56
+ return (
57
+ <div className={cn('flex items-start gap-3', className)}>
58
+ <RadioGroupItem ref={ref} id={optionId} {...props} />
59
+ <div className="space-y-0.5">
60
+ <label
61
+ htmlFor={optionId}
62
+ className="text-sm font-medium leading-none cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
63
+ >
64
+ {label}
65
+ </label>
66
+ {description && (
67
+ <p className="text-xs text-muted-foreground">{description}</p>
68
+ )}
69
+ </div>
70
+ </div>
71
+ )
72
+ })
73
+ RadioGroupOption.displayName = 'RadioGroupOption'
74
+
75
+ // Card variant for selection cards
76
+ export interface RadioGroupCardProps
77
+ extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> {
78
+ label: string
79
+ description?: string
80
+ children?: React.ReactNode
81
+ }
82
+
83
+ const RadioGroupCard = React.forwardRef<
84
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
85
+ RadioGroupCardProps
86
+ >(({ label, description, children, className, id, ...props }, ref) => {
87
+ const cardId = id || React.useId()
88
+
89
+ return (
90
+ <RadioGroupPrimitive.Item
91
+ ref={ref}
92
+ id={cardId}
93
+ className={cn(
94
+ 'relative flex cursor-pointer rounded-lg border bg-background p-4 transition-all hover:border-[var(--cyan)]/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[state=checked]:border-[var(--cyan)] data-[state=checked]:ring-1 data-[state=checked]:ring-[var(--cyan)] disabled:cursor-not-allowed disabled:opacity-50',
95
+ className
96
+ )}
97
+ {...props}
98
+ >
99
+ <div className="flex w-full items-start gap-3">
100
+ <div className="flex h-5 items-center">
101
+ <div className="aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow">
102
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center h-full">
103
+ <Circle weight="fill" className="h-2.5 w-2.5 fill-[var(--cyan)]" />
104
+ </RadioGroupPrimitive.Indicator>
105
+ </div>
106
+ </div>
107
+ <div className="flex-1 space-y-1">
108
+ <label
109
+ htmlFor={cardId}
110
+ className="text-sm font-medium leading-none cursor-pointer"
111
+ >
112
+ {label}
113
+ </label>
114
+ {description && (
115
+ <p className="text-xs text-muted-foreground">{description}</p>
116
+ )}
117
+ {children}
118
+ </div>
119
+ </div>
120
+ </RadioGroupPrimitive.Item>
121
+ )
122
+ })
123
+ RadioGroupCard.displayName = 'RadioGroupCard'
124
+
125
+ export { RadioGroup, RadioGroupItem, RadioGroupOption, RadioGroupCard }
@@ -0,0 +1,137 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { CaretRight } from '@phosphor-icons/react'
5
+ import { cn } from '../lib/utils'
6
+
7
+ export interface SettingsNavItem {
8
+ title: string
9
+ description: string
10
+ href: string
11
+ icon: React.ReactNode
12
+ }
13
+
14
+ export interface SettingsNavGroup {
15
+ title: string
16
+ items: SettingsNavItem[]
17
+ }
18
+
19
+ export interface SettingsNavProps extends React.HTMLAttributes<HTMLDivElement> {
20
+ groups: SettingsNavGroup[]
21
+ /** Custom Link component (e.g., Next.js Link) */
22
+ LinkComponent?: React.ComponentType<{
23
+ href: string
24
+ className?: string
25
+ children?: React.ReactNode
26
+ }>
27
+ }
28
+
29
+ const SettingsNav = React.forwardRef<HTMLDivElement, SettingsNavProps>(
30
+ ({ groups, LinkComponent, className, ...props }, ref) => {
31
+ const Link = LinkComponent || 'a'
32
+
33
+ return (
34
+ <div ref={ref} className={cn('space-y-8', className)} {...props}>
35
+ {groups.map((group) => (
36
+ <div key={group.title}>
37
+ <h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3 px-1">
38
+ {group.title}
39
+ </h2>
40
+ <div className="space-y-1">
41
+ {group.items.map((item) => (
42
+ <Link
43
+ key={item.href}
44
+ href={item.href}
45
+ className="group flex items-center gap-4 p-4 rounded-sm transition-all hover:bg-gray-50 border border-transparent hover:border-gray-200"
46
+ >
47
+ <div className="flex items-center justify-center w-10 h-10 rounded-sm bg-gray-100 group-hover:bg-[var(--cyan)]/10 transition-colors">
48
+ <span className="text-gray-600 group-hover:text-[var(--cyan)] transition-colors [&>svg]:w-5 [&>svg]:h-5">
49
+ {item.icon}
50
+ </span>
51
+ </div>
52
+ <div className="flex-1 min-w-0">
53
+ <h3 className="font-medium text-[var(--black)] group-hover:text-[var(--cyan)] transition-colors">
54
+ {item.title}
55
+ </h3>
56
+ <p className="text-sm text-muted-foreground">
57
+ {item.description}
58
+ </p>
59
+ </div>
60
+ <CaretRight
61
+ size={18}
62
+ className="text-gray-300 group-hover:text-[var(--cyan)] group-hover:translate-x-0.5 transition-all shrink-0"
63
+ />
64
+ </Link>
65
+ ))}
66
+ </div>
67
+ </div>
68
+ ))}
69
+ </div>
70
+ )
71
+ }
72
+ )
73
+ SettingsNav.displayName = 'SettingsNav'
74
+
75
+ // Single item variant
76
+ export interface SettingsNavLinkProps
77
+ extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
78
+ title: string
79
+ description?: string
80
+ icon?: React.ReactNode
81
+ /** Custom Link component */
82
+ LinkComponent?: React.ComponentType<{
83
+ href: string
84
+ className?: string
85
+ children?: React.ReactNode
86
+ }>
87
+ }
88
+
89
+ const SettingsNavLink = React.forwardRef<HTMLAnchorElement, SettingsNavLinkProps>(
90
+ ({ title, description, icon, href, LinkComponent, className, ...props }, ref) => {
91
+ const content = (
92
+ <>
93
+ {icon && (
94
+ <div className="flex items-center justify-center w-10 h-10 rounded-sm bg-gray-100 group-hover:bg-[var(--cyan)]/10 transition-colors">
95
+ <span className="text-gray-600 group-hover:text-[var(--cyan)] transition-colors [&>svg]:w-5 [&>svg]:h-5">
96
+ {icon}
97
+ </span>
98
+ </div>
99
+ )}
100
+ <div className="flex-1 min-w-0">
101
+ <h3 className="font-medium text-[var(--black)] group-hover:text-[var(--cyan)] transition-colors">
102
+ {title}
103
+ </h3>
104
+ {description && (
105
+ <p className="text-sm text-muted-foreground">{description}</p>
106
+ )}
107
+ </div>
108
+ <CaretRight
109
+ size={18}
110
+ className="text-gray-300 group-hover:text-[var(--cyan)] group-hover:translate-x-0.5 transition-all shrink-0"
111
+ />
112
+ </>
113
+ )
114
+
115
+ const linkClassName = cn(
116
+ 'group flex items-center gap-4 p-4 rounded-sm transition-all hover:bg-gray-50 border border-transparent hover:border-gray-200',
117
+ className
118
+ )
119
+
120
+ if (LinkComponent && href) {
121
+ return (
122
+ <LinkComponent href={href} className={linkClassName}>
123
+ {content}
124
+ </LinkComponent>
125
+ )
126
+ }
127
+
128
+ return (
129
+ <a ref={ref} href={href} className={linkClassName} {...props}>
130
+ {content}
131
+ </a>
132
+ )
133
+ }
134
+ )
135
+ SettingsNavLink.displayName = 'SettingsNavLink'
136
+
137
+ export { SettingsNav, SettingsNavLink }
@@ -0,0 +1,103 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../lib/utils'
5
+
6
+ export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ /** Variant of the skeleton */
8
+ variant?: 'default' | 'circular' | 'rounded'
9
+ /** Width of the skeleton (can be number for px or string like '100%') */
10
+ width?: number | string
11
+ /** Height of the skeleton (can be number for px or string like '2rem') */
12
+ height?: number | string
13
+ /** Whether to show animation */
14
+ animate?: boolean
15
+ }
16
+
17
+ const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
18
+ ({ className, variant = 'default', width, height, animate = true, style, ...props }, ref) => {
19
+ const variantStyles = {
20
+ default: 'rounded-sm',
21
+ circular: 'rounded-full',
22
+ rounded: 'rounded-md',
23
+ }
24
+
25
+ return (
26
+ <div
27
+ ref={ref}
28
+ className={cn(
29
+ 'bg-muted',
30
+ animate && 'animate-pulse',
31
+ variantStyles[variant],
32
+ className
33
+ )}
34
+ style={{
35
+ width: typeof width === 'number' ? `${width}px` : width,
36
+ height: typeof height === 'number' ? `${height}px` : height,
37
+ ...style,
38
+ }}
39
+ {...props}
40
+ />
41
+ )
42
+ }
43
+ )
44
+ Skeleton.displayName = 'Skeleton'
45
+
46
+ // Common skeleton patterns
47
+ export interface SkeletonTextProps extends Omit<SkeletonProps, 'variant'> {
48
+ /** Number of lines to render */
49
+ lines?: number
50
+ /** Gap between lines */
51
+ gap?: number
52
+ }
53
+
54
+ const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(
55
+ ({ lines = 3, gap = 8, className, ...props }, ref) => {
56
+ return (
57
+ <div ref={ref} className={cn('space-y-2', className)} style={{ gap }}>
58
+ {Array.from({ length: lines }).map((_, i) => (
59
+ <Skeleton
60
+ key={i}
61
+ height={16}
62
+ width={i === lines - 1 ? '70%' : '100%'}
63
+ {...props}
64
+ />
65
+ ))}
66
+ </div>
67
+ )
68
+ }
69
+ )
70
+ SkeletonText.displayName = 'SkeletonText'
71
+
72
+ export interface SkeletonCardProps extends React.HTMLAttributes<HTMLDivElement> {
73
+ /** Whether to include a header */
74
+ hasHeader?: boolean
75
+ /** Whether to include an avatar/icon */
76
+ hasAvatar?: boolean
77
+ }
78
+
79
+ const SkeletonCard = React.forwardRef<HTMLDivElement, SkeletonCardProps>(
80
+ ({ hasHeader = true, hasAvatar = false, className, ...props }, ref) => {
81
+ return (
82
+ <div
83
+ ref={ref}
84
+ className={cn('rounded-lg border p-6 space-y-4', className)}
85
+ {...props}
86
+ >
87
+ {hasHeader && (
88
+ <div className="flex items-center gap-4">
89
+ {hasAvatar && <Skeleton variant="circular" width={40} height={40} />}
90
+ <div className="space-y-2 flex-1">
91
+ <Skeleton height={20} width="50%" />
92
+ <Skeleton height={14} width="30%" />
93
+ </div>
94
+ </div>
95
+ )}
96
+ <SkeletonText lines={3} />
97
+ </div>
98
+ )
99
+ }
100
+ )
101
+ SkeletonCard.displayName = 'SkeletonCard'
102
+
103
+ export { Skeleton, SkeletonText, SkeletonCard }