@alfredmouelle/create-stack 0.1.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/README.md +56 -0
- package/_stack/apps/next-base/.dockerignore +10 -0
- package/_stack/apps/next-base/Dockerfile +34 -0
- package/_stack/apps/next-base/README.md +32 -0
- package/_stack/apps/next-base/components.json +25 -0
- package/_stack/apps/next-base/drizzle.config.ts +16 -0
- package/_stack/apps/next-base/next.config.ts +8 -0
- package/_stack/apps/next-base/package.json +70 -0
- package/_stack/apps/next-base/postcss.config.mjs +7 -0
- package/_stack/apps/next-base/src/app/api/auth/[...all]/route.ts +4 -0
- package/_stack/apps/next-base/src/app/api/trpc/[trpc]/route.ts +23 -0
- package/_stack/apps/next-base/src/app/auth/_components/forgot-password-form.tsx +92 -0
- package/_stack/apps/next-base/src/app/auth/_components/reset-password-form.tsx +105 -0
- package/_stack/apps/next-base/src/app/auth/_components/sign-in-form.tsx +126 -0
- package/_stack/apps/next-base/src/app/auth/_components/sign-up-form.tsx +139 -0
- package/_stack/apps/next-base/src/app/auth/_components/verify-email-actions.tsx +45 -0
- package/_stack/apps/next-base/src/app/auth/forgot-password/page.tsx +19 -0
- package/_stack/apps/next-base/src/app/auth/layout.tsx +9 -0
- package/_stack/apps/next-base/src/app/auth/reset-password/page.tsx +26 -0
- package/_stack/apps/next-base/src/app/auth/sign-in/page.tsx +27 -0
- package/_stack/apps/next-base/src/app/auth/sign-up/page.tsx +27 -0
- package/_stack/apps/next-base/src/app/auth/verify-email/page.tsx +30 -0
- package/_stack/apps/next-base/src/app/dashboard/page.tsx +12 -0
- package/_stack/apps/next-base/src/app/globals.css +171 -0
- package/_stack/apps/next-base/src/app/layout.tsx +23 -0
- package/_stack/apps/next-base/src/app/page.tsx +15 -0
- package/_stack/apps/next-base/src/components/data-table.tsx +77 -0
- package/_stack/apps/next-base/src/components/infinite-data-table.tsx +102 -0
- package/_stack/apps/next-base/src/components/sortable-header.tsx +37 -0
- package/_stack/apps/next-base/src/components/theme-provider.tsx +8 -0
- package/_stack/apps/next-base/src/components/theme-toggle.tsx +37 -0
- package/_stack/apps/next-base/src/components/ui/button.tsx +64 -0
- package/_stack/apps/next-base/src/components/ui/calendar.tsx +185 -0
- package/_stack/apps/next-base/src/components/ui/card.tsx +84 -0
- package/_stack/apps/next-base/src/components/ui/date-picker.tsx +85 -0
- package/_stack/apps/next-base/src/components/ui/date-range-picker.tsx +138 -0
- package/_stack/apps/next-base/src/components/ui/dropdown-menu.tsx +246 -0
- package/_stack/apps/next-base/src/components/ui/form.tsx +149 -0
- package/_stack/apps/next-base/src/components/ui/input-group.tsx +97 -0
- package/_stack/apps/next-base/src/components/ui/input.tsx +18 -0
- package/_stack/apps/next-base/src/components/ui/label.tsx +18 -0
- package/_stack/apps/next-base/src/components/ui/popover.tsx +76 -0
- package/_stack/apps/next-base/src/components/ui/skeleton.tsx +13 -0
- package/_stack/apps/next-base/src/components/ui/spinner.tsx +8 -0
- package/_stack/apps/next-base/src/components/ui/table.tsx +87 -0
- package/_stack/apps/next-base/src/emails/components/components.tsx +199 -0
- package/_stack/apps/next-base/src/emails/components/context.tsx +18 -0
- package/_stack/apps/next-base/src/emails/components/index.ts +23 -0
- package/_stack/apps/next-base/src/emails/components/theme.ts +65 -0
- package/_stack/apps/next-base/src/emails/reset-password.tsx +16 -0
- package/_stack/apps/next-base/src/emails/verify-email.tsx +15 -0
- package/_stack/apps/next-base/src/env.ts +41 -0
- package/_stack/apps/next-base/src/features/auth/auth-card.tsx +30 -0
- package/_stack/apps/next-base/src/features/auth/form-alert.tsx +27 -0
- package/_stack/apps/next-base/src/features/auth/google-button.tsx +66 -0
- package/_stack/apps/next-base/src/features/auth/schemas.ts +35 -0
- package/_stack/apps/next-base/src/lib/date.ts +4 -0
- package/_stack/apps/next-base/src/lib/utils.ts +6 -0
- package/_stack/apps/next-base/src/server/api/root.ts +10 -0
- package/_stack/apps/next-base/src/server/api/routers/health.router.ts +8 -0
- package/_stack/apps/next-base/src/server/api/trpc.ts +56 -0
- package/_stack/apps/next-base/src/server/auth/guards.ts +10 -0
- package/_stack/apps/next-base/src/server/better-auth/client.ts +9 -0
- package/_stack/apps/next-base/src/server/better-auth/config.ts +60 -0
- package/_stack/apps/next-base/src/server/better-auth/emails.tsx +25 -0
- package/_stack/apps/next-base/src/server/better-auth/index.ts +1 -0
- package/_stack/apps/next-base/src/server/better-auth/server.ts +14 -0
- package/_stack/apps/next-base/src/server/db/index.ts +6 -0
- package/_stack/apps/next-base/src/server/db/keyset.ts +63 -0
- package/_stack/apps/next-base/src/server/db/schemas/auth.schema.ts +71 -0
- package/_stack/apps/next-base/src/server/db/schemas/index.ts +2 -0
- package/_stack/apps/next-base/src/server/db/seed.ts +27 -0
- package/_stack/apps/next-base/src/server/email/adapters/resend/config.ts +7 -0
- package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +75 -0
- package/_stack/apps/next-base/src/server/email/core/address.ts +21 -0
- package/_stack/apps/next-base/src/server/email/core/port.ts +89 -0
- package/_stack/apps/next-base/src/server/email/core/render.ts +16 -0
- package/_stack/apps/next-base/src/server/email/factory.ts +47 -0
- package/_stack/apps/next-base/src/server/email/index.ts +36 -0
- package/_stack/apps/next-base/src/trpc/query-client.ts +19 -0
- package/_stack/apps/next-base/src/trpc/react.tsx +62 -0
- package/_stack/apps/next-base/src/trpc/server.ts +23 -0
- package/_stack/apps/next-base/tsconfig.json +37 -0
- package/_stack/apps/tanstack-base/.dockerignore +13 -0
- package/_stack/apps/tanstack-base/Dockerfile +28 -0
- package/_stack/apps/tanstack-base/README.md +31 -0
- package/_stack/apps/tanstack-base/components.json +25 -0
- package/_stack/apps/tanstack-base/drizzle.config.ts +16 -0
- package/_stack/apps/tanstack-base/package.json +85 -0
- package/_stack/apps/tanstack-base/public/favicon.ico +0 -0
- package/_stack/apps/tanstack-base/public/logo192.png +0 -0
- package/_stack/apps/tanstack-base/public/logo512.png +0 -0
- package/_stack/apps/tanstack-base/public/manifest.json +25 -0
- package/_stack/apps/tanstack-base/public/robots.txt +3 -0
- package/_stack/apps/tanstack-base/src/components/data-table.tsx +77 -0
- package/_stack/apps/tanstack-base/src/components/form/field-error.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +47 -0
- package/_stack/apps/tanstack-base/src/components/infinite-data-table.tsx +102 -0
- package/_stack/apps/tanstack-base/src/components/sortable-header.tsx +37 -0
- package/_stack/apps/tanstack-base/src/components/theme-provider.tsx +69 -0
- package/_stack/apps/tanstack-base/src/components/theme-toggle.tsx +35 -0
- package/_stack/apps/tanstack-base/src/components/ui/button.tsx +64 -0
- package/_stack/apps/tanstack-base/src/components/ui/calendar.tsx +185 -0
- package/_stack/apps/tanstack-base/src/components/ui/card.tsx +84 -0
- package/_stack/apps/tanstack-base/src/components/ui/date-picker.tsx +83 -0
- package/_stack/apps/tanstack-base/src/components/ui/date-range-picker.tsx +136 -0
- package/_stack/apps/tanstack-base/src/components/ui/dropdown-menu.tsx +246 -0
- package/_stack/apps/tanstack-base/src/components/ui/input-group.tsx +97 -0
- package/_stack/apps/tanstack-base/src/components/ui/input.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/ui/label.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/ui/popover.tsx +74 -0
- package/_stack/apps/tanstack-base/src/components/ui/skeleton.tsx +13 -0
- package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +8 -0
- package/_stack/apps/tanstack-base/src/components/ui/table.tsx +87 -0
- package/_stack/apps/tanstack-base/src/emails/components/components.tsx +199 -0
- package/_stack/apps/tanstack-base/src/emails/components/context.tsx +18 -0
- package/_stack/apps/tanstack-base/src/emails/components/index.ts +23 -0
- package/_stack/apps/tanstack-base/src/emails/components/theme.ts +65 -0
- package/_stack/apps/tanstack-base/src/emails/reset-password.tsx +16 -0
- package/_stack/apps/tanstack-base/src/emails/verify-email.tsx +15 -0
- package/_stack/apps/tanstack-base/src/env.ts +41 -0
- package/_stack/apps/tanstack-base/src/features/auth/auth-card.tsx +30 -0
- package/_stack/apps/tanstack-base/src/features/auth/form-alert.tsx +27 -0
- package/_stack/apps/tanstack-base/src/features/auth/google-button.tsx +64 -0
- package/_stack/apps/tanstack-base/src/features/auth/schemas.ts +35 -0
- package/_stack/apps/tanstack-base/src/lib/date.ts +4 -0
- package/_stack/apps/tanstack-base/src/lib/utils.ts +6 -0
- package/_stack/apps/tanstack-base/src/router.tsx +40 -0
- package/_stack/apps/tanstack-base/src/routes/__root.tsx +73 -0
- package/_stack/apps/tanstack-base/src/routes/_authed/dashboard.tsx +12 -0
- package/_stack/apps/tanstack-base/src/routes/_authed.tsx +21 -0
- package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +14 -0
- package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +31 -0
- package/_stack/apps/tanstack-base/src/routes/auth/forgot-password.tsx +89 -0
- package/_stack/apps/tanstack-base/src/routes/auth/reset-password.tsx +111 -0
- package/_stack/apps/tanstack-base/src/routes/auth/sign-in.tsx +117 -0
- package/_stack/apps/tanstack-base/src/routes/auth/sign-up.tsx +119 -0
- package/_stack/apps/tanstack-base/src/routes/auth/verify-email.tsx +72 -0
- package/_stack/apps/tanstack-base/src/routes/auth.tsx +22 -0
- package/_stack/apps/tanstack-base/src/routes/index.tsx +18 -0
- package/_stack/apps/tanstack-base/src/server/api/root.ts +10 -0
- package/_stack/apps/tanstack-base/src/server/api/routers/health.router.ts +8 -0
- package/_stack/apps/tanstack-base/src/server/api/trpc.ts +61 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/client.ts +9 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +68 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/emails.tsx +25 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/index.ts +1 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +9 -0
- package/_stack/apps/tanstack-base/src/server/db/index.ts +6 -0
- package/_stack/apps/tanstack-base/src/server/db/keyset.ts +63 -0
- package/_stack/apps/tanstack-base/src/server/db/schemas/auth.schema.ts +71 -0
- package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +2 -0
- package/_stack/apps/tanstack-base/src/server/db/seed.ts +27 -0
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/config.ts +7 -0
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +75 -0
- package/_stack/apps/tanstack-base/src/server/email/core/address.ts +21 -0
- package/_stack/apps/tanstack-base/src/server/email/core/port.ts +89 -0
- package/_stack/apps/tanstack-base/src/server/email/core/render.ts +16 -0
- package/_stack/apps/tanstack-base/src/server/email/factory.ts +47 -0
- package/_stack/apps/tanstack-base/src/server/email/index.ts +36 -0
- package/_stack/apps/tanstack-base/src/styles.css +171 -0
- package/_stack/apps/tanstack-base/src/trpc/devtools.tsx +6 -0
- package/_stack/apps/tanstack-base/src/trpc/query-client.ts +19 -0
- package/_stack/apps/tanstack-base/src/trpc/react.tsx +49 -0
- package/_stack/apps/tanstack-base/src/trpc/server.ts +11 -0
- package/_stack/apps/tanstack-base/tsconfig.json +27 -0
- package/_stack/apps/tanstack-base/tsr.config.json +3 -0
- package/_stack/apps/tanstack-base/vite.config.ts +15 -0
- package/_stack/packages/analytics/capability.json +26 -0
- package/_stack/packages/cache/capability.json +21 -0
- package/_stack/packages/error-tracking/capability.json +21 -0
- package/_stack/packages/jobs/capability.json +26 -0
- package/_stack/packages/logger/capability.json +21 -0
- package/_stack/packages/mailer/capability.json +28 -0
- package/_stack/packages/mailer/package.json +37 -0
- package/_stack/packages/mailer/src/adapters/brevo/config.ts +7 -0
- package/_stack/packages/mailer/src/adapters/brevo/index.ts +90 -0
- package/_stack/packages/mailer/src/adapters/resend/config.ts +7 -0
- package/_stack/packages/mailer/src/adapters/resend/index.ts +75 -0
- package/_stack/packages/mailer/src/adapters/ses/config.ts +13 -0
- package/_stack/packages/mailer/src/adapters/ses/index.ts +103 -0
- package/_stack/packages/storage/capability.json +32 -0
- package/_stack/patterns/README.md +58 -0
- package/_stack/patterns/_baseline/README-author.md +10 -0
- package/_stack/patterns/_baseline/biome.jsonc +119 -0
- package/_stack/patterns/_baseline/env.ts +31 -0
- package/_stack/patterns/_baseline/tsconfig.json +27 -0
- package/_stack/patterns/better-auth/pattern.json +73 -0
- package/_stack/patterns/better-auth-next/pattern.json +76 -0
- package/_stack/patterns/data-table/pattern.json +43 -0
- package/_stack/patterns/drizzle/pattern.json +61 -0
- package/_stack/patterns/trpc/pattern.json +61 -0
- package/_stack/patterns/trpc-next/pattern.json +64 -0
- package/index.mjs +216 -0
- package/lib/build.mjs +64 -0
- package/lib/env.mjs +56 -0
- package/lib/identity.mjs +33 -0
- package/lib/mailer.mjs +95 -0
- package/lib/manifests.mjs +61 -0
- package/lib/scaffold.mjs +49 -0
- package/lib/strip.mjs +132 -0
- package/lib/util.mjs +82 -0
- package/package.json +51 -0
- package/templates/next/layout.no-trpc.tsx +22 -0
- package/templates/tanstack/__root.no-trpc.tsx +63 -0
- package/templates/tanstack/router.no-trpc.tsx +24 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { flexRender, type Table as ReactTable } from '@tanstack/react-table'
|
|
2
|
+
import { Skeleton } from '~/components/ui/skeleton'
|
|
3
|
+
import {
|
|
4
|
+
Table,
|
|
5
|
+
TableBody,
|
|
6
|
+
TableCell,
|
|
7
|
+
TableHead,
|
|
8
|
+
TableHeader,
|
|
9
|
+
TableRow,
|
|
10
|
+
} from '~/components/ui/table'
|
|
11
|
+
import { cn } from '~/lib/utils'
|
|
12
|
+
|
|
13
|
+
interface InfiniteDataTableProps<TData> {
|
|
14
|
+
table: ReactTable<TData>
|
|
15
|
+
columnCount: number
|
|
16
|
+
isLoading: boolean
|
|
17
|
+
isRefetching: boolean
|
|
18
|
+
isFetchingNextPage: boolean
|
|
19
|
+
hasFilters: boolean
|
|
20
|
+
emptyLabel: string
|
|
21
|
+
emptyFilteredLabel: string
|
|
22
|
+
sentinelRef: (node: HTMLDivElement | null) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function InfiniteDataTable<TData>({
|
|
26
|
+
table,
|
|
27
|
+
columnCount,
|
|
28
|
+
isLoading,
|
|
29
|
+
isRefetching,
|
|
30
|
+
isFetchingNextPage,
|
|
31
|
+
hasFilters,
|
|
32
|
+
emptyLabel,
|
|
33
|
+
emptyFilteredLabel,
|
|
34
|
+
sentinelRef,
|
|
35
|
+
}: InfiniteDataTableProps<TData>) {
|
|
36
|
+
const dataRows = table.getRowModel().rows
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
className={cn(
|
|
41
|
+
'overflow-hidden rounded-lg border transition-opacity',
|
|
42
|
+
isRefetching && 'opacity-60',
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<Table>
|
|
46
|
+
<TableHeader>
|
|
47
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
48
|
+
<TableRow key={headerGroup.id}>
|
|
49
|
+
{headerGroup.headers.map((header) => (
|
|
50
|
+
<TableHead key={header.id}>
|
|
51
|
+
{header.isPlaceholder
|
|
52
|
+
? null
|
|
53
|
+
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
54
|
+
</TableHead>
|
|
55
|
+
))}
|
|
56
|
+
</TableRow>
|
|
57
|
+
))}
|
|
58
|
+
</TableHeader>
|
|
59
|
+
<TableBody>
|
|
60
|
+
{isLoading ? <SkeletonRows columns={columnCount} /> : null}
|
|
61
|
+
|
|
62
|
+
{!isLoading && dataRows.length === 0 ? (
|
|
63
|
+
<TableRow>
|
|
64
|
+
<TableCell className="h-24 text-center text-muted-foreground" colSpan={columnCount}>
|
|
65
|
+
{hasFilters ? emptyFilteredLabel : emptyLabel}
|
|
66
|
+
</TableCell>
|
|
67
|
+
</TableRow>
|
|
68
|
+
) : null}
|
|
69
|
+
|
|
70
|
+
{dataRows.map((row) => (
|
|
71
|
+
<TableRow className="group" key={row.id}>
|
|
72
|
+
{row.getVisibleCells().map((cell) => (
|
|
73
|
+
<TableCell key={cell.id}>
|
|
74
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
75
|
+
</TableCell>
|
|
76
|
+
))}
|
|
77
|
+
</TableRow>
|
|
78
|
+
))}
|
|
79
|
+
|
|
80
|
+
{isFetchingNextPage ? <SkeletonRows columns={columnCount} /> : null}
|
|
81
|
+
</TableBody>
|
|
82
|
+
</Table>
|
|
83
|
+
<div ref={sentinelRef} />
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function SkeletonRows({ columns }: { columns: number }) {
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
{Array.from({ length: 5 }, (_, rowIndex) => (
|
|
92
|
+
<TableRow key={rowIndex}>
|
|
93
|
+
{Array.from({ length: columns }, (_, cellIndex) => (
|
|
94
|
+
<TableCell key={cellIndex}>
|
|
95
|
+
<Skeleton className="h-6 w-full" />
|
|
96
|
+
</TableCell>
|
|
97
|
+
))}
|
|
98
|
+
</TableRow>
|
|
99
|
+
))}
|
|
100
|
+
</>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react'
|
|
2
|
+
import { Button } from '~/components/ui/button'
|
|
3
|
+
|
|
4
|
+
export interface SortState<Field extends string> {
|
|
5
|
+
field: Field
|
|
6
|
+
direction: 'asc' | 'desc'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SortableHeaderProps<Field extends string> {
|
|
10
|
+
label: string
|
|
11
|
+
field: Field
|
|
12
|
+
sort: SortState<Field>
|
|
13
|
+
onSort: (field: Field) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SortableHeader<Field extends string>({
|
|
17
|
+
label,
|
|
18
|
+
field,
|
|
19
|
+
sort,
|
|
20
|
+
onSort,
|
|
21
|
+
}: SortableHeaderProps<Field>) {
|
|
22
|
+
const active = sort.field === field
|
|
23
|
+
const Icon = active ? (sort.direction === 'asc' ? ArrowUp : ArrowDown) : ArrowUpDown
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Button
|
|
27
|
+
className="-ml-2 h-8 cursor-pointer text-muted-foreground data-[active=true]:text-foreground"
|
|
28
|
+
data-active={active}
|
|
29
|
+
onClick={() => onSort(field)}
|
|
30
|
+
size="sm"
|
|
31
|
+
variant="ghost"
|
|
32
|
+
>
|
|
33
|
+
{label}
|
|
34
|
+
<Icon className="size-3.5" />
|
|
35
|
+
</Button>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
4
|
+
import type { ComponentProps } from 'react'
|
|
5
|
+
|
|
6
|
+
export function ThemeProvider({ children, ...props }: ComponentProps<typeof NextThemesProvider>) {
|
|
7
|
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
|
8
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Monitor, Moon, Sun } from 'lucide-react'
|
|
4
|
+
import { useTheme } from 'next-themes'
|
|
5
|
+
import { Button } from '~/components/ui/button'
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuTrigger,
|
|
11
|
+
} from '~/components/ui/dropdown-menu'
|
|
12
|
+
|
|
13
|
+
export function ThemeToggle() {
|
|
14
|
+
const { setTheme } = useTheme()
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<DropdownMenu>
|
|
18
|
+
<DropdownMenuTrigger asChild>
|
|
19
|
+
<Button aria-label="Toggle theme" size="icon" type="button" variant="ghost">
|
|
20
|
+
<Sun className="dark:hidden" />
|
|
21
|
+
<Moon className="hidden dark:block" />
|
|
22
|
+
</Button>
|
|
23
|
+
</DropdownMenuTrigger>
|
|
24
|
+
<DropdownMenuContent align="end">
|
|
25
|
+
<DropdownMenuItem onClick={() => setTheme('light')}>
|
|
26
|
+
<Sun /> Light
|
|
27
|
+
</DropdownMenuItem>
|
|
28
|
+
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
|
29
|
+
<Moon /> Dark
|
|
30
|
+
</DropdownMenuItem>
|
|
31
|
+
<DropdownMenuItem onClick={() => setTheme('system')}>
|
|
32
|
+
<Monitor /> System
|
|
33
|
+
</DropdownMenuItem>
|
|
34
|
+
</DropdownMenuContent>
|
|
35
|
+
</DropdownMenu>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
|
+
import { Slot } from 'radix-ui'
|
|
3
|
+
import type { ComponentProps } from 'react'
|
|
4
|
+
import { cn } from '~/lib/utils'
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"group/button inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-4xl border border-transparent bg-clip-padding font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
|
|
12
|
+
outline:
|
|
13
|
+
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent dark:hover:bg-input/30',
|
|
14
|
+
secondary:
|
|
15
|
+
'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
|
16
|
+
ghost:
|
|
17
|
+
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
|
|
18
|
+
destructive:
|
|
19
|
+
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 dark:hover:bg-destructive/30',
|
|
20
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default:
|
|
24
|
+
'h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5',
|
|
25
|
+
xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
|
|
26
|
+
sm: 'h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
|
27
|
+
lg: 'h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
|
|
28
|
+
icon: 'size-9',
|
|
29
|
+
'icon-xs': "size-6 [&_svg:not([class*='size-'])]:size-3",
|
|
30
|
+
'icon-sm': 'size-8',
|
|
31
|
+
'icon-lg': 'size-10',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
variant: 'default',
|
|
36
|
+
size: 'default',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
function Button({
|
|
42
|
+
className,
|
|
43
|
+
variant = 'default',
|
|
44
|
+
size = 'default',
|
|
45
|
+
asChild = false,
|
|
46
|
+
...props
|
|
47
|
+
}: ComponentProps<'button'> &
|
|
48
|
+
VariantProps<typeof buttonVariants> & {
|
|
49
|
+
asChild?: boolean
|
|
50
|
+
}) {
|
|
51
|
+
const Comp = asChild ? Slot.Root : 'button'
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Comp
|
|
55
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
56
|
+
data-size={size}
|
|
57
|
+
data-slot="button"
|
|
58
|
+
data-variant={variant}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
import { type DayButton, DayPicker, getDefaultClassNames, type Locale } from 'react-day-picker'
|
|
6
|
+
import { Button, buttonVariants } from '~/components/ui/button'
|
|
7
|
+
import { cn } from '~/lib/utils'
|
|
8
|
+
|
|
9
|
+
function Calendar({
|
|
10
|
+
className,
|
|
11
|
+
classNames,
|
|
12
|
+
showOutsideDays = true,
|
|
13
|
+
captionLayout = 'label',
|
|
14
|
+
buttonVariant = 'ghost',
|
|
15
|
+
locale,
|
|
16
|
+
formatters,
|
|
17
|
+
components,
|
|
18
|
+
...props
|
|
19
|
+
}: React.ComponentProps<typeof DayPicker> & {
|
|
20
|
+
buttonVariant?: React.ComponentProps<typeof Button>['variant']
|
|
21
|
+
}) {
|
|
22
|
+
const defaultClassNames = getDefaultClassNames()
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<DayPicker
|
|
26
|
+
captionLayout={captionLayout}
|
|
27
|
+
className={cn(
|
|
28
|
+
'group/calendar bg-background in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent p-3 [--cell-radius:var(--radius-4xl)] [--cell-size:--spacing(8)]',
|
|
29
|
+
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
|
30
|
+
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
classNames={{
|
|
34
|
+
root: cn('w-fit', defaultClassNames.root),
|
|
35
|
+
months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months),
|
|
36
|
+
month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
|
|
37
|
+
nav: cn(
|
|
38
|
+
'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1',
|
|
39
|
+
defaultClassNames.nav,
|
|
40
|
+
),
|
|
41
|
+
button_previous: cn(
|
|
42
|
+
buttonVariants({ variant: buttonVariant }),
|
|
43
|
+
'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
|
|
44
|
+
defaultClassNames.button_previous,
|
|
45
|
+
),
|
|
46
|
+
button_next: cn(
|
|
47
|
+
buttonVariants({ variant: buttonVariant }),
|
|
48
|
+
'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
|
|
49
|
+
defaultClassNames.button_next,
|
|
50
|
+
),
|
|
51
|
+
month_caption: cn(
|
|
52
|
+
'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
|
|
53
|
+
defaultClassNames.month_caption,
|
|
54
|
+
),
|
|
55
|
+
dropdowns: cn(
|
|
56
|
+
'flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm',
|
|
57
|
+
defaultClassNames.dropdowns,
|
|
58
|
+
),
|
|
59
|
+
dropdown_root: cn('relative rounded-(--cell-radius)', defaultClassNames.dropdown_root),
|
|
60
|
+
dropdown: cn('absolute inset-0 bg-popover opacity-0', defaultClassNames.dropdown),
|
|
61
|
+
caption_label: cn(
|
|
62
|
+
'select-none font-medium',
|
|
63
|
+
captionLayout === 'label'
|
|
64
|
+
? 'text-sm'
|
|
65
|
+
: 'flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
|
|
66
|
+
defaultClassNames.caption_label,
|
|
67
|
+
),
|
|
68
|
+
month_grid: cn('w-full border-collapse', defaultClassNames.month_grid),
|
|
69
|
+
weekdays: cn('flex', defaultClassNames.weekdays),
|
|
70
|
+
weekday: cn(
|
|
71
|
+
'flex-1 select-none rounded-(--cell-radius) font-normal text-[0.8rem] text-muted-foreground',
|
|
72
|
+
defaultClassNames.weekday,
|
|
73
|
+
),
|
|
74
|
+
week: cn('mt-2 flex w-full', defaultClassNames.week),
|
|
75
|
+
week_number_header: cn('w-(--cell-size) select-none', defaultClassNames.week_number_header),
|
|
76
|
+
week_number: cn(
|
|
77
|
+
'select-none text-[0.8rem] text-muted-foreground',
|
|
78
|
+
defaultClassNames.week_number,
|
|
79
|
+
),
|
|
80
|
+
day: cn(
|
|
81
|
+
'group/day relative aspect-square h-full w-full select-none rounded-(--cell-radius) p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)',
|
|
82
|
+
props.showWeekNumber
|
|
83
|
+
? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)'
|
|
84
|
+
: '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)',
|
|
85
|
+
defaultClassNames.day,
|
|
86
|
+
),
|
|
87
|
+
range_start: cn(
|
|
88
|
+
'relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted',
|
|
89
|
+
defaultClassNames.range_start,
|
|
90
|
+
),
|
|
91
|
+
range_middle: cn('rounded-none', defaultClassNames.range_middle),
|
|
92
|
+
range_end: cn(
|
|
93
|
+
'relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted',
|
|
94
|
+
defaultClassNames.range_end,
|
|
95
|
+
),
|
|
96
|
+
today: cn(
|
|
97
|
+
'rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none',
|
|
98
|
+
defaultClassNames.today,
|
|
99
|
+
),
|
|
100
|
+
outside: cn(
|
|
101
|
+
'text-muted-foreground aria-selected:text-muted-foreground',
|
|
102
|
+
defaultClassNames.outside,
|
|
103
|
+
),
|
|
104
|
+
disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
|
|
105
|
+
hidden: cn('invisible', defaultClassNames.hidden),
|
|
106
|
+
...classNames,
|
|
107
|
+
}}
|
|
108
|
+
components={{
|
|
109
|
+
Root: ({ className, rootRef, ...props }) => {
|
|
110
|
+
return <div className={cn(className)} data-slot="calendar" ref={rootRef} {...props} />
|
|
111
|
+
},
|
|
112
|
+
Chevron: ({ className, orientation, ...props }) => {
|
|
113
|
+
if (orientation === 'left') {
|
|
114
|
+
return <ChevronLeftIcon className={cn('size-4', className)} {...props} />
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (orientation === 'right') {
|
|
118
|
+
return <ChevronRightIcon className={cn('size-4', className)} {...props} />
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return <ChevronDownIcon className={cn('size-4', className)} {...props} />
|
|
122
|
+
},
|
|
123
|
+
DayButton: ({ ...props }) => <CalendarDayButton locale={locale} {...props} />,
|
|
124
|
+
WeekNumber: ({ children, ...props }) => {
|
|
125
|
+
return (
|
|
126
|
+
<td {...props}>
|
|
127
|
+
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
|
128
|
+
{children}
|
|
129
|
+
</div>
|
|
130
|
+
</td>
|
|
131
|
+
)
|
|
132
|
+
},
|
|
133
|
+
...components,
|
|
134
|
+
}}
|
|
135
|
+
formatters={{
|
|
136
|
+
formatMonthDropdown: (date) => date.toLocaleString(locale?.code, { month: 'short' }),
|
|
137
|
+
...formatters,
|
|
138
|
+
}}
|
|
139
|
+
locale={locale}
|
|
140
|
+
showOutsideDays={showOutsideDays}
|
|
141
|
+
{...props}
|
|
142
|
+
/>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function CalendarDayButton({
|
|
147
|
+
className,
|
|
148
|
+
day,
|
|
149
|
+
modifiers,
|
|
150
|
+
locale,
|
|
151
|
+
...props
|
|
152
|
+
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
|
|
153
|
+
const defaultClassNames = getDefaultClassNames()
|
|
154
|
+
|
|
155
|
+
const ref = React.useRef<HTMLButtonElement>(null)
|
|
156
|
+
React.useEffect(() => {
|
|
157
|
+
if (modifiers.focused) ref.current?.focus()
|
|
158
|
+
}, [modifiers.focused])
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Button
|
|
162
|
+
className={cn(
|
|
163
|
+
'relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 font-normal leading-none data-[range-end=true]:rounded-(--cell-radius) data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-end=true]:bg-primary data-[range-middle=true]:bg-muted data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70',
|
|
164
|
+
defaultClassNames.day,
|
|
165
|
+
className,
|
|
166
|
+
)}
|
|
167
|
+
data-day={day.date.toLocaleDateString(locale?.code)}
|
|
168
|
+
data-range-end={modifiers.range_end}
|
|
169
|
+
data-range-middle={modifiers.range_middle}
|
|
170
|
+
data-range-start={modifiers.range_start}
|
|
171
|
+
data-selected-single={
|
|
172
|
+
modifiers.selected &&
|
|
173
|
+
!modifiers.range_start &&
|
|
174
|
+
!modifiers.range_end &&
|
|
175
|
+
!modifiers.range_middle
|
|
176
|
+
}
|
|
177
|
+
ref={ref}
|
|
178
|
+
size="icon"
|
|
179
|
+
variant="ghost"
|
|
180
|
+
{...props}
|
|
181
|
+
/>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { Calendar, CalendarDayButton }
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react'
|
|
2
|
+
import { cn } from '~/lib/utils'
|
|
3
|
+
|
|
4
|
+
function Card({
|
|
5
|
+
className,
|
|
6
|
+
size = 'default',
|
|
7
|
+
...props
|
|
8
|
+
}: ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={cn(
|
|
12
|
+
'group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-4xl bg-card py-(--card-spacing) text-card-foreground text-sm shadow-md ring-1 ring-foreground/5 [--card-spacing:--spacing(6)] has-[>img:first-child]:pt-0 dark:ring-foreground/10 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-4xl *:[img:last-child]:rounded-b-4xl',
|
|
13
|
+
className,
|
|
14
|
+
)}
|
|
15
|
+
data-size={size}
|
|
16
|
+
data-slot="card"
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function CardHeader({ className, ...props }: ComponentProps<'div'>) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={cn(
|
|
26
|
+
'group/card-header @container/card-header grid auto-rows-min items-start gap-1.5 rounded-t-4xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)',
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
data-slot="card-header"
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function CardTitle({ className, ...props }: ComponentProps<'div'>) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={cn('font-heading font-medium text-base', className)}
|
|
39
|
+
data-slot="card-title"
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function CardDescription({ className, ...props }: ComponentProps<'div'>) {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
className={cn('text-muted-foreground text-sm', className)}
|
|
49
|
+
data-slot="card-description"
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function CardAction({ className, ...props }: ComponentProps<'div'>) {
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
|
59
|
+
data-slot="card-action"
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function CardContent({ className, ...props }: ComponentProps<'div'>) {
|
|
66
|
+
return (
|
|
67
|
+
<div className={cn('px-(--card-spacing)', className)} data-slot="card-content" {...props} />
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function CardFooter({ className, ...props }: ComponentProps<'div'>) {
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
className={cn(
|
|
75
|
+
'flex items-center rounded-b-4xl px-(--card-spacing) [.border-t]:pt-(--card-spacing)',
|
|
76
|
+
className,
|
|
77
|
+
)}
|
|
78
|
+
data-slot="card-footer"
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { format, isValid, parseISO } from 'date-fns'
|
|
4
|
+
import { CalendarIcon, X } from 'lucide-react'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import { Button } from '~/components/ui/button'
|
|
7
|
+
import { Calendar } from '~/components/ui/calendar'
|
|
8
|
+
import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover'
|
|
9
|
+
import { toISODate } from '~/lib/date'
|
|
10
|
+
import { cn } from '~/lib/utils'
|
|
11
|
+
|
|
12
|
+
interface DatePickerProps {
|
|
13
|
+
value: string
|
|
14
|
+
onChange: (value: string) => void
|
|
15
|
+
placeholder?: string
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
className?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function DatePicker({
|
|
21
|
+
value,
|
|
22
|
+
onChange,
|
|
23
|
+
placeholder = 'Pick a date',
|
|
24
|
+
disabled,
|
|
25
|
+
className,
|
|
26
|
+
}: DatePickerProps) {
|
|
27
|
+
const [open, setOpen] = useState(false)
|
|
28
|
+
const parsed = value ? parseISO(value) : undefined
|
|
29
|
+
const date = parsed && isValid(parsed) ? parsed : undefined
|
|
30
|
+
|
|
31
|
+
const clear = () => {
|
|
32
|
+
onChange('')
|
|
33
|
+
setOpen(false)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Popover onOpenChange={setOpen} open={open}>
|
|
38
|
+
<div className={cn('group relative w-full', className)}>
|
|
39
|
+
<PopoverTrigger asChild>
|
|
40
|
+
<Button
|
|
41
|
+
className={cn(
|
|
42
|
+
'w-full cursor-pointer justify-start px-3 text-left font-normal',
|
|
43
|
+
!date && 'text-muted-foreground',
|
|
44
|
+
)}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
variant="outline"
|
|
47
|
+
>
|
|
48
|
+
<CalendarIcon
|
|
49
|
+
className={cn(
|
|
50
|
+
'mr-2 size-4 opacity-50 transition-opacity',
|
|
51
|
+
date && !disabled && 'group-hover:opacity-0',
|
|
52
|
+
)}
|
|
53
|
+
/>
|
|
54
|
+
{date ? format(date, 'PPP') : <span>{placeholder}</span>}
|
|
55
|
+
</Button>
|
|
56
|
+
</PopoverTrigger>
|
|
57
|
+
|
|
58
|
+
{date && !disabled && (
|
|
59
|
+
<button
|
|
60
|
+
aria-label="Clear date"
|
|
61
|
+
className="absolute top-1/2 left-3 z-10 flex size-4 -translate-y-1/2 cursor-pointer items-center justify-center text-muted-foreground opacity-0 transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100"
|
|
62
|
+
onClick={clear}
|
|
63
|
+
type="button"
|
|
64
|
+
>
|
|
65
|
+
<X className="size-4" />
|
|
66
|
+
</button>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<PopoverContent align="start" className="w-auto p-0">
|
|
71
|
+
<Calendar
|
|
72
|
+
autoFocus
|
|
73
|
+
mode="single"
|
|
74
|
+
onSelect={(selected) => {
|
|
75
|
+
if (selected) {
|
|
76
|
+
onChange(toISODate(selected))
|
|
77
|
+
setOpen(false)
|
|
78
|
+
}
|
|
79
|
+
}}
|
|
80
|
+
selected={date}
|
|
81
|
+
/>
|
|
82
|
+
</PopoverContent>
|
|
83
|
+
</Popover>
|
|
84
|
+
)
|
|
85
|
+
}
|