@specverse/engine-realize 3.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/examples/09-api/ai-spec.yaml +194 -0
- package/assets/examples/09-api/converted.yaml +95 -0
- package/assets/examples/09-api/diagram-architecture.mmd +10 -0
- package/assets/examples/09-api/diagram-er.mmd +10 -0
- package/assets/examples/09-api/documentation.html +104 -0
- package/assets/examples/09-api/documentation.md +95 -0
- package/assets/examples/09-api/inferred-spec.yaml +420 -0
- package/assets/examples/09-api/openapi.json +61 -0
- package/assets/examples/10-api/README.md +216 -0
- package/assets/examples/10-api/ai-spec.yaml +194 -0
- package/assets/examples/10-api/converted.yaml +96 -0
- package/assets/examples/10-api/diagram-architecture.mmd +10 -0
- package/assets/examples/10-api/diagram-er.mmd +10 -0
- package/assets/examples/10-api/documentation.html +104 -0
- package/assets/examples/10-api/documentation.md +95 -0
- package/assets/examples/10-api/inferred-spec.yaml +7 -0
- package/assets/examples/10-api/metadata.yaml +89 -0
- package/assets/examples/10-api/openapi.json +61 -0
- package/assets/examples/10-api/package-integration-test.js +177 -0
- package/assets/examples/10-api/usage-example.js +323 -0
- package/assets/examples/10-api/usage-example.ts +363 -0
- package/assets/examples/10-api/workflow-test.js +113 -0
- package/assets/examples/manifests/01-simple-default-mappings.yaml +36 -0
- package/assets/examples/manifests/02-capability-mappings.yaml +55 -0
- package/assets/examples/manifests/03-hybrid-mappings.yaml +109 -0
- package/assets/examples/manifests/README.md +245 -0
- package/assets/examples/manifests/backend-only.yaml +43 -0
- package/assets/examples/manifests/blog-api.md +78 -0
- package/assets/examples/manifests/blog-api.specly +79 -0
- package/assets/examples/manifests/frontend-only.yaml +27 -0
- package/assets/examples/manifests/fullstack-app.yaml +44 -0
- package/assets/examples/manifests/fullstack-monorepo.yaml +62 -0
- package/assets/examples/validate-examples-with-expected-failures.cjs +328 -0
- package/assets/examples/validate-examples.cjs +225 -0
- package/assets/examples-decomposed/cloud-native-manifest.example.yaml +8 -0
- package/assets/examples-decomposed/cloud-native-manifest.md +379 -0
- package/assets/examples-decomposed/cloud-native-manifest.specly +60 -0
- package/assets/examples-decomposed/docker-compose-manifest.example.yaml +8 -0
- package/assets/examples-decomposed/docker-compose-manifest.md +326 -0
- package/assets/examples-decomposed/docker-compose-manifest.specly +40 -0
- package/assets/examples-decomposed/kubernetes-deployment-manifest.example.yaml +8 -0
- package/assets/examples-decomposed/kubernetes-deployment-manifest.md +237 -0
- package/assets/examples-decomposed/kubernetes-deployment-manifest.specly +41 -0
- package/assets/templates/README.md +559 -0
- package/assets/templates/TEMPLATE-ENHANCEMENTS-V33.md +462 -0
- package/assets/templates/backend-only/CLAUDE.md +73 -0
- package/assets/templates/backend-only/README.md +197 -0
- package/assets/templates/backend-only/deployments/README.md +149 -0
- package/assets/templates/backend-only/deployments/development.specly +53 -0
- package/assets/templates/backend-only/deployments/production.specly +87 -0
- package/assets/templates/backend-only/docs/README.md +50 -0
- package/assets/templates/backend-only/docs/api/README.md +7 -0
- package/assets/templates/backend-only/docs/diagrams/README.md +85 -0
- package/assets/templates/backend-only/docs/example-documentation-template.md +269 -0
- package/assets/templates/backend-only/docs/guides/README.md +15 -0
- package/assets/templates/backend-only/dot.env.example +18 -0
- package/assets/templates/backend-only/generated/README.md +56 -0
- package/assets/templates/backend-only/generated/code/integration-test.template.js +320 -0
- package/assets/templates/backend-only/generated/code/package.json.template +34 -0
- package/assets/templates/backend-only/generated/docs/README.md +49 -0
- package/assets/templates/backend-only/gitignore +54 -0
- package/assets/templates/backend-only/manifests/README.md +72 -0
- package/assets/templates/backend-only/manifests/docker-compose.specly +91 -0
- package/assets/templates/backend-only/manifests/implementation.yaml +100 -0
- package/assets/templates/backend-only/manifests/kubernetes.specly +140 -0
- package/assets/templates/backend-only/package.json +59 -0
- package/assets/templates/backend-only/scripts/test-all.sh +160 -0
- package/assets/templates/backend-only/scripts/test-generated-code.sh +165 -0
- package/assets/templates/backend-only/specs/main.specly +67 -0
- package/assets/templates/default/CLAUDE.md +141 -0
- package/assets/templates/default/README.md +404 -0
- package/assets/templates/default/deployments/README.md +149 -0
- package/assets/templates/default/deployments/development.specly +53 -0
- package/assets/templates/default/deployments/production.specly +87 -0
- package/assets/templates/default/docs/README.md +50 -0
- package/assets/templates/default/docs/api/README.md +7 -0
- package/assets/templates/default/docs/diagrams/README.md +85 -0
- package/assets/templates/default/docs/example-documentation-template.md +269 -0
- package/assets/templates/default/docs/guides/README.md +15 -0
- package/assets/templates/default/dot.env.example +18 -0
- package/assets/templates/default/generated/README.md +56 -0
- package/assets/templates/default/generated/code/integration-test.template.js +320 -0
- package/assets/templates/default/generated/code/package.json.template +34 -0
- package/assets/templates/default/generated/docs/README.md +49 -0
- package/assets/templates/default/gitignore +54 -0
- package/assets/templates/default/manifests/README.md +72 -0
- package/assets/templates/default/manifests/docker-compose.specly +91 -0
- package/assets/templates/default/manifests/implementation.yaml +176 -0
- package/assets/templates/default/manifests/kubernetes.specly +140 -0
- package/assets/templates/default/package.json +61 -0
- package/assets/templates/default/scripts/test-all.sh +160 -0
- package/assets/templates/default/scripts/test-generated-code.sh +165 -0
- package/assets/templates/default/specs/main.specly +67 -0
- package/assets/templates/frontend-only/CLAUDE.md +75 -0
- package/assets/templates/frontend-only/README.md +231 -0
- package/assets/templates/frontend-only/deployments/README.md +149 -0
- package/assets/templates/frontend-only/deployments/development.specly +53 -0
- package/assets/templates/frontend-only/deployments/production.specly +87 -0
- package/assets/templates/frontend-only/docs/README.md +50 -0
- package/assets/templates/frontend-only/docs/api/README.md +7 -0
- package/assets/templates/frontend-only/docs/diagrams/README.md +85 -0
- package/assets/templates/frontend-only/docs/example-documentation-template.md +269 -0
- package/assets/templates/frontend-only/docs/guides/README.md +15 -0
- package/assets/templates/frontend-only/dot.env.example +18 -0
- package/assets/templates/frontend-only/generated/README.md +56 -0
- package/assets/templates/frontend-only/generated/code/integration-test.template.js +320 -0
- package/assets/templates/frontend-only/generated/code/package.json.template +34 -0
- package/assets/templates/frontend-only/generated/docs/README.md +49 -0
- package/assets/templates/frontend-only/gitignore +54 -0
- package/assets/templates/frontend-only/manifests/README.md +72 -0
- package/assets/templates/frontend-only/manifests/docker-compose.specly +91 -0
- package/assets/templates/frontend-only/manifests/implementation.yaml +58 -0
- package/assets/templates/frontend-only/manifests/kubernetes.specly +140 -0
- package/assets/templates/frontend-only/package.json +59 -0
- package/assets/templates/frontend-only/scripts/test-all.sh +160 -0
- package/assets/templates/frontend-only/scripts/test-generated-code.sh +165 -0
- package/assets/templates/frontend-only/specs/main.specly +57 -0
- package/assets/templates/full-stack/AI-GUIDE.md +60 -0
- package/assets/templates/full-stack/CLAUDE.md +141 -0
- package/assets/templates/full-stack/README.md +382 -0
- package/assets/templates/full-stack/archive/AI-GUIDE-legacy.md +392 -0
- package/assets/templates/full-stack/deployments/README.md +149 -0
- package/assets/templates/full-stack/deployments/development.specly +53 -0
- package/assets/templates/full-stack/deployments/production.specly +87 -0
- package/assets/templates/full-stack/docs/README.md +51 -0
- package/assets/templates/full-stack/docs/api/README.md +7 -0
- package/assets/templates/full-stack/docs/diagrams/README.md +85 -0
- package/assets/templates/full-stack/docs/example-documentation-template.md +269 -0
- package/assets/templates/full-stack/docs/guides/README.md +15 -0
- package/assets/templates/full-stack/generated/README.md +56 -0
- package/assets/templates/full-stack/generated/code/integration-test.template.js +320 -0
- package/assets/templates/full-stack/generated/code/package.json.template +34 -0
- package/assets/templates/full-stack/generated/docs/README.md +49 -0
- package/assets/templates/full-stack/gitignore +54 -0
- package/assets/templates/full-stack/manifests/README.md +72 -0
- package/assets/templates/full-stack/manifests/docker-compose.specly +91 -0
- package/assets/templates/full-stack/manifests/implementation.yaml +155 -0
- package/assets/templates/full-stack/manifests/kubernetes.specly +140 -0
- package/assets/templates/full-stack/package.json +45 -0
- package/assets/templates/full-stack/scripts/test-all.sh +160 -0
- package/assets/templates/full-stack/scripts/test-generated-code.sh +165 -0
- package/assets/templates/full-stack/specs/example-v33.specly +297 -0
- package/assets/templates/full-stack/specs/main-simple.specly +73 -0
- package/assets/templates/full-stack/specs/main.specly +408 -0
- package/dist/engines/code-generator.d.ts +86 -0
- package/dist/engines/code-generator.d.ts.map +1 -0
- package/dist/engines/code-generator.js +159 -0
- package/dist/engines/code-generator.js.map +1 -0
- package/dist/engines/engine-registry.d.ts +94 -0
- package/dist/engines/engine-registry.d.ts.map +1 -0
- package/dist/engines/engine-registry.js +163 -0
- package/dist/engines/engine-registry.js.map +1 -0
- package/dist/engines/index.d.ts +10 -0
- package/dist/engines/index.d.ts.map +1 -0
- package/dist/engines/index.js +12 -0
- package/dist/engines/index.js.map +1 -0
- package/dist/engines/typescript-engine.d.ts +74 -0
- package/dist/engines/typescript-engine.d.ts.map +1 -0
- package/dist/engines/typescript-engine.js +288 -0
- package/dist/engines/typescript-engine.js.map +1 -0
- package/dist/generators/index.d.ts +11 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +11 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +434 -0
- package/dist/index.js.map +1 -0
- package/dist/library/index.d.ts +12 -0
- package/dist/library/index.d.ts.map +1 -0
- package/dist/library/index.js +15 -0
- package/dist/library/index.js.map +1 -0
- package/dist/library/library.d.ts +132 -0
- package/dist/library/library.d.ts.map +1 -0
- package/dist/library/library.js +343 -0
- package/dist/library/library.js.map +1 -0
- package/dist/library/loader.d.ts +73 -0
- package/dist/library/loader.d.ts.map +1 -0
- package/dist/library/loader.js +150 -0
- package/dist/library/loader.js.map +1 -0
- package/dist/library/resolver.d.ts +104 -0
- package/dist/library/resolver.d.ts.map +1 -0
- package/dist/library/resolver.js +299 -0
- package/dist/library/resolver.js.map +1 -0
- package/dist/library/validator.d.ts +65 -0
- package/dist/library/validator.d.ts.map +1 -0
- package/dist/library/validator.js +203 -0
- package/dist/library/validator.js.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/instance-factory.d.ts +289 -0
- package/dist/types/instance-factory.d.ts.map +1 -0
- package/dist/types/instance-factory.js +8 -0
- package/dist/types/instance-factory.js.map +1 -0
- package/dist/types/unified-mappings.d.ts +163 -0
- package/dist/types/unified-mappings.d.ts.map +1 -0
- package/dist/types/unified-mappings.js +110 -0
- package/dist/types/unified-mappings.js.map +1 -0
- package/dist/utils/ai-spec-loader.d.ts +77 -0
- package/dist/utils/ai-spec-loader.d.ts.map +1 -0
- package/dist/utils/ai-spec-loader.js +138 -0
- package/dist/utils/ai-spec-loader.js.map +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/manifest-loader.d.ts +107 -0
- package/dist/utils/manifest-loader.d.ts.map +1 -0
- package/dist/utils/manifest-loader.js +168 -0
- package/dist/utils/manifest-loader.js.map +1 -0
- package/dist/utils/mapping-migration.d.ts +53 -0
- package/dist/utils/mapping-migration.d.ts.map +1 -0
- package/dist/utils/mapping-migration.js +194 -0
- package/dist/utils/mapping-migration.js.map +1 -0
- package/libs/instance-factories/CURVED-INTERFACE.md +278 -0
- package/libs/instance-factories/README.md +433 -0
- package/libs/instance-factories/applications/generic-app.yaml +52 -0
- package/libs/instance-factories/applications/react-app.yaml +186 -0
- package/libs/instance-factories/applications/templates/generic/backend-env-generator.ts +31 -0
- package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +80 -0
- package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +69 -0
- package/libs/instance-factories/applications/templates/generic/main-generator.ts +308 -0
- package/libs/instance-factories/applications/templates/react/_view-components-source.ts +555 -0
- package/libs/instance-factories/applications/templates/react/api-client-generator.ts +436 -0
- package/libs/instance-factories/applications/templates/react/api-types-generator.ts +153 -0
- package/libs/instance-factories/applications/templates/react/app-tsx-generator.ts +94 -0
- package/libs/instance-factories/applications/templates/react/env-example-generator.ts +24 -0
- package/libs/instance-factories/applications/templates/react/field-helpers-generator.ts +106 -0
- package/libs/instance-factories/applications/templates/react/gitignore-generator.ts +38 -0
- package/libs/instance-factories/applications/templates/react/index-css-generator.ts +85 -0
- package/libs/instance-factories/applications/templates/react/index-html-generator.ts +30 -0
- package/libs/instance-factories/applications/templates/react/main-tsx-generator.ts +34 -0
- package/libs/instance-factories/applications/templates/react/package-json-generator.ts +54 -0
- package/libs/instance-factories/applications/templates/react/pattern-adapter-generator.ts +179 -0
- package/libs/instance-factories/applications/templates/react/react-pattern-adapter.tsx +1347 -0
- package/libs/instance-factories/applications/templates/react/relationship-field-generator.ts +150 -0
- package/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.ts +704 -0
- package/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.ts +84 -0
- package/libs/instance-factories/applications/templates/react/tsconfig-generator.ts +35 -0
- package/libs/instance-factories/applications/templates/react/use-api-hooks-generator.ts +121 -0
- package/libs/instance-factories/applications/templates/react/view-dashboard-generator.ts +150 -0
- package/libs/instance-factories/applications/templates/react/view-detail-generator.ts +150 -0
- package/libs/instance-factories/applications/templates/react/view-form-generator.ts +362 -0
- package/libs/instance-factories/applications/templates/react/view-list-generator.ts +98 -0
- package/libs/instance-factories/applications/templates/react/view-router-generator.ts +89 -0
- package/libs/instance-factories/applications/templates/react/vite-config-generator.ts +49 -0
- package/libs/instance-factories/archived/fastify-prisma.yaml +104 -0
- package/libs/instance-factories/cli/commander-js.yaml +55 -0
- package/libs/instance-factories/cli/templates/commander/cli-entry-generator.d.ts +12 -0
- package/libs/instance-factories/cli/templates/commander/cli-entry-generator.d.ts.map +1 -0
- package/libs/instance-factories/cli/templates/commander/cli-entry-generator.js +115 -0
- package/libs/instance-factories/cli/templates/commander/cli-entry-generator.js.map +1 -0
- package/libs/instance-factories/cli/templates/commander/cli-entry-generator.ts +145 -0
- package/libs/instance-factories/cli/templates/commander/command-generator.d.ts +14 -0
- package/libs/instance-factories/cli/templates/commander/command-generator.d.ts.map +1 -0
- package/libs/instance-factories/cli/templates/commander/command-generator.js +182 -0
- package/libs/instance-factories/cli/templates/commander/command-generator.js.map +1 -0
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +992 -0
- package/libs/instance-factories/communication/event-emitter.yaml +56 -0
- package/libs/instance-factories/communication/rabbitmq-events.yaml +87 -0
- package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +93 -0
- package/libs/instance-factories/communication/templates/eventemitter/publisher-generator.ts +117 -0
- package/libs/instance-factories/communication/templates/eventemitter/subscriber-generator.ts +101 -0
- package/libs/instance-factories/controllers/fastify.yaml +127 -0
- package/libs/instance-factories/controllers/templates/fastify/meta-routes-generator.ts +103 -0
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +389 -0
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +76 -0
- package/libs/instance-factories/infrastructure/docker-k8s.yaml +61 -0
- package/libs/instance-factories/infrastructure/templates/docker-k8s/infrastructure-generator.ts +46 -0
- package/libs/instance-factories/orms/prisma.yaml +89 -0
- package/libs/instance-factories/orms/templates/prisma/schema-generator.ts +563 -0
- package/libs/instance-factories/orms/templates/prisma/services-generator.ts +408 -0
- package/libs/instance-factories/scaffolding/generic-scaffold.yaml +65 -0
- package/libs/instance-factories/scaffolding/templates/generic/env-example-generator.ts +73 -0
- package/libs/instance-factories/scaffolding/templates/generic/env-generator.ts +85 -0
- package/libs/instance-factories/scaffolding/templates/generic/gitignore-generator.ts +69 -0
- package/libs/instance-factories/scaffolding/templates/generic/package-json-generator.ts +176 -0
- package/libs/instance-factories/scaffolding/templates/generic/readme-generator.ts +207 -0
- package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +78 -0
- package/libs/instance-factories/scaffolding/templates/generic/tsconfig-react-generator.ts +41 -0
- package/libs/instance-factories/sdks/python-sdk.yaml +66 -0
- package/libs/instance-factories/sdks/templates/python/sdk-generator.ts +50 -0
- package/libs/instance-factories/sdks/templates/typescript/sdk-generator.ts +49 -0
- package/libs/instance-factories/sdks/typescript-sdk.yaml +59 -0
- package/libs/instance-factories/services/prisma-services.yaml +71 -0
- package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +303 -0
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +532 -0
- package/libs/instance-factories/services/templates/prisma/service-generator.ts +315 -0
- package/libs/instance-factories/shared/path-resolver.ts +111 -0
- package/libs/instance-factories/storage/mongodb.yaml +79 -0
- package/libs/instance-factories/storage/postgresql.yaml +75 -0
- package/libs/instance-factories/storage/redis.yaml +79 -0
- package/libs/instance-factories/storage/templates/mongodb/config-generator.ts +15 -0
- package/libs/instance-factories/storage/templates/mongodb/docker-generator.ts +18 -0
- package/libs/instance-factories/storage/templates/postgresql/config-generator.ts +54 -0
- package/libs/instance-factories/storage/templates/postgresql/docker-generator.ts +55 -0
- package/libs/instance-factories/storage/templates/redis/config-generator.ts +16 -0
- package/libs/instance-factories/storage/templates/redis/docker-generator.ts +18 -0
- package/libs/instance-factories/test-generation.ts +192 -0
- package/libs/instance-factories/testing/templates/vitest/tests-generator.ts +51 -0
- package/libs/instance-factories/testing/vitest-tests.yaml +63 -0
- package/libs/instance-factories/tools/templates/mcp/mcp-server-generator.ts +136 -0
- package/libs/instance-factories/tools/templates/mcp/static/docs/DEPLOYMENT_GUIDE.md +630 -0
- package/libs/instance-factories/tools/templates/mcp/static/docs/HYBRID_RESOURCE_SYSTEM.md +330 -0
- package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/EXTENSION_DEPLOYMENT.md +552 -0
- package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/LOCAL_DEPLOYMENT.md +164 -0
- package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/WEB_DEPLOYMENT.md +247 -0
- package/libs/instance-factories/tools/templates/mcp/static/package.json +92 -0
- package/libs/instance-factories/tools/templates/mcp/static/scripts/build-enterprise.js +284 -0
- package/libs/instance-factories/tools/templates/mcp/static/scripts/build-extension.js +139 -0
- package/libs/instance-factories/tools/templates/mcp/static/scripts/build-local.js +74 -0
- package/libs/instance-factories/tools/templates/mcp/static/scripts/build-web.js +156 -0
- package/libs/instance-factories/tools/templates/mcp/static/scripts/copy-canonical-files.js +41 -0
- package/libs/instance-factories/tools/templates/mcp/static/scripts/test-deployments.js +259 -0
- package/libs/instance-factories/tools/templates/mcp/static/scripts/test-hybrid-resources.js +231 -0
- package/libs/instance-factories/tools/templates/mcp/static/scripts/test-hybrid-simple.js +196 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.ts +293 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.ts +90 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/index.ts +24 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.ts +15 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.ts +106 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.ts +75 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.ts +239 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.ts +1501 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.ts +211 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.ts +308 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.ts +210 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.ts +356 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.ts +524 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.ts +530 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.ts +594 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.ts +170 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/CLIProxyService.init.test.ts +544 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/CLIProxyService.test.ts +189 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/ResourcesProviderService.test.ts +89 -0
- package/libs/instance-factories/tools/templates/mcp/static/src/types/index.ts +110 -0
- package/libs/instance-factories/tools/templates/mcp/static/tsconfig.json +28 -0
- package/libs/instance-factories/tools/templates/vscode/static/extension.ts +1195 -0
- package/libs/instance-factories/tools/templates/vscode/static/language-configuration.json +34 -0
- package/libs/instance-factories/tools/templates/vscode/static/schemas/specverse-v3-schema.json +4279 -0
- package/libs/instance-factories/tools/templates/vscode/static/syntaxes/specverse.tmLanguage.json +138 -0
- package/libs/instance-factories/tools/templates/vscode/static/themes/README.md +74 -0
- package/libs/instance-factories/tools/templates/vscode/static/themes/complete-specverse-colors.json +122 -0
- package/libs/instance-factories/tools/templates/vscode/static/themes/specverse-basic-theme.json +65 -0
- package/libs/instance-factories/tools/templates/vscode/static/themes/specverse-complete-theme.json +123 -0
- package/libs/instance-factories/tools/templates/vscode/static/themes/specverse-theme-colors.json +64 -0
- package/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.ts +214 -0
- package/libs/instance-factories/validation/templates/zod/validation-generator.ts +46 -0
- package/libs/instance-factories/validation/zod.yaml +56 -0
- package/libs/instance-factories/views/index.d.ts +13 -0
- package/libs/instance-factories/views/index.d.ts.map +1 -0
- package/libs/instance-factories/views/index.js +18 -0
- package/libs/instance-factories/views/index.js.map +1 -0
- package/libs/instance-factories/views/index.ts +45 -0
- package/libs/instance-factories/views/react-components.yaml +129 -0
- package/libs/instance-factories/views/templates/ARCHITECTURE.md +198 -0
- package/libs/instance-factories/views/templates/react/adapters/antd-adapter.ts +869 -0
- package/libs/instance-factories/views/templates/react/adapters/mui-adapter.ts +953 -0
- package/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.ts +806 -0
- package/libs/instance-factories/views/templates/react/app-generator.ts +55 -0
- package/libs/instance-factories/views/templates/react/components-generator.ts +391 -0
- package/libs/instance-factories/views/templates/react/forms-generator.ts +343 -0
- package/libs/instance-factories/views/templates/react/frontend-package-json-generator.ts +54 -0
- package/libs/instance-factories/views/templates/react/hooks-generator.ts +122 -0
- package/libs/instance-factories/views/templates/react/index-css-generator.ts +209 -0
- package/libs/instance-factories/views/templates/react/index-html-generator.ts +34 -0
- package/libs/instance-factories/views/templates/react/main-tsx-generator.ts +29 -0
- package/libs/instance-factories/views/templates/react/react-component-generator.d.ts +152 -0
- package/libs/instance-factories/views/templates/react/react-component-generator.d.ts.map +1 -0
- package/libs/instance-factories/views/templates/react/react-component-generator.js +398 -0
- package/libs/instance-factories/views/templates/react/react-component-generator.js.map +1 -0
- package/libs/instance-factories/views/templates/react/react-component-generator.ts +533 -0
- package/libs/instance-factories/views/templates/react/router-generator.ts +197 -0
- package/libs/instance-factories/views/templates/react/router-generic-generator.ts +103 -0
- package/libs/instance-factories/views/templates/react/spec-json-generator.ts +17 -0
- package/libs/instance-factories/views/templates/react/types-generator.ts +76 -0
- package/libs/instance-factories/views/templates/react/views-metadata-generator.ts +42 -0
- package/libs/instance-factories/views/templates/react/vite-config-generator.ts +38 -0
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.d.ts.map +1 -0
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js.map +1 -0
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.ts +474 -0
- package/libs/instance-factories/views/templates/shared/__tests__/composite-patterns.test.ts +242 -0
- package/libs/instance-factories/views/templates/shared/adapter-types.d.ts +77 -0
- package/libs/instance-factories/views/templates/shared/adapter-types.d.ts.map +1 -0
- package/libs/instance-factories/views/templates/shared/adapter-types.js +47 -0
- package/libs/instance-factories/views/templates/shared/adapter-types.js.map +1 -0
- package/libs/instance-factories/views/templates/shared/adapter-types.ts +142 -0
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts +63 -0
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts.map +1 -0
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.js +822 -0
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.js.map +1 -0
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.ts +908 -0
- package/libs/instance-factories/views/templates/shared/base-generator.d.ts +247 -0
- package/libs/instance-factories/views/templates/shared/base-generator.d.ts.map +1 -0
- package/libs/instance-factories/views/templates/shared/base-generator.js +363 -0
- package/libs/instance-factories/views/templates/shared/base-generator.js.map +1 -0
- package/libs/instance-factories/views/templates/shared/base-generator.ts +608 -0
- package/libs/instance-factories/views/templates/shared/component-metadata.d.ts +254 -0
- package/libs/instance-factories/views/templates/shared/component-metadata.d.ts.map +1 -0
- package/libs/instance-factories/views/templates/shared/component-metadata.js +602 -0
- package/libs/instance-factories/views/templates/shared/component-metadata.js.map +1 -0
- package/libs/instance-factories/views/templates/shared/component-metadata.ts +803 -0
- package/libs/instance-factories/views/templates/shared/composite-pattern-types.ts +250 -0
- package/libs/instance-factories/views/templates/shared/composite-patterns.ts +535 -0
- package/libs/instance-factories/views/templates/shared/index.ts +68 -0
- package/libs/instance-factories/views/templates/shared/pattern-validator.ts +279 -0
- package/libs/instance-factories/views/templates/shared/property-mapper.d.ts +149 -0
- package/libs/instance-factories/views/templates/shared/property-mapper.d.ts.map +1 -0
- package/libs/instance-factories/views/templates/shared/property-mapper.js +580 -0
- package/libs/instance-factories/views/templates/shared/property-mapper.js.map +1 -0
- package/libs/instance-factories/views/templates/shared/property-mapper.ts +700 -0
- package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts +143 -0
- package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts.map +1 -0
- package/libs/instance-factories/views/templates/shared/syntax-mapper.js +420 -0
- package/libs/instance-factories/views/templates/shared/syntax-mapper.js.map +1 -0
- package/libs/instance-factories/views/templates/shared/syntax-mapper.ts +539 -0
- package/package.json +42 -0
- package/schema/SPECVERSE-SCHEMA.json +4274 -0
|
@@ -0,0 +1,1347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Pattern Adapter
|
|
3
|
+
*
|
|
4
|
+
* Maps tech-independent composite view patterns from @specverse/lang
|
|
5
|
+
* to React-specific implementations with hooks and Tailwind styling.
|
|
6
|
+
*
|
|
7
|
+
* This adapter implements the unified view architecture by:
|
|
8
|
+
* 1. Using COMPOSITE_VIEW_PATTERNS as the single source of truth
|
|
9
|
+
* 2. Mapping semantic CURVED operations to React hooks/API calls
|
|
10
|
+
* 3. Rendering using ATOMIC_COMPONENTS_REGISTRY via Tailwind
|
|
11
|
+
*
|
|
12
|
+
* Stage 2: React Adapter Refactor
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useMemo } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
COMPOSITE_VIEW_PATTERNS,
|
|
18
|
+
ATOMIC_COMPONENTS_REGISTRY,
|
|
19
|
+
type CompositeViewPattern,
|
|
20
|
+
type CURVEDOperation
|
|
21
|
+
} from '@specverse/lang/browser';
|
|
22
|
+
import { createUniversalTailwindAdapter } from './tailwind-adapter-generator';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* React-specific protocol mapping for CURVED operations
|
|
26
|
+
*
|
|
27
|
+
* Maps semantic operations to HTTP methods and endpoints.
|
|
28
|
+
* This is what lives in instance factories for code generation,
|
|
29
|
+
* but for runtime we need it here.
|
|
30
|
+
*/
|
|
31
|
+
export const REACT_PROTOCOL_MAPPING: Record<CURVEDOperation, {
|
|
32
|
+
method: string;
|
|
33
|
+
pathPattern: string;
|
|
34
|
+
}> = {
|
|
35
|
+
create: {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
pathPattern: '/api/{resource}'
|
|
38
|
+
},
|
|
39
|
+
update: {
|
|
40
|
+
method: 'PUT',
|
|
41
|
+
pathPattern: '/api/{resource}/{id}'
|
|
42
|
+
},
|
|
43
|
+
retrieve: {
|
|
44
|
+
method: 'GET',
|
|
45
|
+
pathPattern: '/api/{resource}/{id}'
|
|
46
|
+
},
|
|
47
|
+
retrieve_many: {
|
|
48
|
+
method: 'GET',
|
|
49
|
+
pathPattern: '/api/{resource}'
|
|
50
|
+
},
|
|
51
|
+
validate: {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
pathPattern: '/api/{resource}/validate'
|
|
54
|
+
},
|
|
55
|
+
evolve: {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
pathPattern: '/api/{resource}/{id}/evolve'
|
|
58
|
+
},
|
|
59
|
+
delete: {
|
|
60
|
+
method: 'DELETE',
|
|
61
|
+
pathPattern: '/api/{resource}/{id}'
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* React Pattern Adapter Configuration
|
|
67
|
+
*/
|
|
68
|
+
export interface ReactPatternAdapterConfig {
|
|
69
|
+
// API base URL
|
|
70
|
+
apiBaseUrl?: string;
|
|
71
|
+
|
|
72
|
+
// Custom protocol mapping (overrides defaults)
|
|
73
|
+
protocolMapping?: Partial<typeof REACT_PROTOCOL_MAPPING>;
|
|
74
|
+
|
|
75
|
+
// Tailwind adapter instance
|
|
76
|
+
tailwindAdapter?: ReturnType<typeof createUniversalTailwindAdapter>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Pattern rendering context with React-specific data
|
|
81
|
+
*/
|
|
82
|
+
export interface PatternRenderContext {
|
|
83
|
+
// Pattern being rendered
|
|
84
|
+
pattern: CompositeViewPattern;
|
|
85
|
+
|
|
86
|
+
// View spec from API
|
|
87
|
+
viewSpec: any;
|
|
88
|
+
|
|
89
|
+
// Model data (entity instances)
|
|
90
|
+
modelData: Record<string, any[]>;
|
|
91
|
+
|
|
92
|
+
// Model schemas (structure definitions with attributes and relationships)
|
|
93
|
+
modelSchemas?: Record<string, any>;
|
|
94
|
+
|
|
95
|
+
// Primary model name
|
|
96
|
+
primaryModel?: string;
|
|
97
|
+
|
|
98
|
+
// Selected entity (for detail/dashboard views)
|
|
99
|
+
selectedEntity?: any;
|
|
100
|
+
|
|
101
|
+
// Entities for primary model
|
|
102
|
+
primaryEntities?: any[];
|
|
103
|
+
|
|
104
|
+
// Protocol mapping
|
|
105
|
+
protocolMapping: typeof REACT_PROTOCOL_MAPPING;
|
|
106
|
+
|
|
107
|
+
// Tailwind adapter
|
|
108
|
+
tailwindAdapter: ReturnType<typeof createUniversalTailwindAdapter>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* React Pattern Adapter
|
|
113
|
+
*
|
|
114
|
+
* Provides React-specific rendering of tech-independent composite patterns.
|
|
115
|
+
*/
|
|
116
|
+
export class ReactPatternAdapter {
|
|
117
|
+
private tailwindAdapter: ReturnType<typeof createUniversalTailwindAdapter>;
|
|
118
|
+
|
|
119
|
+
constructor(config: ReactPatternAdapterConfig = {}) {
|
|
120
|
+
this.tailwindAdapter = config.tailwindAdapter || createUniversalTailwindAdapter();
|
|
121
|
+
// Store for future use
|
|
122
|
+
// this._config = config;
|
|
123
|
+
// this._protocolMapping = { ...REACT_PROTOCOL_MAPPING, ...config.protocolMapping };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Detect pattern type from view spec
|
|
128
|
+
*/
|
|
129
|
+
detectPattern(viewSpec: any): CompositeViewPattern | null {
|
|
130
|
+
const viewType = viewSpec.type?.toLowerCase();
|
|
131
|
+
|
|
132
|
+
// Map view type to pattern ID
|
|
133
|
+
const typeToPattern: Record<string, string> = {
|
|
134
|
+
'form': 'form-view',
|
|
135
|
+
'list': 'list-view',
|
|
136
|
+
'detail': 'detail-view',
|
|
137
|
+
'dashboard': 'dashboard-view'
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const patternId = typeToPattern[viewType];
|
|
141
|
+
if (!patternId) return null;
|
|
142
|
+
|
|
143
|
+
return COMPOSITE_VIEW_PATTERNS[patternId];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Render a pattern to HTML
|
|
148
|
+
*/
|
|
149
|
+
renderPattern(context: PatternRenderContext): string {
|
|
150
|
+
const { pattern } = context;
|
|
151
|
+
|
|
152
|
+
// Render based on pattern category
|
|
153
|
+
switch (pattern.category) {
|
|
154
|
+
case 'data-entry':
|
|
155
|
+
return this.renderFormView(context);
|
|
156
|
+
case 'data-display':
|
|
157
|
+
if (pattern.id === 'list-view') {
|
|
158
|
+
return this.renderListView(context);
|
|
159
|
+
} else if (pattern.id === 'detail-view') {
|
|
160
|
+
return this.renderDetailView(context);
|
|
161
|
+
}
|
|
162
|
+
return this.renderGenericDataDisplay(context);
|
|
163
|
+
case 'dashboard':
|
|
164
|
+
return this.renderDashboardView(context);
|
|
165
|
+
default:
|
|
166
|
+
return this.renderFallback(context);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Render FormView pattern
|
|
172
|
+
*/
|
|
173
|
+
private renderFormView(context: PatternRenderContext): string {
|
|
174
|
+
const { viewSpec, modelData, modelSchemas, primaryModel } = context;
|
|
175
|
+
let components = viewSpec.uiComponents || {};
|
|
176
|
+
|
|
177
|
+
// FALLBACK: Generate default form component if none defined
|
|
178
|
+
if (Object.keys(components).length === 0 && primaryModel) {
|
|
179
|
+
components = {
|
|
180
|
+
[`${primaryModel}Form`]: {
|
|
181
|
+
type: 'form',
|
|
182
|
+
properties: {
|
|
183
|
+
model: primaryModel,
|
|
184
|
+
sections: [
|
|
185
|
+
{
|
|
186
|
+
title: `${primaryModel} Details`,
|
|
187
|
+
fields: this.inferFieldsFromSchema(modelSchemas, primaryModel)
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let html = '<div class="space-y-4">';
|
|
196
|
+
|
|
197
|
+
// Render form components
|
|
198
|
+
for (const [componentName, componentDef] of Object.entries(components)) {
|
|
199
|
+
const def = componentDef as any;
|
|
200
|
+
const type = def.type?.toLowerCase();
|
|
201
|
+
const properties = def.properties || def;
|
|
202
|
+
|
|
203
|
+
if (type === 'form') {
|
|
204
|
+
html += this.renderFormComponent(componentName, def, modelData, modelSchemas, primaryModel, this.tailwindAdapter);
|
|
205
|
+
} else if (this.tailwindAdapter.components[type]) {
|
|
206
|
+
html += this.renderAtomicComponent(componentName, type, properties, this.tailwindAdapter);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
html += '</div>';
|
|
211
|
+
return html;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Render ListView pattern
|
|
216
|
+
*/
|
|
217
|
+
private renderListView(context: PatternRenderContext): string {
|
|
218
|
+
const { viewSpec, modelData, primaryModel } = context;
|
|
219
|
+
let components = viewSpec.uiComponents || {};
|
|
220
|
+
|
|
221
|
+
// FALLBACK: Generate default table component if none defined
|
|
222
|
+
if (Object.keys(components).length === 0 && primaryModel) {
|
|
223
|
+
const columns = this.inferFieldsFromModel(modelData, primaryModel);
|
|
224
|
+
components = {
|
|
225
|
+
[`${primaryModel}Table`]: {
|
|
226
|
+
type: 'table',
|
|
227
|
+
properties: {
|
|
228
|
+
model: primaryModel,
|
|
229
|
+
columns: columns
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let html = '<div class="space-y-4">';
|
|
236
|
+
|
|
237
|
+
// Render list components (filters, table/list, pagination)
|
|
238
|
+
for (const [componentName, componentDef] of Object.entries(components)) {
|
|
239
|
+
const def = componentDef as any;
|
|
240
|
+
const type = def.type?.toLowerCase();
|
|
241
|
+
const properties = def.properties || def;
|
|
242
|
+
|
|
243
|
+
if (type === 'table') {
|
|
244
|
+
html += this.renderTableComponent(componentName, def, modelData, primaryModel, this.tailwindAdapter);
|
|
245
|
+
} else if (type === 'list') {
|
|
246
|
+
html += this.renderListComponent(componentName, def, modelData, primaryModel, this.tailwindAdapter);
|
|
247
|
+
} else if (this.tailwindAdapter.components[type]) {
|
|
248
|
+
html += this.renderAtomicComponent(componentName, type, properties, this.tailwindAdapter);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
html += '</div>';
|
|
253
|
+
return html;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Render DetailView pattern
|
|
258
|
+
*/
|
|
259
|
+
private renderDetailView(context: PatternRenderContext): string {
|
|
260
|
+
const { viewSpec, selectedEntity, primaryModel, modelData, modelSchemas } = context;
|
|
261
|
+
let components = viewSpec.uiComponents || {};
|
|
262
|
+
|
|
263
|
+
if (!selectedEntity) {
|
|
264
|
+
return '<div class="p-4 text-gray-500 dark:text-gray-400">No entity selected</div>';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// FALLBACK: Generate default components if none defined
|
|
268
|
+
if (Object.keys(components).length === 0 && primaryModel) {
|
|
269
|
+
// Get fields from schema or fall back to data inference
|
|
270
|
+
const fields = this.inferFieldsFromSchema(modelSchemas, primaryModel);
|
|
271
|
+
|
|
272
|
+
// Start with content component
|
|
273
|
+
components = {
|
|
274
|
+
[`${primaryModel}Content`]: {
|
|
275
|
+
type: 'content',
|
|
276
|
+
fields: fields,
|
|
277
|
+
properties: {
|
|
278
|
+
model: primaryModel
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// Add list components for hasMany relationships (from schema)
|
|
284
|
+
if (modelSchemas && modelSchemas[primaryModel]?.relationships) {
|
|
285
|
+
const schemaRelationships = modelSchemas[primaryModel].relationships;
|
|
286
|
+
|
|
287
|
+
for (const [relName, relDef] of Object.entries(schemaRelationships)) {
|
|
288
|
+
const relDefObj = relDef as any;
|
|
289
|
+
|
|
290
|
+
// Only include hasMany relationships (these show as lists in detail view)
|
|
291
|
+
if (relDefObj.type === 'hasMany') {
|
|
292
|
+
const targetModel = relDefObj.targetModel || relDefObj.model || relName.charAt(0).toUpperCase() + relName.slice(1);
|
|
293
|
+
|
|
294
|
+
// Infer fields for the related model
|
|
295
|
+
const relatedFields = this.inferFieldsFromSchema(modelSchemas, targetModel);
|
|
296
|
+
|
|
297
|
+
components[`${relName}List`] = {
|
|
298
|
+
type: 'list',
|
|
299
|
+
fields: relatedFields.slice(0, 5), // Limit to first 5 fields for table
|
|
300
|
+
properties: {
|
|
301
|
+
model: targetModel,
|
|
302
|
+
relationship: relName
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let html = '<div class="space-y-4">';
|
|
311
|
+
|
|
312
|
+
// Render detail components
|
|
313
|
+
for (const [componentName, componentDef] of Object.entries(components)) {
|
|
314
|
+
const def = componentDef as any;
|
|
315
|
+
const type = def.type?.toLowerCase();
|
|
316
|
+
const properties = def.properties || def;
|
|
317
|
+
|
|
318
|
+
if (type === 'content') {
|
|
319
|
+
html += this.renderContentComponent(componentName, def, selectedEntity, this.tailwindAdapter);
|
|
320
|
+
} else if (type === 'list') {
|
|
321
|
+
html += this.renderDetailListComponent(componentName, def, modelData, selectedEntity, primaryModel, this.tailwindAdapter);
|
|
322
|
+
} else if (type === 'card') {
|
|
323
|
+
html += this.renderCardComponent(componentName, def, selectedEntity, this.tailwindAdapter);
|
|
324
|
+
} else if (this.tailwindAdapter.components[type]) {
|
|
325
|
+
html += this.renderAtomicComponent(componentName, type, properties, this.tailwindAdapter);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
html += '</div>';
|
|
330
|
+
return html;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Render DashboardView pattern
|
|
335
|
+
*/
|
|
336
|
+
private renderDashboardView(context: PatternRenderContext): string {
|
|
337
|
+
const { viewSpec, modelData } = context;
|
|
338
|
+
const components = viewSpec.uiComponents || {};
|
|
339
|
+
|
|
340
|
+
let html = '<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">';
|
|
341
|
+
|
|
342
|
+
// Render dashboard components (metrics, charts, etc.)
|
|
343
|
+
for (const [componentName, componentDef] of Object.entries(components)) {
|
|
344
|
+
const def = componentDef as any;
|
|
345
|
+
const type = def.type?.toLowerCase();
|
|
346
|
+
const properties = def.properties || def;
|
|
347
|
+
|
|
348
|
+
if (type === 'card' && properties.variant === 'metric') {
|
|
349
|
+
html += this.renderMetricCard(componentName, def, modelData, this.tailwindAdapter);
|
|
350
|
+
} else if (this.tailwindAdapter.components[type]) {
|
|
351
|
+
html += this.renderAtomicComponent(componentName, type, properties, this.tailwindAdapter);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
html += '</div>';
|
|
356
|
+
return html;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Render generic data display (fallback)
|
|
361
|
+
*/
|
|
362
|
+
private renderGenericDataDisplay(_context: PatternRenderContext): string {
|
|
363
|
+
return '<div class="p-4 text-gray-500 dark:text-gray-400">Generic data display</div>';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Render fallback for unknown patterns
|
|
368
|
+
*/
|
|
369
|
+
private renderFallback(context: PatternRenderContext): string {
|
|
370
|
+
const { pattern } = context;
|
|
371
|
+
return `<div class="p-4 bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-700 rounded">
|
|
372
|
+
<p class="text-yellow-800 dark:text-yellow-200">Pattern not yet implemented: ${pattern.name}</p>
|
|
373
|
+
</div>`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Helper: Render form component
|
|
378
|
+
*
|
|
379
|
+
* Generates a complete form with:
|
|
380
|
+
* - Inferred field types from model data (text, number, boolean)
|
|
381
|
+
* - Proper input types and validation
|
|
382
|
+
* - Required field indicators
|
|
383
|
+
* - Submit and reset buttons
|
|
384
|
+
* - Professional styling matching admin-demo
|
|
385
|
+
*/
|
|
386
|
+
private renderFormComponent(
|
|
387
|
+
componentName: string,
|
|
388
|
+
componentDef: any,
|
|
389
|
+
modelData: Record<string, any[]>,
|
|
390
|
+
modelSchemas: Record<string, any> | undefined,
|
|
391
|
+
primaryModel: string | undefined,
|
|
392
|
+
_tailwindAdapter: ReturnType<typeof createUniversalTailwindAdapter>
|
|
393
|
+
): string {
|
|
394
|
+
const properties = componentDef.properties || componentDef;
|
|
395
|
+
const sections = properties.sections || [];
|
|
396
|
+
const model = properties.model || primaryModel;
|
|
397
|
+
|
|
398
|
+
// Infer field types from model schema or data
|
|
399
|
+
const fieldTypes = this.inferFieldTypes(modelSchemas, modelData, model);
|
|
400
|
+
|
|
401
|
+
let html = `
|
|
402
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
403
|
+
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
|
404
|
+
<h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200">${componentName}</h4>
|
|
405
|
+
</div>
|
|
406
|
+
<div class="p-6">
|
|
407
|
+
<form class="space-y-6">
|
|
408
|
+
`;
|
|
409
|
+
|
|
410
|
+
// Render form sections
|
|
411
|
+
for (const section of sections) {
|
|
412
|
+
const fields = section.fields || [];
|
|
413
|
+
html += `
|
|
414
|
+
<div>
|
|
415
|
+
${section.title ? `<h5 class="font-semibold text-gray-900 dark:text-gray-100 mb-4">${section.title}</h5>` : ''}
|
|
416
|
+
<div class="space-y-4">
|
|
417
|
+
`;
|
|
418
|
+
|
|
419
|
+
// Show helpful message if no fields
|
|
420
|
+
if (fields.length === 0) {
|
|
421
|
+
html += `
|
|
422
|
+
<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">
|
|
423
|
+
No fields available for this form. Model data may be empty or all fields are system-generated.
|
|
424
|
+
</div>
|
|
425
|
+
`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
for (const field of fields) {
|
|
429
|
+
const fieldName = typeof field === 'string' ? field : field.name || field.fieldName;
|
|
430
|
+
const fieldLabel = typeof field === 'string'
|
|
431
|
+
? this.humanizeFieldName(fieldName)
|
|
432
|
+
: field.label || this.humanizeFieldName(fieldName);
|
|
433
|
+
const fieldType = fieldTypes[fieldName] || 'string';
|
|
434
|
+
const isRequired = field.required !== false; // Default to required
|
|
435
|
+
|
|
436
|
+
html += `<div>`;
|
|
437
|
+
html += `
|
|
438
|
+
<label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
|
|
439
|
+
${fieldLabel}
|
|
440
|
+
${isRequired ? '<span class="text-red-500 dark:text-red-400 ml-1">*</span>' : ''}
|
|
441
|
+
</label>
|
|
442
|
+
`;
|
|
443
|
+
|
|
444
|
+
// Generate appropriate input based on field type
|
|
445
|
+
if (fieldType === 'boolean') {
|
|
446
|
+
html += `
|
|
447
|
+
<div class="flex items-center">
|
|
448
|
+
<input
|
|
449
|
+
type="checkbox"
|
|
450
|
+
name="${fieldName}"
|
|
451
|
+
class="h-4 w-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500"
|
|
452
|
+
/>
|
|
453
|
+
<span class="ml-2 text-sm text-gray-600 dark:text-gray-300">Yes/No</span>
|
|
454
|
+
</div>
|
|
455
|
+
`;
|
|
456
|
+
} else if (fieldType === 'number') {
|
|
457
|
+
html += `
|
|
458
|
+
<input
|
|
459
|
+
type="number"
|
|
460
|
+
name="${fieldName}"
|
|
461
|
+
placeholder="Enter ${fieldLabel.toLowerCase()}"
|
|
462
|
+
${isRequired ? 'required' : ''}
|
|
463
|
+
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"
|
|
464
|
+
/>
|
|
465
|
+
`;
|
|
466
|
+
} else {
|
|
467
|
+
// Text input (default)
|
|
468
|
+
html += `
|
|
469
|
+
<input
|
|
470
|
+
type="text"
|
|
471
|
+
name="${fieldName}"
|
|
472
|
+
placeholder="Enter ${fieldLabel.toLowerCase()}"
|
|
473
|
+
${isRequired ? 'required' : ''}
|
|
474
|
+
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"
|
|
475
|
+
/>
|
|
476
|
+
`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
html += `</div>`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
html += `
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Add relationship fields (belongsTo) as select dropdowns
|
|
489
|
+
const relationshipFields = this.inferRelationshipFields(modelSchemas, modelData, model);
|
|
490
|
+
if (relationshipFields.length > 0) {
|
|
491
|
+
html += `
|
|
492
|
+
<div>
|
|
493
|
+
<h5 class="font-semibold text-gray-900 dark:text-gray-100 mb-4">Relationships</h5>
|
|
494
|
+
<div class="space-y-4">
|
|
495
|
+
`;
|
|
496
|
+
|
|
497
|
+
for (const relField of relationshipFields) {
|
|
498
|
+
const relatedEntities = modelData[relField.targetModel] || [];
|
|
499
|
+
|
|
500
|
+
html += `
|
|
501
|
+
<div>
|
|
502
|
+
<label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
|
|
503
|
+
${this.humanizeFieldName(relField.name)}
|
|
504
|
+
</label>
|
|
505
|
+
<select
|
|
506
|
+
name="${relField.foreignKey}"
|
|
507
|
+
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"
|
|
508
|
+
>
|
|
509
|
+
<option value="">-- Select ${relField.targetModel} --</option>
|
|
510
|
+
${relatedEntities.map((entity: any) => {
|
|
511
|
+
const displayName = entity.data?.name || entity.data?.title || entity.id;
|
|
512
|
+
return `<option value="${entity.id}">${displayName}</option>`;
|
|
513
|
+
}).join('')}
|
|
514
|
+
</select>
|
|
515
|
+
</div>
|
|
516
|
+
`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
html += `
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Add form action buttons
|
|
526
|
+
html += `
|
|
527
|
+
<div class="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-600">
|
|
528
|
+
<button
|
|
529
|
+
type="submit"
|
|
530
|
+
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded font-medium transition-colors"
|
|
531
|
+
>
|
|
532
|
+
Create ${model || 'Entity'}
|
|
533
|
+
</button>
|
|
534
|
+
<button
|
|
535
|
+
type="reset"
|
|
536
|
+
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"
|
|
537
|
+
>
|
|
538
|
+
Reset
|
|
539
|
+
</button>
|
|
540
|
+
</div>
|
|
541
|
+
</form>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
`;
|
|
545
|
+
|
|
546
|
+
return html;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Helper: Infer relationship fields from model schema or data
|
|
551
|
+
*
|
|
552
|
+
* Reads relationship definitions from the model schema (like original FormView).
|
|
553
|
+
* Falls back to inferring from foreign keys in data if no schema available.
|
|
554
|
+
*
|
|
555
|
+
* Strategy:
|
|
556
|
+
* 1. Read from schema.relationships (preferred - matches original FormView)
|
|
557
|
+
* 2. If entities exist, scan their fields for foreign keys
|
|
558
|
+
* 3. If no entities, look at available models and infer common relationships
|
|
559
|
+
*/
|
|
560
|
+
private inferRelationshipFields(
|
|
561
|
+
modelSchemas: Record<string, any> | undefined,
|
|
562
|
+
modelData: Record<string, any[]>,
|
|
563
|
+
modelName: string | undefined
|
|
564
|
+
): Array<{
|
|
565
|
+
name: string;
|
|
566
|
+
foreignKey: string;
|
|
567
|
+
targetModel: string;
|
|
568
|
+
}> {
|
|
569
|
+
const relationships: Array<{
|
|
570
|
+
name: string;
|
|
571
|
+
foreignKey: string;
|
|
572
|
+
targetModel: string;
|
|
573
|
+
}> = [];
|
|
574
|
+
|
|
575
|
+
if (!modelName) return relationships;
|
|
576
|
+
|
|
577
|
+
// Strategy 1: Read from schema.relationships (like original FormView)
|
|
578
|
+
if (modelSchemas && modelSchemas[modelName]?.relationships) {
|
|
579
|
+
const schemaRelationships = modelSchemas[modelName].relationships;
|
|
580
|
+
|
|
581
|
+
// Extract belongsTo relationships (these need form dropdowns)
|
|
582
|
+
for (const [relName, relDef] of Object.entries(schemaRelationships)) {
|
|
583
|
+
const relDefObj = relDef as any;
|
|
584
|
+
|
|
585
|
+
// Only include belongsTo relationships in forms
|
|
586
|
+
if (relDefObj.type === 'belongsTo') {
|
|
587
|
+
relationships.push({
|
|
588
|
+
name: relName,
|
|
589
|
+
foreignKey: relDefObj.foreignKey || `${relName}Id`,
|
|
590
|
+
targetModel: relDefObj.targetModel || relDefObj.model || relName.charAt(0).toUpperCase() + relName.slice(1)
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return relationships;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Strategy 2: If we have entities, scan for foreign keys
|
|
599
|
+
const entities = modelData[modelName] || [];
|
|
600
|
+
if (entities.length > 0 && entities[0]?.data) {
|
|
601
|
+
const firstEntity = entities[0];
|
|
602
|
+
|
|
603
|
+
// Look for foreign key fields (ending in "Id")
|
|
604
|
+
for (const fieldName of Object.keys(firstEntity.data)) {
|
|
605
|
+
if (fieldName.endsWith('Id') && fieldName !== 'id') {
|
|
606
|
+
// Extract relationship name (e.g., "authorId" -> "author")
|
|
607
|
+
const relName = fieldName.slice(0, -2);
|
|
608
|
+
// Capitalize to get model name (e.g., "author" -> "Author")
|
|
609
|
+
const targetModel = relName.charAt(0).toUpperCase() + relName.slice(1);
|
|
610
|
+
|
|
611
|
+
// Check if this model exists in modelData
|
|
612
|
+
if (modelData[targetModel]) {
|
|
613
|
+
relationships.push({
|
|
614
|
+
name: relName,
|
|
615
|
+
foreignKey: fieldName,
|
|
616
|
+
targetModel: targetModel
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Strategy 3: If no entities or no relationships found, infer from available models
|
|
624
|
+
// This ensures forms are usable even when creating the first entity
|
|
625
|
+
if (relationships.length === 0) {
|
|
626
|
+
const availableModels = Object.keys(modelData);
|
|
627
|
+
|
|
628
|
+
// Common relationship patterns based on model names
|
|
629
|
+
for (const targetModel of availableModels) {
|
|
630
|
+
if (targetModel !== modelName) {
|
|
631
|
+
// Create lowercase version for foreign key
|
|
632
|
+
const relName = targetModel.charAt(0).toLowerCase() + targetModel.slice(1);
|
|
633
|
+
const foreignKey = `${relName}Id`;
|
|
634
|
+
|
|
635
|
+
// Add as potential relationship
|
|
636
|
+
relationships.push({
|
|
637
|
+
name: relName,
|
|
638
|
+
foreignKey: foreignKey,
|
|
639
|
+
targetModel: targetModel
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return relationships;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Helper: Humanize field name
|
|
650
|
+
* Converts camelCase/snake_case to readable labels
|
|
651
|
+
*/
|
|
652
|
+
private humanizeFieldName(fieldName: string): string {
|
|
653
|
+
return fieldName
|
|
654
|
+
.replace(/([A-Z])/g, ' $1') // Add space before capitals
|
|
655
|
+
.replace(/_/g, ' ') // Replace underscores with spaces
|
|
656
|
+
.replace(/^./, (str) => str.toUpperCase()) // Capitalize first letter
|
|
657
|
+
.trim();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Helper: Infer field types from model schema or data
|
|
662
|
+
* Prefers schema definitions, falls back to examining actual data
|
|
663
|
+
*/
|
|
664
|
+
private inferFieldTypes(modelSchemas: Record<string, any> | undefined, modelData: Record<string, any[]>, modelName: string | undefined): Record<string, string> {
|
|
665
|
+
const types: Record<string, string> = {};
|
|
666
|
+
|
|
667
|
+
if (!modelName) return types;
|
|
668
|
+
|
|
669
|
+
// Strategy 1: Use schema if available
|
|
670
|
+
if (modelSchemas && modelSchemas[modelName]?.attributes) {
|
|
671
|
+
const attributes = modelSchemas[modelName].attributes;
|
|
672
|
+
for (const [fieldName, attrDef] of Object.entries(attributes)) {
|
|
673
|
+
const typeStr = typeof attrDef === 'string' ? attrDef : (attrDef as any)?.type || 'string';
|
|
674
|
+
|
|
675
|
+
if (typeStr.toLowerCase().includes('bool')) {
|
|
676
|
+
types[fieldName] = 'boolean';
|
|
677
|
+
} else if (typeStr.toLowerCase().includes('int') || typeStr.toLowerCase().includes('number') || typeStr.toLowerCase().includes('float')) {
|
|
678
|
+
types[fieldName] = 'number';
|
|
679
|
+
} else {
|
|
680
|
+
types[fieldName] = 'string';
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return types;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Strategy 2: Examine actual data if no schema
|
|
687
|
+
if (!modelData[modelName] || modelData[modelName].length === 0) return types;
|
|
688
|
+
|
|
689
|
+
const firstEntity = modelData[modelName][0];
|
|
690
|
+
if (!firstEntity || !firstEntity.data) return types;
|
|
691
|
+
|
|
692
|
+
for (const [fieldName, value] of Object.entries(firstEntity.data)) {
|
|
693
|
+
if (typeof value === 'boolean') {
|
|
694
|
+
types[fieldName] = 'boolean';
|
|
695
|
+
} else if (typeof value === 'number') {
|
|
696
|
+
types[fieldName] = 'number';
|
|
697
|
+
} else {
|
|
698
|
+
types[fieldName] = 'string';
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return types;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Helper: Infer field names from model schema
|
|
707
|
+
* Reads schema.attributes to get field definitions
|
|
708
|
+
*/
|
|
709
|
+
private inferFieldsFromSchema(modelSchemas: Record<string, any> | undefined, modelName: string | undefined): string[] {
|
|
710
|
+
if (!modelName || !modelSchemas || !modelSchemas[modelName]) {
|
|
711
|
+
// No schema - return common default fields
|
|
712
|
+
return ['name', 'title', 'description'];
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const schema = modelSchemas[modelName];
|
|
716
|
+
if (!schema.attributes) {
|
|
717
|
+
return ['name', 'title', 'description'];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Get field names from schema attributes, filter out system fields
|
|
721
|
+
const fields = Object.keys(schema.attributes).filter(field =>
|
|
722
|
+
field !== 'id' &&
|
|
723
|
+
field !== 'createdAt' &&
|
|
724
|
+
field !== 'updatedAt' &&
|
|
725
|
+
!this.isAutoGeneratedField(field, schema.attributes[field])
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
return fields.length > 0 ? fields : ['name', 'title', 'description'];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Helper: Check if field is auto-generated (shouldn't show in forms)
|
|
733
|
+
*/
|
|
734
|
+
private isAutoGeneratedField(fieldName: string, attrDef: any): boolean {
|
|
735
|
+
// System fields
|
|
736
|
+
if (['id', 'createdAt', 'updatedAt'].includes(fieldName)) return true;
|
|
737
|
+
|
|
738
|
+
// Check attribute definition for auto flag
|
|
739
|
+
if (typeof attrDef === 'object' && attrDef.auto) return true;
|
|
740
|
+
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Helper: Render table component
|
|
746
|
+
*
|
|
747
|
+
* Generates a data table with:
|
|
748
|
+
* - Column headers with proper formatting
|
|
749
|
+
* - Data type-aware value rendering (objects, booleans, etc.)
|
|
750
|
+
* - Empty state handling
|
|
751
|
+
* - Hover effects and professional styling
|
|
752
|
+
* - Responsive scrolling
|
|
753
|
+
*/
|
|
754
|
+
private renderTableComponent(
|
|
755
|
+
componentName: string,
|
|
756
|
+
componentDef: any,
|
|
757
|
+
modelData: Record<string, any[]>,
|
|
758
|
+
primaryModel: string | undefined,
|
|
759
|
+
_tailwindAdapter: ReturnType<typeof createUniversalTailwindAdapter>
|
|
760
|
+
): string {
|
|
761
|
+
const properties = componentDef.properties || componentDef;
|
|
762
|
+
const tableModel = properties.model || primaryModel;
|
|
763
|
+
const entities = modelData[tableModel || ''] || [];
|
|
764
|
+
const columns = properties.columns || [];
|
|
765
|
+
|
|
766
|
+
let html = `
|
|
767
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
768
|
+
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
|
769
|
+
<h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200">${componentName}</h4>
|
|
770
|
+
</div>
|
|
771
|
+
<div class="overflow-auto max-h-96">
|
|
772
|
+
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
773
|
+
<thead class="bg-gray-50 dark:bg-gray-700 sticky top-0">
|
|
774
|
+
<tr>
|
|
775
|
+
${columns.map((col: string) => `
|
|
776
|
+
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-200 uppercase tracking-wider">
|
|
777
|
+
${this.humanizeFieldName(col)}
|
|
778
|
+
</th>
|
|
779
|
+
`).join('')}
|
|
780
|
+
</tr>
|
|
781
|
+
</thead>
|
|
782
|
+
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
783
|
+
`;
|
|
784
|
+
|
|
785
|
+
if (entities.length === 0) {
|
|
786
|
+
html += `
|
|
787
|
+
<tr>
|
|
788
|
+
<td colspan="${columns.length}" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400 italic">
|
|
789
|
+
No ${tableModel || 'data'} entities yet
|
|
790
|
+
</td>
|
|
791
|
+
</tr>
|
|
792
|
+
`;
|
|
793
|
+
} else {
|
|
794
|
+
for (const entity of entities) {
|
|
795
|
+
html += `<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">`;
|
|
796
|
+
|
|
797
|
+
for (const col of columns) {
|
|
798
|
+
const value = entity.data?.[col];
|
|
799
|
+
let displayValue: string;
|
|
800
|
+
|
|
801
|
+
// Format value based on type
|
|
802
|
+
if (value === undefined || value === null || value === '') {
|
|
803
|
+
displayValue = '<span class="text-gray-400 dark:text-gray-500">—</span>';
|
|
804
|
+
} else if (typeof value === 'boolean') {
|
|
805
|
+
displayValue = `
|
|
806
|
+
<span class="inline-block px-2 py-1 rounded text-xs font-medium ${
|
|
807
|
+
value
|
|
808
|
+
? 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200'
|
|
809
|
+
: 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200'
|
|
810
|
+
}">
|
|
811
|
+
${value ? 'Yes' : 'No'}
|
|
812
|
+
</span>
|
|
813
|
+
`;
|
|
814
|
+
} else if (typeof value === 'object') {
|
|
815
|
+
// Show [Object] for objects in tables
|
|
816
|
+
displayValue = '<span class="text-gray-500 dark:text-gray-400 italic">[Object]</span>';
|
|
817
|
+
} else {
|
|
818
|
+
// Escape HTML for safe rendering
|
|
819
|
+
displayValue = String(value)
|
|
820
|
+
.replace(/&/g, '&')
|
|
821
|
+
.replace(/</g, '<')
|
|
822
|
+
.replace(/>/g, '>')
|
|
823
|
+
.replace(/"/g, '"');
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
html += `<td class="px-4 py-2 text-sm text-gray-900 dark:text-gray-100 whitespace-nowrap">${displayValue}</td>`;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
html += `</tr>`;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
html += `
|
|
834
|
+
</tbody>
|
|
835
|
+
</table>
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
838
|
+
`;
|
|
839
|
+
|
|
840
|
+
return html;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Helper: Render list component
|
|
845
|
+
*
|
|
846
|
+
* Generates a list view with:
|
|
847
|
+
* - Flexible field display (comma-separated or multi-line)
|
|
848
|
+
* - Data type-aware rendering
|
|
849
|
+
* - Empty state handling
|
|
850
|
+
* - Professional card-based styling
|
|
851
|
+
*/
|
|
852
|
+
private renderListComponent(
|
|
853
|
+
componentName: string,
|
|
854
|
+
componentDef: any,
|
|
855
|
+
modelData: Record<string, any[]>,
|
|
856
|
+
primaryModel: string | undefined,
|
|
857
|
+
_tailwindAdapter: ReturnType<typeof createUniversalTailwindAdapter>
|
|
858
|
+
): string {
|
|
859
|
+
const properties = componentDef.properties || componentDef;
|
|
860
|
+
const listModel = properties.model || primaryModel;
|
|
861
|
+
const entities = modelData[listModel || ''] || [];
|
|
862
|
+
const fields = properties.fields || [];
|
|
863
|
+
|
|
864
|
+
let html = `
|
|
865
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
866
|
+
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
|
867
|
+
<h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200">${componentName}</h4>
|
|
868
|
+
</div>
|
|
869
|
+
<div class="p-4">
|
|
870
|
+
`;
|
|
871
|
+
|
|
872
|
+
if (entities.length === 0) {
|
|
873
|
+
html += `
|
|
874
|
+
<div class="text-sm text-gray-500 dark:text-gray-400 italic text-center py-4">
|
|
875
|
+
No ${listModel || 'items'} yet
|
|
876
|
+
</div>
|
|
877
|
+
`;
|
|
878
|
+
} else {
|
|
879
|
+
html += '<ul class="space-y-2">';
|
|
880
|
+
|
|
881
|
+
for (const entity of entities) {
|
|
882
|
+
html += `
|
|
883
|
+
<li class="p-3 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
|
884
|
+
<div class="text-sm text-gray-900 dark:text-gray-100">
|
|
885
|
+
`;
|
|
886
|
+
|
|
887
|
+
// Render each field
|
|
888
|
+
const fieldValues: string[] = [];
|
|
889
|
+
for (const field of fields) {
|
|
890
|
+
const value = entity.data?.[field];
|
|
891
|
+
|
|
892
|
+
if (value === undefined || value === null || value === '') {
|
|
893
|
+
continue; // Skip empty values
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
let displayValue: string;
|
|
897
|
+
|
|
898
|
+
if (typeof value === 'boolean') {
|
|
899
|
+
displayValue = value ? 'Yes' : 'No';
|
|
900
|
+
} else if (typeof value === 'object') {
|
|
901
|
+
displayValue = '[Object]';
|
|
902
|
+
} else {
|
|
903
|
+
displayValue = String(value)
|
|
904
|
+
.replace(/&/g, '&')
|
|
905
|
+
.replace(/</g, '<')
|
|
906
|
+
.replace(/>/g, '>')
|
|
907
|
+
.replace(/"/g, '"');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
fieldValues.push(`<strong>${this.humanizeFieldName(field)}:</strong> ${displayValue}`);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
html += fieldValues.join(' • ');
|
|
914
|
+
|
|
915
|
+
html += `
|
|
916
|
+
</div>
|
|
917
|
+
</li>
|
|
918
|
+
`;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
html += '</ul>';
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
html += `
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
`;
|
|
928
|
+
|
|
929
|
+
return html;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Helper: Render content component (for detail views)
|
|
934
|
+
*
|
|
935
|
+
* Displays specific fields from an entity with rich formatting.
|
|
936
|
+
* This matches the original DetailView's "content" component behavior.
|
|
937
|
+
*/
|
|
938
|
+
private renderContentComponent(
|
|
939
|
+
componentName: string,
|
|
940
|
+
componentDef: any,
|
|
941
|
+
selectedEntity: any,
|
|
942
|
+
_tailwindAdapter: ReturnType<typeof createUniversalTailwindAdapter>
|
|
943
|
+
): string {
|
|
944
|
+
let fields = componentDef.fields || [];
|
|
945
|
+
const entityData = selectedEntity?.data || selectedEntity || {};
|
|
946
|
+
|
|
947
|
+
// FALLBACK: If no fields, infer from entity data
|
|
948
|
+
if (fields.length === 0 && entityData) {
|
|
949
|
+
fields = Object.keys(entityData).filter(key =>
|
|
950
|
+
key !== 'id' &&
|
|
951
|
+
!key.endsWith('Id') &&
|
|
952
|
+
!['createdAt', 'updatedAt'].includes(key)
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
let html = `
|
|
957
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
958
|
+
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
|
959
|
+
<h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200 capitalize">${componentName}</h4>
|
|
960
|
+
</div>
|
|
961
|
+
<div class="p-4">
|
|
962
|
+
`;
|
|
963
|
+
|
|
964
|
+
// Show debug info if still no fields
|
|
965
|
+
if (fields.length === 0) {
|
|
966
|
+
html += `
|
|
967
|
+
<p class="text-sm text-yellow-600 dark:text-yellow-400 italic">
|
|
968
|
+
No fields available (entity keys: ${Object.keys(entityData).join(', ')})
|
|
969
|
+
</p>
|
|
970
|
+
`;
|
|
971
|
+
} else {
|
|
972
|
+
html += '<div class="space-y-3">';
|
|
973
|
+
|
|
974
|
+
let displayedFields = 0;
|
|
975
|
+
for (const fieldName of fields) {
|
|
976
|
+
// Skip id fields
|
|
977
|
+
if (fieldName === 'id') continue;
|
|
978
|
+
|
|
979
|
+
const value = entityData[fieldName];
|
|
980
|
+
|
|
981
|
+
// Format the value (show even if null/undefined/empty)
|
|
982
|
+
let formattedValue: string;
|
|
983
|
+
|
|
984
|
+
if (value === undefined || value === null) {
|
|
985
|
+
formattedValue = '<span class="text-gray-400 dark:text-gray-500 italic">Not set</span>';
|
|
986
|
+
} else if (typeof value === 'object') {
|
|
987
|
+
// Objects: Show as formatted JSON
|
|
988
|
+
const jsonStr = JSON.stringify(value, null, 2)
|
|
989
|
+
.replace(/&/g, '&')
|
|
990
|
+
.replace(/</g, '<')
|
|
991
|
+
.replace(/>/g, '>')
|
|
992
|
+
.replace(/"/g, '"');
|
|
993
|
+
formattedValue = `
|
|
994
|
+
<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>
|
|
995
|
+
`;
|
|
996
|
+
} else if (typeof value === 'boolean') {
|
|
997
|
+
// Booleans: Show as Yes/No badge
|
|
998
|
+
formattedValue = `
|
|
999
|
+
<span class="inline-block px-2 py-1 rounded text-xs font-medium ${
|
|
1000
|
+
value
|
|
1001
|
+
? 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200'
|
|
1002
|
+
: 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200'
|
|
1003
|
+
}">
|
|
1004
|
+
${value ? 'Yes' : 'No'}
|
|
1005
|
+
</span>
|
|
1006
|
+
`;
|
|
1007
|
+
} else if (value === '') {
|
|
1008
|
+
formattedValue = '<span class="text-gray-400 dark:text-gray-500 italic">Empty</span>';
|
|
1009
|
+
} else {
|
|
1010
|
+
// Other values: Escape HTML and show as text
|
|
1011
|
+
formattedValue = String(value)
|
|
1012
|
+
.replace(/&/g, '&')
|
|
1013
|
+
.replace(/</g, '<')
|
|
1014
|
+
.replace(/>/g, '>')
|
|
1015
|
+
.replace(/"/g, '"');
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
html += `
|
|
1019
|
+
<div class="grid grid-cols-[120px_1fr] gap-4 items-start">
|
|
1020
|
+
<label class="font-semibold text-sm text-gray-700 dark:text-gray-200 capitalize text-left">${fieldName}</label>
|
|
1021
|
+
<div class="text-sm text-gray-900 dark:text-gray-100">${formattedValue}</div>
|
|
1022
|
+
</div>
|
|
1023
|
+
`;
|
|
1024
|
+
displayedFields++;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (displayedFields === 0) {
|
|
1028
|
+
html += '<p class="text-sm text-gray-500 dark:text-gray-400 italic">No fields to display</p>';
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
html += '</div>'; // Close space-y-3
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
html += `
|
|
1035
|
+
</div>
|
|
1036
|
+
</div>
|
|
1037
|
+
`;
|
|
1038
|
+
|
|
1039
|
+
return html;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Helper: Render list component in detail view (for related entities)
|
|
1044
|
+
*
|
|
1045
|
+
* Shows related entities in a table format.
|
|
1046
|
+
* This matches the original DetailView's "list" component behavior.
|
|
1047
|
+
*/
|
|
1048
|
+
private renderDetailListComponent(
|
|
1049
|
+
componentName: string,
|
|
1050
|
+
componentDef: any,
|
|
1051
|
+
modelData: Record<string, any[]>,
|
|
1052
|
+
selectedEntity: any,
|
|
1053
|
+
primaryModel: string | undefined,
|
|
1054
|
+
_tailwindAdapter: ReturnType<typeof createUniversalTailwindAdapter>
|
|
1055
|
+
): string {
|
|
1056
|
+
const properties = componentDef.properties || componentDef;
|
|
1057
|
+
let fields = componentDef.fields || properties.fields || [];
|
|
1058
|
+
let relatedModel = properties.model;
|
|
1059
|
+
|
|
1060
|
+
// FALLBACK: Infer model from component name if not specified
|
|
1061
|
+
// E.g., "commentsList" -> "Comment", "postsList" -> "Post"
|
|
1062
|
+
if (!relatedModel) {
|
|
1063
|
+
const lowerName = componentName.toLowerCase();
|
|
1064
|
+
// Try to extract model name from component name
|
|
1065
|
+
for (const modelName of Object.keys(modelData)) {
|
|
1066
|
+
if (lowerName.includes(modelName.toLowerCase())) {
|
|
1067
|
+
relatedModel = modelName;
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Get all entities for the related model
|
|
1074
|
+
const allRelatedEntities = relatedModel ? modelData[relatedModel] || [] : [];
|
|
1075
|
+
|
|
1076
|
+
// Filter by foreign key relationship
|
|
1077
|
+
// For example, if viewing Post (primaryModel), filter Comments by postId
|
|
1078
|
+
const foreignKey = primaryModel ? `${primaryModel.charAt(0).toLowerCase()}${primaryModel.slice(1)}Id` : null;
|
|
1079
|
+
const relatedEntities = foreignKey && selectedEntity?.id
|
|
1080
|
+
? allRelatedEntities.filter((e: any) => e.data?.[foreignKey] === selectedEntity.id)
|
|
1081
|
+
: allRelatedEntities;
|
|
1082
|
+
|
|
1083
|
+
// If no fields specified, infer from first entity
|
|
1084
|
+
if (fields.length === 0 && relatedEntities.length > 0) {
|
|
1085
|
+
const firstEntity = relatedEntities[0];
|
|
1086
|
+
if (firstEntity.data) {
|
|
1087
|
+
fields = Object.keys(firstEntity.data)
|
|
1088
|
+
.filter(key => key !== 'id' && !key.endsWith('Id'))
|
|
1089
|
+
.slice(0, 5); // Limit to first 5 fields
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
let html = `
|
|
1094
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
1095
|
+
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
|
1096
|
+
<h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200 capitalize">${componentName}</h4>
|
|
1097
|
+
</div>
|
|
1098
|
+
<div class="p-4">
|
|
1099
|
+
`;
|
|
1100
|
+
|
|
1101
|
+
if (relatedEntities.length === 0) {
|
|
1102
|
+
html += `
|
|
1103
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
|
|
1104
|
+
No ${relatedModel || 'related'} entities ${foreignKey ? `with ${foreignKey} = ${selectedEntity?.id}` : ''}
|
|
1105
|
+
</p>
|
|
1106
|
+
`;
|
|
1107
|
+
} else if (fields.length === 0) {
|
|
1108
|
+
html += `
|
|
1109
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
|
|
1110
|
+
${relatedEntities.length} ${relatedModel} entities found but no fields to display
|
|
1111
|
+
</p>
|
|
1112
|
+
`;
|
|
1113
|
+
} else {
|
|
1114
|
+
html += `
|
|
1115
|
+
<div class="overflow-auto max-h-96">
|
|
1116
|
+
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
1117
|
+
<thead class="bg-gray-50 dark:bg-gray-700 sticky top-0">
|
|
1118
|
+
<tr>
|
|
1119
|
+
${fields.map((fieldName: string) => `
|
|
1120
|
+
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-200 uppercase tracking-wider">
|
|
1121
|
+
${fieldName}
|
|
1122
|
+
</th>
|
|
1123
|
+
`).join('')}
|
|
1124
|
+
</tr>
|
|
1125
|
+
</thead>
|
|
1126
|
+
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
1127
|
+
${relatedEntities.map((entity: any) => `
|
|
1128
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
1129
|
+
${fields.map((fieldName: string) => `
|
|
1130
|
+
<td class="px-4 py-2 text-sm text-gray-900 dark:text-gray-100 whitespace-nowrap">
|
|
1131
|
+
${entity.data?.[fieldName] ?? '—'}
|
|
1132
|
+
</td>
|
|
1133
|
+
`).join('')}
|
|
1134
|
+
</tr>
|
|
1135
|
+
`).join('')}
|
|
1136
|
+
</tbody>
|
|
1137
|
+
</table>
|
|
1138
|
+
</div>
|
|
1139
|
+
`;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
html += `
|
|
1143
|
+
</div>
|
|
1144
|
+
</div>
|
|
1145
|
+
`;
|
|
1146
|
+
|
|
1147
|
+
return html;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Helper: Render card component
|
|
1152
|
+
*
|
|
1153
|
+
* Displays entity fields with sophisticated formatting:
|
|
1154
|
+
* - Objects: Formatted JSON in code block
|
|
1155
|
+
* - Booleans: Yes/No badges
|
|
1156
|
+
* - Other values: Plain text
|
|
1157
|
+
*/
|
|
1158
|
+
private renderCardComponent(
|
|
1159
|
+
componentName: string,
|
|
1160
|
+
_componentDef: any,
|
|
1161
|
+
selectedEntity: any,
|
|
1162
|
+
_tailwindAdapter: ReturnType<typeof createUniversalTailwindAdapter>
|
|
1163
|
+
): string {
|
|
1164
|
+
const entityData = selectedEntity.data || {};
|
|
1165
|
+
const fields = Object.keys(entityData);
|
|
1166
|
+
|
|
1167
|
+
let html = `
|
|
1168
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
1169
|
+
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
|
1170
|
+
<h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200">${componentName}</h4>
|
|
1171
|
+
</div>
|
|
1172
|
+
<div class="p-4">
|
|
1173
|
+
<div class="space-y-3">
|
|
1174
|
+
${fields.map(field => {
|
|
1175
|
+
const value = entityData[field];
|
|
1176
|
+
|
|
1177
|
+
// Skip undefined/null values
|
|
1178
|
+
if (value === undefined || value === null) return '';
|
|
1179
|
+
|
|
1180
|
+
// Format value based on type
|
|
1181
|
+
let formattedValue: string;
|
|
1182
|
+
|
|
1183
|
+
if (typeof value === 'object') {
|
|
1184
|
+
// Objects: Show as formatted JSON
|
|
1185
|
+
const jsonStr = JSON.stringify(value, null, 2)
|
|
1186
|
+
.replace(/&/g, '&')
|
|
1187
|
+
.replace(/</g, '<')
|
|
1188
|
+
.replace(/>/g, '>')
|
|
1189
|
+
.replace(/"/g, '"');
|
|
1190
|
+
formattedValue = `
|
|
1191
|
+
<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>
|
|
1192
|
+
`;
|
|
1193
|
+
} else if (typeof value === 'boolean') {
|
|
1194
|
+
// Booleans: Show as Yes/No badge
|
|
1195
|
+
formattedValue = `
|
|
1196
|
+
<span class="inline-block px-2 py-1 rounded text-xs font-medium ${
|
|
1197
|
+
value
|
|
1198
|
+
? 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200'
|
|
1199
|
+
: 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200'
|
|
1200
|
+
}">
|
|
1201
|
+
${value ? 'Yes' : 'No'}
|
|
1202
|
+
</span>
|
|
1203
|
+
`;
|
|
1204
|
+
} else {
|
|
1205
|
+
// Other values: Escape HTML and show as text
|
|
1206
|
+
const escaped = String(value)
|
|
1207
|
+
.replace(/&/g, '&')
|
|
1208
|
+
.replace(/</g, '<')
|
|
1209
|
+
.replace(/>/g, '>')
|
|
1210
|
+
.replace(/"/g, '"');
|
|
1211
|
+
formattedValue = escaped || '—';
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
return `
|
|
1215
|
+
<div class="grid grid-cols-[140px_1fr] gap-4 items-start">
|
|
1216
|
+
<label class="font-semibold text-sm text-gray-700 dark:text-gray-200 capitalize">${field}:</label>
|
|
1217
|
+
<div class="text-sm text-gray-900 dark:text-gray-100">${formattedValue}</div>
|
|
1218
|
+
</div>
|
|
1219
|
+
`;
|
|
1220
|
+
}).join('')}
|
|
1221
|
+
</div>
|
|
1222
|
+
</div>
|
|
1223
|
+
</div>
|
|
1224
|
+
`;
|
|
1225
|
+
|
|
1226
|
+
return html;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Helper: Render metric card
|
|
1231
|
+
*/
|
|
1232
|
+
private renderMetricCard(
|
|
1233
|
+
componentName: string,
|
|
1234
|
+
componentDef: any,
|
|
1235
|
+
modelData: Record<string, any[]>,
|
|
1236
|
+
_tailwindAdapter: ReturnType<typeof createUniversalTailwindAdapter>
|
|
1237
|
+
): string {
|
|
1238
|
+
const properties = componentDef.properties || componentDef;
|
|
1239
|
+
const metric = properties.metric;
|
|
1240
|
+
const metricModel = properties.model;
|
|
1241
|
+
const entities = modelData[metricModel] || [];
|
|
1242
|
+
|
|
1243
|
+
// Calculate metric value
|
|
1244
|
+
let metricValue = '0';
|
|
1245
|
+
if (entities.length > 0 && metric) {
|
|
1246
|
+
const values = entities
|
|
1247
|
+
.map((e: any) => e.data?.[metric])
|
|
1248
|
+
.filter((v: any) => v !== undefined && v !== null);
|
|
1249
|
+
|
|
1250
|
+
if (values.length > 0 && typeof values[0] === 'number') {
|
|
1251
|
+
const sum = values.reduce((acc: number, v: number) => acc + v, 0);
|
|
1252
|
+
metricValue = String(Math.round(sum));
|
|
1253
|
+
} else {
|
|
1254
|
+
metricValue = String(values.length);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
return `
|
|
1259
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6">
|
|
1260
|
+
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide">
|
|
1261
|
+
${metric || componentName}
|
|
1262
|
+
</p>
|
|
1263
|
+
<p class="mt-2 text-3xl font-semibold text-gray-900 dark:text-gray-100">
|
|
1264
|
+
${metricValue}
|
|
1265
|
+
</p>
|
|
1266
|
+
</div>
|
|
1267
|
+
`;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Helper: Render atomic component
|
|
1272
|
+
*/
|
|
1273
|
+
private renderAtomicComponent(
|
|
1274
|
+
componentName: string,
|
|
1275
|
+
type: string,
|
|
1276
|
+
properties: Record<string, any>,
|
|
1277
|
+
_tailwindAdapter: ReturnType<typeof createUniversalTailwindAdapter>
|
|
1278
|
+
): string {
|
|
1279
|
+
if (!this.tailwindAdapter.components[type]) {
|
|
1280
|
+
return `<div class="p-4 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 rounded">
|
|
1281
|
+
<p class="text-red-800 dark:text-red-200">Unknown component type: ${type}</p>
|
|
1282
|
+
</div>`;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
return `
|
|
1286
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
1287
|
+
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
|
1288
|
+
<h4 class="font-semibold text-sm text-gray-700 dark:text-gray-200">${componentName}</h4>
|
|
1289
|
+
</div>
|
|
1290
|
+
<div class="p-4">
|
|
1291
|
+
${this.tailwindAdapter.components[type].render({ properties })}
|
|
1292
|
+
</div>
|
|
1293
|
+
</div>
|
|
1294
|
+
`;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Helper: Infer field names from model data
|
|
1299
|
+
*
|
|
1300
|
+
* Extracts field names from the first entity in modelData for the given model.
|
|
1301
|
+
* Filters out system fields like id, createdAt, updatedAt, and foreign key fields.
|
|
1302
|
+
*
|
|
1303
|
+
* @param modelData - Map of model names to entity arrays
|
|
1304
|
+
* @param modelName - Name of the model to extract fields from
|
|
1305
|
+
* @returns Array of field names suitable for display
|
|
1306
|
+
*/
|
|
1307
|
+
private inferFieldsFromModel(modelData: Record<string, any[]>, modelName: string | undefined): string[] {
|
|
1308
|
+
if (!modelName || !modelData[modelName]) {
|
|
1309
|
+
// No model data - return common default fields
|
|
1310
|
+
return ['name', 'title', 'description'];
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const entities = modelData[modelName];
|
|
1314
|
+
if (entities.length === 0) {
|
|
1315
|
+
// No entities yet - return common default fields for this model
|
|
1316
|
+
return ['name', 'title', 'description'];
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const firstEntity = entities[0];
|
|
1320
|
+
if (!firstEntity || !firstEntity.data) {
|
|
1321
|
+
return ['name', 'title', 'description'];
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Get field names from entity data, filter out system fields
|
|
1325
|
+
const fields = Object.keys(firstEntity.data).filter(field =>
|
|
1326
|
+
field !== 'id' &&
|
|
1327
|
+
!field.endsWith('Id') &&
|
|
1328
|
+
field !== 'createdAt' &&
|
|
1329
|
+
field !== 'updatedAt'
|
|
1330
|
+
);
|
|
1331
|
+
|
|
1332
|
+
// If after filtering we have no fields, return common defaults
|
|
1333
|
+
return fields.length > 0 ? fields : ['name', 'title', 'description'];
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Hook: Use pattern adapter
|
|
1339
|
+
*/
|
|
1340
|
+
export function usePatternAdapter(config: ReactPatternAdapterConfig = {}) {
|
|
1341
|
+
return useMemo(() => new ReactPatternAdapter(config), [config]);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Export pattern registry for external use
|
|
1346
|
+
*/
|
|
1347
|
+
export { COMPOSITE_VIEW_PATTERNS, ATOMIC_COMPONENTS_REGISTRY };
|