@mettlecast/domain-cli 0.2.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 (301) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +133 -0
  3. package/dist/builder/build-registry.d.ts +20 -0
  4. package/dist/builder/build-registry.js +178 -0
  5. package/dist/builder/build-types.d.ts +10 -0
  6. package/dist/builder/build-types.js +32 -0
  7. package/dist/builder/load-domain-module-config.d.ts +12 -0
  8. package/dist/builder/load-domain-module-config.js +34 -0
  9. package/dist/builder/load-module.d.ts +26 -0
  10. package/dist/builder/load-module.js +114 -0
  11. package/dist/builder/zod-to-json.d.ts +10 -0
  12. package/dist/builder/zod-to-json.js +15 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +338 -0
  15. package/dist/commands/add-api.d.ts +32 -0
  16. package/dist/commands/add-api.js +106 -0
  17. package/dist/commands/add-component.d.ts +15 -0
  18. package/dist/commands/add-component.js +78 -0
  19. package/dist/commands/add-domain.d.ts +21 -0
  20. package/dist/commands/add-domain.js +71 -0
  21. package/dist/commands/add-flow.d.ts +14 -0
  22. package/dist/commands/add-flow.js +108 -0
  23. package/dist/commands/add-hook.d.ts +15 -0
  24. package/dist/commands/add-hook.js +65 -0
  25. package/dist/commands/add-migration.d.ts +26 -0
  26. package/dist/commands/add-migration.js +93 -0
  27. package/dist/commands/add-module.d.ts +7 -0
  28. package/dist/commands/add-module.js +261 -0
  29. package/dist/commands/add-page.d.ts +13 -0
  30. package/dist/commands/add-page.js +170 -0
  31. package/dist/commands/add-seed-page.d.ts +5 -0
  32. package/dist/commands/add-seed-page.js +251 -0
  33. package/dist/commands/add-subscriber.d.ts +30 -0
  34. package/dist/commands/add-subscriber.js +87 -0
  35. package/dist/commands/add-test.d.ts +22 -0
  36. package/dist/commands/add-test.js +285 -0
  37. package/dist/commands/audit.d.ts +39 -0
  38. package/dist/commands/audit.js +171 -0
  39. package/dist/commands/build-catalog.d.ts +68 -0
  40. package/dist/commands/build-catalog.js +105 -0
  41. package/dist/commands/build-flows.d.ts +26 -0
  42. package/dist/commands/build-flows.js +174 -0
  43. package/dist/commands/build.d.ts +17 -0
  44. package/dist/commands/build.js +39 -0
  45. package/dist/commands/check-hashes.d.ts +38 -0
  46. package/dist/commands/check-hashes.js +97 -0
  47. package/dist/commands/create-project.d.ts +5 -0
  48. package/dist/commands/create-project.js +272 -0
  49. package/dist/commands/dev.d.ts +15 -0
  50. package/dist/commands/dev.js +27 -0
  51. package/dist/commands/doctor.d.ts +48 -0
  52. package/dist/commands/doctor.js +1301 -0
  53. package/dist/commands/explain.d.ts +34 -0
  54. package/dist/commands/explain.js +167 -0
  55. package/dist/commands/power-tune.d.ts +1 -0
  56. package/dist/commands/power-tune.js +87 -0
  57. package/dist/commands/reseed-page.d.ts +6 -0
  58. package/dist/commands/reseed-page.js +297 -0
  59. package/dist/commands/show-dns.d.ts +15 -0
  60. package/dist/commands/show-dns.js +101 -0
  61. package/dist/commands/show.d.ts +46 -0
  62. package/dist/commands/show.js +135 -0
  63. package/dist/commands/test.d.ts +18 -0
  64. package/dist/commands/test.js +56 -0
  65. package/dist/commands/upgrade-backend.d.ts +9 -0
  66. package/dist/commands/upgrade-backend.js +46 -0
  67. package/dist/commands/upgrade-event.d.ts +22 -0
  68. package/dist/commands/upgrade-event.js +111 -0
  69. package/dist/commands/upgrade-frontend.d.ts +5 -0
  70. package/dist/commands/upgrade-frontend.js +99 -0
  71. package/dist/commands/upgrade.d.ts +7 -0
  72. package/dist/commands/upgrade.js +495 -0
  73. package/dist/commands/validate.d.ts +30 -0
  74. package/dist/commands/validate.js +148 -0
  75. package/dist/runtime-stubs/hydrate-local-ctx.d.ts +49 -0
  76. package/dist/runtime-stubs/hydrate-local-ctx.js +45 -0
  77. package/dist/runtime-stubs/load-secrets.d.ts +11 -0
  78. package/dist/runtime-stubs/load-secrets.js +37 -0
  79. package/dist/server/api-server.d.ts +21 -0
  80. package/dist/server/api-server.js +14 -0
  81. package/dist/server/mount-routes.d.ts +19 -0
  82. package/dist/server/mount-routes.js +42 -0
  83. package/dist/templates/api-skeleton.d.ts +18 -0
  84. package/dist/templates/api-skeleton.js +69 -0
  85. package/dist/templates/claude-md.d.ts +4 -0
  86. package/dist/templates/claude-md.js +7 -0
  87. package/dist/templates/dashboard-pages/index.d.ts +101 -0
  88. package/dist/templates/dashboard-pages/index.js +22 -0
  89. package/dist/templates/domain-config.d.ts +10 -0
  90. package/dist/templates/domain-config.js +19 -0
  91. package/dist/templates/events-skeleton.d.ts +9 -0
  92. package/dist/templates/events-skeleton.js +23 -0
  93. package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
  94. package/dist/templates/patterns/api/create-with-event.js +69 -0
  95. package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
  96. package/dist/templates/patterns/api/idempotent-mutation.js +99 -0
  97. package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
  98. package/dist/templates/patterns/api/paginated-list.js +71 -0
  99. package/dist/templates/patterns/api/simple-crud.d.ts +6 -0
  100. package/dist/templates/patterns/api/simple-crud.js +95 -0
  101. package/dist/templates/patterns/api/system-admin.d.ts +5 -0
  102. package/dist/templates/patterns/api/system-admin.js +98 -0
  103. package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
  104. package/dist/templates/patterns/api/webhook-receiver-style.js +125 -0
  105. package/dist/templates/patterns/component/index.d.ts +25 -0
  106. package/dist/templates/patterns/component/index.js +814 -0
  107. package/dist/templates/patterns/flow/autonomous-remediation.d.ts +5 -0
  108. package/dist/templates/patterns/flow/autonomous-remediation.js +98 -0
  109. package/dist/templates/patterns/flow/fan-out-reaction.d.ts +5 -0
  110. package/dist/templates/patterns/flow/fan-out-reaction.js +53 -0
  111. package/dist/templates/patterns/flow/human-in-loop.d.ts +5 -0
  112. package/dist/templates/patterns/flow/human-in-loop.js +90 -0
  113. package/dist/templates/patterns/flow/multi-step-orchestration.d.ts +5 -0
  114. package/dist/templates/patterns/flow/multi-step-orchestration.js +67 -0
  115. package/dist/templates/patterns/hook/index.d.ts +7 -0
  116. package/dist/templates/patterns/hook/index.js +378 -0
  117. package/dist/templates/patterns/migration/column-add.d.ts +5 -0
  118. package/dist/templates/patterns/migration/column-add.js +52 -0
  119. package/dist/templates/patterns/migration/event-store.d.ts +6 -0
  120. package/dist/templates/patterns/migration/event-store.js +97 -0
  121. package/dist/templates/patterns/migration/index.d.ts +5 -0
  122. package/dist/templates/patterns/migration/index.js +52 -0
  123. package/dist/templates/patterns/migration/rls-policy.d.ts +5 -0
  124. package/dist/templates/patterns/migration/rls-policy.js +78 -0
  125. package/dist/templates/patterns/migration/tenant-scoped-table.d.ts +6 -0
  126. package/dist/templates/patterns/migration/tenant-scoped-table.js +81 -0
  127. package/dist/templates/patterns/page/dashboard.d.ts +4 -0
  128. package/dist/templates/patterns/page/dashboard.js +164 -0
  129. package/dist/templates/patterns/page/detail.d.ts +4 -0
  130. package/dist/templates/patterns/page/detail.js +162 -0
  131. package/dist/templates/patterns/page/form.d.ts +4 -0
  132. package/dist/templates/patterns/page/form.js +192 -0
  133. package/dist/templates/patterns/page/index.d.ts +8 -0
  134. package/dist/templates/patterns/page/index.js +14 -0
  135. package/dist/templates/patterns/page/list.d.ts +4 -0
  136. package/dist/templates/patterns/page/list.js +138 -0
  137. package/dist/templates/patterns/page/settings.d.ts +4 -0
  138. package/dist/templates/patterns/page/settings.js +318 -0
  139. package/dist/templates/patterns/page/wizard.d.ts +4 -0
  140. package/dist/templates/patterns/page/wizard.js +293 -0
  141. package/dist/templates/patterns/subscriber/audit-relay.d.ts +5 -0
  142. package/dist/templates/patterns/subscriber/audit-relay.js +51 -0
  143. package/dist/templates/patterns/subscriber/cascade-deletion.d.ts +5 -0
  144. package/dist/templates/patterns/subscriber/cascade-deletion.js +60 -0
  145. package/dist/templates/patterns/subscriber/single-step-projection.d.ts +5 -0
  146. package/dist/templates/patterns/subscriber/single-step-projection.js +36 -0
  147. package/dist/templates/readme.d.ts +4 -0
  148. package/dist/templates/readme.js +7 -0
  149. package/dist/templates/subscriber-skeleton.d.ts +11 -0
  150. package/dist/templates/subscriber-skeleton.js +23 -0
  151. package/dist/utils/checksum.d.ts +18 -0
  152. package/dist/utils/checksum.js +39 -0
  153. package/dist/utils/file-helpers.d.ts +35 -0
  154. package/dist/utils/file-helpers.js +59 -0
  155. package/dist/utils/header-inject.d.ts +15 -0
  156. package/dist/utils/header-inject.js +93 -0
  157. package/dist/utils/install-file.d.ts +17 -0
  158. package/dist/utils/install-file.js +78 -0
  159. package/dist/utils/logger.d.ts +12 -0
  160. package/dist/utils/logger.js +24 -0
  161. package/dist/utils/manifest.d.ts +28 -0
  162. package/dist/utils/manifest.js +93 -0
  163. package/dist/utils/s3-fetch.d.ts +25 -0
  164. package/dist/utils/s3-fetch.js +52 -0
  165. package/dist/utils/scaffold-config.d.ts +95 -0
  166. package/dist/utils/scaffold-config.js +53 -0
  167. package/dist/utils/token-substitution.d.ts +35 -0
  168. package/dist/utils/token-substitution.js +52 -0
  169. package/dist/utils/zod-extractor.d.ts +10 -0
  170. package/dist/utils/zod-extractor.js +28 -0
  171. package/package.json +43 -0
  172. package/src/__tests__/build-registry.test.ts +115 -0
  173. package/src/__tests__/build-types.test.ts +100 -0
  174. package/src/__tests__/builder/walkDomainDir.test.ts +140 -0
  175. package/src/__tests__/commands/add-domain.test.ts +174 -0
  176. package/src/__tests__/commands/add-flow.test.ts +69 -0
  177. package/src/__tests__/commands/add-module.test.ts +207 -0
  178. package/src/__tests__/commands/build-flows.test.ts +154 -0
  179. package/src/__tests__/commands/check-hashes.test.ts +142 -0
  180. package/src/__tests__/commands/create-project.test.ts +712 -0
  181. package/src/__tests__/commands/upgrade.test.ts +762 -0
  182. package/src/__tests__/doctor.test.ts +291 -0
  183. package/src/__tests__/hydrate-local-ctx.test.ts +71 -0
  184. package/src/__tests__/load-domain-module-config.test.ts +10 -0
  185. package/src/__tests__/mount-routes.test.ts +100 -0
  186. package/src/__tests__/scaffold-src/part-a-layout.test.ts +46 -0
  187. package/src/__tests__/scripts/package-scaffold.test.ts +45 -0
  188. package/src/__tests__/utils/checksum.test.ts +90 -0
  189. package/src/__tests__/utils/header-inject.test.ts +89 -0
  190. package/src/__tests__/utils/install-file.test.ts +207 -0
  191. package/src/__tests__/utils/manifest.test.ts +229 -0
  192. package/src/__tests__/utils/token-substitution.test.ts +134 -0
  193. package/src/__tests__/validate.test.ts +18 -0
  194. package/src/builder/build-registry.ts +237 -0
  195. package/src/builder/build-types.ts +36 -0
  196. package/src/builder/load-domain-module-config.ts +43 -0
  197. package/src/builder/load-module.ts +133 -0
  198. package/src/builder/zod-to-json.ts +16 -0
  199. package/src/cli.ts +387 -0
  200. package/src/commands/add-api.ts +145 -0
  201. package/src/commands/add-component.ts +116 -0
  202. package/src/commands/add-domain.ts +100 -0
  203. package/src/commands/add-flow.ts +136 -0
  204. package/src/commands/add-hook.ts +100 -0
  205. package/src/commands/add-migration.ts +129 -0
  206. package/src/commands/add-module.ts +347 -0
  207. package/src/commands/add-page.ts +242 -0
  208. package/src/commands/add-seed-page.ts +338 -0
  209. package/src/commands/add-subscriber.ts +123 -0
  210. package/src/commands/add-test.ts +328 -0
  211. package/src/commands/audit.ts +217 -0
  212. package/src/commands/build-catalog.ts +181 -0
  213. package/src/commands/build-flows.ts +198 -0
  214. package/src/commands/build.ts +59 -0
  215. package/src/commands/check-hashes.ts +150 -0
  216. package/src/commands/create-project.ts +367 -0
  217. package/src/commands/dev.ts +47 -0
  218. package/src/commands/doctor.ts +1362 -0
  219. package/src/commands/explain.ts +209 -0
  220. package/src/commands/power-tune.ts +112 -0
  221. package/src/commands/reseed-page.ts +410 -0
  222. package/src/commands/show-dns.ts +139 -0
  223. package/src/commands/show.ts +179 -0
  224. package/src/commands/test.ts +91 -0
  225. package/src/commands/upgrade-backend.ts +72 -0
  226. package/src/commands/upgrade-event.ts +175 -0
  227. package/src/commands/upgrade-frontend.ts +135 -0
  228. package/src/commands/upgrade.ts +668 -0
  229. package/src/commands/validate.ts +208 -0
  230. package/src/runtime-stubs/hydrate-local-ctx.ts +102 -0
  231. package/src/runtime-stubs/load-secrets.ts +40 -0
  232. package/src/server/api-server.ts +34 -0
  233. package/src/server/mount-routes.ts +68 -0
  234. package/src/templates/api-skeleton.ts +72 -0
  235. package/src/templates/claude-md.ts +7 -0
  236. package/src/templates/dashboard-pages/account/api-keys.tsx +107 -0
  237. package/src/templates/dashboard-pages/account/audit-log.tsx +60 -0
  238. package/src/templates/dashboard-pages/account/index.ts +5 -0
  239. package/src/templates/dashboard-pages/account/members.tsx +107 -0
  240. package/src/templates/dashboard-pages/account/profile.tsx +53 -0
  241. package/src/templates/dashboard-pages/account/workspace-settings.tsx +64 -0
  242. package/src/templates/dashboard-pages/auth/accept-invitation.tsx +62 -0
  243. package/src/templates/dashboard-pages/auth/choose-org.tsx +66 -0
  244. package/src/templates/dashboard-pages/auth/forgot-password.tsx +69 -0
  245. package/src/templates/dashboard-pages/auth/index.ts +10 -0
  246. package/src/templates/dashboard-pages/auth/login.tsx +75 -0
  247. package/src/templates/dashboard-pages/auth/mfa-setup.tsx +56 -0
  248. package/src/templates/dashboard-pages/auth/mfa-verify.tsx +49 -0
  249. package/src/templates/dashboard-pages/auth/reset-password.tsx +64 -0
  250. package/src/templates/dashboard-pages/auth/sign-out.tsx +20 -0
  251. package/src/templates/dashboard-pages/auth/signup.tsx +71 -0
  252. package/src/templates/dashboard-pages/auth/verify-email.tsx +53 -0
  253. package/src/templates/dashboard-pages/index.ts +22 -0
  254. package/src/templates/dashboard-pages/shell/dashboard-home.tsx +41 -0
  255. package/src/templates/dashboard-pages/shell/forbidden.tsx +46 -0
  256. package/src/templates/dashboard-pages/shell/index.ts +5 -0
  257. package/src/templates/dashboard-pages/shell/maintenance.tsx +27 -0
  258. package/src/templates/dashboard-pages/shell/not-found.tsx +34 -0
  259. package/src/templates/dashboard-pages/shell/server-error.tsx +44 -0
  260. package/src/templates/domain-config.ts +20 -0
  261. package/src/templates/events-skeleton.ts +24 -0
  262. package/src/templates/patterns/api/create-with-event.ts +72 -0
  263. package/src/templates/patterns/api/idempotent-mutation.ts +102 -0
  264. package/src/templates/patterns/api/paginated-list.ts +74 -0
  265. package/src/templates/patterns/api/simple-crud.ts +100 -0
  266. package/src/templates/patterns/api/system-admin.ts +101 -0
  267. package/src/templates/patterns/api/webhook-receiver-style.ts +128 -0
  268. package/src/templates/patterns/component/index.ts +833 -0
  269. package/src/templates/patterns/flow/autonomous-remediation.ts +101 -0
  270. package/src/templates/patterns/flow/fan-out-reaction.ts +56 -0
  271. package/src/templates/patterns/flow/human-in-loop.ts +93 -0
  272. package/src/templates/patterns/flow/multi-step-orchestration.ts +70 -0
  273. package/src/templates/patterns/hook/index.ts +382 -0
  274. package/src/templates/patterns/migration/column-add.ts +54 -0
  275. package/src/templates/patterns/migration/event-store.ts +99 -0
  276. package/src/templates/patterns/migration/index.ts +54 -0
  277. package/src/templates/patterns/migration/rls-policy.ts +80 -0
  278. package/src/templates/patterns/migration/tenant-scoped-table.ts +83 -0
  279. package/src/templates/patterns/page/dashboard.ts +164 -0
  280. package/src/templates/patterns/page/detail.ts +162 -0
  281. package/src/templates/patterns/page/form.ts +192 -0
  282. package/src/templates/patterns/page/index.ts +17 -0
  283. package/src/templates/patterns/page/list.ts +138 -0
  284. package/src/templates/patterns/page/settings.ts +318 -0
  285. package/src/templates/patterns/page/wizard.ts +293 -0
  286. package/src/templates/patterns/subscriber/audit-relay.ts +54 -0
  287. package/src/templates/patterns/subscriber/cascade-deletion.ts +63 -0
  288. package/src/templates/patterns/subscriber/single-step-projection.ts +39 -0
  289. package/src/templates/readme.ts +7 -0
  290. package/src/templates/subscriber-skeleton.ts +28 -0
  291. package/src/utils/checksum.ts +43 -0
  292. package/src/utils/file-helpers.ts +91 -0
  293. package/src/utils/header-inject.ts +126 -0
  294. package/src/utils/install-file.ts +99 -0
  295. package/src/utils/logger.ts +26 -0
  296. package/src/utils/manifest.ts +137 -0
  297. package/src/utils/s3-fetch.ts +91 -0
  298. package/src/utils/scaffold-config.ts +172 -0
  299. package/src/utils/token-substitution.ts +80 -0
  300. package/src/utils/zod-extractor.ts +31 -0
  301. package/tsconfig.json +13 -0
