@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,47 @@
|
|
|
1
|
+
import type { AnyFieldApi } from '@tanstack/react-form'
|
|
2
|
+
import type { ChangeEvent, ComponentProps, ReactNode } from 'react'
|
|
3
|
+
import { Input } from '~/components/ui/input'
|
|
4
|
+
import { InputGroup, InputGroupAddon, InputGroupInput } from '~/components/ui/input-group'
|
|
5
|
+
import { Label } from '~/components/ui/label'
|
|
6
|
+
import { FieldError, hasFieldError } from './field-error'
|
|
7
|
+
|
|
8
|
+
type NativeInputProps = Pick<
|
|
9
|
+
ComponentProps<'input'>,
|
|
10
|
+
'type' | 'placeholder' | 'autoComplete' | 'inputMode' | 'min' | 'max' | 'step'
|
|
11
|
+
>
|
|
12
|
+
|
|
13
|
+
interface TextFieldProps extends NativeInputProps {
|
|
14
|
+
field: AnyFieldApi
|
|
15
|
+
label: string
|
|
16
|
+
description?: string
|
|
17
|
+
/** Optional leading adornment (e.g. a lucide icon). */
|
|
18
|
+
icon?: ReactNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function TextField({ field, label, description, icon, ...inputProps }: TextFieldProps) {
|
|
22
|
+
const shared = {
|
|
23
|
+
'aria-invalid': hasFieldError(field),
|
|
24
|
+
id: field.name,
|
|
25
|
+
name: field.name,
|
|
26
|
+
onBlur: field.handleBlur,
|
|
27
|
+
onChange: (e: ChangeEvent<HTMLInputElement>) => field.handleChange(e.target.value),
|
|
28
|
+
value: String(field.state.value ?? ''),
|
|
29
|
+
...inputProps,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="grid gap-2">
|
|
34
|
+
<Label htmlFor={field.name}>{label}</Label>
|
|
35
|
+
{icon ? (
|
|
36
|
+
<InputGroup className="h-10">
|
|
37
|
+
<InputGroupAddon align="inline-start">{icon}</InputGroupAddon>
|
|
38
|
+
<InputGroupInput {...shared} />
|
|
39
|
+
</InputGroup>
|
|
40
|
+
) : (
|
|
41
|
+
<Input className="h-10" {...shared} />
|
|
42
|
+
)}
|
|
43
|
+
<FieldError field={field} />
|
|
44
|
+
{description ? <p className="text-muted-foreground text-xs">{description}</p> : null}
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -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,69 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useContext, useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export type Theme = 'dark' | 'light' | 'system'
|
|
4
|
+
|
|
5
|
+
export const THEME_STORAGE_KEY = 'theme'
|
|
6
|
+
|
|
7
|
+
interface ThemeProviderState {
|
|
8
|
+
theme: Theme
|
|
9
|
+
setTheme: (theme: Theme) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ThemeProviderContext = createContext<ThemeProviderState | null>(null)
|
|
13
|
+
|
|
14
|
+
const applyTheme = (theme: Theme) => {
|
|
15
|
+
const root = document.documentElement
|
|
16
|
+
const resolved =
|
|
17
|
+
theme === 'system'
|
|
18
|
+
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
19
|
+
? 'dark'
|
|
20
|
+
: 'light'
|
|
21
|
+
: theme
|
|
22
|
+
|
|
23
|
+
root.classList.remove('light', 'dark')
|
|
24
|
+
root.classList.add(resolved)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ThemeProvider({
|
|
28
|
+
children,
|
|
29
|
+
defaultTheme = 'system',
|
|
30
|
+
}: {
|
|
31
|
+
children: ReactNode
|
|
32
|
+
defaultTheme?: Theme
|
|
33
|
+
}) {
|
|
34
|
+
const [theme, setThemeState] = useState<Theme>(defaultTheme)
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const stored = localStorage.getItem(THEME_STORAGE_KEY) as Theme | null
|
|
38
|
+
if (stored) setThemeState(stored)
|
|
39
|
+
}, [])
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
applyTheme(theme)
|
|
43
|
+
if (theme !== 'system') return
|
|
44
|
+
|
|
45
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
|
46
|
+
const onChange = () => applyTheme('system')
|
|
47
|
+
media.addEventListener('change', onChange)
|
|
48
|
+
return () => media.removeEventListener('change', onChange)
|
|
49
|
+
}, [theme])
|
|
50
|
+
|
|
51
|
+
const setTheme = (next: Theme) => {
|
|
52
|
+
localStorage.setItem(THEME_STORAGE_KEY, next)
|
|
53
|
+
setThemeState(next)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<ThemeProviderContext.Provider value={{ theme, setTheme }}>
|
|
58
|
+
{children}
|
|
59
|
+
</ThemeProviderContext.Provider>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useTheme() {
|
|
64
|
+
const context = useContext(ThemeProviderContext)
|
|
65
|
+
if (!context) {
|
|
66
|
+
throw new Error('useTheme must be used within a ThemeProvider')
|
|
67
|
+
}
|
|
68
|
+
return context
|
|
69
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Monitor, Moon, Sun } from 'lucide-react'
|
|
2
|
+
import { useTheme } from '~/components/theme-provider'
|
|
3
|
+
import { Button } from '~/components/ui/button'
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from '~/components/ui/dropdown-menu'
|
|
10
|
+
|
|
11
|
+
export function ThemeToggle() {
|
|
12
|
+
const { setTheme } = useTheme()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<DropdownMenu>
|
|
16
|
+
<DropdownMenuTrigger asChild>
|
|
17
|
+
<Button aria-label="Toggle theme" size="icon" type="button" variant="ghost">
|
|
18
|
+
<Sun className="dark:hidden" />
|
|
19
|
+
<Moon className="hidden dark:block" />
|
|
20
|
+
</Button>
|
|
21
|
+
</DropdownMenuTrigger>
|
|
22
|
+
<DropdownMenuContent align="end">
|
|
23
|
+
<DropdownMenuItem onClick={() => setTheme('light')}>
|
|
24
|
+
<Sun /> Light
|
|
25
|
+
</DropdownMenuItem>
|
|
26
|
+
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
|
27
|
+
<Moon /> Dark
|
|
28
|
+
</DropdownMenuItem>
|
|
29
|
+
<DropdownMenuItem onClick={() => setTheme('system')}>
|
|
30
|
+
<Monitor /> System
|
|
31
|
+
</DropdownMenuItem>
|
|
32
|
+
</DropdownMenuContent>
|
|
33
|
+
</DropdownMenu>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -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 }
|