@compilr-dev/factory 0.1.6 → 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 (49) 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/api.js +96 -13
  29. package/dist/toolkits/react-node/config.d.ts +1 -4
  30. package/dist/toolkits/react-node/config.js +3 -84
  31. package/dist/toolkits/react-node/entity.js +64 -22
  32. package/dist/toolkits/react-node/helpers.d.ts +2 -23
  33. package/dist/toolkits/react-node/helpers.js +2 -67
  34. package/dist/toolkits/react-node/seed.d.ts +4 -3
  35. package/dist/toolkits/react-node/seed.js +4 -111
  36. package/dist/toolkits/react-node/shared.d.ts +2 -3
  37. package/dist/toolkits/react-node/shared.js +2 -115
  38. package/dist/toolkits/react-node/types-gen.js +26 -0
  39. package/dist/toolkits/shared/color-utils.d.ts +10 -0
  40. package/dist/toolkits/shared/color-utils.js +85 -0
  41. package/dist/toolkits/shared/components.d.ts +12 -0
  42. package/dist/toolkits/shared/components.js +121 -0
  43. package/dist/toolkits/shared/helpers.d.ts +28 -0
  44. package/dist/toolkits/shared/helpers.js +72 -0
  45. package/dist/toolkits/shared/index.d.ts +9 -0
  46. package/dist/toolkits/shared/index.js +9 -0
  47. package/dist/toolkits/shared/seed-data.d.ts +18 -0
  48. package/dist/toolkits/shared/seed-data.js +119 -0
  49. package/package.json +1 -1
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — Prisma Schema Generator
3
+ *
4
+ * Generates: prisma/schema.prisma, lib/prisma.ts
5
+ */
6
+ import { toCamelCase } from '../../model/naming.js';
7
+ import { prismaFieldType, prismaModelName, fkFieldName, belongsToRels, hasManyRels, } from './helpers.js';
8
+ export function generatePrismaFiles(model) {
9
+ return [generateSchema(model), generatePrismaClient()];
10
+ }
11
+ function generateSchema(model) {
12
+ const lines = [];
13
+ lines.push(`generator client {`);
14
+ lines.push(` provider = "prisma-client-js"`);
15
+ lines.push(`}`);
16
+ lines.push('');
17
+ lines.push(`datasource db {`);
18
+ lines.push(` provider = "sqlite"`);
19
+ lines.push(` url = env("DATABASE_URL")`);
20
+ lines.push(`}`);
21
+ for (const entity of model.entities) {
22
+ lines.push('');
23
+ lines.push(generatePrismaModel(model, entity));
24
+ }
25
+ lines.push('');
26
+ return { path: 'prisma/schema.prisma', content: lines.join('\n') };
27
+ }
28
+ function generatePrismaModel(model, entity) {
29
+ const modelName = prismaModelName(entity);
30
+ const bto = belongsToRels(entity);
31
+ const hm = hasManyRels(entity);
32
+ const lines = [];
33
+ lines.push(`model ${modelName} {`);
34
+ lines.push(` id Int @id @default(autoincrement())`);
35
+ // Regular fields
36
+ for (const field of entity.fields) {
37
+ const pType = prismaFieldType(field);
38
+ const optional = field.required ? '' : '?';
39
+ lines.push(` ${field.name.padEnd(9)} ${pType}${optional}`);
40
+ }
41
+ // BelongsTo relationships — FK field + relation
42
+ for (const rel of bto) {
43
+ const fk = fkFieldName(rel);
44
+ const targetModel = prismaModelName({ name: rel.target });
45
+ const relField = toCamelCase(rel.target);
46
+ lines.push(` ${fk.padEnd(9)} Int`);
47
+ lines.push(` ${relField.padEnd(9)} ${targetModel} @relation(fields: [${fk}], references: [id])`);
48
+ }
49
+ // HasMany relationships — reverse relation array
50
+ for (const rel of hm) {
51
+ const targetEntity = model.entities.find((e) => e.name === rel.target);
52
+ if (!targetEntity)
53
+ continue;
54
+ const targetModel = prismaModelName(targetEntity);
55
+ const relField = toCamelCase(targetEntity.pluralName);
56
+ lines.push(` ${relField.padEnd(9)} ${targetModel}[]`);
57
+ }
58
+ // Timestamps
59
+ lines.push(` createdAt DateTime @default(now())`);
60
+ lines.push(` updatedAt DateTime @updatedAt`);
61
+ lines.push(`}`);
62
+ return lines.join('\n');
63
+ }
64
+ function generatePrismaClient() {
65
+ return {
66
+ path: 'lib/prisma.ts',
67
+ content: `import { PrismaClient } from '@prisma/client';
68
+
69
+ const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
70
+
71
+ export const prisma = globalForPrisma.prisma || new PrismaClient();
72
+
73
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
74
+ `,
75
+ };
76
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — Seed Data Generator
3
+ *
4
+ * Generates prisma/seed.ts using Prisma's createMany for bulk inserts.
5
+ * Entities are ordered by dependency (parents before children).
6
+ */
7
+ import type { ApplicationModel } from '../../model/types.js';
8
+ import type { FactoryFile } from '../types.js';
9
+ export declare function generateSeedFile(model: ApplicationModel): FactoryFile[];
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — Seed Data Generator
3
+ *
4
+ * Generates prisma/seed.ts using Prisma's createMany for bulk inserts.
5
+ * Entities are ordered by dependency (parents before children).
6
+ */
7
+ import { toCamelCase } from '../../model/naming.js';
8
+ import { fkFieldName, belongsToRels } from './helpers.js';
9
+ import { generateFieldValue, SEED_COUNT } from '../shared/seed-data.js';
10
+ export function generateSeedFile(model) {
11
+ const ordered = topologicalSort(model.entities);
12
+ const seedBlocks = ordered.map((entity) => generateEntitySeed(model, entity)).join('\n\n');
13
+ return [
14
+ {
15
+ path: 'prisma/seed.ts',
16
+ content: `import { PrismaClient } from '@prisma/client';
17
+
18
+ const prisma = new PrismaClient();
19
+
20
+ async function main() {
21
+ ${seedBlocks}
22
+
23
+ console.log('Seed data created successfully.');
24
+ }
25
+
26
+ main()
27
+ .then(() => prisma.$disconnect())
28
+ .catch((e) => {
29
+ console.error(e);
30
+ prisma.$disconnect();
31
+ process.exit(1);
32
+ });
33
+ `,
34
+ },
35
+ ];
36
+ }
37
+ function generateEntitySeed(model, entity) {
38
+ const camel = toCamelCase(entity.name);
39
+ const rels = belongsToRels(entity);
40
+ const items = [];
41
+ for (let i = 0; i < SEED_COUNT; i++) {
42
+ const fields = [];
43
+ for (const field of entity.fields) {
44
+ const value = generateFieldValue(field, i, entity.name);
45
+ // For Prisma, booleans and numbers don't need quotes; strings already have them
46
+ if (field.type === 'date') {
47
+ // Prisma expects Date objects for DateTime fields
48
+ fields.push(` ${field.name}: new Date(${value}),`);
49
+ }
50
+ else {
51
+ fields.push(` ${field.name}: ${value},`);
52
+ }
53
+ }
54
+ // FK fields from belongsTo relationships
55
+ for (const rel of rels) {
56
+ const fk = fkFieldName(rel);
57
+ const targetCount = SEED_COUNT;
58
+ fields.push(` ${fk}: ${String((i % targetCount) + 1)},`);
59
+ }
60
+ items.push(` {\n${fields.join('\n')}\n },`);
61
+ }
62
+ return ` await prisma.${camel}.createMany({
63
+ data: [
64
+ ${items.join('\n')}
65
+ ],
66
+ });`;
67
+ }
68
+ /** Sort entities so that parents come before children (entities with belongsTo go after their targets). */
69
+ function topologicalSort(entities) {
70
+ const entityMap = new Map(entities.map((e) => [e.name, e]));
71
+ const sorted = [];
72
+ const visited = new Set();
73
+ const visiting = new Set();
74
+ function visit(entity) {
75
+ if (visited.has(entity.name))
76
+ return;
77
+ if (visiting.has(entity.name)) {
78
+ // Circular dependency — just add it
79
+ sorted.push(entity);
80
+ visited.add(entity.name);
81
+ return;
82
+ }
83
+ visiting.add(entity.name);
84
+ // Visit dependencies first
85
+ for (const rel of entity.relationships) {
86
+ if (rel.type === 'belongsTo') {
87
+ const target = entityMap.get(rel.target);
88
+ if (target)
89
+ visit(target);
90
+ }
91
+ }
92
+ visiting.delete(entity.name);
93
+ visited.add(entity.name);
94
+ sorted.push(entity);
95
+ }
96
+ for (const entity of entities) {
97
+ visit(entity);
98
+ }
99
+ return sorted;
100
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — Static Files Generator
3
+ *
4
+ * Generates: .gitignore, README.md
5
+ */
6
+ import type { ApplicationModel } from '../../model/types.js';
7
+ import type { FactoryFile } from '../types.js';
8
+ export declare function generateStaticFiles(model: ApplicationModel): FactoryFile[];
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — Static Files Generator
3
+ *
4
+ * Generates: .gitignore, README.md
5
+ */
6
+ export function generateStaticFiles(model) {
7
+ return [generateGitignore(), generateReadme(model)];
8
+ }
9
+ function generateGitignore() {
10
+ return {
11
+ path: '.gitignore',
12
+ content: `node_modules
13
+ .next
14
+ .env
15
+ prisma/dev.db
16
+ prisma/dev.db-journal
17
+ *.local
18
+ `,
19
+ };
20
+ }
21
+ function generateReadme(model) {
22
+ let entitiesSection = '';
23
+ if (model.entities.length > 0) {
24
+ const entityLines = model.entities
25
+ .map((e) => {
26
+ const relCount = e.relationships.length;
27
+ const parts = [`${String(e.fields.length)} fields`];
28
+ if (relCount > 0) {
29
+ parts.push(`${String(relCount)} relationship${relCount > 1 ? 's' : ''}`);
30
+ }
31
+ return `- **${e.name}** (${e.icon}) — ${parts.join(', ')}`;
32
+ })
33
+ .join('\n');
34
+ entitiesSection = `\n## Entities\n\n${entityLines}\n`;
35
+ }
36
+ return {
37
+ path: 'README.md',
38
+ content: `# ${model.identity.name}
39
+
40
+ ${model.identity.description}
41
+ ${entitiesSection}
42
+ ## Getting Started
43
+
44
+ \`\`\`bash
45
+ npm install
46
+ npx prisma db push
47
+ npx prisma db seed
48
+ npm run dev
49
+ \`\`\`
50
+
51
+ - **App:** http://localhost:3000
52
+
53
+ ## Tech Stack
54
+
55
+ - Next.js 14 (App Router)
56
+ - TypeScript
57
+ - Prisma ORM (SQLite)
58
+ - Tailwind CSS
59
+ `,
60
+ };
61
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — TypeScript Types Generator
3
+ *
4
+ * Generates src/types/index.ts with interfaces for each entity.
5
+ * These serve as the frontend contract (Client Components use these types;
6
+ * Prisma generates its own types server-side).
7
+ */
8
+ import type { ApplicationModel } from '../../model/types.js';
9
+ import type { FactoryFile } from '../types.js';
10
+ export declare function generateTypesFile(model: ApplicationModel): FactoryFile[];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Next.js + Prisma Toolkit — TypeScript Types Generator
3
+ *
4
+ * Generates src/types/index.ts with interfaces for each entity.
5
+ * These serve as the frontend contract (Client Components use these types;
6
+ * Prisma generates its own types server-side).
7
+ */
8
+ import { tsType, fkFieldName, belongsToRels } from './helpers.js';
9
+ const SHARED_TYPES = `// --- Shared API Types ---
10
+
11
+ export interface PaginatedResponse<T> {
12
+ data: T[];
13
+ total: number;
14
+ page: number;
15
+ pageSize: number;
16
+ totalPages: number;
17
+ }
18
+
19
+ export interface ApiResponse<T> {
20
+ data: T;
21
+ error?: string;
22
+ }
23
+
24
+ export interface ApiListResponse<T> {
25
+ data: T[];
26
+ total: number;
27
+ }
28
+
29
+ export interface SortParams {
30
+ field: string;
31
+ direction: 'asc' | 'desc';
32
+ }`;
33
+ export function generateTypesFile(model) {
34
+ const lines = ['// Auto-generated TypeScript types from Application Model', ''];
35
+ lines.push(SHARED_TYPES);
36
+ lines.push('');
37
+ for (const entity of model.entities) {
38
+ lines.push(generateInterface(entity));
39
+ lines.push('');
40
+ }
41
+ return [{ path: 'src/types/index.ts', content: lines.join('\n') }];
42
+ }
43
+ function generateInterface(entity) {
44
+ const lines = [];
45
+ lines.push(`export interface ${entity.name} {`);
46
+ lines.push(' id: number;');
47
+ for (const field of entity.fields) {
48
+ const optional = field.required ? '' : '?';
49
+ lines.push(` ${field.name}${optional}: ${tsType(field)};`);
50
+ }
51
+ // FK fields from belongsTo
52
+ for (const rel of belongsToRels(entity)) {
53
+ const fk = fkFieldName(rel);
54
+ lines.push(` ${fk}: number;`);
55
+ lines.push(` ${rel.target.charAt(0).toLowerCase() + rel.target.slice(1)}?: ${rel.target};`);
56
+ }
57
+ // Implicit timestamps
58
+ lines.push(' createdAt: string;');
59
+ lines.push(' updatedAt: string;');
60
+ lines.push('}');
61
+ return lines.join('\n');
62
+ }
@@ -4,7 +4,7 @@
4
4
  * Generates: server/index.ts, server/routes/{entity}.ts, server/data/{entity}.ts
5
5
  */
6
6
  import { toCamelCase, toKebabCase } from '../../model/naming.js';
7
- import { tsType, fkFieldName, belongsToRels } from './helpers.js';
7
+ import { tsType, fkFieldName, belongsToRels, hasManyRels } from './helpers.js';
8
8
  import { generateSeedData } from './seed.js';
9
9
  export function generateApiFiles(model) {
10
10
  return [
@@ -71,6 +71,23 @@ function generateDataStore(model, entity) {
71
71
  }
72
72
  typeFields.push(' createdAt: string;');
73
73
  typeFields.push(' updatedAt: string;');
74
+ // Collect searchable fields (string + enum types)
75
+ const searchableFields = entity.fields
76
+ .filter((f) => f.type === 'string' || f.type === 'enum')
77
+ .map((f) => `item.${f.name}`);
78
+ const searchFilter = searchableFields.length > 0
79
+ ? ` const q = options.search.toLowerCase();
80
+ result = result.filter((item) =>
81
+ [${searchableFields.join(', ')}].some((v) =>
82
+ v !== undefined && String(v).toLowerCase().includes(q),
83
+ ),
84
+ );`
85
+ : ` const q = options.search.toLowerCase();
86
+ result = result.filter((item) =>
87
+ Object.values(item).some((v) =>
88
+ v !== undefined && String(v).toLowerCase().includes(q),
89
+ ),
90
+ );`;
74
91
  const seedData = generateSeedData(model, entity);
75
92
  return {
76
93
  path: `server/data/${fileName}.ts`,
@@ -80,6 +97,11 @@ export interface ${typeName}Record {
80
97
  ${typeFields.join('\n')}
81
98
  }
82
99
 
100
+ export interface QueryOptions {
101
+ search?: string;
102
+ filters?: Record<string, string>;
103
+ }
104
+
83
105
  ${seedData}
84
106
 
85
107
  let items: ${typeName}Record[] = [...${varName}SeedData];
@@ -93,6 +115,21 @@ export function getById(id: number): ${typeName}Record | undefined {
93
115
  return items.find((item) => item.id === id);
94
116
  }
95
117
 
118
+ export function query(options: QueryOptions): { data: ${typeName}Record[]; total: number } {
119
+ let result = items;
120
+ if (options.search) {
121
+ ${searchFilter}
122
+ }
123
+ if (options.filters) {
124
+ for (const [field, value] of Object.entries(options.filters)) {
125
+ result = result.filter((item) =>
126
+ String((item as Record<string, unknown>)[field]) === value,
127
+ );
128
+ }
129
+ }
130
+ return { data: result, total: result.length };
131
+ }
132
+
96
133
  export function create(data: Omit<${typeName}Record, 'id' | 'createdAt' | 'updatedAt'>): ${typeName}Record {
97
134
  const now = new Date().toISOString();
98
135
  const item = { ...data, id: nextId++, createdAt: now, updatedAt: now };
@@ -124,6 +161,7 @@ function generateRoutes(model, entity) {
124
161
  const varName = toCamelCase(entity.pluralName) + 'Router';
125
162
  const dataImport = fileName;
126
163
  const btoRels = belongsToRels(entity);
164
+ const hmRels = hasManyRels(entity);
127
165
  // Populate belongsTo entities
128
166
  const populateImports = btoRels
129
167
  .map((rel) => {
@@ -135,6 +173,17 @@ function generateRoutes(model, entity) {
135
173
  })
136
174
  .filter(Boolean)
137
175
  .join('\n');
176
+ // Aliased getAll imports for hasMany targets (used by ?include=)
177
+ const hasManyImports = hmRels
178
+ .map((rel) => {
179
+ const targetEntity = model.entities.find((e) => e.name === rel.target);
180
+ if (!targetEntity)
181
+ return '';
182
+ const targetFile = toKebabCase(targetEntity.pluralName).toLowerCase();
183
+ return `import { getAll as getAll${targetEntity.pluralName} } from '../data/${targetFile}.js';`;
184
+ })
185
+ .filter(Boolean)
186
+ .join('\n');
138
187
  const populateLogic = btoRels.length > 0
139
188
  ? `
140
189
  function populate(item: Record<string, unknown>): Record<string, unknown> {
@@ -151,30 +200,64 @@ ${btoRels
151
200
  `
152
201
  : '';
153
202
  const getListReturn = btoRels.length > 0
154
- ? 'res.json(items.map((item) => populate(item as unknown as Record<string, unknown>)));'
155
- : 'res.json(items);';
156
- const getByIdReturn = btoRels.length > 0
157
- ? 'res.json(populate(item as unknown as Record<string, unknown>));'
158
- : 'res.json(item);';
203
+ ? 'res.json({ data: result.data.map((item) => populate(item as unknown as Record<string, unknown>)), total: result.total });'
204
+ : 'res.json({ data: result.data, total: result.total });';
205
+ // GET by ID with optional ?include= for hasMany
206
+ let getByIdBody;
207
+ if (hmRels.length > 0) {
208
+ const includeChecks = hmRels
209
+ .map((rel) => {
210
+ const targetEntity = model.entities.find((e) => e.name === rel.target);
211
+ if (!targetEntity)
212
+ return '';
213
+ const targetVar = toCamelCase(targetEntity.pluralName);
214
+ const fkRel = targetEntity.relationships.find((r) => r.type === 'belongsTo' && r.target === entity.name) ?? { type: 'belongsTo', target: entity.name };
215
+ const fk = fkFieldName(fkRel);
216
+ return ` if (include.includes('${targetVar}')) {
217
+ result.${targetVar} = getAll${targetEntity.pluralName}().filter((r) => r.${fk} === item.id);
218
+ }`;
219
+ })
220
+ .filter(Boolean)
221
+ .join('\n');
222
+ const populateExpr = btoRels.length > 0 ? 'populate(item as unknown as Record<string, unknown>)' : '{ ...item }';
223
+ getByIdBody = ` const item = getById(Number(req.params.id));
224
+ if (!item) return res.status(404).json({ error: 'Not found' });
225
+ const include = (req.query.include as string ?? '').split(',').filter(Boolean);
226
+ const result: Record<string, unknown> = ${populateExpr};
227
+ ${includeChecks}
228
+ res.json(result);`;
229
+ }
230
+ else {
231
+ const getByIdReturn = btoRels.length > 0
232
+ ? 'res.json(populate(item as unknown as Record<string, unknown>));'
233
+ : 'res.json(item);';
234
+ getByIdBody = ` const item = getById(Number(req.params.id));
235
+ if (!item) return res.status(404).json({ error: 'Not found' });
236
+ ${getByIdReturn}`;
237
+ }
159
238
  return {
160
239
  path: `server/routes/${fileName}.ts`,
161
240
  content: `import { Router } from 'express';
162
- import { getAll, getById, create, update, remove } from '../data/${dataImport}.js';
163
- ${populateImports}
241
+ import { getAll, getById, query, create, update, remove } from '../data/${dataImport}.js';
242
+ ${populateImports}${hasManyImports ? '\n' + hasManyImports : ''}
164
243
 
165
244
  export const ${varName} = Router();
166
245
  ${populateLogic}
167
246
  // GET all
168
- ${varName}.get('/', (_req, res) => {
169
- const items = getAll();
247
+ ${varName}.get('/', (req, res) => {
248
+ const search = req.query.search as string | undefined;
249
+ const filters: Record<string, string> = {};
250
+ for (const [key, value] of Object.entries(req.query)) {
251
+ const match = key.match(/^filter\\[(.+)\\]$/);
252
+ if (match && typeof value === 'string') filters[match[1]] = value;
253
+ }
254
+ const result = query({ search, filters });
170
255
  ${getListReturn}
171
256
  });
172
257
 
173
258
  // GET by ID
174
259
  ${varName}.get('/:id', (req, res) => {
175
- const item = getById(Number(req.params.id));
176
- if (!item) return res.status(404).json({ error: 'Not found' });
177
- ${getByIdReturn}
260
+ ${getByIdBody}
178
261
  });
179
262
 
180
263
  // POST create
@@ -7,7 +7,4 @@
7
7
  import type { ApplicationModel } from '../../model/types.js';
8
8
  import type { FactoryFile } from '../types.js';
9
9
  export declare function generateConfigFiles(model: ApplicationModel): FactoryFile[];
10
- declare function hexToHsl(hex: string): [number, number, number];
11
- declare function hslToHex(h: number, s: number, l: number): string;
12
- declare function generateColorShades(hex: string): string;
13
- export { hexToHsl, hslToHex, generateColorShades };
10
+ export { hexToHsl, hslToHex, generateColorShades } from '../shared/color-utils.js';
@@ -5,6 +5,7 @@
5
5
  * tsconfig.json, postcss.config.js
6
6
  */
7
7
  import { toKebabCase } from '../../model/naming.js';
8
+ import { generateColorShades } from '../shared/color-utils.js';
8
9
  export function generateConfigFiles(model) {
9
10
  const appSlug = toKebabCase(model.identity.name);
10
11
  return [
@@ -109,90 +110,8 @@ function generateTsConfig() {
109
110
  };
110
111
  return { path: 'tsconfig.json', content: JSON.stringify(config, null, 2) + '\n' };
111
112
  }
112
- // =============================================================================
113
- // Color Shade Utilities
114
- // =============================================================================
115
- const SHADE_LIGHTNESS = {
116
- '50': 97,
117
- '100': 94,
118
- '200': 86,
119
- '300': 77,
120
- '400': 66,
121
- '500': 55,
122
- '600': 44,
123
- '700': 36,
124
- '800': 27,
125
- '900': 20,
126
- '950': 14,
127
- };
128
- function hexToHsl(hex) {
129
- const raw = hex.replace('#', '');
130
- const r = parseInt(raw.substring(0, 2), 16) / 255;
131
- const g = parseInt(raw.substring(2, 4), 16) / 255;
132
- const b = parseInt(raw.substring(4, 6), 16) / 255;
133
- const max = Math.max(r, g, b);
134
- const min = Math.min(r, g, b);
135
- const l = (max + min) / 2;
136
- if (max === min)
137
- return [0, 0, Math.round(l * 100)];
138
- const d = max - min;
139
- const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
140
- let h = 0;
141
- if (max === r)
142
- h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
143
- else if (max === g)
144
- h = ((b - r) / d + 2) / 6;
145
- else
146
- h = ((r - g) / d + 4) / 6;
147
- return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
148
- }
149
- function hslToHex(h, s, l) {
150
- const sN = s / 100;
151
- const lN = l / 100;
152
- const c = (1 - Math.abs(2 * lN - 1)) * sN;
153
- const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
154
- const m = lN - c / 2;
155
- let r = 0, g = 0, b = 0;
156
- if (h < 60)
157
- [r, g, b] = [c, x, 0];
158
- else if (h < 120)
159
- [r, g, b] = [x, c, 0];
160
- else if (h < 180)
161
- [r, g, b] = [0, c, x];
162
- else if (h < 240)
163
- [r, g, b] = [0, x, c];
164
- else if (h < 300)
165
- [r, g, b] = [x, 0, c];
166
- else
167
- [r, g, b] = [c, 0, x];
168
- const toHex = (v) => Math.round((v + m) * 255)
169
- .toString(16)
170
- .padStart(2, '0');
171
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
172
- }
173
- function generateColorShades(hex) {
174
- const [h, s] = hexToHsl(hex);
175
- const entries = [];
176
- for (const [shade, lightness] of Object.entries(SHADE_LIGHTNESS)) {
177
- entries.push(` ${shade}: '${hslToHex(h, s, lightness)}',`);
178
- }
179
- // Find the shade closest to the original for DEFAULT
180
- const [, , originalL] = hexToHsl(hex);
181
- let closestShade = '500';
182
- let closestDiff = Infinity;
183
- for (const [shade, lightness] of Object.entries(SHADE_LIGHTNESS)) {
184
- const diff = Math.abs(lightness - originalL);
185
- if (diff < closestDiff) {
186
- closestDiff = diff;
187
- closestShade = shade;
188
- }
189
- }
190
- const defaultHex = hslToHex(h, s, SHADE_LIGHTNESS[closestShade]);
191
- entries.push(` DEFAULT: '${defaultHex}',`);
192
- return `{\n${entries.join('\n')}\n }`;
193
- }
194
- // Export for testing
195
- export { hexToHsl, hslToHex, generateColorShades };
113
+ // Re-export for testing (from shared)
114
+ export { hexToHsl, hslToHex, generateColorShades } from '../shared/color-utils.js';
196
115
  function generatePostCssConfig() {
197
116
  return {
198
117
  path: 'postcss.config.js',