@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.
Files changed (206) hide show
  1. package/README.md +56 -0
  2. package/_stack/apps/next-base/.dockerignore +10 -0
  3. package/_stack/apps/next-base/Dockerfile +34 -0
  4. package/_stack/apps/next-base/README.md +32 -0
  5. package/_stack/apps/next-base/components.json +25 -0
  6. package/_stack/apps/next-base/drizzle.config.ts +16 -0
  7. package/_stack/apps/next-base/next.config.ts +8 -0
  8. package/_stack/apps/next-base/package.json +70 -0
  9. package/_stack/apps/next-base/postcss.config.mjs +7 -0
  10. package/_stack/apps/next-base/src/app/api/auth/[...all]/route.ts +4 -0
  11. package/_stack/apps/next-base/src/app/api/trpc/[trpc]/route.ts +23 -0
  12. package/_stack/apps/next-base/src/app/auth/_components/forgot-password-form.tsx +92 -0
  13. package/_stack/apps/next-base/src/app/auth/_components/reset-password-form.tsx +105 -0
  14. package/_stack/apps/next-base/src/app/auth/_components/sign-in-form.tsx +126 -0
  15. package/_stack/apps/next-base/src/app/auth/_components/sign-up-form.tsx +139 -0
  16. package/_stack/apps/next-base/src/app/auth/_components/verify-email-actions.tsx +45 -0
  17. package/_stack/apps/next-base/src/app/auth/forgot-password/page.tsx +19 -0
  18. package/_stack/apps/next-base/src/app/auth/layout.tsx +9 -0
  19. package/_stack/apps/next-base/src/app/auth/reset-password/page.tsx +26 -0
  20. package/_stack/apps/next-base/src/app/auth/sign-in/page.tsx +27 -0
  21. package/_stack/apps/next-base/src/app/auth/sign-up/page.tsx +27 -0
  22. package/_stack/apps/next-base/src/app/auth/verify-email/page.tsx +30 -0
  23. package/_stack/apps/next-base/src/app/dashboard/page.tsx +12 -0
  24. package/_stack/apps/next-base/src/app/globals.css +171 -0
  25. package/_stack/apps/next-base/src/app/layout.tsx +23 -0
  26. package/_stack/apps/next-base/src/app/page.tsx +15 -0
  27. package/_stack/apps/next-base/src/components/data-table.tsx +77 -0
  28. package/_stack/apps/next-base/src/components/infinite-data-table.tsx +102 -0
  29. package/_stack/apps/next-base/src/components/sortable-header.tsx +37 -0
  30. package/_stack/apps/next-base/src/components/theme-provider.tsx +8 -0
  31. package/_stack/apps/next-base/src/components/theme-toggle.tsx +37 -0
  32. package/_stack/apps/next-base/src/components/ui/button.tsx +64 -0
  33. package/_stack/apps/next-base/src/components/ui/calendar.tsx +185 -0
  34. package/_stack/apps/next-base/src/components/ui/card.tsx +84 -0
  35. package/_stack/apps/next-base/src/components/ui/date-picker.tsx +85 -0
  36. package/_stack/apps/next-base/src/components/ui/date-range-picker.tsx +138 -0
  37. package/_stack/apps/next-base/src/components/ui/dropdown-menu.tsx +246 -0
  38. package/_stack/apps/next-base/src/components/ui/form.tsx +149 -0
  39. package/_stack/apps/next-base/src/components/ui/input-group.tsx +97 -0
  40. package/_stack/apps/next-base/src/components/ui/input.tsx +18 -0
  41. package/_stack/apps/next-base/src/components/ui/label.tsx +18 -0
  42. package/_stack/apps/next-base/src/components/ui/popover.tsx +76 -0
  43. package/_stack/apps/next-base/src/components/ui/skeleton.tsx +13 -0
  44. package/_stack/apps/next-base/src/components/ui/spinner.tsx +8 -0
  45. package/_stack/apps/next-base/src/components/ui/table.tsx +87 -0
  46. package/_stack/apps/next-base/src/emails/components/components.tsx +199 -0
  47. package/_stack/apps/next-base/src/emails/components/context.tsx +18 -0
  48. package/_stack/apps/next-base/src/emails/components/index.ts +23 -0
  49. package/_stack/apps/next-base/src/emails/components/theme.ts +65 -0
  50. package/_stack/apps/next-base/src/emails/reset-password.tsx +16 -0
  51. package/_stack/apps/next-base/src/emails/verify-email.tsx +15 -0
  52. package/_stack/apps/next-base/src/env.ts +41 -0
  53. package/_stack/apps/next-base/src/features/auth/auth-card.tsx +30 -0
  54. package/_stack/apps/next-base/src/features/auth/form-alert.tsx +27 -0
  55. package/_stack/apps/next-base/src/features/auth/google-button.tsx +66 -0
  56. package/_stack/apps/next-base/src/features/auth/schemas.ts +35 -0
  57. package/_stack/apps/next-base/src/lib/date.ts +4 -0
  58. package/_stack/apps/next-base/src/lib/utils.ts +6 -0
  59. package/_stack/apps/next-base/src/server/api/root.ts +10 -0
  60. package/_stack/apps/next-base/src/server/api/routers/health.router.ts +8 -0
  61. package/_stack/apps/next-base/src/server/api/trpc.ts +56 -0
  62. package/_stack/apps/next-base/src/server/auth/guards.ts +10 -0
  63. package/_stack/apps/next-base/src/server/better-auth/client.ts +9 -0
  64. package/_stack/apps/next-base/src/server/better-auth/config.ts +60 -0
  65. package/_stack/apps/next-base/src/server/better-auth/emails.tsx +25 -0
  66. package/_stack/apps/next-base/src/server/better-auth/index.ts +1 -0
  67. package/_stack/apps/next-base/src/server/better-auth/server.ts +14 -0
  68. package/_stack/apps/next-base/src/server/db/index.ts +6 -0
  69. package/_stack/apps/next-base/src/server/db/keyset.ts +63 -0
  70. package/_stack/apps/next-base/src/server/db/schemas/auth.schema.ts +71 -0
  71. package/_stack/apps/next-base/src/server/db/schemas/index.ts +2 -0
  72. package/_stack/apps/next-base/src/server/db/seed.ts +27 -0
  73. package/_stack/apps/next-base/src/server/email/adapters/resend/config.ts +7 -0
  74. package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +75 -0
  75. package/_stack/apps/next-base/src/server/email/core/address.ts +21 -0
  76. package/_stack/apps/next-base/src/server/email/core/port.ts +89 -0
  77. package/_stack/apps/next-base/src/server/email/core/render.ts +16 -0
  78. package/_stack/apps/next-base/src/server/email/factory.ts +47 -0
  79. package/_stack/apps/next-base/src/server/email/index.ts +36 -0
  80. package/_stack/apps/next-base/src/trpc/query-client.ts +19 -0
  81. package/_stack/apps/next-base/src/trpc/react.tsx +62 -0
  82. package/_stack/apps/next-base/src/trpc/server.ts +23 -0
  83. package/_stack/apps/next-base/tsconfig.json +37 -0
  84. package/_stack/apps/tanstack-base/.dockerignore +13 -0
  85. package/_stack/apps/tanstack-base/Dockerfile +28 -0
  86. package/_stack/apps/tanstack-base/README.md +31 -0
  87. package/_stack/apps/tanstack-base/components.json +25 -0
  88. package/_stack/apps/tanstack-base/drizzle.config.ts +16 -0
  89. package/_stack/apps/tanstack-base/package.json +85 -0
  90. package/_stack/apps/tanstack-base/public/favicon.ico +0 -0
  91. package/_stack/apps/tanstack-base/public/logo192.png +0 -0
  92. package/_stack/apps/tanstack-base/public/logo512.png +0 -0
  93. package/_stack/apps/tanstack-base/public/manifest.json +25 -0
  94. package/_stack/apps/tanstack-base/public/robots.txt +3 -0
  95. package/_stack/apps/tanstack-base/src/components/data-table.tsx +77 -0
  96. package/_stack/apps/tanstack-base/src/components/form/field-error.tsx +18 -0
  97. package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +47 -0
  98. package/_stack/apps/tanstack-base/src/components/infinite-data-table.tsx +102 -0
  99. package/_stack/apps/tanstack-base/src/components/sortable-header.tsx +37 -0
  100. package/_stack/apps/tanstack-base/src/components/theme-provider.tsx +69 -0
  101. package/_stack/apps/tanstack-base/src/components/theme-toggle.tsx +35 -0
  102. package/_stack/apps/tanstack-base/src/components/ui/button.tsx +64 -0
  103. package/_stack/apps/tanstack-base/src/components/ui/calendar.tsx +185 -0
  104. package/_stack/apps/tanstack-base/src/components/ui/card.tsx +84 -0
  105. package/_stack/apps/tanstack-base/src/components/ui/date-picker.tsx +83 -0
  106. package/_stack/apps/tanstack-base/src/components/ui/date-range-picker.tsx +136 -0
  107. package/_stack/apps/tanstack-base/src/components/ui/dropdown-menu.tsx +246 -0
  108. package/_stack/apps/tanstack-base/src/components/ui/input-group.tsx +97 -0
  109. package/_stack/apps/tanstack-base/src/components/ui/input.tsx +18 -0
  110. package/_stack/apps/tanstack-base/src/components/ui/label.tsx +18 -0
  111. package/_stack/apps/tanstack-base/src/components/ui/popover.tsx +74 -0
  112. package/_stack/apps/tanstack-base/src/components/ui/skeleton.tsx +13 -0
  113. package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +8 -0
  114. package/_stack/apps/tanstack-base/src/components/ui/table.tsx +87 -0
  115. package/_stack/apps/tanstack-base/src/emails/components/components.tsx +199 -0
  116. package/_stack/apps/tanstack-base/src/emails/components/context.tsx +18 -0
  117. package/_stack/apps/tanstack-base/src/emails/components/index.ts +23 -0
  118. package/_stack/apps/tanstack-base/src/emails/components/theme.ts +65 -0
  119. package/_stack/apps/tanstack-base/src/emails/reset-password.tsx +16 -0
  120. package/_stack/apps/tanstack-base/src/emails/verify-email.tsx +15 -0
  121. package/_stack/apps/tanstack-base/src/env.ts +41 -0
  122. package/_stack/apps/tanstack-base/src/features/auth/auth-card.tsx +30 -0
  123. package/_stack/apps/tanstack-base/src/features/auth/form-alert.tsx +27 -0
  124. package/_stack/apps/tanstack-base/src/features/auth/google-button.tsx +64 -0
  125. package/_stack/apps/tanstack-base/src/features/auth/schemas.ts +35 -0
  126. package/_stack/apps/tanstack-base/src/lib/date.ts +4 -0
  127. package/_stack/apps/tanstack-base/src/lib/utils.ts +6 -0
  128. package/_stack/apps/tanstack-base/src/router.tsx +40 -0
  129. package/_stack/apps/tanstack-base/src/routes/__root.tsx +73 -0
  130. package/_stack/apps/tanstack-base/src/routes/_authed/dashboard.tsx +12 -0
  131. package/_stack/apps/tanstack-base/src/routes/_authed.tsx +21 -0
  132. package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +14 -0
  133. package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +31 -0
  134. package/_stack/apps/tanstack-base/src/routes/auth/forgot-password.tsx +89 -0
  135. package/_stack/apps/tanstack-base/src/routes/auth/reset-password.tsx +111 -0
  136. package/_stack/apps/tanstack-base/src/routes/auth/sign-in.tsx +117 -0
  137. package/_stack/apps/tanstack-base/src/routes/auth/sign-up.tsx +119 -0
  138. package/_stack/apps/tanstack-base/src/routes/auth/verify-email.tsx +72 -0
  139. package/_stack/apps/tanstack-base/src/routes/auth.tsx +22 -0
  140. package/_stack/apps/tanstack-base/src/routes/index.tsx +18 -0
  141. package/_stack/apps/tanstack-base/src/server/api/root.ts +10 -0
  142. package/_stack/apps/tanstack-base/src/server/api/routers/health.router.ts +8 -0
  143. package/_stack/apps/tanstack-base/src/server/api/trpc.ts +61 -0
  144. package/_stack/apps/tanstack-base/src/server/better-auth/client.ts +9 -0
  145. package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +68 -0
  146. package/_stack/apps/tanstack-base/src/server/better-auth/emails.tsx +25 -0
  147. package/_stack/apps/tanstack-base/src/server/better-auth/index.ts +1 -0
  148. package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +9 -0
  149. package/_stack/apps/tanstack-base/src/server/db/index.ts +6 -0
  150. package/_stack/apps/tanstack-base/src/server/db/keyset.ts +63 -0
  151. package/_stack/apps/tanstack-base/src/server/db/schemas/auth.schema.ts +71 -0
  152. package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +2 -0
  153. package/_stack/apps/tanstack-base/src/server/db/seed.ts +27 -0
  154. package/_stack/apps/tanstack-base/src/server/email/adapters/resend/config.ts +7 -0
  155. package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +75 -0
  156. package/_stack/apps/tanstack-base/src/server/email/core/address.ts +21 -0
  157. package/_stack/apps/tanstack-base/src/server/email/core/port.ts +89 -0
  158. package/_stack/apps/tanstack-base/src/server/email/core/render.ts +16 -0
  159. package/_stack/apps/tanstack-base/src/server/email/factory.ts +47 -0
  160. package/_stack/apps/tanstack-base/src/server/email/index.ts +36 -0
  161. package/_stack/apps/tanstack-base/src/styles.css +171 -0
  162. package/_stack/apps/tanstack-base/src/trpc/devtools.tsx +6 -0
  163. package/_stack/apps/tanstack-base/src/trpc/query-client.ts +19 -0
  164. package/_stack/apps/tanstack-base/src/trpc/react.tsx +49 -0
  165. package/_stack/apps/tanstack-base/src/trpc/server.ts +11 -0
  166. package/_stack/apps/tanstack-base/tsconfig.json +27 -0
  167. package/_stack/apps/tanstack-base/tsr.config.json +3 -0
  168. package/_stack/apps/tanstack-base/vite.config.ts +15 -0
  169. package/_stack/packages/analytics/capability.json +26 -0
  170. package/_stack/packages/cache/capability.json +21 -0
  171. package/_stack/packages/error-tracking/capability.json +21 -0
  172. package/_stack/packages/jobs/capability.json +26 -0
  173. package/_stack/packages/logger/capability.json +21 -0
  174. package/_stack/packages/mailer/capability.json +28 -0
  175. package/_stack/packages/mailer/package.json +37 -0
  176. package/_stack/packages/mailer/src/adapters/brevo/config.ts +7 -0
  177. package/_stack/packages/mailer/src/adapters/brevo/index.ts +90 -0
  178. package/_stack/packages/mailer/src/adapters/resend/config.ts +7 -0
  179. package/_stack/packages/mailer/src/adapters/resend/index.ts +75 -0
  180. package/_stack/packages/mailer/src/adapters/ses/config.ts +13 -0
  181. package/_stack/packages/mailer/src/adapters/ses/index.ts +103 -0
  182. package/_stack/packages/storage/capability.json +32 -0
  183. package/_stack/patterns/README.md +58 -0
  184. package/_stack/patterns/_baseline/README-author.md +10 -0
  185. package/_stack/patterns/_baseline/biome.jsonc +119 -0
  186. package/_stack/patterns/_baseline/env.ts +31 -0
  187. package/_stack/patterns/_baseline/tsconfig.json +27 -0
  188. package/_stack/patterns/better-auth/pattern.json +73 -0
  189. package/_stack/patterns/better-auth-next/pattern.json +76 -0
  190. package/_stack/patterns/data-table/pattern.json +43 -0
  191. package/_stack/patterns/drizzle/pattern.json +61 -0
  192. package/_stack/patterns/trpc/pattern.json +61 -0
  193. package/_stack/patterns/trpc-next/pattern.json +64 -0
  194. package/index.mjs +216 -0
  195. package/lib/build.mjs +64 -0
  196. package/lib/env.mjs +56 -0
  197. package/lib/identity.mjs +33 -0
  198. package/lib/mailer.mjs +95 -0
  199. package/lib/manifests.mjs +61 -0
  200. package/lib/scaffold.mjs +49 -0
  201. package/lib/strip.mjs +132 -0
  202. package/lib/util.mjs +82 -0
  203. package/package.json +51 -0
  204. package/templates/next/layout.no-trpc.tsx +22 -0
  205. package/templates/tanstack/__root.no-trpc.tsx +63 -0
  206. 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 }