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.
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/cli.js +230 -0
- package/package.json +50 -0
- package/template/README.md +69 -0
- package/template/_env +11 -0
- package/template/_env.example +12 -0
- package/template/_gitignore +10 -0
- package/template/biome.json +43 -0
- package/template/bun.lock +1372 -0
- package/template/e2e/starter.spec.ts +237 -0
- package/template/ecosystem.config.cjs +28 -0
- package/template/ecosystem.dev.config.cjs +29 -0
- package/template/package.json +55 -0
- package/template/packages/backend/package.json +31 -0
- package/template/packages/backend/src/cli.ts +12 -0
- package/template/packages/backend/src/domain/errors.ts +9 -0
- package/template/packages/backend/src/domain/repository/github-cache.test.ts +72 -0
- package/template/packages/backend/src/domain/repository/github-cache.ts +205 -0
- package/template/packages/backend/src/index.ts +56 -0
- package/template/packages/backend/src/lib/db.ts +7 -0
- package/template/packages/backend/src/surface.ts +13 -0
- package/template/packages/backend/src/tools.ts +21 -0
- package/template/packages/backend/src/transport/errors.ts +28 -0
- package/template/packages/backend/src/transport/repository-service.ts +18 -0
- package/template/packages/backend/tsconfig.json +8 -0
- package/template/packages/config/package.json +22 -0
- package/template/packages/config/src/server.ts +24 -0
- package/template/packages/config/tsconfig.json +5 -0
- package/template/packages/db/migrations/20260808000000_init/migration.sql +16 -0
- package/template/packages/db/migrations/20260808170000_repository_visibility/migration.sql +4 -0
- package/template/packages/db/migrations/migration_lock.toml +1 -0
- package/template/packages/db/package.json +29 -0
- package/template/packages/db/prisma.config.ts +8 -0
- package/template/packages/db/schema.prisma +31 -0
- package/template/packages/db/tsconfig.json +5 -0
- package/template/packages/frontend/messages/en.json +15 -0
- package/template/packages/frontend/messages/ru.json +15 -0
- package/template/packages/frontend/next.config.ts +18 -0
- package/template/packages/frontend/package.json +62 -0
- package/template/packages/frontend/postcss.config.mjs +1 -0
- package/template/packages/frontend/public/ASSETS.md +6 -0
- package/template/packages/frontend/public/favicon/mascot-stitch.png +0 -0
- package/template/packages/frontend/public/icons/biome.svg +1 -0
- package/template/packages/frontend/public/icons/bun.svg +19 -0
- package/template/packages/frontend/public/icons/next.svg +1 -0
- package/template/packages/frontend/public/icons/postgresql.svg +1 -0
- package/template/packages/frontend/public/icons/prisma.svg +6 -0
- package/template/packages/frontend/public/icons/react-query.svg +9 -0
- package/template/packages/frontend/public/icons/react.svg +1 -0
- package/template/packages/frontend/public/icons/tailwind.svg +12 -0
- package/template/packages/frontend/public/icons/tsconfig.svg +1 -0
- package/template/packages/frontend/public/icons/typescript.svg +7 -0
- package/template/packages/frontend/public/icons/zod.svg +1 -0
- package/template/packages/frontend/public/mascot-stitch.png +0 -0
- package/template/packages/frontend/public/theme-dark.svg +8 -0
- package/template/packages/frontend/public/theme-light.svg +8 -0
- package/template/packages/frontend/src/app/[locale]/error.tsx +19 -0
- package/template/packages/frontend/src/app/[locale]/layout.tsx +59 -0
- package/template/packages/frontend/src/app/[locale]/loading.tsx +14 -0
- package/template/packages/frontend/src/app/[locale]/page.tsx +44 -0
- package/template/packages/frontend/src/app/[locale]/starter-page.tsx +151 -0
- package/template/packages/frontend/src/app/[locale]/ui/[story]/page.tsx +40 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/account-showcase.tsx +112 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/admin-showcase.tsx +101 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/catalogue-navigation.tsx +56 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/catalogue-shell.tsx +51 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/code-foundation.tsx +35 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/components-story.tsx +54 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/data-story.tsx +126 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/image-preloader.tsx +8 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/index.ts +3 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-sections.tsx +269 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-showcase.tsx +159 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/motion-story.tsx +78 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/overlays-story.tsx +134 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/payment-showcase.tsx +82 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/presentation-story.tsx +68 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/primitives-story.tsx +267 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/story-page.tsx +14 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/theme-story-client.tsx +208 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/theme-story.tsx +13 -0
- package/template/packages/frontend/src/app/[locale]/ui/layout.tsx +6 -0
- package/template/packages/frontend/src/app/[locale]/ui/page.tsx +10 -0
- package/template/packages/frontend/src/app/api/og/[locale]/[page]/route.tsx +53 -0
- package/template/packages/frontend/src/app/globals.css +210 -0
- package/template/packages/frontend/src/app/robots.ts +9 -0
- package/template/packages/frontend/src/app/sitemap.ts +22 -0
- package/template/packages/frontend/src/components/brand-mark.tsx +22 -0
- package/template/packages/frontend/src/components/repository-summary.tsx +107 -0
- package/template/packages/frontend/src/components/system-controls.tsx +92 -0
- package/template/packages/frontend/src/components/ui/adaptive-modal.tsx +382 -0
- package/template/packages/frontend/src/components/ui/animated-counter.tsx +80 -0
- package/template/packages/frontend/src/components/ui/avatar.tsx +46 -0
- package/template/packages/frontend/src/components/ui/badge.tsx +94 -0
- package/template/packages/frontend/src/components/ui/button-variants.ts +24 -0
- package/template/packages/frontend/src/components/ui/button.tsx +81 -0
- package/template/packages/frontend/src/components/ui/card-deck.tsx +73 -0
- package/template/packages/frontend/src/components/ui/card.tsx +81 -0
- package/template/packages/frontend/src/components/ui/charts/area-chart.tsx +167 -0
- package/template/packages/frontend/src/components/ui/charts/index.ts +7 -0
- package/template/packages/frontend/src/components/ui/checkbox.tsx +133 -0
- package/template/packages/frontend/src/components/ui/confirmation-modal.tsx +129 -0
- package/template/packages/frontend/src/components/ui/copyable-text.tsx +85 -0
- package/template/packages/frontend/src/components/ui/drawer.tsx +136 -0
- package/template/packages/frontend/src/components/ui/dropdown-menu.tsx +185 -0
- package/template/packages/frontend/src/components/ui/field.tsx +28 -0
- package/template/packages/frontend/src/components/ui/hand-drawn-icons.tsx +41 -0
- package/template/packages/frontend/src/components/ui/index.ts +36 -0
- package/template/packages/frontend/src/components/ui/input.tsx +24 -0
- package/template/packages/frontend/src/components/ui/label.tsx +18 -0
- package/template/packages/frontend/src/components/ui/otp-input.tsx +140 -0
- package/template/packages/frontend/src/components/ui/select.tsx +240 -0
- package/template/packages/frontend/src/components/ui/sidebar.tsx +343 -0
- package/template/packages/frontend/src/components/ui/skeleton.tsx +8 -0
- package/template/packages/frontend/src/components/ui/slider.tsx +31 -0
- package/template/packages/frontend/src/components/ui/slot-drum.tsx +50 -0
- package/template/packages/frontend/src/components/ui/spinner.tsx +30 -0
- package/template/packages/frontend/src/components/ui/switch.tsx +31 -0
- package/template/packages/frontend/src/components/ui/table.tsx +106 -0
- package/template/packages/frontend/src/components/ui/tabs.tsx +196 -0
- package/template/packages/frontend/src/components/ui/textarea.tsx +21 -0
- package/template/packages/frontend/src/components/ui/toaster.tsx +72 -0
- package/template/packages/frontend/src/components/ui/tooltip.tsx +31 -0
- package/template/packages/frontend/src/env.ts +21 -0
- package/template/packages/frontend/src/features/data-table/components/column-helpers.tsx +136 -0
- package/template/packages/frontend/src/features/data-table/components/data-table.tsx +303 -0
- package/template/packages/frontend/src/features/data-table/components/inline-stat.tsx +113 -0
- package/template/packages/frontend/src/features/data-table/components/sortable-header.tsx +33 -0
- package/template/packages/frontend/src/features/data-table/components/table-error.tsx +24 -0
- package/template/packages/frontend/src/features/data-table/components/table-filters.tsx +82 -0
- package/template/packages/frontend/src/features/data-table/hooks/useTableState.ts +111 -0
- package/template/packages/frontend/src/features/data-table/index.ts +26 -0
- package/template/packages/frontend/src/features/data-table/table-features.ts +26 -0
- package/template/packages/frontend/src/hooks/index.ts +3 -0
- package/template/packages/frontend/src/hooks/useInfiniteScroll.ts +67 -0
- package/template/packages/frontend/src/hooks/useIsTouchDevice.ts +25 -0
- package/template/packages/frontend/src/hooks/useMediaQuery.ts +30 -0
- package/template/packages/frontend/src/i18n/locales.ts +6 -0
- package/template/packages/frontend/src/i18n/navigation.ts +4 -0
- package/template/packages/frontend/src/i18n/request.ts +9 -0
- package/template/packages/frontend/src/i18n/routing.ts +8 -0
- package/template/packages/frontend/src/lib/api/client.ts +20 -0
- package/template/packages/frontend/src/lib/api/queries.ts +11 -0
- package/template/packages/frontend/src/lib/query-client.ts +20 -0
- package/template/packages/frontend/src/lib/realtime/repository.ts +21 -0
- package/template/packages/frontend/src/lib/seo/metadata.ts +50 -0
- package/template/packages/frontend/src/lib/seo/pages.test.ts +25 -0
- package/template/packages/frontend/src/lib/seo/pages.ts +107 -0
- package/template/packages/frontend/src/lib/utils/cn.ts +6 -0
- package/template/packages/frontend/src/lib/utils/format.ts +25 -0
- package/template/packages/frontend/src/lib/utils/index.ts +2 -0
- package/template/packages/frontend/src/providers/index.tsx +18 -0
- package/template/packages/frontend/src/providers/react-query.tsx +9 -0
- package/template/packages/frontend/src/providers/realtime.tsx +16 -0
- package/template/packages/frontend/src/proxy.ts +6 -0
- package/template/packages/frontend/src/theme/config.ts +20 -0
- package/template/packages/frontend/src/theme/transition.ts +54 -0
- package/template/packages/frontend/tsconfig.json +13 -0
- package/template/packages/shared/package.json +23 -0
- package/template/packages/shared/src/contracts/repository.ts +28 -0
- package/template/packages/shared/src/events/repository.ts +9 -0
- package/template/packages/shared/src/index.ts +3 -0
- package/template/packages/shared/src/schemas/repository.test.ts +36 -0
- package/template/packages/shared/src/schemas/repository.ts +28 -0
- package/template/packages/shared/tsconfig.json +5 -0
- package/template/playwright.config.ts +15 -0
- package/template/scripts/check-authored.ts +81 -0
- package/template/scripts/dev.ts +23 -0
- package/template/scripts/local-env.ts +12 -0
- package/template/scripts/runtime-smoke.ts +67 -0
- package/template/scripts/tooling-env.ts +15 -0
- 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
|
+
}
|