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,85 @@
1
+ 'use client';
2
+
3
+ import { IconCheck, IconCopy } from '@tabler/icons-react';
4
+ import { type ReactNode, useCallback, useState } from 'react';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ export interface CopyableTextProps {
8
+ /** Value to copy to clipboard */
9
+ value: string;
10
+ /** Custom display content (defaults to value) */
11
+ children?: ReactNode;
12
+ /** Additional class names */
13
+ className?: string;
14
+ /** Icon size (default: 14) */
15
+ iconSize?: number;
16
+ /** Duration to show success state in ms (default: 2000) */
17
+ successDuration?: number;
18
+ /** Callback after successful copy */
19
+ onCopy?: (value: string) => void;
20
+ }
21
+
22
+ /**
23
+ * Text with click-to-copy functionality.
24
+ * Shows copy icon on hover, checkmark on success.
25
+ *
26
+ * @example
27
+ * // Basic usage
28
+ * <CopyableText value={email} />
29
+ *
30
+ * // Custom display
31
+ * <CopyableText value={fullId}>
32
+ * {shortId}
33
+ * </CopyableText>
34
+ *
35
+ * // Custom styling
36
+ * <CopyableText value={code} className="font-mono text-xs" />
37
+ */
38
+ export function CopyableText({
39
+ value,
40
+ children,
41
+ className,
42
+ iconSize = 14,
43
+ successDuration = 2000,
44
+ onCopy,
45
+ }: CopyableTextProps) {
46
+ const [copied, setCopied] = useState(false);
47
+
48
+ const handleCopy = useCallback(async () => {
49
+ try {
50
+ await navigator.clipboard.writeText(value);
51
+ setCopied(true);
52
+ onCopy?.(value);
53
+ setTimeout(() => setCopied(false), successDuration);
54
+ } catch (err) {
55
+ console.error('Failed to copy:', err);
56
+ }
57
+ }, [value, successDuration, onCopy]);
58
+
59
+ return (
60
+ <button
61
+ type='button'
62
+ onClick={handleCopy}
63
+ className={cn(
64
+ 'group inline-flex items-center gap-1.5 text-left transition-colors',
65
+ 'text-muted-foreground hover:text-foreground',
66
+ 'cursor-pointer',
67
+ className,
68
+ )}
69
+ title={`Copy: ${value}`}
70
+ >
71
+ <span className='truncate'>{children ?? value}</span>
72
+ <span className='shrink-0'>
73
+ {copied ? (
74
+ <IconCheck size={iconSize} className='text-success' aria-label='Copied' />
75
+ ) : (
76
+ <IconCopy
77
+ size={iconSize}
78
+ className='opacity-0 group-hover:opacity-50 transition-opacity'
79
+ aria-label='Copy to clipboard'
80
+ />
81
+ )}
82
+ </span>
83
+ </button>
84
+ );
85
+ }
@@ -0,0 +1,136 @@
1
+ 'use client';
2
+
3
+ import {
4
+ type ComponentPropsWithoutRef,
5
+ type ComponentRef,
6
+ createContext,
7
+ forwardRef,
8
+ type HTMLAttributes,
9
+ useContext,
10
+ } from 'react';
11
+ import { Drawer as DrawerPrimitive } from 'vaul';
12
+ import { cn } from '@/lib/utils';
13
+
14
+ type DrawerDirection = 'left' | 'right' | 'top' | 'bottom';
15
+
16
+ const DrawerDirectionContext = createContext<DrawerDirection>('right');
17
+
18
+ const Drawer = ({
19
+ shouldScaleBackground = true,
20
+ direction = 'right',
21
+ ...props
22
+ }: ComponentPropsWithoutRef<typeof DrawerPrimitive.Root>) => (
23
+ <DrawerDirectionContext.Provider value={direction}>
24
+ <DrawerPrimitive.Root
25
+ direction={direction}
26
+ shouldScaleBackground={shouldScaleBackground}
27
+ {...props}
28
+ />
29
+ </DrawerDirectionContext.Provider>
30
+ );
31
+ Drawer.displayName = 'Drawer';
32
+
33
+ const DrawerTrigger = DrawerPrimitive.Trigger;
34
+
35
+ const DrawerPortal = DrawerPrimitive.Portal;
36
+
37
+ const DrawerClose = DrawerPrimitive.Close;
38
+
39
+ const DrawerOverlay = forwardRef<
40
+ ComponentRef<typeof DrawerPrimitive.Overlay>,
41
+ ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
42
+ >(({ className, ...props }, ref) => (
43
+ <DrawerPrimitive.Overlay
44
+ ref={ref}
45
+ className={cn('fixed inset-0 z-50 bg-overlay backdrop-blur-sm', className)}
46
+ {...props}
47
+ />
48
+ ));
49
+ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
50
+
51
+ const DrawerContent = forwardRef<
52
+ ComponentRef<typeof DrawerPrimitive.Content>,
53
+ ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
54
+ >(({ className, children, ...props }, ref) => {
55
+ const direction = useContext(DrawerDirectionContext);
56
+ const directionStyles = {
57
+ left: 'inset-y-3 left-3 h-auto w-[min(22rem,calc(100vw-1.5rem))] rounded-2xl',
58
+ right: 'inset-y-3 right-3 h-auto w-[min(22rem,calc(100vw-1.5rem))] rounded-2xl',
59
+ top: 'inset-x-3 top-3 w-auto rounded-2xl',
60
+ bottom: 'inset-x-3 bottom-3 w-auto rounded-2xl',
61
+ };
62
+
63
+ return (
64
+ <DrawerPortal>
65
+ <DrawerOverlay />
66
+ <DrawerPrimitive.Content
67
+ ref={ref}
68
+ className={cn(
69
+ 'fixed z-50 flex flex-col overflow-hidden border bg-card shadow-xl',
70
+ directionStyles[direction],
71
+ className,
72
+ )}
73
+ {...props}
74
+ >
75
+ {/* Handle for bottom/top drawers */}
76
+ {(direction === 'bottom' || direction === 'top') && (
77
+ <div
78
+ className={cn(
79
+ 'mx-auto w-12 h-1.5 rounded-full bg-muted-foreground/20',
80
+ direction === 'bottom' ? 'mt-4' : 'mb-4 mt-auto',
81
+ )}
82
+ />
83
+ )}
84
+ {children}
85
+ </DrawerPrimitive.Content>
86
+ </DrawerPortal>
87
+ );
88
+ });
89
+ DrawerContent.displayName = 'DrawerContent';
90
+
91
+ const DrawerHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
92
+ <div className={cn('flex flex-col space-y-1.5 p-4', className)} {...props} />
93
+ );
94
+ DrawerHeader.displayName = 'DrawerHeader';
95
+
96
+ const DrawerFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
97
+ <div className={cn('mt-auto flex flex-col gap-2 p-4', className)} {...props} />
98
+ );
99
+ DrawerFooter.displayName = 'DrawerFooter';
100
+
101
+ const DrawerTitle = forwardRef<
102
+ ComponentRef<typeof DrawerPrimitive.Title>,
103
+ ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
104
+ >(({ className, ...props }, ref) => (
105
+ <DrawerPrimitive.Title
106
+ ref={ref}
107
+ className={cn('text-lg font-semibold leading-none tracking-tight', className)}
108
+ {...props}
109
+ />
110
+ ));
111
+ DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
112
+
113
+ const DrawerDescription = forwardRef<
114
+ ComponentRef<typeof DrawerPrimitive.Description>,
115
+ ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
116
+ >(({ className, ...props }, ref) => (
117
+ <DrawerPrimitive.Description
118
+ ref={ref}
119
+ className={cn('text-sm text-muted-foreground', className)}
120
+ {...props}
121
+ />
122
+ ));
123
+ DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
124
+
125
+ export {
126
+ Drawer,
127
+ DrawerClose,
128
+ DrawerContent,
129
+ DrawerDescription,
130
+ DrawerFooter,
131
+ DrawerHeader,
132
+ DrawerOverlay,
133
+ DrawerPortal,
134
+ DrawerTitle,
135
+ DrawerTrigger,
136
+ };
@@ -0,0 +1,185 @@
1
+ 'use client';
2
+
3
+ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
4
+ import { IconCheck, IconChevronRight, IconCircle } from '@tabler/icons-react';
5
+ import {
6
+ type ComponentPropsWithoutRef,
7
+ type ComponentRef,
8
+ forwardRef,
9
+ type HTMLAttributes,
10
+ } from 'react';
11
+ import { cn } from '@/lib/utils';
12
+
13
+ const DropdownMenu = DropdownMenuPrimitive.Root;
14
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
15
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
16
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
17
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
19
+
20
+ const DropdownMenuSubTrigger = forwardRef<
21
+ ComponentRef<typeof DropdownMenuPrimitive.SubTrigger>,
22
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
23
+ inset?: boolean;
24
+ }
25
+ >(({ className, inset, children, ...props }, ref) => (
26
+ <DropdownMenuPrimitive.SubTrigger
27
+ ref={ref}
28
+ className={cn(
29
+ 'flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none focus:bg-muted data-[state=open]:bg-muted [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
30
+ inset && 'pl-8',
31
+ className,
32
+ )}
33
+ {...props}
34
+ >
35
+ {children}
36
+ <IconChevronRight className='ml-auto' />
37
+ </DropdownMenuPrimitive.SubTrigger>
38
+ ));
39
+ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
40
+
41
+ const DropdownMenuSubContent = forwardRef<
42
+ ComponentRef<typeof DropdownMenuPrimitive.SubContent>,
43
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
44
+ >(({ className, ...props }, ref) => (
45
+ <DropdownMenuPrimitive.SubContent
46
+ ref={ref}
47
+ className={cn(
48
+ 'z-50 min-w-32 overflow-hidden rounded-xl border bg-background p-1 text-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
49
+ className,
50
+ )}
51
+ {...props}
52
+ />
53
+ ));
54
+ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
55
+
56
+ const DropdownMenuContent = forwardRef<
57
+ ComponentRef<typeof DropdownMenuPrimitive.Content>,
58
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
59
+ >(({ className, sideOffset = 4, ...props }, ref) => (
60
+ <DropdownMenuPrimitive.Portal>
61
+ <DropdownMenuPrimitive.Content
62
+ ref={ref}
63
+ sideOffset={sideOffset}
64
+ className={cn(
65
+ 'z-50 min-w-32 overflow-hidden rounded-xl border bg-background p-1 text-foreground shadow-lg',
66
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
67
+ className,
68
+ )}
69
+ {...props}
70
+ />
71
+ </DropdownMenuPrimitive.Portal>
72
+ ));
73
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
74
+
75
+ const DropdownMenuItem = forwardRef<
76
+ ComponentRef<typeof DropdownMenuPrimitive.Item>,
77
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
78
+ inset?: boolean;
79
+ }
80
+ >(({ className, inset, ...props }, ref) => (
81
+ <DropdownMenuPrimitive.Item
82
+ ref={ref}
83
+ className={cn(
84
+ 'relative flex cursor-pointer select-none items-center gap-2 rounded-lg px-2 py-1.5 text-sm outline-none transition-colors focus:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
85
+ inset && 'pl-8',
86
+ className,
87
+ )}
88
+ {...props}
89
+ />
90
+ ));
91
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
92
+
93
+ const DropdownMenuCheckboxItem = forwardRef<
94
+ ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem>,
95
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
96
+ >(({ className, children, checked, ...props }, ref) => (
97
+ <DropdownMenuPrimitive.CheckboxItem
98
+ ref={ref}
99
+ className={cn(
100
+ 'relative flex cursor-pointer select-none items-center rounded-lg py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50',
101
+ className,
102
+ )}
103
+ checked={checked}
104
+ {...props}
105
+ >
106
+ <span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
107
+ <DropdownMenuPrimitive.ItemIndicator>
108
+ <IconCheck className='h-4 w-4' />
109
+ </DropdownMenuPrimitive.ItemIndicator>
110
+ </span>
111
+ {children}
112
+ </DropdownMenuPrimitive.CheckboxItem>
113
+ ));
114
+ DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
115
+
116
+ const DropdownMenuRadioItem = forwardRef<
117
+ ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,
118
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
119
+ >(({ className, children, ...props }, ref) => (
120
+ <DropdownMenuPrimitive.RadioItem
121
+ ref={ref}
122
+ className={cn(
123
+ 'relative flex cursor-pointer select-none items-center rounded-lg py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50',
124
+ className,
125
+ )}
126
+ {...props}
127
+ >
128
+ <span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
129
+ <DropdownMenuPrimitive.ItemIndicator>
130
+ <IconCircle className='h-2 w-2 fill-current' />
131
+ </DropdownMenuPrimitive.ItemIndicator>
132
+ </span>
133
+ {children}
134
+ </DropdownMenuPrimitive.RadioItem>
135
+ ));
136
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
137
+
138
+ const DropdownMenuLabel = forwardRef<
139
+ ComponentRef<typeof DropdownMenuPrimitive.Label>,
140
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
141
+ inset?: boolean;
142
+ }
143
+ >(({ className, inset, ...props }, ref) => (
144
+ <DropdownMenuPrimitive.Label
145
+ ref={ref}
146
+ className={cn('px-2 py-1.5 text-sm font-medium', inset && 'pl-8', className)}
147
+ {...props}
148
+ />
149
+ ));
150
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
151
+
152
+ const DropdownMenuSeparator = forwardRef<
153
+ ComponentRef<typeof DropdownMenuPrimitive.Separator>,
154
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
155
+ >(({ className, ...props }, ref) => (
156
+ <DropdownMenuPrimitive.Separator
157
+ ref={ref}
158
+ className={cn('-mx-1 my-1 h-px bg-border', className)}
159
+ {...props}
160
+ />
161
+ ));
162
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
163
+
164
+ const DropdownMenuShortcut = ({ className, ...props }: HTMLAttributes<HTMLSpanElement>) => (
165
+ <span className={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...props} />
166
+ );
167
+ DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
168
+
169
+ export {
170
+ DropdownMenu,
171
+ DropdownMenuCheckboxItem,
172
+ DropdownMenuContent,
173
+ DropdownMenuGroup,
174
+ DropdownMenuItem,
175
+ DropdownMenuLabel,
176
+ DropdownMenuPortal,
177
+ DropdownMenuRadioGroup,
178
+ DropdownMenuRadioItem,
179
+ DropdownMenuSeparator,
180
+ DropdownMenuShortcut,
181
+ DropdownMenuSub,
182
+ DropdownMenuSubContent,
183
+ DropdownMenuSubTrigger,
184
+ DropdownMenuTrigger,
185
+ };
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Label } from './label';
3
+
4
+ interface FieldProps {
5
+ label: string;
6
+ htmlFor: string;
7
+ description?: string;
8
+ error?: string;
9
+ children: ReactNode;
10
+ }
11
+
12
+ export function Field({ label, htmlFor, description, error, children }: FieldProps) {
13
+ const messageId = `${htmlFor}-message`;
14
+ return (
15
+ <div className='grid gap-2'>
16
+ <Label htmlFor={htmlFor}>{label}</Label>
17
+ {children}
18
+ {(error || description) && (
19
+ <p
20
+ id={messageId}
21
+ className={error ? 'text-sm text-destructive' : 'text-sm text-muted-foreground'}
22
+ >
23
+ {error ?? description}
24
+ </p>
25
+ )}
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,41 @@
1
+ import { cn } from '@/lib/utils';
2
+
3
+ interface HandDrawnIconProps {
4
+ className?: string;
5
+ }
6
+
7
+ /** Рукописный крестик — для "плохо", "не нужно" */
8
+ export function HandDrawnX({ className }: HandDrawnIconProps) {
9
+ return (
10
+ <svg
11
+ aria-hidden='true'
12
+ viewBox='0 0 24 24'
13
+ fill='none'
14
+ stroke='currentColor'
15
+ strokeWidth='2.5'
16
+ strokeLinecap='round'
17
+ className={cn('w-4 h-4', className)}
18
+ >
19
+ <path d='M5 5 C 8 9, 14 16, 19 19' />
20
+ <path d='M19 5 C 15 8, 10 14, 5 19' />
21
+ </svg>
22
+ );
23
+ }
24
+
25
+ /** Рукописная галочка — для "хорошо", "нужно" */
26
+ export function HandDrawnCheck({ className }: HandDrawnIconProps) {
27
+ return (
28
+ <svg
29
+ aria-hidden='true'
30
+ viewBox='0 0 24 24'
31
+ fill='none'
32
+ stroke='currentColor'
33
+ strokeWidth='2.5'
34
+ strokeLinecap='round'
35
+ strokeLinejoin='round'
36
+ className={cn('w-4 h-4', className)}
37
+ >
38
+ <path d='M4 12 C 6 14, 8 17, 10 19 C 12 15, 16 9, 20 5' />
39
+ </svg>
40
+ );
41
+ }
@@ -0,0 +1,36 @@
1
+ export { AdaptiveModal } from './adaptive-modal';
2
+ export { AnimatedCounter } from './animated-counter';
3
+ export { Avatar, AvatarFallback, AvatarImage } from './avatar';
4
+ export { Badge } from './badge';
5
+ export { Button } from './button';
6
+ export { buttonVariants } from './button-variants';
7
+ export * from './card';
8
+ export { CardDeck, type CardDeckProps } from './card-deck';
9
+ export * from './charts';
10
+ export { Checkbox, TriStateCheckbox } from './checkbox';
11
+ export { ConfirmationModal } from './confirmation-modal';
12
+ export { CopyableText } from './copyable-text';
13
+ export * from './drawer';
14
+ export * from './dropdown-menu';
15
+ export { Field } from './field';
16
+ export { HandDrawnCheck, HandDrawnX } from './hand-drawn-icons';
17
+ export { Input } from './input';
18
+ export { Label } from './label';
19
+ export { OtpInput } from './otp-input';
20
+ export * from './select';
21
+ export * from './sidebar';
22
+ export { Skeleton } from './skeleton';
23
+ export { Slider } from './slider';
24
+ export { SlotDrum } from './slot-drum';
25
+ export { Spinner } from './spinner';
26
+ export { Switch } from './switch';
27
+ export * from './table';
28
+ export { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs';
29
+ export { Textarea } from './textarea';
30
+ export { Toaster, toast } from './toaster';
31
+ export {
32
+ Tooltip,
33
+ TooltipContent,
34
+ TooltipProvider,
35
+ TooltipTrigger,
36
+ } from './tooltip';
@@ -0,0 +1,24 @@
1
+ import { forwardRef, type InputHTMLAttributes } from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
5
+
6
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
7
+ ({ className, ...props }, ref) => {
8
+ return (
9
+ <input
10
+ ref={ref}
11
+ className={cn(
12
+ 'h-10 w-full px-4 bg-background border border-border rounded-lg',
13
+ 'placeholder:text-muted-foreground',
14
+ 'focus:outline-none focus:border-primary',
15
+ 'transition-colors',
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ );
21
+ },
22
+ );
23
+
24
+ Input.displayName = 'Input';
@@ -0,0 +1,18 @@
1
+ import type { LabelHTMLAttributes } from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
5
+ htmlFor: string;
6
+ }
7
+
8
+ export function Label({ children, className, htmlFor, ...props }: LabelProps) {
9
+ return (
10
+ <label
11
+ className={cn('text-sm font-medium text-foreground', className)}
12
+ htmlFor={htmlFor}
13
+ {...props}
14
+ >
15
+ {children}
16
+ </label>
17
+ );
18
+ }
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * OTP Input
5
+ *
6
+ * Компонент для ввода 6-значного кода верификации.
7
+ * - 6 отдельных полей ввода
8
+ * - Автофокус на следующее поле
9
+ * - Поддержка вставки из буфера
10
+ */
11
+
12
+ import { type ClipboardEvent, type KeyboardEvent, useId, useRef, useState } from 'react';
13
+ import { cn } from '@/lib/utils';
14
+
15
+ interface OtpInputProps {
16
+ length?: number;
17
+ value: string;
18
+ onChange: (value: string) => void;
19
+ disabled?: boolean;
20
+ error?: boolean;
21
+ className?: string;
22
+ }
23
+
24
+ export function OtpInput({
25
+ length = 6,
26
+ value,
27
+ onChange,
28
+ disabled = false,
29
+ error = false,
30
+ className,
31
+ }: OtpInputProps) {
32
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
33
+ const inputGroupId = useId();
34
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
35
+
36
+ const digits = value.split('').slice(0, length);
37
+ while (digits.length < length) {
38
+ digits.push('');
39
+ }
40
+ const positions = digits.map((digit, position) => ({
41
+ digit,
42
+ id: `${inputGroupId}-digit-${position + 1}`,
43
+ position,
44
+ }));
45
+
46
+ const focusInput = (index: number) => {
47
+ const input = inputRefs.current[index];
48
+ if (input) {
49
+ input.focus();
50
+ input.select();
51
+ }
52
+ };
53
+
54
+ const handleChange = (index: number, digit: string) => {
55
+ if (disabled) return;
56
+
57
+ // Только цифры
58
+ if (digit && !/^\d$/.test(digit)) return;
59
+
60
+ const newDigits = [...digits];
61
+ newDigits[index] = digit;
62
+ const newValue = newDigits.join('');
63
+ onChange(newValue);
64
+
65
+ // Автофокус на следующее поле
66
+ if (digit && index < length - 1) {
67
+ focusInput(index + 1);
68
+ }
69
+ };
70
+
71
+ const handleKeyDown = (index: number, e: KeyboardEvent<HTMLInputElement>) => {
72
+ if (disabled) return;
73
+
74
+ if (e.key === 'Backspace') {
75
+ e.preventDefault();
76
+ if (digits[index]) {
77
+ // Удалить текущую цифру
78
+ handleChange(index, '');
79
+ } else if (index > 0) {
80
+ // Перейти на предыдущее поле и удалить
81
+ focusInput(index - 1);
82
+ handleChange(index - 1, '');
83
+ }
84
+ } else if (e.key === 'ArrowLeft' && index > 0) {
85
+ e.preventDefault();
86
+ focusInput(index - 1);
87
+ } else if (e.key === 'ArrowRight' && index < length - 1) {
88
+ e.preventDefault();
89
+ focusInput(index + 1);
90
+ }
91
+ };
92
+
93
+ const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
94
+ if (disabled) return;
95
+
96
+ e.preventDefault();
97
+ const pastedData = e.clipboardData.getData('text');
98
+ const digits = pastedData.replace(/\D/g, '').slice(0, length);
99
+
100
+ if (digits.length > 0) {
101
+ onChange(digits);
102
+ // Фокус на последнюю заполненную цифру или следующую пустую
103
+ const focusIndex = Math.min(digits.length, length - 1);
104
+ focusInput(focusIndex);
105
+ }
106
+ };
107
+
108
+ return (
109
+ <div className={cn('flex gap-2 justify-center', className)}>
110
+ {positions.map(({ digit, id, position }) => (
111
+ <input
112
+ key={id}
113
+ ref={(el) => {
114
+ inputRefs.current[position] = el;
115
+ }}
116
+ type='text'
117
+ inputMode='numeric'
118
+ maxLength={1}
119
+ value={digit}
120
+ disabled={disabled}
121
+ onChange={(e) => handleChange(position, e.target.value)}
122
+ onKeyDown={(e) => handleKeyDown(position, e)}
123
+ onPaste={handlePaste}
124
+ onFocus={() => setFocusedIndex(position)}
125
+ onBlur={() => setFocusedIndex(null)}
126
+ className={cn(
127
+ 'w-12 h-14 text-center text-2xl font-mono font-semibold',
128
+ 'bg-background border rounded-lg',
129
+ 'focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary',
130
+ 'transition-all duration-150',
131
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
132
+ error ? 'border-destructive focus:ring-destructive' : 'border-border',
133
+ focusedIndex === position && 'scale-105',
134
+ )}
135
+ aria-label={`Digit ${position + 1}`}
136
+ />
137
+ ))}
138
+ </div>
139
+ );
140
+ }