@compilr-dev/factory 0.1.3 → 0.1.4
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.
- package/dist/factory/skill.js +1 -1
- package/dist/factory/tools.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/model/operations-entity.js +10 -3
- package/dist/model/schema.d.ts +4 -1
- package/dist/model/schema.js +9 -6
- package/dist/model/tools.d.ts +1 -0
- package/dist/model/tools.js +4 -2
- package/dist/toolkits/react-node/dashboard.js +41 -5
- package/dist/toolkits/react-node/router.js +20 -1
- package/dist/toolkits/react-node/shell.js +145 -9
- package/dist/toolkits/react-node/static.js +15 -1
- package/package.json +1 -1
package/dist/factory/skill.js
CHANGED
|
@@ -33,7 +33,7 @@ You have access to these tools:
|
|
|
33
33
|
Before building the model, understand the project:
|
|
34
34
|
|
|
35
35
|
1. Check for existing context:
|
|
36
|
-
- Use \`
|
|
36
|
+
- Use \`project_document_get\` to look for PRD, architecture, and design docs
|
|
37
37
|
- Read any existing Application Model with \`app_model_get\`
|
|
38
38
|
|
|
39
39
|
2. If NO model exists, gather requirements from the user:
|
package/dist/factory/tools.js
CHANGED
|
@@ -9,8 +9,9 @@ import { createListToolkitsTool } from './list-toolkits-tool.js';
|
|
|
9
9
|
import { defaultRegistry } from './registry.js';
|
|
10
10
|
export function createFactoryTools(config) {
|
|
11
11
|
const registry = config.registry ?? defaultRegistry;
|
|
12
|
+
const validToolkitIds = registry.list().map((t) => t.id);
|
|
12
13
|
return [
|
|
13
|
-
...createModelTools(config),
|
|
14
|
+
...createModelTools({ ...config, validToolkitIds }),
|
|
14
15
|
createScaffoldTool({ ...config, registry }),
|
|
15
16
|
createListToolkitsTool(registry),
|
|
16
17
|
];
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export type { ApplicationModel, Entity, Field, FieldType, View, Relationship, RelationshipType, Identity, Layout, ShellType, Features, Theme, TechStack, Meta, } from './model/types.js';
|
|
7
7
|
export { validateModel } from './model/schema.js';
|
|
8
|
-
export type { ValidationError, ValidationResult } from './model/schema.js';
|
|
8
|
+
export type { ValidationError, ValidationResult, ValidateModelOptions } from './model/schema.js';
|
|
9
9
|
export { createDefaultModel, createDefaultEntity, createDefaultField } from './model/defaults.js';
|
|
10
10
|
export { toPascalCase, toCamelCase, toKebabCase, toPlural, isPascalCase, isCamelCase, isValidHexColor, } from './model/naming.js';
|
|
11
11
|
export type { FactoryFile, FactoryResult, FactoryToolkit } from './toolkits/types.js';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* addEntity, updateEntity, removeEntity, renameEntity, reorderEntities
|
|
5
5
|
*/
|
|
6
|
-
import { isPascalCase } from './naming.js';
|
|
6
|
+
import { isPascalCase, toCamelCase } from './naming.js';
|
|
7
7
|
function findEntity(model, name) {
|
|
8
8
|
const entity = model.entities.find((e) => e.name === name);
|
|
9
9
|
if (!entity) {
|
|
@@ -72,15 +72,22 @@ export function renameEntity(model, op) {
|
|
|
72
72
|
if (entity.name === op.entity) {
|
|
73
73
|
entity = { ...entity, name: op.newName };
|
|
74
74
|
}
|
|
75
|
-
// Update relationship targets
|
|
75
|
+
// Update relationship targets and cascade fieldName
|
|
76
76
|
const hasTargetRef = entity.relationships.some((r) => r.target === op.entity);
|
|
77
77
|
if (hasTargetRef) {
|
|
78
|
+
const oldDefaultFieldName = toCamelCase(op.entity) + 'Id';
|
|
79
|
+
const newDefaultFieldName = toCamelCase(op.newName) + 'Id';
|
|
78
80
|
entity = {
|
|
79
81
|
...entity,
|
|
80
82
|
relationships: entity.relationships.map((r) => {
|
|
81
83
|
if (r.target !== op.entity)
|
|
82
84
|
return r;
|
|
83
|
-
|
|
85
|
+
const updated = { ...r, target: op.newName };
|
|
86
|
+
// Cascade fieldName if it matches the old default pattern (or is undefined/implicit)
|
|
87
|
+
if (r.fieldName === undefined || r.fieldName === oldDefaultFieldName) {
|
|
88
|
+
updated.fieldName = newDefaultFieldName;
|
|
89
|
+
}
|
|
90
|
+
return updated;
|
|
84
91
|
}),
|
|
85
92
|
};
|
|
86
93
|
}
|
package/dist/model/schema.d.ts
CHANGED
|
@@ -12,4 +12,7 @@ export interface ValidationResult {
|
|
|
12
12
|
readonly valid: boolean;
|
|
13
13
|
readonly errors: readonly ValidationError[];
|
|
14
14
|
}
|
|
15
|
-
export
|
|
15
|
+
export interface ValidateModelOptions {
|
|
16
|
+
readonly validToolkitIds?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function validateModel(model: ApplicationModel, options?: ValidateModelOptions): ValidationResult;
|
package/dist/model/schema.js
CHANGED
|
@@ -206,7 +206,7 @@ function validateTheme(model) {
|
|
|
206
206
|
}
|
|
207
207
|
return errors;
|
|
208
208
|
}
|
|
209
|
-
function validateTechStack(model) {
|
|
209
|
+
function validateTechStack(model, validToolkitIds) {
|
|
210
210
|
const errors = [];
|
|
211
211
|
if (model.techStack.toolkit.trim().length === 0) {
|
|
212
212
|
errors.push({
|
|
@@ -214,6 +214,12 @@ function validateTechStack(model) {
|
|
|
214
214
|
message: 'Toolkit is required',
|
|
215
215
|
});
|
|
216
216
|
}
|
|
217
|
+
else if (validToolkitIds && !validToolkitIds.includes(model.techStack.toolkit)) {
|
|
218
|
+
errors.push({
|
|
219
|
+
path: 'techStack.toolkit',
|
|
220
|
+
message: `Unknown toolkit "${model.techStack.toolkit}". Valid toolkits: ${validToolkitIds.join(', ')}`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
217
223
|
return errors;
|
|
218
224
|
}
|
|
219
225
|
function validateMeta(model) {
|
|
@@ -235,10 +241,7 @@ function validateMeta(model) {
|
|
|
235
241
|
}
|
|
236
242
|
return errors;
|
|
237
243
|
}
|
|
238
|
-
|
|
239
|
-
// Public API
|
|
240
|
-
// =============================================================================
|
|
241
|
-
export function validateModel(model) {
|
|
244
|
+
export function validateModel(model, options) {
|
|
242
245
|
const errors = [];
|
|
243
246
|
errors.push(...validateIdentity(model));
|
|
244
247
|
// Entity uniqueness
|
|
@@ -260,7 +263,7 @@ export function validateModel(model) {
|
|
|
260
263
|
errors.push(...validateLayout(model));
|
|
261
264
|
errors.push(...validateFeatures(model));
|
|
262
265
|
errors.push(...validateTheme(model));
|
|
263
|
-
errors.push(...validateTechStack(model));
|
|
266
|
+
errors.push(...validateTechStack(model, options?.validToolkitIds));
|
|
264
267
|
errors.push(...validateMeta(model));
|
|
265
268
|
return {
|
|
266
269
|
valid: errors.length === 0,
|
package/dist/model/tools.d.ts
CHANGED
|
@@ -10,5 +10,6 @@ import type { PlatformContext } from '@compilr-dev/sdk';
|
|
|
10
10
|
export interface ModelToolsConfig {
|
|
11
11
|
readonly context: PlatformContext;
|
|
12
12
|
readonly cwd?: string;
|
|
13
|
+
readonly validToolkitIds?: readonly string[];
|
|
13
14
|
}
|
|
14
15
|
export declare function createModelTools(config: ModelToolsConfig): Tool<never>[];
|
package/dist/model/tools.js
CHANGED
|
@@ -222,7 +222,9 @@ EXAMPLES:
|
|
|
222
222
|
// Apply
|
|
223
223
|
const updated = applyOperation(model, operation);
|
|
224
224
|
// Validate result
|
|
225
|
-
const validation = validateModel(updated
|
|
225
|
+
const validation = validateModel(updated, {
|
|
226
|
+
validToolkitIds: config.validToolkitIds,
|
|
227
|
+
});
|
|
226
228
|
if (!validation.valid) {
|
|
227
229
|
const msgs = validation.errors.map((e) => `${e.path}: ${e.message}`).join('; ');
|
|
228
230
|
return createErrorResult(`Operation would produce invalid model: ${msgs}`);
|
|
@@ -422,7 +424,7 @@ function createAppModelValidateTool(config) {
|
|
|
422
424
|
errors: [{ path: '', message: 'No Application Model found' }],
|
|
423
425
|
});
|
|
424
426
|
}
|
|
425
|
-
const result = validateModel(model);
|
|
427
|
+
const result = validateModel(model, { validToolkitIds: config.validToolkitIds });
|
|
426
428
|
return createSuccessResult({
|
|
427
429
|
valid: result.valid,
|
|
428
430
|
errors: result.errors,
|
|
@@ -7,16 +7,21 @@ import { toKebabCase, toCamelCase } from '../../model/naming.js';
|
|
|
7
7
|
export function generateDashboard(model) {
|
|
8
8
|
if (!model.features.dashboard)
|
|
9
9
|
return [];
|
|
10
|
+
const typeImports = model.entities.map((e) => e.name).join(', ');
|
|
11
|
+
// State: counts + recent items
|
|
12
|
+
const stateLines = model.entities
|
|
13
|
+
.map((e) => {
|
|
14
|
+
const camel = toCamelCase(e.name);
|
|
15
|
+
return ` const [${camel}Count, set${e.name}Count] = useState(0);\n const [recent${e.name}s, setRecent${e.name}s] = useState<${e.name}[]>([]);`;
|
|
16
|
+
})
|
|
17
|
+
.join('\n');
|
|
18
|
+
// Fetch: counts + recent items
|
|
10
19
|
const fetchCalls = model.entities
|
|
11
20
|
.map((e) => {
|
|
12
21
|
const apiUrl = '/api/' + toKebabCase(e.pluralName).toLowerCase();
|
|
13
|
-
return ` fetch('${apiUrl}').then(r => r.json()).then((d: ${e.name}[]) => set${e.name}Count(d.length)),`;
|
|
22
|
+
return ` fetch('${apiUrl}').then(r => r.json()).then((d: ${e.name}[]) => { set${e.name}Count(d.length); setRecent${e.name}s(d.slice(-5).reverse()); }),`;
|
|
14
23
|
})
|
|
15
24
|
.join('\n');
|
|
16
|
-
const stateLines = model.entities
|
|
17
|
-
.map((e) => ` const [${toCamelCase(e.name)}Count, set${e.name}Count] = useState(0);`)
|
|
18
|
-
.join('\n');
|
|
19
|
-
const typeImports = model.entities.map((e) => e.name).join(', ');
|
|
20
25
|
const cards = model.entities
|
|
21
26
|
.map((e) => {
|
|
22
27
|
const path = '/' + toKebabCase(e.pluralName).toLowerCase();
|
|
@@ -27,6 +32,21 @@ export function generateDashboard(model) {
|
|
|
27
32
|
</Link>`;
|
|
28
33
|
})
|
|
29
34
|
.join('\n');
|
|
35
|
+
// Find first string field per entity for display name
|
|
36
|
+
const recentRows = model.entities
|
|
37
|
+
.map((e) => {
|
|
38
|
+
const firstStringField = e.fields.find((f) => f.type === 'string');
|
|
39
|
+
const displayExpr = firstStringField ? `item.${firstStringField.name}` : `String(item.id)`;
|
|
40
|
+
const path = '/' + toKebabCase(e.pluralName).toLowerCase();
|
|
41
|
+
return ` {recent${e.name}s.map((item) => (
|
|
42
|
+
<tr key={item.id}>
|
|
43
|
+
<td className="px-4 py-2 text-sm">${e.icon} ${e.name}</td>
|
|
44
|
+
<td className="px-4 py-2 text-sm"><Link to={\`${path}/\${String(item.id)}\`} className="text-primary hover:underline">{${displayExpr}}</Link></td>
|
|
45
|
+
<td className="px-4 py-2 text-sm text-gray-500">{String(item.id)}</td>
|
|
46
|
+
</tr>
|
|
47
|
+
))}`;
|
|
48
|
+
})
|
|
49
|
+
.join('\n');
|
|
30
50
|
return [
|
|
31
51
|
{
|
|
32
52
|
path: 'src/pages/Dashboard.tsx',
|
|
@@ -51,6 +71,22 @@ ${fetchCalls}
|
|
|
51
71
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-${String(Math.min(model.entities.length, 4))}">
|
|
52
72
|
${cards}
|
|
53
73
|
</div>
|
|
74
|
+
|
|
75
|
+
<h2 className="mb-4 mt-8 text-xl font-bold text-gray-900 dark:text-white">Recent Activity</h2>
|
|
76
|
+
<div className="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
|
77
|
+
<table className="w-full text-left">
|
|
78
|
+
<thead className="bg-gray-50 dark:bg-gray-800">
|
|
79
|
+
<tr>
|
|
80
|
+
<th className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400">Type</th>
|
|
81
|
+
<th className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400">Name</th>
|
|
82
|
+
<th className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400">ID</th>
|
|
83
|
+
</tr>
|
|
84
|
+
</thead>
|
|
85
|
+
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
86
|
+
${recentRows}
|
|
87
|
+
</tbody>
|
|
88
|
+
</table>
|
|
89
|
+
</div>
|
|
54
90
|
</div>
|
|
55
91
|
);
|
|
56
92
|
}
|
|
@@ -7,6 +7,7 @@ import { toKebabCase } from '../../model/naming.js';
|
|
|
7
7
|
export function generateRouter(model) {
|
|
8
8
|
const imports = [];
|
|
9
9
|
const routes = [];
|
|
10
|
+
const topLevelRoutes = [];
|
|
10
11
|
// Layout import
|
|
11
12
|
imports.push("import Layout from './components/layout/Layout';");
|
|
12
13
|
// Dashboard
|
|
@@ -28,6 +29,24 @@ export function generateRouter(model) {
|
|
|
28
29
|
routes.push(` { path: '${routeBase}/:id', element: <${entity.name}Detail /> },`);
|
|
29
30
|
routes.push(` { path: '${routeBase}/:id/edit', element: <${entity.name}Form /> },`);
|
|
30
31
|
}
|
|
32
|
+
// Settings route (inside Layout)
|
|
33
|
+
if (model.features.settings) {
|
|
34
|
+
imports.push("import Settings from './pages/Settings';");
|
|
35
|
+
routes.push(" { path: 'settings', element: <Settings /> },");
|
|
36
|
+
}
|
|
37
|
+
// Profile route (inside Layout)
|
|
38
|
+
if (model.features.userProfiles) {
|
|
39
|
+
imports.push("import Profile from './pages/Profile';");
|
|
40
|
+
routes.push(" { path: 'profile', element: <Profile /> },");
|
|
41
|
+
}
|
|
42
|
+
// Auth routes (top-level, outside Layout)
|
|
43
|
+
if (model.features.auth) {
|
|
44
|
+
imports.push("import Login from './pages/Login';");
|
|
45
|
+
imports.push("import Register from './pages/Register';");
|
|
46
|
+
topLevelRoutes.push(" { path: '/login', element: <Login /> },");
|
|
47
|
+
topLevelRoutes.push(" { path: '/register', element: <Register /> },");
|
|
48
|
+
}
|
|
49
|
+
const topLevelSection = topLevelRoutes.length > 0 ? '\n' + topLevelRoutes.join('\n') : '';
|
|
31
50
|
return [
|
|
32
51
|
{
|
|
33
52
|
path: 'src/router.tsx',
|
|
@@ -41,7 +60,7 @@ export const router = createBrowserRouter([
|
|
|
41
60
|
children: [
|
|
42
61
|
${routes.join('\n')}
|
|
43
62
|
],
|
|
44
|
-
}
|
|
63
|
+
},${topLevelSection}
|
|
45
64
|
]);
|
|
46
65
|
`,
|
|
47
66
|
},
|
|
@@ -5,7 +5,22 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { toKebabCase } from '../../model/naming.js';
|
|
7
7
|
export function generateShellFiles(model) {
|
|
8
|
-
|
|
8
|
+
const files = [
|
|
9
|
+
generateApp(model),
|
|
10
|
+
generateLayout(model),
|
|
11
|
+
generateSidebar(model),
|
|
12
|
+
generateHeader(model),
|
|
13
|
+
];
|
|
14
|
+
if (model.features.settings) {
|
|
15
|
+
files.push(generateSettingsPage());
|
|
16
|
+
}
|
|
17
|
+
if (model.features.auth) {
|
|
18
|
+
files.push(...generateAuthPages());
|
|
19
|
+
}
|
|
20
|
+
if (model.features.userProfiles) {
|
|
21
|
+
files.push(generateProfilePage());
|
|
22
|
+
}
|
|
23
|
+
return files;
|
|
9
24
|
}
|
|
10
25
|
function generateApp(_model) {
|
|
11
26
|
return {
|
|
@@ -63,15 +78,21 @@ export default function Layout() {
|
|
|
63
78
|
};
|
|
64
79
|
}
|
|
65
80
|
function generateSidebar(model) {
|
|
66
|
-
const
|
|
67
|
-
|
|
81
|
+
const items = [];
|
|
82
|
+
if (model.features.dashboard) {
|
|
83
|
+
items.push(" { label: '\u{1F4CA} Dashboard', path: '/' },");
|
|
84
|
+
}
|
|
85
|
+
for (const e of model.entities) {
|
|
68
86
|
const path = '/' + toKebabCase(e.pluralName).toLowerCase();
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
87
|
+
items.push(` { label: '${e.icon} ${e.pluralName}', path: '${path}' },`);
|
|
88
|
+
}
|
|
89
|
+
if (model.features.settings) {
|
|
90
|
+
items.push(" { label: '\u2699\uFE0F Settings', path: '/settings' },");
|
|
91
|
+
}
|
|
92
|
+
const navItems = items.join('\n');
|
|
72
93
|
return {
|
|
73
94
|
path: 'src/components/layout/Sidebar.tsx',
|
|
74
|
-
content: `import { useState } from 'react';
|
|
95
|
+
content: `import { useState, useEffect } from 'react';
|
|
75
96
|
import { Link, useLocation } from 'react-router-dom';
|
|
76
97
|
|
|
77
98
|
const navItems = [
|
|
@@ -79,9 +100,15 @@ ${navItems}
|
|
|
79
100
|
];
|
|
80
101
|
|
|
81
102
|
export default function Sidebar() {
|
|
82
|
-
const [collapsed, setCollapsed] = useState(
|
|
103
|
+
const [collapsed, setCollapsed] = useState(window.innerWidth < 1024);
|
|
83
104
|
const location = useLocation();
|
|
84
105
|
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const onResize = () => setCollapsed(window.innerWidth < 1024);
|
|
108
|
+
window.addEventListener('resize', onResize);
|
|
109
|
+
return () => window.removeEventListener('resize', onResize);
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
85
112
|
return (
|
|
86
113
|
<aside
|
|
87
114
|
className={\`\${collapsed ? 'w-16' : 'w-64'} flex flex-col border-r border-gray-200 bg-white transition-all dark:border-gray-700 dark:bg-gray-800\`}
|
|
@@ -121,6 +148,7 @@ export default function Sidebar() {
|
|
|
121
148
|
};
|
|
122
149
|
}
|
|
123
150
|
function generateHeader(model) {
|
|
151
|
+
const hasSidebar = model.layout.shell === 'sidebar-header';
|
|
124
152
|
const darkModeToggle = model.features.darkMode
|
|
125
153
|
? `
|
|
126
154
|
<button
|
|
@@ -130,6 +158,10 @@ function generateHeader(model) {
|
|
|
130
158
|
🌓
|
|
131
159
|
</button>`
|
|
132
160
|
: '';
|
|
161
|
+
const appNameSection = !hasSidebar
|
|
162
|
+
? `
|
|
163
|
+
<span className="text-lg font-bold text-gray-900 dark:text-white">${model.identity.name}</span>`
|
|
164
|
+
: '';
|
|
133
165
|
return {
|
|
134
166
|
path: 'src/components/layout/Header.tsx',
|
|
135
167
|
content: `import { useLocation } from 'react-router-dom';
|
|
@@ -141,7 +173,9 @@ export default function Header() {
|
|
|
141
173
|
|
|
142
174
|
return (
|
|
143
175
|
<header className="flex h-14 items-center justify-between border-b border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-800">
|
|
144
|
-
<
|
|
176
|
+
<div className="flex items-center gap-4">${appNameSection}
|
|
177
|
+
<span className="text-sm text-gray-500 dark:text-gray-400">{breadcrumb}</span>
|
|
178
|
+
</div>
|
|
145
179
|
<div className="flex items-center gap-2">${darkModeToggle}
|
|
146
180
|
</div>
|
|
147
181
|
</header>
|
|
@@ -150,3 +184,105 @@ export default function Header() {
|
|
|
150
184
|
`,
|
|
151
185
|
};
|
|
152
186
|
}
|
|
187
|
+
function generateSettingsPage() {
|
|
188
|
+
return {
|
|
189
|
+
path: 'src/pages/Settings.tsx',
|
|
190
|
+
content: `export default function Settings() {
|
|
191
|
+
return (
|
|
192
|
+
<div>
|
|
193
|
+
<h1 className="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Settings</h1>
|
|
194
|
+
<p className="text-gray-500 dark:text-gray-400">
|
|
195
|
+
Application settings will be configured here.
|
|
196
|
+
</p>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function generateAuthPages() {
|
|
204
|
+
return [
|
|
205
|
+
{
|
|
206
|
+
path: 'src/pages/Login.tsx',
|
|
207
|
+
content: `import { Link } from 'react-router-dom';
|
|
208
|
+
|
|
209
|
+
export default function Login() {
|
|
210
|
+
return (
|
|
211
|
+
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
|
|
212
|
+
<div className="w-full max-w-md rounded-lg border border-gray-200 bg-white p-8 shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
|
213
|
+
<h1 className="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Login</h1>
|
|
214
|
+
<form className="space-y-4">
|
|
215
|
+
<div>
|
|
216
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
|
|
217
|
+
<input type="email" className="mt-1 block w-full rounded border border-gray-300 px-3 py-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
|
218
|
+
</div>
|
|
219
|
+
<div>
|
|
220
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
|
|
221
|
+
<input type="password" className="mt-1 block w-full rounded border border-gray-300 px-3 py-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
|
222
|
+
</div>
|
|
223
|
+
<button type="submit" className="w-full rounded bg-primary px-4 py-2 text-white hover:bg-primary/90">
|
|
224
|
+
Sign In
|
|
225
|
+
</button>
|
|
226
|
+
</form>
|
|
227
|
+
<p className="mt-4 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
228
|
+
Don't have an account? <Link to="/register" className="text-primary hover:underline">Register</Link>
|
|
229
|
+
</p>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
`,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
path: 'src/pages/Register.tsx',
|
|
238
|
+
content: `import { Link } from 'react-router-dom';
|
|
239
|
+
|
|
240
|
+
export default function Register() {
|
|
241
|
+
return (
|
|
242
|
+
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
|
|
243
|
+
<div className="w-full max-w-md rounded-lg border border-gray-200 bg-white p-8 shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
|
244
|
+
<h1 className="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Register</h1>
|
|
245
|
+
<form className="space-y-4">
|
|
246
|
+
<div>
|
|
247
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
|
|
248
|
+
<input type="text" className="mt-1 block w-full rounded border border-gray-300 px-3 py-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
|
249
|
+
</div>
|
|
250
|
+
<div>
|
|
251
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
|
|
252
|
+
<input type="email" className="mt-1 block w-full rounded border border-gray-300 px-3 py-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
|
253
|
+
</div>
|
|
254
|
+
<div>
|
|
255
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
|
|
256
|
+
<input type="password" className="mt-1 block w-full rounded border border-gray-300 px-3 py-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
|
257
|
+
</div>
|
|
258
|
+
<button type="submit" className="w-full rounded bg-primary px-4 py-2 text-white hover:bg-primary/90">
|
|
259
|
+
Create Account
|
|
260
|
+
</button>
|
|
261
|
+
</form>
|
|
262
|
+
<p className="mt-4 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
263
|
+
Already have an account? <Link to="/login" className="text-primary hover:underline">Login</Link>
|
|
264
|
+
</p>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
`,
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
function generateProfilePage() {
|
|
274
|
+
return {
|
|
275
|
+
path: 'src/pages/Profile.tsx',
|
|
276
|
+
content: `export default function Profile() {
|
|
277
|
+
return (
|
|
278
|
+
<div>
|
|
279
|
+
<h1 className="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Profile</h1>
|
|
280
|
+
<p className="text-gray-500 dark:text-gray-400">
|
|
281
|
+
User profile information will be displayed here.
|
|
282
|
+
</p>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
@@ -32,12 +32,26 @@ VITE_API_URL=http://localhost:3001
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
function generateReadme(model) {
|
|
35
|
+
let entitiesSection = '';
|
|
36
|
+
if (model.entities.length > 0) {
|
|
37
|
+
const entityLines = model.entities
|
|
38
|
+
.map((e) => {
|
|
39
|
+
const relCount = e.relationships.length;
|
|
40
|
+
const parts = [`${String(e.fields.length)} fields`];
|
|
41
|
+
if (relCount > 0) {
|
|
42
|
+
parts.push(`${String(relCount)} relationship${relCount > 1 ? 's' : ''}`);
|
|
43
|
+
}
|
|
44
|
+
return `- **${e.name}** (${e.icon}) — ${parts.join(', ')}`;
|
|
45
|
+
})
|
|
46
|
+
.join('\n');
|
|
47
|
+
entitiesSection = `\n## Entities\n\n${entityLines}\n`;
|
|
48
|
+
}
|
|
35
49
|
return {
|
|
36
50
|
path: 'README.md',
|
|
37
51
|
content: `# ${model.identity.name}
|
|
38
52
|
|
|
39
53
|
${model.identity.description}
|
|
40
|
-
|
|
54
|
+
${entitiesSection}
|
|
41
55
|
## Getting Started
|
|
42
56
|
|
|
43
57
|
\`\`\`bash
|