@specverse/engines 4.1.5 → 4.1.7

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 (122) hide show
  1. package/dist/libs/instance-factories/applications/templates/generic/backend-env-generator.js +22 -0
  2. package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +66 -0
  3. package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +54 -0
  4. package/dist/libs/instance-factories/applications/templates/generic/main-generator.js +290 -0
  5. package/dist/libs/instance-factories/applications/templates/react/_view-components-source.js +530 -0
  6. package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +437 -0
  7. package/dist/libs/instance-factories/applications/templates/react/api-types-generator.js +146 -0
  8. package/dist/libs/instance-factories/applications/templates/react/app-tsx-generator.js +73 -0
  9. package/dist/libs/instance-factories/applications/templates/react/env-example-generator.js +18 -0
  10. package/dist/libs/instance-factories/applications/templates/react/field-helpers-generator.js +99 -0
  11. package/dist/libs/instance-factories/applications/templates/react/gitignore-generator.js +35 -0
  12. package/dist/libs/instance-factories/applications/templates/react/index-css-generator.js +9 -0
  13. package/dist/libs/instance-factories/applications/templates/react/index-html-generator.js +23 -0
  14. package/dist/libs/instance-factories/applications/templates/react/main-tsx-generator.js +29 -0
  15. package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +49 -0
  16. package/dist/libs/instance-factories/applications/templates/react/pattern-adapter-generator.js +156 -0
  17. package/dist/libs/instance-factories/applications/templates/react/react-pattern-adapter.js +935 -0
  18. package/dist/libs/instance-factories/applications/templates/react/relationship-field-generator.js +143 -0
  19. package/dist/libs/instance-factories/applications/templates/react/runtime-app-tsx-generator.js +101 -0
  20. package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +50 -0
  21. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.js +646 -0
  22. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.js +65 -0
  23. package/dist/libs/instance-factories/applications/templates/react/tsconfig-generator.js +28 -0
  24. package/dist/libs/instance-factories/applications/templates/react/use-api-hooks-generator.js +132 -0
  25. package/dist/libs/instance-factories/applications/templates/react/view-dashboard-generator.js +143 -0
  26. package/dist/libs/instance-factories/applications/templates/react/view-detail-generator.js +143 -0
  27. package/dist/libs/instance-factories/applications/templates/react/view-form-generator.js +355 -0
  28. package/dist/libs/instance-factories/applications/templates/react/view-list-generator.js +91 -0
  29. package/dist/libs/instance-factories/applications/templates/react/view-router-generator.js +79 -0
  30. package/dist/libs/instance-factories/applications/templates/react/vite-config-generator.js +42 -0
  31. package/dist/libs/instance-factories/cli/templates/commander/cli-bin-wrapper-generator.js +11 -0
  32. package/dist/libs/instance-factories/cli/templates/commander/cli-entry-generator.js +111 -0
  33. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +928 -0
  34. package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +83 -0
  35. package/dist/libs/instance-factories/communication/templates/eventemitter/publisher-generator.js +91 -0
  36. package/dist/libs/instance-factories/communication/templates/eventemitter/subscriber-generator.js +86 -0
  37. package/dist/libs/instance-factories/controllers/templates/fastify/meta-routes-generator.js +93 -0
  38. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +280 -0
  39. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +125 -0
  40. package/dist/libs/instance-factories/infrastructure/templates/docker-k8s/infrastructure-generator.js +25 -0
  41. package/dist/libs/instance-factories/orms/templates/prisma/schema-generator.js +371 -0
  42. package/dist/libs/instance-factories/orms/templates/prisma/services-generator.js +266 -0
  43. package/dist/libs/instance-factories/scaffolding/templates/generic/env-example-generator.js +51 -0
  44. package/dist/libs/instance-factories/scaffolding/templates/generic/env-generator.js +61 -0
  45. package/dist/libs/instance-factories/scaffolding/templates/generic/gitignore-generator.js +59 -0
  46. package/dist/libs/instance-factories/scaffolding/templates/generic/package-json-generator.js +126 -0
  47. package/dist/libs/instance-factories/scaffolding/templates/generic/readme-generator.js +159 -0
  48. package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +56 -0
  49. package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-react-generator.js +37 -0
  50. package/dist/libs/instance-factories/sdks/templates/python/sdk-generator.js +29 -0
  51. package/dist/libs/instance-factories/sdks/templates/typescript/sdk-generator.js +28 -0
  52. package/dist/libs/instance-factories/services/templates/memory/generate-interpreter.js +14 -0
  53. package/dist/libs/instance-factories/services/templates/memory/step-conventions-memory.js +415 -0
  54. package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +177 -0
  55. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +413 -0
  56. package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +243 -0
  57. package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +264 -0
  58. package/dist/libs/instance-factories/services/templates/shared-patterns.js +24 -0
  59. package/dist/libs/instance-factories/shared/path-resolver.js +59 -0
  60. package/dist/libs/instance-factories/storage/templates/mongodb/config-generator.js +13 -0
  61. package/dist/libs/instance-factories/storage/templates/mongodb/docker-generator.js +16 -0
  62. package/dist/libs/instance-factories/storage/templates/postgresql/config-generator.js +45 -0
  63. package/dist/libs/instance-factories/storage/templates/postgresql/docker-generator.js +46 -0
  64. package/dist/libs/instance-factories/storage/templates/redis/config-generator.js +14 -0
  65. package/dist/libs/instance-factories/storage/templates/redis/docker-generator.js +16 -0
  66. package/dist/libs/instance-factories/test-generation.js +145 -0
  67. package/dist/libs/instance-factories/testing/templates/vitest/tests-generator.js +30 -0
  68. package/dist/libs/instance-factories/tools/templates/mcp/mcp-server-generator.js +149 -0
  69. package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +232 -0
  70. package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +49 -0
  71. package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +18 -0
  72. package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
  73. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +97 -0
  74. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +64 -0
  75. package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +182 -0
  76. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +1210 -0
  77. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +172 -0
  78. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +240 -0
  79. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +147 -0
  80. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +281 -0
  81. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +409 -0
  82. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +414 -0
  83. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +467 -0
  84. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +135 -0
  85. package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
  86. package/dist/libs/instance-factories/tools/templates/vscode/static/extension.js +965 -0
  87. package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js +238 -0
  88. package/dist/libs/instance-factories/validation/templates/zod/validation-generator.js +25 -0
  89. package/dist/libs/instance-factories/views/index.js +48 -0
  90. package/dist/libs/instance-factories/views/templates/react/adapters/antd-adapter.js +742 -0
  91. package/dist/libs/instance-factories/views/templates/react/adapters/mui-adapter.js +824 -0
  92. package/dist/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.js +719 -0
  93. package/dist/libs/instance-factories/views/templates/react/app-generator.js +45 -0
  94. package/dist/libs/instance-factories/views/templates/react/components-generator.js +779 -0
  95. package/dist/libs/instance-factories/views/templates/react/forms-generator.js +285 -0
  96. package/dist/libs/instance-factories/views/templates/react/frontend-package-json-generator.js +46 -0
  97. package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +111 -0
  98. package/dist/libs/instance-factories/views/templates/react/index-css-generator.js +9 -0
  99. package/dist/libs/instance-factories/views/templates/react/index-html-generator.js +23 -0
  100. package/dist/libs/instance-factories/views/templates/react/main-tsx-generator.js +21 -0
  101. package/dist/libs/instance-factories/views/templates/react/react-component-generator.js +299 -0
  102. package/dist/libs/instance-factories/views/templates/react/router-generator.js +136 -0
  103. package/dist/libs/instance-factories/views/templates/react/router-generic-generator.js +107 -0
  104. package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +179 -0
  105. package/dist/libs/instance-factories/views/templates/react/spec-json-generator.js +7 -0
  106. package/dist/libs/instance-factories/views/templates/react/types-generator.js +56 -0
  107. package/dist/libs/instance-factories/views/templates/react/views-metadata-generator.js +27 -0
  108. package/dist/libs/instance-factories/views/templates/react/vite-config-generator.js +29 -0
  109. package/dist/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js +261 -0
  110. package/dist/libs/instance-factories/views/templates/shared/adapter-types.js +34 -0
  111. package/dist/libs/instance-factories/views/templates/shared/atomic-components-registry.js +800 -0
  112. package/dist/libs/instance-factories/views/templates/shared/base-generator.js +305 -0
  113. package/dist/libs/instance-factories/views/templates/shared/component-metadata.js +517 -0
  114. package/dist/libs/instance-factories/views/templates/shared/composite-pattern-types.js +0 -0
  115. package/dist/libs/instance-factories/views/templates/shared/composite-patterns.js +445 -0
  116. package/dist/libs/instance-factories/views/templates/shared/index.js +80 -0
  117. package/dist/libs/instance-factories/views/templates/shared/pattern-validator.js +210 -0
  118. package/dist/libs/instance-factories/views/templates/shared/property-mapper.js +492 -0
  119. package/dist/libs/instance-factories/views/templates/shared/syntax-mapper.js +321 -0
  120. package/dist/realize/index.js +36 -12
  121. package/dist/realize/index.js.map +1 -1
  122. package/package.json +3 -2
