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,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,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
|
+
}
|