@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.
- package/dist/components/alert-dialog.d.ts +34 -0
- package/dist/components/alert-dialog.d.ts.map +1 -0
- package/dist/components/breadcrumbs.d.ts +30 -0
- package/dist/components/breadcrumbs.d.ts.map +1 -0
- package/dist/components/date-range-picker.d.ts +36 -0
- package/dist/components/date-range-picker.d.ts.map +1 -0
- package/dist/components/pagination.d.ts +29 -0
- package/dist/components/pagination.d.ts.map +1 -0
- package/dist/components/popover.d.ts +10 -0
- package/dist/components/popover.d.ts.map +1 -0
- package/dist/components/radio-group.d.ts +17 -0
- package/dist/components/radio-group.d.ts.map +1 -0
- package/dist/components/settings-nav.d.ts +35 -0
- package/dist/components/settings-nav.d.ts.map +1 -0
- package/dist/components/skeleton.d.ts +28 -0
- package/dist/components/skeleton.d.ts.map +1 -0
- package/dist/components/step-progress.d.ts +28 -0
- package/dist/components/step-progress.d.ts.map +1 -0
- package/dist/components/switch.d.ts +15 -0
- package/dist/components/switch.d.ts.map +1 -0
- package/dist/components/tabs.d.ts +10 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tooltip.d.ts +17 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1264 -87
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1210 -89
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -1
- package/src/components/alert-dialog.tsx +203 -0
- package/src/components/breadcrumbs.tsx +160 -0
- package/src/components/date-range-picker.tsx +183 -0
- package/src/components/pagination.tsx +220 -0
- package/src/components/popover.tsx +53 -0
- package/src/components/radio-group.tsx +125 -0
- package/src/components/settings-nav.tsx +137 -0
- package/src/components/skeleton.tsx +103 -0
- package/src/components/step-progress.tsx +205 -0
- package/src/components/switch.tsx +95 -0
- package/src/components/tabs.tsx +92 -0
- package/src/components/tooltip.tsx +78 -0
- 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 }
|