@@ -0,0 +1,935 @@
1
+ import { useMemo } from "react";
2
+ import {
3
+ COMPOSITE_VIEW_PATTERNS,
4
+ ATOMIC_COMPONENTS_REGISTRY
5
+ } from "@specverse/lang/browser";
6
+ import { createUniversalTailwindAdapter } from "./tailwind-adapter-generator";
7
+ const REACT_PROTOCOL_MAPPING = {
8
+ create: {
9
+ method: "POST",
10
+ pathPattern: "/api/{resource}"
11
+ },
12
+ update: {
13
+ method: "PUT",
14
+ pathPattern: "/api/{resource}/{id}"
15
+ },
16
+ retrieve: {
17
+ method: "GET",
18
+ pathPattern: "/api/{resource}/{id}"
19
+ },
20
+ retrieve_many: {
21
+ method: "GET",
22
+ pathPattern: "/api/{resource}"
23
+ },
24
+ validate: {
25
+ method: "POST",
26
+ pathPattern: "/api/{resource}/validate"
27
+ },
28
+ evolve: {
29
+ method: "POST",
30
+ pathPattern: "/api/{resource}/{id}/evolve"
31
+ },
32
+ delete: {
33
+ method: "DELETE",
34
+ pathPattern: "/api/{resource}/{id}"
35
+ }
36
+ };
37
+ class ReactPatternAdapter {
38
+ tailwindAdapter;
39
+ constructor(config = {}) {
40
+ this.tailwindAdapter = config.tailwindAdapter || createUniversalTailwindAdapter();
41
+ }
42
+ /**
43
+ * Detect pattern type from view spec
44
+ */
45
+ detectPattern(viewSpec) {
46
+ const viewType = viewSpec.type?.toLowerCase();
47
+ const typeToPattern = {
48
+ "form": "form-view",
49
+ "list": "list-view",
50
+ "detail": "detail-view",
51
+ "dashboard": "dashboard-view"
52
+ };
53
+ const patternId = typeToPattern[viewType];
54
+ if (!patternId) return null;
55
+ return COMPOSITE_VIEW_PATTERNS[patternId];
56
+ }
57
+ /**
58
+ * Render a pattern to HTML
59
+ */
60
+ renderPattern(context) {
61
+ const { pattern } = context;
62
+ switch (pattern.category) {
63
+ case "data-entry":
64
+ return this.renderFormView(context);
65
+ case "data-display":
66
+ if (pattern.id === "list-view") {
67
+ return this.renderListView(context);
68
+ } else if (pattern.id === "detail-view") {
69
+ return this.renderDetailView(context);
70
+ }
71
+ return this.renderGenericDataDisplay(context);
72
+ case "dashboard":
73
+ return this.renderDashboardView(context);
74
+ default:
75
+ return this.renderFallback(context);
76
+ }
77
+ }
78
+ /**
79
+ * Render FormView pattern
80
+ */
81
+ renderFormView(context) {
82
+ const { viewSpec, modelData, modelSchemas, primaryModel } = context;
83
+ let components = viewSpec.uiComponents || {};
84
+ if (Object.keys(components).length === 0 && primaryModel) {
85
+ components = {
86
+ [`${primaryModel}Form`]: {
87
+ type: "form",
88
+ properties: {
89
+ model: primaryModel,
90
+ sections: [
91
+ {
92
+ title: `${primaryModel} Details`,
93
+ fields: this.inferFieldsFromSchema(modelSchemas, primaryModel)
94
+ }
95
+ ]
96
+ }
97
+ }
98
+ };
99
+ }
100
+ let html = '<div class="space-y-4">';
101
+ for (const [componentName, componentDef] of Object.entries(components)) {
102
+ const def = componentDef;
103
+ const type = def.type?.toLowerCase();
104
+ const properties = def.properties || def;
105
+ if (type === "form") {
106
+ html += this.renderFormComponent(componentName, def, modelData, modelSchemas, primaryModel, this.tailwindAdapter);
107
+ } else if (this.tailwindAdapter.components[type]) {
108
+ html += this.renderAtomicComponent(componentName, type, properties, this.tailwindAdapter);
109
+ }
110
+ }
111
+ html += "</div>";
112
+ return html;
113
+ }
114
+ /**
115
+ * Render ListView pattern
116
+ */
117
+ renderListView(context) {
118
+ const { viewSpec, modelData, primaryModel } = context;
119
+ let components = viewSpec.uiComponents || {};
120
+ if (Object.keys(components).length === 0 && primaryModel) {
121
+ const columns = this.inferFieldsFromModel(modelData, primaryModel);
122
+ components = {
123
+ [`${primaryModel}Table`]: {
124
+ type: "table",
125
+ properties: {
126
+ model: primaryModel,
127
+ columns
128
+ }
129
+ }
130
+ };
131
+ }
132
+ let html = '<div class="space-y-4">';
133
+ for (const [componentName, componentDef] of Object.entries(components)) {
134
+ const def = componentDef;
135
+ const type = def.type?.toLowerCase();
136
+ const properties = def.properties || def;
137
+ if (type === "table") {
138
+ html += this.renderTableComponent(componentName, def, modelData, primaryModel, this.tailwindAdapter);
139
+ } else if (type === "list") {
140
+ html += this.renderListComponent(componentName, def, modelData, primaryModel, this.tailwindAdapter);
141
+ } else if (this.tailwindAdapter.components[type]) {
142
+ html += this.renderAtomicComponent(componentName, type, properties, this.tailwindAdapter);
143
+ }
144
+ }
145
+ html += "</div>";
146
+ return html;
147
+ }
148
+ /**
149
+ * Render DetailView pattern
150
+ */
151
+ renderDetailView(context) {
152
+ const { viewSpec, selectedEntity, primaryModel, modelData, modelSchemas } = context;
153
+ let components = viewSpec.uiComponents || {};
154
+ if (!selectedEntity) {
155
+ return '<div class="p-4 text-gray-500 dark:text-gray-400">No entity selected</div>';
156
+ }
157
+ if (Object.keys(components).length === 0 && primaryModel) {
158
+ const fields = this.inferFieldsFromSchema(modelSchemas, primaryModel);
159
+ components = {
160
+ [`${primaryModel}Content`]: {
161
+ type: "content",
162
+ fields,
163
+ properties: {
164
+ model: primaryModel
165
+ }
166
+ }
167
+ };
168
+ if (modelSchemas && modelSchemas[primaryModel]?.relationships) {
169
+ const schemaRelationships = modelSchemas[primaryModel].relationships;
170
+ for (const [relName, relDef] of Object.entries(schemaRelationships)) {
171
+ const relDefObj = relDef;
172
+ if (relDefObj.type === "hasMany") {
173
+ const targetModel = relDefObj.targetModel || relDefObj.model || relName.charAt(0).toUpperCase() + relName.slice(1);
174
+ const relatedFields = this.inferFieldsFromSchema(modelSchemas, targetModel);
175
+ components[`${relName}List`] = {
176
+ type: "list",
177
+ fields: relatedFields.slice(0, 5),
178
+ // Limit to first 5 fields for table
179
+ properties: {
180
+ model: targetModel,
181
+ relationship: relName
182
+ }
183
+ };
184
+ }
185
+ }
186
+ }
187
+ }
188
+ let html = '<div class="space-y-4">';
189
+ for (const [componentName, componentDef] of Object.entries(components)) {
190
+ const def = componentDef;
191
+ const type = def.type?.toLowerCase();
192
+ const properties = def.properties || def;
193
+ if (type === "content") {
194
+ html += this.renderContentComponent(componentName, def, selectedEntity, this.tailwindAdapter);
195
+ } else if (type === "list") {
196
+ html += this.renderDetailListComponent(componentName, def, modelData, selectedEntity, primaryModel, this.tailwindAdapter);
197
+ } else if (type === "card") {
198
+ html += this.renderCardComponent(componentName, def, selectedEntity, this.tailwindAdapter);
199
+ } else if (this.tailwindAdapter.components[type]) {
200
+ html += this.renderAtomicComponent(componentName, type, properties, this.tailwindAdapter);
201
+ }
202
+ }
203
+ html += "</div>";
204
+ return html;
205
+ }
206
+ /**
207
+ * Render DashboardView pattern
208
+ */
209
+ renderDashboardView(context) {
210
+ const { viewSpec, modelData } = context;
211
+ const components = viewSpec.uiComponents || {};
212
+ let html = '<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">';
213
+ for (const [componentName, componentDef] of Object.entries(components)) {
214
+ const def = componentDef;
215
+ const type = def.type?.toLowerCase();
216
+ const properties = def.properties || def;
217
+ if (type === "card" && properties.variant === "metric") {
218
+ html += this.renderMetricCard(componentName, def, modelData, this.tailwindAdapter);
219
+ } else if (this.tailwindAdapter.components[type]) {
220
+ html += this.renderAtomicComponent(componentName, type, properties, this.tailwindAdapter);
221
+ }
222
+ }
223
+ html += "</div>";
224
+ return html;
225
+ }
226
+ /**
227
+ * Render generic data display (fallback)
228
+ */
229
+ renderGenericDataDisplay(_context) {
230
+ return '<div class="p-4 text-gray-500 dark:text-gray-400">Generic data display</div>';
231
+ }
232
+ /**
233
+ * Render fallback for unknown patterns
234
+ */
235
+ renderFallback(context) {
236
+ const { pattern } = context;
237
+ return `<div class="p-4 bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-700 rounded">
238
+ <p class="text-yellow-800 dark:text-yellow-200">Pattern not yet implemented: ${pattern.name}</p>
239
+ </div>`;
240
+ }
241
+ /**
242
+ * Helper: Render form component
243
+ *
244
+ * Generates a complete form with:
245
+ * - Inferred field types from model data (text, number, boolean)
246
+ * - Proper input types and validation
247
+ * - Required field indicators
248
+ * - Submit and reset buttons
249
+ * - Professional styling matching admin-demo
250
+ */
251
+ renderFormComponent(componentName, componentDef, modelData, modelSchemas, primaryModel, _tailwindAdapter) {
252
+ const properties = componentDef.properties || componentDef;
253
+ const sections = properties.sections || [];
254
+ const model = properties.model || primaryModel;
255
+ const fieldTypes = this.inferFieldTypes(modelSchemas, modelData, model);
256
+ let html = `
257
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
258
+ <div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
259
+ <h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200">${componentName}</h4>
260
+ </div>
261
+ <div class="p-6">
262
+ <form class="space-y-6">
263
+ `;
264
+ for (const section of sections) {
265
+ const fields = section.fields || [];
266
+ html += `
267
+ <div>
268
+ ${section.title ? `<h5 class="font-semibold text-gray-900 dark:text-gray-100 mb-4">${section.title}</h5>` : ""}
269
+ <div class="space-y-4">
270
+ `;
271
+ if (fields.length === 0) {
272
+ html += `
273
+ <div class="text-sm text-gray-500 dark:text-gray-400 italic p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded">
274
+ No fields available for this form. Model data may be empty or all fields are system-generated.
275
+ </div>
276
+ `;
277
+ }
278
+ for (const field of fields) {
279
+ const fieldName = typeof field === "string" ? field : field.name || field.fieldName;
280
+ const fieldLabel = typeof field === "string" ? this.humanizeFieldName(fieldName) : field.label || this.humanizeFieldName(fieldName);
281
+ const fieldType = fieldTypes[fieldName] || "string";
282
+ const isRequired = field.required !== false;
283
+ html += `<div>`;
284
+ html += `
285
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
286
+ ${fieldLabel}
287
+ ${isRequired ? '<span class="text-red-500 dark:text-red-400 ml-1">*</span>' : ""}
288
+ </label>
289
+ `;
290
+ if (fieldType === "boolean") {
291
+ html += `
292
+ <div class="flex items-center">
293
+ <input
294
+ type="checkbox"
295
+ name="${fieldName}"
296
+ class="h-4 w-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500"
297
+ />
298
+ <span class="ml-2 text-sm text-gray-600 dark:text-gray-300">Yes/No</span>
299
+ </div>
300
+ `;
301
+ } else if (fieldType === "number") {
302
+ html += `
303
+ <input
304
+ type="number"
305
+ name="${fieldName}"
306
+ placeholder="Enter ${fieldLabel.toLowerCase()}"
307
+ ${isRequired ? "required" : ""}
308
+ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
309
+ />
310
+ `;
311
+ } else {
312
+ html += `
313
+ <input
314
+ type="text"
315
+ name="${fieldName}"
316
+ placeholder="Enter ${fieldLabel.toLowerCase()}"
317
+ ${isRequired ? "required" : ""}
318
+ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
319
+ />
320
+ `;
321
+ }
322
+ html += `</div>`;
323
+ }
324
+ html += `
325
+ </div>
326
+ </div>
327
+ `;
328
+ }
329
+ const relationshipFields = this.inferRelationshipFields(modelSchemas, modelData, model);
330
+ if (relationshipFields.length > 0) {
331
+ html += `
332
+ <div>
333
+ <h5 class="font-semibold text-gray-900 dark:text-gray-100 mb-4">Relationships</h5>
334
+ <div class="space-y-4">
335
+ `;
336
+ for (const relField of relationshipFields) {
337
+ const relatedEntities = modelData[relField.targetModel] || [];
338
+ html += `
339
+ <div>
340
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
341
+ ${this.humanizeFieldName(relField.name)}
342
+ </label>
343
+ <select
344
+ name="${relField.foreignKey}"
345
+ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
346
+ >
347
+ <option value="">-- Select ${relField.targetModel} --</option>
348
+ ${relatedEntities.map((entity) => {
349
+ const displayName = entity.data?.name || entity.data?.title || entity.id;
350
+ return `<option value="${entity.id}">${displayName}</option>`;
351
+ }).join("")}
352
+ </select>
353
+ </div>
354
+ `;
355
+ }
356
+ html += `
357
+ </div>
358
+ </div>
359
+ `;
360
+ }
361
+ html += `
362
+ <div class="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-600">
363
+ <button
364
+ type="submit"
365
+ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded font-medium transition-colors"
366
+ >
367
+ Create ${model || "Entity"}
368
+ </button>
369
+ <button
370
+ type="reset"
371
+ class="px-6 py-2 bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-gray-100 rounded font-medium transition-colors"
372
+ >
373
+ Reset
374
+ </button>
375
+ </div>
376
+ </form>
377
+ </div>
378
+ </div>
379
+ `;
380
+ return html;
381
+ }
382
+ /**
383
+ * Helper: Infer relationship fields from model schema or data
384
+ *
385
+ * Reads relationship definitions from the model schema (like original FormView).
386
+ * Falls back to inferring from foreign keys in data if no schema available.
387
+ *
388
+ * Strategy:
389
+ * 1. Read from schema.relationships (preferred - matches original FormView)
390
+ * 2. If entities exist, scan their fields for foreign keys
391
+ * 3. If no entities, look at available models and infer common relationships
392
+ */
393
+ inferRelationshipFields(modelSchemas, modelData, modelName) {
394
+ const relationships = [];
395
+ if (!modelName) return relationships;
396
+ if (modelSchemas && modelSchemas[modelName]?.relationships) {
397
+ const schemaRelationships = modelSchemas[modelName].relationships;
398
+ for (const [relName, relDef] of Object.entries(schemaRelationships)) {
399
+ const relDefObj = relDef;
400
+ if (relDefObj.type === "belongsTo") {
401
+ relationships.push({
402
+ name: relName,
403
+ foreignKey: relDefObj.foreignKey || `${relName}Id`,
404
+ targetModel: relDefObj.targetModel || relDefObj.model || relName.charAt(0).toUpperCase() + relName.slice(1)
405
+ });
406
+ }
407
+ }
408
+ return relationships;
409
+ }
410
+ const entities = modelData[modelName] || [];
411
+ if (entities.length > 0 && entities[0]?.data) {
412
+ const firstEntity = entities[0];
413
+ for (const fieldName of Object.keys(firstEntity.data)) {
414
+ if (fieldName.endsWith("Id") && fieldName !== "id") {
415
+ const relName = fieldName.slice(0, -2);
416
+ const targetModel = relName.charAt(0).toUpperCase() + relName.slice(1);
417
+ if (modelData[targetModel]) {
418
+ relationships.push({
419
+ name: relName,
420
+ foreignKey: fieldName,
421
+ targetModel
422
+ });
423
+ }
424
+ }
425
+ }
426
+ }
427
+ if (relationships.length === 0) {
428
+ const availableModels = Object.keys(modelData);
429
+ for (const targetModel of availableModels) {
430
+ if (targetModel !== modelName) {
431
+ const relName = targetModel.charAt(0).toLowerCase() + targetModel.slice(1);
432
+ const foreignKey = `${relName}Id`;
433
+ relationships.push({
434
+ name: relName,
435
+ foreignKey,
436
+ targetModel
437
+ });
438
+ }
439
+ }
440
+ }
441
+ return relationships;
442
+ }
443
+ /**
444
+ * Helper: Humanize field name
445
+ * Converts camelCase/snake_case to readable labels
446
+ */
447
+ humanizeFieldName(fieldName) {
448
+ return fieldName.replace(/([A-Z])/g, " $1").replace(/_/g, " ").replace(/^./, (str) => str.toUpperCase()).trim();
449
+ }
450
+ /**
451
+ * Helper: Infer field types from model schema or data
452
+ * Prefers schema definitions, falls back to examining actual data
453
+ */
454
+ inferFieldTypes(modelSchemas, modelData, modelName) {
455
+ const types = {};
456
+ if (!modelName) return types;
457
+ if (modelSchemas && modelSchemas[modelName]?.attributes) {
458
+ const attributes = modelSchemas[modelName].attributes;
459
+ for (const [fieldName, attrDef] of Object.entries(attributes)) {
460
+ const typeStr = typeof attrDef === "string" ? attrDef : attrDef?.type || "string";
461
+ if (typeStr.toLowerCase().includes("bool")) {
462
+ types[fieldName] = "boolean";
463
+ } else if (typeStr.toLowerCase().includes("int") || typeStr.toLowerCase().includes("number") || typeStr.toLowerCase().includes("float")) {
464
+ types[fieldName] = "number";
465
+ } else {
466
+ types[fieldName] = "string";
467
+ }
468
+ }
469
+ return types;
470
+ }
471
+ if (!modelData[modelName] || modelData[modelName].length === 0) return types;
472
+ const firstEntity = modelData[modelName][0];
473
+ if (!firstEntity || !firstEntity.data) return types;
474
+ for (const [fieldName, value] of Object.entries(firstEntity.data)) {
475
+ if (typeof value === "boolean") {
476
+ types[fieldName] = "boolean";
477
+ } else if (typeof value === "number") {
478
+ types[fieldName] = "number";
479
+ } else {
480
+ types[fieldName] = "string";
481
+ }
482
+ }
483
+ return types;
484
+ }
485
+ /**
486
+ * Helper: Infer field names from model schema
487
+ * Reads schema.attributes to get field definitions
488
+ */
489
+ inferFieldsFromSchema(modelSchemas, modelName) {
490
+ if (!modelName || !modelSchemas || !modelSchemas[modelName]) {
491
+ return ["name", "title", "description"];
492
+ }
493
+ const schema = modelSchemas[modelName];
494
+ if (!schema.attributes) {
495
+ return ["name", "title", "description"];
496
+ }
497
+ const fields = Object.keys(schema.attributes).filter(
498
+ (field) => field !== "id" && field !== "createdAt" && field !== "updatedAt" && !this.isAutoGeneratedField(field, schema.attributes[field])
499
+ );
500
+ return fields.length > 0 ? fields : ["name", "title", "description"];
501
+ }
502
+ /**
503
+ * Helper: Check if field is auto-generated (shouldn't show in forms)
504
+ */
505
+ isAutoGeneratedField(fieldName, attrDef) {
506
+ if (["id", "createdAt", "updatedAt"].includes(fieldName)) return true;
507
+ if (typeof attrDef === "object" && attrDef.auto) return true;
508
+ return false;
509
+ }
510
+ /**
511
+ * Helper: Render table component
512
+ *
513
+ * Generates a data table with:
514
+ * - Column headers with proper formatting
515
+ * - Data type-aware value rendering (objects, booleans, etc.)
516
+ * - Empty state handling
517
+ * - Hover effects and professional styling
518
+ * - Responsive scrolling
519
+ */
520
+ renderTableComponent(componentName, componentDef, modelData, primaryModel, _tailwindAdapter) {
521
+ const properties = componentDef.properties || componentDef;
522
+ const tableModel = properties.model || primaryModel;
523
+ const entities = modelData[tableModel || ""] || [];
524
+ const columns = properties.columns || [];
525
+ let html = `
526
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
527
+ <div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
528
+ <h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200">${componentName}</h4>
529
+ </div>
530
+ <div class="overflow-auto max-h-96">
531
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
532
+ <thead class="bg-gray-50 dark:bg-gray-700 sticky top-0">
533
+ <tr>
534
+ ${columns.map((col) => `
535
+ <th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-200 uppercase tracking-wider">
536
+ ${this.humanizeFieldName(col)}
537
+ </th>
538
+ `).join("")}
539
+ </tr>
540
+ </thead>
541
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
542
+ `;
543
+ if (entities.length === 0) {
544
+ html += `
545
+ <tr>
546
+ <td colspan="${columns.length}" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400 italic">
547
+ No ${tableModel || "data"} entities yet
548
+ </td>
549
+ </tr>
550
+ `;
551
+ } else {
552
+ for (const entity of entities) {
553
+ html += `<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">`;
554
+ for (const col of columns) {
555
+ const value = entity.data?.[col];
556
+ let displayValue;
557
+ if (value === void 0 || value === null || value === "") {
558
+ displayValue = '<span class="text-gray-400 dark:text-gray-500">\u2014</span>';
559
+ } else if (typeof value === "boolean") {
560
+ displayValue = `
561
+ <span class="inline-block px-2 py-1 rounded text-xs font-medium ${value ? "bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200" : "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200"}">
562
+ ${value ? "Yes" : "No"}
563
+ </span>
564
+ `;
565
+ } else if (typeof value === "object") {
566
+ displayValue = '<span class="text-gray-500 dark:text-gray-400 italic">[Object]</span>';
567
+ } else {
568
+ displayValue = String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
569
+ }
570
+ html += `<td class="px-4 py-2 text-sm text-gray-900 dark:text-gray-100 whitespace-nowrap">${displayValue}</td>`;
571
+ }
572
+ html += `</tr>`;
573
+ }
574
+ }
575
+ html += `
576
+ </tbody>
577
+ </table>
578
+ </div>
579
+ </div>
580
+ `;
581
+ return html;
582
+ }
583
+ /**
584
+ * Helper: Render list component
585
+ *
586
+ * Generates a list view with:
587
+ * - Flexible field display (comma-separated or multi-line)
588
+ * - Data type-aware rendering
589
+ * - Empty state handling
590
+ * - Professional card-based styling
591
+ */
592
+ renderListComponent(componentName, componentDef, modelData, primaryModel, _tailwindAdapter) {
593
+ const properties = componentDef.properties || componentDef;
594
+ const listModel = properties.model || primaryModel;
595
+ const entities = modelData[listModel || ""] || [];
596
+ const fields = properties.fields || [];
597
+ let html = `
598
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
599
+ <div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
600
+ <h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200">${componentName}</h4>
601
+ </div>
602
+ <div class="p-4">
603
+ `;
604
+ if (entities.length === 0) {
605
+ html += `
606
+ <div class="text-sm text-gray-500 dark:text-gray-400 italic text-center py-4">
607
+ No ${listModel || "items"} yet
608
+ </div>
609
+ `;
610
+ } else {
611
+ html += '<ul class="space-y-2">';
612
+ for (const entity of entities) {
613
+ html += `
614
+ <li class="p-3 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
615
+ <div class="text-sm text-gray-900 dark:text-gray-100">
616
+ `;
617
+ const fieldValues = [];
618
+ for (const field of fields) {
619
+ const value = entity.data?.[field];
620
+ if (value === void 0 || value === null || value === "") {
621
+ continue;
622
+ }
623
+ let displayValue;
624
+ if (typeof value === "boolean") {
625
+ displayValue = value ? "Yes" : "No";
626
+ } else if (typeof value === "object") {
627
+ displayValue = "[Object]";
628
+ } else {
629
+ displayValue = String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
630
+ }
631
+ fieldValues.push(`<strong>${this.humanizeFieldName(field)}:</strong> ${displayValue}`);
632
+ }
633
+ html += fieldValues.join(" \u2022 ");
634
+ html += `
635
+ </div>
636
+ </li>
637
+ `;
638
+ }
639
+ html += "</ul>";
640
+ }
641
+ html += `
642
+ </div>
643
+ </div>
644
+ `;
645
+ return html;
646
+ }
647
+ /**
648
+ * Helper: Render content component (for detail views)
649
+ *
650
+ * Displays specific fields from an entity with rich formatting.
651
+ * This matches the original DetailView's "content" component behavior.
652
+ */
653
+ renderContentComponent(componentName, componentDef, selectedEntity, _tailwindAdapter) {
654
+ let fields = componentDef.fields || [];
655
+ const entityData = selectedEntity?.data || selectedEntity || {};
656
+ if (fields.length === 0 && entityData) {
657
+ fields = Object.keys(entityData).filter(
658
+ (key) => key !== "id" && !key.endsWith("Id") && !["createdAt", "updatedAt"].includes(key)
659
+ );
660
+ }
661
+ let html = `
662
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
663
+ <div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
664
+ <h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200 capitalize">${componentName}</h4>
665
+ </div>
666
+ <div class="p-4">
667
+ `;
668
+ if (fields.length === 0) {
669
+ html += `
670
+ <p class="text-sm text-yellow-600 dark:text-yellow-400 italic">
671
+ No fields available (entity keys: ${Object.keys(entityData).join(", ")})
672
+ </p>
673
+ `;
674
+ } else {
675
+ html += '<div class="space-y-3">';
676
+ let displayedFields = 0;
677
+ for (const fieldName of fields) {
678
+ if (fieldName === "id") continue;
679
+ const value = entityData[fieldName];
680
+ let formattedValue;
681
+ if (value === void 0 || value === null) {
682
+ formattedValue = '<span class="text-gray-400 dark:text-gray-500 italic">Not set</span>';
683
+ } else if (typeof value === "object") {
684
+ const jsonStr = JSON.stringify(value, null, 2).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
685
+ formattedValue = `
686
+ <pre class="bg-gray-50 dark:bg-gray-900 p-2 rounded border border-gray-200 dark:border-gray-600 text-xs overflow-auto text-gray-900 dark:text-gray-100">${jsonStr}</pre>
687
+ `;
688
+ } else if (typeof value === "boolean") {
689
+ formattedValue = `
690
+ <span class="inline-block px-2 py-1 rounded text-xs font-medium ${value ? "bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200" : "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200"}">
691
+ ${value ? "Yes" : "No"}
692
+ </span>
693
+ `;
694
+ } else if (value === "") {
695
+ formattedValue = '<span class="text-gray-400 dark:text-gray-500 italic">Empty</span>';
696
+ } else {
697
+ formattedValue = String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
698
+ }
699
+ html += `
700
+ <div class="grid grid-cols-[120px_1fr] gap-4 items-start">
701
+ <label class="font-semibold text-sm text-gray-700 dark:text-gray-200 capitalize text-left">${fieldName}</label>
702
+ <div class="text-sm text-gray-900 dark:text-gray-100">${formattedValue}</div>
703
+ </div>
704
+ `;
705
+ displayedFields++;
706
+ }
707
+ if (displayedFields === 0) {
708
+ html += '<p class="text-sm text-gray-500 dark:text-gray-400 italic">No fields to display</p>';
709
+ }
710
+ html += "</div>";
711
+ }
712
+ html += `
713
+ </div>
714
+ </div>
715
+ `;
716
+ return html;
717
+ }
718
+ /**
719
+ * Helper: Render list component in detail view (for related entities)
720
+ *
721
+ * Shows related entities in a table format.
722
+ * This matches the original DetailView's "list" component behavior.
723
+ */
724
+ renderDetailListComponent(componentName, componentDef, modelData, selectedEntity, primaryModel, _tailwindAdapter) {
725
+ const properties = componentDef.properties || componentDef;
726
+ let fields = componentDef.fields || properties.fields || [];
727
+ let relatedModel = properties.model;
728
+ if (!relatedModel) {
729
+ const lowerName = componentName.toLowerCase();
730
+ for (const modelName of Object.keys(modelData)) {
731
+ if (lowerName.includes(modelName.toLowerCase())) {
732
+ relatedModel = modelName;
733
+ break;
734
+ }
735
+ }
736
+ }
737
+ const allRelatedEntities = relatedModel ? modelData[relatedModel] || [] : [];
738
+ const foreignKey = primaryModel ? `${primaryModel.charAt(0).toLowerCase()}${primaryModel.slice(1)}Id` : null;
739
+ const relatedEntities = foreignKey && selectedEntity?.id ? allRelatedEntities.filter((e) => e.data?.[foreignKey] === selectedEntity.id) : allRelatedEntities;
740
+ if (fields.length === 0 && relatedEntities.length > 0) {
741
+ const firstEntity = relatedEntities[0];
742
+ if (firstEntity.data) {
743
+ fields = Object.keys(firstEntity.data).filter((key) => key !== "id" && !key.endsWith("Id")).slice(0, 5);
744
+ }
745
+ }
746
+ let html = `
747
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
748
+ <div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
749
+ <h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200 capitalize">${componentName}</h4>
750
+ </div>
751
+ <div class="p-4">
752
+ `;
753
+ if (relatedEntities.length === 0) {
754
+ html += `
755
+ <p class="text-sm text-gray-500 dark:text-gray-400 italic">
756
+ No ${relatedModel || "related"} entities ${foreignKey ? `with ${foreignKey} = ${selectedEntity?.id}` : ""}
757
+ </p>
758
+ `;
759
+ } else if (fields.length === 0) {
760
+ html += `
761
+ <p class="text-sm text-gray-500 dark:text-gray-400 italic">
762
+ ${relatedEntities.length} ${relatedModel} entities found but no fields to display
763
+ </p>
764
+ `;
765
+ } else {
766
+ html += `
767
+ <div class="overflow-auto max-h-96">
768
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
769
+ <thead class="bg-gray-50 dark:bg-gray-700 sticky top-0">
770
+ <tr>
771
+ ${fields.map((fieldName) => `
772
+ <th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-200 uppercase tracking-wider">
773
+ ${fieldName}
774
+ </th>
775
+ `).join("")}
776
+ </tr>
777
+ </thead>
778
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
779
+ ${relatedEntities.map((entity) => `
780
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
781
+ ${fields.map((fieldName) => `
782
+ <td class="px-4 py-2 text-sm text-gray-900 dark:text-gray-100 whitespace-nowrap">
783
+ ${entity.data?.[fieldName] ?? "\u2014"}
784
+ </td>
785
+ `).join("")}
786
+ </tr>
787
+ `).join("")}
788
+ </tbody>
789
+ </table>
790
+ </div>
791
+ `;
792
+ }
793
+ html += `
794
+ </div>
795
+ </div>
796
+ `;
797
+ return html;
798
+ }
799
+ /**
800
+ * Helper: Render card component
801
+ *
802
+ * Displays entity fields with sophisticated formatting:
803
+ * - Objects: Formatted JSON in code block
804
+ * - Booleans: Yes/No badges
805
+ * - Other values: Plain text
806
+ */
807
+ renderCardComponent(componentName, _componentDef, selectedEntity, _tailwindAdapter) {
808
+ const entityData = selectedEntity.data || {};
809
+ const fields = Object.keys(entityData);
810
+ let html = `
811
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
812
+ <div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
813
+ <h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200">${componentName}</h4>
814
+ </div>
815
+ <div class="p-4">
816
+ <div class="space-y-3">
817
+ ${fields.map((field) => {
818
+ const value = entityData[field];
819
+ if (value === void 0 || value === null) return "";
820
+ let formattedValue;
821
+ if (typeof value === "object") {
822
+ const jsonStr = JSON.stringify(value, null, 2).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
823
+ formattedValue = `
824
+ <pre class="bg-gray-50 dark:bg-gray-900 p-2 rounded border border-gray-200 dark:border-gray-600 text-xs overflow-auto text-gray-900 dark:text-gray-100">${jsonStr}</pre>
825
+ `;
826
+ } else if (typeof value === "boolean") {
827
+ formattedValue = `
828
+ <span class="inline-block px-2 py-1 rounded text-xs font-medium ${value ? "bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200" : "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200"}">
829
+ ${value ? "Yes" : "No"}
830
+ </span>
831
+ `;
832
+ } else {
833
+ const escaped = String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
834
+ formattedValue = escaped || "\u2014";
835
+ }
836
+ return `
837
+ <div class="grid grid-cols-[140px_1fr] gap-4 items-start">
838
+ <label class="font-semibold text-sm text-gray-700 dark:text-gray-200 capitalize">${field}:</label>
839
+ <div class="text-sm text-gray-900 dark:text-gray-100">${formattedValue}</div>
840
+ </div>
841
+ `;
842
+ }).join("")}
843
+ </div>
844
+ </div>
845
+ </div>
846
+ `;
847
+ return html;
848
+ }
849
+ /**
850
+ * Helper: Render metric card
851
+ */
852
+ renderMetricCard(componentName, componentDef, modelData, _tailwindAdapter) {
853
+ const properties = componentDef.properties || componentDef;
854
+ const metric = properties.metric;
855
+ const metricModel = properties.model;
856
+ const entities = modelData[metricModel] || [];
857
+ let metricValue = "0";
858
+ if (entities.length > 0 && metric) {
859
+ const values = entities.map((e) => e.data?.[metric]).filter((v) => v !== void 0 && v !== null);
860
+ if (values.length > 0 && typeof values[0] === "number") {
861
+ const sum = values.reduce((acc, v) => acc + v, 0);
862
+ metricValue = String(Math.round(sum));
863
+ } else {
864
+ metricValue = String(values.length);
865
+ }
866
+ }
867
+ return `
868
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6">
869
+ <p class="text-sm font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide">
870
+ ${metric || componentName}
871
+ </p>
872
+ <p class="mt-2 text-3xl font-semibold text-gray-900 dark:text-gray-100">
873
+ ${metricValue}
874
+ </p>
875
+ </div>
876
+ `;
877
+ }
878
+ /**
879
+ * Helper: Render atomic component
880
+ */
881
+ renderAtomicComponent(componentName, type, properties, _tailwindAdapter) {
882
+ if (!this.tailwindAdapter.components[type]) {
883
+ return `<div class="p-4 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 rounded">
884
+ <p class="text-red-800 dark:text-red-200">Unknown component type: ${type}</p>
885
+ </div>`;
886
+ }
887
+ return `
888
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
889
+ <div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
890
+ <h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200">${componentName}</h4>
891
+ </div>
892
+ <div class="p-4">
893
+ ${this.tailwindAdapter.components[type].render({ properties })}
894
+ </div>
895
+ </div>
896
+ `;
897
+ }
898
+ /**
899
+ * Helper: Infer field names from model data
900
+ *
901
+ * Extracts field names from the first entity in modelData for the given model.
902
+ * Filters out system fields like id, createdAt, updatedAt, and foreign key fields.
903
+ *
904
+ * @param modelData - Map of model names to entity arrays
905
+ * @param modelName - Name of the model to extract fields from
906
+ * @returns Array of field names suitable for display
907
+ */
908
+ inferFieldsFromModel(modelData, modelName) {
909
+ if (!modelName || !modelData[modelName]) {
910
+ return ["name", "title", "description"];
911
+ }
912
+ const entities = modelData[modelName];
913
+ if (entities.length === 0) {
914
+ return ["name", "title", "description"];
915
+ }
916
+ const firstEntity = entities[0];
917
+ if (!firstEntity || !firstEntity.data) {
918
+ return ["name", "title", "description"];
919
+ }
920
+ const fields = Object.keys(firstEntity.data).filter(
921
+ (field) => field !== "id" && !field.endsWith("Id") && field !== "createdAt" && field !== "updatedAt"
922
+ );
923
+ return fields.length > 0 ? fields : ["name", "title", "description"];
924
+ }
925
+ }
926
+ function usePatternAdapter(config = {}) {
927
+ return useMemo(() => new ReactPatternAdapter(config), [config]);
928
+ }
929
+ export {
930
+ ATOMIC_COMPONENTS_REGISTRY,
931
+ COMPOSITE_VIEW_PATTERNS,
932
+ REACT_PROTOCOL_MAPPING,
933
+ ReactPatternAdapter,
934
+ usePatternAdapter
935
+ };