@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,138 @@
1
+ 'use client'
2
+
3
+ import { format, parseISO } from 'date-fns'
4
+ import { CalendarRange, X } from 'lucide-react'
5
+ import { type ComponentProps, useState } from 'react'
6
+ import type { DateRange } from 'react-day-picker'
7
+ import { Button } from '~/components/ui/button'
8
+ import { Calendar } from '~/components/ui/calendar'
9
+ import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover'
10
+ import { toISODate } from '~/lib/date'
11
+ import { cn } from '~/lib/utils'
12
+
13
+ export interface DateRangeValue {
14
+ from: string
15
+ to: string
16
+ }
17
+
18
+ interface DateRangePickerProps {
19
+ value: DateRangeValue | null
20
+ onChange: (value: DateRangeValue | null) => void
21
+ placeholder?: string
22
+ numberOfMonths?: number
23
+ align?: ComponentProps<typeof PopoverContent>['align']
24
+ triggerVariant?: ComponentProps<typeof Button>['variant']
25
+ triggerClassName?: string
26
+ formatLabel?: (value: DateRangeValue) => string
27
+ }
28
+
29
+ const defaultFormatLabel = (value: DateRangeValue) =>
30
+ `${format(parseISO(value.from), 'd MMM')} – ${format(parseISO(value.to), 'd MMM')}`
31
+
32
+ export function DateRangePicker({
33
+ value,
34
+ onChange,
35
+ placeholder = 'Plage de dates',
36
+ numberOfMonths = 2,
37
+ align = 'end',
38
+ triggerVariant = 'outline',
39
+ triggerClassName,
40
+ formatLabel = defaultFormatLabel,
41
+ }: DateRangePickerProps) {
42
+ const [open, setOpen] = useState(false)
43
+ const [draft, setDraft] = useState<DateRange | undefined>()
44
+ const hasValue = value !== null
45
+
46
+ const handleOpenChange = (next: boolean) => {
47
+ if (next) {
48
+ setDraft(value ? { from: parseISO(value.from), to: parseISO(value.to) } : undefined)
49
+ }
50
+ setOpen(next)
51
+ }
52
+
53
+ const applyDraft = () => {
54
+ if (!(draft?.from && draft.to)) return
55
+ onChange({
56
+ from: toISODate(draft.from),
57
+ to: toISODate(draft.to),
58
+ })
59
+ setOpen(false)
60
+ }
61
+
62
+ const reset = () => {
63
+ setDraft(undefined)
64
+ onChange(null)
65
+ setOpen(false)
66
+ }
67
+
68
+ return (
69
+ <Popover onOpenChange={handleOpenChange} open={open}>
70
+ <div className="group relative inline-flex shrink-0">
71
+ <PopoverTrigger asChild>
72
+ <Button
73
+ className={cn('cursor-pointer', triggerClassName)}
74
+ size="sm"
75
+ variant={hasValue ? 'secondary' : triggerVariant}
76
+ >
77
+ <CalendarRange
78
+ className={cn('transition-opacity', hasValue && 'group-hover:opacity-0')}
79
+ />
80
+ {hasValue ? formatLabel(value) : placeholder}
81
+ </Button>
82
+ </PopoverTrigger>
83
+
84
+ {hasValue && (
85
+ <button
86
+ aria-label="Clear range"
87
+ 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"
88
+ onClick={(event) => {
89
+ event.stopPropagation()
90
+ reset()
91
+ }}
92
+ type="button"
93
+ >
94
+ <X className="size-4" />
95
+ </button>
96
+ )}
97
+ </div>
98
+ <PopoverContent align={align} className="w-auto p-0">
99
+ <Calendar
100
+ autoFocus
101
+ mode="range"
102
+ numberOfMonths={numberOfMonths}
103
+ onSelect={setDraft}
104
+ selected={draft}
105
+ />
106
+ <div className="flex items-center justify-between gap-2 border-t p-2">
107
+ <Button
108
+ className="cursor-pointer"
109
+ disabled={!(hasValue || draft?.from)}
110
+ onClick={reset}
111
+ size="sm"
112
+ variant="ghost"
113
+ >
114
+ Réinitialiser
115
+ </Button>
116
+ <div className="flex gap-2">
117
+ <Button
118
+ className="cursor-pointer"
119
+ onClick={() => setOpen(false)}
120
+ size="sm"
121
+ variant="ghost"
122
+ >
123
+ Annuler
124
+ </Button>
125
+ <Button
126
+ className="cursor-pointer"
127
+ disabled={!(draft?.from && draft.to)}
128
+ onClick={applyDraft}
129
+ size="sm"
130
+ >
131
+ Appliquer
132
+ </Button>
133
+ </div>
134
+ </div>
135
+ </PopoverContent>
136
+ </Popover>
137
+ )
138
+ }
@@ -0,0 +1,246 @@
1
+ 'use client'
2
+
3
+ import { CheckIcon, ChevronRightIcon } from 'lucide-react'
4
+ import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui'
5
+ import type * as React from 'react'
6
+ import { cn } from '~/lib/utils'
7
+
8
+ function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
9
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
10
+ }
11
+
12
+ function DropdownMenuPortal({
13
+ ...props
14
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
15
+ return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
16
+ }
17
+
18
+ function DropdownMenuTrigger({
19
+ ...props
20
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
21
+ return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
22
+ }
23
+
24
+ function DropdownMenuContent({
25
+ className,
26
+ align = 'start',
27
+ sideOffset = 4,
28
+ ...props
29
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
30
+ return (
31
+ <DropdownMenuPrimitive.Portal>
32
+ <DropdownMenuPrimitive.Content
33
+ align={align}
34
+ className={cn(
35
+ 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 relative z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-48 origin-(--radix-dropdown-menu-content-transform-origin) animate-none! overflow-y-auto overflow-x-hidden rounded-3xl bg-popover/70 p-1.5 text-popover-foreground shadow-lg ring-1 ring-foreground/5 duration-100 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 data-closed:animate-out data-open:animate-in data-[state=closed]:overflow-hidden **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[variant=destructive]:**:text-accent-foreground! **:data-[variant=destructive]:text-accent-foreground! **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[variant=destructive]:focus:bg-foreground/10! dark:ring-foreground/10',
36
+ className,
37
+ )}
38
+ data-slot="dropdown-menu-content"
39
+ sideOffset={sideOffset}
40
+ {...props}
41
+ />
42
+ </DropdownMenuPrimitive.Portal>
43
+ )
44
+ }
45
+
46
+ function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
47
+ return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
48
+ }
49
+
50
+ function DropdownMenuItem({
51
+ className,
52
+ inset,
53
+ variant = 'default',
54
+ ...props
55
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
56
+ inset?: boolean
57
+ variant?: 'default' | 'destructive'
58
+ }) {
59
+ return (
60
+ <DropdownMenuPrimitive.Item
61
+ className={cn(
62
+ "group/dropdown-menu-item relative flex cursor-default select-none items-center gap-2.5 rounded-2xl px-3 py-2 font-medium text-sm outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-9.5 data-[variant=destructive]:text-destructive data-disabled:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 data-[variant=destructive]:*:[svg]:text-destructive",
63
+ className,
64
+ )}
65
+ data-inset={inset}
66
+ data-slot="dropdown-menu-item"
67
+ data-variant={variant}
68
+ {...props}
69
+ />
70
+ )
71
+ }
72
+
73
+ function DropdownMenuCheckboxItem({
74
+ className,
75
+ children,
76
+ checked,
77
+ inset,
78
+ ...props
79
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
80
+ inset?: boolean
81
+ }) {
82
+ return (
83
+ <DropdownMenuPrimitive.CheckboxItem
84
+ checked={checked}
85
+ className={cn(
86
+ "relative flex cursor-default select-none items-center gap-2.5 rounded-2xl py-2 pr-8 pl-3 font-medium text-sm outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-9.5 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
87
+ className,
88
+ )}
89
+ data-inset={inset}
90
+ data-slot="dropdown-menu-checkbox-item"
91
+ {...props}
92
+ >
93
+ <span
94
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
95
+ data-slot="dropdown-menu-checkbox-item-indicator"
96
+ >
97
+ <DropdownMenuPrimitive.ItemIndicator>
98
+ <CheckIcon />
99
+ </DropdownMenuPrimitive.ItemIndicator>
100
+ </span>
101
+ {children}
102
+ </DropdownMenuPrimitive.CheckboxItem>
103
+ )
104
+ }
105
+
106
+ function DropdownMenuRadioGroup({
107
+ ...props
108
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
109
+ return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />
110
+ }
111
+
112
+ function DropdownMenuRadioItem({
113
+ className,
114
+ children,
115
+ inset,
116
+ ...props
117
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
118
+ inset?: boolean
119
+ }) {
120
+ return (
121
+ <DropdownMenuPrimitive.RadioItem
122
+ className={cn(
123
+ "relative flex cursor-default select-none items-center gap-2.5 rounded-2xl py-2 pr-8 pl-3 font-medium text-sm outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-9.5 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
124
+ className,
125
+ )}
126
+ data-inset={inset}
127
+ data-slot="dropdown-menu-radio-item"
128
+ {...props}
129
+ >
130
+ <span
131
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
132
+ data-slot="dropdown-menu-radio-item-indicator"
133
+ >
134
+ <DropdownMenuPrimitive.ItemIndicator>
135
+ <CheckIcon />
136
+ </DropdownMenuPrimitive.ItemIndicator>
137
+ </span>
138
+ {children}
139
+ </DropdownMenuPrimitive.RadioItem>
140
+ )
141
+ }
142
+
143
+ function DropdownMenuLabel({
144
+ className,
145
+ inset,
146
+ ...props
147
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
148
+ inset?: boolean
149
+ }) {
150
+ return (
151
+ <DropdownMenuPrimitive.Label
152
+ className={cn('px-3 py-2.5 text-muted-foreground text-xs data-inset:pl-9.5', className)}
153
+ data-inset={inset}
154
+ data-slot="dropdown-menu-label"
155
+ {...props}
156
+ />
157
+ )
158
+ }
159
+
160
+ function DropdownMenuSeparator({
161
+ className,
162
+ ...props
163
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
164
+ return (
165
+ <DropdownMenuPrimitive.Separator
166
+ className={cn('-mx-1.5 my-1.5 h-px bg-border/50', className)}
167
+ data-slot="dropdown-menu-separator"
168
+ {...props}
169
+ />
170
+ )
171
+ }
172
+
173
+ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
174
+ return (
175
+ <span
176
+ className={cn(
177
+ 'ml-auto text-muted-foreground text-xs tracking-widest group-focus/dropdown-menu-item:text-accent-foreground',
178
+ className,
179
+ )}
180
+ data-slot="dropdown-menu-shortcut"
181
+ {...props}
182
+ />
183
+ )
184
+ }
185
+
186
+ function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
187
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
188
+ }
189
+
190
+ function DropdownMenuSubTrigger({
191
+ className,
192
+ inset,
193
+ children,
194
+ ...props
195
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
196
+ inset?: boolean
197
+ }) {
198
+ return (
199
+ <DropdownMenuPrimitive.SubTrigger
200
+ className={cn(
201
+ "flex cursor-default select-none items-center gap-2 rounded-2xl px-3 py-2 font-medium text-sm outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-inset:pl-9.5 data-open:text-accent-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
202
+ className,
203
+ )}
204
+ data-inset={inset}
205
+ data-slot="dropdown-menu-sub-trigger"
206
+ {...props}
207
+ >
208
+ {children}
209
+ <ChevronRightIcon className="ml-auto" />
210
+ </DropdownMenuPrimitive.SubTrigger>
211
+ )
212
+ }
213
+
214
+ function DropdownMenuSubContent({
215
+ className,
216
+ ...props
217
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
218
+ return (
219
+ <DropdownMenuPrimitive.SubContent
220
+ className={cn(
221
+ 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 relative z-50 min-w-36 origin-(--radix-dropdown-menu-content-transform-origin) animate-none! overflow-hidden rounded-3xl bg-popover/70 p-1.5 text-popover-foreground shadow-lg ring-1 ring-foreground/5 duration-100 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 data-closed:animate-out data-open:animate-in **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[variant=destructive]:**:text-accent-foreground! **:data-[variant=destructive]:text-accent-foreground! **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[variant=destructive]:focus:bg-foreground/10! dark:ring-foreground/10',
222
+ className,
223
+ )}
224
+ data-slot="dropdown-menu-sub-content"
225
+ {...props}
226
+ />
227
+ )
228
+ }
229
+
230
+ export {
231
+ DropdownMenu,
232
+ DropdownMenuCheckboxItem,
233
+ DropdownMenuContent,
234
+ DropdownMenuGroup,
235
+ DropdownMenuItem,
236
+ DropdownMenuLabel,
237
+ DropdownMenuPortal,
238
+ DropdownMenuRadioGroup,
239
+ DropdownMenuRadioItem,
240
+ DropdownMenuSeparator,
241
+ DropdownMenuShortcut,
242
+ DropdownMenuSub,
243
+ DropdownMenuSubContent,
244
+ DropdownMenuSubTrigger,
245
+ DropdownMenuTrigger,
246
+ }
@@ -0,0 +1,149 @@
1
+ 'use client'
2
+
3
+ import type { Label as LabelPrimitive } from 'radix-ui'
4
+ import { Slot } from 'radix-ui'
5
+ import { type ComponentProps, createContext, useContext, useId } from 'react'
6
+ import {
7
+ Controller,
8
+ type ControllerProps,
9
+ type FieldPath,
10
+ type FieldValues,
11
+ FormProvider,
12
+ useFormContext,
13
+ useFormState,
14
+ } from 'react-hook-form'
15
+ import { Label } from '~/components/ui/label'
16
+ import { cn } from '~/lib/utils'
17
+
18
+ const Form = FormProvider
19
+
20
+ type FormFieldContextValue<
21
+ TFieldValues extends FieldValues = FieldValues,
22
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
23
+ > = {
24
+ name: TName
25
+ }
26
+
27
+ const FormFieldContext = createContext<FormFieldContextValue>({} as FormFieldContextValue)
28
+
29
+ const FormField = <
30
+ TFieldValues extends FieldValues = FieldValues,
31
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
32
+ >({
33
+ ...props
34
+ }: ControllerProps<TFieldValues, TName>) => {
35
+ return (
36
+ <FormFieldContext.Provider value={{ name: props.name }}>
37
+ <Controller {...props} />
38
+ </FormFieldContext.Provider>
39
+ )
40
+ }
41
+
42
+ const useFormField = () => {
43
+ const fieldContext = useContext(FormFieldContext)
44
+ const itemContext = useContext(FormItemContext)
45
+ const { getFieldState } = useFormContext()
46
+ const formState = useFormState({ name: fieldContext.name })
47
+ const fieldState = getFieldState(fieldContext.name, formState)
48
+
49
+ if (!fieldContext) {
50
+ throw new Error('useFormField should be used within <FormField>')
51
+ }
52
+
53
+ const { id } = itemContext
54
+
55
+ return {
56
+ id,
57
+ name: fieldContext.name,
58
+ formItemId: `${id}-form-item`,
59
+ formDescriptionId: `${id}-form-item-description`,
60
+ formMessageId: `${id}-form-item-message`,
61
+ ...fieldState,
62
+ }
63
+ }
64
+
65
+ type FormItemContextValue = {
66
+ id: string
67
+ }
68
+
69
+ const FormItemContext = createContext<FormItemContextValue>({} as FormItemContextValue)
70
+
71
+ function FormItem({ className, ...props }: ComponentProps<'div'>) {
72
+ const id = useId()
73
+
74
+ return (
75
+ <FormItemContext.Provider value={{ id }}>
76
+ <div className={cn('grid gap-2', className)} data-slot="form-item" {...props} />
77
+ </FormItemContext.Provider>
78
+ )
79
+ }
80
+
81
+ function FormLabel({ className, ...props }: ComponentProps<typeof LabelPrimitive.Root>) {
82
+ const { error, formItemId } = useFormField()
83
+
84
+ return (
85
+ <Label
86
+ className={cn('data-[error=true]:text-destructive', className)}
87
+ data-error={!!error}
88
+ data-slot="form-label"
89
+ htmlFor={formItemId}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ function FormControl({ ...props }: ComponentProps<typeof Slot.Root>) {
96
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
97
+
98
+ return (
99
+ <Slot.Root
100
+ aria-describedby={error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}`}
101
+ aria-invalid={!!error}
102
+ data-slot="form-control"
103
+ id={formItemId}
104
+ {...props}
105
+ />
106
+ )
107
+ }
108
+
109
+ function FormDescription({ className, ...props }: ComponentProps<'p'>) {
110
+ const { formDescriptionId } = useFormField()
111
+
112
+ return (
113
+ <p
114
+ className={cn('text-muted-foreground text-sm', className)}
115
+ data-slot="form-description"
116
+ id={formDescriptionId}
117
+ {...props}
118
+ />
119
+ )
120
+ }
121
+
122
+ function FormMessage({ className, ...props }: ComponentProps<'p'>) {
123
+ const { error, formMessageId } = useFormField()
124
+ const body = error ? String(error?.message ?? '') : props.children
125
+
126
+ if (!body) return null
127
+
128
+ return (
129
+ <p
130
+ className={cn('text-destructive text-xs', className)}
131
+ data-slot="form-message"
132
+ id={formMessageId}
133
+ {...props}
134
+ >
135
+ {body}
136
+ </p>
137
+ )
138
+ }
139
+
140
+ export {
141
+ Form,
142
+ FormControl,
143
+ FormDescription,
144
+ FormField,
145
+ FormItem,
146
+ FormLabel,
147
+ FormMessage,
148
+ useFormField,
149
+ }
@@ -0,0 +1,97 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority'
2
+ import type { ComponentProps } from 'react'
3
+ import { Button } from '~/components/ui/button'
4
+ import { Input } from '~/components/ui/input'
5
+ import { cn } from '~/lib/utils'
6
+
7
+ function InputGroup({ className, ...props }: ComponentProps<'div'>) {
8
+ return (
9
+ <div
10
+ className={cn(
11
+ 'group/input-group relative flex h-9 w-full min-w-0 items-center rounded-4xl border border-transparent bg-input/50 outline-none transition-[color,box-shadow,background-color] has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
12
+ className,
13
+ )}
14
+ data-slot="input-group"
15
+ role="group"
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ const inputGroupAddonVariants = cva(
22
+ "flex h-auto cursor-text select-none items-center justify-center gap-2 py-2 font-medium text-muted-foreground text-sm group-data-[disabled=true]/input-group:opacity-50 [&>svg:not([class*='size-'])]:size-4",
23
+ {
24
+ variants: {
25
+ align: {
26
+ 'inline-start': 'order-first pl-3 has-[>button]:-ml-1',
27
+ 'inline-end': 'order-last pr-3 has-[>button]:-mr-1',
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ align: 'inline-start',
32
+ },
33
+ },
34
+ )
35
+
36
+ function InputGroupAddon({
37
+ className,
38
+ align = 'inline-start',
39
+ ...props
40
+ }: ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
41
+ return (
42
+ <div
43
+ className={cn(inputGroupAddonVariants({ align }), className)}
44
+ data-align={align}
45
+ data-slot="input-group-addon"
46
+ onClick={(e) => {
47
+ if ((e.target as HTMLElement).closest('button')) return
48
+ e.currentTarget.parentElement?.querySelector('input')?.focus()
49
+ }}
50
+ role="group"
51
+ {...props}
52
+ />
53
+ )
54
+ }
55
+
56
+ function InputGroupText({ className, ...props }: ComponentProps<'span'>) {
57
+ return (
58
+ <span
59
+ className={cn(
60
+ "flex items-center gap-2 text-muted-foreground text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
61
+ className,
62
+ )}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ function InputGroupInput({ className, ...props }: ComponentProps<'input'>) {
69
+ return (
70
+ <Input
71
+ className={cn(
72
+ 'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent',
73
+ className,
74
+ )}
75
+ data-slot="input-group-control"
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function InputGroupButton({
82
+ className,
83
+ type = 'button',
84
+ variant = 'ghost',
85
+ ...props
86
+ }: ComponentProps<typeof Button>) {
87
+ return (
88
+ <Button
89
+ className={cn('flex items-center gap-2 rounded-4xl text-sm shadow-none', className)}
90
+ type={type}
91
+ variant={variant}
92
+ {...props}
93
+ />
94
+ )
95
+ }
96
+
97
+ export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText }
@@ -0,0 +1,18 @@
1
+ import type { ComponentProps } from 'react'
2
+ import { cn } from '~/lib/utils'
3
+
4
+ function Input({ className, type, ...props }: ComponentProps<'input'>) {
5
+ return (
6
+ <input
7
+ className={cn(
8
+ 'h-9 w-full min-w-0 rounded-3xl border border-transparent bg-input/50 px-3 py-1 text-base outline-none transition-[color,box-shadow,background-color] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
9
+ className,
10
+ )}
11
+ data-slot="input"
12
+ type={type}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Input }
@@ -0,0 +1,18 @@
1
+ import { Label as LabelPrimitive } from 'radix-ui'
2
+ import type { ComponentProps } from 'react'
3
+ import { cn } from '~/lib/utils'
4
+
5
+ function Label({ className, ...props }: ComponentProps<typeof LabelPrimitive.Root>) {
6
+ return (
7
+ <LabelPrimitive.Root
8
+ className={cn(
9
+ 'flex select-none items-center gap-2 font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50',
10
+ className,
11
+ )}
12
+ data-slot="label"
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Label }