meyi-vault-client-dev 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -0
- package/package.json +69 -0
- package/src/VaultApp.tsx +82 -0
- package/src/components/datetime-picker.tsx +262 -0
- package/src/components/dialogs.tsx +467 -0
- package/src/components/searchable-multi-select.tsx +275 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/calendar.tsx +210 -0
- package/src/components/ui/command.tsx +181 -0
- package/src/components/ui/dialog.tsx +140 -0
- package/src/components/ui/input.tsx +27 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/popover.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +64 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/table.tsx +122 -0
- package/src/components/vault/entries-table.tsx +283 -0
- package/src/components/vault/password-cell.tsx +26 -0
- package/src/context/VaultProvider.tsx +49 -0
- package/src/hooks/useVault.ts +177 -0
- package/src/index.tsx +99 -0
- package/src/lib/utils.ts +6 -0
- package/src/pages/AuditPage.tsx +408 -0
- package/src/pages/EntriesPage.tsx +111 -0
- package/src/pages/VaultPage.tsx +184 -0
- package/src/pages/meyi-connect-password.code-workspace +11 -0
- package/src/services/api.ts +139 -0
- package/src/style.css +5 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// components/searchable-multi-select.tsx
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
3
|
+
import { Check, ChevronsUpDown, Loader2, X } from 'lucide-react'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import {
|
|
7
|
+
Command,
|
|
8
|
+
CommandEmpty,
|
|
9
|
+
CommandGroup,
|
|
10
|
+
CommandInput,
|
|
11
|
+
CommandItem,
|
|
12
|
+
CommandList,
|
|
13
|
+
} from '@/components/ui/command'
|
|
14
|
+
import {
|
|
15
|
+
Popover,
|
|
16
|
+
PopoverContent,
|
|
17
|
+
PopoverTrigger,
|
|
18
|
+
} from '@/components/ui/popover'
|
|
19
|
+
import { Badge } from '@/components/ui/badge'
|
|
20
|
+
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
21
|
+
|
|
22
|
+
export interface FieldMapping {
|
|
23
|
+
value: string // The field name for the value (e.g., 'id', 'user_id')
|
|
24
|
+
label: string // The field name for the label (e.g., 'name', 'username', 'full_name')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SearchableMultiSelectProps {
|
|
28
|
+
value: string[]
|
|
29
|
+
onChange: (value: string[]) => void
|
|
30
|
+
fetchData: (search: string, page: number) => Promise<{
|
|
31
|
+
data: any[]
|
|
32
|
+
hasMore: boolean
|
|
33
|
+
}>
|
|
34
|
+
fieldMapping: FieldMapping
|
|
35
|
+
placeholder?: string
|
|
36
|
+
disabled?: boolean
|
|
37
|
+
className?: string
|
|
38
|
+
emptyText?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function SearchableMultiSelect({
|
|
42
|
+
value = [],
|
|
43
|
+
onChange,
|
|
44
|
+
fetchData,
|
|
45
|
+
fieldMapping,
|
|
46
|
+
placeholder = 'Select items...',
|
|
47
|
+
disabled = false,
|
|
48
|
+
className,
|
|
49
|
+
emptyText = 'No items found.',
|
|
50
|
+
}: SearchableMultiSelectProps) {
|
|
51
|
+
const [open, setOpen] = useState(false)
|
|
52
|
+
const [search, setSearch] = useState('')
|
|
53
|
+
const [options, setOptions] = useState<any[]>([])
|
|
54
|
+
const [selectedItems, setSelectedItems] = useState<any[]>([])
|
|
55
|
+
const [loading, setLoading] = useState(false)
|
|
56
|
+
const [page, setPage] = useState(1)
|
|
57
|
+
const [hasMore, setHasMore] = useState(true)
|
|
58
|
+
const observerTarget = useRef<HTMLDivElement>(null)
|
|
59
|
+
|
|
60
|
+
// Fetch data
|
|
61
|
+
const loadData = useCallback(
|
|
62
|
+
async (searchTerm: string, pageNum: number, reset: boolean = false) => {
|
|
63
|
+
setLoading(true)
|
|
64
|
+
try {
|
|
65
|
+
const result = await fetchData(searchTerm, pageNum)
|
|
66
|
+
setOptions((prev) => (reset ? result.data : [...prev, ...result.data]))
|
|
67
|
+
setHasMore(result.hasMore)
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Error fetching data:', error)
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false)
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
[fetchData]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// Initial load and search
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const timeoutId = setTimeout(() => {
|
|
80
|
+
setPage(1)
|
|
81
|
+
loadData(search, 1, true)
|
|
82
|
+
}, 300)
|
|
83
|
+
return () => clearTimeout(timeoutId)
|
|
84
|
+
}, [search, loadData])
|
|
85
|
+
|
|
86
|
+
// Intersection observer for pagination
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const observer = new IntersectionObserver(
|
|
89
|
+
(entries) => {
|
|
90
|
+
if (entries[0].isIntersecting && hasMore && !loading) {
|
|
91
|
+
const nextPage = page + 1
|
|
92
|
+
setPage(nextPage)
|
|
93
|
+
loadData(search, nextPage, false)
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{ threshold: 1.0 }
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const currentTarget = observerTarget.current
|
|
100
|
+
if (currentTarget) {
|
|
101
|
+
observer.observe(currentTarget)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
if (currentTarget) {
|
|
106
|
+
observer.unobserve(currentTarget)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, [hasMore, loading, page, search, loadData])
|
|
110
|
+
|
|
111
|
+
// Load selected items details
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const loadSelectedDetails = async () => {
|
|
114
|
+
if (value.length > 0 && options.length > 0) {
|
|
115
|
+
const selected = options.filter((opt) =>
|
|
116
|
+
value.includes(opt[fieldMapping.value])
|
|
117
|
+
)
|
|
118
|
+
setSelectedItems(selected)
|
|
119
|
+
} else if (value.length === 0) {
|
|
120
|
+
setSelectedItems([])
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
loadSelectedDetails()
|
|
124
|
+
}, [value, options, fieldMapping.value])
|
|
125
|
+
|
|
126
|
+
const handleSelect = (item: any) => {
|
|
127
|
+
const itemValue = item[fieldMapping.value]
|
|
128
|
+
const newValue = value.includes(itemValue)
|
|
129
|
+
? value.filter((v) => v !== itemValue)
|
|
130
|
+
: [...value, itemValue]
|
|
131
|
+
|
|
132
|
+
onChange(newValue)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const handleRemove = (itemValue: string, e?: React.MouseEvent) => {
|
|
136
|
+
e?.stopPropagation()
|
|
137
|
+
onChange(value.filter((v) => v !== itemValue))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const getInitials = (name: string) => {
|
|
141
|
+
if (!name) return 'U'
|
|
142
|
+
return name
|
|
143
|
+
.split(' ')
|
|
144
|
+
.map((n) => n[0])
|
|
145
|
+
.join('')
|
|
146
|
+
.toUpperCase()
|
|
147
|
+
.slice(0, 2)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const getSelectedItem = (itemValue: string) => {
|
|
151
|
+
return selectedItems.find((item) => item[fieldMapping.value] === itemValue)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const handleClearSearch = () => {
|
|
155
|
+
setSearch('')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
160
|
+
<PopoverTrigger asChild>
|
|
161
|
+
<Button
|
|
162
|
+
variant="outline"
|
|
163
|
+
role="combobox"
|
|
164
|
+
aria-expanded={open}
|
|
165
|
+
disabled={disabled}
|
|
166
|
+
className={cn(
|
|
167
|
+
'w-full justify-between h-auto min-h-10 px-3 py-2',
|
|
168
|
+
className
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
<div className="flex flex-wrap gap-1.5 flex-1 max-h-32 overflow-y-auto py-1 scrollbar-hide">
|
|
172
|
+
{value.length === 0 ? (
|
|
173
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
174
|
+
) : (
|
|
175
|
+
value.map((itemValue) => {
|
|
176
|
+
const item = getSelectedItem(itemValue)
|
|
177
|
+
const displayName = item
|
|
178
|
+
? item[fieldMapping.label]
|
|
179
|
+
: itemValue
|
|
180
|
+
return (
|
|
181
|
+
<Badge
|
|
182
|
+
key={itemValue}
|
|
183
|
+
variant="secondary"
|
|
184
|
+
className="pl-2 pr-1 py-1 gap-1.5 rounded-full hover:bg-secondary/80 border-primary/10"
|
|
185
|
+
>
|
|
186
|
+
<div className="flex items-center gap-1.5">
|
|
187
|
+
<div className="h-5 w-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-[10px] font-bold">
|
|
188
|
+
{getInitials(displayName)}
|
|
189
|
+
</div>
|
|
190
|
+
<span className="text-xs max-w-[120px] truncate font-medium">
|
|
191
|
+
{displayName}
|
|
192
|
+
</span>
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
onClick={(e) => handleRemove(itemValue, e)}
|
|
196
|
+
className="ml-0.5 rounded-full hover:bg-destructive/10 hover:text-destructive p-0.5 transition-colors"
|
|
197
|
+
>
|
|
198
|
+
<X className="h-3 w-3" />
|
|
199
|
+
</button>
|
|
200
|
+
</div>
|
|
201
|
+
</Badge>
|
|
202
|
+
)
|
|
203
|
+
})
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
207
|
+
</Button>
|
|
208
|
+
</PopoverTrigger>
|
|
209
|
+
<PopoverContent className="w-full p-0" align="start">
|
|
210
|
+
<Command shouldFilter={false}>
|
|
211
|
+
<div className="relative">
|
|
212
|
+
<CommandInput
|
|
213
|
+
placeholder="Search..."
|
|
214
|
+
value={search}
|
|
215
|
+
onValueChange={setSearch}
|
|
216
|
+
className="pr-8"
|
|
217
|
+
/>
|
|
218
|
+
{search && (
|
|
219
|
+
<Button
|
|
220
|
+
type="button"
|
|
221
|
+
variant="ghost"
|
|
222
|
+
size="sm"
|
|
223
|
+
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-6 w-6 p-0 hover:bg-muted"
|
|
224
|
+
onClick={handleClearSearch}
|
|
225
|
+
>
|
|
226
|
+
<X className="h-3 w-3" />
|
|
227
|
+
</Button>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
<CommandList>
|
|
231
|
+
<CommandEmpty>{emptyText}</CommandEmpty>
|
|
232
|
+
<CommandGroup>
|
|
233
|
+
<ScrollArea className="h-72">
|
|
234
|
+
{Array.isArray(options)
|
|
235
|
+
? options.map((item) => {
|
|
236
|
+
const itemValue = item[fieldMapping.value]
|
|
237
|
+
const itemLabel = item[fieldMapping.label]
|
|
238
|
+
const isSelected = value.includes(itemValue)
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<CommandItem
|
|
242
|
+
key={itemValue}
|
|
243
|
+
onSelect={() => handleSelect(item)}
|
|
244
|
+
className="cursor-pointer"
|
|
245
|
+
>
|
|
246
|
+
<div className="flex items-center gap-2 flex-1">
|
|
247
|
+
<div className="h-7 w-7 rounded-full bg-muted flex items-center justify-center text-xs font-medium">
|
|
248
|
+
{getInitials(itemLabel)}
|
|
249
|
+
</div>
|
|
250
|
+
<span className="flex-1">{itemLabel}</span>
|
|
251
|
+
<Check
|
|
252
|
+
className={cn(
|
|
253
|
+
'h-4 w-4',
|
|
254
|
+
isSelected ? 'opacity-100' : 'opacity-0'
|
|
255
|
+
)}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
</CommandItem>
|
|
259
|
+
)
|
|
260
|
+
})
|
|
261
|
+
: null}
|
|
262
|
+
{loading && (
|
|
263
|
+
<div className="flex items-center justify-center py-4">
|
|
264
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
<div ref={observerTarget} className="h-2" />
|
|
268
|
+
</ScrollArea>
|
|
269
|
+
</CommandGroup>
|
|
270
|
+
</CommandList>
|
|
271
|
+
</Command>
|
|
272
|
+
</PopoverContent>
|
|
273
|
+
</Popover>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
const badgeVariants = cva(
|
|
7
|
+
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
success: 'bg-green-500 text-white',
|
|
12
|
+
default:
|
|
13
|
+
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
|
14
|
+
secondary:
|
|
15
|
+
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
|
16
|
+
destructive:
|
|
17
|
+
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
|
18
|
+
outline:
|
|
19
|
+
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: 'default',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
function Badge({
|
|
29
|
+
className,
|
|
30
|
+
variant,
|
|
31
|
+
asChild = false,
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<'span'> &
|
|
34
|
+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
35
|
+
const Comp = asChild ? Slot : 'span'
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Comp
|
|
39
|
+
data-slot='badge'
|
|
40
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default:
|
|
12
|
+
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
|
13
|
+
destructive:
|
|
14
|
+
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
|
15
|
+
outline:
|
|
16
|
+
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
|
17
|
+
secondary:
|
|
18
|
+
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
|
19
|
+
ghost:
|
|
20
|
+
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
|
21
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
25
|
+
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
|
26
|
+
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
|
27
|
+
icon: 'size-9',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: 'default',
|
|
32
|
+
size: 'default',
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
function Button({
|
|
38
|
+
className,
|
|
39
|
+
variant,
|
|
40
|
+
size,
|
|
41
|
+
asChild = false,
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<'button'> &
|
|
44
|
+
VariantProps<typeof buttonVariants> & {
|
|
45
|
+
asChild?: boolean
|
|
46
|
+
}) {
|
|
47
|
+
const Comp = asChild ? Slot : 'button'
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Comp
|
|
51
|
+
data-slot='button'
|
|
52
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
ChevronDownIcon,
|
|
4
|
+
ChevronLeftIcon,
|
|
5
|
+
ChevronRightIcon,
|
|
6
|
+
} from 'lucide-react'
|
|
7
|
+
import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'
|
|
8
|
+
import { cn } from '@/lib/utils'
|
|
9
|
+
import { Button, buttonVariants } from '@/components/ui/button'
|
|
10
|
+
|
|
11
|
+
function Calendar({
|
|
12
|
+
className,
|
|
13
|
+
classNames,
|
|
14
|
+
showOutsideDays = true,
|
|
15
|
+
captionLayout = 'label',
|
|
16
|
+
buttonVariant = 'ghost',
|
|
17
|
+
formatters,
|
|
18
|
+
components,
|
|
19
|
+
...props
|
|
20
|
+
}: React.ComponentProps<typeof DayPicker> & {
|
|
21
|
+
buttonVariant?: React.ComponentProps<typeof Button>['variant']
|
|
22
|
+
}) {
|
|
23
|
+
const defaultClassNames = getDefaultClassNames()
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<DayPicker
|
|
27
|
+
showOutsideDays={showOutsideDays}
|
|
28
|
+
className={cn(
|
|
29
|
+
'bg-card group/calendar p-3',
|
|
30
|
+
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
|
31
|
+
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
captionLayout={captionLayout}
|
|
35
|
+
formatters={{
|
|
36
|
+
formatMonthDropdown: (date) =>
|
|
37
|
+
date.toLocaleString('default', { month: 'short' }),
|
|
38
|
+
...formatters,
|
|
39
|
+
}}
|
|
40
|
+
classNames={{
|
|
41
|
+
root: cn('w-fit', defaultClassNames.root),
|
|
42
|
+
months: cn(
|
|
43
|
+
'flex gap-4 flex-col md:flex-row relative',
|
|
44
|
+
defaultClassNames.months
|
|
45
|
+
),
|
|
46
|
+
month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
|
|
47
|
+
nav: cn(
|
|
48
|
+
'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
|
|
49
|
+
defaultClassNames.nav
|
|
50
|
+
),
|
|
51
|
+
button_previous: cn(
|
|
52
|
+
buttonVariants({ variant: buttonVariant }),
|
|
53
|
+
'h-8 w-8 aria-disabled:opacity-50 p-0 select-none',
|
|
54
|
+
defaultClassNames.button_previous
|
|
55
|
+
),
|
|
56
|
+
button_next: cn(
|
|
57
|
+
buttonVariants({ variant: buttonVariant }),
|
|
58
|
+
'h-8 w-8 aria-disabled:opacity-50 p-0 select-none',
|
|
59
|
+
defaultClassNames.button_next
|
|
60
|
+
),
|
|
61
|
+
month_caption: cn(
|
|
62
|
+
'flex items-center justify-center h-8 w-full px-8',
|
|
63
|
+
defaultClassNames.month_caption
|
|
64
|
+
),
|
|
65
|
+
dropdowns: cn(
|
|
66
|
+
'w-full flex items-center text-sm font-medium justify-center h-8 gap-1.5',
|
|
67
|
+
defaultClassNames.dropdowns
|
|
68
|
+
),
|
|
69
|
+
dropdown_root: cn(
|
|
70
|
+
'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md',
|
|
71
|
+
defaultClassNames.dropdown_root
|
|
72
|
+
),
|
|
73
|
+
dropdown: cn(
|
|
74
|
+
'absolute bg-popover inset-0 opacity-0',
|
|
75
|
+
defaultClassNames.dropdown
|
|
76
|
+
),
|
|
77
|
+
caption_label: cn(
|
|
78
|
+
'select-none font-medium',
|
|
79
|
+
captionLayout === 'label'
|
|
80
|
+
? 'text-sm'
|
|
81
|
+
: 'rounded-md ps-2 pe-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5',
|
|
82
|
+
defaultClassNames.caption_label
|
|
83
|
+
),
|
|
84
|
+
month_grid: 'w-full border-collapse',
|
|
85
|
+
weekdays: cn('flex gap-1', defaultClassNames.weekdays),
|
|
86
|
+
weekday: cn(
|
|
87
|
+
'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none text-center w-8',
|
|
88
|
+
defaultClassNames.weekday
|
|
89
|
+
),
|
|
90
|
+
week: cn('flex w-full mt-2 gap-1', defaultClassNames.week),
|
|
91
|
+
week_number_header: cn(
|
|
92
|
+
'select-none w-8 text-center',
|
|
93
|
+
defaultClassNames.week_number_header
|
|
94
|
+
),
|
|
95
|
+
week_number: cn(
|
|
96
|
+
'text-[0.8rem] select-none text-muted-foreground w-8 text-center',
|
|
97
|
+
defaultClassNames.week_number
|
|
98
|
+
),
|
|
99
|
+
day: cn(
|
|
100
|
+
'relative w-8 h-8 p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none',
|
|
101
|
+
defaultClassNames.day
|
|
102
|
+
),
|
|
103
|
+
range_start: cn(
|
|
104
|
+
'rounded-l-md bg-accent',
|
|
105
|
+
defaultClassNames.range_start
|
|
106
|
+
),
|
|
107
|
+
range_middle: cn('rounded-none', defaultClassNames.range_middle),
|
|
108
|
+
range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
|
|
109
|
+
today: cn(
|
|
110
|
+
'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
|
|
111
|
+
defaultClassNames.today
|
|
112
|
+
),
|
|
113
|
+
outside: cn(
|
|
114
|
+
'text-muted-foreground aria-selected:text-muted-foreground',
|
|
115
|
+
defaultClassNames.outside
|
|
116
|
+
),
|
|
117
|
+
disabled: cn(
|
|
118
|
+
'text-muted-foreground opacity-50',
|
|
119
|
+
defaultClassNames.disabled
|
|
120
|
+
),
|
|
121
|
+
hidden: cn('invisible', defaultClassNames.hidden),
|
|
122
|
+
...classNames,
|
|
123
|
+
}}
|
|
124
|
+
components={{
|
|
125
|
+
Root: ({ className, rootRef, ...props }) => {
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
data-slot='calendar'
|
|
129
|
+
ref={rootRef}
|
|
130
|
+
className={cn(className)}
|
|
131
|
+
{...props}
|
|
132
|
+
/>
|
|
133
|
+
)
|
|
134
|
+
},
|
|
135
|
+
Chevron: ({ className, orientation, ...props }) => {
|
|
136
|
+
if (orientation === 'left') {
|
|
137
|
+
return (
|
|
138
|
+
<ChevronLeftIcon className={cn('size-4', className)} {...props} />
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (orientation === 'right') {
|
|
143
|
+
return (
|
|
144
|
+
<ChevronRightIcon
|
|
145
|
+
className={cn('size-4', className)}
|
|
146
|
+
{...props}
|
|
147
|
+
/>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<ChevronDownIcon className={cn('size-4', className)} {...props} />
|
|
153
|
+
)
|
|
154
|
+
},
|
|
155
|
+
DayButton: CalendarDayButton,
|
|
156
|
+
WeekNumber: ({ children, ...props }) => {
|
|
157
|
+
return (
|
|
158
|
+
<td {...props}>
|
|
159
|
+
<div className='flex size-(--cell-size) items-center justify-center text-center'>
|
|
160
|
+
{children}
|
|
161
|
+
</div>
|
|
162
|
+
</td>
|
|
163
|
+
)
|
|
164
|
+
},
|
|
165
|
+
...components,
|
|
166
|
+
}}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function CalendarDayButton({
|
|
173
|
+
className,
|
|
174
|
+
day,
|
|
175
|
+
modifiers,
|
|
176
|
+
...props
|
|
177
|
+
}: React.ComponentProps<typeof DayButton>) {
|
|
178
|
+
const defaultClassNames = getDefaultClassNames()
|
|
179
|
+
|
|
180
|
+
const ref = React.useRef<HTMLButtonElement>(null)
|
|
181
|
+
React.useEffect(() => {
|
|
182
|
+
if (modifiers.focused) ref.current?.focus()
|
|
183
|
+
}, [modifiers.focused])
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<Button
|
|
187
|
+
ref={ref}
|
|
188
|
+
variant='ghost'
|
|
189
|
+
size='icon'
|
|
190
|
+
data-day={day.date.toLocaleDateString()}
|
|
191
|
+
data-selected-single={
|
|
192
|
+
modifiers.selected &&
|
|
193
|
+
!modifiers.range_start &&
|
|
194
|
+
!modifiers.range_end &&
|
|
195
|
+
!modifiers.range_middle
|
|
196
|
+
}
|
|
197
|
+
data-range-start={modifiers.range_start}
|
|
198
|
+
data-range-end={modifiers.range_end}
|
|
199
|
+
data-range-middle={modifiers.range_middle}
|
|
200
|
+
className={cn(
|
|
201
|
+
'data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square h-8 w-8 min-w-8 flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70',
|
|
202
|
+
defaultClassNames.day,
|
|
203
|
+
className
|
|
204
|
+
)}
|
|
205
|
+
{...props}
|
|
206
|
+
/>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { Calendar, CalendarDayButton }
|