create-fluxstack 1.10.1 → 1.12.1

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 (257) hide show
  1. package/.dockerignore +1 -2
  2. package/Dockerfile +8 -8
  3. package/LLMD/INDEX.md +64 -0
  4. package/LLMD/MAINTENANCE.md +197 -0
  5. package/LLMD/MIGRATION.md +156 -0
  6. package/LLMD/config/.gitkeep +1 -0
  7. package/LLMD/config/declarative-system.md +268 -0
  8. package/LLMD/config/environment-vars.md +327 -0
  9. package/LLMD/config/runtime-reload.md +401 -0
  10. package/LLMD/core/.gitkeep +1 -0
  11. package/LLMD/core/build-system.md +599 -0
  12. package/LLMD/core/framework-lifecycle.md +229 -0
  13. package/LLMD/core/plugin-system.md +451 -0
  14. package/LLMD/patterns/.gitkeep +1 -0
  15. package/LLMD/patterns/anti-patterns.md +297 -0
  16. package/LLMD/patterns/project-structure.md +264 -0
  17. package/LLMD/patterns/type-safety.md +440 -0
  18. package/LLMD/reference/.gitkeep +1 -0
  19. package/LLMD/reference/cli-commands.md +250 -0
  20. package/LLMD/reference/plugin-hooks.md +357 -0
  21. package/LLMD/reference/routing.md +39 -0
  22. package/LLMD/reference/troubleshooting.md +364 -0
  23. package/LLMD/resources/.gitkeep +1 -0
  24. package/LLMD/resources/controllers.md +465 -0
  25. package/LLMD/resources/live-components.md +703 -0
  26. package/LLMD/resources/live-rooms.md +482 -0
  27. package/LLMD/resources/live-upload.md +130 -0
  28. package/LLMD/resources/plugins-external.md +617 -0
  29. package/LLMD/resources/routes-eden.md +254 -0
  30. package/README.md +37 -17
  31. package/app/client/index.html +0 -1
  32. package/app/client/src/App.tsx +107 -150
  33. package/app/client/src/components/AppLayout.tsx +68 -0
  34. package/app/client/src/components/BackButton.tsx +13 -0
  35. package/app/client/src/components/DemoPage.tsx +20 -0
  36. package/app/client/src/components/LiveUploadWidget.tsx +204 -0
  37. package/app/client/src/lib/eden-api.ts +85 -60
  38. package/app/client/src/live/ChatDemo.tsx +107 -0
  39. package/app/client/src/live/CounterDemo.tsx +206 -0
  40. package/app/client/src/live/FormDemo.tsx +119 -0
  41. package/app/client/src/live/RoomChatDemo.tsx +161 -0
  42. package/app/client/src/live/UploadDemo.tsx +21 -0
  43. package/app/client/src/main.tsx +4 -1
  44. package/app/client/src/pages/ApiTestPage.tsx +108 -0
  45. package/app/client/src/pages/HomePage.tsx +76 -0
  46. package/app/server/app.ts +1 -4
  47. package/app/server/controllers/users.controller.ts +36 -44
  48. package/app/server/index.ts +25 -35
  49. package/app/server/live/LiveChat.ts +77 -0
  50. package/app/server/live/LiveCounter.ts +67 -0
  51. package/app/server/live/LiveForm.ts +63 -0
  52. package/app/server/live/LiveLocalCounter.ts +32 -0
  53. package/app/server/live/LiveRoomChat.ts +127 -0
  54. package/app/server/live/LiveUpload.ts +81 -0
  55. package/app/server/routes/index.ts +3 -1
  56. package/app/server/routes/room.routes.ts +117 -0
  57. package/app/server/routes/users.routes.ts +35 -27
  58. package/app/shared/types/index.ts +14 -2
  59. package/config/app.config.ts +2 -62
  60. package/config/client.config.ts +2 -95
  61. package/config/database.config.ts +2 -99
  62. package/config/fluxstack.config.ts +25 -45
  63. package/config/index.ts +57 -38
  64. package/config/monitoring.config.ts +2 -114
  65. package/config/plugins.config.ts +2 -80
  66. package/config/server.config.ts +2 -68
  67. package/config/services.config.ts +2 -130
  68. package/config/system/app.config.ts +29 -0
  69. package/config/system/build.config.ts +49 -0
  70. package/config/system/client.config.ts +68 -0
  71. package/config/system/database.config.ts +17 -0
  72. package/config/system/fluxstack.config.ts +114 -0
  73. package/config/{logger.config.ts → system/logger.config.ts} +3 -1
  74. package/config/system/monitoring.config.ts +114 -0
  75. package/config/system/plugins.config.ts +84 -0
  76. package/config/{runtime.config.ts → system/runtime.config.ts} +1 -1
  77. package/config/system/server.config.ts +68 -0
  78. package/config/system/services.config.ts +46 -0
  79. package/config/{system.config.ts → system/system.config.ts} +1 -1
  80. package/core/build/flux-plugins-generator.ts +325 -325
  81. package/core/build/index.ts +39 -27
  82. package/core/build/live-components-generator.ts +3 -3
  83. package/core/build/optimizer.ts +235 -235
  84. package/core/cli/command-registry.ts +6 -4
  85. package/core/cli/commands/build.ts +79 -0
  86. package/core/cli/commands/create.ts +54 -0
  87. package/core/cli/commands/dev.ts +101 -0
  88. package/core/cli/commands/help.ts +34 -0
  89. package/core/cli/commands/index.ts +34 -0
  90. package/core/cli/commands/make-plugin.ts +90 -0
  91. package/core/cli/commands/plugin-add.ts +197 -0
  92. package/core/cli/commands/plugin-deps.ts +2 -2
  93. package/core/cli/commands/plugin-list.ts +208 -0
  94. package/core/cli/commands/plugin-remove.ts +170 -0
  95. package/core/cli/generators/component.ts +769 -769
  96. package/core/cli/generators/controller.ts +1 -1
  97. package/core/cli/generators/index.ts +146 -146
  98. package/core/cli/generators/interactive.ts +227 -227
  99. package/core/cli/generators/plugin.ts +2 -2
  100. package/core/cli/generators/prompts.ts +82 -82
  101. package/core/cli/generators/route.ts +6 -6
  102. package/core/cli/generators/service.ts +2 -2
  103. package/core/cli/generators/template-engine.ts +4 -3
  104. package/core/cli/generators/types.ts +2 -2
  105. package/core/cli/generators/utils.ts +191 -191
  106. package/core/cli/index.ts +115 -686
  107. package/core/cli/plugin-discovery.ts +2 -2
  108. package/core/client/LiveComponentsProvider.tsx +60 -8
  109. package/core/client/api/eden.ts +183 -0
  110. package/core/client/api/index.ts +11 -0
  111. package/core/client/components/Live.tsx +104 -0
  112. package/core/client/fluxstack.ts +1 -9
  113. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
  114. package/core/client/hooks/state-validator.ts +1 -1
  115. package/core/client/hooks/useAuth.ts +48 -48
  116. package/core/client/hooks/useChunkedUpload.ts +85 -35
  117. package/core/client/hooks/useLiveChunkedUpload.ts +87 -0
  118. package/core/client/hooks/useLiveComponent.ts +800 -0
  119. package/core/client/hooks/useLiveUpload.ts +71 -0
  120. package/core/client/hooks/useRoom.ts +409 -0
  121. package/core/client/hooks/useRoomProxy.ts +382 -0
  122. package/core/client/index.ts +17 -68
  123. package/core/client/standalone-entry.ts +8 -0
  124. package/core/client/standalone.ts +74 -53
  125. package/core/client/state/createStore.ts +192 -192
  126. package/core/client/state/index.ts +14 -14
  127. package/core/config/index.ts +70 -291
  128. package/core/config/schema.ts +42 -723
  129. package/core/framework/client.ts +131 -131
  130. package/core/framework/index.ts +7 -7
  131. package/core/framework/server.ts +47 -40
  132. package/core/framework/types.ts +2 -2
  133. package/core/index.ts +23 -4
  134. package/core/live/ComponentRegistry.ts +3 -3
  135. package/core/live/types.ts +77 -0
  136. package/core/plugins/built-in/index.ts +134 -134
  137. package/core/plugins/built-in/live-components/commands/create-live-component.ts +242 -1066
  138. package/core/plugins/built-in/live-components/index.ts +1 -1
  139. package/core/plugins/built-in/monitoring/index.ts +111 -47
  140. package/core/plugins/built-in/static/index.ts +1 -1
  141. package/core/plugins/built-in/swagger/index.ts +68 -265
  142. package/core/plugins/built-in/vite/index.ts +85 -185
  143. package/core/plugins/built-in/vite/vite-dev.ts +10 -16
  144. package/core/plugins/config.ts +9 -7
  145. package/core/plugins/dependency-manager.ts +31 -1
  146. package/core/plugins/discovery.ts +19 -7
  147. package/core/plugins/executor.ts +2 -2
  148. package/core/plugins/index.ts +203 -203
  149. package/core/plugins/manager.ts +27 -39
  150. package/core/plugins/module-resolver.ts +19 -8
  151. package/core/plugins/registry.ts +255 -19
  152. package/core/plugins/types.ts +20 -53
  153. package/core/server/framework.ts +66 -43
  154. package/core/server/index.ts +15 -15
  155. package/core/server/live/ComponentRegistry.ts +78 -71
  156. package/core/server/live/FileUploadManager.ts +23 -10
  157. package/core/server/live/LiveComponentPerformanceMonitor.ts +1 -1
  158. package/core/server/live/LiveRoomManager.ts +261 -0
  159. package/core/server/live/RoomEventBus.ts +234 -0
  160. package/core/server/live/RoomStateManager.ts +172 -0
  161. package/core/server/live/StateSignature.ts +643 -643
  162. package/core/server/live/WebSocketConnectionManager.ts +30 -19
  163. package/core/server/live/auto-generated-components.ts +21 -9
  164. package/core/server/live/index.ts +14 -0
  165. package/core/server/live/websocket-plugin.ts +214 -67
  166. package/core/server/middleware/elysia-helpers.ts +7 -2
  167. package/core/server/middleware/errorHandling.ts +1 -1
  168. package/core/server/middleware/index.ts +31 -31
  169. package/core/server/plugins/database.ts +180 -180
  170. package/core/server/plugins/static-files-plugin.ts +69 -69
  171. package/core/server/plugins/swagger.ts +1 -1
  172. package/core/server/rooms/RoomBroadcaster.ts +357 -0
  173. package/core/server/rooms/RoomSystem.ts +463 -0
  174. package/core/server/rooms/index.ts +13 -0
  175. package/core/server/services/BaseService.ts +1 -1
  176. package/core/server/services/ServiceContainer.ts +1 -1
  177. package/core/server/services/index.ts +8 -8
  178. package/core/templates/create-project.ts +12 -12
  179. package/core/testing/index.ts +9 -9
  180. package/core/testing/setup.ts +73 -73
  181. package/core/types/api.ts +168 -168
  182. package/core/types/build.ts +219 -219
  183. package/core/types/config.ts +56 -26
  184. package/core/types/index.ts +4 -4
  185. package/core/types/plugin.ts +107 -107
  186. package/core/types/types.ts +353 -14
  187. package/core/utils/build-logger.ts +324 -324
  188. package/core/utils/config-schema.ts +480 -480
  189. package/core/utils/env.ts +2 -8
  190. package/core/utils/errors/codes.ts +114 -114
  191. package/core/utils/errors/handlers.ts +36 -1
  192. package/core/utils/errors/index.ts +49 -5
  193. package/core/utils/errors/middleware.ts +113 -113
  194. package/core/utils/helpers.ts +6 -16
  195. package/core/utils/index.ts +17 -17
  196. package/core/utils/logger/colors.ts +114 -114
  197. package/core/utils/logger/config.ts +13 -9
  198. package/core/utils/logger/formatter.ts +82 -82
  199. package/core/utils/logger/group-logger.ts +101 -101
  200. package/core/utils/logger/index.ts +6 -1
  201. package/core/utils/logger/stack-trace.ts +3 -1
  202. package/core/utils/logger/startup-banner.ts +82 -82
  203. package/core/utils/logger/winston-logger.ts +152 -152
  204. package/core/utils/monitoring/index.ts +211 -211
  205. package/core/utils/sync-version.ts +66 -66
  206. package/core/utils/version.ts +1 -1
  207. package/create-fluxstack.ts +8 -7
  208. package/package.json +12 -13
  209. package/plugins/crypto-auth/cli/make-protected-route.command.ts +1 -1
  210. package/plugins/crypto-auth/client/CryptoAuthClient.ts +302 -302
  211. package/plugins/crypto-auth/client/components/index.ts +11 -11
  212. package/plugins/crypto-auth/client/index.ts +11 -11
  213. package/plugins/crypto-auth/config/index.ts +1 -1
  214. package/plugins/crypto-auth/index.ts +4 -4
  215. package/plugins/crypto-auth/package.json +65 -65
  216. package/plugins/crypto-auth/server/AuthMiddleware.ts +1 -1
  217. package/plugins/crypto-auth/server/CryptoAuthService.ts +185 -185
  218. package/plugins/crypto-auth/server/index.ts +21 -21
  219. package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +3 -3
  220. package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +1 -1
  221. package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +2 -2
  222. package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +2 -2
  223. package/plugins/crypto-auth/server/middlewares/helpers.ts +1 -1
  224. package/plugins/crypto-auth/server/middlewares/index.ts +22 -22
  225. package/tsconfig.api-strict.json +16 -0
  226. package/tsconfig.json +48 -52
  227. package/{app/client/tsconfig.node.json → tsconfig.node.json} +25 -25
  228. package/types/global.d.ts +29 -29
  229. package/types/vitest.d.ts +8 -8
  230. package/vite.config.ts +38 -62
  231. package/vitest.config.live.ts +10 -9
  232. package/vitest.config.ts +29 -17
  233. package/app/client/README.md +0 -69
  234. package/app/client/SIMPLIFICATION.md +0 -140
  235. package/app/client/frontend-only.ts +0 -12
  236. package/app/client/src/live/FileUploadExample.tsx +0 -359
  237. package/app/client/src/live/MinimalLiveClock.tsx +0 -47
  238. package/app/client/src/live/QuickUploadTest.tsx +0 -193
  239. package/app/client/tsconfig.app.json +0 -45
  240. package/app/client/tsconfig.json +0 -7
  241. package/app/client/zustand-setup.md +0 -65
  242. package/app/server/backend-only.ts +0 -18
  243. package/app/server/live/LiveClockComponent.ts +0 -215
  244. package/app/server/live/LiveFileUploadComponent.ts +0 -77
  245. package/app/server/routes/env-test.ts +0 -110
  246. package/core/client/hooks/index.ts +0 -7
  247. package/core/client/hooks/useHybridLiveComponent.ts +0 -685
  248. package/core/client/hooks/useTypedLiveComponent.ts +0 -133
  249. package/core/client/hooks/useWebSocket.ts +0 -361
  250. package/core/config/env.ts +0 -546
  251. package/core/config/loader.ts +0 -522
  252. package/core/config/runtime-config.ts +0 -327
  253. package/core/config/validator.ts +0 -540
  254. package/core/server/backend-entry.ts +0 -51
  255. package/core/server/standalone.ts +0 -106
  256. package/core/utils/regenerate-files.ts +0 -69
  257. package/fluxstack.config.ts +0 -354
