@specverse/engines 4.1.30 → 4.2.0
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/manifests/frontend-only.yaml +3 -6
- package/assets/examples/manifests/fullstack-app.yaml +5 -7
- package/assets/examples/manifests/fullstack-monorepo.yaml +3 -6
- package/dist/inference/comprehensive-engine.d.ts.map +1 -1
- package/dist/inference/comprehensive-engine.js +3 -19
- package/dist/inference/comprehensive-engine.js.map +1 -1
- package/dist/inference/core/rule-engine.d.ts +31 -0
- package/dist/inference/core/rule-engine.d.ts.map +1 -1
- package/dist/inference/core/rule-engine.js +117 -33
- package/dist/inference/core/rule-engine.js.map +1 -1
- package/dist/inference/core/rule-file-types.d.ts +0 -2
- package/dist/inference/core/rule-file-types.d.ts.map +1 -1
- package/dist/inference/core/rule-file-types.js +3 -6
- package/dist/inference/core/rule-file-types.js.map +1 -1
- package/dist/inference/core/rule-loader.d.ts +5 -15
- package/dist/inference/core/rule-loader.d.ts.map +1 -1
- package/dist/inference/core/rule-loader.js +43 -132
- package/dist/inference/core/rule-loader.js.map +1 -1
- package/dist/inference/core/types.d.ts +0 -6
- package/dist/inference/core/types.d.ts.map +1 -1
- package/dist/inference/core/types.js +0 -4
- package/dist/inference/core/types.js.map +1 -1
- package/dist/inference/logical/generators/component-type-resolver.d.ts +0 -26
- package/dist/inference/logical/generators/component-type-resolver.d.ts.map +1 -1
- package/dist/inference/logical/generators/component-type-resolver.js +0 -19
- package/dist/inference/logical/generators/component-type-resolver.js.map +1 -1
- package/dist/inference/logical/generators/specialist-view-expander.d.ts +1 -17
- package/dist/inference/logical/generators/specialist-view-expander.d.ts.map +1 -1
- package/dist/inference/logical/generators/specialist-view-expander.js +0 -15
- package/dist/inference/logical/generators/specialist-view-expander.js.map +1 -1
- package/dist/inference/logical/generators/view-generator.d.ts +4 -14
- package/dist/inference/logical/generators/view-generator.d.ts.map +1 -1
- package/dist/inference/logical/generators/view-generator.js +6 -26
- package/dist/inference/logical/generators/view-generator.js.map +1 -1
- package/dist/inference/logical/index.d.ts +2 -2
- package/dist/inference/logical/index.d.ts.map +1 -1
- package/dist/inference/logical/logical-engine.d.ts.map +1 -1
- package/dist/inference/logical/logical-engine.js +17 -80
- package/dist/inference/logical/logical-engine.js.map +1 -1
- package/dist/inference/quint-transpiler.d.ts +5 -3
- package/dist/inference/quint-transpiler.d.ts.map +1 -1
- package/dist/inference/quint-transpiler.js +11 -6
- package/dist/inference/quint-transpiler.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +110 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +121 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +78 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +190 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +45 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/html-to-jsx.js +192 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +46 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/orchestrator.js +30 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +38 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/regen-safety.js +89 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +56 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +66 -0
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +14 -11
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +15 -22
- package/dist/realize/index.js.map +1 -1
- package/dist/registry/utils/manifest-adapter.d.ts +8 -1
- package/dist/registry/utils/manifest-adapter.d.ts.map +1 -1
- package/dist/registry/utils/manifest-adapter.js +8 -1
- package/dist/registry/utils/manifest-adapter.js.map +1 -1
- package/libs/instance-factories/applications/react-app-starter.yaml +150 -0
- package/libs/instance-factories/applications/templates/react-starter/README.md +211 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +153 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +145 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/form-body-composer.test.ts +175 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/helpers-emitter.test.ts +55 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/html-to-jsx.test.ts +140 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +146 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +163 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p2-factory-imports.test.ts +116 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +183 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/regen-safety.test.ts +144 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +114 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/view-emitter.test.ts +107 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +139 -0
- package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +141 -0
- package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +174 -0
- package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +135 -0
- package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +306 -0
- package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +60 -0
- package/libs/instance-factories/applications/templates/react-starter/html-to-jsx.ts +334 -0
- package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +120 -0
- package/libs/instance-factories/applications/templates/react-starter/orchestrator.ts +80 -0
- package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +57 -0
- package/libs/instance-factories/applications/templates/react-starter/regen-safety.ts +157 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/dashboard.tsx.template +47 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +94 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +114 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +72 -0
- package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +151 -0
- package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +137 -0
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +14 -11
- package/package.json +3 -3
- package/dist/libs/instance-factories/applications/templates/react/_view-components-source.js +0 -530
- package/dist/libs/instance-factories/applications/templates/react/app-tsx-generator.js +0 -73
- package/dist/libs/instance-factories/applications/templates/react/field-helpers-generator.js +0 -99
- package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +0 -49
- package/dist/libs/instance-factories/applications/templates/react/pattern-adapter-generator.js +0 -156
- package/dist/libs/instance-factories/applications/templates/react/react-pattern-adapter.js +0 -935
- package/dist/libs/instance-factories/applications/templates/react/relationship-field-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.js +0 -646
- package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.js +0 -65
- package/dist/libs/instance-factories/applications/templates/react/view-dashboard-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/view-detail-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/view-form-generator.js +0 -355
- package/dist/libs/instance-factories/applications/templates/react/view-list-generator.js +0 -91
- package/dist/libs/instance-factories/applications/templates/react/view-router-generator.js +0 -79
- package/dist/libs/instance-factories/views/index.js +0 -48
- package/dist/libs/instance-factories/views/templates/react/adapters/antd-adapter.js +0 -742
- package/dist/libs/instance-factories/views/templates/react/adapters/mui-adapter.js +0 -824
- package/dist/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.js +0 -719
- package/dist/libs/instance-factories/views/templates/react/app-generator.js +0 -45
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +0 -820
- package/dist/libs/instance-factories/views/templates/react/forms-generator.js +0 -275
- package/dist/libs/instance-factories/views/templates/react/frontend-package-json-generator.js +0 -46
- package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +0 -81
- package/dist/libs/instance-factories/views/templates/react/index-css-generator.js +0 -9
- package/dist/libs/instance-factories/views/templates/react/index-html-generator.js +0 -23
- package/dist/libs/instance-factories/views/templates/react/main-tsx-generator.js +0 -21
- package/dist/libs/instance-factories/views/templates/react/react-component-generator.js +0 -299
- package/dist/libs/instance-factories/views/templates/react/router-generator.js +0 -136
- package/dist/libs/instance-factories/views/templates/react/router-generic-generator.js +0 -107
- package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +0 -187
- package/dist/libs/instance-factories/views/templates/react/spec-json-generator.js +0 -7
- package/dist/libs/instance-factories/views/templates/react/types-generator.js +0 -56
- package/dist/libs/instance-factories/views/templates/react/views-metadata-generator.js +0 -27
- package/dist/libs/instance-factories/views/templates/react/vite-config-generator.js +0 -29
- package/dist/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js +0 -261
- package/dist/libs/instance-factories/views/templates/shared/adapter-types.js +0 -34
- package/dist/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -800
- package/dist/libs/instance-factories/views/templates/shared/base-generator.js +0 -305
- package/dist/libs/instance-factories/views/templates/shared/component-metadata.js +0 -517
- package/dist/libs/instance-factories/views/templates/shared/composite-pattern-types.js +0 -0
- package/dist/libs/instance-factories/views/templates/shared/composite-patterns.js +0 -445
- package/dist/libs/instance-factories/views/templates/shared/index.js +0 -80
- package/dist/libs/instance-factories/views/templates/shared/pattern-validator.js +0 -210
- package/dist/libs/instance-factories/views/templates/shared/property-mapper.js +0 -492
- package/dist/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -321
- package/dist/realize/index.js.bak +0 -758
- package/libs/instance-factories/applications/react-app.yaml +0 -186
- package/libs/instance-factories/applications/templates/react/_view-components-source.ts +0 -555
- package/libs/instance-factories/applications/templates/react/app-tsx-generator.ts +0 -94
- package/libs/instance-factories/applications/templates/react/field-helpers-generator.ts +0 -106
- package/libs/instance-factories/applications/templates/react/package-json-generator.ts +0 -57
- package/libs/instance-factories/applications/templates/react/pattern-adapter-generator.ts +0 -179
- package/libs/instance-factories/applications/templates/react/react-pattern-adapter.tsx +0 -1347
- package/libs/instance-factories/applications/templates/react/relationship-field-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.ts +0 -704
- package/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.ts +0 -84
- package/libs/instance-factories/applications/templates/react/view-dashboard-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/view-detail-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/view-form-generator.ts +0 -362
- package/libs/instance-factories/applications/templates/react/view-list-generator.ts +0 -98
- package/libs/instance-factories/applications/templates/react/view-router-generator.ts +0 -89
- package/libs/instance-factories/views/README.md +0 -62
- package/libs/instance-factories/views/index.d.ts +0 -13
- package/libs/instance-factories/views/index.d.ts.map +0 -1
- package/libs/instance-factories/views/index.js +0 -18
- package/libs/instance-factories/views/index.js.map +0 -1
- package/libs/instance-factories/views/index.ts +0 -45
- package/libs/instance-factories/views/react-components.yaml +0 -129
- package/libs/instance-factories/views/templates/ARCHITECTURE.md +0 -198
- package/libs/instance-factories/views/templates/react/adapters/antd-adapter.ts +0 -869
- package/libs/instance-factories/views/templates/react/adapters/mui-adapter.ts +0 -953
- package/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.ts +0 -806
- package/libs/instance-factories/views/templates/react/app-generator.ts +0 -55
- package/libs/instance-factories/views/templates/react/components-generator.ts +0 -938
- package/libs/instance-factories/views/templates/react/forms-generator.ts +0 -325
- package/libs/instance-factories/views/templates/react/frontend-package-json-generator.ts +0 -57
- package/libs/instance-factories/views/templates/react/hooks-generator.ts +0 -106
- package/libs/instance-factories/views/templates/react/index-css-generator.ts +0 -14
- package/libs/instance-factories/views/templates/react/index-html-generator.ts +0 -34
- package/libs/instance-factories/views/templates/react/main-tsx-generator.ts +0 -29
- package/libs/instance-factories/views/templates/react/react-component-generator.d.ts +0 -152
- package/libs/instance-factories/views/templates/react/react-component-generator.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/react/react-component-generator.js +0 -398
- package/libs/instance-factories/views/templates/react/react-component-generator.js.map +0 -1
- package/libs/instance-factories/views/templates/react/react-component-generator.ts +0 -533
- package/libs/instance-factories/views/templates/react/router-generator.ts +0 -197
- package/libs/instance-factories/views/templates/react/router-generic-generator.ts +0 -132
- package/libs/instance-factories/views/templates/react/shared-utils-generator.ts +0 -196
- package/libs/instance-factories/views/templates/react/spec-json-generator.ts +0 -17
- package/libs/instance-factories/views/templates/react/types-generator.ts +0 -76
- package/libs/instance-factories/views/templates/react/views-metadata-generator.ts +0 -42
- package/libs/instance-factories/views/templates/react/vite-config-generator.ts +0 -38
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js.map +0 -1
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.ts +0 -474
- package/libs/instance-factories/views/templates/shared/__tests__/composite-patterns.test.ts +0 -242
- package/libs/instance-factories/views/templates/shared/adapter-types.d.ts +0 -77
- package/libs/instance-factories/views/templates/shared/adapter-types.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/adapter-types.js +0 -47
- package/libs/instance-factories/views/templates/shared/adapter-types.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/adapter-types.ts +0 -142
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts +0 -63
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -822
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.ts +0 -908
- package/libs/instance-factories/views/templates/shared/base-generator.d.ts +0 -247
- package/libs/instance-factories/views/templates/shared/base-generator.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/base-generator.js +0 -363
- package/libs/instance-factories/views/templates/shared/base-generator.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/base-generator.ts +0 -608
- package/libs/instance-factories/views/templates/shared/component-metadata.d.ts +0 -254
- package/libs/instance-factories/views/templates/shared/component-metadata.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/component-metadata.js +0 -602
- package/libs/instance-factories/views/templates/shared/component-metadata.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/component-metadata.ts +0 -803
- package/libs/instance-factories/views/templates/shared/composite-pattern-types.ts +0 -250
- package/libs/instance-factories/views/templates/shared/composite-patterns.ts +0 -535
- package/libs/instance-factories/views/templates/shared/index.ts +0 -68
- package/libs/instance-factories/views/templates/shared/pattern-validator.ts +0 -279
- package/libs/instance-factories/views/templates/shared/property-mapper.d.ts +0 -149
- package/libs/instance-factories/views/templates/shared/property-mapper.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/property-mapper.js +0 -580
- package/libs/instance-factories/views/templates/shared/property-mapper.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/property-mapper.ts +0 -700
- package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts +0 -143
- package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -420
- package/libs/instance-factories/views/templates/shared/syntax-mapper.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/syntax-mapper.ts +0 -539
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List-view body composer for ReactAppStarter
|
|
3
|
+
*
|
|
4
|
+
* Implements the `renderBody` contract of view-emitter.ts for list
|
|
5
|
+
* views. Uses the canonical Tailwind adapter from
|
|
6
|
+
* `@specverse/runtime/views/tailwind` to render the TABLE SHELL, then
|
|
7
|
+
* injects a JSX `.map()` expression into the tbody so the generated
|
|
8
|
+
* component renders rows from the `filtered` array defined by the
|
|
9
|
+
* skeleton.
|
|
10
|
+
*
|
|
11
|
+
* Why split it this way: the adapter produces static HTML (the same
|
|
12
|
+
* HTML app-demo uses). For Factory B we want React that maps over
|
|
13
|
+
* runtime data — so we take the adapter's shell and replace a sentinel
|
|
14
|
+
* inside the tbody with a `{filtered.map(...)}` expression. The shell
|
|
15
|
+
* stays canonical; only the row-iteration JSX is synthesized here.
|
|
16
|
+
*
|
|
17
|
+
* See README.md for the full architecture.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createUniversalTailwindAdapter } from '@specverse/runtime/views/tailwind';
|
|
21
|
+
import { inferFieldsFromSchema } from '@specverse/runtime/views/core';
|
|
22
|
+
import { htmlToJsx } from './html-to-jsx.js';
|
|
23
|
+
import type { EmitContext, ModelSpec } from './view-emitter.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sentinel token. Must be ASCII-safe (no curly braces) so it survives
|
|
27
|
+
* the html-to-jsx transform unchanged, and must be unique enough not
|
|
28
|
+
* to appear in real HTML output.
|
|
29
|
+
*/
|
|
30
|
+
const TBODY_SENTINEL = '__SPECVERSE_TBODY_ROWS__';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compose the list view's body as JSX-safe source. Returned string is
|
|
34
|
+
* meant to be dropped at `{{BODY}}` inside `skeletons/list.tsx.template`.
|
|
35
|
+
*/
|
|
36
|
+
export function composeListBody(context: EmitContext): string {
|
|
37
|
+
const columns = inferColumns(context);
|
|
38
|
+
const headers = columns.map(humanize);
|
|
39
|
+
|
|
40
|
+
const adapter = createUniversalTailwindAdapter({ darkMode: true });
|
|
41
|
+
const shellHtml = adapter.components.table.render({
|
|
42
|
+
properties: { columns: headers },
|
|
43
|
+
children: TBODY_SENTINEL,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Transform the static HTML shell to JSX-safe source. The sentinel
|
|
47
|
+
// lands in a text position inside the <tbody> and survives the
|
|
48
|
+
// transform untouched.
|
|
49
|
+
const shellJsx = htmlToJsx(shellHtml);
|
|
50
|
+
|
|
51
|
+
// Build the row-iteration JSX that replaces the sentinel. The
|
|
52
|
+
// skeleton's useMemo produces `filtered: Model[]`, so the map
|
|
53
|
+
// expression binds over that. onSelect is a prop the skeleton
|
|
54
|
+
// declares. Delete action wires to the deleteItem mutation.
|
|
55
|
+
const rowMap = buildRowMap(columns);
|
|
56
|
+
|
|
57
|
+
if (!shellJsx.includes(TBODY_SENTINEL)) {
|
|
58
|
+
// Defensive: catches adapter output changes that no longer flow
|
|
59
|
+
// `children` into the tbody. Caught at composer time, not runtime.
|
|
60
|
+
throw new Error(
|
|
61
|
+
'composeListBody: tbody sentinel not present in adapter output. ' +
|
|
62
|
+
'The canonical Tailwind adapter may have changed its table rendering.'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return shellJsx.replace(TBODY_SENTINEL, rowMap);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Select the non-metadata attributes of a model. Delegates to the
|
|
71
|
+
* canonical pattern library so the column set is identical to what
|
|
72
|
+
* the runtime React adapter picks for the same model — parity test P3
|
|
73
|
+
* relies on this being one-and-the-same function.
|
|
74
|
+
*/
|
|
75
|
+
function inferColumns(context: EmitContext): string[] {
|
|
76
|
+
return inferFieldsFromSchema(context.modelSchemas, context.model.name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Convert a camelCase attribute name to Title-Case words for display
|
|
81
|
+
* as a column header. Matches the convention used by runtime's
|
|
82
|
+
* pattern adapter (see `humanize` in runtime react-pattern-adapter).
|
|
83
|
+
*/
|
|
84
|
+
function humanize(name: string): string {
|
|
85
|
+
return name
|
|
86
|
+
.replace(/([A-Z])/g, ' $1')
|
|
87
|
+
.replace(/^./, c => c.toUpperCase())
|
|
88
|
+
.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Emit the `{filtered.map(...)}` JSX expression that renders each
|
|
93
|
+
* row. Keep the output readable so users inspecting the generated
|
|
94
|
+
* file can follow what's happening.
|
|
95
|
+
*/
|
|
96
|
+
function buildRowMap(columns: string[]): string {
|
|
97
|
+
// Type-cast to `any` to avoid TS generic syntax (`<string, unknown>`)
|
|
98
|
+
// inside JSX expressions, which the TSX parser can't always
|
|
99
|
+
// disambiguate from a JSX opening tag. `any` is the right choice for
|
|
100
|
+
// starter-kit output anyway — the user will often reshape the row
|
|
101
|
+
// type after editing.
|
|
102
|
+
const cells = columns
|
|
103
|
+
.map(col =>
|
|
104
|
+
` <td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">` +
|
|
105
|
+
`{String((item as any).${col} ?? '')}</td>`
|
|
106
|
+
)
|
|
107
|
+
.join('\n');
|
|
108
|
+
|
|
109
|
+
return [
|
|
110
|
+
'{filtered.map((item, idx) => (',
|
|
111
|
+
' <tr',
|
|
112
|
+
' key={idx}',
|
|
113
|
+
' onClick={() => onSelect?.(item)}',
|
|
114
|
+
' className="hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"',
|
|
115
|
+
' >',
|
|
116
|
+
cells,
|
|
117
|
+
' </tr>',
|
|
118
|
+
'))}',
|
|
119
|
+
].join('\n');
|
|
120
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory B top-level orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Single entry point the realize engine calls. Returns a map of
|
|
5
|
+
* `relativePath → content` for every file the factory decides to
|
|
6
|
+
* write, after applying regeneration-safety triage. Realize handles
|
|
7
|
+
* the actual filesystem writes via its normal multi-file generator
|
|
8
|
+
* pipeline.
|
|
9
|
+
*
|
|
10
|
+
* Skipped (user-edited) files are NOT in the returned map — realize
|
|
11
|
+
* never sees them, so user edits are safe. The hash manifest is
|
|
12
|
+
* emitted as one of the returned files (`.specverse-gen/hashes.json`)
|
|
13
|
+
* so it rolls through the same write pipeline.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { generate as generateViews } from './views-generator.js';
|
|
17
|
+
import { generate as generateAppTsx } from './app-tsx-generator.js';
|
|
18
|
+
import { generate as generatePackageJson } from './package-json-generator.js';
|
|
19
|
+
import {
|
|
20
|
+
loadHashManifest,
|
|
21
|
+
reconcileWrites,
|
|
22
|
+
summarize,
|
|
23
|
+
HASHES_DIR,
|
|
24
|
+
HASHES_FILE,
|
|
25
|
+
} from './regen-safety.js';
|
|
26
|
+
import type { ExpandedSpec } from './views-generator.js';
|
|
27
|
+
|
|
28
|
+
export interface OrchestratorContext {
|
|
29
|
+
/** Expanded spec (post-inference). */
|
|
30
|
+
spec: ExpandedSpec & { metadata?: { name?: string }; name?: string };
|
|
31
|
+
/** Resolved manifest configuration. */
|
|
32
|
+
manifest?: unknown;
|
|
33
|
+
/** Absolute path to the project root (the frontend directory for this factory). */
|
|
34
|
+
projectRoot: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The shape realize expects from a multi-file generator: a map of
|
|
39
|
+
* relative paths to their contents. Realize creates parent dirs and
|
|
40
|
+
* writes the files verbatim.
|
|
41
|
+
*
|
|
42
|
+
* Skipped (user-edited) files are intentionally absent — realize
|
|
43
|
+
* never sees them.
|
|
44
|
+
*/
|
|
45
|
+
export type OrchestratorOutput = Record<string, string>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Produce the complete file set for a Factory B run. Realize writes
|
|
49
|
+
* whatever comes back; the orchestrator is the sole source of truth
|
|
50
|
+
* for "what ends up on disk."
|
|
51
|
+
*/
|
|
52
|
+
export async function generate(context: OrchestratorContext): Promise<OrchestratorOutput> {
|
|
53
|
+
const { spec, projectRoot } = context;
|
|
54
|
+
|
|
55
|
+
// 1. Produce all file contents (pure composition — no I/O yet).
|
|
56
|
+
const proposed: Record<string, string> = {};
|
|
57
|
+
|
|
58
|
+
const viewFiles = await generateViews({ spec });
|
|
59
|
+
Object.assign(proposed, viewFiles);
|
|
60
|
+
|
|
61
|
+
proposed['src/App.tsx'] = await generateAppTsx({ spec });
|
|
62
|
+
proposed['package.json'] = await generatePackageJson({ spec });
|
|
63
|
+
|
|
64
|
+
// 2. Load prior hash manifest + triage. Read-only.
|
|
65
|
+
const prevManifest = loadHashManifest(projectRoot);
|
|
66
|
+
const result = reconcileWrites(projectRoot, proposed, prevManifest);
|
|
67
|
+
|
|
68
|
+
// 3. Log the summary for operator visibility.
|
|
69
|
+
console.log(summarize(result, projectRoot));
|
|
70
|
+
|
|
71
|
+
// 4. Emit the hash manifest as one of the written files so it flows
|
|
72
|
+
// through realize's normal write pipeline.
|
|
73
|
+
const manifestPath = `${HASHES_DIR}/${HASHES_FILE}`;
|
|
74
|
+
const out: OrchestratorOutput = {
|
|
75
|
+
...result.approvedWrites,
|
|
76
|
+
[manifestPath]: JSON.stringify(result.manifest, null, 2) + '\n',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return out;
|
|
80
|
+
}
|
package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package.json generator for ReactAppStarter
|
|
3
|
+
*
|
|
4
|
+
* Deliberately does NOT include @specverse/runtime. The promise of
|
|
5
|
+
* Factory B is that the generated project is standalone — everything
|
|
6
|
+
* needed to render is emitted as local source.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface PackageJsonGeneratorContext {
|
|
10
|
+
spec: {
|
|
11
|
+
metadata?: { name?: string };
|
|
12
|
+
name?: string;
|
|
13
|
+
};
|
|
14
|
+
manifest?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function generate(context: PackageJsonGeneratorContext): Promise<string> {
|
|
18
|
+
const appName = context.spec?.metadata?.name ?? context.spec?.name ?? 'specverse-starter-app';
|
|
19
|
+
const packageName = slugify(appName);
|
|
20
|
+
|
|
21
|
+
const pkg = {
|
|
22
|
+
name: packageName,
|
|
23
|
+
private: true,
|
|
24
|
+
version: '0.1.0',
|
|
25
|
+
type: 'module',
|
|
26
|
+
scripts: {
|
|
27
|
+
dev: 'vite',
|
|
28
|
+
build: 'tsc && vite build',
|
|
29
|
+
preview: 'vite preview',
|
|
30
|
+
typecheck: 'tsc --noEmit',
|
|
31
|
+
},
|
|
32
|
+
dependencies: {
|
|
33
|
+
react: '^18.2.0',
|
|
34
|
+
'react-dom': '^18.2.0',
|
|
35
|
+
'@tanstack/react-query': '^5.0.0',
|
|
36
|
+
},
|
|
37
|
+
devDependencies: {
|
|
38
|
+
'@types/react': '^18.2.0',
|
|
39
|
+
'@types/react-dom': '^18.2.0',
|
|
40
|
+
'@vitejs/plugin-react': '^4.2.0',
|
|
41
|
+
autoprefixer: '^10.4.20',
|
|
42
|
+
postcss: '^8.4.47',
|
|
43
|
+
tailwindcss: '^3.4.13',
|
|
44
|
+
typescript: '^5.2.0',
|
|
45
|
+
vite: '^6.0.0',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return JSON.stringify(pkg, null, 2) + '\n';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function slugify(s: string): string {
|
|
53
|
+
return s
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
56
|
+
.replace(/^-+|-+$/g, '') || 'specverse-starter-app';
|
|
57
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regeneration safety for ReactAppStarter
|
|
3
|
+
*
|
|
4
|
+
* When realize runs Factory B, generated files can end up in two states:
|
|
5
|
+
*
|
|
6
|
+
* 1. Pristine — the file on disk is byte-identical to what we last
|
|
7
|
+
* wrote. Safe to overwrite with a new version.
|
|
8
|
+
*
|
|
9
|
+
* 2. User-edited — the user modified the file. We must NOT clobber
|
|
10
|
+
* their work.
|
|
11
|
+
*
|
|
12
|
+
* The mechanism is an SHA-256 hash recorded per file in
|
|
13
|
+
* `.specverse-gen/hashes.json` at write time. At regeneration, we
|
|
14
|
+
* compare the on-disk hash against the recorded one. Mismatch → skip
|
|
15
|
+
* with a warning. Match → overwrite and update the hash.
|
|
16
|
+
*
|
|
17
|
+
* The factory orchestrator wraps realize's write calls through
|
|
18
|
+
* `reconcileWrites` below, which handles all three outcomes:
|
|
19
|
+
* - brand-new file → write, record hash
|
|
20
|
+
* - pristine existing → overwrite, update hash
|
|
21
|
+
* - user-edited existing → skip, leave hash unchanged
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createHash } from 'crypto';
|
|
25
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
26
|
+
import { join, relative } from 'path';
|
|
27
|
+
|
|
28
|
+
/** Relative path (from project root) → SHA-256 hex digest. */
|
|
29
|
+
export type HashManifest = Record<string, string>;
|
|
30
|
+
|
|
31
|
+
export const HASHES_DIR = '.specverse-gen';
|
|
32
|
+
export const HASHES_FILE = 'hashes.json';
|
|
33
|
+
|
|
34
|
+
export function sha256(s: string): string {
|
|
35
|
+
return createHash('sha256').update(s, 'utf8').digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load a `.specverse-gen/hashes.json` from the project root. Returns
|
|
40
|
+
* an empty manifest if the file is missing or malformed — the
|
|
41
|
+
* calling convention is that a missing manifest means "no record of
|
|
42
|
+
* prior generation, treat everything as new."
|
|
43
|
+
*/
|
|
44
|
+
export function loadHashManifest(projectRoot: string): HashManifest {
|
|
45
|
+
const path = join(projectRoot, HASHES_DIR, HASHES_FILE);
|
|
46
|
+
if (!existsSync(path)) return {};
|
|
47
|
+
try {
|
|
48
|
+
const raw = readFileSync(path, 'utf8');
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
51
|
+
const out: HashManifest = {};
|
|
52
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
53
|
+
if (typeof v === 'string') out[k] = v;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Malformed — treat as missing. Next generate pass will overwrite.
|
|
59
|
+
}
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Write the manifest back to `.specverse-gen/hashes.json`. */
|
|
64
|
+
export function saveHashManifest(projectRoot: string, manifest: HashManifest): void {
|
|
65
|
+
const dir = join(projectRoot, HASHES_DIR);
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
const path = join(dir, HASHES_FILE);
|
|
68
|
+
writeFileSync(path, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ReconcileResult {
|
|
72
|
+
/** Relative path → content for every file approved for writing. */
|
|
73
|
+
approvedWrites: Record<string, string>;
|
|
74
|
+
/** Files we skipped because the user edited them (or couldn't confirm origin). */
|
|
75
|
+
skipped: { path: string; reason: string }[];
|
|
76
|
+
/** The updated hash manifest reflecting all approved writes. */
|
|
77
|
+
manifest: HashManifest;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Pure-planning triage. For each proposed file, decides whether the
|
|
82
|
+
* write is safe:
|
|
83
|
+
* - Path doesn't exist → APPROVE, record hash.
|
|
84
|
+
* - Path exists + on-disk hash matches recorded hash → APPROVE
|
|
85
|
+
* (pristine overwrite), update hash.
|
|
86
|
+
* - Path exists + hash mismatch → SKIP (user-edited), preserve old
|
|
87
|
+
* hash record.
|
|
88
|
+
* - Path exists + no recorded hash → SKIP (cautious default).
|
|
89
|
+
*
|
|
90
|
+
* Reads the filesystem but doesn't write. Returns the approved writes
|
|
91
|
+
* as a map the caller can pass to the realize write pipeline.
|
|
92
|
+
*/
|
|
93
|
+
export function reconcileWrites(
|
|
94
|
+
projectRoot: string,
|
|
95
|
+
proposed: Record<string, string>,
|
|
96
|
+
prevManifest: HashManifest
|
|
97
|
+
): ReconcileResult {
|
|
98
|
+
const manifest: HashManifest = { ...prevManifest };
|
|
99
|
+
const approvedWrites: Record<string, string> = {};
|
|
100
|
+
const skipped: { path: string; reason: string }[] = [];
|
|
101
|
+
|
|
102
|
+
for (const [relPath, content] of Object.entries(proposed)) {
|
|
103
|
+
const abs = join(projectRoot, relPath);
|
|
104
|
+
const newHash = sha256(content);
|
|
105
|
+
|
|
106
|
+
if (!existsSync(abs)) {
|
|
107
|
+
approvedWrites[relPath] = content;
|
|
108
|
+
manifest[relPath] = newHash;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const currentContent = readFileSync(abs, 'utf8');
|
|
113
|
+
const currentHash = sha256(currentContent);
|
|
114
|
+
const recordedHash = prevManifest[relPath];
|
|
115
|
+
|
|
116
|
+
if (recordedHash == null) {
|
|
117
|
+
skipped.push({
|
|
118
|
+
path: relPath,
|
|
119
|
+
reason: 'no prior hash recorded — cannot confirm this file was generated by us',
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (currentHash === recordedHash) {
|
|
125
|
+
approvedWrites[relPath] = content;
|
|
126
|
+
manifest[relPath] = newHash;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
skipped.push({
|
|
131
|
+
path: relPath,
|
|
132
|
+
reason: 'file has been edited since last generation',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { approvedWrites, skipped, manifest };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Human-readable summary of a ReconcileResult. Used by the factory
|
|
141
|
+
* orchestrator to log after a run.
|
|
142
|
+
*/
|
|
143
|
+
export function summarize(result: ReconcileResult, projectRoot: string): string {
|
|
144
|
+
const lines: string[] = [];
|
|
145
|
+
const writeCount = Object.keys(result.approvedWrites).length;
|
|
146
|
+
lines.push(`[ReactAppStarter] Approved ${writeCount} file(s) for writing.`);
|
|
147
|
+
if (result.skipped.length > 0) {
|
|
148
|
+
lines.push(`[ReactAppStarter] Skipped ${result.skipped.length} user-edited file(s):`);
|
|
149
|
+
for (const { path, reason } of result.skipped) {
|
|
150
|
+
lines.push(` - ${path} (${reason})`);
|
|
151
|
+
}
|
|
152
|
+
lines.push(
|
|
153
|
+
`To accept upstream regeneration for a skipped file, delete it (\`rm ${relative(process.cwd(), projectRoot)}/PATH\`) and re-run \`spv realize\`.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return lines.join('\n');
|
|
157
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{MODEL_NAME}}DashboardView — generated by @specverse/realize (ReactAppStarter)
|
|
3
|
+
*
|
|
4
|
+
* Safe to edit. Edits are preserved across regeneration via content
|
|
5
|
+
* hashing (see .specverse-gen/hashes.json). To accept an upstream
|
|
6
|
+
* regeneration of this file, delete it first, then run `spv realize`.
|
|
7
|
+
*
|
|
8
|
+
* A minimal dashboard: summary counts derived from the list query,
|
|
9
|
+
* plus a compact preview of recent records. Charts and aggregation
|
|
10
|
+
* metrics are deferred — add them as the backend grows suitable
|
|
11
|
+
* endpoints.
|
|
12
|
+
*/
|
|
13
|
+
import { useMemo } from 'react';
|
|
14
|
+
import { use{{PLURAL_MODEL}}Query } from '../hooks/useApi';
|
|
15
|
+
import type { {{MODEL_NAME}} } from '../types/api';
|
|
16
|
+
|
|
17
|
+
interface {{MODEL_NAME}}DashboardViewProps {
|
|
18
|
+
/** Number of recent records to preview. Default 5. */
|
|
19
|
+
previewLimit?: number;
|
|
20
|
+
onSelect?: (item: {{MODEL_NAME}}) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function {{MODEL_NAME}}DashboardView({
|
|
24
|
+
previewLimit = 5,
|
|
25
|
+
onSelect,
|
|
26
|
+
}: {{MODEL_NAME}}DashboardViewProps) {
|
|
27
|
+
const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
|
|
28
|
+
|
|
29
|
+
const preview = useMemo(
|
|
30
|
+
() => items.slice(0, previewLimit),
|
|
31
|
+
[items, previewLimit]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (isLoading) return <div className="p-4 text-gray-500">Loading dashboard…</div>;
|
|
35
|
+
if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="p-6 space-y-6">
|
|
39
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
40
|
+
{{MODEL_NAME}} dashboard
|
|
41
|
+
</h2>
|
|
42
|
+
|
|
43
|
+
{/* Pattern-rendered dashboard body (metrics + preview). Edit freely. */}
|
|
44
|
+
{{BODY}}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{MODEL_NAME}}DetailView — generated by @specverse/realize (ReactAppStarter)
|
|
3
|
+
*
|
|
4
|
+
* Safe to edit. Edits are preserved across regeneration via content
|
|
5
|
+
* hashing (see .specverse-gen/hashes.json). To accept an upstream
|
|
6
|
+
* regeneration of this file, delete it first, then run `spv realize`.
|
|
7
|
+
*/
|
|
8
|
+
import { use{{PLURAL_MODEL}}Query, useDelete{{MODEL_NAME}}Mutation } from '../hooks/useApi';
|
|
9
|
+
import type { {{MODEL_NAME}} } from '../types/api';
|
|
10
|
+
|
|
11
|
+
interface {{MODEL_NAME}}DetailViewProps {
|
|
12
|
+
entityId: string | number;
|
|
13
|
+
onEdit?: (item: {{MODEL_NAME}}) => void;
|
|
14
|
+
onBack?: () => void;
|
|
15
|
+
/** Called after the delete mutation succeeds. */
|
|
16
|
+
onDeleted?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function {{MODEL_NAME}}DetailView({
|
|
20
|
+
entityId,
|
|
21
|
+
onEdit,
|
|
22
|
+
onBack,
|
|
23
|
+
onDeleted,
|
|
24
|
+
}: {{MODEL_NAME}}DetailViewProps) {
|
|
25
|
+
const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
|
|
26
|
+
const deleteItem = useDelete{{MODEL_NAME}}Mutation();
|
|
27
|
+
|
|
28
|
+
const item = items.find(
|
|
29
|
+
(x: {{MODEL_NAME}}) => (x as any).id === entityId
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (isLoading) return <div className="p-4 text-gray-500">Loading…</div>;
|
|
33
|
+
if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;
|
|
34
|
+
if (!item) return <div className="p-4 text-gray-400">No {{SINGULAR_LOWER}} matching id {String(entityId)}.</div>;
|
|
35
|
+
|
|
36
|
+
const handleDelete = async () => {
|
|
37
|
+
if (!confirm('Delete this {{SINGULAR_LOWER}}?')) return;
|
|
38
|
+
try {
|
|
39
|
+
await deleteItem.mutateAsync((item as any).id);
|
|
40
|
+
onDeleted?.();
|
|
41
|
+
} catch {
|
|
42
|
+
// deleteItem.error is surfaced below
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="p-6 space-y-4">
|
|
48
|
+
<div className="flex items-center justify-between">
|
|
49
|
+
<div className="flex items-center gap-3">
|
|
50
|
+
{onBack && (
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={onBack}
|
|
54
|
+
className="text-sm text-gray-500 hover:text-gray-700"
|
|
55
|
+
>
|
|
56
|
+
← Back
|
|
57
|
+
</button>
|
|
58
|
+
)}
|
|
59
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
60
|
+
{{MODEL_NAME}} detail
|
|
61
|
+
</h2>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="flex gap-2">
|
|
64
|
+
{onEdit && (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={() => onEdit(item)}
|
|
68
|
+
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
69
|
+
>
|
|
70
|
+
Edit
|
|
71
|
+
</button>
|
|
72
|
+
)}
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={handleDelete}
|
|
76
|
+
disabled={deleteItem.isPending}
|
|
77
|
+
className="rounded bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-60"
|
|
78
|
+
>
|
|
79
|
+
{deleteItem.isPending ? 'Deleting…' : 'Delete'}
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Pattern-rendered detail body (fields). Edit freely. */}
|
|
85
|
+
{{BODY}}
|
|
86
|
+
|
|
87
|
+
{deleteItem.isError && (
|
|
88
|
+
<div className="p-2 text-sm text-red-600">
|
|
89
|
+
Delete failed: {String(deleteItem.error)}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{MODEL_NAME}}FormView — generated by @specverse/realize (ReactAppStarter)
|
|
3
|
+
*
|
|
4
|
+
* Safe to edit. Edits are preserved across regeneration via content
|
|
5
|
+
* hashing (see .specverse-gen/hashes.json). To accept an upstream
|
|
6
|
+
* regeneration of this file, delete it first, then run `spv realize`.
|
|
7
|
+
*
|
|
8
|
+
* Controlled form. One field per non-auto-generated attribute.
|
|
9
|
+
* Mode: 'create' (default) or 'update' — passed as a prop.
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useState } from 'react';
|
|
12
|
+
import {
|
|
13
|
+
use{{PLURAL_MODEL}}Query,
|
|
14
|
+
useCreate{{MODEL_NAME}}Mutation,
|
|
15
|
+
useUpdate{{MODEL_NAME}}Mutation,
|
|
16
|
+
} from '../hooks/useApi';
|
|
17
|
+
import type { {{MODEL_NAME}} } from '../types/api';
|
|
18
|
+
|
|
19
|
+
type FormMode = 'create' | 'update';
|
|
20
|
+
|
|
21
|
+
interface {{MODEL_NAME}}FormViewProps {
|
|
22
|
+
mode?: FormMode;
|
|
23
|
+
/** Required in update mode. */
|
|
24
|
+
entityId?: string | number;
|
|
25
|
+
onSuccess?: (item: {{MODEL_NAME}}) => void;
|
|
26
|
+
onCancel?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function {{MODEL_NAME}}FormView({
|
|
30
|
+
mode = 'create',
|
|
31
|
+
entityId,
|
|
32
|
+
onSuccess,
|
|
33
|
+
onCancel,
|
|
34
|
+
}: {{MODEL_NAME}}FormViewProps) {
|
|
35
|
+
const { data: items = [] } = use{{PLURAL_MODEL}}Query();
|
|
36
|
+
const createItem = useCreate{{MODEL_NAME}}Mutation();
|
|
37
|
+
const updateItem = useUpdate{{MODEL_NAME}}Mutation();
|
|
38
|
+
|
|
39
|
+
const existing =
|
|
40
|
+
mode === 'update'
|
|
41
|
+
? items.find((x: {{MODEL_NAME}}) => (x as any).id === entityId)
|
|
42
|
+
: undefined;
|
|
43
|
+
|
|
44
|
+
const [formData, setFormData] = useState<Partial<{{MODEL_NAME}}>>(existing ?? {});
|
|
45
|
+
|
|
46
|
+
// When the fetched list lands after initial render (update mode),
|
|
47
|
+
// hydrate the form with the loaded entity's data.
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (existing) setFormData(existing);
|
|
50
|
+
}, [existing]);
|
|
51
|
+
|
|
52
|
+
const handleChange = (field: string, value: unknown) => {
|
|
53
|
+
setFormData(prev => ({ ...prev, [field]: value }) as Partial<{{MODEL_NAME}}>);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleSubmit = async (event: React.FormEvent) => {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
try {
|
|
59
|
+
const result =
|
|
60
|
+
mode === 'create'
|
|
61
|
+
? await createItem.mutateAsync(formData as {{MODEL_NAME}})
|
|
62
|
+
: await updateItem.mutateAsync({
|
|
63
|
+
id: entityId as any,
|
|
64
|
+
data: formData as Partial<{{MODEL_NAME}}>,
|
|
65
|
+
});
|
|
66
|
+
onSuccess?.(result as {{MODEL_NAME}});
|
|
67
|
+
} catch {
|
|
68
|
+
// Mutation errors are surfaced below via createItem / updateItem.
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const mutation = mode === 'create' ? createItem : updateItem;
|
|
73
|
+
const submitLabel =
|
|
74
|
+
mode === 'create'
|
|
75
|
+
? mutation.isPending ? 'Creating…' : 'Create {{MODEL_NAME}}'
|
|
76
|
+
: mutation.isPending ? 'Updating…' : 'Update {{MODEL_NAME}}';
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
|
80
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
81
|
+
{mode === 'create' ? 'New {{MODEL_NAME}}' : 'Edit {{MODEL_NAME}}'}
|
|
82
|
+
</h2>
|
|
83
|
+
|
|
84
|
+
{/* Pattern-rendered form fields. Edit freely. */}
|
|
85
|
+
{{BODY}}
|
|
86
|
+
|
|
87
|
+
<div className="flex gap-2 border-t border-gray-200 dark:border-gray-700 pt-4">
|
|
88
|
+
<button
|
|
89
|
+
type="submit"
|
|
90
|
+
disabled={mutation.isPending}
|
|
91
|
+
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-60"
|
|
92
|
+
>
|
|
93
|
+
{submitLabel}
|
|
94
|
+
</button>
|
|
95
|
+
{onCancel && (
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={onCancel}
|
|
99
|
+
className="rounded border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-800"
|
|
100
|
+
>
|
|
101
|
+
Cancel
|
|
102
|
+
</button>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{mutation.isError && (
|
|
107
|
+
<div className="rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300">
|
|
108
|
+
{mode === 'create' ? 'Create failed: ' : 'Update failed: '}
|
|
109
|
+
{String(mutation.error)}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</form>
|
|
113
|
+
);
|
|
114
|
+
}
|