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,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
|
4
|
+
import { type ComponentPropsWithoutRef, type ComponentRef, forwardRef } from 'react';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
export const Switch = forwardRef<
|
|
8
|
+
ComponentRef<typeof SwitchPrimitive.Root>,
|
|
9
|
+
ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
|
|
10
|
+
>(({ className, ...props }, ref) => (
|
|
11
|
+
<SwitchPrimitive.Root
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn(
|
|
14
|
+
'peer relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full transition-colors',
|
|
15
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
|
16
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
17
|
+
'data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted',
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
<SwitchPrimitive.Thumb
|
|
23
|
+
className={cn(
|
|
24
|
+
'pointer-events-none block h-5 w-5 rounded-full bg-white shadow-sm transition-transform',
|
|
25
|
+
'data-[state=checked]:translate-x-[22px] data-[state=unchecked]:translate-x-[2px]',
|
|
26
|
+
)}
|
|
27
|
+
/>
|
|
28
|
+
</SwitchPrimitive.Root>
|
|
29
|
+
));
|
|
30
|
+
|
|
31
|
+
Switch.displayName = 'Switch';
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
type HTMLAttributes,
|
|
4
|
+
type TdHTMLAttributes,
|
|
5
|
+
type ThHTMLAttributes,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
const Table = forwardRef<HTMLTableElement, HTMLAttributes<HTMLTableElement>>(
|
|
10
|
+
({ className, ...props }, ref) => (
|
|
11
|
+
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
|
12
|
+
),
|
|
13
|
+
);
|
|
14
|
+
Table.displayName = 'Table';
|
|
15
|
+
|
|
16
|
+
const TableHeader = forwardRef<
|
|
17
|
+
HTMLTableSectionElement,
|
|
18
|
+
HTMLAttributes<HTMLTableSectionElement>
|
|
19
|
+
>(({ className, ...props }, ref) => (
|
|
20
|
+
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
|
21
|
+
));
|
|
22
|
+
TableHeader.displayName = 'TableHeader';
|
|
23
|
+
|
|
24
|
+
const TableBody = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(
|
|
25
|
+
({ className, ...props }, ref) => (
|
|
26
|
+
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
TableBody.displayName = 'TableBody';
|
|
30
|
+
|
|
31
|
+
const TableFooter = forwardRef<
|
|
32
|
+
HTMLTableSectionElement,
|
|
33
|
+
HTMLAttributes<HTMLTableSectionElement>
|
|
34
|
+
>(({ className, ...props }, ref) => (
|
|
35
|
+
<tfoot
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
));
|
|
41
|
+
TableFooter.displayName = 'TableFooter';
|
|
42
|
+
|
|
43
|
+
const TableRow = forwardRef<HTMLTableRowElement, HTMLAttributes<HTMLTableRowElement>>(
|
|
44
|
+
({ className, ...props }, ref) => (
|
|
45
|
+
<tr
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn(
|
|
48
|
+
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
|
49
|
+
className,
|
|
50
|
+
)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
TableRow.displayName = 'TableRow';
|
|
56
|
+
|
|
57
|
+
const TableHead = forwardRef<HTMLTableCellElement, ThHTMLAttributes<HTMLTableCellElement>>(
|
|
58
|
+
({ className, ...props }, ref) => (
|
|
59
|
+
<th
|
|
60
|
+
ref={ref}
|
|
61
|
+
className={cn(
|
|
62
|
+
'h-10 px-3 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 *:[[role=checkbox]]:translate-y-[2px]',
|
|
63
|
+
className,
|
|
64
|
+
)}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
TableHead.displayName = 'TableHead';
|
|
70
|
+
|
|
71
|
+
const TableCell = forwardRef<HTMLTableCellElement, TdHTMLAttributes<HTMLTableCellElement>>(
|
|
72
|
+
({ className, ...props }, ref) => (
|
|
73
|
+
<td
|
|
74
|
+
ref={ref}
|
|
75
|
+
className={cn(
|
|
76
|
+
'p-3 align-middle [&:has([role=checkbox])]:pr-0 *:[[role=checkbox]]:translate-y-[2px]',
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
TableCell.displayName = 'TableCell';
|
|
84
|
+
|
|
85
|
+
const TableCaption = forwardRef<
|
|
86
|
+
HTMLTableCaptionElement,
|
|
87
|
+
HTMLAttributes<HTMLTableCaptionElement>
|
|
88
|
+
>(({ className, ...props }, ref) => (
|
|
89
|
+
<caption
|
|
90
|
+
ref={ref}
|
|
91
|
+
className={cn('mt-4 text-sm text-muted-foreground', className)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
));
|
|
95
|
+
TableCaption.displayName = 'TableCaption';
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
Table,
|
|
99
|
+
TableBody,
|
|
100
|
+
TableCaption,
|
|
101
|
+
TableCell,
|
|
102
|
+
TableFooter,
|
|
103
|
+
TableHead,
|
|
104
|
+
TableHeader,
|
|
105
|
+
TableRow,
|
|
106
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tabs - Компонент табов на основе Radix UI
|
|
5
|
+
*
|
|
6
|
+
* Композиция примитивов для максимальной гибкости.
|
|
7
|
+
* Плавная анимация активного таба через Framer Motion.
|
|
8
|
+
*
|
|
9
|
+
* Использование:
|
|
10
|
+
* <Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
11
|
+
* <TabsList>
|
|
12
|
+
* <TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
|
13
|
+
* <TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
|
14
|
+
* </TabsList>
|
|
15
|
+
* </Tabs>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
19
|
+
import { motion } from 'framer-motion';
|
|
20
|
+
import type { ComponentPropsWithoutRef, ComponentRef, CSSProperties } from 'react';
|
|
21
|
+
import {
|
|
22
|
+
createContext,
|
|
23
|
+
forwardRef,
|
|
24
|
+
useCallback,
|
|
25
|
+
useContext,
|
|
26
|
+
useId,
|
|
27
|
+
useMemo,
|
|
28
|
+
useState,
|
|
29
|
+
} from 'react';
|
|
30
|
+
import { cn } from '@/lib/utils';
|
|
31
|
+
|
|
32
|
+
// ==========================================================================
|
|
33
|
+
// Context для layoutId анимации
|
|
34
|
+
// ==========================================================================
|
|
35
|
+
|
|
36
|
+
interface TabsContext {
|
|
37
|
+
layoutId: string;
|
|
38
|
+
activeValue?: string;
|
|
39
|
+
disableAnimation?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TabsAnimationContext = createContext<TabsContext>({
|
|
43
|
+
layoutId: 'tabs-indicator',
|
|
44
|
+
activeValue: undefined,
|
|
45
|
+
disableAnimation: false,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ==========================================================================
|
|
49
|
+
// Tabs Root
|
|
50
|
+
// ==========================================================================
|
|
51
|
+
|
|
52
|
+
interface TabsProps extends ComponentPropsWithoutRef<typeof TabsPrimitive.Root> {
|
|
53
|
+
/** Уникальный ID для анимации (нужен если несколько Tabs на странице) */
|
|
54
|
+
layoutId?: string;
|
|
55
|
+
/** Отключить анимацию sliding indicator */
|
|
56
|
+
disableAnimation?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const Tabs = forwardRef<ComponentRef<typeof TabsPrimitive.Root>, TabsProps>(function Tabs(
|
|
60
|
+
{
|
|
61
|
+
className,
|
|
62
|
+
layoutId: layoutIdProp,
|
|
63
|
+
disableAnimation = false,
|
|
64
|
+
value,
|
|
65
|
+
defaultValue,
|
|
66
|
+
onValueChange,
|
|
67
|
+
...props
|
|
68
|
+
},
|
|
69
|
+
ref,
|
|
70
|
+
) {
|
|
71
|
+
// Если layoutId не задан — генерируем стабильный уникальный, чтобы несколько Tabs
|
|
72
|
+
// на странице не "склеивались" по layout-анимации.
|
|
73
|
+
const reactId = useId();
|
|
74
|
+
const layoutId = layoutIdProp ?? `tabs-indicator-${reactId}`;
|
|
75
|
+
|
|
76
|
+
const isControlled = value !== undefined;
|
|
77
|
+
const [uncontrolledValue, setUncontrolledValue] = useState<string | undefined>(defaultValue);
|
|
78
|
+
|
|
79
|
+
const handleValueChange = useCallback(
|
|
80
|
+
(nextValue: string) => {
|
|
81
|
+
if (!isControlled) setUncontrolledValue(nextValue);
|
|
82
|
+
onValueChange?.(nextValue);
|
|
83
|
+
},
|
|
84
|
+
[isControlled, onValueChange],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const activeValue = isControlled ? value : uncontrolledValue;
|
|
88
|
+
|
|
89
|
+
const contextValue = useMemo(
|
|
90
|
+
() => ({ layoutId, activeValue, disableAnimation }),
|
|
91
|
+
[layoutId, activeValue, disableAnimation],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Важно: Radix Tabs поддерживает controlled и uncontrolled режимы. Чтобы не
|
|
95
|
+
// передать одновременно value и defaultValue — задаём пропы условно.
|
|
96
|
+
const rootProps: ComponentPropsWithoutRef<typeof TabsPrimitive.Root> = {
|
|
97
|
+
...props,
|
|
98
|
+
className: cn('flex flex-col gap-2', className),
|
|
99
|
+
onValueChange: handleValueChange,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (activeValue !== undefined) rootProps.value = activeValue;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<TabsAnimationContext.Provider value={contextValue}>
|
|
106
|
+
<TabsPrimitive.Root ref={ref} {...rootProps} />
|
|
107
|
+
</TabsAnimationContext.Provider>
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ==========================================================================
|
|
112
|
+
// TabsList
|
|
113
|
+
// ==========================================================================
|
|
114
|
+
|
|
115
|
+
const TabsList = forwardRef<
|
|
116
|
+
ComponentRef<typeof TabsPrimitive.List>,
|
|
117
|
+
ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
118
|
+
>(function TabsList({ className, ...props }, ref) {
|
|
119
|
+
return (
|
|
120
|
+
<TabsPrimitive.List
|
|
121
|
+
ref={ref}
|
|
122
|
+
className={cn('inline-flex items-center gap-1 rounded-xl bg-muted p-1 w-fit', className)}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ==========================================================================
|
|
129
|
+
// TabsTrigger
|
|
130
|
+
// ==========================================================================
|
|
131
|
+
|
|
132
|
+
const TabsTrigger = forwardRef<
|
|
133
|
+
ComponentRef<typeof TabsPrimitive.Trigger>,
|
|
134
|
+
ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
135
|
+
>(function TabsTrigger({ className, children, value, ...props }, ref) {
|
|
136
|
+
const { layoutId, activeValue, disableAnimation } = useContext(TabsAnimationContext);
|
|
137
|
+
const isActive = value === activeValue;
|
|
138
|
+
const indicatorClassName = 'absolute inset-0 rounded-lg bg-background shadow-sm';
|
|
139
|
+
const indicatorStyle: CSSProperties = { pointerEvents: 'none' };
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<TabsPrimitive.Trigger
|
|
143
|
+
ref={ref}
|
|
144
|
+
value={value}
|
|
145
|
+
className={cn(
|
|
146
|
+
'relative flex-1 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer whitespace-nowrap',
|
|
147
|
+
'text-muted-foreground hover:text-foreground',
|
|
148
|
+
'data-[state=active]:text-foreground',
|
|
149
|
+
className,
|
|
150
|
+
)}
|
|
151
|
+
{...props}
|
|
152
|
+
>
|
|
153
|
+
{isActive &&
|
|
154
|
+
(disableAnimation ? (
|
|
155
|
+
<div aria-hidden='true' className={indicatorClassName} style={indicatorStyle} />
|
|
156
|
+
) : (
|
|
157
|
+
<motion.div
|
|
158
|
+
aria-hidden='true'
|
|
159
|
+
layoutId={layoutId}
|
|
160
|
+
className={indicatorClassName}
|
|
161
|
+
transition={{ type: 'spring', stiffness: 500, damping: 35 }}
|
|
162
|
+
style={indicatorStyle}
|
|
163
|
+
/>
|
|
164
|
+
))}
|
|
165
|
+
<span className='relative z-10'>{children}</span>
|
|
166
|
+
</TabsPrimitive.Trigger>
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ==========================================================================
|
|
171
|
+
// TabsContent
|
|
172
|
+
// ==========================================================================
|
|
173
|
+
|
|
174
|
+
const TabsContent = forwardRef<
|
|
175
|
+
ComponentRef<typeof TabsPrimitive.Content>,
|
|
176
|
+
ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
177
|
+
>(function TabsContent({ className, ...props }, ref) {
|
|
178
|
+
return (
|
|
179
|
+
<TabsPrimitive.Content
|
|
180
|
+
ref={ref}
|
|
181
|
+
className={cn('flex-1 outline-none', className)}
|
|
182
|
+
{...props}
|
|
183
|
+
/>
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
Tabs.displayName = 'Tabs';
|
|
188
|
+
TabsList.displayName = 'TabsList';
|
|
189
|
+
TabsTrigger.displayName = 'TabsTrigger';
|
|
190
|
+
TabsContent.displayName = 'TabsContent';
|
|
191
|
+
|
|
192
|
+
// ==========================================================================
|
|
193
|
+
// Exports
|
|
194
|
+
// ==========================================================================
|
|
195
|
+
|
|
196
|
+
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { forwardRef, type TextareaHTMLAttributes } from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
export const Textarea = forwardRef<
|
|
5
|
+
HTMLTextAreaElement,
|
|
6
|
+
TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
7
|
+
>(({ className, ...props }, ref) => (
|
|
8
|
+
<textarea
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn(
|
|
11
|
+
'min-h-28 w-full resize-y rounded-lg border border-border bg-background px-4 py-3',
|
|
12
|
+
'placeholder:text-muted-foreground focus:border-primary focus:outline-none',
|
|
13
|
+
'transition-colors',
|
|
14
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
));
|
|
20
|
+
|
|
21
|
+
Textarea.displayName = 'Textarea';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { IconAlertTriangle, IconCheck, IconInfoCircle, IconX } from '@tabler/icons-react';
|
|
4
|
+
import { useThemeValue } from '@wrksz/themes/client/use-theme-value';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
import { type ExternalToast, Toaster as SonnerToaster, toast as sonnerToast } from 'sonner';
|
|
7
|
+
|
|
8
|
+
type ToastPosition = 'bottom-center' | 'bottom-right' | 'top-left' | 'top-center' | 'top-right';
|
|
9
|
+
|
|
10
|
+
interface ToastOptions {
|
|
11
|
+
position?: ToastPosition;
|
|
12
|
+
duration?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const getResponsivePosition = (): ToastPosition => {
|
|
16
|
+
if (typeof window === 'undefined') return 'bottom-center';
|
|
17
|
+
return window.innerWidth >= 1024 ? 'bottom-right' : 'bottom-center';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const getBaseOptions = (options?: ToastOptions): ExternalToast => ({
|
|
21
|
+
position: options?.position ?? getResponsivePosition(),
|
|
22
|
+
duration: options?.duration,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const toast = {
|
|
26
|
+
success: (message: ReactNode, options?: ToastOptions) =>
|
|
27
|
+
sonnerToast.success(message, getBaseOptions(options)),
|
|
28
|
+
error: (message: ReactNode, options?: ToastOptions) =>
|
|
29
|
+
sonnerToast.error(message, getBaseOptions(options)),
|
|
30
|
+
info: (message: ReactNode, options?: ToastOptions) =>
|
|
31
|
+
sonnerToast.info(message, getBaseOptions(options)),
|
|
32
|
+
warning: (message: ReactNode, options?: ToastOptions) =>
|
|
33
|
+
sonnerToast.warning(message, getBaseOptions(options)),
|
|
34
|
+
loading: (message: ReactNode, options?: ToastOptions) =>
|
|
35
|
+
sonnerToast.loading(message, getBaseOptions(options)),
|
|
36
|
+
custom: sonnerToast.custom,
|
|
37
|
+
dismiss: sonnerToast.dismiss,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function Toaster() {
|
|
41
|
+
const theme = useThemeValue<'light' | 'dark' | 'system'>({
|
|
42
|
+
light: 'light',
|
|
43
|
+
dark: 'dark',
|
|
44
|
+
default: 'system',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<SonnerToaster
|
|
49
|
+
theme={theme}
|
|
50
|
+
position='bottom-right'
|
|
51
|
+
icons={{
|
|
52
|
+
success: <IconCheck size={18} stroke={2.5} />,
|
|
53
|
+
error: <IconX size={18} stroke={2.5} />,
|
|
54
|
+
info: <IconInfoCircle size={18} stroke={2} />,
|
|
55
|
+
warning: <IconAlertTriangle size={18} stroke={2} />,
|
|
56
|
+
}}
|
|
57
|
+
toastOptions={{
|
|
58
|
+
unstyled: true,
|
|
59
|
+
classNames: {
|
|
60
|
+
toast:
|
|
61
|
+
'flex items-center gap-3 w-full p-4 rounded-xl shadow-lg font-sans font-medium text-sm border',
|
|
62
|
+
title: 'font-medium',
|
|
63
|
+
description: 'text-sm opacity-90',
|
|
64
|
+
success: 'bg-success text-success-foreground border-success-foreground/10',
|
|
65
|
+
error: 'bg-destructive text-destructive-foreground border-destructive-foreground/10',
|
|
66
|
+
info: 'bg-card text-card-foreground border-border [&>svg]:text-info',
|
|
67
|
+
warning: 'bg-warning text-warning-foreground border-warning-foreground/10',
|
|
68
|
+
},
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
4
|
+
import { type ComponentPropsWithoutRef, type ComponentRef, forwardRef } from 'react';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
const Tooltip = TooltipPrimitive.Root;
|
|
8
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
9
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
10
|
+
|
|
11
|
+
const TooltipContent = forwardRef<
|
|
12
|
+
ComponentRef<typeof TooltipPrimitive.Content>,
|
|
13
|
+
ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
14
|
+
>(({ className, sideOffset = 6, ...props }, ref) => (
|
|
15
|
+
<TooltipPrimitive.Portal>
|
|
16
|
+
<TooltipPrimitive.Content
|
|
17
|
+
ref={ref}
|
|
18
|
+
sideOffset={sideOffset}
|
|
19
|
+
collisionPadding={12}
|
|
20
|
+
className={cn(
|
|
21
|
+
'z-50 max-w-xs overflow-hidden rounded-md border border-border bg-card px-3 py-2 text-sm text-card-foreground shadow-md',
|
|
22
|
+
'animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
23
|
+
className,
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
</TooltipPrimitive.Portal>
|
|
28
|
+
));
|
|
29
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
30
|
+
|
|
31
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createEnv } from '@t3-oss/env-nextjs';
|
|
3
|
+
import { config } from 'dotenv';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
config({ path: path.resolve(process.cwd(), '../../.env'), quiet: true });
|
|
7
|
+
|
|
8
|
+
export const env = createEnv({
|
|
9
|
+
server: { INTERNAL_API_URL: z.url(), WEB_PORT: z.coerce.number().int().positive() },
|
|
10
|
+
client: {
|
|
11
|
+
NEXT_PUBLIC_API_URL: z.url(),
|
|
12
|
+
NEXT_PUBLIC_WEB_URL: z.url(),
|
|
13
|
+
},
|
|
14
|
+
runtimeEnv: {
|
|
15
|
+
INTERNAL_API_URL: process.env.INTERNAL_API_URL,
|
|
16
|
+
WEB_PORT: process.env.WEB_PORT,
|
|
17
|
+
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
|
18
|
+
NEXT_PUBLIC_WEB_URL: process.env.NEXT_PUBLIC_WEB_URL,
|
|
19
|
+
},
|
|
20
|
+
emptyStringAsUndefined: true,
|
|
21
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { Icon } from '@tabler/icons-react';
|
|
4
|
+
import { IconDots } from '@tabler/icons-react';
|
|
5
|
+
import type { ColumnDef } from '@tanstack/react-table';
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
Checkbox,
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuItem,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
TriStateCheckbox,
|
|
14
|
+
} from '@/components/ui';
|
|
15
|
+
import { formatDateTime } from '@/lib/utils';
|
|
16
|
+
import type { dataTableFeatures } from '../table-features';
|
|
17
|
+
import { SortableHeader } from './sortable-header';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a select column with checkbox for row selection
|
|
21
|
+
*/
|
|
22
|
+
export function createSelectColumn<T extends object>(): ColumnDef<
|
|
23
|
+
typeof dataTableFeatures,
|
|
24
|
+
T,
|
|
25
|
+
unknown
|
|
26
|
+
> {
|
|
27
|
+
return {
|
|
28
|
+
id: 'select',
|
|
29
|
+
header: ({ table }) => (
|
|
30
|
+
<TriStateCheckbox
|
|
31
|
+
checked={
|
|
32
|
+
table.getIsAllPageRowsSelected()
|
|
33
|
+
? true
|
|
34
|
+
: table.getIsSomePageRowsSelected()
|
|
35
|
+
? 'indeterminate'
|
|
36
|
+
: false
|
|
37
|
+
}
|
|
38
|
+
onCheckedChange={(value) => table.toggleAllPageRowsSelected(value === true)}
|
|
39
|
+
aria-label='Select all'
|
|
40
|
+
/>
|
|
41
|
+
),
|
|
42
|
+
cell: ({ row }) => (
|
|
43
|
+
<Checkbox
|
|
44
|
+
checked={row.getIsSelected()}
|
|
45
|
+
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
46
|
+
aria-label='Select row'
|
|
47
|
+
/>
|
|
48
|
+
),
|
|
49
|
+
enableSorting: false,
|
|
50
|
+
enableHiding: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a date/time column with sortable header
|
|
56
|
+
*/
|
|
57
|
+
export function createDateColumn<T extends object>(
|
|
58
|
+
accessorKey: keyof T & string,
|
|
59
|
+
headerLabel: string,
|
|
60
|
+
): ColumnDef<typeof dataTableFeatures, T, unknown> {
|
|
61
|
+
return {
|
|
62
|
+
accessorKey,
|
|
63
|
+
header: ({ column }) => <SortableHeader column={column}>{headerLabel}</SortableHeader>,
|
|
64
|
+
cell: ({ row }) => {
|
|
65
|
+
const value = row.getValue(accessorKey);
|
|
66
|
+
if (!value) return null;
|
|
67
|
+
|
|
68
|
+
if (!(value instanceof Date) && typeof value !== 'string') return null;
|
|
69
|
+
const { date, time } = formatDateTime(value);
|
|
70
|
+
return (
|
|
71
|
+
<div className='flex flex-col'>
|
|
72
|
+
<span className='text-sm text-muted-foreground'>{date}</span>
|
|
73
|
+
<span className='text-xs text-muted-foreground/70'>{time}</span>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ActionItem<T extends object> {
|
|
81
|
+
icon: Icon;
|
|
82
|
+
label: string;
|
|
83
|
+
onClick?: (row: T) => void;
|
|
84
|
+
variant?: 'default' | 'destructive';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ActionsCell<T extends object>({
|
|
88
|
+
row,
|
|
89
|
+
getActions,
|
|
90
|
+
}: {
|
|
91
|
+
row: T;
|
|
92
|
+
getActions: (row: T) => ActionItem<T>[];
|
|
93
|
+
}) {
|
|
94
|
+
const actions = getActions(row);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className='flex justify-end pr-2'>
|
|
98
|
+
<DropdownMenu>
|
|
99
|
+
<DropdownMenuTrigger asChild>
|
|
100
|
+
<Button variant='ghost' size='icon' className='h-7 w-7'>
|
|
101
|
+
<IconDots size={14} />
|
|
102
|
+
<span className='sr-only'>Открыть меню</span>
|
|
103
|
+
</Button>
|
|
104
|
+
</DropdownMenuTrigger>
|
|
105
|
+
<DropdownMenuContent align='end'>
|
|
106
|
+
{actions.map((action) => (
|
|
107
|
+
<DropdownMenuItem
|
|
108
|
+
key={action.label}
|
|
109
|
+
onClick={() => action.onClick?.(row)}
|
|
110
|
+
className={
|
|
111
|
+
action.variant === 'destructive'
|
|
112
|
+
? 'text-destructive focus:text-destructive'
|
|
113
|
+
: undefined
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
<action.icon size={14} />
|
|
117
|
+
{action.label}
|
|
118
|
+
</DropdownMenuItem>
|
|
119
|
+
))}
|
|
120
|
+
</DropdownMenuContent>
|
|
121
|
+
</DropdownMenu>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates an actions column with dropdown menu
|
|
128
|
+
*/
|
|
129
|
+
export function createActionsColumn<T extends object>(
|
|
130
|
+
getActions: (row: T) => ActionItem<T>[],
|
|
131
|
+
): ColumnDef<typeof dataTableFeatures, T, unknown> {
|
|
132
|
+
return {
|
|
133
|
+
id: 'actions',
|
|
134
|
+
cell: ({ row }) => <ActionsCell row={row.original} getActions={getActions} />,
|
|
135
|
+
};
|
|
136
|
+
}
|