@@ -1,770 +1,770 @@
1
- import type { Generator } from "./index"
2
- import type { GeneratorContext, GeneratorOptions, Template } from "./types"
3
- import { templateEngine } from "./template-engine"
4
-
5
- export class ComponentGenerator implements Generator {
6
- name = 'component'
7
- description = 'Generate a new React component'
8
-
9
- async generate(context: GeneratorContext, options: GeneratorOptions): Promise<void> {
10
- const template = this.getTemplate(options.template)
11
-
12
- if (template.hooks?.beforeGenerate) {
13
- await template.hooks.beforeGenerate(context, options)
14
- }
15
-
16
- const files = await templateEngine.processTemplate(template, context, options)
17
-
18
- if (options.dryRun) {
19
- console.log(`\n📋 Would generate component '${options.name}':\n`)
20
- for (const file of files) {
21
- console.log(`${file.action === 'create' ? '📄' : '✏️'} ${file.path}`)
22
- }
23
- return
24
- }
25
-
26
- await templateEngine.generateFiles(files, options.dryRun)
27
-
28
- if (template.hooks?.afterGenerate) {
29
- const filePaths = files.map(f => f.path)
30
- await template.hooks.afterGenerate(context, options, filePaths)
31
- }
32
-
33
- console.log(`\n✅ Generated component '${options.name}' with ${files.length} files`)
34
- }
35
-
36
- private getTemplate(templateName?: string): Template {
37
- switch (templateName) {
38
- case 'functional':
39
- return this.getFunctionalTemplate()
40
- case 'page':
41
- return this.getPageTemplate()
42
- case 'form':
43
- return this.getFormTemplate()
44
- case 'full':
45
- return this.getFullTemplate()
46
- default:
47
- return this.getBasicTemplate()
48
- }
49
- }
50
-
51
- private getBasicTemplate(): Template {
52
- return {
53
- name: 'basic-component',
54
- description: 'Basic React component with TypeScript',
55
- files: [
56
- {
57
- path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.tsx',
58
- content: `import React from 'react'
59
- import './{{pascalName}}.css'
60
-
61
- export interface {{pascalName}}Props {
62
- className?: string
63
- children?: React.ReactNode
64
- }
65
-
66
- export const {{pascalName}}: React.FC<{{pascalName}}Props> = ({
67
- className = '',
68
- children,
69
- ...props
70
- }) => {
71
- return (
72
- <div className={\`{{kebabName}} \${className}\`.trim()} {...props}>
73
- <h2>{{pascalName}} Component</h2>
74
- {children}
75
- </div>
76
- )
77
- }
78
-
79
- export default {{pascalName}}
80
- `
81
- },
82
- {
83
- path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.css',
84
- content: `.{{kebabName}} {
85
- /* Add your styles here */
86
- padding: 1rem;
87
- border: 1px solid #e2e8f0;
88
- border-radius: 0.5rem;
89
- background-color: #ffffff;
90
- }
91
-
92
- .{{kebabName}} h2 {
93
- margin: 0 0 1rem 0;
94
- font-size: 1.25rem;
95
- font-weight: 600;
96
- color: #1a202c;
97
- }
98
-
99
- /* Responsive styles */
100
- @media (max-width: 768px) {
101
- .{{kebabName}} {
102
- padding: 0.75rem;
103
- }
104
-
105
- .{{kebabName}} h2 {
106
- font-size: 1.125rem;
107
- }
108
- }
109
- `
110
- },
111
- {
112
- path: 'app/client/src/components/{{pascalName}}/index.ts',
113
- content: `export { {{pascalName}}, type {{pascalName}}Props } from './{{pascalName}}'
114
- export { default } from './{{pascalName}}'
115
- `
116
- }
117
- ],
118
- hooks: {
119
- afterGenerate: async (context, options, files) => {
120
- context.logger.info(`Generated component files:`)
121
- files.forEach(file => {
122
- context.logger.info(` - ${file}`)
123
- })
124
- context.logger.info(`\nUsage example:`)
125
- context.logger.info(`import { ${options.name} } from './components/${options.name}'`)
126
- context.logger.info(`\n<${options.name}>Content here</${options.name}>`)
127
- }
128
- }
129
- }
130
- }
131
-
132
- private getFunctionalTemplate(): Template {
133
- return {
134
- name: 'functional-component',
135
- description: 'Functional component with hooks',
136
- files: [
137
- {
138
- path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.tsx',
139
- content: `import React, { useState, useEffect } from 'react'
140
- import './{{pascalName}}.css'
141
-
142
- export interface {{pascalName}}Props {
143
- className?: string
144
- onAction?: (data: any) => void
145
- }
146
-
147
- export const {{pascalName}}: React.FC<{{pascalName}}Props> = ({
148
- className = '',
149
- onAction,
150
- ...props
151
- }) => {
152
- const [loading, setLoading] = useState(false)
153
- const [data, setData] = useState<any>(null)
154
-
155
- useEffect(() => {
156
- // Component initialization logic
157
- console.log('{{pascalName}} mounted')
158
-
159
- return () => {
160
- // Cleanup logic
161
- console.log('{{pascalName}} unmounted')
162
- }
163
- }, [])
164
-
165
- const handleAction = async () => {
166
- setLoading(true)
167
- try {
168
- // Simulate async operation
169
- await new Promise(resolve => setTimeout(resolve, 1000))
170
- const result = { message: 'Action completed', timestamp: new Date() }
171
- setData(result)
172
- onAction?.(result)
173
- } catch (error) {
174
- console.error('Action failed:', error)
175
- } finally {
176
- setLoading(false)
177
- }
178
- }
179
-
180
- return (
181
- <div className={\`{{kebabName}} \${className}\`.trim()} {...props}>
182
- <h2>{{pascalName}}</h2>
183
-
184
- <div className="{{kebabName}}__content">
185
- {data && (
186
- <div className="{{kebabName}}__data">
187
- <p>Last action: {data.message}</p>
188
- <small>{data.timestamp?.toLocaleString()}</small>
189
- </div>
190
- )}
191
-
192
- <button
193
- className="{{kebabName}}__button"
194
- onClick={handleAction}
195
- disabled={loading}
196
- >
197
- {loading ? 'Loading...' : 'Perform Action'}
198
- </button>
199
- </div>
200
- </div>
201
- )
202
- }
203
-
204
- export default {{pascalName}}
205
- `
206
- },
207
- {
208
- path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.css',
209
- content: `.{{kebabName}} {
210
- padding: 1.5rem;
211
- border: 1px solid #e2e8f0;
212
- border-radius: 0.75rem;
213
- background-color: #ffffff;
214
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
215
- }
216
-
217
- .{{kebabName}} h2 {
218
- margin: 0 0 1rem 0;
219
- font-size: 1.5rem;
220
- font-weight: 600;
221
- color: #2d3748;
222
- }
223
-
224
- .{{kebabName}}__content {
225
- display: flex;
226
- flex-direction: column;
227
- gap: 1rem;
228
- }
229
-
230
- .{{kebabName}}__data {
231
- padding: 1rem;
232
- background-color: #f7fafc;
233
- border-radius: 0.5rem;
234
- border-left: 4px solid #4299e1;
235
- }
236
-
237
- .{{kebabName}}__data p {
238
- margin: 0 0 0.5rem 0;
239
- font-weight: 500;
240
- color: #2d3748;
241
- }
242
-
243
- .{{kebabName}}__data small {
244
- color: #718096;
245
- }
246
-
247
- .{{kebabName}}__button {
248
- padding: 0.75rem 1.5rem;
249
- background-color: #4299e1;
250
- color: white;
251
- border: none;
252
- border-radius: 0.5rem;
253
- font-weight: 500;
254
- cursor: pointer;
255
- transition: background-color 0.2s;
256
- }
257
-
258
- .{{kebabName}}__button:hover:not(:disabled) {
259
- background-color: #3182ce;
260
- }
261
-
262
- .{{kebabName}}__button:disabled {
263
- background-color: #a0aec0;
264
- cursor: not-allowed;
265
- }
266
-
267
- /* Responsive styles */
268
- @media (max-width: 768px) {
269
- .{{kebabName}} {
270
- padding: 1rem;
271
- }
272
-
273
- .{{kebabName}} h2 {
274
- font-size: 1.25rem;
275
- }
276
- }
277
- `
278
- },
279
- {
280
- path: 'app/client/src/components/{{pascalName}}/index.ts',
281
- content: `export { {{pascalName}}, type {{pascalName}}Props } from './{{pascalName}}'
282
- export { default } from './{{pascalName}}'
283
- `
284
- }
285
- ]
286
- }
287
- }
288
-
289
- private getPageTemplate(): Template {
290
- return {
291
- name: 'page-component',
292
- description: 'Page component with layout and SEO',
293
- files: [
294
- {
295
- path: 'app/client/src/pages/{{pascalName}}Page/{{pascalName}}Page.tsx',
296
- content: `import React, { useEffect } from 'react'
297
- import './{{pascalName}}Page.css'
298
-
299
- export interface {{pascalName}}PageProps {
300
- className?: string
301
- }
302
-
303
- export const {{pascalName}}Page: React.FC<{{pascalName}}PageProps> = ({
304
- className = '',
305
- ...props
306
- }) => {
307
- useEffect(() => {
308
- // Set page title
309
- document.title = '{{pascalName}} - FluxStack App'
310
-
311
- // Set meta description
312
- const metaDescription = document.querySelector('meta[name="description"]')
313
- if (metaDescription) {
314
- metaDescription.setAttribute('content', '{{pascalName}} page description')
315
- }
316
- }, [])
317
-
318
- return (
319
- <div className={\`{{kebabName}}-page \${className}\`.trim()} {...props}>
320
- <header className="{{kebabName}}-page__header">
321
- <h1>{{pascalName}}</h1>
322
- <p className="{{kebabName}}-page__subtitle">
323
- Welcome to the {{pascalName}} page
324
- </p>
325
- </header>
326
-
327
- <main className="{{kebabName}}-page__main">
328
- <section className="{{kebabName}}-page__section">
329
- <h2>Section Title</h2>
330
- <p>Add your page content here.</p>
331
- </section>
332
- </main>
333
-
334
- <footer className="{{kebabName}}-page__footer">
335
- <p>&copy; {new Date().getFullYear()} FluxStack App</p>
336
- </footer>
337
- </div>
338
- )
339
- }
340
-
341
- export default {{pascalName}}Page
342
- `
343
- },
344
- {
345
- path: 'app/client/src/pages/{{pascalName}}Page/{{pascalName}}Page.css',
346
- content: `.{{kebabName}}-page {
347
- min-height: 100vh;
348
- display: flex;
349
- flex-direction: column;
350
- }
351
-
352
- .{{kebabName}}-page__header {
353
- padding: 2rem;
354
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
355
- color: white;
356
- text-align: center;
357
- }
358
-
359
- .{{kebabName}}-page__header h1 {
360
- margin: 0 0 0.5rem 0;
361
- font-size: 2.5rem;
362
- font-weight: 700;
363
- }
364
-
365
- .{{kebabName}}-page__subtitle {
366
- margin: 0;
367
- font-size: 1.125rem;
368
- opacity: 0.9;
369
- }
370
-
371
- .{{kebabName}}-page__main {
372
- flex: 1;
373
- padding: 2rem;
374
- max-width: 1200px;
375
- margin: 0 auto;
376
- width: 100%;
377
- }
378
-
379
- .{{kebabName}}-page__section {
380
- margin-bottom: 2rem;
381
- }
382
-
383
- .{{kebabName}}-page__section h2 {
384
- margin: 0 0 1rem 0;
385
- font-size: 1.75rem;
386
- font-weight: 600;
387
- color: #2d3748;
388
- }
389
-
390
- .{{kebabName}}-page__section p {
391
- margin: 0;
392
- line-height: 1.6;
393
- color: #4a5568;
394
- }
395
-
396
- .{{kebabName}}-page__footer {
397
- padding: 1rem 2rem;
398
- background-color: #f7fafc;
399
- border-top: 1px solid #e2e8f0;
400
- text-align: center;
401
- color: #718096;
402
- }
403
-
404
- /* Responsive styles */
405
- @media (max-width: 768px) {
406
- .{{kebabName}}-page__header {
407
- padding: 1.5rem 1rem;
408
- }
409
-
410
- .{{kebabName}}-page__header h1 {
411
- font-size: 2rem;
412
- }
413
-
414
- .{{kebabName}}-page__main {
415
- padding: 1.5rem 1rem;
416
- }
417
-
418
- .{{kebabName}}-page__section h2 {
419
- font-size: 1.5rem;
420
- }
421
- }
422
- `
423
- },
424
- {
425
- path: 'app/client/src/pages/{{pascalName}}Page/index.ts',
426
- content: `export { {{pascalName}}Page, type {{pascalName}}PageProps } from './{{pascalName}}Page'
427
- export { default } from './{{pascalName}}Page'
428
- `
429
- }
430
- ]
431
- }
432
- }
433
-
434
- private getFormTemplate(): Template {
435
- return {
436
- name: 'form-component',
437
- description: 'Form component with validation',
438
- files: [
439
- {
440
- path: 'app/client/src/components/{{pascalName}}Form/{{pascalName}}Form.tsx',
441
- content: `import React, { useState } from 'react'
442
- import './{{pascalName}}Form.css'
443
-
444
- export interface {{pascalName}}FormData {
445
- name: string
446
- email: string
447
- message: string
448
- }
449
-
450
- export interface {{pascalName}}FormProps {
451
- className?: string
452
- onSubmit?: (data: {{pascalName}}FormData) => void | Promise<void>
453
- initialData?: Partial<{{pascalName}}FormData>
454
- }
455
-
456
- export const {{pascalName}}Form: React.FC<{{pascalName}}FormProps> = ({
457
- className = '',
458
- onSubmit,
459
- initialData = {},
460
- ...props
461
- }) => {
462
- const [formData, setFormData] = useState<{{pascalName}}FormData>({
463
- name: initialData.name || '',
464
- email: initialData.email || '',
465
- message: initialData.message || ''
466
- })
467
-
468
- const [errors, setErrors] = useState<Partial<{{pascalName}}FormData>>({})
469
- const [loading, setLoading] = useState(false)
470
-
471
- const validateForm = (): boolean => {
472
- const newErrors: Partial<{{pascalName}}FormData> = {}
473
-
474
- if (!formData.name.trim()) {
475
- newErrors.name = 'Name is required'
476
- }
477
-
478
- if (!formData.email.trim()) {
479
- newErrors.email = 'Email is required'
480
- } else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(formData.email)) {
481
- newErrors.email = 'Please enter a valid email'
482
- }
483
-
484
- if (!formData.message.trim()) {
485
- newErrors.message = 'Message is required'
486
- }
487
-
488
- setErrors(newErrors)
489
- return Object.keys(newErrors).length === 0
490
- }
491
-
492
- const handleSubmit = async (e: React.FormEvent) => {
493
- e.preventDefault()
494
-
495
- if (!validateForm()) {
496
- return
497
- }
498
-
499
- setLoading(true)
500
- try {
501
- await onSubmit?.(formData)
502
- // Reset form on successful submission
503
- setFormData({ name: '', email: '', message: '' })
504
- setErrors({})
505
- } catch (error) {
506
- console.error('Form submission failed:', error)
507
- } finally {
508
- setLoading(false)
509
- }
510
- }
511
-
512
- const handleChange = (field: keyof {{pascalName}}FormData) => (
513
- e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
514
- ) => {
515
- setFormData(prev => ({ ...prev, [field]: e.target.value }))
516
- // Clear error when user starts typing
517
- if (errors[field]) {
518
- setErrors(prev => ({ ...prev, [field]: undefined }))
519
- }
520
- }
521
-
522
- return (
523
- <form
524
- className={\`{{kebabName}}-form \${className}\`.trim()}
525
- onSubmit={handleSubmit}
526
- {...props}
527
- >
528
- <h2>{{pascalName}} Form</h2>
529
-
530
- <div className="{{kebabName}}-form__field">
531
- <label htmlFor="{{kebabName}}-name">Name *</label>
532
- <input
533
- id="{{kebabName}}-name"
534
- type="text"
535
- value={formData.name}
536
- onChange={handleChange('name')}
537
- className={errors.name ? 'error' : ''}
538
- placeholder="Enter your name"
539
- />
540
- {errors.name && <span className="{{kebabName}}-form__error">{errors.name}</span>}
541
- </div>
542
-
543
- <div className="{{kebabName}}-form__field">
544
- <label htmlFor="{{kebabName}}-email">Email *</label>
545
- <input
546
- id="{{kebabName}}-email"
547
- type="email"
548
- value={formData.email}
549
- onChange={handleChange('email')}
550
- className={errors.email ? 'error' : ''}
551
- placeholder="Enter your email"
552
- />
553
- {errors.email && <span className="{{kebabName}}-form__error">{errors.email}</span>}
554
- </div>
555
-
556
- <div className="{{kebabName}}-form__field">
557
- <label htmlFor="{{kebabName}}-message">Message *</label>
558
- <textarea
559
- id="{{kebabName}}-message"
560
- value={formData.message}
561
- onChange={handleChange('message')}
562
- className={errors.message ? 'error' : ''}
563
- placeholder="Enter your message"
564
- rows={4}
565
- />
566
- {errors.message && <span className="{{kebabName}}-form__error">{errors.message}</span>}
567
- </div>
568
-
569
- <button
570
- type="submit"
571
- className="{{kebabName}}-form__submit"
572
- disabled={loading}
573
- >
574
- {loading ? 'Submitting...' : 'Submit'}
575
- </button>
576
- </form>
577
- )
578
- }
579
-
580
- export default {{pascalName}}Form
581
- `
582
- },
583
- {
584
- path: 'app/client/src/components/{{pascalName}}Form/{{pascalName}}Form.css',
585
- content: `.{{kebabName}}-form {
586
- max-width: 500px;
587
- padding: 2rem;
588
- border: 1px solid #e2e8f0;
589
- border-radius: 0.75rem;
590
- background-color: #ffffff;
591
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
592
- }
593
-
594
- .{{kebabName}}-form h2 {
595
- margin: 0 0 1.5rem 0;
596
- font-size: 1.5rem;
597
- font-weight: 600;
598
- color: #2d3748;
599
- text-align: center;
600
- }
601
-
602
- .{{kebabName}}-form__field {
603
- margin-bottom: 1.5rem;
604
- }
605
-
606
- .{{kebabName}}-form__field label {
607
- display: block;
608
- margin-bottom: 0.5rem;
609
- font-weight: 500;
610
- color: #374151;
611
- }
612
-
613
- .{{kebabName}}-form__field input,
614
- .{{kebabName}}-form__field textarea {
615
- width: 100%;
616
- padding: 0.75rem;
617
- border: 1px solid #d1d5db;
618
- border-radius: 0.5rem;
619
- font-size: 1rem;
620
- transition: border-color 0.2s, box-shadow 0.2s;
621
- box-sizing: border-box;
622
- }
623
-
624
- .{{kebabName}}-form__field input:focus,
625
- .{{kebabName}}-form__field textarea:focus {
626
- outline: none;
627
- border-color: #4299e1;
628
- box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
629
- }
630
-
631
- .{{kebabName}}-form__field input.error,
632
- .{{kebabName}}-form__field textarea.error {
633
- border-color: #e53e3e;
634
- box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.1);
635
- }
636
-
637
- .{{kebabName}}-form__error {
638
- display: block;
639
- margin-top: 0.25rem;
640
- font-size: 0.875rem;
641
- color: #e53e3e;
642
- }
643
-
644
- .{{kebabName}}-form__submit {
645
- width: 100%;
646
- padding: 0.75rem 1.5rem;
647
- background-color: #4299e1;
648
- color: white;
649
- border: none;
650
- border-radius: 0.5rem;
651
- font-size: 1rem;
652
- font-weight: 500;
653
- cursor: pointer;
654
- transition: background-color 0.2s;
655
- }
656
-
657
- .{{kebabName}}-form__submit:hover:not(:disabled) {
658
- background-color: #3182ce;
659
- }
660
-
661
- .{{kebabName}}-form__submit:disabled {
662
- background-color: #a0aec0;
663
- cursor: not-allowed;
664
- }
665
-
666
- /* Responsive styles */
667
- @media (max-width: 768px) {
668
- .{{kebabName}}-form {
669
- padding: 1.5rem;
670
- }
671
-
672
- .{{kebabName}}-form h2 {
673
- font-size: 1.25rem;
674
- }
675
- }
676
- `
677
- },
678
- {
679
- path: 'app/client/src/components/{{pascalName}}Form/index.ts',
680
- content: `export { {{pascalName}}Form, type {{pascalName}}FormProps, type {{pascalName}}FormData } from './{{pascalName}}Form'
681
- export { default } from './{{pascalName}}Form'
682
- `
683
- }
684
- ]
685
- }
686
- }
687
-
688
- private getFullTemplate(): Template {
689
- return {
690
- name: 'full-component',
691
- description: 'Complete component with tests and stories',
692
- files: [
693
- ...this.getBasicTemplate().files,
694
- {
695
- path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.test.tsx',
696
- content: `import React from 'react'
697
- import { render, screen } from '@testing-library/react'
698
- import { {{pascalName}} } from './{{pascalName}}'
699
-
700
- describe('{{pascalName}}', () => {
701
- it('renders without crashing', () => {
702
- render(<{{pascalName}} />)
703
- expect(screen.getByText('{{pascalName}} Component')).toBeInTheDocument()
704
- })
705
-
706
- it('applies custom className', () => {
707
- const { container } = render(<{{pascalName}} className="custom-class" />)
708
- expect(container.firstChild).toHaveClass('{{kebabName}}', 'custom-class')
709
- })
710
-
711
- it('renders children content', () => {
712
- render(
713
- <{{pascalName}}>
714
- <p>Test content</p>
715
- </{{pascalName}}>
716
- )
717
- expect(screen.getByText('Test content')).toBeInTheDocument()
718
- })
719
- })
720
- `
721
- },
722
- {
723
- path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.stories.tsx',
724
- content: `import type { Meta, StoryObj } from '@storybook/react'
725
- import { {{pascalName}} } from './{{pascalName}}'
726
-
727
- const meta: Meta<typeof {{pascalName}}> = {
728
- title: 'Components/{{pascalName}}',
729
- component: {{pascalName}},
730
- parameters: {
731
- layout: 'centered',
732
- },
733
- tags: ['autodocs'],
734
- argTypes: {
735
- className: {
736
- control: 'text',
737
- description: 'Additional CSS classes'
738
- }
739
- },
740
- }
741
-
742
- export default meta
743
- type Story = StoryObj<typeof meta>
744
-
745
- export const Default: Story = {
746
- args: {},
747
- }
748
-
749
- export const WithCustomClass: Story = {
750
- args: {
751
- className: 'custom-styling',
752
- },
753
- }
754
-
755
- export const WithChildren: Story = {
756
- args: {
757
- children: (
758
- <div>
759
- <p>This is custom content inside the {{pascalName}} component.</p>
760
- <button>Click me</button>
761
- </div>
762
- ),
763
- },
764
- }
765
- `
766
- }
767
- ]
768
- }
769
- }
1
+ import type { Generator } from "./index"
2
+ import type { GeneratorContext, GeneratorOptions, Template } from "./types"
3
+ import { templateEngine } from "./template-engine"
4
+
5
+ export class ComponentGenerator implements Generator {
6
+ name = 'component'
7
+ description = 'Generate a new React component'
8
+
9
+ async generate(context: GeneratorContext, options: GeneratorOptions): Promise<void> {
10
+ const template = this.getTemplate(options.template)
11
+
12
+ if (template.hooks?.beforeGenerate) {
13
+ await template.hooks.beforeGenerate(context, options)
14
+ }
15
+
16
+ const files = await templateEngine.processTemplate(template, context, options)
17
+
18
+ if (options.dryRun) {
19
+ console.log(`\n📋 Would generate component '${options.name}':\n`)
20
+ for (const file of files) {
21
+ console.log(`${file.action === 'create' ? '📄' : '✏️'} ${file.path}`)
22
+ }
23
+ return
24
+ }
25
+
26
+ await templateEngine.generateFiles(files, options.dryRun)
27
+
28
+ if (template.hooks?.afterGenerate) {
29
+ const filePaths = files.map(f => f.path)
30
+ await template.hooks.afterGenerate(context, options, filePaths)
31
+ }
32
+
33
+ console.log(`\n✅ Generated component '${options.name}' with ${files.length} files`)
34
+ }
35
+
36
+ private getTemplate(templateName?: string): Template {
37
+ switch (templateName) {
38
+ case 'functional':
39
+ return this.getFunctionalTemplate()
40
+ case 'page':
41
+ return this.getPageTemplate()
42
+ case 'form':
43
+ return this.getFormTemplate()
44
+ case 'full':
45
+ return this.getFullTemplate()
46
+ default:
47
+ return this.getBasicTemplate()
48
+ }
49
+ }
50
+
51
+ private getBasicTemplate(): Template {
52
+ return {
53
+ name: 'basic-component',
54
+ description: 'Basic React component with TypeScript',
55
+ files: [
56
+ {
57
+ path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.tsx',
58
+ content: `import React from 'react'
59
+ import './{{pascalName}}.css'
60
+
61
+ export interface {{pascalName}}Props {
62
+ className?: string
63
+ children?: React.ReactNode
64
+ }
65
+
66
+ export const {{pascalName}}: React.FC<{{pascalName}}Props> = ({
67
+ className = '',
68
+ children,
69
+ ...props
70
+ }) => {
71
+ return (
72
+ <div className={\`{{kebabName}} \${className}\`.trim()} {...props}>
73
+ <h2>{{pascalName}} Component</h2>
74
+ {children}
75
+ </div>
76
+ )
77
+ }
78
+
79
+ export default {{pascalName}}
80
+ `
81
+ },
82
+ {
83
+ path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.css',
84
+ content: `.{{kebabName}} {
85
+ /* Add your styles here */
86
+ padding: 1rem;
87
+ border: 1px solid #e2e8f0;
88
+ border-radius: 0.5rem;
89
+ background-color: #ffffff;
90
+ }
91
+
92
+ .{{kebabName}} h2 {
93
+ margin: 0 0 1rem 0;
94
+ font-size: 1.25rem;
95
+ font-weight: 600;
96
+ color: #1a202c;
97
+ }
98
+
99
+ /* Responsive styles */
100
+ @media (max-width: 768px) {
101
+ .{{kebabName}} {
102
+ padding: 0.75rem;
103
+ }
104
+
105
+ .{{kebabName}} h2 {
106
+ font-size: 1.125rem;
107
+ }
108
+ }
109
+ `
110
+ },
111
+ {
112
+ path: 'app/client/src/components/{{pascalName}}/index.ts',
113
+ content: `export { {{pascalName}}, type {{pascalName}}Props } from './{{pascalName}}'
114
+ export { default } from './{{pascalName}}'
115
+ `
116
+ }
117
+ ],
118
+ hooks: {
119
+ afterGenerate: async (context, options, files) => {
120
+ context.logger.info(`Generated component files:`)
121
+ files.forEach(file => {
122
+ context.logger.info(` - ${file}`)
123
+ })
124
+ context.logger.info(`\nUsage example:`)
125
+ context.logger.info(`import { ${options.name} } from './components/${options.name}'`)
126
+ context.logger.info(`\n<${options.name}>Content here</${options.name}>`)
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ private getFunctionalTemplate(): Template {
133
+ return {
134
+ name: 'functional-component',
135
+ description: 'Functional component with hooks',
136
+ files: [
137
+ {
138
+ path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.tsx',
139
+ content: `import React, { useState, useEffect } from 'react'
140
+ import './{{pascalName}}.css'
141
+
142
+ export interface {{pascalName}}Props {
143
+ className?: string
144
+ onAction?: (data: any) => void
145
+ }
146
+
147
+ export const {{pascalName}}: React.FC<{{pascalName}}Props> = ({
148
+ className = '',
149
+ onAction,
150
+ ...props
151
+ }) => {
152
+ const [loading, setLoading] = useState(false)
153
+ const [data, setData] = useState<any>(null)
154
+
155
+ useEffect(() => {
156
+ // Component initialization logic
157
+ console.log('{{pascalName}} mounted')
158
+
159
+ return () => {
160
+ // Cleanup logic
161
+ console.log('{{pascalName}} unmounted')
162
+ }
163
+ }, [])
164
+
165
+ const handleAction = async () => {
166
+ setLoading(true)
167
+ try {
168
+ // Simulate async operation
169
+ await new Promise(resolve => setTimeout(resolve, 1000))
170
+ const result = { message: 'Action completed', timestamp: new Date() }
171
+ setData(result)
172
+ onAction?.(result)
173
+ } catch (error) {
174
+ console.error('Action failed:', error)
175
+ } finally {
176
+ setLoading(false)
177
+ }
178
+ }
179
+
180
+ return (
181
+ <div className={\`{{kebabName}} \${className}\`.trim()} {...props}>
182
+ <h2>{{pascalName}}</h2>
183
+
184
+ <div className="{{kebabName}}__content">
185
+ {data && (
186
+ <div className="{{kebabName}}__data">
187
+ <p>Last action: {data.message}</p>
188
+ <small>{data.timestamp?.toLocaleString()}</small>
189
+ </div>
190
+ )}
191
+
192
+ <button
193
+ className="{{kebabName}}__button"
194
+ onClick={handleAction}
195
+ disabled={loading}
196
+ >
197
+ {loading ? 'Loading...' : 'Perform Action'}
198
+ </button>
199
+ </div>
200
+ </div>
201
+ )
202
+ }
203
+
204
+ export default {{pascalName}}
205
+ `
206
+ },
207
+ {
208
+ path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.css',
209
+ content: `.{{kebabName}} {
210
+ padding: 1.5rem;
211
+ border: 1px solid #e2e8f0;
212
+ border-radius: 0.75rem;
213
+ background-color: #ffffff;
214
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
215
+ }
216
+
217
+ .{{kebabName}} h2 {
218
+ margin: 0 0 1rem 0;
219
+ font-size: 1.5rem;
220
+ font-weight: 600;
221
+ color: #2d3748;
222
+ }
223
+
224
+ .{{kebabName}}__content {
225
+ display: flex;
226
+ flex-direction: column;
227
+ gap: 1rem;
228
+ }
229
+
230
+ .{{kebabName}}__data {
231
+ padding: 1rem;
232
+ background-color: #f7fafc;
233
+ border-radius: 0.5rem;
234
+ border-left: 4px solid #4299e1;
235
+ }
236
+
237
+ .{{kebabName}}__data p {
238
+ margin: 0 0 0.5rem 0;
239
+ font-weight: 500;
240
+ color: #2d3748;
241
+ }
242
+
243
+ .{{kebabName}}__data small {
244
+ color: #718096;
245
+ }
246
+
247
+ .{{kebabName}}__button {
248
+ padding: 0.75rem 1.5rem;
249
+ background-color: #4299e1;
250
+ color: white;
251
+ border: none;
252
+ border-radius: 0.5rem;
253
+ font-weight: 500;
254
+ cursor: pointer;
255
+ transition: background-color 0.2s;
256
+ }
257
+
258
+ .{{kebabName}}__button:hover:not(:disabled) {
259
+ background-color: #3182ce;
260
+ }
261
+
262
+ .{{kebabName}}__button:disabled {
263
+ background-color: #a0aec0;
264
+ cursor: not-allowed;
265
+ }
266
+
267
+ /* Responsive styles */
268
+ @media (max-width: 768px) {
269
+ .{{kebabName}} {
270
+ padding: 1rem;
271
+ }
272
+
273
+ .{{kebabName}} h2 {
274
+ font-size: 1.25rem;
275
+ }
276
+ }
277
+ `
278
+ },
279
+ {
280
+ path: 'app/client/src/components/{{pascalName}}/index.ts',
281
+ content: `export { {{pascalName}}, type {{pascalName}}Props } from './{{pascalName}}'
282
+ export { default } from './{{pascalName}}'
283
+ `
284
+ }
285
+ ]
286
+ }
287
+ }
288
+
289
+ private getPageTemplate(): Template {
290
+ return {
291
+ name: 'page-component',
292
+ description: 'Page component with layout and SEO',
293
+ files: [
294
+ {
295
+ path: 'app/client/src/pages/{{pascalName}}Page/{{pascalName}}Page.tsx',
296
+ content: `import React, { useEffect } from 'react'
297
+ import './{{pascalName}}Page.css'
298
+
299
+ export interface {{pascalName}}PageProps {
300
+ className?: string
301
+ }
302
+
303
+ export const {{pascalName}}Page: React.FC<{{pascalName}}PageProps> = ({
304
+ className = '',
305
+ ...props
306
+ }) => {
307
+ useEffect(() => {
308
+ // Set page title
309
+ document.title = '{{pascalName}} - FluxStack App'
310
+
311
+ // Set meta description
312
+ const metaDescription = document.querySelector('meta[name="description"]')
313
+ if (metaDescription) {
314
+ metaDescription.setAttribute('content', '{{pascalName}} page description')
315
+ }
316
+ }, [])
317
+
318
+ return (
319
+ <div className={\`{{kebabName}}-page \${className}\`.trim()} {...props}>
320
+ <header className="{{kebabName}}-page__header">
321
+ <h1>{{pascalName}}</h1>
322
+ <p className="{{kebabName}}-page__subtitle">
323
+ Welcome to the {{pascalName}} page
324
+ </p>
325
+ </header>
326
+
327
+ <main className="{{kebabName}}-page__main">
328
+ <section className="{{kebabName}}-page__section">
329
+ <h2>Section Title</h2>
330
+ <p>Add your page content here.</p>
331
+ </section>
332
+ </main>
333
+
334
+ <footer className="{{kebabName}}-page__footer">
335
+ <p>&copy; {new Date().getFullYear()} FluxStack App</p>
336
+ </footer>
337
+ </div>
338
+ )
339
+ }
340
+
341
+ export default {{pascalName}}Page
342
+ `
343
+ },
344
+ {
345
+ path: 'app/client/src/pages/{{pascalName}}Page/{{pascalName}}Page.css',
346
+ content: `.{{kebabName}}-page {
347
+ min-height: 100vh;
348
+ display: flex;
349
+ flex-direction: column;
350
+ }
351
+
352
+ .{{kebabName}}-page__header {
353
+ padding: 2rem;
354
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
355
+ color: white;
356
+ text-align: center;
357
+ }
358
+
359
+ .{{kebabName}}-page__header h1 {
360
+ margin: 0 0 0.5rem 0;
361
+ font-size: 2.5rem;
362
+ font-weight: 700;
363
+ }
364
+
365
+ .{{kebabName}}-page__subtitle {
366
+ margin: 0;
367
+ font-size: 1.125rem;
368
+ opacity: 0.9;
369
+ }
370
+
371
+ .{{kebabName}}-page__main {
372
+ flex: 1;
373
+ padding: 2rem;
374
+ max-width: 1200px;
375
+ margin: 0 auto;
376
+ width: 100%;
377
+ }
378
+
379
+ .{{kebabName}}-page__section {
380
+ margin-bottom: 2rem;
381
+ }
382
+
383
+ .{{kebabName}}-page__section h2 {
384
+ margin: 0 0 1rem 0;
385
+ font-size: 1.75rem;
386
+ font-weight: 600;
387
+ color: #2d3748;
388
+ }
389
+
390
+ .{{kebabName}}-page__section p {
391
+ margin: 0;
392
+ line-height: 1.6;
393
+ color: #4a5568;
394
+ }
395
+
396
+ .{{kebabName}}-page__footer {
397
+ padding: 1rem 2rem;
398
+ background-color: #f7fafc;
399
+ border-top: 1px solid #e2e8f0;
400
+ text-align: center;
401
+ color: #718096;
402
+ }
403
+
404
+ /* Responsive styles */
405
+ @media (max-width: 768px) {
406
+ .{{kebabName}}-page__header {
407
+ padding: 1.5rem 1rem;
408
+ }
409
+
410
+ .{{kebabName}}-page__header h1 {
411
+ font-size: 2rem;
412
+ }
413
+
414
+ .{{kebabName}}-page__main {
415
+ padding: 1.5rem 1rem;
416
+ }
417
+
418
+ .{{kebabName}}-page__section h2 {
419
+ font-size: 1.5rem;
420
+ }
421
+ }
422
+ `
423
+ },
424
+ {
425
+ path: 'app/client/src/pages/{{pascalName}}Page/index.ts',
426
+ content: `export { {{pascalName}}Page, type {{pascalName}}PageProps } from './{{pascalName}}Page'
427
+ export { default } from './{{pascalName}}Page'
428
+ `
429
+ }
430
+ ]
431
+ }
432
+ }
433
+
434
+ private getFormTemplate(): Template {
435
+ return {
436
+ name: 'form-component',
437
+ description: 'Form component with validation',
438
+ files: [
439
+ {
440
+ path: 'app/client/src/components/{{pascalName}}Form/{{pascalName}}Form.tsx',
441
+ content: `import React, { useState } from 'react'
442
+ import './{{pascalName}}Form.css'
443
+
444
+ export interface {{pascalName}}FormData {
445
+ name: string
446
+ email: string
447
+ message: string
448
+ }
449
+
450
+ export interface {{pascalName}}FormProps {
451
+ className?: string
452
+ onSubmit?: (data: {{pascalName}}FormData) => void | Promise<void>
453
+ initialData?: Partial<{{pascalName}}FormData>
454
+ }
455
+
456
+ export const {{pascalName}}Form: React.FC<{{pascalName}}FormProps> = ({
457
+ className = '',
458
+ onSubmit,
459
+ initialData = {},
460
+ ...props
461
+ }) => {
462
+ const [formData, setFormData] = useState<{{pascalName}}FormData>({
463
+ name: initialData.name || '',
464
+ email: initialData.email || '',
465
+ message: initialData.message || ''
466
+ })
467
+
468
+ const [errors, setErrors] = useState<Partial<{{pascalName}}FormData>>({})
469
+ const [loading, setLoading] = useState(false)
470
+
471
+ const validateForm = (): boolean => {
472
+ const newErrors: Partial<{{pascalName}}FormData> = {}
473
+
474
+ if (!formData.name.trim()) {
475
+ newErrors.name = 'Name is required'
476
+ }
477
+
478
+ if (!formData.email.trim()) {
479
+ newErrors.email = 'Email is required'
480
+ } else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(formData.email)) {
481
+ newErrors.email = 'Please enter a valid email'
482
+ }
483
+
484
+ if (!formData.message.trim()) {
485
+ newErrors.message = 'Message is required'
486
+ }
487
+
488
+ setErrors(newErrors)
489
+ return Object.keys(newErrors).length === 0
490
+ }
491
+
492
+ const handleSubmit = async (e: React.FormEvent) => {
493
+ e.preventDefault()
494
+
495
+ if (!validateForm()) {
496
+ return
497
+ }
498
+
499
+ setLoading(true)
500
+ try {
501
+ await onSubmit?.(formData)
502
+ // Reset form on successful submission
503
+ setFormData({ name: '', email: '', message: '' })
504
+ setErrors({})
505
+ } catch (error) {
506
+ console.error('Form submission failed:', error)
507
+ } finally {
508
+ setLoading(false)
509
+ }
510
+ }
511
+
512
+ const handleChange = (field: keyof {{pascalName}}FormData) => (
513
+ e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
514
+ ) => {
515
+ setFormData(prev => ({ ...prev, [field]: e.target.value }))
516
+ // Clear error when user starts typing
517
+ if (errors[field]) {
518
+ setErrors(prev => ({ ...prev, [field]: undefined }))
519
+ }
520
+ }
521
+
522
+ return (
523
+ <form
524
+ className={\`{{kebabName}}-form \${className}\`.trim()}
525
+ onSubmit={handleSubmit}
526
+ {...props}
527
+ >
528
+ <h2>{{pascalName}} Form</h2>
529
+
530
+ <div className="{{kebabName}}-form__field">
531
+ <label htmlFor="{{kebabName}}-name">Name *</label>
532
+ <input
533
+ id="{{kebabName}}-name"
534
+ type="text"
535
+ value={formData.name}
536
+ onChange={handleChange('name')}
537
+ className={errors.name ? 'error' : ''}
538
+ placeholder="Enter your name"
539
+ />
540
+ {errors.name && <span className="{{kebabName}}-form__error">{errors.name}</span>}
541
+ </div>
542
+
543
+ <div className="{{kebabName}}-form__field">
544
+ <label htmlFor="{{kebabName}}-email">Email *</label>
545
+ <input
546
+ id="{{kebabName}}-email"
547
+ type="email"
548
+ value={formData.email}
549
+ onChange={handleChange('email')}
550
+ className={errors.email ? 'error' : ''}
551
+ placeholder="Enter your email"
552
+ />
553
+ {errors.email && <span className="{{kebabName}}-form__error">{errors.email}</span>}
554
+ </div>
555
+
556
+ <div className="{{kebabName}}-form__field">
557
+ <label htmlFor="{{kebabName}}-message">Message *</label>
558
+ <textarea
559
+ id="{{kebabName}}-message"
560
+ value={formData.message}
561
+ onChange={handleChange('message')}
562
+ className={errors.message ? 'error' : ''}
563
+ placeholder="Enter your message"
564
+ rows={4}
565
+ />
566
+ {errors.message && <span className="{{kebabName}}-form__error">{errors.message}</span>}
567
+ </div>
568
+
569
+ <button
570
+ type="submit"
571
+ className="{{kebabName}}-form__submit"
572
+ disabled={loading}
573
+ >
574
+ {loading ? 'Submitting...' : 'Submit'}
575
+ </button>
576
+ </form>
577
+ )
578
+ }
579
+
580
+ export default {{pascalName}}Form
581
+ `
582
+ },
583
+ {
584
+ path: 'app/client/src/components/{{pascalName}}Form/{{pascalName}}Form.css',
585
+ content: `.{{kebabName}}-form {
586
+ max-width: 500px;
587
+ padding: 2rem;
588
+ border: 1px solid #e2e8f0;
589
+ border-radius: 0.75rem;
590
+ background-color: #ffffff;
591
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
592
+ }
593
+
594
+ .{{kebabName}}-form h2 {
595
+ margin: 0 0 1.5rem 0;
596
+ font-size: 1.5rem;
597
+ font-weight: 600;
598
+ color: #2d3748;
599
+ text-align: center;
600
+ }
601
+
602
+ .{{kebabName}}-form__field {
603
+ margin-bottom: 1.5rem;
604
+ }
605
+
606
+ .{{kebabName}}-form__field label {
607
+ display: block;
608
+ margin-bottom: 0.5rem;
609
+ font-weight: 500;
610
+ color: #374151;
611
+ }
612
+
613
+ .{{kebabName}}-form__field input,
614
+ .{{kebabName}}-form__field textarea {
615
+ width: 100%;
616
+ padding: 0.75rem;
617
+ border: 1px solid #d1d5db;
618
+ border-radius: 0.5rem;
619
+ font-size: 1rem;
620
+ transition: border-color 0.2s, box-shadow 0.2s;
621
+ box-sizing: border-box;
622
+ }
623
+
624
+ .{{kebabName}}-form__field input:focus,
625
+ .{{kebabName}}-form__field textarea:focus {
626
+ outline: none;
627
+ border-color: #4299e1;
628
+ box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
629
+ }
630
+
631
+ .{{kebabName}}-form__field input.error,
632
+ .{{kebabName}}-form__field textarea.error {
633
+ border-color: #e53e3e;
634
+ box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.1);
635
+ }
636
+
637
+ .{{kebabName}}-form__error {
638
+ display: block;
639
+ margin-top: 0.25rem;
640
+ font-size: 0.875rem;
641
+ color: #e53e3e;
642
+ }
643
+
644
+ .{{kebabName}}-form__submit {
645
+ width: 100%;
646
+ padding: 0.75rem 1.5rem;
647
+ background-color: #4299e1;
648
+ color: white;
649
+ border: none;
650
+ border-radius: 0.5rem;
651
+ font-size: 1rem;
652
+ font-weight: 500;
653
+ cursor: pointer;
654
+ transition: background-color 0.2s;
655
+ }
656
+
657
+ .{{kebabName}}-form__submit:hover:not(:disabled) {
658
+ background-color: #3182ce;
659
+ }
660
+
661
+ .{{kebabName}}-form__submit:disabled {
662
+ background-color: #a0aec0;
663
+ cursor: not-allowed;
664
+ }
665
+
666
+ /* Responsive styles */
667
+ @media (max-width: 768px) {
668
+ .{{kebabName}}-form {
669
+ padding: 1.5rem;
670
+ }
671
+
672
+ .{{kebabName}}-form h2 {
673
+ font-size: 1.25rem;
674
+ }
675
+ }
676
+ `
677
+ },
678
+ {
679
+ path: 'app/client/src/components/{{pascalName}}Form/index.ts',
680
+ content: `export { {{pascalName}}Form, type {{pascalName}}FormProps, type {{pascalName}}FormData } from './{{pascalName}}Form'
681
+ export { default } from './{{pascalName}}Form'
682
+ `
683
+ }
684
+ ]
685
+ }
686
+ }
687
+
688
+ private getFullTemplate(): Template {
689
+ return {
690
+ name: 'full-component',
691
+ description: 'Complete component with tests and stories',
692
+ files: [
693
+ ...this.getBasicTemplate().files,
694
+ {
695
+ path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.test.tsx',
696
+ content: `import React from 'react'
697
+ import { render, screen } from '@testing-library/react'
698
+ import { {{pascalName}} } from './{{pascalName}}'
699
+
700
+ describe('{{pascalName}}', () => {
701
+ it('renders without crashing', () => {
702
+ render(<{{pascalName}} />)
703
+ expect(screen.getByText('{{pascalName}} Component')).toBeInTheDocument()
704
+ })
705
+
706
+ it('applies custom className', () => {
707
+ const { container } = render(<{{pascalName}} className="custom-class" />)
708
+ expect(container.firstChild).toHaveClass('{{kebabName}}', 'custom-class')
709
+ })
710
+
711
+ it('renders children content', () => {
712
+ render(
713
+ <{{pascalName}}>
714
+ <p>Test content</p>
715
+ </{{pascalName}}>
716
+ )
717
+ expect(screen.getByText('Test content')).toBeInTheDocument()
718
+ })
719
+ })
720
+ `
721
+ },
722
+ {
723
+ path: 'app/client/src/components/{{pascalName}}/{{pascalName}}.stories.tsx',
724
+ content: `import type { Meta, StoryObj } from '@storybook/react'
725
+ import { {{pascalName}} } from './{{pascalName}}'
726
+
727
+ const meta: Meta<typeof {{pascalName}}> = {
728
+ title: 'Components/{{pascalName}}',
729
+ component: {{pascalName}},
730
+ parameters: {
731
+ layout: 'centered',
732
+ },
733
+ tags: ['autodocs'],
734
+ argTypes: {
735
+ className: {
736
+ control: 'text',
737
+ description: 'Additional CSS classes'
738
+ }
739
+ },
740
+ }
741
+
742
+ export default meta
743
+ type Story = StoryObj<typeof meta>
744
+
745
+ export const Default: Story = {
746
+ args: {},
747
+ }
748
+
749
+ export const WithCustomClass: Story = {
750
+ args: {
751
+ className: 'custom-styling',
752
+ },
753
+ }
754
+
755
+ export const WithChildren: Story = {
756
+ args: {
757
+ children: (
758
+ <div>
759
+ <p>This is custom content inside the {{pascalName}} component.</p>
760
+ <button>Click me</button>
761
+ </div>
762
+ ),
763
+ },
764
+ }
765
+ `
766
+ }
767
+ ]
768
+ }
769
+ }
770
770
  }