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,53 @@
1
+ import { ImageResponse } from 'next/og';
2
+ import { LocaleSchema } from '@/i18n/locales';
3
+ import { getSeoPage, publicPageIds, SeoPageIdSchema, SITE_NAME } from '@/lib/seo/pages';
4
+
5
+ export const runtime = 'nodejs';
6
+ export const size = { width: 1200, height: 630 };
7
+
8
+ export function generateStaticParams() {
9
+ return LocaleSchema.options.flatMap((locale) =>
10
+ publicPageIds.map((page) => ({ locale, page })),
11
+ );
12
+ }
13
+
14
+ export async function GET(
15
+ _request: Request,
16
+ { params }: { params: Promise<{ locale: string; page: string }> },
17
+ ) {
18
+ const { locale, page } = await params;
19
+ const parsedLocale = LocaleSchema.safeParse(locale);
20
+ const parsedPage = SeoPageIdSchema.safeParse(page);
21
+ if (!parsedLocale.success || !parsedPage.success) {
22
+ return new Response('Unknown SEO page', { status: 404 });
23
+ }
24
+
25
+ const metadata = getSeoPage(parsedPage.data, parsedLocale.data);
26
+ return new ImageResponse(
27
+ <div
28
+ style={{
29
+ alignItems: 'center',
30
+ background: '#111214',
31
+ color: '#f7f7f8',
32
+ display: 'flex',
33
+ height: '100%',
34
+ justifyContent: 'center',
35
+ width: '100%',
36
+ }}
37
+ >
38
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 26, maxWidth: 940 }}>
39
+ <div style={{ color: '#a3a7b2', fontSize: 28 }}>{metadata.eyebrow}</div>
40
+ <div style={{ fontSize: 74, fontWeight: 500, letterSpacing: -3, lineHeight: 1.05 }}>
41
+ {metadata.title}
42
+ </div>
43
+ <div style={{ color: '#c7cad1', fontSize: 31, lineHeight: 1.35 }}>
44
+ {metadata.description}
45
+ </div>
46
+ <div style={{ color: '#8d929e', display: 'flex', fontSize: 24, marginTop: 10 }}>
47
+ {SITE_NAME}
48
+ </div>
49
+ </div>
50
+ </div>,
51
+ size,
52
+ );
53
+ }
@@ -0,0 +1,210 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:where(.dark, .dark *));
5
+
6
+ .technology-icon-invert-dark {
7
+ filter: none;
8
+ }
9
+
10
+ .dark .technology-icon-invert-dark {
11
+ filter: invert(1);
12
+ }
13
+
14
+ @theme inline {
15
+ --color-background: var(--background);
16
+ --color-foreground: var(--foreground);
17
+ --color-card: var(--card);
18
+ --color-card-foreground: var(--card-foreground);
19
+ --color-primary: var(--primary);
20
+ --color-primary-foreground: var(--primary-foreground);
21
+ --color-muted: var(--muted);
22
+ --color-muted-foreground: var(--muted-foreground);
23
+ --color-secondary: var(--secondary);
24
+ --color-secondary-foreground: var(--secondary-foreground);
25
+ --color-border: var(--border);
26
+ --color-input: var(--input);
27
+ --color-ring: var(--ring);
28
+ --color-destructive: var(--destructive);
29
+ --color-destructive-foreground: var(--destructive-foreground);
30
+ --color-success: var(--success);
31
+ --color-success-foreground: var(--success-foreground);
32
+ --color-warning: var(--warning);
33
+ --color-warning-foreground: var(--warning-foreground);
34
+ --color-info: var(--info);
35
+ --color-info-foreground: var(--info-foreground);
36
+ --color-overlay: var(--overlay);
37
+ --font-display: var(--font-montserrat);
38
+ --font-sans: var(--font-montserrat);
39
+ --font-mono:
40
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
41
+ }
42
+
43
+ :root,
44
+ .light {
45
+ --background: oklch(1 0 0);
46
+ --foreground: oklch(0.13 0 0);
47
+ --card: oklch(0.99 0 0);
48
+ --card-foreground: oklch(0.13 0 0);
49
+ --primary: oklch(0.13 0 0);
50
+ --primary-foreground: oklch(1 0 0);
51
+ --muted: oklch(0.97 0 0);
52
+ --muted-foreground: oklch(0.45 0 0);
53
+ --secondary: oklch(0.93 0 0);
54
+ --secondary-foreground: oklch(0.18 0 0);
55
+ --border: oklch(0.91 0 0);
56
+ --input: oklch(0.91 0 0);
57
+ --ring: oklch(0.13 0 0);
58
+ --destructive: oklch(0.55 0.2 25);
59
+ --destructive-foreground: oklch(1 0 0);
60
+ --success: oklch(0.6 0.16 145);
61
+ --success-foreground: oklch(1 0 0);
62
+ --warning: oklch(0.75 0.14 70);
63
+ --warning-foreground: oklch(0.2 0.05 70);
64
+ --info: oklch(0.55 0.15 230);
65
+ --info-foreground: oklch(1 0 0);
66
+ --overlay: oklch(0 0 0 / 0.5);
67
+ /* Chart colors */
68
+ --chart-1: oklch(0.25 0 0);
69
+ --chart-2: oklch(0.65 0 0);
70
+ }
71
+
72
+ .dark {
73
+ --background: oklch(0.16 0.005 260);
74
+ --foreground: oklch(0.9 0 0);
75
+ --card: oklch(0.19 0.005 260);
76
+ --card-foreground: oklch(0.9 0 0);
77
+ --primary: oklch(0.9 0 0);
78
+ --primary-foreground: oklch(0.16 0 0);
79
+ --muted: oklch(0.22 0.005 260);
80
+ --muted-foreground: oklch(0.6 0 0);
81
+ --secondary: oklch(0.25 0.005 260);
82
+ --secondary-foreground: oklch(0.9 0 0);
83
+ --border: oklch(0.26 0.005 260);
84
+ --input: oklch(0.22 0.005 260);
85
+ --ring: oklch(0.9 0 0);
86
+ --destructive: oklch(0.55 0.18 25);
87
+ --destructive-foreground: oklch(1 0 0);
88
+ --success: oklch(0.55 0.14 145);
89
+ --success-foreground: oklch(1 0 0);
90
+ --warning: oklch(0.7 0.13 70);
91
+ --warning-foreground: oklch(0.2 0.05 70);
92
+ --info: oklch(0.55 0.15 230);
93
+ --info-foreground: oklch(1 0 0);
94
+ --overlay: oklch(0 0 0 / 0.68);
95
+ /* Chart colors */
96
+ --chart-1: oklch(0.85 0 0);
97
+ --chart-2: oklch(0.5 0 0);
98
+ }
99
+
100
+ @layer base {
101
+ * {
102
+ @apply border-border;
103
+ }
104
+ body {
105
+ @apply bg-background text-foreground;
106
+ }
107
+
108
+ button,
109
+ [role="button"],
110
+ a[href],
111
+ summary,
112
+ label:has(input) {
113
+ cursor: pointer;
114
+ }
115
+
116
+ button:disabled,
117
+ [role="button"][aria-disabled="true"] {
118
+ cursor: not-allowed;
119
+ }
120
+
121
+ /* Selection — инвертированные цвета под тему */
122
+ ::selection {
123
+ @apply bg-foreground text-background;
124
+ }
125
+ }
126
+
127
+ ::view-transition-old(root),
128
+ ::view-transition-new(root) {
129
+ animation: none;
130
+ mix-blend-mode: normal;
131
+ }
132
+
133
+ ::view-transition-old(root) {
134
+ z-index: 1;
135
+ }
136
+
137
+ ::view-transition-new(root) {
138
+ z-index: 2;
139
+ }
140
+
141
+ html[data-theme-transition="crossfade"]::view-transition-new(root) {
142
+ animation: theme-crossfade var(--theme-transition-duration) var(--theme-transition-easing);
143
+ }
144
+
145
+ html[data-theme-transition="radial"]::view-transition-new(root) {
146
+ animation: theme-radial-reveal var(--theme-transition-duration) var(--theme-transition-easing);
147
+ }
148
+
149
+ @keyframes theme-crossfade {
150
+ from {
151
+ opacity: 0;
152
+ }
153
+ to {
154
+ opacity: 1;
155
+ }
156
+ }
157
+
158
+ @keyframes theme-radial-reveal {
159
+ from {
160
+ clip-path: circle(0 at var(--theme-transition-x) var(--theme-transition-y));
161
+ }
162
+ to {
163
+ clip-path: circle(150vmax at var(--theme-transition-x) var(--theme-transition-y));
164
+ }
165
+ }
166
+
167
+ .shiki span {
168
+ color: var(--shiki-light);
169
+ }
170
+
171
+ .dark .shiki span {
172
+ color: var(--shiki-dark);
173
+ }
174
+
175
+ /* Typing animation for code */
176
+ .typing-code .type-char {
177
+ opacity: 0;
178
+ animation: typeReveal 0.05s forwards;
179
+ animation-delay: var(--delay);
180
+ }
181
+
182
+ @keyframes typeReveal {
183
+ to {
184
+ opacity: 1;
185
+ }
186
+ }
187
+
188
+ @media (prefers-reduced-motion: reduce) {
189
+ *,
190
+ *::before,
191
+ *::after {
192
+ scroll-behavior: auto;
193
+ animation-duration: 0.01ms;
194
+ animation-iteration-count: 1;
195
+ transition-duration: 0.01ms;
196
+ }
197
+
198
+ ::view-transition-old(root),
199
+ ::view-transition-new(root) {
200
+ animation: none;
201
+ }
202
+ }
203
+
204
+ /* Snapshot capture must override component transitions while the theme class changes. */
205
+ .theme-transitioning *,
206
+ .theme-transitioning *::before,
207
+ .theme-transitioning *::after {
208
+ /* biome-ignore lint/complexity/noImportantStyles: native snapshot integrity requires precedence */
209
+ transition: none !important;
210
+ }
@@ -0,0 +1,9 @@
1
+ import type { MetadataRoute } from 'next';
2
+ import { absoluteSiteUrl } from '@/lib/seo/metadata';
3
+
4
+ export default function robots(): MetadataRoute.Robots {
5
+ return {
6
+ rules: { userAgent: '*', allow: '/' },
7
+ sitemap: absoluteSiteUrl('/sitemap.xml'),
8
+ };
9
+ }
@@ -0,0 +1,22 @@
1
+ import type { MetadataRoute } from 'next';
2
+ import { locales } from '@/i18n/locales';
3
+ import { absoluteSiteUrl } from '@/lib/seo/metadata';
4
+ import { localizedPagePath, publicPageIds } from '@/lib/seo/pages';
5
+
6
+ export default function sitemap(): MetadataRoute.Sitemap {
7
+ return publicPageIds.flatMap((pageId) =>
8
+ locales.map((locale) => ({
9
+ url: absoluteSiteUrl(localizedPagePath(pageId, locale)),
10
+ changeFrequency: pageId === 'home' ? 'weekly' : 'monthly',
11
+ priority: pageId === 'home' ? 1 : 0.7,
12
+ alternates: {
13
+ languages: Object.fromEntries(
14
+ locales.map((availableLocale) => [
15
+ availableLocale,
16
+ absoluteSiteUrl(localizedPagePath(pageId, availableLocale)),
17
+ ]),
18
+ ),
19
+ },
20
+ })),
21
+ );
22
+ }
@@ -0,0 +1,22 @@
1
+ import Image from 'next/image';
2
+ import { cn } from '@/lib/utils/cn';
3
+
4
+ export function BrandMark({
5
+ className,
6
+ priority = false,
7
+ }: {
8
+ className?: string;
9
+ priority?: boolean;
10
+ }) {
11
+ return (
12
+ <Image
13
+ alt=''
14
+ aria-hidden='true'
15
+ className={cn('size-8 shrink-0 object-contain', className)}
16
+ height={32}
17
+ priority={priority}
18
+ src='/mascot-stitch.png'
19
+ width={32}
20
+ />
21
+ );
22
+ }
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+
3
+ import {
4
+ IconArrowUpRight,
5
+ IconBrandGithub,
6
+ IconGitCommit,
7
+ IconRefresh,
8
+ IconStar,
9
+ } from '@tabler/icons-react';
10
+ import { useQueryClient } from '@tanstack/react-query';
11
+ import { useLocale } from 'next-intl';
12
+ import { Button, Skeleton } from '@/components/ui';
13
+ import { useRefreshRepository, useRepository } from '@/lib/api/queries';
14
+
15
+ function RepositorySkeleton() {
16
+ return (
17
+ <div className='mx-auto flex w-full max-w-4xl items-center gap-4 rounded-xl border border-border bg-card px-4 py-4'>
18
+ <Skeleton className='size-10 shrink-0 rounded-xl' />
19
+ <div className='flex-1 space-y-2'>
20
+ <Skeleton className='h-4 w-44' />
21
+ <Skeleton className='h-3 w-64 max-w-full' />
22
+ </div>
23
+ <Skeleton className='hidden h-8 w-52 sm:block' />
24
+ </div>
25
+ );
26
+ }
27
+
28
+ export function RepositorySummary() {
29
+ const locale = useLocale();
30
+ const queryClient = useQueryClient();
31
+ const repository = useRepository();
32
+ const refresh = useRefreshRepository({
33
+ onSuccess: async () => {
34
+ await queryClient.invalidateQueries({ queryKey: useRepository.getKey() });
35
+ },
36
+ });
37
+
38
+ if (repository.isLoading) return <RepositorySkeleton />;
39
+ if (repository.isError || !repository.data) {
40
+ return (
41
+ <div className='mx-auto w-full max-w-4xl rounded-xl border border-border bg-card px-4 py-4 text-sm text-muted-foreground'>
42
+ Repository data is unavailable. Check the API connection and retry.
43
+ </div>
44
+ );
45
+ }
46
+
47
+ const snapshot = repository.data;
48
+ const commitDate = snapshot.latestCommit
49
+ ? new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(
50
+ new Date(snapshot.latestCommit.committedAt),
51
+ )
52
+ : '—';
53
+
54
+ return (
55
+ <div className='mx-auto flex w-full max-w-4xl flex-col gap-4 rounded-xl border border-border bg-card/80 px-4 py-4 text-left backdrop-blur sm:flex-row sm:items-center sm:px-5'>
56
+ <a
57
+ className='group flex min-w-0 flex-1 items-center gap-3'
58
+ href={snapshot.htmlUrl}
59
+ rel='noreferrer'
60
+ target='_blank'
61
+ >
62
+ <span className='flex size-10 shrink-0 items-center justify-center rounded-xl border border-border bg-background'>
63
+ <IconBrandGithub size={21} />
64
+ </span>
65
+ <span className='min-w-0'>
66
+ <span className='flex min-w-0 items-center gap-2 font-medium'>
67
+ <span className='truncate'>{snapshot.fullName}</span>
68
+ <IconArrowUpRight
69
+ className='shrink-0 opacity-50 group-hover:opacity-100'
70
+ size={16}
71
+ />
72
+ </span>
73
+ <span className='mt-1 block truncate text-xs text-muted-foreground'>
74
+ Live backend query · {snapshot.visibility} · {snapshot.language}
75
+ </span>
76
+ </span>
77
+ </a>
78
+
79
+ <div className='flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground sm:justify-end'>
80
+ <span className='flex items-center gap-1.5' title='Commits'>
81
+ <IconGitCommit size={16} />
82
+ <span className='font-medium text-foreground'>
83
+ {snapshot.commitCount.toLocaleString(locale)}
84
+ </span>
85
+ </span>
86
+ <span className='flex items-center gap-1.5' title='Stars'>
87
+ <IconStar size={16} />
88
+ <span className='font-medium text-foreground'>
89
+ {snapshot.stars.toLocaleString(locale)}
90
+ </span>
91
+ </span>
92
+ <span className='whitespace-nowrap text-xs'>{commitDate}</span>
93
+ <Button
94
+ aria-label='Refresh repository data'
95
+ aria-busy={refresh.isPending || undefined}
96
+ className='shrink-0 disabled:opacity-100'
97
+ disabled={refresh.isPending}
98
+ onClick={() => refresh.mutate(undefined)}
99
+ size='icon'
100
+ variant='outline'
101
+ >
102
+ <IconRefresh className={refresh.isPending ? 'animate-spin' : undefined} size={18} />
103
+ </Button>
104
+ </div>
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ import {
4
+ IconCheck,
5
+ IconDeviceDesktop,
6
+ IconLanguage,
7
+ IconMoon,
8
+ IconSun,
9
+ } from '@tabler/icons-react';
10
+ import { useHydrated } from '@wrksz/themes/client/use-hydrated';
11
+ import { useTheme } from '@wrksz/themes/client/use-theme';
12
+ import {
13
+ Button,
14
+ DropdownMenu,
15
+ DropdownMenuContent,
16
+ DropdownMenuItem,
17
+ DropdownMenuTrigger,
18
+ } from '@/components/ui';
19
+ import { Link, usePathname } from '@/i18n/navigation';
20
+ import type { AppTheme } from '@/theme/config';
21
+ import { defaultThemeTransition, runThemeTransition } from '@/theme/transition';
22
+
23
+ const themeOptions = [
24
+ { value: 'light', label: 'Light', icon: IconSun },
25
+ { value: 'dark', label: 'Dark', icon: IconMoon },
26
+ { value: 'system', label: 'System', icon: IconDeviceDesktop },
27
+ ] satisfies ReadonlyArray<{
28
+ value: AppTheme | 'system';
29
+ label: string;
30
+ icon: typeof IconSun;
31
+ }>;
32
+
33
+ export function ThemeToggle() {
34
+ const hydrated = useHydrated();
35
+ const { theme, resolvedTheme, setTheme } = useTheme<AppTheme>();
36
+ const selectedTheme = hydrated ? theme : undefined;
37
+ const ActiveIcon = resolvedTheme === 'dark' ? IconMoon : IconSun;
38
+
39
+ return (
40
+ <DropdownMenu>
41
+ <DropdownMenuTrigger asChild>
42
+ <Button
43
+ size='icon'
44
+ variant='outline'
45
+ aria-label={`Theme: ${selectedTheme ?? 'loading'}`}
46
+ >
47
+ {hydrated ? <ActiveIcon /> : <IconDeviceDesktop />}
48
+ </Button>
49
+ </DropdownMenuTrigger>
50
+ <DropdownMenuContent align='end'>
51
+ {themeOptions.map((option) => (
52
+ <DropdownMenuItem
53
+ key={option.value}
54
+ onSelect={() =>
55
+ runThemeTransition(() => setTheme(option.value), defaultThemeTransition)
56
+ }
57
+ >
58
+ <option.icon />
59
+ <span className='flex-1'>{option.label}</span>
60
+ {selectedTheme === option.value ? <IconCheck aria-hidden /> : null}
61
+ </DropdownMenuItem>
62
+ ))}
63
+ </DropdownMenuContent>
64
+ </DropdownMenu>
65
+ );
66
+ }
67
+
68
+ export function LanguageSwitcher() {
69
+ const pathname = usePathname();
70
+
71
+ return (
72
+ <DropdownMenu>
73
+ <DropdownMenuTrigger asChild>
74
+ <Button size='icon' variant='outline' aria-label='Change language'>
75
+ <IconLanguage />
76
+ </Button>
77
+ </DropdownMenuTrigger>
78
+ <DropdownMenuContent align='end'>
79
+ <DropdownMenuItem asChild>
80
+ <Link href={pathname} locale='en'>
81
+ English
82
+ </Link>
83
+ </DropdownMenuItem>
84
+ <DropdownMenuItem asChild>
85
+ <Link href={pathname} locale='ru'>
86
+ Русский
87
+ </Link>
88
+ </DropdownMenuItem>
89
+ </DropdownMenuContent>
90
+ </DropdownMenu>
91
+ );
92
+ }