create-top-secret-starter 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/index.js +117 -0
  2. package/package.json +33 -0
  3. package/templates/template-router/.env.example +1 -0
  4. package/templates/template-router/.oxfmtrc.json +13 -0
  5. package/templates/template-router/.oxlintrc.json +8 -0
  6. package/templates/template-router/_gitignore +39 -0
  7. package/templates/template-router/components.json +25 -0
  8. package/templates/template-router/index.html +24 -0
  9. package/templates/template-router/package.json +58 -0
  10. package/templates/template-router/public/favicon.svg +1 -0
  11. package/templates/template-router/src/api/auth/auth.test.ts +46 -0
  12. package/templates/template-router/src/api/auth/guards.ts +18 -0
  13. package/templates/template-router/src/api/auth/index.ts +46 -0
  14. package/templates/template-router/src/api/auth/refresh.test.ts +89 -0
  15. package/templates/template-router/src/api/auth/router-bridge.test.ts +74 -0
  16. package/templates/template-router/src/api/auth/router-bridge.ts +26 -0
  17. package/templates/template-router/src/api/auth/schema.ts +20 -0
  18. package/templates/template-router/src/api/auth/session-store.test.ts +101 -0
  19. package/templates/template-router/src/api/auth/session-store.ts +56 -0
  20. package/templates/template-router/src/api/index.ts +51 -0
  21. package/templates/template-router/src/components/status-page.tsx +68 -0
  22. package/templates/template-router/src/components/ui/button.tsx +58 -0
  23. package/templates/template-router/src/components/ui/dialog.tsx +136 -0
  24. package/templates/template-router/src/components/ui/empty.tsx +94 -0
  25. package/templates/template-router/src/components/ui/field.tsx +222 -0
  26. package/templates/template-router/src/components/ui/input.tsx +20 -0
  27. package/templates/template-router/src/components/ui/label.tsx +18 -0
  28. package/templates/template-router/src/components/ui/select.tsx +188 -0
  29. package/templates/template-router/src/components/ui/separator.tsx +21 -0
  30. package/templates/template-router/src/components/ui/skeleton.tsx +13 -0
  31. package/templates/template-router/src/components/ui/sonner.tsx +43 -0
  32. package/templates/template-router/src/components/ui/tooltip.tsx +52 -0
  33. package/templates/template-router/src/env.ts +20 -0
  34. package/templates/template-router/src/index.css +134 -0
  35. package/templates/template-router/src/lib/query-client.test.ts +82 -0
  36. package/templates/template-router/src/lib/query-client.ts +30 -0
  37. package/templates/template-router/src/lib/single-flight.test.ts +48 -0
  38. package/templates/template-router/src/lib/single-flight.ts +9 -0
  39. package/templates/template-router/src/lib/utils.ts +1 -0
  40. package/templates/template-router/src/main.tsx +26 -0
  41. package/templates/template-router/src/mocks/mock-server.ts +149 -0
  42. package/templates/template-router/src/providers/index.tsx +27 -0
  43. package/templates/template-router/src/providers/theme.test.tsx +68 -0
  44. package/templates/template-router/src/providers/theme.tsx +76 -0
  45. package/templates/template-router/src/routeTree.gen.ts +102 -0
  46. package/templates/template-router/src/router.tsx +29 -0
  47. package/templates/template-router/src/routes/-error.tsx +34 -0
  48. package/templates/template-router/src/routes/-not-found.tsx +27 -0
  49. package/templates/template-router/src/routes/__root.tsx +15 -0
  50. package/templates/template-router/src/routes/_authenticated/index.tsx +9 -0
  51. package/templates/template-router/src/routes/_authenticated.tsx +38 -0
  52. package/templates/template-router/src/routes/sign-in.tsx +100 -0
  53. package/templates/template-router/src/test/setup.ts +5 -0
  54. package/templates/template-router/tsconfig.app.json +29 -0
  55. package/templates/template-router/tsconfig.json +7 -0
  56. package/templates/template-router/tsconfig.node.json +23 -0
  57. package/templates/template-router/vite.config.ts +33 -0
