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,81 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Button - Универсальный компонент кнопки
5
+ *
6
+ * Варианты: default, primary, ghost, outline, destructive
7
+ * Размеры: sm, md, lg, icon
8
+ * Поддержка: loading, disabled, asChild (для Link)
9
+ *
10
+ * Powered by Radix UI Slot & CVA for Polymorphism
11
+ */
12
+
13
+ import { Slot } from '@radix-ui/react-slot';
14
+ import type { VariantProps } from 'class-variance-authority';
15
+ import { type ButtonHTMLAttributes, forwardRef } from 'react';
16
+ import { cn } from '@/lib/utils';
17
+ import { buttonVariants } from './button-variants';
18
+ import { Spinner } from './spinner';
19
+
20
+ // ==========================================================================
21
+ // Styles with CVA
22
+ // ==========================================================================
23
+
24
+ // ==========================================================================
25
+ // Types
26
+ // ==========================================================================
27
+
28
+ export interface ButtonProps
29
+ extends ButtonHTMLAttributes<HTMLButtonElement>,
30
+ VariantProps<typeof buttonVariants> {
31
+ asChild?: boolean;
32
+ loading?: boolean;
33
+ }
34
+
35
+ // ==========================================================================
36
+ // Component
37
+ // ==========================================================================
38
+
39
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
40
+ (
41
+ {
42
+ className,
43
+ variant,
44
+ size,
45
+ asChild = false,
46
+ loading = false,
47
+ children,
48
+ disabled,
49
+ ...props
50
+ },
51
+ ref,
52
+ ) => {
53
+ const Comp = asChild ? Slot : 'button';
54
+ const replacesChildrenWhileLoading = size === 'icon' || size === 'icon-md';
55
+
56
+ return (
57
+ <Comp
58
+ className={cn(buttonVariants({ variant, size, className }))}
59
+ ref={ref}
60
+ disabled={disabled || loading}
61
+ aria-busy={loading || undefined}
62
+ {...props}
63
+ >
64
+ {loading ? (
65
+ replacesChildrenWhileLoading ? (
66
+ <Spinner />
67
+ ) : (
68
+ <>
69
+ <Spinner />
70
+ {children}
71
+ </>
72
+ )
73
+ ) : (
74
+ children
75
+ )}
76
+ </Comp>
77
+ );
78
+ },
79
+ );
80
+
81
+ Button.displayName = 'Button';
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+
5
+ export interface CardDeckProps<T> {
6
+ /** Массив элементов для отображения */
7
+ items: T[];
8
+ /** Рендер-функция для каждой карточки */
9
+ renderCard: (item: T, index: number) => ReactNode;
10
+ /** Отступ между картами в px (отрицательный для наложения) @default -10 */
11
+ overlap?: number;
12
+ /** Шаг наклона в градусах @default 3 */
13
+ rotationStep?: number;
14
+ /** Индекс центральной карты (по умолчанию — середина массива) */
15
+ centerIndex?: number;
16
+ /** Дополнительные классы для контейнера */
17
+ className?: string;
18
+ /** Уникальный ключ для элемента */
19
+ keyExtractor?: (item: T, index: number) => string | number;
20
+ }
21
+
22
+ /**
23
+ * Универсальный компонент "колода карт" с эффектом веера.
24
+ *
25
+ * Автоматически рассчитывает наклон каждой карты относительно центра.
26
+ * Карты накладываются друг на друга с заданным overlap.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * <CardDeck
31
+ * items={projects}
32
+ * renderCard={(project) => <ProjectCard project={project} />}
33
+ * overlap={-12}
34
+ * rotationStep={4}
35
+ * />
36
+ * ```
37
+ */
38
+ export function CardDeck<T>({
39
+ items,
40
+ renderCard,
41
+ overlap = -10,
42
+ rotationStep = 3,
43
+ centerIndex,
44
+ className,
45
+ keyExtractor,
46
+ }: CardDeckProps<T>) {
47
+ // Центр по умолчанию — середина массива
48
+ const center = centerIndex ?? Math.floor((items.length - 1) / 2);
49
+
50
+ return (
51
+ <div className={`flex justify-center ${className ?? ''}`}>
52
+ {items.map((item, index) => {
53
+ const rotation = (index - center) * rotationStep;
54
+ const key = keyExtractor ? keyExtractor(item, index) : index;
55
+
56
+ return (
57
+ <div
58
+ key={key}
59
+ className='shrink-0 cursor-pointer group'
60
+ style={{ marginLeft: index === 0 ? 0 : overlap }}
61
+ >
62
+ <div
63
+ className='relative transition-[filter,opacity] duration-200 ease-in-out group-hover:z-50 group-hover:brightness-105'
64
+ style={{ transform: `rotate(${rotation}deg)` }}
65
+ >
66
+ {renderCard(item, index)}
67
+ </div>
68
+ </div>
69
+ );
70
+ })}
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,81 @@
1
+ import { forwardRef, type HTMLAttributes } from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ // ==========================================================================
5
+ // Card Root
6
+ // ==========================================================================
7
+
8
+ const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
9
+ ({ className, ...props }, ref) => (
10
+ <div
11
+ ref={ref}
12
+ className={cn('rounded-xl border bg-card text-card-foreground', className)}
13
+ {...props}
14
+ />
15
+ ),
16
+ );
17
+ Card.displayName = 'Card';
18
+
19
+ // ==========================================================================
20
+ // Card Header
21
+ // ==========================================================================
22
+
23
+ const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
24
+ ({ className, ...props }, ref) => (
25
+ <div ref={ref} className={cn('flex flex-col space-y-1.5 p-4', className)} {...props} />
26
+ ),
27
+ );
28
+ CardHeader.displayName = 'CardHeader';
29
+
30
+ // ==========================================================================
31
+ // Card Title
32
+ // ==========================================================================
33
+
34
+ interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {
35
+ level?: 2 | 3 | 4;
36
+ }
37
+
38
+ const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(
39
+ ({ className, level = 3, ...props }, ref) => {
40
+ const titleClassName = cn('font-medium font-display tracking-tight', className);
41
+ if (level === 2) return <h2 ref={ref} className={titleClassName} {...props} />;
42
+ if (level === 4) return <h4 ref={ref} className={titleClassName} {...props} />;
43
+ return <h3 ref={ref} className={titleClassName} {...props} />;
44
+ },
45
+ );
46
+ CardTitle.displayName = 'CardTitle';
47
+
48
+ // ==========================================================================
49
+ // Card Description
50
+ // ==========================================================================
51
+
52
+ const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
53
+ ({ className, ...props }, ref) => (
54
+ <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
55
+ ),
56
+ );
57
+ CardDescription.displayName = 'CardDescription';
58
+
59
+ // ==========================================================================
60
+ // Card Content
61
+ // ==========================================================================
62
+
63
+ const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
64
+ ({ className, ...props }, ref) => (
65
+ <div ref={ref} className={cn('p-4 pt-0', className)} {...props} />
66
+ ),
67
+ );
68
+ CardContent.displayName = 'CardContent';
69
+
70
+ // ==========================================================================
71
+ // Card Footer
72
+ // ==========================================================================
73
+
74
+ const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
75
+ ({ className, ...props }, ref) => (
76
+ <div ref={ref} className={cn('flex items-center p-4 pt-0', className)} {...props} />
77
+ ),
78
+ );
79
+ CardFooter.displayName = 'CardFooter';
80
+
81
+ export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
@@ -0,0 +1,167 @@
1
+ 'use client';
2
+
3
+ import type { Locale } from 'date-fns';
4
+ import { format } from 'date-fns';
5
+ import type { ReactNode } from 'react';
6
+ import {
7
+ Area,
8
+ AreaChart as RechartsAreaChart,
9
+ ResponsiveContainer,
10
+ Tooltip,
11
+ XAxis,
12
+ } from 'recharts';
13
+
14
+ // =============================================================================
15
+ // Types
16
+ // =============================================================================
17
+
18
+ export interface AreaConfig {
19
+ key: string;
20
+ label: string;
21
+ color: string;
22
+ gradientOpacity?: { start: number; end: number };
23
+ }
24
+
25
+ export interface AreaChartDataPoint {
26
+ day: string;
27
+ [key: string]: string | number;
28
+ }
29
+
30
+ export interface AreaChartProps {
31
+ data: AreaChartDataPoint[];
32
+ areas: AreaConfig[];
33
+ height?: number;
34
+ dateLocale: Locale;
35
+ dateFormat?: string;
36
+ tooltipDateFormat?: string;
37
+ renderTooltip?: (props: TooltipRenderProps) => ReactNode;
38
+ }
39
+
40
+ export interface TooltipRenderProps {
41
+ date: string;
42
+ values: { key: string; label: string; value: number; color: string }[];
43
+ }
44
+
45
+ // =============================================================================
46
+ // Constants
47
+ // =============================================================================
48
+
49
+ const TOOLTIP_CLASS =
50
+ 'bg-card border border-border rounded-lg p-3 shadow-lg animate-in fade-in duration-150';
51
+
52
+ // =============================================================================
53
+ // Component
54
+ // =============================================================================
55
+
56
+ export function AreaChart({
57
+ data,
58
+ areas,
59
+ height = 280,
60
+ dateLocale,
61
+ dateFormat = 'd MMM',
62
+ tooltipDateFormat = 'd MMMM yyyy',
63
+ renderTooltip,
64
+ }: AreaChartProps) {
65
+ return (
66
+ <div className='w-full **:outline-none'>
67
+ <ResponsiveContainer width='100%' height={height}>
68
+ <RechartsAreaChart data={data} margin={{ top: 10, right: 0, left: 0, bottom: 20 }}>
69
+ <defs>
70
+ {areas.map((area) => (
71
+ <linearGradient
72
+ key={area.key}
73
+ id={`gradient-${area.key}`}
74
+ x1='0'
75
+ y1='0'
76
+ x2='0'
77
+ y2='1'
78
+ >
79
+ <stop
80
+ offset='0%'
81
+ stopColor={area.color}
82
+ stopOpacity={area.gradientOpacity?.start ?? 0.4}
83
+ />
84
+ <stop
85
+ offset='100%'
86
+ stopColor={area.color}
87
+ stopOpacity={area.gradientOpacity?.end ?? 0.05}
88
+ />
89
+ </linearGradient>
90
+ ))}
91
+ </defs>
92
+
93
+ <XAxis
94
+ dataKey='day'
95
+ axisLine={false}
96
+ tickLine={false}
97
+ tick={{ fontSize: 12, fill: 'var(--muted-foreground)' }}
98
+ dy={5}
99
+ tickFormatter={(value) =>
100
+ format(new Date(value), dateFormat, { locale: dateLocale })
101
+ }
102
+ />
103
+
104
+ <Tooltip
105
+ cursor={false}
106
+ isAnimationActive={false}
107
+ offset={20}
108
+ wrapperStyle={{ zIndex: 10, outline: 'none' }}
109
+ content={({ active, payload, label }) => {
110
+ if (!active || !payload?.length) return null;
111
+
112
+ const labelValue =
113
+ typeof label === 'string' || typeof label === 'number' ? label : '';
114
+ const formattedDate = format(new Date(labelValue), tooltipDateFormat, {
115
+ locale: dateLocale,
116
+ });
117
+
118
+ const values = areas.map((area) => ({
119
+ key: area.key,
120
+ label: area.label,
121
+ value: (() => {
122
+ const value = payload.find((point) => point.dataKey === area.key)?.value;
123
+ return typeof value === 'number' ? value : 0;
124
+ })(),
125
+ color: area.color,
126
+ }));
127
+
128
+ if (renderTooltip) {
129
+ return renderTooltip({ date: formattedDate, values });
130
+ }
131
+
132
+ return (
133
+ <div className={TOOLTIP_CLASS}>
134
+ <p className='font-medium text-sm mb-2'>{formattedDate}</p>
135
+ <div className='space-y-1 text-xs'>
136
+ {values.map(({ key, label, value, color }) => (
137
+ <div key={key} className='flex items-center gap-2'>
138
+ <div
139
+ className='w-2.5 h-2.5 rounded-full'
140
+ style={{ backgroundColor: color }}
141
+ />
142
+ <span className='text-muted-foreground'>{label}:</span>
143
+ <span className='font-medium'>{value.toLocaleString()}</span>
144
+ </div>
145
+ ))}
146
+ </div>
147
+ </div>
148
+ );
149
+ }}
150
+ />
151
+
152
+ {areas.map((area) => (
153
+ <Area
154
+ key={area.key}
155
+ type='monotone'
156
+ dataKey={area.key}
157
+ stroke={area.color}
158
+ strokeWidth={2}
159
+ fill={`url(#gradient-${area.key})`}
160
+ name={area.label}
161
+ />
162
+ ))}
163
+ </RechartsAreaChart>
164
+ </ResponsiveContainer>
165
+ </div>
166
+ );
167
+ }
@@ -0,0 +1,7 @@
1
+ export type {
2
+ AreaChartDataPoint,
3
+ AreaChartProps,
4
+ AreaConfig,
5
+ TooltipRenderProps,
6
+ } from './area-chart';
7
+ export { AreaChart } from './area-chart';
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+
3
+ import type { CheckedState } from '@radix-ui/react-checkbox';
4
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
5
+ import { IconCheck, IconMinus } from '@tabler/icons-react';
6
+ import { AnimatePresence, motion } from 'framer-motion';
7
+ import { type ComponentPropsWithoutRef, type ComponentRef, forwardRef, useId } from 'react';
8
+ import { cn } from '@/lib/utils';
9
+
10
+ // Базовые пропсы без checked/onCheckedChange
11
+ type BaseCheckboxProps = Omit<
12
+ ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
13
+ 'checked' | 'onCheckedChange'
14
+ >;
15
+
16
+ /**
17
+ * Внутренний компонент - вся UI логика в одном месте
18
+ */
19
+ interface CheckboxBaseProps extends ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {
20
+ label?: string;
21
+ }
22
+
23
+ const CheckboxBase = forwardRef<ComponentRef<typeof CheckboxPrimitive.Root>, CheckboxBaseProps>(
24
+ ({ className, checked, label, id, ...props }, ref) => {
25
+ const generatedId = useId();
26
+ const checkboxId = id || generatedId;
27
+ const isChecked = checked === true;
28
+ const isIndeterminate = checked === 'indeterminate';
29
+
30
+ return (
31
+ <div className='flex items-center gap-2'>
32
+ <CheckboxPrimitive.Root
33
+ ref={ref}
34
+ id={checkboxId}
35
+ checked={checked}
36
+ className={cn(
37
+ 'relative flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-md border transition-colors',
38
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
39
+ 'disabled:cursor-not-allowed disabled:opacity-50 disabled:pointer-events-none',
40
+ 'data-[state=checked]:border-primary data-[state=checked]:bg-primary',
41
+ 'data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary',
42
+ 'data-[state=unchecked]:border-border data-[state=unchecked]:bg-background data-[state=unchecked]:hover:border-primary/50',
43
+ className,
44
+ )}
45
+ {...props}
46
+ >
47
+ <AnimatePresence initial={false}>
48
+ {(isChecked || isIndeterminate) && (
49
+ <CheckboxPrimitive.Indicator forceMount>
50
+ <motion.span
51
+ initial={{ scale: 0, opacity: 0 }}
52
+ animate={{ scale: 1, opacity: 1 }}
53
+ exit={{ scale: 0, opacity: 0 }}
54
+ transition={{ type: 'spring', stiffness: 500, damping: 30 }}
55
+ >
56
+ {isIndeterminate ? (
57
+ <IconMinus
58
+ size={14}
59
+ stroke={3}
60
+ className='text-primary-foreground transition-colors'
61
+ />
62
+ ) : (
63
+ <IconCheck
64
+ size={14}
65
+ stroke={3}
66
+ className='text-primary-foreground transition-colors'
67
+ />
68
+ )}
69
+ </motion.span>
70
+ </CheckboxPrimitive.Indicator>
71
+ )}
72
+ </AnimatePresence>
73
+ </CheckboxPrimitive.Root>
74
+ {label && (
75
+ <label
76
+ htmlFor={checkboxId}
77
+ className={cn(
78
+ 'text-sm cursor-pointer select-none',
79
+ props.disabled && 'cursor-not-allowed opacity-50',
80
+ )}
81
+ >
82
+ {label}
83
+ </label>
84
+ )}
85
+ </div>
86
+ );
87
+ },
88
+ );
89
+
90
+ CheckboxBase.displayName = 'CheckboxBase';
91
+
92
+ /**
93
+ * Обычный Checkbox - boolean API (90% случаев)
94
+ */
95
+ interface CheckboxProps extends BaseCheckboxProps {
96
+ label?: string;
97
+ checked?: boolean;
98
+ onCheckedChange?: (checked: boolean) => void;
99
+ }
100
+
101
+ export const Checkbox = forwardRef<ComponentRef<typeof CheckboxPrimitive.Root>, CheckboxProps>(
102
+ ({ onCheckedChange, ...props }, ref) => {
103
+ // Адаптер: фильтруем indeterminate, возвращаем только boolean
104
+ const handleChange = (state: CheckedState) => {
105
+ if (state !== 'indeterminate' && onCheckedChange) {
106
+ onCheckedChange(state);
107
+ }
108
+ };
109
+
110
+ return <CheckboxBase ref={ref} onCheckedChange={handleChange} {...props} />;
111
+ },
112
+ );
113
+
114
+ Checkbox.displayName = 'Checkbox';
115
+
116
+ /**
117
+ * TriStateCheckbox - для редких случаев (таблицы, деревья)
118
+ */
119
+ interface TriStateCheckboxProps extends BaseCheckboxProps {
120
+ label?: string;
121
+ checked?: CheckedState;
122
+ onCheckedChange?: (checked: CheckedState) => void;
123
+ }
124
+
125
+ export const TriStateCheckbox = forwardRef<
126
+ ComponentRef<typeof CheckboxPrimitive.Root>,
127
+ TriStateCheckboxProps
128
+ >((props, ref) => {
129
+ // Прямой проброс - без фильтрации
130
+ return <CheckboxBase ref={ref} {...props} />;
131
+ });
132
+
133
+ TriStateCheckbox.displayName = 'TriStateCheckbox';
@@ -0,0 +1,129 @@
1
+ 'use client';
2
+
3
+ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
4
+ import { AnimatePresence, motion, type Transition } from 'framer-motion';
5
+ import type { ReactNode } from 'react';
6
+ import { cn } from '@/lib/utils';
7
+ import { Button } from './button';
8
+
9
+ export interface ConfirmationModalProps {
10
+ /** Controlled open state */
11
+ isOpen: boolean;
12
+ /** Called when modal should close */
13
+ onClose: () => void;
14
+ /** Called when user confirms the action */
15
+ onConfirm: () => void;
16
+ /** Modal title */
17
+ title: string;
18
+ /** Description text */
19
+ description?: string;
20
+ /** Custom content instead of description */
21
+ children?: ReactNode;
22
+ /** Text for cancel button */
23
+ cancelText?: string;
24
+ /** Text for confirm button */
25
+ confirmText?: string;
26
+ /** Visual variant for confirm button */
27
+ variant?: 'default' | 'destructive';
28
+ /** Loading state for confirm button */
29
+ isLoading?: boolean;
30
+ }
31
+
32
+ const BACKDROP_TRANSITION: Transition = { duration: 0.2 };
33
+ const MODAL_OPEN_TRANSITION: Transition = { type: 'spring', stiffness: 400, damping: 30 };
34
+ const MODAL_CLOSE_TRANSITION: Transition = { type: 'tween', duration: 0.15 };
35
+
36
+ const ANIMATION = {
37
+ backdrop: BACKDROP_TRANSITION,
38
+ modal: {
39
+ open: MODAL_OPEN_TRANSITION,
40
+ close: MODAL_CLOSE_TRANSITION,
41
+ },
42
+ };
43
+
44
+ export function ConfirmationModal({
45
+ isOpen,
46
+ onClose,
47
+ onConfirm,
48
+ title,
49
+ description,
50
+ children,
51
+ cancelText = 'Cancel',
52
+ confirmText = 'Confirm',
53
+ variant = 'default',
54
+ isLoading = false,
55
+ }: ConfirmationModalProps) {
56
+ return (
57
+ <AlertDialogPrimitive.Root open={isOpen} onOpenChange={onClose}>
58
+ <AnimatePresence>
59
+ {isOpen && (
60
+ <AlertDialogPrimitive.Portal forceMount>
61
+ {/* Backdrop */}
62
+ <AlertDialogPrimitive.Overlay asChild>
63
+ <motion.div
64
+ className='fixed inset-0 z-60 bg-overlay backdrop-blur-sm'
65
+ initial={{ opacity: 0 }}
66
+ animate={{ opacity: 1 }}
67
+ exit={{ opacity: 0 }}
68
+ transition={ANIMATION.backdrop}
69
+ />
70
+ </AlertDialogPrimitive.Overlay>
71
+
72
+ {/* Content */}
73
+ <AlertDialogPrimitive.Content asChild>
74
+ <div className='fixed inset-0 z-70 flex items-center justify-center p-4'>
75
+ <motion.div
76
+ className={cn(
77
+ 'w-full max-w-md rounded-2xl border border-border bg-card p-6 shadow-xl outline-none',
78
+ )}
79
+ initial={{ opacity: 0, scale: 0.95 }}
80
+ animate={{
81
+ opacity: 1,
82
+ scale: 1,
83
+ transition: ANIMATION.modal.open,
84
+ }}
85
+ exit={{
86
+ opacity: 0,
87
+ scale: 0.95,
88
+ transition: ANIMATION.modal.close,
89
+ }}
90
+ >
91
+ {/* Title */}
92
+ <AlertDialogPrimitive.Title className='text-xl font-semibold'>
93
+ {title}
94
+ </AlertDialogPrimitive.Title>
95
+
96
+ {/* Description or custom content */}
97
+ {description && (
98
+ <AlertDialogPrimitive.Description className='mt-2 text-muted-foreground'>
99
+ {description}
100
+ </AlertDialogPrimitive.Description>
101
+ )}
102
+ {children && <div className='mt-2'>{children}</div>}
103
+
104
+ {/* Actions */}
105
+ <div className='mt-6 flex justify-end gap-3'>
106
+ <AlertDialogPrimitive.Cancel asChild>
107
+ <Button variant='outline' disabled={isLoading}>
108
+ {cancelText}
109
+ </Button>
110
+ </AlertDialogPrimitive.Cancel>
111
+ <AlertDialogPrimitive.Action asChild>
112
+ <Button
113
+ variant={variant === 'destructive' ? 'destructive' : 'default'}
114
+ onClick={onConfirm}
115
+ disabled={isLoading}
116
+ >
117
+ {isLoading ? 'Loading...' : confirmText}
118
+ </Button>
119
+ </AlertDialogPrimitive.Action>
120
+ </div>
121
+ </motion.div>
122
+ </div>
123
+ </AlertDialogPrimitive.Content>
124
+ </AlertDialogPrimitive.Portal>
125
+ )}
126
+ </AnimatePresence>
127
+ </AlertDialogPrimitive.Root>
128
+ );
129
+ }