@dilipod/ui 0.2.15 → 0.3.0
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 +1263 -87
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1209 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dilipod/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Dilipod Design System - Shared UI components and styles",
|
|
5
5
|
"author": "Dilipod <hello@dilipod.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,12 +62,18 @@
|
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@phosphor-icons/react": "^2.1.7",
|
|
64
64
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
65
|
+
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
65
66
|
"@radix-ui/react-avatar": "^1.1.3",
|
|
66
67
|
"@radix-ui/react-dialog": "^1.1.14",
|
|
67
68
|
"@radix-ui/react-dropdown-menu": "^2.1.3",
|
|
68
69
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
70
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
71
|
+
"@radix-ui/react-radio-group": "^1.3.8",
|
|
69
72
|
"@radix-ui/react-slot": "^1.2.3",
|
|
73
|
+
"@radix-ui/react-switch": "^1.2.6",
|
|
74
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
70
75
|
"@radix-ui/react-toast": "^1.2.15",
|
|
76
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
71
77
|
"class-variance-authority": "^0.7.1",
|
|
72
78
|
"clsx": "^2.1.1",
|
|
73
79
|
"tailwind-merge": "^3.3.0",
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
|
5
|
+
import { cn } from '../lib/utils'
|
|
6
|
+
import { buttonVariants } from './button'
|
|
7
|
+
|
|
8
|
+
const AlertDialog = AlertDialogPrimitive.Root
|
|
9
|
+
|
|
10
|
+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
|
11
|
+
|
|
12
|
+
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
|
13
|
+
|
|
14
|
+
const AlertDialogOverlay = React.forwardRef<
|
|
15
|
+
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
|
16
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
|
17
|
+
>(({ className, ...props }, ref) => (
|
|
18
|
+
<AlertDialogPrimitive.Overlay
|
|
19
|
+
className={cn(
|
|
20
|
+
'fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
ref={ref}
|
|
25
|
+
/>
|
|
26
|
+
))
|
|
27
|
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
|
28
|
+
|
|
29
|
+
const AlertDialogContent = React.forwardRef<
|
|
30
|
+
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
31
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
32
|
+
>(({ className, ...props }, ref) => (
|
|
33
|
+
<AlertDialogPortal>
|
|
34
|
+
<AlertDialogOverlay />
|
|
35
|
+
<AlertDialogPrimitive.Content
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn(
|
|
38
|
+
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-sm',
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
</AlertDialogPortal>
|
|
44
|
+
))
|
|
45
|
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
|
46
|
+
|
|
47
|
+
const AlertDialogHeader = ({
|
|
48
|
+
className,
|
|
49
|
+
...props
|
|
50
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
51
|
+
<div
|
|
52
|
+
className={cn(
|
|
53
|
+
'flex flex-col space-y-2 text-center sm:text-left',
|
|
54
|
+
className
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
AlertDialogHeader.displayName = 'AlertDialogHeader'
|
|
60
|
+
|
|
61
|
+
const AlertDialogFooter = ({
|
|
62
|
+
className,
|
|
63
|
+
...props
|
|
64
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
65
|
+
<div
|
|
66
|
+
className={cn(
|
|
67
|
+
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
|
68
|
+
className
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
AlertDialogFooter.displayName = 'AlertDialogFooter'
|
|
74
|
+
|
|
75
|
+
const AlertDialogTitle = React.forwardRef<
|
|
76
|
+
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
|
77
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
|
78
|
+
>(({ className, ...props }, ref) => (
|
|
79
|
+
<AlertDialogPrimitive.Title
|
|
80
|
+
ref={ref}
|
|
81
|
+
className={cn('text-lg font-semibold text-[var(--black)]', className)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
))
|
|
85
|
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
|
86
|
+
|
|
87
|
+
const AlertDialogDescription = React.forwardRef<
|
|
88
|
+
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
|
89
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
|
90
|
+
>(({ className, ...props }, ref) => (
|
|
91
|
+
<AlertDialogPrimitive.Description
|
|
92
|
+
ref={ref}
|
|
93
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
))
|
|
97
|
+
AlertDialogDescription.displayName =
|
|
98
|
+
AlertDialogPrimitive.Description.displayName
|
|
99
|
+
|
|
100
|
+
const AlertDialogAction = React.forwardRef<
|
|
101
|
+
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
|
102
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
|
103
|
+
>(({ className, ...props }, ref) => (
|
|
104
|
+
<AlertDialogPrimitive.Action
|
|
105
|
+
ref={ref}
|
|
106
|
+
className={cn(buttonVariants(), className)}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
))
|
|
110
|
+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
|
111
|
+
|
|
112
|
+
const AlertDialogCancel = React.forwardRef<
|
|
113
|
+
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
|
114
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
|
115
|
+
>(({ className, ...props }, ref) => (
|
|
116
|
+
<AlertDialogPrimitive.Cancel
|
|
117
|
+
ref={ref}
|
|
118
|
+
className={cn(
|
|
119
|
+
buttonVariants({ variant: 'outline' }),
|
|
120
|
+
'mt-2 sm:mt-0',
|
|
121
|
+
className
|
|
122
|
+
)}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
))
|
|
126
|
+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
|
127
|
+
|
|
128
|
+
// Convenience component for common confirmation dialogs
|
|
129
|
+
export interface ConfirmDialogProps {
|
|
130
|
+
open: boolean
|
|
131
|
+
onOpenChange: (open: boolean) => void
|
|
132
|
+
title: string
|
|
133
|
+
description?: string
|
|
134
|
+
confirmText?: string
|
|
135
|
+
cancelText?: string
|
|
136
|
+
onConfirm: () => void
|
|
137
|
+
onCancel?: () => void
|
|
138
|
+
variant?: 'default' | 'destructive'
|
|
139
|
+
loading?: boolean
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ConfirmDialog({
|
|
143
|
+
open,
|
|
144
|
+
onOpenChange,
|
|
145
|
+
title,
|
|
146
|
+
description,
|
|
147
|
+
confirmText = 'Confirm',
|
|
148
|
+
cancelText = 'Cancel',
|
|
149
|
+
onConfirm,
|
|
150
|
+
onCancel,
|
|
151
|
+
variant = 'default',
|
|
152
|
+
loading = false,
|
|
153
|
+
}: ConfirmDialogProps) {
|
|
154
|
+
return (
|
|
155
|
+
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
156
|
+
<AlertDialogContent>
|
|
157
|
+
<AlertDialogHeader>
|
|
158
|
+
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
159
|
+
{description && (
|
|
160
|
+
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
161
|
+
)}
|
|
162
|
+
</AlertDialogHeader>
|
|
163
|
+
<AlertDialogFooter>
|
|
164
|
+
<AlertDialogCancel
|
|
165
|
+
onClick={() => {
|
|
166
|
+
onCancel?.()
|
|
167
|
+
onOpenChange(false)
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
{cancelText}
|
|
171
|
+
</AlertDialogCancel>
|
|
172
|
+
<AlertDialogAction
|
|
173
|
+
onClick={() => {
|
|
174
|
+
onConfirm()
|
|
175
|
+
}}
|
|
176
|
+
className={cn(
|
|
177
|
+
variant === 'destructive' &&
|
|
178
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
|
179
|
+
)}
|
|
180
|
+
disabled={loading}
|
|
181
|
+
>
|
|
182
|
+
{loading ? 'Loading...' : confirmText}
|
|
183
|
+
</AlertDialogAction>
|
|
184
|
+
</AlertDialogFooter>
|
|
185
|
+
</AlertDialogContent>
|
|
186
|
+
</AlertDialog>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export {
|
|
191
|
+
AlertDialog,
|
|
192
|
+
AlertDialogPortal,
|
|
193
|
+
AlertDialogOverlay,
|
|
194
|
+
AlertDialogTrigger,
|
|
195
|
+
AlertDialogContent,
|
|
196
|
+
AlertDialogHeader,
|
|
197
|
+
AlertDialogFooter,
|
|
198
|
+
AlertDialogTitle,
|
|
199
|
+
AlertDialogDescription,
|
|
200
|
+
AlertDialogAction,
|
|
201
|
+
AlertDialogCancel,
|
|
202
|
+
ConfirmDialog,
|
|
203
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { CaretRight, House } from '@phosphor-icons/react'
|
|
5
|
+
import { cn } from '../lib/utils'
|
|
6
|
+
|
|
7
|
+
export interface BreadcrumbItem {
|
|
8
|
+
label: string
|
|
9
|
+
href?: string
|
|
10
|
+
icon?: React.ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BreadcrumbsProps extends React.HTMLAttributes<HTMLElement> {
|
|
14
|
+
items: BreadcrumbItem[]
|
|
15
|
+
/** Custom Link component (e.g., Next.js Link) */
|
|
16
|
+
LinkComponent?: React.ComponentType<{
|
|
17
|
+
href: string
|
|
18
|
+
className?: string
|
|
19
|
+
children?: React.ReactNode
|
|
20
|
+
}>
|
|
21
|
+
/** Separator between items */
|
|
22
|
+
separator?: React.ReactNode
|
|
23
|
+
/** Whether to show home icon for first item */
|
|
24
|
+
showHomeIcon?: boolean
|
|
25
|
+
/** Maximum items to show (will collapse middle items) */
|
|
26
|
+
maxItems?: number
|
|
27
|
+
/** Size variant */
|
|
28
|
+
size?: 'sm' | 'default'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const Breadcrumbs = React.forwardRef<HTMLElement, BreadcrumbsProps>(
|
|
32
|
+
(
|
|
33
|
+
{
|
|
34
|
+
items,
|
|
35
|
+
LinkComponent,
|
|
36
|
+
separator,
|
|
37
|
+
showHomeIcon = false,
|
|
38
|
+
maxItems,
|
|
39
|
+
size = 'default',
|
|
40
|
+
className,
|
|
41
|
+
...props
|
|
42
|
+
},
|
|
43
|
+
ref
|
|
44
|
+
) => {
|
|
45
|
+
const sizeStyles = {
|
|
46
|
+
sm: 'text-xs gap-1',
|
|
47
|
+
default: 'text-sm gap-1.5',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const iconSize = size === 'sm' ? 10 : 12
|
|
51
|
+
|
|
52
|
+
// Collapse items if maxItems is set
|
|
53
|
+
let displayItems = items
|
|
54
|
+
let isCollapsed = false
|
|
55
|
+
if (maxItems && items.length > maxItems) {
|
|
56
|
+
isCollapsed = true
|
|
57
|
+
displayItems = [
|
|
58
|
+
items[0],
|
|
59
|
+
{ label: '...', href: undefined },
|
|
60
|
+
...items.slice(-(maxItems - 2)),
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const defaultSeparator = (
|
|
65
|
+
<CaretRight size={iconSize} className="text-gray-400" />
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<nav
|
|
70
|
+
ref={ref}
|
|
71
|
+
aria-label="Breadcrumb"
|
|
72
|
+
className={cn('flex items-center text-gray-500', sizeStyles[size], className)}
|
|
73
|
+
{...props}
|
|
74
|
+
>
|
|
75
|
+
<ol className={cn('flex items-center', sizeStyles[size])}>
|
|
76
|
+
{displayItems.map((item, index) => {
|
|
77
|
+
const isFirst = index === 0
|
|
78
|
+
const isLast = index === displayItems.length - 1
|
|
79
|
+
const isEllipsis = item.label === '...'
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<li key={index} className={cn('flex items-center', sizeStyles[size])}>
|
|
83
|
+
{index > 0 && (
|
|
84
|
+
<span className="mx-1 text-gray-400">
|
|
85
|
+
{separator || defaultSeparator}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{isEllipsis ? (
|
|
90
|
+
<span className="text-gray-400">{item.label}</span>
|
|
91
|
+
) : item.href && !isLast ? (
|
|
92
|
+
LinkComponent ? (
|
|
93
|
+
<LinkComponent
|
|
94
|
+
href={item.href}
|
|
95
|
+
className="flex items-center gap-1 hover:text-[var(--black)] transition-colors"
|
|
96
|
+
>
|
|
97
|
+
{isFirst && showHomeIcon && (
|
|
98
|
+
<House size={iconSize + 2} weight="fill" />
|
|
99
|
+
)}
|
|
100
|
+
{item.icon}
|
|
101
|
+
<span>{item.label}</span>
|
|
102
|
+
</LinkComponent>
|
|
103
|
+
) : (
|
|
104
|
+
<a
|
|
105
|
+
href={item.href}
|
|
106
|
+
className="flex items-center gap-1 hover:text-[var(--black)] transition-colors"
|
|
107
|
+
>
|
|
108
|
+
{isFirst && showHomeIcon && (
|
|
109
|
+
<House size={iconSize + 2} weight="fill" />
|
|
110
|
+
)}
|
|
111
|
+
{item.icon}
|
|
112
|
+
<span>{item.label}</span>
|
|
113
|
+
</a>
|
|
114
|
+
)
|
|
115
|
+
) : (
|
|
116
|
+
<span
|
|
117
|
+
className={cn(
|
|
118
|
+
'flex items-center gap-1',
|
|
119
|
+
isLast && 'text-[var(--black)] font-medium'
|
|
120
|
+
)}
|
|
121
|
+
aria-current={isLast ? 'page' : undefined}
|
|
122
|
+
>
|
|
123
|
+
{isFirst && showHomeIcon && (
|
|
124
|
+
<House size={iconSize + 2} weight="fill" />
|
|
125
|
+
)}
|
|
126
|
+
{item.icon}
|
|
127
|
+
<span>{item.label}</span>
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
</li>
|
|
131
|
+
)
|
|
132
|
+
})}
|
|
133
|
+
</ol>
|
|
134
|
+
</nav>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
Breadcrumbs.displayName = 'Breadcrumbs'
|
|
139
|
+
|
|
140
|
+
// Simple breadcrumb link component for use within the Breadcrumbs
|
|
141
|
+
export interface BreadcrumbLinkProps
|
|
142
|
+
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
143
|
+
asChild?: boolean
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const BreadcrumbLink = React.forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
|
|
147
|
+
({ className, ...props }, ref) => (
|
|
148
|
+
<a
|
|
149
|
+
ref={ref}
|
|
150
|
+
className={cn(
|
|
151
|
+
'hover:text-[var(--black)] transition-colors',
|
|
152
|
+
className
|
|
153
|
+
)}
|
|
154
|
+
{...props}
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
BreadcrumbLink.displayName = 'BreadcrumbLink'
|
|
159
|
+
|
|
160
|
+
export { Breadcrumbs, BreadcrumbLink }
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '../lib/utils'
|
|
5
|
+
import { Button } from './button'
|
|
6
|
+
import { Input } from './input'
|
|
7
|
+
import { Label } from './label'
|
|
8
|
+
|
|
9
|
+
export type DateRangePreset = '7d' | '30d' | '90d' | 'custom' | 'today' | 'yesterday' | 'this-week' | 'this-month' | 'this-year'
|
|
10
|
+
|
|
11
|
+
export interface DateRangePickerProps {
|
|
12
|
+
/** Currently selected preset */
|
|
13
|
+
value: DateRangePreset
|
|
14
|
+
/** Callback when preset changes */
|
|
15
|
+
onChange: (value: DateRangePreset) => void
|
|
16
|
+
/** Custom start date (ISO string) */
|
|
17
|
+
customStart?: string
|
|
18
|
+
/** Custom end date (ISO string) */
|
|
19
|
+
customEnd?: string
|
|
20
|
+
/** Callback when custom dates change */
|
|
21
|
+
onCustomChange?: (start: string, end: string) => void
|
|
22
|
+
/** Available presets to show */
|
|
23
|
+
presets?: DateRangePreset[]
|
|
24
|
+
/** Size variant */
|
|
25
|
+
size?: 'sm' | 'default'
|
|
26
|
+
/** Additional class name */
|
|
27
|
+
className?: string
|
|
28
|
+
/** Show custom date inputs inline */
|
|
29
|
+
inlineCustom?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const presetLabels: Record<DateRangePreset, string> = {
|
|
33
|
+
'today': 'Today',
|
|
34
|
+
'yesterday': 'Yesterday',
|
|
35
|
+
'7d': 'Last 7 days',
|
|
36
|
+
'30d': 'Last 30 days',
|
|
37
|
+
'90d': 'Last 90 days',
|
|
38
|
+
'this-week': 'This week',
|
|
39
|
+
'this-month': 'This month',
|
|
40
|
+
'this-year': 'This year',
|
|
41
|
+
'custom': 'Custom',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DateRangePicker = React.forwardRef<HTMLDivElement, DateRangePickerProps>(
|
|
45
|
+
(
|
|
46
|
+
{
|
|
47
|
+
value,
|
|
48
|
+
onChange,
|
|
49
|
+
customStart = '',
|
|
50
|
+
customEnd = '',
|
|
51
|
+
onCustomChange,
|
|
52
|
+
presets = ['7d', '30d', '90d', 'custom'],
|
|
53
|
+
size = 'default',
|
|
54
|
+
className,
|
|
55
|
+
inlineCustom = true,
|
|
56
|
+
},
|
|
57
|
+
ref
|
|
58
|
+
) => {
|
|
59
|
+
const buttonSize = size === 'sm' ? 'sm' : 'default'
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div ref={ref} className={cn('flex flex-wrap items-center gap-2', className)}>
|
|
63
|
+
{presets.map((preset) => (
|
|
64
|
+
<Button
|
|
65
|
+
key={preset}
|
|
66
|
+
variant={value === preset ? 'default' : 'outline'}
|
|
67
|
+
size={buttonSize}
|
|
68
|
+
onClick={() => onChange(preset)}
|
|
69
|
+
>
|
|
70
|
+
{presetLabels[preset]}
|
|
71
|
+
</Button>
|
|
72
|
+
))}
|
|
73
|
+
|
|
74
|
+
{value === 'custom' && inlineCustom && onCustomChange && (
|
|
75
|
+
<div className="flex items-center gap-2">
|
|
76
|
+
<div className="space-y-1">
|
|
77
|
+
<Label htmlFor="start-date" className="text-xs sr-only">
|
|
78
|
+
Start
|
|
79
|
+
</Label>
|
|
80
|
+
<Input
|
|
81
|
+
id="start-date"
|
|
82
|
+
type="date"
|
|
83
|
+
value={customStart}
|
|
84
|
+
onChange={(e) => onCustomChange(e.target.value, customEnd)}
|
|
85
|
+
className={cn(size === 'sm' ? 'h-8 text-xs' : 'h-9')}
|
|
86
|
+
aria-label="Start date"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
<span className="text-muted-foreground">to</span>
|
|
90
|
+
<div className="space-y-1">
|
|
91
|
+
<Label htmlFor="end-date" className="text-xs sr-only">
|
|
92
|
+
End
|
|
93
|
+
</Label>
|
|
94
|
+
<Input
|
|
95
|
+
id="end-date"
|
|
96
|
+
type="date"
|
|
97
|
+
value={customEnd}
|
|
98
|
+
onChange={(e) => onCustomChange(customStart, e.target.value)}
|
|
99
|
+
className={cn(size === 'sm' ? 'h-8 text-xs' : 'h-9')}
|
|
100
|
+
aria-label="End date"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
DateRangePicker.displayName = 'DateRangePicker'
|
|
110
|
+
|
|
111
|
+
// Compact variant with dropdown
|
|
112
|
+
export interface DateRangeSelectProps {
|
|
113
|
+
value: DateRangePreset
|
|
114
|
+
onChange: (value: DateRangePreset) => void
|
|
115
|
+
presets?: DateRangePreset[]
|
|
116
|
+
className?: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const DateRangeSelect = React.forwardRef<HTMLSelectElement, DateRangeSelectProps>(
|
|
120
|
+
({ value, onChange, presets = ['7d', '30d', '90d'], className }, ref) => {
|
|
121
|
+
return (
|
|
122
|
+
<select
|
|
123
|
+
ref={ref}
|
|
124
|
+
value={value}
|
|
125
|
+
onChange={(e) => onChange(e.target.value as DateRangePreset)}
|
|
126
|
+
className={cn(
|
|
127
|
+
'h-9 rounded-sm border border-input bg-background px-3 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
128
|
+
className
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
{presets.map((preset) => (
|
|
132
|
+
<option key={preset} value={preset}>
|
|
133
|
+
{presetLabels[preset]}
|
|
134
|
+
</option>
|
|
135
|
+
))}
|
|
136
|
+
</select>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
DateRangeSelect.displayName = 'DateRangeSelect'
|
|
141
|
+
|
|
142
|
+
// Helper to calculate date range from preset
|
|
143
|
+
export function getDateRangeFromPreset(preset: DateRangePreset): { start: Date; end: Date } {
|
|
144
|
+
const now = new Date()
|
|
145
|
+
const end = new Date(now)
|
|
146
|
+
end.setHours(23, 59, 59, 999)
|
|
147
|
+
|
|
148
|
+
let start = new Date(now)
|
|
149
|
+
start.setHours(0, 0, 0, 0)
|
|
150
|
+
|
|
151
|
+
switch (preset) {
|
|
152
|
+
case 'today':
|
|
153
|
+
break
|
|
154
|
+
case 'yesterday':
|
|
155
|
+
start.setDate(start.getDate() - 1)
|
|
156
|
+
end.setDate(end.getDate() - 1)
|
|
157
|
+
break
|
|
158
|
+
case '7d':
|
|
159
|
+
start.setDate(start.getDate() - 7)
|
|
160
|
+
break
|
|
161
|
+
case '30d':
|
|
162
|
+
start.setDate(start.getDate() - 30)
|
|
163
|
+
break
|
|
164
|
+
case '90d':
|
|
165
|
+
start.setDate(start.getDate() - 90)
|
|
166
|
+
break
|
|
167
|
+
case 'this-week':
|
|
168
|
+
start.setDate(start.getDate() - start.getDay())
|
|
169
|
+
break
|
|
170
|
+
case 'this-month':
|
|
171
|
+
start.setDate(1)
|
|
172
|
+
break
|
|
173
|
+
case 'this-year':
|
|
174
|
+
start.setMonth(0, 1)
|
|
175
|
+
break
|
|
176
|
+
default:
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { start, end }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export { DateRangePicker, DateRangeSelect }
|