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