create-stitchkit 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 (174) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -0
  4. package/dist/cli.js +230 -0
  5. package/package.json +50 -0
  6. package/template/README.md +69 -0
  7. package/template/_env +11 -0
  8. package/template/_env.example +12 -0
  9. package/template/_gitignore +10 -0
  10. package/template/biome.json +43 -0
  11. package/template/bun.lock +1372 -0
  12. package/template/e2e/starter.spec.ts +237 -0
  13. package/template/ecosystem.config.cjs +28 -0
  14. package/template/ecosystem.dev.config.cjs +29 -0
  15. package/template/package.json +55 -0
  16. package/template/packages/backend/package.json +31 -0
  17. package/template/packages/backend/src/cli.ts +12 -0
  18. package/template/packages/backend/src/domain/errors.ts +9 -0
  19. package/template/packages/backend/src/domain/repository/github-cache.test.ts +72 -0
  20. package/template/packages/backend/src/domain/repository/github-cache.ts +205 -0
  21. package/template/packages/backend/src/index.ts +56 -0
  22. package/template/packages/backend/src/lib/db.ts +7 -0
  23. package/template/packages/backend/src/surface.ts +13 -0
  24. package/template/packages/backend/src/tools.ts +21 -0
  25. package/template/packages/backend/src/transport/errors.ts +28 -0
  26. package/template/packages/backend/src/transport/repository-service.ts +18 -0
  27. package/template/packages/backend/tsconfig.json +8 -0
  28. package/template/packages/config/package.json +22 -0
  29. package/template/packages/config/src/server.ts +24 -0
  30. package/template/packages/config/tsconfig.json +5 -0
  31. package/template/packages/db/migrations/20260808000000_init/migration.sql +16 -0
  32. package/template/packages/db/migrations/20260808170000_repository_visibility/migration.sql +4 -0
  33. package/template/packages/db/migrations/migration_lock.toml +1 -0
  34. package/template/packages/db/package.json +29 -0
  35. package/template/packages/db/prisma.config.ts +8 -0
  36. package/template/packages/db/schema.prisma +31 -0
  37. package/template/packages/db/tsconfig.json +5 -0
  38. package/template/packages/frontend/messages/en.json +15 -0
  39. package/template/packages/frontend/messages/ru.json +15 -0
  40. package/template/packages/frontend/next.config.ts +18 -0
  41. package/template/packages/frontend/package.json +62 -0
  42. package/template/packages/frontend/postcss.config.mjs +1 -0
  43. package/template/packages/frontend/public/ASSETS.md +6 -0
  44. package/template/packages/frontend/public/favicon/mascot-stitch.png +0 -0
  45. package/template/packages/frontend/public/icons/biome.svg +1 -0
  46. package/template/packages/frontend/public/icons/bun.svg +19 -0
  47. package/template/packages/frontend/public/icons/next.svg +1 -0
  48. package/template/packages/frontend/public/icons/postgresql.svg +1 -0
  49. package/template/packages/frontend/public/icons/prisma.svg +6 -0
  50. package/template/packages/frontend/public/icons/react-query.svg +9 -0
  51. package/template/packages/frontend/public/icons/react.svg +1 -0
  52. package/template/packages/frontend/public/icons/tailwind.svg +12 -0
  53. package/template/packages/frontend/public/icons/tsconfig.svg +1 -0
  54. package/template/packages/frontend/public/icons/typescript.svg +7 -0
  55. package/template/packages/frontend/public/icons/zod.svg +1 -0
  56. package/template/packages/frontend/public/mascot-stitch.png +0 -0
  57. package/template/packages/frontend/public/theme-dark.svg +8 -0
  58. package/template/packages/frontend/public/theme-light.svg +8 -0
  59. package/template/packages/frontend/src/app/[locale]/error.tsx +19 -0
  60. package/template/packages/frontend/src/app/[locale]/layout.tsx +59 -0
  61. package/template/packages/frontend/src/app/[locale]/loading.tsx +14 -0
  62. package/template/packages/frontend/src/app/[locale]/page.tsx +44 -0
  63. package/template/packages/frontend/src/app/[locale]/starter-page.tsx +151 -0
  64. package/template/packages/frontend/src/app/[locale]/ui/[story]/page.tsx +40 -0
  65. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/account-showcase.tsx +112 -0
  66. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/admin-showcase.tsx +101 -0
  67. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/catalogue-navigation.tsx +56 -0
  68. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/catalogue-shell.tsx +51 -0
  69. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/code-foundation.tsx +35 -0
  70. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/components-story.tsx +54 -0
  71. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/data-story.tsx +126 -0
  72. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/image-preloader.tsx +8 -0
  73. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/index.ts +3 -0
  74. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-sections.tsx +269 -0
  75. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-showcase.tsx +159 -0
  76. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/motion-story.tsx +78 -0
  77. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/overlays-story.tsx +134 -0
  78. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/payment-showcase.tsx +82 -0
  79. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/presentation-story.tsx +68 -0
  80. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/primitives-story.tsx +267 -0
  81. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/story-page.tsx +14 -0
  82. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/theme-story-client.tsx +208 -0
  83. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/theme-story.tsx +13 -0
  84. package/template/packages/frontend/src/app/[locale]/ui/layout.tsx +6 -0
  85. package/template/packages/frontend/src/app/[locale]/ui/page.tsx +10 -0
  86. package/template/packages/frontend/src/app/api/og/[locale]/[page]/route.tsx +53 -0
  87. package/template/packages/frontend/src/app/globals.css +210 -0
  88. package/template/packages/frontend/src/app/robots.ts +9 -0
  89. package/template/packages/frontend/src/app/sitemap.ts +22 -0
  90. package/template/packages/frontend/src/components/brand-mark.tsx +22 -0
  91. package/template/packages/frontend/src/components/repository-summary.tsx +107 -0
  92. package/template/packages/frontend/src/components/system-controls.tsx +92 -0
  93. package/template/packages/frontend/src/components/ui/adaptive-modal.tsx +382 -0
  94. package/template/packages/frontend/src/components/ui/animated-counter.tsx +80 -0
  95. package/template/packages/frontend/src/components/ui/avatar.tsx +46 -0
  96. package/template/packages/frontend/src/components/ui/badge.tsx +94 -0
  97. package/template/packages/frontend/src/components/ui/button-variants.ts +24 -0
  98. package/template/packages/frontend/src/components/ui/button.tsx +81 -0
  99. package/template/packages/frontend/src/components/ui/card-deck.tsx +73 -0
  100. package/template/packages/frontend/src/components/ui/card.tsx +81 -0
  101. package/template/packages/frontend/src/components/ui/charts/area-chart.tsx +167 -0
  102. package/template/packages/frontend/src/components/ui/charts/index.ts +7 -0
  103. package/template/packages/frontend/src/components/ui/checkbox.tsx +133 -0
  104. package/template/packages/frontend/src/components/ui/confirmation-modal.tsx +129 -0
  105. package/template/packages/frontend/src/components/ui/copyable-text.tsx +85 -0
  106. package/template/packages/frontend/src/components/ui/drawer.tsx +136 -0
  107. package/template/packages/frontend/src/components/ui/dropdown-menu.tsx +185 -0
  108. package/template/packages/frontend/src/components/ui/field.tsx +28 -0
  109. package/template/packages/frontend/src/components/ui/hand-drawn-icons.tsx +41 -0
  110. package/template/packages/frontend/src/components/ui/index.ts +36 -0
  111. package/template/packages/frontend/src/components/ui/input.tsx +24 -0
  112. package/template/packages/frontend/src/components/ui/label.tsx +18 -0
  113. package/template/packages/frontend/src/components/ui/otp-input.tsx +140 -0
  114. package/template/packages/frontend/src/components/ui/select.tsx +240 -0
  115. package/template/packages/frontend/src/components/ui/sidebar.tsx +343 -0
  116. package/template/packages/frontend/src/components/ui/skeleton.tsx +8 -0
  117. package/template/packages/frontend/src/components/ui/slider.tsx +31 -0
  118. package/template/packages/frontend/src/components/ui/slot-drum.tsx +50 -0
  119. package/template/packages/frontend/src/components/ui/spinner.tsx +30 -0
  120. package/template/packages/frontend/src/components/ui/switch.tsx +31 -0
  121. package/template/packages/frontend/src/components/ui/table.tsx +106 -0
  122. package/template/packages/frontend/src/components/ui/tabs.tsx +196 -0
  123. package/template/packages/frontend/src/components/ui/textarea.tsx +21 -0
  124. package/template/packages/frontend/src/components/ui/toaster.tsx +72 -0
  125. package/template/packages/frontend/src/components/ui/tooltip.tsx +31 -0
  126. package/template/packages/frontend/src/env.ts +21 -0
  127. package/template/packages/frontend/src/features/data-table/components/column-helpers.tsx +136 -0
  128. package/template/packages/frontend/src/features/data-table/components/data-table.tsx +303 -0
  129. package/template/packages/frontend/src/features/data-table/components/inline-stat.tsx +113 -0
  130. package/template/packages/frontend/src/features/data-table/components/sortable-header.tsx +33 -0
  131. package/template/packages/frontend/src/features/data-table/components/table-error.tsx +24 -0
  132. package/template/packages/frontend/src/features/data-table/components/table-filters.tsx +82 -0
  133. package/template/packages/frontend/src/features/data-table/hooks/useTableState.ts +111 -0
  134. package/template/packages/frontend/src/features/data-table/index.ts +26 -0
  135. package/template/packages/frontend/src/features/data-table/table-features.ts +26 -0
  136. package/template/packages/frontend/src/hooks/index.ts +3 -0
  137. package/template/packages/frontend/src/hooks/useInfiniteScroll.ts +67 -0
  138. package/template/packages/frontend/src/hooks/useIsTouchDevice.ts +25 -0
  139. package/template/packages/frontend/src/hooks/useMediaQuery.ts +30 -0
  140. package/template/packages/frontend/src/i18n/locales.ts +6 -0
  141. package/template/packages/frontend/src/i18n/navigation.ts +4 -0
  142. package/template/packages/frontend/src/i18n/request.ts +9 -0
  143. package/template/packages/frontend/src/i18n/routing.ts +8 -0
  144. package/template/packages/frontend/src/lib/api/client.ts +20 -0
  145. package/template/packages/frontend/src/lib/api/queries.ts +11 -0
  146. package/template/packages/frontend/src/lib/query-client.ts +20 -0
  147. package/template/packages/frontend/src/lib/realtime/repository.ts +21 -0
  148. package/template/packages/frontend/src/lib/seo/metadata.ts +50 -0
  149. package/template/packages/frontend/src/lib/seo/pages.test.ts +25 -0
  150. package/template/packages/frontend/src/lib/seo/pages.ts +107 -0
  151. package/template/packages/frontend/src/lib/utils/cn.ts +6 -0
  152. package/template/packages/frontend/src/lib/utils/format.ts +25 -0
  153. package/template/packages/frontend/src/lib/utils/index.ts +2 -0
  154. package/template/packages/frontend/src/providers/index.tsx +18 -0
  155. package/template/packages/frontend/src/providers/react-query.tsx +9 -0
  156. package/template/packages/frontend/src/providers/realtime.tsx +16 -0
  157. package/template/packages/frontend/src/proxy.ts +6 -0
  158. package/template/packages/frontend/src/theme/config.ts +20 -0
  159. package/template/packages/frontend/src/theme/transition.ts +54 -0
  160. package/template/packages/frontend/tsconfig.json +13 -0
  161. package/template/packages/shared/package.json +23 -0
  162. package/template/packages/shared/src/contracts/repository.ts +28 -0
  163. package/template/packages/shared/src/events/repository.ts +9 -0
  164. package/template/packages/shared/src/index.ts +3 -0
  165. package/template/packages/shared/src/schemas/repository.test.ts +36 -0
  166. package/template/packages/shared/src/schemas/repository.ts +28 -0
  167. package/template/packages/shared/tsconfig.json +5 -0
  168. package/template/playwright.config.ts +15 -0
  169. package/template/scripts/check-authored.ts +81 -0
  170. package/template/scripts/dev.ts +23 -0
  171. package/template/scripts/local-env.ts +12 -0
  172. package/template/scripts/runtime-smoke.ts +67 -0
  173. package/template/scripts/tooling-env.ts +15 -0
  174. package/template/tsconfig.json +13 -0
