@topogram/cli 0.3.48 → 0.3.50
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/CHANGELOG.md +9 -8
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/generator/context/slice.js +131 -0
- package/src/generator/registry.js +6 -3
- package/src/generator/runtime/app-bundle.js +8 -7
- package/src/generator/runtime/compile-check.js +3 -2
- package/src/generator/runtime/environment.js +8 -7
- package/src/generator/runtime/runtime-check.js +5 -5
- package/src/generator/runtime/shared.js +9 -0
- package/src/generator/runtime/smoke.js +2 -1
- package/src/generator/surfaces/shared.js +2 -1
- package/src/generator/surfaces/web/design-intent.js +308 -0
- package/src/generator/surfaces/web/react-components.js +3 -7
- package/src/generator/surfaces/web/react.js +35 -28
- package/src/generator/surfaces/web/sveltekit-components.js +3 -7
- package/src/generator/surfaces/web/sveltekit.js +28 -23
- package/src/generator/surfaces/web/vanilla.js +102 -36
- package/src/import/core/runner.js +63 -20
- package/src/realization/ui/build-ui-shared-realization.js +48 -16
- package/src/realization/ui/build-web-realization.js +5 -38
- package/src/resolver/index.js +13 -0
- package/src/ui/taxonomy.js +201 -0
- package/src/validator/index.js +124 -4
- package/src/validator/kinds.js +21 -52
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { buildWebRealization } from "../../../realization/ui/index.js";
|
|
4
|
+
import { buildDesignIntentCoverage, renderDesignIntentCss } from "./design-intent.js";
|
|
4
5
|
|
|
5
6
|
function slugify(value) {
|
|
6
7
|
return String(value || "page")
|
|
@@ -9,14 +10,8 @@ function slugify(value) {
|
|
|
9
10
|
.replace(/^-+|-+$/g, "") || "page";
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
function titleForScreen(
|
|
13
|
-
|
|
14
|
-
const screen = (projection.uiScreens || []).find((entry) => entry.id === screenId);
|
|
15
|
-
if (screen?.title) {
|
|
16
|
-
return screen.title;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return screenId
|
|
13
|
+
function titleForScreen(screenId) {
|
|
14
|
+
return String(screenId || "page")
|
|
20
15
|
.split(/[_\-\s]+/)
|
|
21
16
|
.filter(Boolean)
|
|
22
17
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
@@ -55,10 +50,12 @@ ${body}
|
|
|
55
50
|
`;
|
|
56
51
|
}
|
|
57
52
|
|
|
58
|
-
function renderStyles() {
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
function renderStyles(design) {
|
|
54
|
+
return `${renderDesignIntentCss(design)}
|
|
55
|
+
|
|
56
|
+
:root {
|
|
57
|
+
color: var(--topogram-text-color);
|
|
58
|
+
background: var(--topogram-surface-background);
|
|
62
59
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
63
60
|
}
|
|
64
61
|
|
|
@@ -70,14 +67,14 @@ body {
|
|
|
70
67
|
display: flex;
|
|
71
68
|
align-items: center;
|
|
72
69
|
justify-content: space-between;
|
|
73
|
-
gap:
|
|
70
|
+
gap: var(--topogram-space-unit);
|
|
74
71
|
padding: 1rem 1.25rem;
|
|
75
|
-
border-bottom: 1px solid
|
|
76
|
-
background:
|
|
72
|
+
border-bottom: 1px solid var(--topogram-border-color);
|
|
73
|
+
background: var(--topogram-surface-card);
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
.brand {
|
|
80
|
-
color:
|
|
77
|
+
color: var(--topogram-text-color);
|
|
81
78
|
font-weight: 700;
|
|
82
79
|
text-decoration: none;
|
|
83
80
|
}
|
|
@@ -89,27 +86,32 @@ nav {
|
|
|
89
86
|
}
|
|
90
87
|
|
|
91
88
|
nav a {
|
|
92
|
-
color:
|
|
89
|
+
color: var(--topogram-action-primary-background);
|
|
93
90
|
text-decoration: none;
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
main {
|
|
97
94
|
display: grid;
|
|
98
|
-
gap:
|
|
95
|
+
gap: var(--topogram-space-unit);
|
|
99
96
|
max-width: 56rem;
|
|
100
97
|
margin: 0 auto;
|
|
101
|
-
padding:
|
|
98
|
+
padding: var(--topogram-page-padding);
|
|
102
99
|
}
|
|
103
100
|
|
|
104
101
|
.panel {
|
|
105
|
-
border: 1px solid
|
|
106
|
-
border-radius:
|
|
107
|
-
background:
|
|
102
|
+
border: 1px solid var(--topogram-border-color);
|
|
103
|
+
border-radius: var(--topogram-radius-card);
|
|
104
|
+
background: var(--topogram-surface-card);
|
|
108
105
|
padding: 1.25rem;
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
.muted {
|
|
112
|
-
color:
|
|
109
|
+
color: var(--topogram-muted-color);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
a:focus-visible {
|
|
113
|
+
outline: var(--topogram-focus-outline);
|
|
114
|
+
outline-offset: 2px;
|
|
113
115
|
}
|
|
114
116
|
`;
|
|
115
117
|
}
|
|
@@ -158,6 +160,67 @@ console.log(\`Checked \${htmlFiles.length} vanilla page(s).\`);
|
|
|
158
160
|
`;
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
function buildVanillaGenerationCoverage(contract, files, routes) {
|
|
164
|
+
const diagnostics = [];
|
|
165
|
+
const designIntent = buildDesignIntentCoverage(contract, files, "styles.css");
|
|
166
|
+
diagnostics.push(...designIntent.diagnostics);
|
|
167
|
+
const screens = routes.map((route) => {
|
|
168
|
+
const contents = files[route.file] || "";
|
|
169
|
+
const rendered = Boolean(contents);
|
|
170
|
+
if (!rendered) {
|
|
171
|
+
diagnostics.push({
|
|
172
|
+
code: "screen_route_not_rendered",
|
|
173
|
+
severity: "error",
|
|
174
|
+
screen: route.screenId,
|
|
175
|
+
route: route.path,
|
|
176
|
+
message: `Screen '${route.screenId}' has route '${route.path}' but no vanilla HTML page was generated.`,
|
|
177
|
+
suggested_fix: "Check the vanilla web generator route emission for this screen."
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
id: route.screenId,
|
|
182
|
+
route: route.path,
|
|
183
|
+
page: route.file,
|
|
184
|
+
rendered,
|
|
185
|
+
renderer: rendered ? "generator" : "missing",
|
|
186
|
+
component_usages: []
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
type: "generation_coverage",
|
|
191
|
+
surface: "web",
|
|
192
|
+
generator: "topogram/vanilla-web",
|
|
193
|
+
projection: {
|
|
194
|
+
id: contract.projection.id,
|
|
195
|
+
name: contract.projection.name,
|
|
196
|
+
platform: contract.projection.platform
|
|
197
|
+
},
|
|
198
|
+
summary: {
|
|
199
|
+
routed_screens: screens.length,
|
|
200
|
+
rendered_screens: screens.filter((screen) => screen.rendered).length,
|
|
201
|
+
implementation_screens: 0,
|
|
202
|
+
generator_screens: screens.filter((screen) => screen.renderer === "generator").length,
|
|
203
|
+
component_usages: 0,
|
|
204
|
+
rendered_component_usages: 0,
|
|
205
|
+
diagnostics: diagnostics.length,
|
|
206
|
+
errors: diagnostics.filter((diagnostic) => diagnostic.severity === "error").length,
|
|
207
|
+
warnings: diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length
|
|
208
|
+
},
|
|
209
|
+
design_intent: designIntent.coverage,
|
|
210
|
+
screens,
|
|
211
|
+
diagnostics
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function assertGenerationCoverage(coverage) {
|
|
216
|
+
const errors = (coverage.diagnostics || []).filter((diagnostic) => diagnostic.severity === "error");
|
|
217
|
+
if (errors.length === 0) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const details = errors.map((diagnostic) => diagnostic.message).join("; ");
|
|
221
|
+
throw new Error(`Vanilla web generation coverage failed: ${details}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
161
224
|
function renderDevScript() {
|
|
162
225
|
return `import http from "node:http";
|
|
163
226
|
import fs from "node:fs";
|
|
@@ -192,20 +255,19 @@ http.createServer((req, res) => {
|
|
|
192
255
|
}
|
|
193
256
|
|
|
194
257
|
export function generateVanillaWebApp(graph, options = {}) {
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
file: routeFileName(route.path)
|
|
258
|
+
const realization = buildWebRealization(graph, options);
|
|
259
|
+
const contract = realization.contract;
|
|
260
|
+
const routeScreens = (contract.screens || []).filter((screen) => Boolean(screen.route));
|
|
261
|
+
const routes = (routeScreens.length > 0 ? routeScreens : [{ id: "home", route: "/", title: "Home" }]).map((screen) => ({
|
|
262
|
+
screenId: screen.id,
|
|
263
|
+
path: screen.route || "/",
|
|
264
|
+
title: screen.title || titleForScreen(screen.id),
|
|
265
|
+
file: routeFileName(screen.route || "/")
|
|
204
266
|
}));
|
|
205
267
|
const nav = routes.map(({ title, file }) => ({ title, file }));
|
|
206
268
|
const files = {
|
|
207
269
|
"package.json": `${JSON.stringify({
|
|
208
|
-
name: projection.id,
|
|
270
|
+
name: contract.projection.id,
|
|
209
271
|
private: true,
|
|
210
272
|
version: "0.1.0",
|
|
211
273
|
type: "module",
|
|
@@ -215,7 +277,7 @@ export function generateVanillaWebApp(graph, options = {}) {
|
|
|
215
277
|
check: "node ./scripts/check.mjs"
|
|
216
278
|
}
|
|
217
279
|
}, null, 2)}\n`,
|
|
218
|
-
"styles.css": renderStyles(),
|
|
280
|
+
"styles.css": renderStyles(contract.design),
|
|
219
281
|
"app.js": renderBrowserScript(),
|
|
220
282
|
"scripts/build.mjs": renderBuildScript(),
|
|
221
283
|
"scripts/check.mjs": renderCheckScript(),
|
|
@@ -229,11 +291,15 @@ export function generateVanillaWebApp(graph, options = {}) {
|
|
|
229
291
|
body: ` <section class="panel">
|
|
230
292
|
<p class="muted">Page ${index + 1} of ${routes.length}</p>
|
|
231
293
|
<h1>${route.title}</h1>
|
|
232
|
-
<p>This page was generated from the <code>${projection.id}</code> Topogram web projection.</p>
|
|
294
|
+
<p>This page was generated from the <code>${contract.projection.id}</code> Topogram web projection.</p>
|
|
233
295
|
<p class="muted">Generated timestamp: <span data-generated-at>pending</span></p>
|
|
234
296
|
</section>`
|
|
235
297
|
});
|
|
236
298
|
});
|
|
237
299
|
|
|
300
|
+
const coverage = buildVanillaGenerationCoverage(contract, files, routes);
|
|
301
|
+
assertGenerationCoverage(coverage);
|
|
302
|
+
files["topogram/generation-coverage.json"] = `${JSON.stringify(coverage, null, 2)}\n`;
|
|
303
|
+
files["topogram/ui-web-contract.json"] = `${JSON.stringify(contract, null, 2)}\n`;
|
|
238
304
|
return files;
|
|
239
305
|
}
|
|
@@ -2,6 +2,9 @@ import { IMPORT_TRACKS } from "./contracts.js";
|
|
|
2
2
|
import { dedupeCandidateRecords, ensureTrailingNewline, idHintify, makeCandidateRecord } from "./shared.js";
|
|
3
3
|
import { createImportContext } from "./context.js";
|
|
4
4
|
import { getEnrichersForTrack, getExtractorsForTrack } from "./registry.js";
|
|
5
|
+
import {
|
|
6
|
+
collectionPatternFromPresentations
|
|
7
|
+
} from "../../ui/taxonomy.js";
|
|
5
8
|
|
|
6
9
|
function parseImportTracks(fromValue) {
|
|
7
10
|
if (!fromValue) {
|
|
@@ -95,25 +98,6 @@ function selectDetectionsForTrack(track, detections) {
|
|
|
95
98
|
return detections;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
function collectionPatternFromPresentations(presentations) {
|
|
99
|
-
if (presentations.includes("data_grid")) return "data_grid_view";
|
|
100
|
-
if (presentations.includes("table")) return "resource_table";
|
|
101
|
-
if (presentations.includes("cards")) return "resource_cards";
|
|
102
|
-
if (presentations.includes("board")) return "board_view";
|
|
103
|
-
if (presentations.includes("calendar")) return "calendar_view";
|
|
104
|
-
if (presentations.includes("gallery")) return "resource_cards";
|
|
105
|
-
return "search_results";
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function presentationFromPattern(pattern) {
|
|
109
|
-
if (pattern === "data_grid_view") return "data_grid";
|
|
110
|
-
if (pattern === "resource_table") return "table";
|
|
111
|
-
if (pattern === "resource_cards") return "cards";
|
|
112
|
-
if (pattern === "board_view") return "board";
|
|
113
|
-
if (pattern === "calendar_view") return "calendar";
|
|
114
|
-
return "list";
|
|
115
|
-
}
|
|
116
|
-
|
|
117
101
|
function importedApiCapabilityIds(allCandidates) {
|
|
118
102
|
return [...(allCandidates?.api?.capabilities || [])]
|
|
119
103
|
.map((capability) => capability.id_hint)
|
|
@@ -181,6 +165,17 @@ function deriveUiComponentCandidates(candidates) {
|
|
|
181
165
|
pattern,
|
|
182
166
|
data_prop: "rows",
|
|
183
167
|
data_source: loadCapability,
|
|
168
|
+
inferred_props: [{ name: "rows", type: "array", required: true, source: loadCapability }],
|
|
169
|
+
inferred_events: [],
|
|
170
|
+
inferred_region: "results",
|
|
171
|
+
inferred_pattern: pattern,
|
|
172
|
+
evidence: screen.provenance || [],
|
|
173
|
+
missing_decisions: [
|
|
174
|
+
"confirm component reuse boundary",
|
|
175
|
+
"confirm prop names and data source",
|
|
176
|
+
"confirm events and behavior",
|
|
177
|
+
"confirm supported regions and patterns"
|
|
178
|
+
],
|
|
184
179
|
notes: [
|
|
185
180
|
"Imported component candidates are review-only.",
|
|
186
181
|
"Confirm props, behavior, events, and reuse before adoption."
|
|
@@ -294,8 +289,11 @@ function reportMarkdown(track, candidates) {
|
|
|
294
289
|
);
|
|
295
290
|
}
|
|
296
291
|
if (track === "ui") {
|
|
292
|
+
const componentLines = (candidates.components || []).map((component) =>
|
|
293
|
+
`- \`${component.id_hint}\` confidence ${component.confidence || "unknown"} pattern \`${component.pattern || component.inferred_pattern || "unknown"}\` region \`${component.region || component.inferred_region || "unknown"}\` evidence ${(component.evidence || component.provenance || []).length} missing decisions ${(component.missing_decisions || []).length}`
|
|
294
|
+
);
|
|
297
295
|
return ensureTrailingNewline(
|
|
298
|
-
`# UI Import Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Components: ${candidates.components.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n`
|
|
296
|
+
`# UI Import Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Components: ${candidates.components.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Component Candidates\n\n${componentLines.length ? componentLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review candidates under \`topogram/candidates/app/ui/drafts/components/**\`.\n- Run \`topogram import plan <path>\` before adoption.\n- After adoption, run \`topogram check <path>\`, \`topogram component check <path>\`, and \`topogram component behavior <path>\`.\n`
|
|
299
297
|
);
|
|
300
298
|
}
|
|
301
299
|
if (track === "verification") {
|
|
@@ -327,7 +325,15 @@ function componentCandidateFileName(component) {
|
|
|
327
325
|
}
|
|
328
326
|
|
|
329
327
|
function renderComponentCandidate(component) {
|
|
328
|
+
const evidenceCount = (component.evidence || component.provenance || []).length;
|
|
329
|
+
const missingDecisions = component.missing_decisions || [
|
|
330
|
+
"confirm component reuse boundary",
|
|
331
|
+
"confirm prop names and data source",
|
|
332
|
+
"confirm events and behavior"
|
|
333
|
+
];
|
|
330
334
|
return `component ${component.id_hint} {
|
|
335
|
+
# Import metadata: confidence ${component.confidence || "unknown"}; evidence ${evidenceCount}; inferred pattern ${component.pattern || component.inferred_pattern || "search_results"}; inferred region ${component.region || component.inferred_region || "results"}.
|
|
336
|
+
# Missing decisions: ${missingDecisions.join("; ")}.
|
|
331
337
|
name "${component.label || component.id_hint}"
|
|
332
338
|
description "Candidate reusable component inferred from imported UI evidence. Review props, behavior, events, and reuse before adoption."
|
|
333
339
|
category collection
|
|
@@ -353,6 +359,26 @@ function uiComponentLinesForCandidates(componentCandidates, allCandidates) {
|
|
|
353
359
|
});
|
|
354
360
|
}
|
|
355
361
|
|
|
362
|
+
function enrichUiComponentDataSources(uiCandidates, allCandidates) {
|
|
363
|
+
if (!uiCandidates || !Array.isArray(uiCandidates.components)) {
|
|
364
|
+
return uiCandidates;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
...uiCandidates,
|
|
368
|
+
components: uiCandidates.components.map((component) => {
|
|
369
|
+
const dataSource = inferredDataSourceForComponent(component, allCandidates);
|
|
370
|
+
const dataProp = component.data_prop || "rows";
|
|
371
|
+
return {
|
|
372
|
+
...component,
|
|
373
|
+
data_source: component.data_source || dataSource,
|
|
374
|
+
inferred_props: (component.inferred_props || []).map((prop) =>
|
|
375
|
+
prop.name === dataProp ? { ...prop, source: prop.source || dataSource } : prop
|
|
376
|
+
)
|
|
377
|
+
};
|
|
378
|
+
})
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
356
382
|
function draftUiProjectionFiles(context, candidates, allCandidates = {}) {
|
|
357
383
|
const ui = candidates || { screens: [], routes: [], actions: [], stacks: [] };
|
|
358
384
|
const screens = [...(ui.screens || [])].sort((a, b) => String(a.route_path || "").localeCompare(String(b.route_path || "")) || a.id_hint.localeCompare(b.id_hint));
|
|
@@ -477,6 +503,20 @@ ${capabilityHints.length > 0 ? capabilityHints.map((hint) => ` ${hint}`).join
|
|
|
477
503
|
shell ${shell}
|
|
478
504
|
${presentations.includes("search") ? " global_search true\n" : ""}${presentations.includes("multi_window") ? " windowing multi_window\n" : ""} }
|
|
479
505
|
|
|
506
|
+
ui_design {
|
|
507
|
+
density comfortable
|
|
508
|
+
tone operational
|
|
509
|
+
radius_scale medium
|
|
510
|
+
color_role primary accent
|
|
511
|
+
color_role danger critical
|
|
512
|
+
typography_role body readable
|
|
513
|
+
typography_role heading prominent
|
|
514
|
+
action_role primary prominent
|
|
515
|
+
action_role destructive danger
|
|
516
|
+
accessibility contrast aa
|
|
517
|
+
accessibility focus visible
|
|
518
|
+
}
|
|
519
|
+
|
|
480
520
|
ui_screens {
|
|
481
521
|
${uiScreensBlock}
|
|
482
522
|
}
|
|
@@ -616,6 +656,9 @@ export function runImportApp(inputPath, options = {}) {
|
|
|
616
656
|
}
|
|
617
657
|
|
|
618
658
|
if (candidates.ui) {
|
|
659
|
+
candidates.ui = enrichUiComponentDataSources(candidates.ui, candidates);
|
|
660
|
+
files["candidates/app/ui/candidates.json"] = `${JSON.stringify(candidates.ui, null, 2)}\n`;
|
|
661
|
+
files["candidates/app/ui/report.md"] = reportMarkdown("ui", candidates.ui);
|
|
619
662
|
Object.assign(files, draftUiProjectionFiles(context, candidates.ui, candidates));
|
|
620
663
|
}
|
|
621
664
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getProjection, uiProjectionCandidates } from "../../generator/surfaces/shared.js";
|
|
2
2
|
import { buildComponentBehaviorRealizations } from "../../component-behavior.js";
|
|
3
|
+
import { defaultPatternForScreen } from "../../ui/taxonomy.js";
|
|
3
4
|
|
|
4
5
|
function toBooleanFlag(value, fallback = false) {
|
|
5
6
|
if (value === "true") return true;
|
|
@@ -47,21 +48,6 @@ function ownershipFieldByCapability(graph) {
|
|
|
47
48
|
return output;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function deriveDefaultPattern(screen, collectionEntries) {
|
|
51
|
-
if (screen.kind === "detail") return "detail_panel";
|
|
52
|
-
if (screen.kind === "form") return "edit_form";
|
|
53
|
-
if (screen.kind === "board") return "board_view";
|
|
54
|
-
if (screen.kind === "calendar") return "calendar_view";
|
|
55
|
-
if (screen.kind === "dashboard" || screen.kind === "analytics" || screen.kind === "report") return "summary_stats";
|
|
56
|
-
if (screen.kind === "feed" || screen.kind === "inbox") return "activity_feed";
|
|
57
|
-
const view = collectionEntries.find((entry) => entry.operation === "view")?.value;
|
|
58
|
-
if (view === "data_grid") return "data_grid_view";
|
|
59
|
-
if (view === "table") return "resource_table";
|
|
60
|
-
if (view === "cards" || view === "gallery") return "resource_cards";
|
|
61
|
-
if (screen.kind === "list") return "resource_table";
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
51
|
function componentById(graph, componentId) {
|
|
66
52
|
return (graph.byKind.component || []).find((component) => component.id === componentId) || null;
|
|
67
53
|
}
|
|
@@ -88,6 +74,50 @@ function regionContractFor(regionEntries, regionName) {
|
|
|
88
74
|
return (regionEntries || []).find((entry) => entry.region === regionName) || null;
|
|
89
75
|
}
|
|
90
76
|
|
|
77
|
+
function buildDesignIntentContract(projection) {
|
|
78
|
+
const design = {
|
|
79
|
+
density: "comfortable",
|
|
80
|
+
tone: "operational",
|
|
81
|
+
radiusScale: "medium",
|
|
82
|
+
colorRoles: {},
|
|
83
|
+
typographyRoles: {},
|
|
84
|
+
actionRoles: {},
|
|
85
|
+
accessibility: {}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
for (const entry of projection.uiDesign || []) {
|
|
89
|
+
if (entry.key === "density" && entry.role) {
|
|
90
|
+
design.density = entry.role;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (entry.key === "tone" && entry.role) {
|
|
94
|
+
design.tone = entry.role;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (entry.key === "radius_scale" && entry.role) {
|
|
98
|
+
design.radiusScale = entry.role;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (entry.key === "color_role" && entry.role && entry.value) {
|
|
102
|
+
design.colorRoles[entry.role] = entry.value;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (entry.key === "typography_role" && entry.role && entry.value) {
|
|
106
|
+
design.typographyRoles[entry.role] = entry.value;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (entry.key === "action_role" && entry.role && entry.value) {
|
|
110
|
+
design.actionRoles[entry.role] = entry.value;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (entry.key === "accessibility" && entry.role && entry.value) {
|
|
114
|
+
design.accessibility[entry.role] = entry.value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return design;
|
|
119
|
+
}
|
|
120
|
+
|
|
91
121
|
export function buildComponentUsageContract(graph, entry, options = {}) {
|
|
92
122
|
const componentId = entry.component?.id || null;
|
|
93
123
|
const contract = componentId ? componentContractFor(graph, componentId) : null;
|
|
@@ -184,7 +214,7 @@ function buildUiScreenContract(graph, projection, screen, ownershipFields) {
|
|
|
184
214
|
);
|
|
185
215
|
const visibilityEntries = (projection.uiVisibility || []).filter((entry) => screenActionIds.has(entry.capability?.id));
|
|
186
216
|
const patterns = new Set(regionEntries.map((entry) => entry.pattern).filter(Boolean));
|
|
187
|
-
const derivedDefaultPattern =
|
|
217
|
+
const derivedDefaultPattern = defaultPatternForScreen(screen, collectionEntries);
|
|
188
218
|
if (derivedDefaultPattern) {
|
|
189
219
|
patterns.add(derivedDefaultPattern);
|
|
190
220
|
}
|
|
@@ -286,6 +316,7 @@ export function buildUiSharedRealization(graph, options = {}) {
|
|
|
286
316
|
realizes: projection.realizes,
|
|
287
317
|
outputs: projection.outputs,
|
|
288
318
|
components: buildComponentContractMap(graph, componentUsages),
|
|
319
|
+
design: buildDesignIntentContract(projection),
|
|
289
320
|
appShell: buildAppShellContract(projection),
|
|
290
321
|
navigation: buildNavigationContract(projection, screens),
|
|
291
322
|
screens
|
|
@@ -305,6 +336,7 @@ export function buildUiSharedRealization(graph, options = {}) {
|
|
|
305
336
|
realizes: projection.realizes,
|
|
306
337
|
outputs: projection.outputs,
|
|
307
338
|
components: buildComponentContractMap(graph, componentUsages),
|
|
339
|
+
design: buildDesignIntentContract(projection),
|
|
308
340
|
appShell: buildAppShellContract(projection),
|
|
309
341
|
navigation: buildNavigationContract(projection, screens),
|
|
310
342
|
screens
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { buildApiRealization } from "../api/index.js";
|
|
2
2
|
import { generatorDefaultsMap, getProjection, sharedUiProjectionForWeb } from "../../generator/surfaces/shared.js";
|
|
3
3
|
import {
|
|
4
|
-
buildComponentContractMap,
|
|
5
|
-
buildComponentUsageContract,
|
|
6
4
|
buildUiSharedRealization
|
|
7
5
|
} from "./build-ui-shared-realization.js";
|
|
8
6
|
|
|
@@ -38,6 +36,7 @@ export function buildWebRealization(graph, options = {}) {
|
|
|
38
36
|
realizes: [],
|
|
39
37
|
outputs: [],
|
|
40
38
|
components: {},
|
|
39
|
+
design: null,
|
|
41
40
|
screens: []
|
|
42
41
|
};
|
|
43
42
|
const concreteContract = buildUiSharedRealization(graph, { projectionId: projection.id });
|
|
@@ -70,30 +69,15 @@ export function buildWebRealization(graph, options = {}) {
|
|
|
70
69
|
screenMap.set(screen.id, {
|
|
71
70
|
...existing,
|
|
72
71
|
...screen,
|
|
73
|
-
components: [...(existing.components || [])
|
|
72
|
+
components: [...(existing.components || [])],
|
|
74
73
|
regions: mergeByKey(existing.regions || [], screen.regions || [], (entry) => entry.region),
|
|
75
74
|
patterns: [...new Set([...(existing.patterns || []), ...(screen.patterns || [])])]
|
|
76
75
|
});
|
|
77
76
|
}
|
|
78
|
-
for (const entry of projection.uiComponents || []) {
|
|
79
|
-
if (!screenMap.has(entry.screenId)) {
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
const screen = screenMap.get(entry.screenId);
|
|
83
|
-
if ((screen.components || []).some((usage) => componentUsageFingerprint(usage) === componentUsageFingerprintFromEntry(entry))) {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
screen.components = [...(screen.components || []), buildComponentUsageContract(graph, entry, {
|
|
87
|
-
region: (screen.regions || []).find((region) => region.region === entry.region) || null
|
|
88
|
-
})];
|
|
89
|
-
}
|
|
90
77
|
|
|
91
78
|
const appShell = projection.uiAppShell?.length || !sharedProjection ? concreteContract.appShell : sharedContract.appShell;
|
|
92
79
|
const navigation = projection.uiNavigation?.length || !sharedProjection ? concreteContract.navigation : sharedContract.navigation;
|
|
93
|
-
const
|
|
94
|
-
...(sharedContract.components || {}),
|
|
95
|
-
...buildComponentContractMap(graph, projection.uiComponents || [])
|
|
96
|
-
};
|
|
80
|
+
const design = projection.uiDesign?.length || !sharedProjection ? concreteContract.design : sharedContract.design;
|
|
97
81
|
|
|
98
82
|
const contract = {
|
|
99
83
|
projection: {
|
|
@@ -109,7 +93,8 @@ export function buildWebRealization(graph, options = {}) {
|
|
|
109
93
|
: null,
|
|
110
94
|
generatorDefaults: generatorDefaultsMap(projection),
|
|
111
95
|
outputs: projection.outputs,
|
|
112
|
-
components:
|
|
96
|
+
components: sharedProjection ? (sharedContract.components || {}) : (concreteContract.components || {}),
|
|
97
|
+
design: design || null,
|
|
113
98
|
appShell: appShell || null,
|
|
114
99
|
navigation: {
|
|
115
100
|
groups: navigation?.groups || [],
|
|
@@ -171,21 +156,3 @@ function mergeByKey(left, right, keyFn) {
|
|
|
171
156
|
for (const entry of right) output.set(keyFn(entry), { ...(output.get(keyFn(entry)) || {}), ...entry });
|
|
172
157
|
return [...output.values()];
|
|
173
158
|
}
|
|
174
|
-
|
|
175
|
-
function componentUsageFingerprint(usage) {
|
|
176
|
-
return [
|
|
177
|
-
usage?.region || "",
|
|
178
|
-
usage?.component?.id || "",
|
|
179
|
-
...(usage?.dataBindings || []).map((binding) => `data:${binding.prop}:${binding.source?.id || ""}`),
|
|
180
|
-
...(usage?.eventBindings || []).map((binding) => `event:${binding.event}:${binding.action}:${binding.target?.id || ""}`)
|
|
181
|
-
].join("|");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function componentUsageFingerprintFromEntry(entry) {
|
|
185
|
-
return [
|
|
186
|
-
entry?.region || "",
|
|
187
|
-
entry?.component?.id || "",
|
|
188
|
-
...(entry?.dataBindings || []).map((binding) => `data:${binding.prop}:${binding.source?.id || ""}`),
|
|
189
|
-
...(entry?.eventBindings || []).map((binding) => `event:${binding.event}:${binding.action}:${binding.target?.id || ""}`)
|
|
190
|
-
].join("|");
|
|
191
|
-
}
|
package/src/resolver/index.js
CHANGED
|
@@ -503,6 +503,7 @@ function buildProjectionPlan(statement) {
|
|
|
503
503
|
uiVisibility: statement.uiVisibility,
|
|
504
504
|
uiRoutes: statement.uiRoutes,
|
|
505
505
|
uiWeb: statement.uiWeb,
|
|
506
|
+
uiDesign: statement.uiDesign,
|
|
506
507
|
uiComponents: statement.uiComponents,
|
|
507
508
|
dbTables: statement.dbTables,
|
|
508
509
|
dbColumns: statement.dbColumns,
|
|
@@ -1269,6 +1270,17 @@ function parseProjectionUiAppShellBlock(statement) {
|
|
|
1269
1270
|
}));
|
|
1270
1271
|
}
|
|
1271
1272
|
|
|
1273
|
+
function parseProjectionUiDesignBlock(statement) {
|
|
1274
|
+
return blockEntries(getFieldValue(statement, "ui_design")).map((entry) => ({
|
|
1275
|
+
type: "ui_design_token",
|
|
1276
|
+
key: tokenValue(entry.items[0]) || null,
|
|
1277
|
+
role: tokenValue(entry.items[1]) || null,
|
|
1278
|
+
value: tokenValue(entry.items[2]) || null,
|
|
1279
|
+
raw: normalizeSequence(entry.items),
|
|
1280
|
+
loc: entry.loc
|
|
1281
|
+
}));
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1272
1284
|
function parseProjectionUiNavigationBlock(statement) {
|
|
1273
1285
|
return blockEntries(getFieldValue(statement, "ui_navigation")).map((entry) => {
|
|
1274
1286
|
const directives = {};
|
|
@@ -1869,6 +1881,7 @@ export function normalizeStatement(statement, registry) {
|
|
|
1869
1881
|
uiWeb: parseProjectionUiWebBlock(statement, registry),
|
|
1870
1882
|
uiIos: parseProjectionUiIosBlock(statement, registry),
|
|
1871
1883
|
uiAppShell: parseProjectionUiAppShellBlock(statement),
|
|
1884
|
+
uiDesign: parseProjectionUiDesignBlock(statement),
|
|
1872
1885
|
uiNavigation: parseProjectionUiNavigationBlock(statement),
|
|
1873
1886
|
uiScreenRegions: parseProjectionUiScreenRegionsBlock(statement),
|
|
1874
1887
|
uiComponents: parseProjectionUiComponentsBlock(statement, registry),
|