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