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,82 @@
1
+ 'use client';
2
+
3
+ import { IconCheck, IconCreditCard } from '@tabler/icons-react';
4
+ import { useState } from 'react';
5
+ import {
6
+ AdaptiveModal,
7
+ Badge,
8
+ Button,
9
+ Card,
10
+ CardContent,
11
+ CardHeader,
12
+ CardTitle,
13
+ Field,
14
+ Input,
15
+ } from '@/components/ui';
16
+
17
+ export function PaymentShowcase() {
18
+ const [open, setOpen] = useState(false);
19
+ return (
20
+ <>
21
+ <Card className='max-w-lg'>
22
+ <CardHeader>
23
+ <div className='flex items-center justify-between'>
24
+ <div className='grid size-10 place-items-center rounded-xl bg-muted'>
25
+ <IconCreditCard />
26
+ </div>
27
+ <Badge>Secure checkout</Badge>
28
+ </div>
29
+ <CardTitle className='pt-4 text-2xl'>Complete your order</CardTitle>
30
+ </CardHeader>
31
+ <CardContent className='space-y-5'>
32
+ <div className='rounded-xl bg-muted p-4'>
33
+ <div className='flex justify-between'>
34
+ <span className='text-muted-foreground'>Product foundation</span>
35
+ <span className='font-medium'>$99</span>
36
+ </div>
37
+ </div>
38
+ <div className='space-y-2 text-sm'>
39
+ {[
40
+ 'Typed contract surface',
41
+ 'Complete UI source',
42
+ 'Production deployment profiles',
43
+ ].map((item) => (
44
+ <div className='flex items-center gap-2' key={item}>
45
+ <IconCheck className='text-success' size={17} />
46
+ {item}
47
+ </div>
48
+ ))}
49
+ </div>
50
+ <Button className='w-full' variant='primary' onClick={() => setOpen(true)}>
51
+ Continue to payment
52
+ </Button>
53
+ <p className='text-center text-xs text-muted-foreground'>
54
+ Presentation only. Connect your own provider contract.
55
+ </p>
56
+ </CardContent>
57
+ </Card>
58
+ <AdaptiveModal
59
+ isOpen={open}
60
+ onClose={() => setOpen(false)}
61
+ title='Checkout details'
62
+ desktopWidth='md'
63
+ mobileVariant='sheet'
64
+ footer={
65
+ <Button className='w-full' variant='primary' onClick={() => setOpen(false)}>
66
+ Confirm example order
67
+ </Button>
68
+ }
69
+ >
70
+ <div className='grid gap-4 p-4 pt-1'>
71
+ <Field label='Billing email' htmlFor='checkout-email'>
72
+ <Input id='checkout-email' type='email' placeholder='builder@example.com' />
73
+ </Field>
74
+ <div className='flex justify-between rounded-xl bg-muted p-4 text-sm'>
75
+ <span>Product foundation</span>
76
+ <strong>$99</strong>
77
+ </div>
78
+ </div>
79
+ </AdaptiveModal>
80
+ </>
81
+ );
82
+ }
@@ -0,0 +1,68 @@
1
+ import { LanguageSwitcher, ThemeToggle } from '@/components/system-controls';
2
+ import { Button } from '@/components/ui';
3
+ import { AccountShowcase } from './account-showcase';
4
+ import { AdminShowcase } from './admin-showcase';
5
+ import { StorySection } from './catalogue-shell';
6
+ import { CodeFoundationSection } from './code-foundation';
7
+ import { ImagePreloader } from './image-preloader';
8
+ import {
9
+ AudienceSection,
10
+ BuilderSection,
11
+ DemoModal,
12
+ FrontendSection,
13
+ LearningSection,
14
+ ProblemsSection,
15
+ TemplatePreviewSection,
16
+ WhySection,
17
+ } from './landing-sections';
18
+ import {
19
+ FeatureGrid,
20
+ HeroSection,
21
+ LandingFooter,
22
+ LandingHeader,
23
+ PortfolioSection,
24
+ PricingSection,
25
+ } from './landing-showcase';
26
+ import { PaymentShowcase } from './payment-showcase';
27
+
28
+ export function BlocksStory() {
29
+ return (
30
+ <div>
31
+ <StorySection title='Editorial landing system'>
32
+ <LandingHeader
33
+ actions={
34
+ <>
35
+ <LanguageSwitcher />
36
+ <ThemeToggle />
37
+ <Button size='sm'>Open app</Button>
38
+ </>
39
+ }
40
+ />
41
+ <HeroSection />
42
+ <ProblemsSection />
43
+ <BuilderSection />
44
+ <WhySection />
45
+ <CodeFoundationSection />
46
+ <FeatureGrid />
47
+ <AudienceSection />
48
+ <LearningSection />
49
+ <TemplatePreviewSection />
50
+ <FrontendSection />
51
+ <PortfolioSection />
52
+ <PricingSection />
53
+ <DemoModal />
54
+ <LandingFooter />
55
+ <ImagePreloader sources={['/theme-light.svg', '/theme-dark.svg']} />
56
+ </StorySection>
57
+ <StorySection title='Application shell'>
58
+ <AdminShowcase />
59
+ </StorySection>
60
+ <StorySection title='Account flows'>
61
+ <AccountShowcase />
62
+ </StorySection>
63
+ <StorySection title='Checkout presentation'>
64
+ <PaymentShowcase />
65
+ </StorySection>
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,267 @@
1
+ 'use client';
2
+
3
+ import { IconCheck, IconMail } from '@tabler/icons-react';
4
+ import { ThemedImage } from '@wrksz/themes/client/themed-image';
5
+ import { useState } from 'react';
6
+ import { BrandMark } from '@/components/brand-mark';
7
+ import {
8
+ Avatar,
9
+ AvatarFallback,
10
+ AvatarImage,
11
+ Badge,
12
+ Button,
13
+ Card,
14
+ CardContent,
15
+ CardDescription,
16
+ CardFooter,
17
+ CardHeader,
18
+ CardTitle,
19
+ Checkbox,
20
+ CopyableText,
21
+ Field,
22
+ HandDrawnCheck,
23
+ HandDrawnX,
24
+ Input,
25
+ Label,
26
+ OtpInput,
27
+ Select,
28
+ SelectContent,
29
+ SelectGroup,
30
+ SelectItem,
31
+ SelectLabel,
32
+ SelectSeparator,
33
+ SelectTrigger,
34
+ SelectValue,
35
+ Skeleton,
36
+ Slider,
37
+ Spinner,
38
+ Switch,
39
+ Table,
40
+ TableBody,
41
+ TableCell,
42
+ TableHead,
43
+ TableHeader,
44
+ TableRow,
45
+ Tabs,
46
+ TabsContent,
47
+ TabsList,
48
+ TabsTrigger,
49
+ Textarea,
50
+ TriStateCheckbox,
51
+ toast,
52
+ } from '@/components/ui';
53
+ import { StorySection } from './catalogue-shell';
54
+
55
+ type PrimitiveCategory = 'actions' | 'forms' | 'feedback' | 'media';
56
+
57
+ export function PrimitivesStory({ category }: { category: PrimitiveCategory }) {
58
+ const [otp, setOtp] = useState('');
59
+ const [enabled, setEnabled] = useState(true);
60
+
61
+ return (
62
+ <div>
63
+ {category === 'actions' ? (
64
+ <StorySection title='Actions and status'>
65
+ <div className='flex flex-wrap gap-3'>
66
+ <Button>default</Button>
67
+ <Button variant='primary'>primary</Button>
68
+ <Button variant='outline'>outline</Button>
69
+ <Button variant='ghost'>ghost</Button>
70
+ <Button variant='destructive'>destructive</Button>
71
+ <Button loading>Working</Button>
72
+ <Button size='icon' aria-label='Quick action'>
73
+ <BrandMark className='size-5' />
74
+ </Button>
75
+ </div>
76
+ <div className='flex flex-wrap gap-2'>
77
+ <Badge icon={<IconCheck />}>default</Badge>
78
+ <Badge icon={<IconCheck />} variant='primary'>
79
+ primary
80
+ </Badge>
81
+ <Badge icon={<IconCheck />} variant='secondary'>
82
+ secondary
83
+ </Badge>
84
+ <Badge icon={<IconCheck />} variant='outline'>
85
+ outline
86
+ </Badge>
87
+ <Badge icon={<IconCheck />} variant='success'>
88
+ success
89
+ </Badge>
90
+ <Badge icon={<IconCheck />} variant='warning'>
91
+ warning
92
+ </Badge>
93
+ <Badge icon={<IconCheck />} variant='destructive'>
94
+ destructive
95
+ </Badge>
96
+ </div>
97
+ </StorySection>
98
+ ) : null}
99
+
100
+ {category === 'forms' ? (
101
+ <StorySection title='Forms'>
102
+ <Card className='w-full'>
103
+ <CardHeader>
104
+ <CardTitle>Project details</CardTitle>
105
+ <CardDescription>Reusable accessible form primitives.</CardDescription>
106
+ </CardHeader>
107
+ <CardContent className='grid gap-4 sm:grid-cols-2'>
108
+ <Field label='Project name' htmlFor='story-name'>
109
+ <Input id='story-name' placeholder='Project atlas' />
110
+ </Field>
111
+ <Field label='Contact' htmlFor='story-email'>
112
+ <Input id='story-email' type='email' placeholder='you@example.com' />
113
+ </Field>
114
+ <div className='sm:col-span-2'>
115
+ <Field label='Description' htmlFor='story-description'>
116
+ <Textarea id='story-description' placeholder='What are you building?' />
117
+ </Field>
118
+ </div>
119
+ <div className='space-y-2'>
120
+ <Label htmlFor='story-select'>Stage</Label>
121
+ <Select defaultValue='building'>
122
+ <SelectTrigger id='story-select'>
123
+ <SelectValue />
124
+ </SelectTrigger>
125
+ <SelectContent>
126
+ <SelectGroup>
127
+ <SelectLabel>Planning</SelectLabel>
128
+ <SelectItem value='idea'>Idea</SelectItem>
129
+ </SelectGroup>
130
+ <SelectSeparator />
131
+ <SelectGroup>
132
+ <SelectLabel>Delivery</SelectLabel>
133
+ <SelectItem value='building'>Building</SelectItem>
134
+ <SelectItem value='shipping'>Shipping</SelectItem>
135
+ </SelectGroup>
136
+ </SelectContent>
137
+ </Select>
138
+ </div>
139
+ <div className='space-y-3'>
140
+ <Label htmlFor='story-progress'>Progress</Label>
141
+ <Slider
142
+ id='story-progress'
143
+ aria-label='Project progress'
144
+ defaultValue={[68]}
145
+ max={100}
146
+ />
147
+ </div>
148
+ <Checkbox defaultChecked label='Public roadmap' />
149
+ <div className='flex items-center gap-2 text-sm'>
150
+ <Switch id='story-realtime' checked={enabled} onCheckedChange={setEnabled} />
151
+ <Label htmlFor='story-realtime'>Realtime updates</Label>
152
+ </div>
153
+ <TriStateCheckbox checked='indeterminate' label='Select visible rows' />
154
+ </CardContent>
155
+ </Card>
156
+ </StorySection>
157
+ ) : null}
158
+
159
+ {category === 'feedback' ? (
160
+ <StorySection title='Identity, verification and feedback'>
161
+ <div className='grid gap-6 md:grid-cols-2'>
162
+ <Card>
163
+ <CardContent className='flex items-center gap-4 pt-4'>
164
+ <Avatar>
165
+ <AvatarImage src='https://github.com/github.png' alt='Example user avatar' />
166
+ <AvatarFallback>PB</AvatarFallback>
167
+ </Avatar>
168
+ <div>
169
+ <p className='font-medium'>Product builder</p>
170
+ <CopyableText value='builder@example.com'>
171
+ <span className='flex items-center gap-1'>
172
+ <IconMail size={14} /> builder@example.com
173
+ </span>
174
+ </CopyableText>
175
+ </div>
176
+ </CardContent>
177
+ </Card>
178
+ <Card>
179
+ <CardContent className='pt-4 text-sm text-muted-foreground'>
180
+ Card content
181
+ </CardContent>
182
+ <CardFooter>
183
+ <Button
184
+ size='sm'
185
+ variant='outline'
186
+ onClick={() => toast.info('Project opened')}
187
+ >
188
+ Open project
189
+ </Button>
190
+ </CardFooter>
191
+ </Card>
192
+ <Card>
193
+ <CardContent className='space-y-4 pt-4'>
194
+ <OtpInput value={otp} onChange={setOtp} />
195
+ <div className='grid grid-cols-2 gap-2'>
196
+ <Button onClick={() => toast.success('Changes saved')}>Success toast</Button>
197
+ <Button variant='outline' onClick={() => toast.info('Sync is running')}>
198
+ Info toast
199
+ </Button>
200
+ <Button variant='outline' onClick={() => toast.warning('Review required')}>
201
+ Warning toast
202
+ </Button>
203
+ <Button variant='destructive' onClick={() => toast.error('Action failed')}>
204
+ Error toast
205
+ </Button>
206
+ </div>
207
+ </CardContent>
208
+ </Card>
209
+ </div>
210
+ <div className='flex items-center gap-5'>
211
+ <HandDrawnCheck className='size-12 text-success' />
212
+ <HandDrawnX className='size-12 text-destructive' />
213
+ <Spinner />
214
+ <Skeleton className='h-10 w-40' />
215
+ </div>
216
+ </StorySection>
217
+ ) : null}
218
+
219
+ {category === 'media' ? (
220
+ <StorySection title='Tabs and theme-aware media'>
221
+ <Tabs defaultValue='preview'>
222
+ <TabsList>
223
+ <TabsTrigger value='preview'>Preview</TabsTrigger>
224
+ <TabsTrigger value='details'>Details</TabsTrigger>
225
+ </TabsList>
226
+ <TabsContent value='preview' className='pt-4'>
227
+ <ThemedImage
228
+ src={{ light: '/theme-light.svg', dark: '/theme-dark.svg' }}
229
+ alt='Theme-aware application shell preview'
230
+ width={640}
231
+ height={360}
232
+ className='w-full max-w-2xl rounded-2xl'
233
+ />
234
+ </TabsContent>
235
+ <TabsContent value='details' className='pt-4 text-sm text-muted-foreground'>
236
+ The same component switches assets with semantic theme state.
237
+ </TabsContent>
238
+ </Tabs>
239
+ </StorySection>
240
+ ) : null}
241
+ {category === 'media' ? (
242
+ <StorySection title='Semantic table primitives'>
243
+ <div className='overflow-hidden rounded-xl border border-border'>
244
+ <Table>
245
+ <TableHeader>
246
+ <TableRow>
247
+ <TableHead>Surface</TableHead>
248
+ <TableHead>Status</TableHead>
249
+ </TableRow>
250
+ </TableHeader>
251
+ <TableBody>
252
+ <TableRow>
253
+ <TableCell>HTTP</TableCell>
254
+ <TableCell>Ready</TableCell>
255
+ </TableRow>
256
+ <TableRow>
257
+ <TableCell>MCP</TableCell>
258
+ <TableCell>Ready</TableCell>
259
+ </TableRow>
260
+ </TableBody>
261
+ </Table>
262
+ </div>
263
+ </StorySection>
264
+ ) : null}
265
+ </div>
266
+ );
267
+ }
@@ -0,0 +1,14 @@
1
+ import type { StoryId } from '@/lib/seo/pages';
2
+ import { ComponentsStory } from './components-story';
3
+ import { BlocksStory } from './presentation-story';
4
+ import { ThemeStory } from './theme-story';
5
+
6
+ export function StoryPage({ story }: { story: StoryId }) {
7
+ if (story === 'components') {
8
+ return <ComponentsStory />;
9
+ }
10
+ if (story === 'themes') {
11
+ return <ThemeStory />;
12
+ }
13
+ return <BlocksStory />;
14
+ }
@@ -0,0 +1,208 @@
1
+ 'use client';
2
+
3
+ import { IconCheck, IconDeviceDesktop, IconMoon, IconSun } from '@tabler/icons-react';
4
+ import { ClientThemeProvider } from '@wrksz/themes/client/provider';
5
+ import { ThemedImage } from '@wrksz/themes/client/themed-image';
6
+ import { useHydrated } from '@wrksz/themes/client/use-hydrated';
7
+ import { useTheme } from '@wrksz/themes/client/use-theme';
8
+ import { useThemeEffect } from '@wrksz/themes/client/use-theme-effect';
9
+ import { useThemeValue } from '@wrksz/themes/client/use-theme-value';
10
+ import { useCallback, useState } from 'react';
11
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from '@/components/ui';
12
+ import { type AppTheme, appThemes } from '@/theme/config';
13
+ import {
14
+ runThemeTransition,
15
+ type ThemeTransitionConfig,
16
+ type ThemeTransitionStyle,
17
+ } from '@/theme/transition';
18
+ import { StorySection } from './catalogue-shell';
19
+
20
+ type ThemeSelection = AppTheme | 'system';
21
+
22
+ const themeChoices = [
23
+ { value: 'light', label: 'Light', icon: IconSun },
24
+ { value: 'dark', label: 'Dark', icon: IconMoon },
25
+ { value: 'system', label: 'System', icon: IconDeviceDesktop },
26
+ ] satisfies ReadonlyArray<{
27
+ value: ThemeSelection;
28
+ label: string;
29
+ icon: typeof IconSun;
30
+ }>;
31
+
32
+ const themeImages = {
33
+ light: '/theme-light.svg',
34
+ dark: '/theme-dark.svg',
35
+ } satisfies Record<AppTheme, string>;
36
+
37
+ const transitionStyles = [
38
+ { value: 'crossfade', label: 'Crossfade' },
39
+ { value: 'radial', label: 'Radial reveal' },
40
+ ] satisfies ReadonlyArray<{ value: ThemeTransitionStyle; label: string }>;
41
+
42
+ const transitionDurations = [150, 250, 400];
43
+
44
+ function ScopedThemeCard({ theme }: { theme: AppTheme }) {
45
+ const { resolvedTheme } = useTheme<AppTheme>();
46
+
47
+ return (
48
+ <div
49
+ id={`theme-scope-${theme}`}
50
+ className='rounded-2xl border border-border bg-background p-5 text-foreground'
51
+ >
52
+ <div className='flex items-center justify-between gap-4'>
53
+ <div>
54
+ <p className='font-medium'>{theme === 'light' ? 'Forced light' : 'Forced dark'}</p>
55
+ <p className='mt-1 text-sm text-muted-foreground'>Independent client provider</p>
56
+ </div>
57
+ <Badge variant='secondary'>{resolvedTheme ?? 'hydrating'}</Badge>
58
+ </div>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ export function ThemeStoryClient({ serverTheme }: { serverTheme: ThemeSelection }) {
64
+ const hydrated = useHydrated();
65
+ const { theme, resolvedTheme, systemTheme, setTheme } = useTheme<AppTheme>();
66
+ const surfaceLabel = useThemeValue({
67
+ light: 'Paper surface',
68
+ dark: 'Midnight surface',
69
+ default: 'Resolving surface',
70
+ });
71
+ const [lastChange, setLastChange] = useState('No client change yet');
72
+ const [transitionStyle, setTransitionStyle] = useState<ThemeTransitionStyle>('crossfade');
73
+ const [transitionDuration, setTransitionDuration] = useState(250);
74
+ const transitionConfig = {
75
+ style: transitionStyle,
76
+ durationMs: transitionDuration,
77
+ easing: 'ease-out',
78
+ } satisfies ThemeTransitionConfig;
79
+ const captureThemeChange = useCallback(
80
+ (selected: string | undefined, resolved: string | undefined) => {
81
+ setLastChange(`${selected ?? 'unknown'} → ${resolved ?? 'unknown'}`);
82
+ },
83
+ [],
84
+ );
85
+ useThemeEffect(captureThemeChange);
86
+
87
+ const selectTheme = (selection: ThemeSelection, trigger: HTMLButtonElement) => {
88
+ const bounds = trigger.getBoundingClientRect();
89
+ runThemeTransition(() => setTheme(selection), transitionConfig, {
90
+ x: bounds.left + bounds.width / 2,
91
+ y: bounds.top + bounds.height / 2,
92
+ });
93
+ };
94
+
95
+ return (
96
+ <div>
97
+ <StorySection title='Native transition lab'>
98
+ <div className='grid gap-5 rounded-2xl border border-border bg-card p-5 sm:grid-cols-2'>
99
+ <div>
100
+ <p className='text-sm font-medium'>Effect</p>
101
+ <div className='mt-3 flex flex-wrap gap-2'>
102
+ {transitionStyles.map((option) => (
103
+ <Button
104
+ key={option.value}
105
+ size='sm'
106
+ variant={transitionStyle === option.value ? 'primary' : 'outline'}
107
+ aria-pressed={transitionStyle === option.value}
108
+ onClick={() => setTransitionStyle(option.value)}
109
+ >
110
+ {option.label}
111
+ </Button>
112
+ ))}
113
+ </div>
114
+ </div>
115
+ <div>
116
+ <p className='text-sm font-medium'>Duration</p>
117
+ <div className='mt-3 flex flex-wrap gap-2'>
118
+ {transitionDurations.map((duration) => (
119
+ <Button
120
+ key={duration}
121
+ size='sm'
122
+ variant={transitionDuration === duration ? 'primary' : 'outline'}
123
+ aria-pressed={transitionDuration === duration}
124
+ onClick={() => setTransitionDuration(duration)}
125
+ >
126
+ {duration} ms
127
+ </Button>
128
+ ))}
129
+ </div>
130
+ </div>
131
+ </div>
132
+ <p className='text-sm text-muted-foreground'>
133
+ Theme state still belongs to @wrksz/themes. This page only configures the native
134
+ snapshot animation used by the controls below.
135
+ </p>
136
+ </StorySection>
137
+
138
+ <StorySection title='Global typed theme state'>
139
+ <div className='grid gap-3 sm:grid-cols-3'>
140
+ {themeChoices.map((choice) => (
141
+ <Button
142
+ key={choice.value}
143
+ variant={theme === choice.value ? 'primary' : 'outline'}
144
+ aria-pressed={theme === choice.value}
145
+ onClick={(event) => selectTheme(choice.value, event.currentTarget)}
146
+ >
147
+ <choice.icon />
148
+ {choice.label}
149
+ {theme === choice.value ? <IconCheck aria-hidden /> : null}
150
+ </Button>
151
+ ))}
152
+ </div>
153
+ <div className='grid gap-3 sm:grid-cols-2 lg:grid-cols-4'>
154
+ {[
155
+ ['Selected', hydrated ? (theme ?? 'unknown') : 'hydrating'],
156
+ ['Resolved', hydrated ? (resolvedTheme ?? 'unknown') : 'hydrating'],
157
+ ['Operating system', hydrated ? (systemTheme ?? 'unknown') : 'hydrating'],
158
+ ['Server cookie', serverTheme],
159
+ ].map(([label, value]) => (
160
+ <Card
161
+ key={label}
162
+ data-testid={`theme-state-${label.toLowerCase().replaceAll(' ', '-')}`}
163
+ >
164
+ <CardHeader>
165
+ <CardTitle className='text-sm text-muted-foreground'>{label}</CardTitle>
166
+ </CardHeader>
167
+ <CardContent className='font-medium'>{value}</CardContent>
168
+ </Card>
169
+ ))}
170
+ </div>
171
+ <p className='text-sm text-muted-foreground'>
172
+ {surfaceLabel} · Last observed change: {lastChange}
173
+ </p>
174
+ </StorySection>
175
+
176
+ <StorySection title='Theme-aware media'>
177
+ <ThemedImage
178
+ src={themeImages}
179
+ alt='Application shell selected by the resolved theme'
180
+ width={1280}
181
+ height={720}
182
+ className='w-full rounded-2xl border border-border'
183
+ />
184
+ </StorySection>
185
+
186
+ <StorySection title='Independent scoped themes'>
187
+ <div className='grid gap-4 md:grid-cols-2'>
188
+ <ClientThemeProvider<AppTheme>
189
+ themes={appThemes}
190
+ forcedTheme='light'
191
+ target='#theme-scope-light'
192
+ storage='none'
193
+ >
194
+ <ScopedThemeCard theme='light' />
195
+ </ClientThemeProvider>
196
+ <ClientThemeProvider<AppTheme>
197
+ themes={appThemes}
198
+ forcedTheme='dark'
199
+ target='#theme-scope-dark'
200
+ storage='none'
201
+ >
202
+ <ScopedThemeCard theme='dark' />
203
+ </ClientThemeProvider>
204
+ </div>
205
+ </StorySection>
206
+ </div>
207
+ );
208
+ }
@@ -0,0 +1,13 @@
1
+ import { getTheme } from '@wrksz/themes/next';
2
+ import { appThemes, themeStorageKey } from '@/theme/config';
3
+ import { ThemeStoryClient } from './theme-story-client';
4
+
5
+ export async function ThemeStory() {
6
+ const serverTheme = await getTheme({
7
+ themes: appThemes,
8
+ defaultTheme: 'system',
9
+ storageKey: themeStorageKey,
10
+ });
11
+
12
+ return <ThemeStoryClient serverTheme={serverTheme} />;
13
+ }
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ import { CatalogueShell } from './_catalogue';
3
+
4
+ export default function UiLayout({ children }: { children: ReactNode }) {
5
+ return <CatalogueShell>{children}</CatalogueShell>;
6
+ }
@@ -0,0 +1,10 @@
1
+ import { redirect } from '@/i18n/navigation';
2
+
3
+ export default async function UiCataloguePage({
4
+ params,
5
+ }: {
6
+ params: Promise<{ locale: string }>;
7
+ }) {
8
+ const { locale } = await params;
9
+ redirect({ href: '/ui/components', locale });
10
+ }