@@ -0,0 +1,222 @@
1
+ import { useMemo } from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+
4
+ import { cn } from '@/lib/utils'
5
+ import { Label } from '@/components/ui/label'
6
+ import { Separator } from '@/components/ui/separator'
7
+
8
+ function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
9
+ return (
10
+ <fieldset
11
+ data-slot='field-set'
12
+ className={cn(
13
+ 'flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ function FieldLegend({
22
+ className,
23
+ variant = 'legend',
24
+ ...props
25
+ }: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
26
+ return (
27
+ <legend
28
+ data-slot='field-legend'
29
+ data-variant={variant}
30
+ className={cn(
31
+ 'mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base',
32
+ className
33
+ )}
34
+ {...props}
35
+ />
36
+ )
37
+ }
38
+
39
+ function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
40
+ return (
41
+ <div
42
+ data-slot='field-group'
43
+ className={cn(
44
+ 'group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
45
+ className
46
+ )}
47
+ {...props}
48
+ />
49
+ )
50
+ }
51
+
52
+ const fieldVariants = cva('group/field data-[invalid=true]:text-destructive flex w-full gap-2', {
53
+ variants: {
54
+ orientation: {
55
+ vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
56
+ horizontal:
57
+ 'flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
58
+ responsive:
59
+ 'flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
60
+ }
61
+ },
62
+ defaultVariants: {
63
+ orientation: 'vertical'
64
+ }
65
+ })
66
+
67
+ function Field({
68
+ className,
69
+ orientation = 'vertical',
70
+ ...props
71
+ }: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
72
+ return (
73
+ <div
74
+ role='group'
75
+ data-slot='field'
76
+ data-orientation={orientation}
77
+ className={cn(fieldVariants({ orientation }), className)}
78
+ {...props}
79
+ />
80
+ )
81
+ }
82
+
83
+ function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
84
+ return (
85
+ <div
86
+ data-slot='field-content'
87
+ className={cn('group/field-content flex flex-1 flex-col gap-0.5 leading-snug', className)}
88
+ {...props}
89
+ />
90
+ )
91
+ }
92
+
93
+ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
94
+ return (
95
+ <Label
96
+ data-slot='field-label'
97
+ className={cn(
98
+ 'group/field-label peer/field-label has-data-checked:border-primary/30 has-data-checked:bg-primary/5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5',
99
+ 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
100
+ className
101
+ )}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
108
+ return (
109
+ <div
110
+ data-slot='field-label'
111
+ className={cn(
112
+ 'flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50',
113
+ className
114
+ )}
115
+ {...props}
116
+ />
117
+ )
118
+ }
119
+
120
+ function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
121
+ return (
122
+ <p
123
+ data-slot='field-description'
124
+ className={cn(
125
+ 'text-muted-foreground text-left text-sm leading-normal font-normal group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5',
126
+ 'last:mt-0 nth-last-2:-mt-1',
127
+ '[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
128
+ className
129
+ )}
130
+ {...props}
131
+ />
132
+ )
133
+ }
134
+
135
+ function FieldSeparator({
136
+ children,
137
+ className,
138
+ ...props
139
+ }: React.ComponentProps<'div'> & {
140
+ children?: React.ReactNode
141
+ }) {
142
+ return (
143
+ <div
144
+ data-slot='field-separator'
145
+ data-content={!!children}
146
+ className={cn(
147
+ 'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
148
+ className
149
+ )}
150
+ {...props}
151
+ >
152
+ <Separator className='absolute inset-0 top-1/2' />
153
+ {children && (
154
+ <span
155
+ className='bg-background text-muted-foreground relative mx-auto block w-fit px-2'
156
+ data-slot='field-separator-content'
157
+ >
158
+ {children}
159
+ </span>
160
+ )}
161
+ </div>
162
+ )
163
+ }
164
+
165
+ function FieldError({
166
+ className,
167
+ children,
168
+ errors,
169
+ ...props
170
+ }: React.ComponentProps<'div'> & {
171
+ errors?: Array<{ message?: string } | undefined>
172
+ }) {
173
+ const content = useMemo(() => {
174
+ if (children) {
175
+ return children
176
+ }
177
+
178
+ if (!errors?.length) {
179
+ return null
180
+ }
181
+
182
+ const uniqueErrors = [...new Map(errors.map(error => [error?.message, error])).values()]
183
+
184
+ if (uniqueErrors?.length == 1) {
185
+ return uniqueErrors[0]?.message
186
+ }
187
+
188
+ return (
189
+ <ul className='ml-4 flex list-disc flex-col gap-1'>
190
+ {uniqueErrors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}
191
+ </ul>
192
+ )
193
+ }, [children, errors])
194
+
195
+ if (!content) {
196
+ return null
197
+ }
198
+
199
+ return (
200
+ <div
201
+ role='alert'
202
+ data-slot='field-error'
203
+ className={cn('text-destructive text-sm font-normal', className)}
204
+ {...props}
205
+ >
206
+ {content}
207
+ </div>
208
+ )
209
+ }
210
+
211
+ export {
212
+ Field,
213
+ FieldLabel,
214
+ FieldDescription,
215
+ FieldError,
216
+ FieldGroup,
217
+ FieldLegend,
218
+ FieldSeparator,
219
+ FieldSet,
220
+ FieldContent,
221
+ FieldTitle
222
+ }
@@ -0,0 +1,20 @@
1
+ import * as React from 'react'
2
+ import { Input as InputPrimitive } from '@base-ui/react/input'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ const Input = ({ className, type, ...props }: React.ComponentProps<'input'>) => {
7
+ return (
8
+ <InputPrimitive
9
+ type={type}
10
+ data-slot='input'
11
+ className={cn(
12
+ 'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ export { Input }
@@ -0,0 +1,18 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ function Label({ className, ...props }: React.ComponentProps<'label'>) {
6
+ return (
7
+ <label
8
+ data-slot='label'
9
+ className={cn(
10
+ 'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Label }
@@ -0,0 +1,188 @@
1
+ import * as React from 'react'
2
+ import { Select as SelectPrimitive } from '@base-ui/react/select'
3
+
4
+ import { cn } from '@/lib/utils'
5
+ import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from 'lucide-react'
6
+
7
+ const Select = SelectPrimitive.Root
8
+
9
+ const SelectGroup = ({ className, ...props }: SelectPrimitive.Group.Props) => {
10
+ return (
11
+ <SelectPrimitive.Group
12
+ data-slot='select-group'
13
+ className={cn('scroll-my-1 p-1', className)}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ const SelectValue = ({ className, ...props }: SelectPrimitive.Value.Props) => {
20
+ return (
21
+ <SelectPrimitive.Value
22
+ data-slot='select-value'
23
+ className={cn('flex flex-1 text-left', className)}
24
+ {...props}
25
+ />
26
+ )
27
+ }
28
+
29
+ const SelectTrigger = ({
30
+ className,
31
+ size = 'default',
32
+ children,
33
+ ...props
34
+ }: SelectPrimitive.Trigger.Props & {
35
+ size?: 'sm' | 'default'
36
+ }) => {
37
+ return (
38
+ <SelectPrimitive.Trigger
39
+ data-slot='select-trigger'
40
+ data-size={size}
41
+ className={cn(
42
+ "border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 flex w-fit items-center justify-between gap-1.5 rounded-lg border bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ {children}
48
+ <SelectPrimitive.Icon
49
+ render={<ChevronDownIcon className='text-muted-foreground pointer-events-none size-4' />}
50
+ />
51
+ </SelectPrimitive.Trigger>
52
+ )
53
+ }
54
+
55
+ const SelectContent = ({
56
+ className,
57
+ children,
58
+ side = 'bottom',
59
+ sideOffset = 4,
60
+ align = 'center',
61
+ alignOffset = 0,
62
+ alignItemWithTrigger = true,
63
+ ...props
64
+ }: SelectPrimitive.Popup.Props &
65
+ Pick<
66
+ SelectPrimitive.Positioner.Props,
67
+ 'align' | 'alignOffset' | 'side' | 'sideOffset' | 'alignItemWithTrigger'
68
+ >) => {
69
+ return (
70
+ <SelectPrimitive.Portal>
71
+ <SelectPrimitive.Positioner
72
+ side={side}
73
+ sideOffset={sideOffset}
74
+ align={align}
75
+ alignOffset={alignOffset}
76
+ alignItemWithTrigger={alignItemWithTrigger}
77
+ className='isolate z-50'
78
+ >
79
+ <SelectPrimitive.Popup
80
+ data-slot='select-content'
81
+ data-align-trigger={alignItemWithTrigger}
82
+ className={cn(
83
+ 'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100 data-[align-trigger=true]:animate-none',
84
+ className
85
+ )}
86
+ {...props}
87
+ >
88
+ <SelectScrollUpButton />
89
+ <SelectPrimitive.List>{children}</SelectPrimitive.List>
90
+ <SelectScrollDownButton />
91
+ </SelectPrimitive.Popup>
92
+ </SelectPrimitive.Positioner>
93
+ </SelectPrimitive.Portal>
94
+ )
95
+ }
96
+
97
+ const SelectLabel = ({ className, ...props }: SelectPrimitive.GroupLabel.Props) => {
98
+ return (
99
+ <SelectPrimitive.GroupLabel
100
+ data-slot='select-label'
101
+ className={cn('text-muted-foreground px-1.5 py-1 text-xs', className)}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ const SelectItem = ({ className, children, ...props }: SelectPrimitive.Item.Props) => {
108
+ return (
109
+ <SelectPrimitive.Item
110
+ data-slot='select-item'
111
+ className={cn(
112
+ "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
113
+ className
114
+ )}
115
+ {...props}
116
+ >
117
+ <SelectPrimitive.ItemText className='flex flex-1 shrink-0 gap-2 whitespace-nowrap'>
118
+ {children}
119
+ </SelectPrimitive.ItemText>
120
+ <SelectPrimitive.ItemIndicator
121
+ render={
122
+ <span className='pointer-events-none absolute right-2 flex size-4 items-center justify-center' />
123
+ }
124
+ >
125
+ <CheckIcon className='pointer-events-none' />
126
+ </SelectPrimitive.ItemIndicator>
127
+ </SelectPrimitive.Item>
128
+ )
129
+ }
130
+
131
+ const SelectSeparator = ({ className, ...props }: SelectPrimitive.Separator.Props) => {
132
+ return (
133
+ <SelectPrimitive.Separator
134
+ data-slot='select-separator'
135
+ className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
136
+ {...props}
137
+ />
138
+ )
139
+ }
140
+
141
+ const SelectScrollUpButton = ({
142
+ className,
143
+ ...props
144
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) => {
145
+ return (
146
+ <SelectPrimitive.ScrollUpArrow
147
+ data-slot='select-scroll-up-button'
148
+ className={cn(
149
+ "bg-popover top-0 z-10 flex w-full cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
150
+ className
151
+ )}
152
+ {...props}
153
+ >
154
+ <ChevronUpIcon />
155
+ </SelectPrimitive.ScrollUpArrow>
156
+ )
157
+ }
158
+
159
+ const SelectScrollDownButton = ({
160
+ className,
161
+ ...props
162
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) => {
163
+ return (
164
+ <SelectPrimitive.ScrollDownArrow
165
+ data-slot='select-scroll-down-button'
166
+ className={cn(
167
+ "bg-popover bottom-0 z-10 flex w-full cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
168
+ className
169
+ )}
170
+ {...props}
171
+ >
172
+ <ChevronDownIcon />
173
+ </SelectPrimitive.ScrollDownArrow>
174
+ )
175
+ }
176
+
177
+ export {
178
+ Select,
179
+ SelectContent,
180
+ SelectGroup,
181
+ SelectItem,
182
+ SelectLabel,
183
+ SelectScrollDownButton,
184
+ SelectScrollUpButton,
185
+ SelectSeparator,
186
+ SelectTrigger,
187
+ SelectValue
188
+ }
@@ -0,0 +1,21 @@
1
+ 'use client'
2
+
3
+ import { Separator as SeparatorPrimitive } from '@base-ui/react/separator'
4
+
5
+ import { cn } from '@/lib/utils'
6
+
7
+ function Separator({ className, orientation = 'horizontal', ...props }: SeparatorPrimitive.Props) {
8
+ return (
9
+ <SeparatorPrimitive
10
+ data-slot='separator'
11
+ orientation={orientation}
12
+ className={cn(
13
+ 'bg-border shrink-0 data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch',
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ export { Separator }
@@ -0,0 +1,13 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ const Skeleton = ({ className, ...props }: React.ComponentProps<'div'>) => {
4
+ return (
5
+ <div
6
+ data-slot='skeleton'
7
+ className={cn('bg-muted animate-pulse rounded-md', className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ export { Skeleton }
@@ -0,0 +1,43 @@
1
+ import { useTheme } from '@/providers/theme'
2
+ import {
3
+ CircleCheckIcon,
4
+ InfoIcon,
5
+ Loader2Icon,
6
+ OctagonXIcon,
7
+ TriangleAlertIcon
8
+ } from 'lucide-react'
9
+ import { Toaster as Sonner, type ToasterProps } from 'sonner'
10
+
11
+ const Toaster = ({ ...props }: ToasterProps) => {
12
+ const { theme } = useTheme()
13
+
14
+ return (
15
+ <Sonner
16
+ theme={theme as ToasterProps['theme']}
17
+ className='toaster group'
18
+ icons={{
19
+ success: <CircleCheckIcon className='size-4' />,
20
+ info: <InfoIcon className='size-4' />,
21
+ warning: <TriangleAlertIcon className='size-4' />,
22
+ error: <OctagonXIcon className='size-4' />,
23
+ loading: <Loader2Icon className='size-4 animate-spin' />
24
+ }}
25
+ style={
26
+ {
27
+ '--normal-bg': 'var(--popover)',
28
+ '--normal-text': 'var(--popover-foreground)',
29
+ '--normal-border': 'var(--border)',
30
+ '--border-radius': 'var(--radius)'
31
+ } as React.CSSProperties
32
+ }
33
+ toastOptions={{
34
+ classNames: {
35
+ toast: 'cn-toast'
36
+ }
37
+ }}
38
+ {...props}
39
+ />
40
+ )
41
+ }
42
+
43
+ export { Toaster }
@@ -0,0 +1,52 @@
1
+ import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ const TooltipProvider = ({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) => {
6
+ return <TooltipPrimitive.Provider data-slot='tooltip-provider' delay={delay} {...props} />
7
+ }
8
+
9
+ const Tooltip = ({ ...props }: TooltipPrimitive.Root.Props) => {
10
+ return <TooltipPrimitive.Root data-slot='tooltip' {...props} />
11
+ }
12
+
13
+ const TooltipTrigger = ({ ...props }: TooltipPrimitive.Trigger.Props) => {
14
+ return <TooltipPrimitive.Trigger data-slot='tooltip-trigger' {...props} />
15
+ }
16
+
17
+ const TooltipContent = ({
18
+ className,
19
+ side = 'top',
20
+ sideOffset = 4,
21
+ align = 'center',
22
+ alignOffset = 0,
23
+ children,
24
+ ...props
25
+ }: TooltipPrimitive.Popup.Props &
26
+ Pick<TooltipPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'>) => {
27
+ return (
28
+ <TooltipPrimitive.Portal>
29
+ <TooltipPrimitive.Positioner
30
+ align={align}
31
+ alignOffset={alignOffset}
32
+ side={side}
33
+ sideOffset={sideOffset}
34
+ className='isolate z-50'
35
+ >
36
+ <TooltipPrimitive.Popup
37
+ data-slot='tooltip-content'
38
+ className={cn(
39
+ 'bg-foreground text-background data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm',
40
+ className
41
+ )}
42
+ {...props}
43
+ >
44
+ {children}
45
+ <TooltipPrimitive.Arrow className='bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5' />
46
+ </TooltipPrimitive.Popup>
47
+ </TooltipPrimitive.Positioner>
48
+ </TooltipPrimitive.Portal>
49
+ )
50
+ }
51
+
52
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,20 @@
1
+ import * as z from 'zod/mini'
2
+
3
+ const envSchema = z.object({
4
+ // trailing slash enforced, not documented: ky resolves endpoints via `new URL(path, baseUrl)`, which drops a base's last segment without it
5
+ VITE_API_URL: z._default(z.string().check(z.minLength(1), z.endsWith('/')), '/api/'),
6
+ VITE_ENABLE_MOCKS: z._default(z.enum(['true', 'false']), 'true')
7
+ })
8
+
9
+ const parsed = envSchema.safeParse(import.meta.env)
10
+
11
+ if (!parsed.success) {
12
+ throw new Error(
13
+ `Invalid environment variables: ${JSON.stringify(z.treeifyError(parsed.error), null, 2)}`
14
+ )
15
+ }
16
+
17
+ export const env = parsed.data
18
+
19
+ // dev-only mock backend; see src/mocks/mock-server.ts
20
+ export const mocksEnabled = import.meta.env.DEV && env.VITE_ENABLE_MOCKS === 'true'