@@ -0,0 +1,240 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Select - Dropdown компонент
5
+ *
6
+ * Составные части: Select, SelectTrigger, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator
7
+ * Варианты trigger: default, outline
8
+ * Размеры: sm, md
9
+ *
10
+ * Powered by Radix UI Select & CVA
11
+ */
12
+
13
+ import * as SelectPrimitive from '@radix-ui/react-select';
14
+ import { IconCheck, IconChevronDown, IconChevronUp } from '@tabler/icons-react';
15
+ import { cva, type VariantProps } from 'class-variance-authority';
16
+ import { type ComponentPropsWithoutRef, type ComponentRef, forwardRef } from 'react';
17
+ import { cn } from '@/lib/utils';
18
+
19
+ // ==========================================================================
20
+ // Root & Value
21
+ // ==========================================================================
22
+
23
+ const Select = SelectPrimitive.Root;
24
+ const SelectValue = SelectPrimitive.Value;
25
+
26
+ // ==========================================================================
27
+ // Group & Label
28
+ // ==========================================================================
29
+
30
+ const SelectGroup = SelectPrimitive.Group;
31
+
32
+ const SelectLabel = forwardRef<
33
+ ComponentRef<typeof SelectPrimitive.Label>,
34
+ ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
35
+ >(({ className, ...props }, ref) => (
36
+ <SelectPrimitive.Label
37
+ ref={ref}
38
+ className={cn('px-3 py-1.5 text-xs text-muted-foreground', className)}
39
+ {...props}
40
+ />
41
+ ));
42
+ SelectLabel.displayName = 'SelectLabel';
43
+
44
+ // ==========================================================================
45
+ // Separator
46
+ // ==========================================================================
47
+
48
+ const SelectSeparator = forwardRef<
49
+ ComponentRef<typeof SelectPrimitive.Separator>,
50
+ ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
51
+ >(({ className, ...props }, ref) => (
52
+ <SelectPrimitive.Separator
53
+ ref={ref}
54
+ className={cn('-mx-1 my-1 h-px bg-border', className)}
55
+ {...props}
56
+ />
57
+ ));
58
+ SelectSeparator.displayName = 'SelectSeparator';
59
+
60
+ // ==========================================================================
61
+ // Trigger Styles
62
+ // ==========================================================================
63
+
64
+ const selectTriggerVariants = cva(
65
+ 'flex items-center justify-between gap-2 text-foreground transition-colors cursor-pointer outline-hidden disabled:opacity-50 disabled:cursor-not-allowed data-[placeholder]:text-muted-foreground',
66
+ {
67
+ variants: {
68
+ variant: {
69
+ default: 'bg-muted hover:bg-muted/80',
70
+ outline: 'border border-border bg-transparent hover:bg-muted',
71
+ ghost: 'bg-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground',
72
+ },
73
+ size: {
74
+ sm: 'h-8 px-3 text-sm rounded-lg',
75
+ md: 'h-10 px-4 text-sm rounded-xl',
76
+ },
77
+ },
78
+ defaultVariants: {
79
+ variant: 'default',
80
+ size: 'md',
81
+ },
82
+ },
83
+ );
84
+
85
+ // ==========================================================================
86
+ // Trigger
87
+ // ==========================================================================
88
+
89
+ interface SelectTriggerProps
90
+ extends ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>,
91
+ VariantProps<typeof selectTriggerVariants> {}
92
+
93
+ const SelectTrigger = forwardRef<
94
+ ComponentRef<typeof SelectPrimitive.Trigger>,
95
+ SelectTriggerProps
96
+ >(({ className, variant, size, children, ...props }, ref) => (
97
+ <SelectPrimitive.Trigger
98
+ ref={ref}
99
+ className={cn(selectTriggerVariants({ variant, size, className }))}
100
+ {...props}
101
+ >
102
+ {children}
103
+ <SelectPrimitive.Icon>
104
+ <IconChevronDown className='h-3 w-3 text-muted-foreground' />
105
+ </SelectPrimitive.Icon>
106
+ </SelectPrimitive.Trigger>
107
+ ));
108
+ SelectTrigger.displayName = 'SelectTrigger';
109
+
110
+ // ==========================================================================
111
+ // Content Styles
112
+ // ==========================================================================
113
+
114
+ const selectContentVariants = cva(
115
+ [
116
+ 'relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-xl shadow-lg',
117
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
118
+ 'fade-in-0 fade-out-0 zoom-in-95 zoom-out-95',
119
+ 'duration-200',
120
+ ],
121
+ {
122
+ variants: {
123
+ variant: {
124
+ default: 'border border-border bg-background text-foreground',
125
+ glass: 'border border-border text-foreground bg-background/80 backdrop-blur-sm',
126
+ },
127
+ },
128
+ defaultVariants: {
129
+ variant: 'default',
130
+ },
131
+ },
132
+ );
133
+
134
+ // ==========================================================================
135
+ // Content
136
+ // ==========================================================================
137
+
138
+ const SelectScrollUpButton = forwardRef<
139
+ ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
140
+ ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
141
+ >(({ className, ...props }, ref) => (
142
+ <SelectPrimitive.ScrollUpButton
143
+ ref={ref}
144
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
145
+ {...props}
146
+ >
147
+ <IconChevronUp className='h-4 w-4' stroke={2} />
148
+ </SelectPrimitive.ScrollUpButton>
149
+ ));
150
+ SelectScrollUpButton.displayName = 'SelectScrollUpButton';
151
+
152
+ const SelectScrollDownButton = forwardRef<
153
+ ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
154
+ ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
155
+ >(({ className, ...props }, ref) => (
156
+ <SelectPrimitive.ScrollDownButton
157
+ ref={ref}
158
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
159
+ {...props}
160
+ >
161
+ <IconChevronDown className='h-4 w-4' stroke={2} />
162
+ </SelectPrimitive.ScrollDownButton>
163
+ ));
164
+ SelectScrollDownButton.displayName = 'SelectScrollDownButton';
165
+
166
+ interface SelectContentProps
167
+ extends ComponentPropsWithoutRef<typeof SelectPrimitive.Content>,
168
+ VariantProps<typeof selectContentVariants> {}
169
+
170
+ const SelectContent = forwardRef<
171
+ ComponentRef<typeof SelectPrimitive.Content>,
172
+ SelectContentProps
173
+ >(({ className, children, position = 'popper', variant, ...props }, ref) => (
174
+ <SelectPrimitive.Portal>
175
+ <SelectPrimitive.Content
176
+ ref={ref}
177
+ className={cn(selectContentVariants({ variant }), className)}
178
+ position={position}
179
+ {...props}
180
+ >
181
+ <SelectScrollUpButton />
182
+ <SelectPrimitive.Viewport
183
+ className={cn(
184
+ 'p-1',
185
+ position === 'popper' &&
186
+ 'h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)',
187
+ )}
188
+ >
189
+ {children}
190
+ </SelectPrimitive.Viewport>
191
+ <SelectScrollDownButton />
192
+ </SelectPrimitive.Content>
193
+ </SelectPrimitive.Portal>
194
+ ));
195
+ SelectContent.displayName = 'SelectContent';
196
+
197
+ // ==========================================================================
198
+ // Item
199
+ // ==========================================================================
200
+
201
+ const SelectItem = forwardRef<
202
+ ComponentRef<typeof SelectPrimitive.Item>,
203
+ ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
204
+ >(({ className, children, ...props }, ref) => (
205
+ <SelectPrimitive.Item
206
+ ref={ref}
207
+ className={cn(
208
+ 'relative flex w-full cursor-pointer select-none items-center rounded-lg py-2 pl-3 pr-8 text-sm outline-hidden',
209
+ 'focus:bg-muted',
210
+ 'data-disabled:pointer-events-none data-disabled:opacity-50',
211
+ className,
212
+ )}
213
+ {...props}
214
+ >
215
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
216
+ <span className='absolute right-2 flex h-4 w-4 items-center justify-center'>
217
+ <SelectPrimitive.ItemIndicator>
218
+ <IconCheck className='h-4 w-4' stroke={2} />
219
+ </SelectPrimitive.ItemIndicator>
220
+ </span>
221
+ </SelectPrimitive.Item>
222
+ ));
223
+ SelectItem.displayName = 'SelectItem';
224
+
225
+ // ==========================================================================
226
+ // Exports
227
+ // ==========================================================================
228
+
229
+ export {
230
+ Select,
231
+ SelectContent,
232
+ SelectGroup,
233
+ SelectItem,
234
+ SelectLabel,
235
+ SelectScrollDownButton,
236
+ SelectScrollUpButton,
237
+ SelectSeparator,
238
+ SelectTrigger,
239
+ SelectValue,
240
+ };
@@ -0,0 +1,343 @@
1
+ 'use client';
2
+
3
+ import {
4
+ IconLayoutSidebarLeftCollapseFilled,
5
+ IconLayoutSidebarLeftExpandFilled,
6
+ IconMenu2,
7
+ IconX,
8
+ } from '@tabler/icons-react';
9
+ import {
10
+ type ComponentType,
11
+ createContext,
12
+ type ReactNode,
13
+ useContext,
14
+ useEffect,
15
+ useState,
16
+ } from 'react';
17
+ import { Button } from '@/components/ui/button';
18
+ import {
19
+ Drawer,
20
+ DrawerClose,
21
+ DrawerContent,
22
+ DrawerTitle,
23
+ DrawerTrigger,
24
+ } from '@/components/ui/drawer';
25
+ import { useMediaQuery } from '@/hooks';
26
+ import { Link, usePathname } from '@/i18n/navigation';
27
+ import { cn } from '@/lib/utils';
28
+
29
+ // =============================================================================
30
+ // Types
31
+ // =============================================================================
32
+
33
+ export interface SidebarNavItem {
34
+ name: string;
35
+ href: string;
36
+ icon: ComponentType<{ size?: number; className?: string }>;
37
+ }
38
+
39
+ export interface SidebarProps {
40
+ /** Navigation items */
41
+ navigation: SidebarNavItem[];
42
+ /** Logo/header content */
43
+ logo?: ReactNode;
44
+ /** Link for logo click */
45
+ logoHref?: string;
46
+ /** Footer content - render prop with collapsed state */
47
+ footer?: ReactNode | ((collapsed: boolean) => ReactNode);
48
+ /** Default collapsed state for desktop */
49
+ defaultCollapsed?: boolean;
50
+ /** Custom class for sidebar container */
51
+ className?: string;
52
+ /** Breakpoint for mobile/desktop switch */
53
+ mobileBreakpoint?: 'md' | 'lg';
54
+ /** Desktop placement */
55
+ variant?: 'attached' | 'floating';
56
+ /** Whether the desktop rail can collapse */
57
+ collapsible?: boolean;
58
+ }
59
+
60
+ const sidebarBreakpoints = {
61
+ md: {
62
+ query: '(min-width: 768px)',
63
+ mobileClassName: 'md:hidden',
64
+ desktopClassName: 'md:flex',
65
+ },
66
+ lg: {
67
+ query: '(min-width: 1024px)',
68
+ mobileClassName: 'lg:hidden',
69
+ desktopClassName: 'lg:flex',
70
+ },
71
+ };
72
+
73
+ // =============================================================================
74
+ // Context
75
+ // =============================================================================
76
+
77
+ interface SidebarContextValue {
78
+ collapsed: boolean;
79
+ setCollapsed: (value: boolean) => void;
80
+ }
81
+
82
+ const SidebarContext = createContext<SidebarContextValue | null>(null);
83
+
84
+ export function useSidebarContext() {
85
+ const context = useContext(SidebarContext);
86
+ if (!context) {
87
+ throw new Error('Sidebar components must be used within Sidebar');
88
+ }
89
+ return context;
90
+ }
91
+
92
+ // =============================================================================
93
+ // Navigation Content (shared between mobile & desktop)
94
+ // =============================================================================
95
+
96
+ function NavigationContent({
97
+ navigation,
98
+ collapsed = false,
99
+ onNavigate,
100
+ }: {
101
+ navigation: SidebarNavItem[];
102
+ collapsed?: boolean;
103
+ onNavigate?: () => void;
104
+ }) {
105
+ const pathname = usePathname();
106
+
107
+ return (
108
+ <nav className='flex-1 space-y-1 p-2'>
109
+ {navigation.map((item) => {
110
+ // Exact match only - no startsWith to avoid /admin matching /admin/users
111
+ const isActive = pathname === item.href;
112
+
113
+ return (
114
+ <Link
115
+ key={item.name}
116
+ href={item.href}
117
+ onClick={onNavigate}
118
+ aria-current={isActive ? 'page' : undefined}
119
+ className={cn(
120
+ // Icon stays LEFT, never moves. Text fades out.
121
+ 'flex min-w-0 items-start gap-3 overflow-hidden rounded-md px-3 py-2 text-sm transition-colors',
122
+ isActive
123
+ ? 'bg-muted text-foreground font-medium hover:bg-muted/80'
124
+ : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground',
125
+ )}
126
+ title={collapsed ? item.name : undefined}
127
+ >
128
+ <item.icon size={18} className='mt-0.5 shrink-0' />
129
+ <span
130
+ className={cn(
131
+ 'min-w-0 flex-1 break-words leading-5 transition-opacity duration-200',
132
+ collapsed && 'whitespace-nowrap',
133
+ )}
134
+ style={{
135
+ opacity: collapsed ? 0 : 1,
136
+ }}
137
+ >
138
+ {item.name}
139
+ </span>
140
+ </Link>
141
+ );
142
+ })}
143
+ </nav>
144
+ );
145
+ }
146
+
147
+ // =============================================================================
148
+ // Mobile Sidebar (Drawer)
149
+ // =============================================================================
150
+
151
+ function MobileSidebar({
152
+ navigation,
153
+ logo,
154
+ logoHref,
155
+ footer,
156
+ mobileBreakpoint,
157
+ }: Omit<SidebarProps, 'defaultCollapsed' | 'className'>) {
158
+ const [open, setOpen] = useState(false);
159
+ const breakpoint = mobileBreakpoint ?? 'lg';
160
+ const isDesktop = useMediaQuery(sidebarBreakpoints[breakpoint].query);
161
+
162
+ // Close drawer when screen becomes desktop
163
+ useEffect(() => {
164
+ if (isDesktop && open) {
165
+ setOpen(false);
166
+ }
167
+ }, [isDesktop, open]);
168
+
169
+ return (
170
+ <Drawer direction='left' open={open} onOpenChange={setOpen}>
171
+ <DrawerTrigger asChild>
172
+ <Button
173
+ aria-label='Open navigation'
174
+ variant='ghost'
175
+ size='icon'
176
+ className={cn(
177
+ 'absolute left-3 top-2 z-50',
178
+ sidebarBreakpoints[breakpoint].mobileClassName,
179
+ )}
180
+ >
181
+ <IconMenu2 size={22} />
182
+ </Button>
183
+ </DrawerTrigger>
184
+ <DrawerContent className='w-64' aria-describedby={undefined}>
185
+ {/* Accessibility: hidden title for screen readers */}
186
+ <DrawerTitle className='sr-only'>Navigation Menu</DrawerTitle>
187
+ {/* Header */}
188
+ <div className='h-14 flex items-center justify-between px-3 border-b border-border/50'>
189
+ {logo && logoHref ? (
190
+ <Link
191
+ href={logoHref}
192
+ className='font-display font-semibold text-sm'
193
+ onClick={() => setOpen(false)}
194
+ >
195
+ {logo}
196
+ </Link>
197
+ ) : (
198
+ <span className='font-display font-semibold text-sm'>{logo}</span>
199
+ )}
200
+ <DrawerClose asChild>
201
+ <Button
202
+ aria-label='Close navigation'
203
+ variant='ghost'
204
+ size='icon'
205
+ className='h-8 w-8'
206
+ >
207
+ <IconX size={16} />
208
+ </Button>
209
+ </DrawerClose>
210
+ </div>
211
+
212
+ <NavigationContent navigation={navigation} onNavigate={() => setOpen(false)} />
213
+
214
+ {/* Footer */}
215
+ {footer && (
216
+ <div className='p-3'>{typeof footer === 'function' ? footer(false) : footer}</div>
217
+ )}
218
+ </DrawerContent>
219
+ </Drawer>
220
+ );
221
+ }
222
+
223
+ // =============================================================================
224
+ // Desktop Sidebar
225
+ // =============================================================================
226
+
227
+ function DesktopSidebar({
228
+ navigation,
229
+ logo,
230
+ logoHref,
231
+ footer,
232
+ defaultCollapsed = false,
233
+ className,
234
+ mobileBreakpoint = 'lg',
235
+ variant = 'attached',
236
+ collapsible = true,
237
+ }: SidebarProps) {
238
+ const [collapsed, setCollapsed] = useState(defaultCollapsed);
239
+
240
+ return (
241
+ <SidebarContext.Provider value={{ collapsed, setCollapsed }}>
242
+ <aside
243
+ className={cn(
244
+ 'hidden flex-col transition-[width] duration-300',
245
+ sidebarBreakpoints[mobileBreakpoint].desktopClassName,
246
+ variant === 'floating'
247
+ ? 'absolute left-5 top-20 z-30 max-h-[calc(100dvh-6.5rem)] overflow-y-auto rounded-2xl border border-border bg-card/90 shadow-xl shadow-black/5 backdrop-blur'
248
+ : 'h-full shrink-0 border-r border-border/50 bg-muted/30',
249
+ collapsed && collapsible ? 'w-14' : 'w-60',
250
+ className,
251
+ )}
252
+ >
253
+ {/* Header */}
254
+ <div className='h-12 flex items-center px-3 border-b border-border/50 relative'>
255
+ {/* Logo - left, fades out */}
256
+ <div
257
+ className='transition-opacity duration-200'
258
+ style={{
259
+ opacity: collapsed ? 0 : 1,
260
+ pointerEvents: collapsed ? 'none' : 'auto',
261
+ }}
262
+ >
263
+ {logo && logoHref ? (
264
+ <Link
265
+ href={logoHref}
266
+ className='font-display font-semibold text-base whitespace-nowrap text-foreground'
267
+ >
268
+ {logo}
269
+ </Link>
270
+ ) : (
271
+ <span className='font-display font-semibold text-base whitespace-nowrap text-foreground'>
272
+ {logo}
273
+ </span>
274
+ )}
275
+ </div>
276
+ {/* Button - right when expanded, center when collapsed */}
277
+ {collapsible ? (
278
+ <Button
279
+ aria-label={collapsed ? 'Expand navigation' : 'Collapse navigation'}
280
+ variant='ghost'
281
+ size='icon'
282
+ onClick={() => setCollapsed(!collapsed)}
283
+ className='absolute right-3 h-8 w-8'
284
+ >
285
+ {collapsed ? (
286
+ <IconLayoutSidebarLeftExpandFilled size={16} />
287
+ ) : (
288
+ <IconLayoutSidebarLeftCollapseFilled size={16} />
289
+ )}
290
+ </Button>
291
+ ) : null}
292
+ </div>
293
+
294
+ <NavigationContent navigation={navigation} collapsed={collapsed} />
295
+
296
+ {/* Footer */}
297
+ {footer && (
298
+ <div className='p-2'>{typeof footer === 'function' ? footer(collapsed) : footer}</div>
299
+ )}
300
+ </aside>
301
+ </SidebarContext.Provider>
302
+ );
303
+ }
304
+
305
+ // =============================================================================
306
+ // Main Sidebar Component
307
+ // =============================================================================
308
+
309
+ export function Sidebar(props: SidebarProps) {
310
+ return (
311
+ <>
312
+ <MobileSidebar {...props} />
313
+ <DesktopSidebar {...props} />
314
+ </>
315
+ );
316
+ }
317
+
318
+ // =============================================================================
319
+ // Sidebar Footer Link (helper component)
320
+ // =============================================================================
321
+
322
+ export function SidebarFooterLink({
323
+ href,
324
+ icon: Icon,
325
+ children,
326
+ onClick,
327
+ }: {
328
+ href: string;
329
+ icon: ComponentType<{ size?: number }>;
330
+ children: ReactNode;
331
+ onClick?: () => void;
332
+ }) {
333
+ return (
334
+ <Link
335
+ href={href}
336
+ onClick={onClick}
337
+ className='flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground'
338
+ >
339
+ <Icon size={18} />
340
+ <span>{children}</span>
341
+ </Link>
342
+ );
343
+ }
@@ -0,0 +1,8 @@
1
+ import type { HTMLAttributes } from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ function Skeleton({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
5
+ return <div className={cn('animate-pulse rounded-md bg-muted', className)} {...props} />;
6
+ }
7
+
8
+ export { Skeleton };
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ import * as SliderPrimitive from '@radix-ui/react-slider';
4
+ import { type ComponentPropsWithoutRef, type ComponentRef, forwardRef } from 'react';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ export const Slider = forwardRef<
8
+ ComponentRef<typeof SliderPrimitive.Root>,
9
+ ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
10
+ >(({ className, 'aria-label': ariaLabel, ...props }, ref) => (
11
+ <SliderPrimitive.Root
12
+ ref={ref}
13
+ className={cn('relative flex w-full touch-none select-none items-center', className)}
14
+ {...props}
15
+ >
16
+ <SliderPrimitive.Track className='relative h-2 w-full grow overflow-hidden rounded-full bg-muted'>
17
+ <SliderPrimitive.Range className='absolute h-full bg-primary' />
18
+ </SliderPrimitive.Track>
19
+ <SliderPrimitive.Thumb
20
+ aria-label={ariaLabel}
21
+ className={cn(
22
+ 'block h-5 w-5 rounded-full border-2 border-primary bg-background shadow-md',
23
+ 'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
24
+ 'disabled:pointer-events-none disabled:opacity-50',
25
+ 'cursor-grab active:cursor-grabbing',
26
+ )}
27
+ />
28
+ </SliderPrimitive.Root>
29
+ ));
30
+
31
+ Slider.displayName = 'Slider';
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ import { motion } from 'framer-motion';
4
+ import type { ReactNode } from 'react';
5
+
6
+ const DEFAULT_ITEM_HEIGHT = 36; // h-9
7
+
8
+ interface SlotDrumProps<T> {
9
+ items: T[];
10
+ activeIndex: number;
11
+ renderItem: (item: T, index: number) => ReactNode;
12
+ getItemKey: (item: T) => string;
13
+ itemHeight?: number;
14
+ className?: string;
15
+ }
16
+
17
+ export function SlotDrum<T>({
18
+ items,
19
+ activeIndex,
20
+ renderItem,
21
+ getItemKey,
22
+ itemHeight = DEFAULT_ITEM_HEIGHT,
23
+ className,
24
+ }: SlotDrumProps<T>) {
25
+ return (
26
+ <div
27
+ className={`relative overflow-hidden ${className ?? ''}`}
28
+ style={{ height: itemHeight }}
29
+ >
30
+ <motion.div
31
+ animate={{ y: -activeIndex * itemHeight }}
32
+ transition={{
33
+ type: 'spring',
34
+ stiffness: 300,
35
+ damping: 30,
36
+ }}
37
+ >
38
+ {items.map((item, index) => (
39
+ <div
40
+ key={getItemKey(item)}
41
+ className='flex items-center'
42
+ style={{ height: itemHeight }}
43
+ >
44
+ {renderItem(item, index)}
45
+ </div>
46
+ ))}
47
+ </motion.div>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,30 @@
1
+ import { cn } from '@/lib/utils';
2
+
3
+ export function Spinner({ className }: { className?: string }) {
4
+ return (
5
+ <svg
6
+ aria-label='Loading'
7
+ className={cn('size-4', className)}
8
+ viewBox='0 0 24 24'
9
+ fill='none'
10
+ xmlns='http://www.w3.org/2000/svg'
11
+ >
12
+ <circle cx='12' cy='12' r='9' stroke='currentColor' strokeOpacity='0.2' strokeWidth='2' />
13
+ <path
14
+ d='M21 12a9 9 0 0 0-9-9'
15
+ stroke='currentColor'
16
+ strokeLinecap='round'
17
+ strokeWidth='2'
18
+ >
19
+ <animateTransform
20
+ attributeName='transform'
21
+ type='rotate'
22
+ from='0 12 12'
23
+ to='360 12 12'
24
+ dur='1s'
25
+ repeatCount='indefinite'
26
+ />
27
+ </path>
28
+ </svg>
29
+ );
30
+ }