@@ -0,0 +1,833 @@
1
+ /**
2
+ * Atom component template — single design-token usage (Button, Input, Icon).
3
+ * Uses Tailwind tokens, Radix primitives.
4
+ */
5
+ export function atomTemplate(
6
+ name: string
7
+ ): { component: string; story: string; test: string } {
8
+ const componentName = capitalize(name);
9
+ const camelName = camelize(name);
10
+
11
+ return {
12
+ component: `import { forwardRef } from 'react';
13
+ import { Slot } from '@radix-ui/react-slot';
14
+ import { cn } from '@/lib/utils';
15
+
16
+ /** Props for the ${componentName} atom component. */
17
+ export interface ${componentName}Props extends React.HTMLAttributes<HTMLElement> {
18
+ /** Render as a different element (e.g. \`asChild\` for link-in-button). */
19
+ asChild?: boolean;
20
+ /** Visual variant of the component. */
21
+ variant?: 'default' | 'outline' | 'ghost';
22
+ /** Size preset. */
23
+ size?: 'sm' | 'md' | 'lg';
24
+ /** Disabled state — prevents interaction and applies muted styling. */
25
+ disabled?: boolean;
26
+ }
27
+
28
+ /**
29
+ * \`${componentName}\` — an atom-level design-system component.
30
+ *
31
+ * Uses Tailwind semantic tokens for colours and spacing.
32
+ * Built on Radix \`Slot\` for polymorphic composition.
33
+ */
34
+ export const ${componentName} = forwardRef<HTMLElement, ${componentName}Props>(
35
+ ({ className, variant = 'default', size = 'md', asChild = false, disabled, children, ...props }, ref) => {
36
+ const Comp = asChild ? Slot : 'div';
37
+
38
+ return (
39
+ <Comp
40
+ ref={ref}
41
+ className={cn(
42
+ // Base styles
43
+ 'inline-flex items-center justify-center gap-2 rounded-md font-medium',
44
+ 'transition-colors duration-150',
45
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
46
+ // Size variants
47
+ size === 'sm' && 'h-8 px-3 text-xs',
48
+ size === 'md' && 'h-10 px-4 text-sm',
49
+ size === 'lg' && 'h-12 px-6 text-base',
50
+ // Style variants
51
+ variant === 'default' && 'bg-primary text-primary-foreground hover:bg-primary/90',
52
+ variant === 'outline' && 'border border-border bg-card text-foreground hover:bg-accent hover:text-accent-foreground',
53
+ variant === 'ghost' && 'text-foreground hover:bg-accent hover:text-accent-foreground',
54
+ // Disabled
55
+ disabled && 'pointer-events-none opacity-50',
56
+ className,
57
+ )}
58
+ aria-disabled={disabled}
59
+ data-slot="${camelName}"
60
+ {...props}
61
+ >
62
+ {children}
63
+ </Comp>
64
+ );
65
+ }
66
+ );
67
+
68
+ ${componentName}.displayName = '${componentName}';
69
+ `,
70
+
71
+ story: `import type { Meta, StoryObj } from '@storybook/react';
72
+ import { ${componentName} } from './${camelName}';
73
+ import { Star } from 'lucide-react';
74
+
75
+ const meta: Meta<typeof ${componentName}> = {
76
+ title: 'Atoms/${componentName}',
77
+ component: ${componentName},
78
+ tags: ['autodocs'],
79
+ argTypes: {
80
+ variant: {
81
+ control: 'select',
82
+ options: ['default', 'outline', 'ghost'],
83
+ },
84
+ size: {
85
+ control: 'select',
86
+ options: ['sm', 'md', 'lg'],
87
+ },
88
+ disabled: { control: 'boolean' },
89
+ },
90
+ };
91
+ export default meta;
92
+ type Story = StoryObj<typeof ${componentName}>;
93
+
94
+ /** Default variant at medium size. */
95
+ export const Default: Story = {
96
+ args: {
97
+ children: '${componentName}',
98
+ variant: 'default',
99
+ size: 'md',
100
+ },
101
+ };
102
+
103
+ /** Outline variant, a11y-check: contrast ratio for muted borders. */
104
+ export const Outline: Story = {
105
+ args: {
106
+ children: '${componentName}',
107
+ variant: 'outline',
108
+ size: 'md',
109
+ },
110
+ };
111
+
112
+ /** Small ghost variant — icon-only use case. */
113
+ export const GhostSmall: Story = {
114
+ args: {
115
+ children: <Star className="h-4 w-4" />,
116
+ variant: 'ghost',
117
+ size: 'sm',
118
+ 'aria-label': 'Favorite',
119
+ },
120
+ };
121
+
122
+ /** Large with icon + label. */
123
+ export const LargeWithIcon: Story = {
124
+ args: {
125
+ children: (
126
+ <>
127
+ <Star className="h-5 w-5" />
128
+ ${componentName}
129
+ </>
130
+ ),
131
+ size: 'lg',
132
+ },
133
+ };
134
+
135
+ /** Disabled state. */
136
+ export const Disabled: Story = {
137
+ args: {
138
+ children: 'Disabled',
139
+ disabled: true,
140
+ },
141
+ };
142
+ `,
143
+
144
+ test: `import { test, expect } from '@playwright/test';
145
+ import { runAxe } from '@axe-core/playwright';
146
+
147
+ test.describe('${componentName}', () => {
148
+ test('renders with no a11y violations', async ({ page, mount }) => {
149
+ const component = await mount(
150
+ <${componentName}>Test ${componentName}</${componentName}>
151
+ );
152
+ await expect(component).toBeVisible();
153
+ const results = await runAxe(page, component);
154
+ expect(results.violations).toEqual([]);
155
+ });
156
+
157
+ test('handles disabled state', async ({ page, mount }) => {
158
+ const component = await mount(
159
+ <${componentName} disabled>Disabled</${componentName}>
160
+ );
161
+ await expect(component).toHaveAttribute('aria-disabled', 'true');
162
+ const results = await runAxe(page, component);
163
+ expect(results.violations).toEqual([]);
164
+ });
165
+
166
+ test('variant classes are applied', async ({ page, mount }) => {
167
+ const component = await mount(
168
+ <${componentName} variant="outline">Outline</${componentName}>
169
+ );
170
+ await expect(component).toHaveClass(/border/);
171
+ });
172
+ });
173
+ `,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Molecule component template — composition of 2+ atoms (SearchBar, FormField, DataRow).
179
+ */
180
+ export function moleculeTemplate(
181
+ name: string
182
+ ): { component: string; story: string; test: string } {
183
+ const componentName = capitalize(name);
184
+ const camelName = camelize(name);
185
+
186
+ return {
187
+ component: `import { useState, forwardRef, useCallback } from 'react';
188
+ import { Search, X } from 'lucide-react';
189
+ import { cn } from '@/lib/utils';
190
+ import { Button } from '@/components/ui/button';
191
+ import { Input } from '@/components/ui/input';
192
+
193
+ /** Props for the ${componentName} molecule component. */
194
+ export interface ${componentName}Props {
195
+ /** Placeholder text for the input. */
196
+ placeholder?: string;
197
+ /** Current value (controlled). */
198
+ value?: string;
199
+ /** Default value (uncontrolled). */
200
+ defaultValue?: string;
201
+ /** Called when the value changes. */
202
+ onChange?: (value: string) => void;
203
+ /** Called when the user submits (Enter key). */
204
+ onSubmit?: (value: string) => void;
205
+ /** Called when the clear button is pressed. */
206
+ onClear?: () => void;
207
+ /** Whether the component is disabled. */
208
+ disabled?: boolean;
209
+ /** Additional CSS classes. */
210
+ className?: string;
211
+ }
212
+
213
+ /**
214
+ * \`${componentName}\` — a molecule-level component composing Input + Button atoms.
215
+ *
216
+ * Composes Radix-based atoms with Tailwind semantic tokens
217
+ * to create a search bar with clear and submit behaviours.
218
+ */
219
+ export const ${componentName} = forwardRef<HTMLDivElement, ${componentName}Props>(
220
+ (
221
+ {
222
+ placeholder = 'Search...',
223
+ value: controlledValue,
224
+ defaultValue = '',
225
+ onChange,
226
+ onSubmit,
227
+ onClear,
228
+ disabled = false,
229
+ className,
230
+ },
231
+ ref,
232
+ ) => {
233
+ const isControlled = controlledValue !== undefined;
234
+ const [internalValue, setInternalValue] = useState(defaultValue);
235
+ const value = isControlled ? controlledValue : internalValue;
236
+
237
+ const handleChange = useCallback(
238
+ (e: React.ChangeEvent<HTMLInputElement>) => {
239
+ const next = e.target.value;
240
+ if (!isControlled) setInternalValue(next);
241
+ onChange?.(next);
242
+ },
243
+ [isControlled, onChange],
244
+ );
245
+
246
+ const handleKeyDown = useCallback(
247
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
248
+ if (e.key === 'Enter') {
249
+ onSubmit?.(value);
250
+ }
251
+ if (e.key === 'Escape') {
252
+ onClear?.();
253
+ }
254
+ },
255
+ [value, onSubmit, onClear],
256
+ );
257
+
258
+ const handleClear = useCallback(() => {
259
+ if (!isControlled) setInternalValue('');
260
+ onChange?.('');
261
+ onClear?.();
262
+ }, [isControlled, onChange, onClear]);
263
+
264
+ return (
265
+ <div
266
+ ref={ref}
267
+ className={cn(
268
+ 'flex items-center gap-2 rounded-md border border-input bg-background px-3 py-2',
269
+ 'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
270
+ disabled && 'pointer-events-none opacity-50',
271
+ className,
272
+ )}
273
+ role="search"
274
+ >
275
+ <Search className="h-4 w-4 text-muted-foreground shrink-0" aria-hidden="true" />
276
+ <input
277
+ type="search"
278
+ value={value}
279
+ onChange={handleChange}
280
+ onKeyDown={handleKeyDown}
281
+ placeholder={placeholder}
282
+ disabled={disabled}
283
+ aria-label={placeholder}
284
+ className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
285
+ />
286
+ {value && (
287
+ <Button
288
+ variant="ghost"
289
+ size="sm"
290
+ onClick={handleClear}
291
+ aria-label="Clear search"
292
+ className="h-6 w-6 p-0"
293
+ >
294
+ <X className="h-3 w-3" />
295
+ </Button>
296
+ )}
297
+ </div>
298
+ );
299
+ },
300
+ );
301
+
302
+ ${componentName}.displayName = '${componentName}';
303
+ `,
304
+
305
+ story: `import type { Meta, StoryObj } from '@storybook/react';
306
+ import { ${componentName} } from './${camelName}';
307
+ import { userEvent, within } from '@storybook/test';
308
+
309
+ const meta: Meta<typeof ${componentName}> = {
310
+ title: 'Molecules/${componentName}',
311
+ component: ${componentName},
312
+ tags: ['autodocs'],
313
+ };
314
+ export default meta;
315
+ type Story = StoryObj<typeof ${componentName}>;
316
+
317
+ /** Default empty state. */
318
+ export const Default: Story = {
319
+ args: {
320
+ placeholder: 'Search ${name}...',
321
+ },
322
+ };
323
+
324
+ /** With initial value, clear button visible. */
325
+ export const WithValue: Story = {
326
+ args: {
327
+ placeholder: 'Search ${name}...',
328
+ defaultValue: 'example query',
329
+ },
330
+ };
331
+
332
+ /** Play function that types and submits. */
333
+ export const WithInteraction: Story = {
334
+ args: {
335
+ placeholder: 'Type to search...',
336
+ onSubmit: (value) => alert('Submitted: ' + value),
337
+ },
338
+ play: async ({ canvasElement }) => {
339
+ const canvas = within(canvasElement);
340
+ const input = canvas.getByRole('searchbox');
341
+ await userEvent.type(input, 'hello{Enter}');
342
+ },
343
+ };
344
+ `,
345
+
346
+ test: `import { test, expect } from '@playwright/test';
347
+ import { runAxe } from '@axe-core/playwright';
348
+
349
+ test.describe('${componentName}', () => {
350
+ test('renders with no a11y violations', async ({ page, mount }) => {
351
+ const component = await mount(
352
+ <${componentName} placeholder="Search items..." />
353
+ );
354
+ await expect(component).toBeVisible();
355
+ const results = await runAxe(page, component);
356
+ expect(results.violations).toEqual([]);
357
+ });
358
+
359
+ test('has accessible search role', async ({ page, mount }) => {
360
+ const component = await mount(
361
+ <${componentName} placeholder="Search items..." />
362
+ );
363
+ await expect(component).toHaveAttribute('role', 'search');
364
+ const input = component.locator('input[type="search"]');
365
+ await expect(input).toHaveAttribute('aria-label', 'Search items...');
366
+ });
367
+
368
+ test('clear button appears when value is set', async ({ page, mount }) => {
369
+ const component = await mount(
370
+ <${componentName} defaultValue="query" />
371
+ );
372
+ const clearBtn = component.locator('button[aria-label="Clear search"]');
373
+ await expect(clearBtn).toBeVisible();
374
+ });
375
+
376
+ test('calls onChange when typing', async ({ page, mount }) => {
377
+ let value = '';
378
+ const component = await mount(
379
+ <${componentName} onChange={(v) => (value = v)} />
380
+ );
381
+ const input = component.locator('input[type="search"]');
382
+ await input.fill('test');
383
+ expect(value).toBe('test');
384
+ });
385
+ });
386
+ `,
387
+ };
388
+ }
389
+
390
+ /**
391
+ * Organism component template — complex section (DataTable, WizardFlow, DashboardWidget).
392
+ */
393
+ export function organismTemplate(
394
+ name: string
395
+ ): { component: string; story: string; test: string } {
396
+ const componentName = capitalize(name);
397
+ const camelName = camelize(name);
398
+
399
+ return {
400
+ component: `import { useState, useCallback, forwardRef } from 'react';
401
+ import { cn } from '@/lib/utils';
402
+ import {
403
+ ChevronLeft,
404
+ ChevronRight,
405
+ ChevronsLeft,
406
+ ChevronsRight,
407
+ ArrowUpDown,
408
+ Loader2,
409
+ } from 'lucide-react';
410
+ import { Button } from '@/components/ui/button';
411
+ import { Badge } from '@/components/ui/badge';
412
+ import { EmptyState } from '@/components/state/EmptyState';
413
+ import { ErrorState } from '@/components/state/ErrorState';
414
+ import { LoadingState } from '@/components/state/LoadingState';
415
+
416
+ /** A single column definition for the ${componentName}. */
417
+ export interface ${componentName}Column<T> {
418
+ key: string;
419
+ header: string;
420
+ /** Render function for the cell. */
421
+ render: (item: T) => React.ReactNode;
422
+ /** Whether this column is sortable. */
423
+ sortable?: boolean;
424
+ /** CSS min-width for this column. */
425
+ width?: string;
426
+ }
427
+
428
+ /** Props for the ${componentName} organism component. */
429
+ export interface ${componentName}Props<T> {
430
+ /** Column definitions. */
431
+ columns: ${componentName}Column<T>[];
432
+ /** Row data array. */
433
+ data: T[];
434
+ /** Loading state — shows skeleton rows. */
435
+ isLoading?: boolean;
436
+ /** Error state — shows error message with retry. */
437
+ isError?: boolean;
438
+ /** Error message when isError is true. */
439
+ errorMessage?: string;
440
+ /** Retry callback when isError is true. */
441
+ onRetry?: () => void;
442
+ /** Click handler for a row. */
443
+ onRowClick?: (item: T) => void;
444
+ /** Whether the table is keyboard-navigable. */
445
+ keyboardSelectable?: boolean;
446
+ /** Items per page (default: 25). */
447
+ pageSize?: number;
448
+ /** Additional CSS classes. */
449
+ className?: string;
450
+ }
451
+
452
+ /**
453
+ * \`${componentName}\` — an organism-level component composing molecule and atom primitives.
454
+ *
455
+ * A data table with column sorting, keyboard navigation, loading/empty/error
456
+ * states, and pagination. Uses Tailwind semantic tokens and follows the
457
+ * Design System spacing and typography conventions.
458
+ */
459
+ export function ${componentName}<T extends { id: string }>({
460
+ columns,
461
+ data,
462
+ isLoading = false,
463
+ isError = false,
464
+ errorMessage = 'Failed to load data',
465
+ onRetry,
466
+ onRowClick,
467
+ keyboardSelectable = true,
468
+ pageSize = 25,
469
+ className,
470
+ }: ${componentName}Props<T>) {
471
+ const [sortKey, setSortKey] = useState<string | null>(null);
472
+ const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
473
+ const [page, setPage] = useState(0);
474
+ const [selectedIdx, setSelectedIdx] = useState<number>(-1);
475
+
476
+ const sorted = sortKey
477
+ ? [...data].sort((a, b) => {
478
+ const col = columns.find((c) => c.key === sortKey);
479
+ if (!col) return 0;
480
+ const aVal = String(col.render(a));
481
+ const bVal = String(col.render(b));
482
+ const cmp = aVal.localeCompare(bVal);
483
+ return sortDir === 'asc' ? cmp : -cmp;
484
+ })
485
+ : data;
486
+
487
+ const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
488
+ const paged = sorted.slice(page * pageSize, (page + 1) * pageSize);
489
+
490
+ const handleSort = useCallback(
491
+ (key: string) => {
492
+ if (sortKey === key) {
493
+ setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
494
+ } else {
495
+ setSortKey(key);
496
+ setSortDir('asc');
497
+ }
498
+ },
499
+ [sortKey],
500
+ );
501
+
502
+ const handleKeyDown = useCallback(
503
+ (e: React.KeyboardEvent) => {
504
+ if (!keyboardSelectable) return;
505
+ switch (e.key) {
506
+ case 'ArrowDown':
507
+ e.preventDefault();
508
+ setSelectedIdx((i) => Math.min(i + 1, paged.length - 1));
509
+ break;
510
+ case 'ArrowUp':
511
+ e.preventDefault();
512
+ setSelectedIdx((i) => Math.max(i - 1, 0));
513
+ break;
514
+ case 'Enter':
515
+ if (selectedIdx >= 0 && onRowClick) {
516
+ onRowClick(paged[selectedIdx]);
517
+ }
518
+ break;
519
+ }
520
+ },
521
+ [keyboardSelectable, paged, selectedIdx, onRowClick],
522
+ );
523
+
524
+ if (isLoading) {
525
+ return <LoadingState variant="row" rows={5} />;
526
+ }
527
+
528
+ if (isError) {
529
+ return <ErrorState message={errorMessage} onRetry={onRetry} />;
530
+ }
531
+
532
+ if (!data.length) {
533
+ return <EmptyState icon={ChevronsLeft} title="No data" description="No items to display." />;
534
+ }
535
+
536
+ return (
537
+ <div className={cn('space-y-4', className)}>
538
+ <div className="overflow-x-auto rounded-lg border border-border">
539
+ <table
540
+ className="w-full"
541
+ role="grid"
542
+ aria-label="${componentName}"
543
+ onKeyDown={handleKeyDown}
544
+ >
545
+ <thead>
546
+ <tr className="border-b border-border bg-muted/50">
547
+ {columns.map((col) => (
548
+ <th
549
+ key={col.key}
550
+ className={cn(
551
+ 'px-4 py-3 text-left text-xs font-medium text-muted-foreground',
552
+ col.sortable && 'cursor-pointer select-none hover:text-foreground',
553
+ col.width && 'min-w-[var(--col-width)]',
554
+ )}
555
+ style={{ minWidth: col.width }}
556
+ onClick={col.sortable ? () => handleSort(col.key) : undefined}
557
+ aria-sort={
558
+ sortKey === col.key
559
+ ? sortDir === 'asc'
560
+ ? 'ascending'
561
+ : 'descending'
562
+ : undefined
563
+ }
564
+ >
565
+ <span className="inline-flex items-center gap-1">
566
+ {col.header}
567
+ {col.sortable && <ArrowUpDown className="h-3 w-3" />}
568
+ {sortKey === col.key && (
569
+ <Badge variant="info" className="ml-1 h-4 px-1 text-[10px]">
570
+ {sortDir === 'asc' ? 'ASC' : 'DESC'}
571
+ </Badge>
572
+ )}
573
+ </span>
574
+ </th>
575
+ ))}
576
+ </tr>
577
+ </thead>
578
+ <tbody>
579
+ {paged.map((item, idx) => (
580
+ <tr
581
+ key={item.id}
582
+ className={cn(
583
+ 'border-b border-border transition-colors duration-150',
584
+ onRowClick && 'cursor-pointer hover:bg-muted/50',
585
+ selectedIdx === idx && 'bg-accent',
586
+ )}
587
+ onClick={() => onRowClick?.(item)}
588
+ tabIndex={keyboardSelectable ? 0 : undefined}
589
+ role="row"
590
+ aria-selected={selectedIdx === idx}
591
+ >
592
+ {columns.map((col) => (
593
+ <td key={col.key} className="px-4 py-3 text-sm">
594
+ {col.render(item)}
595
+ </td>
596
+ ))}
597
+ </tr>
598
+ ))}
599
+ </tbody>
600
+ </table>
601
+ </div>
602
+
603
+ {/* Pagination */}
604
+ {totalPages > 1 && (
605
+ <div className="flex items-center justify-between text-sm">
606
+ <p className="text-muted-foreground">
607
+ Showing {page * pageSize + 1}–{Math.min((page + 1) * pageSize, sorted.length)} of{' '}
608
+ {sorted.length}
609
+ </p>
610
+ <div className="flex items-center gap-1">
611
+ <Button
612
+ variant="ghost"
613
+ size="sm"
614
+ onClick={() => setPage(0)}
615
+ disabled={page === 0}
616
+ aria-label="First page"
617
+ >
618
+ <ChevronsLeft className="h-4 w-4" />
619
+ </Button>
620
+ <Button
621
+ variant="ghost"
622
+ size="sm"
623
+ onClick={() => setPage((p) => Math.max(0, p - 1))}
624
+ disabled={page === 0}
625
+ aria-label="Previous page"
626
+ >
627
+ <ChevronLeft className="h-4 w-4" />
628
+ </Button>
629
+ <span className="px-2 text-muted-foreground">
630
+ {page + 1} / {totalPages}
631
+ </span>
632
+ <Button
633
+ variant="ghost"
634
+ size="sm"
635
+ onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
636
+ disabled={page >= totalPages - 1}
637
+ aria-label="Next page"
638
+ >
639
+ <ChevronRight className="h-4 w-4" />
640
+ </Button>
641
+ <Button
642
+ variant="ghost"
643
+ size="sm"
644
+ onClick={() => setPage(totalPages - 1)}
645
+ disabled={page >= totalPages - 1}
646
+ aria-label="Last page"
647
+ >
648
+ <ChevronsRight className="h-4 w-4" />
649
+ </Button>
650
+ </div>
651
+ </div>
652
+ )}
653
+ </div>
654
+ );
655
+ }
656
+
657
+ ${componentName}.displayName = '${componentName}';
658
+ `,
659
+
660
+ story: `import type { Meta, StoryObj } from '@storybook/react';
661
+ import { ${componentName}, type ${componentName}Column } from './${camelName}';
662
+ import { Badge } from '@/components/ui/badge';
663
+
664
+ interface ExampleItem {
665
+ id: string;
666
+ name: string;
667
+ status: string;
668
+ date: string;
669
+ }
670
+
671
+ const COLUMNS: ${componentName}Column<ExampleItem>[] = [
672
+ { key: 'name', header: 'Name', render: (i) => i.name, sortable: true },
673
+ {
674
+ key: 'status',
675
+ header: 'Status',
676
+ render: (i) => (
677
+ <Badge variant={i.status === 'active' ? 'success' : 'neutral'}>
678
+ {i.status}
679
+ </Badge>
680
+ ),
681
+ sortable: true,
682
+ },
683
+ {
684
+ key: 'date',
685
+ header: 'Date',
686
+ render: (i) => new Date(i.date).toLocaleDateString(),
687
+ sortable: true,
688
+ },
689
+ ];
690
+
691
+ const ITEMS: ExampleItem[] = Array.from({ length: 8 }, (_, i) => ({
692
+ id: String(i + 1),
693
+ name: \`Item \${i + 1}\`,
694
+ status: i % 3 === 0 ? 'inactive' : 'active',
695
+ date: new Date(2026, 4, i + 1).toISOString(),
696
+ }));
697
+
698
+ const meta: Meta<typeof ${componentName}> = {
699
+ title: 'Organisms/${componentName}',
700
+ component: ${componentName},
701
+ tags: ['autodocs'],
702
+ };
703
+ export default meta;
704
+ type Story = StoryObj<typeof ${componentName}<ExampleItem>>;
705
+
706
+ /** Default with 8 rows. */
707
+ export const Default: Story = {
708
+ args: {
709
+ columns: COLUMNS,
710
+ data: ITEMS,
711
+ },
712
+ };
713
+
714
+ /** Empty state — no data rows. */
715
+ export const Empty: Story = {
716
+ args: {
717
+ columns: COLUMNS,
718
+ data: [],
719
+ },
720
+ };
721
+
722
+ /** Loading state — shimmer rows. */
723
+ export const Loading: Story = {
724
+ args: {
725
+ columns: COLUMNS,
726
+ data: [],
727
+ isLoading: true,
728
+ },
729
+ };
730
+
731
+ /** Error state with retry. */
732
+ export const Error_: Story = {
733
+ name: 'Error',
734
+ args: {
735
+ columns: COLUMNS,
736
+ data: [],
737
+ isError: true,
738
+ errorMessage: 'Network request failed. Please try again.',
739
+ onRetry: () => alert('Retry clicked'),
740
+ },
741
+ };
742
+ `,
743
+
744
+ test: `import { test, expect } from '@playwright/test';
745
+ import { runAxe } from '@axe-core/playwright';
746
+
747
+ interface TestItem {
748
+ id: string;
749
+ name: string;
750
+ status: string;
751
+ date: string;
752
+ }
753
+
754
+ const COLUMNS = [
755
+ { key: 'name', header: 'Name', render: (i: TestItem) => i.name },
756
+ { key: 'status', header: 'Status', render: (i: TestItem) => i.status },
757
+ ];
758
+
759
+ const DATA: TestItem[] = [
760
+ { id: '1', name: 'Alpha', status: 'active', date: '2026-05-01' },
761
+ { id: '2', name: 'Beta', status: 'inactive', date: '2026-05-02' },
762
+ { id: '3', name: 'Gamma', status: 'active', date: '2026-05-03' },
763
+ ];
764
+
765
+ test.describe('${componentName}', () => {
766
+ test('renders rows with no a11y violations', async ({ page, mount }) => {
767
+ const component = await mount(
768
+ <${componentName} columns={COLUMNS} data={DATA} />
769
+ );
770
+ await expect(component).toBeVisible();
771
+ const results = await runAxe(page, component);
772
+ expect(results.violations).toEqual([]);
773
+ });
774
+
775
+ test('has accessible grid role', async ({ page, mount }) => {
776
+ const component = await mount(
777
+ <${componentName} columns={COLUMNS} data={DATA} />
778
+ );
779
+ await expect(component.locator('table')).toHaveAttribute('role', 'grid');
780
+ });
781
+
782
+ test('shows empty state when no data', async ({ page, mount }) => {
783
+ const component = await mount(
784
+ <${componentName} columns={COLUMNS} data={[]} />
785
+ );
786
+ await expect(component).toContainText('No data');
787
+ });
788
+
789
+ test('shows loading state', async ({ page, mount }) => {
790
+ const component = await mount(
791
+ <${componentName} columns={COLUMNS} data={[]} isLoading />
792
+ );
793
+ await expect(component).not.toContainText('No data'); // loading takes priority
794
+ });
795
+
796
+ test('shows error state', async ({ page, mount }) => {
797
+ const component = await mount(
798
+ <${componentName}
799
+ columns={COLUMNS}
800
+ data={[]}
801
+ isError
802
+ errorMessage="Boom"
803
+ onRetry={() => {}}
804
+ />
805
+ );
806
+ await expect(component).toContainText('Boom');
807
+ });
808
+
809
+ test('keyboard navigation moves selection', async ({ page, mount }) => {
810
+ const component = await mount(
811
+ <${componentName} columns={COLUMNS} data={DATA} keyboardSelectable />
812
+ );
813
+ await component.locator('table').focus();
814
+ await page.keyboard.press('ArrowDown');
815
+ const secondRow = component.locator('tr[aria-selected="true"]');
816
+ await expect(secondRow).toBeVisible();
817
+ });
818
+ });
819
+ `,
820
+ };
821
+ }
822
+
823
+ /** Capitalizes the first character of a string. */
824
+ function capitalize(s: string): string {
825
+ return s.charAt(0).toUpperCase() + s.slice(1);
826
+ }
827
+
828
+ /** Converts PascalCase/kebab-case to camelCase. */
829
+ function camelize(s: string): string {
830
+ return s
831
+ .replace(/[-_](.)/g, (_, c) => c.toUpperCase())
832
+ .replace(/^[A-Z]/, (c) => c.toLowerCase());
833
+ }