@compilr-dev/factory 0.1.7 → 0.1.8

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 (46) hide show
  1. package/dist/factory/registry.js +2 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +2 -0
  4. package/dist/toolkits/next-prisma/api-routes.d.ts +10 -0
  5. package/dist/toolkits/next-prisma/api-routes.js +155 -0
  6. package/dist/toolkits/next-prisma/config.d.ts +9 -0
  7. package/dist/toolkits/next-prisma/config.js +139 -0
  8. package/dist/toolkits/next-prisma/dashboard.d.ts +9 -0
  9. package/dist/toolkits/next-prisma/dashboard.js +87 -0
  10. package/dist/toolkits/next-prisma/entity-components.d.ts +10 -0
  11. package/dist/toolkits/next-prisma/entity-components.js +217 -0
  12. package/dist/toolkits/next-prisma/entity-pages.d.ts +12 -0
  13. package/dist/toolkits/next-prisma/entity-pages.js +348 -0
  14. package/dist/toolkits/next-prisma/helpers.d.ts +13 -0
  15. package/dist/toolkits/next-prisma/helpers.js +37 -0
  16. package/dist/toolkits/next-prisma/index.d.ts +9 -0
  17. package/dist/toolkits/next-prisma/index.js +57 -0
  18. package/dist/toolkits/next-prisma/layout.d.ts +9 -0
  19. package/dist/toolkits/next-prisma/layout.js +157 -0
  20. package/dist/toolkits/next-prisma/prisma.d.ts +8 -0
  21. package/dist/toolkits/next-prisma/prisma.js +76 -0
  22. package/dist/toolkits/next-prisma/seed.d.ts +9 -0
  23. package/dist/toolkits/next-prisma/seed.js +100 -0
  24. package/dist/toolkits/next-prisma/static.d.ts +8 -0
  25. package/dist/toolkits/next-prisma/static.js +61 -0
  26. package/dist/toolkits/next-prisma/types-gen.d.ts +10 -0
  27. package/dist/toolkits/next-prisma/types-gen.js +62 -0
  28. package/dist/toolkits/react-node/config.d.ts +1 -4
  29. package/dist/toolkits/react-node/config.js +3 -84
  30. package/dist/toolkits/react-node/helpers.d.ts +2 -23
  31. package/dist/toolkits/react-node/helpers.js +2 -67
  32. package/dist/toolkits/react-node/seed.d.ts +4 -3
  33. package/dist/toolkits/react-node/seed.js +4 -111
  34. package/dist/toolkits/react-node/shared.d.ts +2 -3
  35. package/dist/toolkits/react-node/shared.js +2 -115
  36. package/dist/toolkits/shared/color-utils.d.ts +10 -0
  37. package/dist/toolkits/shared/color-utils.js +85 -0
  38. package/dist/toolkits/shared/components.d.ts +12 -0
  39. package/dist/toolkits/shared/components.js +121 -0
  40. package/dist/toolkits/shared/helpers.d.ts +28 -0
  41. package/dist/toolkits/shared/helpers.js +72 -0
  42. package/dist/toolkits/shared/index.d.ts +9 -0
  43. package/dist/toolkits/shared/index.js +9 -0
  44. package/dist/toolkits/shared/seed-data.d.ts +18 -0
  45. package/dist/toolkits/shared/seed-data.js +119 -0
  46. package/package.json +1 -1
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — Entity Pages Generator
3
+ *
4
+ * For each entity generates:
5
+ * app/[route]/page.tsx — List page (Server + Client)
6
+ * app/[route]/[id]/page.tsx — Detail page (Server Component)
7
+ * app/[route]/new/page.tsx — New form wrapper
8
+ * app/[route]/[id]/edit/page.tsx — Edit form wrapper
9
+ */
10
+ import { toCamelCase } from '../../model/naming.js';
11
+ import { belongsToRels, hasManyRels, appRoutePath } from './helpers.js';
12
+ export function generateEntityPages(model) {
13
+ return model.entities.flatMap((entity) => [
14
+ generateListPage(model, entity),
15
+ generateDetailPage(model, entity),
16
+ generateNewPage(entity),
17
+ generateEditPage(model, entity),
18
+ ]);
19
+ }
20
+ // =============================================================================
21
+ // List Page — Server Component + Client Component for filtering
22
+ // =============================================================================
23
+ function generateListPage(model, entity) {
24
+ const route = appRoutePath(entity);
25
+ const camel = toCamelCase(entity.name);
26
+ const enumFields = entity.fields.filter((f) => f.type === 'enum' && f.enumValues?.length);
27
+ const hasFilters = enumFields.length > 0;
28
+ const filterDefs = hasFilters
29
+ ? enumFields
30
+ .map((f) => ` { label: '${f.label}', field: '${f.name}', values: [${(f.enumValues ?? []).map((v) => `'${v}'`).join(', ')}] },`)
31
+ .join('\n')
32
+ : '';
33
+ const tableHeaders = entity.fields
34
+ .slice(0, 5)
35
+ .map((f) => ` <th
36
+ className="cursor-pointer select-none px-4 py-2 text-left text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
37
+ onClick={() => handleSort('${f.name}')}
38
+ >
39
+ ${f.label}{sortField === '${f.name}' ? (sortDirection === 'asc' ? ' ↑' : ' ↓') : ''}
40
+ </th>`)
41
+ .join('\n');
42
+ const tableCells = entity.fields
43
+ .slice(0, 5)
44
+ .map((f) => ` <td className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300">{String(item.${f.name})}</td>`)
45
+ .join('\n');
46
+ return {
47
+ path: `app/${route}/page.tsx`,
48
+ content: `import { prisma } from '@/lib/prisma';
49
+ import ${entity.name}ListClient from './${entity.name}ListClient';
50
+
51
+ export default async function ${entity.name}Page() {
52
+ const items = await prisma.${camel}.findMany({
53
+ orderBy: { createdAt: 'desc' },
54
+ });
55
+
56
+ return <${entity.name}ListClient items={JSON.parse(JSON.stringify(items))} />;
57
+ }
58
+
59
+ // --- Client Component ---
60
+ // This is in the same file for simplicity; can be extracted to a separate file.
61
+
62
+ 'use client';
63
+
64
+ import { useState, useCallback } from 'react';
65
+ import Link from 'next/link';
66
+ import ViewToggle from '@/components/shared/ViewToggle';
67
+ import SearchBar from '@/components/shared/SearchBar';${hasFilters ? "\nimport FilterBar from '@/components/shared/FilterBar';" : ''}
68
+ import ${entity.name}Card from '@/components/${route}/${entity.name}Card';
69
+
70
+ ${hasFilters ? `const filterOptions = [\n${filterDefs}\n];\n` : ''}interface ${entity.name}ListClientProps {
71
+ items: Array<Record<string, unknown>>;
72
+ }
73
+
74
+ function ${entity.name}ListClient({ items: initialItems }: ${entity.name}ListClientProps) {
75
+ const [view, setView] = useState<'card' | 'list'>('card');
76
+ const [search, setSearch] = useState('');
77
+ const [sortField, setSortField] = useState<string>('');
78
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');${hasFilters ? '\n const [filters, setFilters] = useState<Record<string, string>>({});' : ''}
79
+
80
+ const onSearch = useCallback((q: string) => setSearch(q), []);
81
+
82
+ const handleSort = useCallback((field: string) => {
83
+ if (sortField === field) {
84
+ setSortDirection((d) => (d === 'asc' ? 'desc' : 'asc'));
85
+ } else {
86
+ setSortField(field);
87
+ setSortDirection('asc');
88
+ }
89
+ }, [sortField]);
90
+
91
+ const filtered = initialItems.filter((item) => {
92
+ if (search) {
93
+ const q = search.toLowerCase();
94
+ const match = Object.values(item).some((v) =>
95
+ String(v).toLowerCase().includes(q),
96
+ );
97
+ if (!match) return false;
98
+ }${hasFilters
99
+ ? `
100
+ for (const [field, value] of Object.entries(filters)) {
101
+ if (value && String(item[field]) !== value) return false;
102
+ }`
103
+ : ''}
104
+ return true;
105
+ });
106
+
107
+ const sorted = [...filtered].sort((a, b) => {
108
+ if (!sortField) return 0;
109
+ const aVal = a[sortField];
110
+ const bVal = b[sortField];
111
+ const cmp = String(aVal ?? '').localeCompare(String(bVal ?? ''), undefined, { numeric: true });
112
+ return sortDirection === 'asc' ? cmp : -cmp;
113
+ });
114
+
115
+ return (
116
+ <div>
117
+ <div className="mb-6 flex items-center justify-between">
118
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-white">${entity.icon} ${entity.pluralName}</h1>
119
+ <div className="flex items-center gap-3">
120
+ <ViewToggle view={view} onChange={setView} />
121
+ <Link
122
+ href="/${route}/new"
123
+ className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:opacity-90"
124
+ >
125
+ + New ${entity.name}
126
+ </Link>
127
+ </div>
128
+ </div>
129
+
130
+ <div className="mb-4 flex flex-wrap items-center gap-3">
131
+ <div className="w-64">
132
+ <SearchBar onSearch={onSearch} placeholder="Search ${entity.pluralName.toLowerCase()}..." />${hasFilters ? '\n </div>\n <FilterBar filters={filterOptions} active={filters} onChange={(f, v) => setFilters({ ...filters, [f]: v })} />' : '\n </div>'}
133
+ </div>
134
+
135
+ {view === 'card' ? (
136
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
137
+ {sorted.map((item) => (
138
+ <${entity.name}Card key={String(item.id)} item={item} />
139
+ ))}
140
+ </div>
141
+ ) : (
142
+ <table className="w-full border-collapse">
143
+ <thead>
144
+ <tr className="border-b border-gray-200 dark:border-gray-700">
145
+ ${tableHeaders}
146
+ </tr>
147
+ </thead>
148
+ <tbody>
149
+ {sorted.map((item) => (
150
+ <tr key={String(item.id)} className="border-b border-gray-100 hover:bg-gray-50 dark:border-gray-800 dark:hover:bg-gray-800/50">
151
+ ${tableCells}
152
+ <td className="px-4 py-2">
153
+ <Link href={\`/${route}/\${String(item.id)}\`} className="text-sm text-primary hover:underline">View</Link>
154
+ </td>
155
+ </tr>
156
+ ))}
157
+ </tbody>
158
+ </table>
159
+ )}
160
+ </div>
161
+ );
162
+ }
163
+ `,
164
+ };
165
+ }
166
+ // =============================================================================
167
+ // Detail Page — Server Component
168
+ // =============================================================================
169
+ function generateDetailPage(model, entity) {
170
+ const route = appRoutePath(entity);
171
+ const camel = toCamelCase(entity.name);
172
+ const fieldRows = entity.fields
173
+ .map((f) => ` <div>
174
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${f.label}</dt>
175
+ <dd className="mt-1 text-gray-900 dark:text-white">{String(item.${f.name}${f.required ? '' : " ?? '—'"})}</dd>
176
+ </div>`)
177
+ .join('\n');
178
+ const btoRels = belongsToRels(entity);
179
+ const hmRels = hasManyRels(entity);
180
+ // Build include object for belongsTo and hasMany
181
+ const includeEntries = [];
182
+ for (const rel of btoRels) {
183
+ const relField = toCamelCase(rel.target);
184
+ includeEntries.push(` ${relField}: true,`);
185
+ }
186
+ for (const rel of hmRels) {
187
+ const targetEntity = model.entities.find((e) => e.name === rel.target);
188
+ if (!targetEntity)
189
+ continue;
190
+ const relField = toCamelCase(targetEntity.pluralName);
191
+ includeEntries.push(` ${relField}: true,`);
192
+ }
193
+ const includeClause = includeEntries.length > 0 ? `\n include: {\n${includeEntries.join('\n')}\n },` : '';
194
+ const relLinks = btoRels
195
+ .map((rel) => {
196
+ const relField = toCamelCase(rel.target);
197
+ const targetEntity = model.entities.find((e) => e.name === rel.target);
198
+ const targetRoute = targetEntity ? appRoutePath(targetEntity) : toCamelCase(rel.target);
199
+ return ` <div>
200
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${rel.target}</dt>
201
+ <dd className="mt-1">
202
+ {item.${relField} && (
203
+ <Link href={\`/${targetRoute}/\${String(item.${relField}.id)}\`} className="text-primary hover:underline">
204
+ View ${rel.target}
205
+ </Link>
206
+ )}
207
+ </dd>
208
+ </div>`;
209
+ })
210
+ .join('\n');
211
+ const hasManyRender = hmRels
212
+ .map((rel) => {
213
+ const targetEntity = model.entities.find((e) => e.name === rel.target);
214
+ if (!targetEntity)
215
+ return '';
216
+ const targetRoute = appRoutePath(targetEntity);
217
+ const relField = toCamelCase(targetEntity.pluralName);
218
+ const firstField = targetEntity.fields[0]?.name ?? 'id';
219
+ return `
220
+ <div className="mt-8">
221
+ <h2 className="mb-4 text-lg font-bold text-gray-900 dark:text-white">${targetEntity.icon} ${targetEntity.pluralName}</h2>
222
+ <div className="space-y-2">
223
+ {item.${relField}.map((rel: Record<string, unknown>) => (
224
+ <Link
225
+ key={String(rel.id)}
226
+ href={\`/${targetRoute}/\${String(rel.id)}\`}
227
+ className="block rounded-lg border border-gray-200 p-3 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800"
228
+ >
229
+ {String(rel.${firstField})}
230
+ </Link>
231
+ ))}
232
+ {item.${relField}.length === 0 && (
233
+ <p className="text-sm text-gray-500">No ${targetEntity.pluralName.toLowerCase()} found.</p>
234
+ )}
235
+ </div>
236
+ </div>`;
237
+ })
238
+ .join('');
239
+ return {
240
+ path: `app/${route}/[id]/page.tsx`,
241
+ content: `import { notFound } from 'next/navigation';
242
+ import Link from 'next/link';
243
+ import { prisma } from '@/lib/prisma';
244
+ import ${entity.name}DeleteButton from './${entity.name}DeleteButton';
245
+
246
+ export default async function ${entity.name}Detail({ params }: { params: { id: string } }) {
247
+ const item = await prisma.${camel}.findUnique({
248
+ where: { id: Number(params.id) },${includeClause}
249
+ }) as Record<string, unknown> | null;
250
+
251
+ if (!item) notFound();
252
+
253
+ return (
254
+ <div>
255
+ <div className="mb-6 flex items-center justify-between">
256
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-white">${entity.icon} ${entity.name} Detail</h1>
257
+ <div className="flex gap-2">
258
+ <Link
259
+ href={\`/${route}/\${String(params.id)}/edit\`}
260
+ className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:opacity-90"
261
+ >
262
+ Edit
263
+ </Link>
264
+ <${entity.name}DeleteButton id={Number(params.id)} />
265
+ </div>
266
+ </div>
267
+
268
+ <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
269
+ ${fieldRows}
270
+ <div>
271
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Created</dt>
272
+ <dd className="mt-1 text-gray-900 dark:text-white">{new Date(item.createdAt as string).toLocaleString()}</dd>
273
+ </div>
274
+ <div>
275
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Last Updated</dt>
276
+ <dd className="mt-1 text-gray-900 dark:text-white">{new Date(item.updatedAt as string).toLocaleString()}</dd>
277
+ </div>
278
+ ${relLinks}
279
+ </dl>${hasManyRender}
280
+ </div>
281
+ );
282
+ }
283
+
284
+ // --- Delete Button (Client Component) ---
285
+ 'use client';
286
+
287
+ import { useRouter } from 'next/navigation';
288
+
289
+ function ${entity.name}DeleteButton({ id }: { id: number }) {
290
+ const router = useRouter();
291
+
292
+ const handleDelete = async () => {
293
+ if (!confirm('Are you sure you want to delete this ${entity.name.toLowerCase()}?')) return;
294
+ await fetch(\`/api/${route}/\${String(id)}\`, { method: 'DELETE' });
295
+ router.push('/${route}');
296
+ };
297
+
298
+ return (
299
+ <button
300
+ onClick={handleDelete}
301
+ className="rounded-lg bg-red-500 px-4 py-2 text-sm font-medium text-white hover:opacity-90"
302
+ >
303
+ Delete
304
+ </button>
305
+ );
306
+ }
307
+ `,
308
+ };
309
+ }
310
+ // =============================================================================
311
+ // New Page
312
+ // =============================================================================
313
+ function generateNewPage(entity) {
314
+ const route = appRoutePath(entity);
315
+ return {
316
+ path: `app/${route}/new/page.tsx`,
317
+ content: `import ${entity.name}Form from '@/components/${route}/${entity.name}Form';
318
+
319
+ export default function New${entity.name}Page() {
320
+ return <${entity.name}Form />;
321
+ }
322
+ `,
323
+ };
324
+ }
325
+ // =============================================================================
326
+ // Edit Page — Server Component that fetches data and passes to Client Form
327
+ // =============================================================================
328
+ function generateEditPage(model, entity) {
329
+ const route = appRoutePath(entity);
330
+ const camel = toCamelCase(entity.name);
331
+ return {
332
+ path: `app/${route}/[id]/edit/page.tsx`,
333
+ content: `import { notFound } from 'next/navigation';
334
+ import { prisma } from '@/lib/prisma';
335
+ import ${entity.name}Form from '@/components/${route}/${entity.name}Form';
336
+
337
+ export default async function Edit${entity.name}Page({ params }: { params: { id: string } }) {
338
+ const item = await prisma.${camel}.findUnique({
339
+ where: { id: Number(params.id) },
340
+ });
341
+
342
+ if (!item) notFound();
343
+
344
+ return <${entity.name}Form initialData={JSON.parse(JSON.stringify(item))} />;
345
+ }
346
+ `,
347
+ };
348
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — Helpers
3
+ *
4
+ * Toolkit-specific helper functions for the next-prisma generator.
5
+ */
6
+ import type { Entity, Field } from '../../model/types.js';
7
+ export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, inputType, } from '../shared/helpers.js';
8
+ /** Map a model FieldType to a Prisma schema type. */
9
+ export declare function prismaFieldType(field: Field): string;
10
+ /** Get the PascalCase model name for Prisma. */
11
+ export declare function prismaModelName(entity: Entity): string;
12
+ /** Get the kebab-case route segment for App Router file paths. */
13
+ export declare function appRoutePath(entity: Entity): string;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — Helpers
3
+ *
4
+ * Toolkit-specific helper functions for the next-prisma generator.
5
+ */
6
+ import { toPascalCase, toKebabCase } from '../../model/naming.js';
7
+ // Re-export shared helpers for convenience
8
+ export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, inputType, } from '../shared/helpers.js';
9
+ /** Map a model FieldType to a Prisma schema type. */
10
+ export function prismaFieldType(field) {
11
+ switch (field.type) {
12
+ case 'string':
13
+ case 'enum':
14
+ return 'String';
15
+ case 'number': {
16
+ const lower = field.name.toLowerCase();
17
+ if (lower.includes('price') || lower.includes('cost') || lower.includes('amount')) {
18
+ return 'Float';
19
+ }
20
+ return 'Int';
21
+ }
22
+ case 'boolean':
23
+ return 'Boolean';
24
+ case 'date':
25
+ return 'DateTime';
26
+ default:
27
+ return 'String';
28
+ }
29
+ }
30
+ /** Get the PascalCase model name for Prisma. */
31
+ export function prismaModelName(entity) {
32
+ return toPascalCase(entity.name);
33
+ }
34
+ /** Get the kebab-case route segment for App Router file paths. */
35
+ export function appRoutePath(entity) {
36
+ return toKebabCase(entity.pluralName).toLowerCase();
37
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit
3
+ *
4
+ * Generates a full-stack app: Next.js 14 App Router + Prisma ORM + Tailwind CSS.
5
+ * Server Components for data fetching, Client Components for interactivity.
6
+ * Deterministic: same ApplicationModel → same output files.
7
+ */
8
+ import type { FactoryToolkit } from '../types.js';
9
+ export declare const nextPrismaToolkit: FactoryToolkit;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit
3
+ *
4
+ * Generates a full-stack app: Next.js 14 App Router + Prisma ORM + Tailwind CSS.
5
+ * Server Components for data fetching, Client Components for interactivity.
6
+ * Deterministic: same ApplicationModel → same output files.
7
+ */
8
+ import { generateConfigFiles } from './config.js';
9
+ import { generatePrismaFiles } from './prisma.js';
10
+ import { generateTypesFile } from './types-gen.js';
11
+ import { generateStaticFiles } from './static.js';
12
+ import { generateSharedComponents } from '../shared/components.js';
13
+ import { generateLayoutFiles } from './layout.js';
14
+ import { generateDashboard } from './dashboard.js';
15
+ import { generateEntityPages } from './entity-pages.js';
16
+ import { generateEntityComponents } from './entity-components.js';
17
+ import { generateApiRoutes } from './api-routes.js';
18
+ import { generateSeedFile } from './seed.js';
19
+ export const nextPrismaToolkit = {
20
+ id: 'next-prisma',
21
+ name: 'Next.js + Prisma',
22
+ description: 'Next.js 14 App Router + Prisma ORM + Tailwind CSS — production-ready full-stack',
23
+ requiredSections: ['identity', 'entities', 'layout', 'features', 'theme'],
24
+ generate(model) {
25
+ const warnings = [];
26
+ const allFiles = [];
27
+ // Collect files from all generators
28
+ const generators = [
29
+ generateConfigFiles(model),
30
+ generatePrismaFiles(model),
31
+ generateTypesFile(model),
32
+ generateStaticFiles(model),
33
+ generateSharedComponents(),
34
+ generateLayoutFiles(model),
35
+ generateDashboard(model),
36
+ generateEntityPages(model),
37
+ generateEntityComponents(model),
38
+ generateApiRoutes(model),
39
+ generateSeedFile(model),
40
+ ];
41
+ for (const files of generators) {
42
+ allFiles.push(...files);
43
+ }
44
+ // Check for potential issues
45
+ if (model.entities.length === 0) {
46
+ warnings.push('No entities defined — generated app will have minimal functionality.');
47
+ }
48
+ if (model.entities.length > 10) {
49
+ warnings.push('Large number of entities — generated sidebar may be crowded.');
50
+ }
51
+ return {
52
+ files: allFiles,
53
+ toolkit: 'next-prisma',
54
+ warnings,
55
+ };
56
+ },
57
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — Layout Generator
3
+ *
4
+ * Generates: app/layout.tsx, app/globals.css,
5
+ * components/layout/Sidebar.tsx, components/layout/Header.tsx
6
+ */
7
+ import type { ApplicationModel } from '../../model/types.js';
8
+ import type { FactoryFile } from '../types.js';
9
+ export declare function generateLayoutFiles(model: ApplicationModel): FactoryFile[];
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — Layout Generator
3
+ *
4
+ * Generates: app/layout.tsx, app/globals.css,
5
+ * components/layout/Sidebar.tsx, components/layout/Header.tsx
6
+ */
7
+ import { appRoutePath } from './helpers.js';
8
+ export function generateLayoutFiles(model) {
9
+ return [
10
+ generateRootLayout(model),
11
+ generateGlobalCss(),
12
+ generateSidebar(model),
13
+ generateHeader(model),
14
+ ];
15
+ }
16
+ function generateRootLayout(model) {
17
+ return {
18
+ path: 'app/layout.tsx',
19
+ content: `import type { Metadata } from 'next';
20
+ import './globals.css';
21
+ import Sidebar from '@/components/layout/Sidebar';
22
+ import Header from '@/components/layout/Header';
23
+
24
+ export const metadata: Metadata = {
25
+ title: '${model.identity.name}',
26
+ description: '${model.identity.description}',
27
+ };
28
+
29
+ export default function RootLayout({
30
+ children,
31
+ }: {
32
+ children: React.ReactNode;
33
+ }) {
34
+ return (
35
+ <html lang="en">
36
+ <body className="bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100">
37
+ <div className="flex h-screen">
38
+ <Sidebar />
39
+ <div className="flex flex-1 flex-col overflow-hidden">
40
+ <Header />
41
+ <main className="flex-1 overflow-y-auto p-6">{children}</main>
42
+ </div>
43
+ </div>
44
+ </body>
45
+ </html>
46
+ );
47
+ }
48
+ `,
49
+ };
50
+ }
51
+ function generateGlobalCss() {
52
+ return {
53
+ path: 'app/globals.css',
54
+ content: `@tailwind base;
55
+ @tailwind components;
56
+ @tailwind utilities;
57
+ `,
58
+ };
59
+ }
60
+ function generateSidebar(model) {
61
+ const navItems = model.entities
62
+ .map((e) => {
63
+ const path = appRoutePath(e);
64
+ return ` { name: '${e.pluralName}', icon: '${e.icon}', href: '/${path}' },`;
65
+ })
66
+ .join('\n');
67
+ return {
68
+ path: 'components/layout/Sidebar.tsx',
69
+ content: `'use client';
70
+
71
+ import { useState } from 'react';
72
+ import Link from 'next/link';
73
+ import { usePathname } from 'next/navigation';
74
+
75
+ const navItems = [
76
+ { name: 'Dashboard', icon: '🏠', href: '/' },
77
+ ${navItems}
78
+ ];
79
+
80
+ export default function Sidebar() {
81
+ const pathname = usePathname();
82
+ const [collapsed, setCollapsed] = useState(false);
83
+
84
+ return (
85
+ <aside
86
+ className={\`flex flex-col border-r border-gray-200 bg-white transition-all dark:border-gray-800 dark:bg-gray-900 \${collapsed ? 'w-16' : 'w-64'}\`}
87
+ >
88
+ <div className="flex h-14 items-center justify-between border-b border-gray-200 px-4 dark:border-gray-800">
89
+ {!collapsed && (
90
+ <span className="text-lg font-bold text-gray-900 dark:text-white">${model.identity.name}</span>
91
+ )}
92
+ <button
93
+ onClick={() => setCollapsed(!collapsed)}
94
+ className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
95
+ >
96
+ {collapsed ? '→' : '←'}
97
+ </button>
98
+ </div>
99
+ <nav className="flex-1 space-y-1 p-2">
100
+ {navItems.map((item) => {
101
+ const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href));
102
+ return (
103
+ <Link
104
+ key={item.href}
105
+ href={item.href}
106
+ className={\`flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors \${
107
+ isActive
108
+ ? 'bg-primary/10 text-primary font-medium'
109
+ : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'
110
+ }\`}
111
+ >
112
+ <span className="text-lg">{item.icon}</span>
113
+ {!collapsed && <span>{item.name}</span>}
114
+ </Link>
115
+ );
116
+ })}
117
+ </nav>
118
+ </aside>
119
+ );
120
+ }
121
+ `,
122
+ };
123
+ }
124
+ function generateHeader(model) {
125
+ const darkModeToggle = model.features.darkMode
126
+ ? `
127
+ <button
128
+ onClick={() => document.documentElement.classList.toggle('dark')}
129
+ className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
130
+ >
131
+ 🌓
132
+ </button>`
133
+ : '';
134
+ return {
135
+ path: 'components/layout/Header.tsx',
136
+ content: `'use client';
137
+
138
+ import { usePathname } from 'next/navigation';
139
+
140
+ export default function Header() {
141
+ const pathname = usePathname();
142
+ const segments = pathname.split('/').filter(Boolean);
143
+ const title = segments.length > 0
144
+ ? segments[segments.length - 1].replace(/-/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase())
145
+ : 'Dashboard';
146
+
147
+ return (
148
+ <header className="flex h-14 items-center justify-between border-b border-gray-200 bg-white px-6 dark:border-gray-800 dark:bg-gray-900">
149
+ <h1 className="text-lg font-semibold text-gray-900 dark:text-white">{title}</h1>
150
+ <div className="flex items-center gap-2">${darkModeToggle}
151
+ </div>
152
+ </header>
153
+ );
154
+ }
155
+ `,
156
+ };
157
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — Prisma Schema Generator
3
+ *
4
+ * Generates: prisma/schema.prisma, lib/prisma.ts
5
+ */
6
+ import type { ApplicationModel } from '../../model/types.js';
7
+ import type { FactoryFile } from '../types.js';
8
+ export declare function generatePrismaFiles(model: ApplicationModel): FactoryFile[];