@topogram/cli 0.3.48 → 0.3.49
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/package.json +1 -1
- package/src/generator/context/slice.js +131 -0
- package/src/generator/registry.js +5 -2
- package/src/generator/surfaces/shared.js +2 -1
- package/src/generator/surfaces/web/react-components.js +3 -7
- package/src/generator/surfaces/web/sveltekit-components.js +3 -7
- 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
package/package.json
CHANGED
|
@@ -194,6 +194,7 @@ function projectionSlice(graph, projectionId) {
|
|
|
194
194
|
verification_targets: recommendedVerificationTargets(graph, [projectionId, ...capabilities, ...entities, ...shapes, ...components], {
|
|
195
195
|
rationale: "Projection slices affect generated contract and runtime surfaces, so verification should follow the projection closure."
|
|
196
196
|
}),
|
|
197
|
+
ui_agent_packet: uiAgentPacketForProjection(graph, projection),
|
|
197
198
|
write_scope: buildDefaultWriteScope(),
|
|
198
199
|
review_boundary: projection.reviewBoundary || {
|
|
199
200
|
automation_class: "review_required",
|
|
@@ -286,6 +287,7 @@ function componentSlice(graph, componentId) {
|
|
|
286
287
|
verification_targets: recommendedVerificationTargets(graph, verificationScope, {
|
|
287
288
|
rationale: "Component changes affect every related projection — verification should follow the component contract closure."
|
|
288
289
|
}),
|
|
290
|
+
ui_agent_packet: uiAgentPacketForComponent(graph, component, projections),
|
|
289
291
|
write_scope: buildDefaultWriteScope(),
|
|
290
292
|
review_boundary: {
|
|
291
293
|
automation_class: "review_required",
|
|
@@ -331,6 +333,135 @@ function componentDependencyKind(dependency) {
|
|
|
331
333
|
return prefix || null;
|
|
332
334
|
}
|
|
333
335
|
|
|
336
|
+
function uiAgentPacketForProjection(graph, projection) {
|
|
337
|
+
if (!String(projection.platform || "").startsWith("ui_")) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const sharedProjection = projection.platform === "ui_shared"
|
|
341
|
+
? projection
|
|
342
|
+
: sharedUiProjectionFor(graph, projection);
|
|
343
|
+
const ownerProjection = sharedProjection || projection;
|
|
344
|
+
return {
|
|
345
|
+
type: "ui_agent_packet",
|
|
346
|
+
version: 1,
|
|
347
|
+
ownership: {
|
|
348
|
+
componentPlacement: "ui_shared",
|
|
349
|
+
designIntent: "ui_shared",
|
|
350
|
+
concreteSurfaceOwns: ["routes", "surface_hints"]
|
|
351
|
+
},
|
|
352
|
+
sharedProjection: sharedProjection
|
|
353
|
+
? {
|
|
354
|
+
id: sharedProjection.id,
|
|
355
|
+
name: sharedProjection.name || sharedProjection.id
|
|
356
|
+
}
|
|
357
|
+
: null,
|
|
358
|
+
screens: (ownerProjection.uiScreens || []).map((screen) => ({
|
|
359
|
+
id: screen.id,
|
|
360
|
+
kind: screen.kind,
|
|
361
|
+
title: screen.title || screen.id
|
|
362
|
+
})),
|
|
363
|
+
routes: (projection.uiRoutes || []).map((route) => ({
|
|
364
|
+
screenId: route.screenId,
|
|
365
|
+
path: route.path
|
|
366
|
+
})),
|
|
367
|
+
components: (ownerProjection.uiComponents || []).map((usage) => componentUsagePacket(usage)),
|
|
368
|
+
design: designIntentPacket(ownerProjection),
|
|
369
|
+
requiredGates: uiRequiredGates(projection.id)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function uiAgentPacketForComponent(graph, component, projectionIds) {
|
|
374
|
+
const projectionSet = new Set(projectionIds);
|
|
375
|
+
const sourceUsages = [];
|
|
376
|
+
for (const projection of graph.byKind.projection || []) {
|
|
377
|
+
for (const usage of projection.uiComponents || []) {
|
|
378
|
+
if (usage.component?.id !== component.id) continue;
|
|
379
|
+
sourceUsages.push({
|
|
380
|
+
projection: {
|
|
381
|
+
id: projection.id,
|
|
382
|
+
platform: projection.platform,
|
|
383
|
+
ownership: projection.platform === "ui_shared" ? "owner" : "concrete"
|
|
384
|
+
},
|
|
385
|
+
usage: componentUsagePacket(usage),
|
|
386
|
+
design: designIntentPacket(projection)
|
|
387
|
+
});
|
|
388
|
+
projectionSet.add(projection.id);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
type: "ui_agent_packet",
|
|
394
|
+
version: 1,
|
|
395
|
+
ownership: {
|
|
396
|
+
componentContract: "component",
|
|
397
|
+
componentPlacement: "ui_shared",
|
|
398
|
+
concreteSurfacesInherit: true
|
|
399
|
+
},
|
|
400
|
+
component: {
|
|
401
|
+
id: component.id,
|
|
402
|
+
name: component.name || component.id,
|
|
403
|
+
category: component.category || null,
|
|
404
|
+
patterns: component.componentContract?.patterns || [],
|
|
405
|
+
regions: component.componentContract?.regions || [],
|
|
406
|
+
behaviors: component.componentContract?.behaviors || []
|
|
407
|
+
},
|
|
408
|
+
sourceUsages,
|
|
409
|
+
inheritedBy: [...projectionSet]
|
|
410
|
+
.filter((projectionId) => !sourceUsages.some((entry) => entry.projection.id === projectionId))
|
|
411
|
+
.sort(),
|
|
412
|
+
requiredGates: uiRequiredGates(null, component.id)
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function sharedUiProjectionFor(graph, projection) {
|
|
417
|
+
for (const reference of projection.realizes || []) {
|
|
418
|
+
const candidate = (graph.byKind.projection || []).find((entry) => entry.id === reference.id);
|
|
419
|
+
if (candidate?.platform === "ui_shared") {
|
|
420
|
+
return candidate;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function componentUsagePacket(usage) {
|
|
427
|
+
return {
|
|
428
|
+
screenId: usage.screenId || null,
|
|
429
|
+
region: usage.region || null,
|
|
430
|
+
componentId: usage.component?.id || null,
|
|
431
|
+
dataBindings: (usage.dataBindings || []).map((binding) => ({
|
|
432
|
+
prop: binding.prop || null,
|
|
433
|
+
source: binding.source?.id || binding.source || null
|
|
434
|
+
})),
|
|
435
|
+
eventBindings: (usage.eventBindings || []).map((binding) => ({
|
|
436
|
+
event: binding.event || null,
|
|
437
|
+
action: binding.action || null,
|
|
438
|
+
target: binding.target?.id || binding.target || null
|
|
439
|
+
}))
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function designIntentPacket(projection) {
|
|
444
|
+
return (projection.uiDesign || []).map((entry) => ({
|
|
445
|
+
key: entry.key,
|
|
446
|
+
role: entry.role,
|
|
447
|
+
value: entry.value
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function uiRequiredGates(projectionId = null, componentId = null) {
|
|
452
|
+
return [
|
|
453
|
+
{ command: "topogram check", reason: "Validate shared UI ownership, taxonomy, references, and topology." },
|
|
454
|
+
{
|
|
455
|
+
command: `topogram component check${projectionId ? ` --projection ${projectionId}` : ""}${componentId ? ` --component ${componentId}` : ""}`,
|
|
456
|
+
reason: "Validate component placement, props, events, regions, and patterns."
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
command: `topogram component behavior${projectionId ? ` --projection ${projectionId}` : ""}${componentId ? ` --component ${componentId}` : ""}`,
|
|
460
|
+
reason: "Inspect behavior realizations and partial bindings before code changes."
|
|
461
|
+
}
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
|
|
334
465
|
function journeySlice(graph, journeyId) {
|
|
335
466
|
const journey = getJourneyDoc(graph, journeyId);
|
|
336
467
|
const capabilities = [...(journey.relatedCapabilities || [])].sort();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
+
import { UI_GENERATOR_RENDERED_COMPONENT_PATTERNS } from "../ui/taxonomy.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @typedef {Object} GeneratorManifest
|
|
@@ -23,6 +24,8 @@ import { createRequire } from "node:module";
|
|
|
23
24
|
* @property {boolean} [planned]
|
|
24
25
|
*/
|
|
25
26
|
|
|
27
|
+
const RENDERED_COMPONENT_PATTERNS = [...UI_GENERATOR_RENDERED_COMPONENT_PATTERNS];
|
|
28
|
+
|
|
26
29
|
/** @type {GeneratorManifest[]} */
|
|
27
30
|
export const GENERATOR_MANIFESTS = [
|
|
28
31
|
{
|
|
@@ -76,7 +79,7 @@ export const GENERATOR_MANIFESTS = [
|
|
|
76
79
|
stack: { runtime: "node", framework: "sveltekit", language: "typescript" },
|
|
77
80
|
capabilities: { routes: true, components: true, coverage: true },
|
|
78
81
|
componentSupport: {
|
|
79
|
-
patterns:
|
|
82
|
+
patterns: RENDERED_COMPONENT_PATTERNS,
|
|
80
83
|
behaviors: ["selection", "sorting", "filtering", "search", "pagination", "bulk_action", "optimistic_update"],
|
|
81
84
|
unsupported: "warning"
|
|
82
85
|
},
|
|
@@ -94,7 +97,7 @@ export const GENERATOR_MANIFESTS = [
|
|
|
94
97
|
stack: { runtime: "browser", framework: "react", language: "typescript" },
|
|
95
98
|
capabilities: { routes: true, components: true, coverage: true },
|
|
96
99
|
componentSupport: {
|
|
97
|
-
patterns:
|
|
100
|
+
patterns: RENDERED_COMPONENT_PATTERNS,
|
|
98
101
|
behaviors: ["selection", "sorting", "filtering", "search", "pagination", "bulk_action", "optimistic_update"],
|
|
99
102
|
unsupported: "warning"
|
|
100
103
|
},
|
|
@@ -38,7 +38,8 @@ export function uiProjectionCandidates(graph) {
|
|
|
38
38
|
(projection.uiAppShell || []).length > 0 ||
|
|
39
39
|
(projection.uiNavigation || []).length > 0 ||
|
|
40
40
|
(projection.uiScreenRegions || []).length > 0 ||
|
|
41
|
-
(projection.uiComponents || []).length > 0
|
|
41
|
+
(projection.uiComponents || []).length > 0 ||
|
|
42
|
+
(projection.uiDesign || []).length > 0
|
|
42
43
|
);
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import { UI_GENERATOR_RENDERED_COMPONENT_PATTERNS } from "../../../ui/taxonomy.js";
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* @typedef {{ id?: string, name?: string }} ComponentReference
|
|
5
7
|
* @typedef {{ component?: ComponentReference, region?: string, pattern?: string }} ComponentUsage
|
|
@@ -71,13 +73,7 @@ export function reactComponentUsageSupport(usage, componentContracts) {
|
|
|
71
73
|
const pattern = usagePattern(usage, componentContracts);
|
|
72
74
|
return {
|
|
73
75
|
pattern,
|
|
74
|
-
supported:
|
|
75
|
-
"summary_stats",
|
|
76
|
-
"board_view",
|
|
77
|
-
"calendar_view",
|
|
78
|
-
"resource_table",
|
|
79
|
-
"data_grid_view"
|
|
80
|
-
].includes(pattern || "")
|
|
76
|
+
supported: UI_GENERATOR_RENDERED_COMPONENT_PATTERNS.has(pattern || "")
|
|
81
77
|
};
|
|
82
78
|
}
|
|
83
79
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import { UI_GENERATOR_RENDERED_COMPONENT_PATTERNS } from "../../../ui/taxonomy.js";
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* @typedef {{ id?: string, name?: string }} ComponentReference
|
|
5
7
|
* @typedef {{ component?: ComponentReference, region?: string, pattern?: string }} ComponentUsage
|
|
@@ -62,13 +64,7 @@ export function svelteKitComponentUsageSupport(usage, componentContracts) {
|
|
|
62
64
|
const pattern = usagePattern(usage, componentContracts);
|
|
63
65
|
return {
|
|
64
66
|
pattern,
|
|
65
|
-
supported:
|
|
66
|
-
"summary_stats",
|
|
67
|
-
"board_view",
|
|
68
|
-
"calendar_view",
|
|
69
|
-
"resource_table",
|
|
70
|
-
"data_grid_view"
|
|
71
|
-
].includes(pattern || "")
|
|
67
|
+
supported: UI_GENERATOR_RENDERED_COMPONENT_PATTERNS.has(pattern || "")
|
|
72
68
|
};
|
|
73
69
|
}
|
|
74
70
|
|
|
@@ -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),
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
export const UI_APP_SHELL_KINDS = new Set([
|
|
4
|
+
"topbar",
|
|
5
|
+
"sidebar",
|
|
6
|
+
"dual_nav",
|
|
7
|
+
"workspace",
|
|
8
|
+
"wizard",
|
|
9
|
+
"bottom_tabs",
|
|
10
|
+
"split_view",
|
|
11
|
+
"menu_bar"
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export const UI_WINDOWING_MODES = new Set(["single_window", "multi_window"]);
|
|
15
|
+
|
|
16
|
+
export const UI_SCREEN_KINDS = new Set([
|
|
17
|
+
"list",
|
|
18
|
+
"detail",
|
|
19
|
+
"form",
|
|
20
|
+
"dashboard",
|
|
21
|
+
"job_status",
|
|
22
|
+
"board",
|
|
23
|
+
"calendar",
|
|
24
|
+
"feed",
|
|
25
|
+
"inbox",
|
|
26
|
+
"settings",
|
|
27
|
+
"wizard",
|
|
28
|
+
"report",
|
|
29
|
+
"analytics"
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export const UI_COLLECTION_PRESENTATIONS = new Set([
|
|
33
|
+
"table",
|
|
34
|
+
"data_grid",
|
|
35
|
+
"cards",
|
|
36
|
+
"list",
|
|
37
|
+
"board",
|
|
38
|
+
"calendar",
|
|
39
|
+
"gallery"
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export const UI_NAVIGATION_PATTERNS = new Set([
|
|
43
|
+
"primary",
|
|
44
|
+
"tabs",
|
|
45
|
+
"stack_navigation",
|
|
46
|
+
"bottom_tabs",
|
|
47
|
+
"segmented_control",
|
|
48
|
+
"command_palette",
|
|
49
|
+
"split_view",
|
|
50
|
+
"navigation_rail"
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
export const UI_REGION_KINDS = new Set([
|
|
54
|
+
"hero",
|
|
55
|
+
"toolbar",
|
|
56
|
+
"filters",
|
|
57
|
+
"search",
|
|
58
|
+
"results",
|
|
59
|
+
"summary",
|
|
60
|
+
"metadata",
|
|
61
|
+
"aside",
|
|
62
|
+
"related",
|
|
63
|
+
"activity",
|
|
64
|
+
"comments",
|
|
65
|
+
"timeline",
|
|
66
|
+
"tabs",
|
|
67
|
+
"bulk_actions",
|
|
68
|
+
"footer_actions"
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
export const UI_PATTERN_KINDS = new Set([
|
|
72
|
+
"resource_table",
|
|
73
|
+
"data_grid_view",
|
|
74
|
+
"resource_cards",
|
|
75
|
+
"detail_panel",
|
|
76
|
+
"edit_form",
|
|
77
|
+
"lookup_select",
|
|
78
|
+
"action_bar",
|
|
79
|
+
"status_badge",
|
|
80
|
+
"summary_stats",
|
|
81
|
+
"activity_feed",
|
|
82
|
+
"comment_thread",
|
|
83
|
+
"timeline_view",
|
|
84
|
+
"board_view",
|
|
85
|
+
"calendar_view",
|
|
86
|
+
"settings_section",
|
|
87
|
+
"wizard_stepper",
|
|
88
|
+
"audit_log",
|
|
89
|
+
"search_results",
|
|
90
|
+
"empty_state_panel",
|
|
91
|
+
"inspector_pane",
|
|
92
|
+
"master_detail"
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
export const UI_ACTION_PRESENTATIONS = new Set([
|
|
96
|
+
"button",
|
|
97
|
+
"menu_item",
|
|
98
|
+
"split_button",
|
|
99
|
+
"bulk_action",
|
|
100
|
+
"modal",
|
|
101
|
+
"drawer",
|
|
102
|
+
"inline_confirm",
|
|
103
|
+
"sheet",
|
|
104
|
+
"bottom_sheet",
|
|
105
|
+
"fab",
|
|
106
|
+
"popover"
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
export const UI_STATE_KINDS = new Set([
|
|
110
|
+
"loading",
|
|
111
|
+
"empty",
|
|
112
|
+
"error",
|
|
113
|
+
"unauthorized",
|
|
114
|
+
"not_found",
|
|
115
|
+
"success"
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
export const UI_PLATFORM_PATTERNS = new Set([
|
|
119
|
+
"bottom_tabs",
|
|
120
|
+
"stack_navigation",
|
|
121
|
+
"split_view",
|
|
122
|
+
"master_detail",
|
|
123
|
+
"navigation_rail",
|
|
124
|
+
"fab",
|
|
125
|
+
"sheet",
|
|
126
|
+
"bottom_sheet",
|
|
127
|
+
"pull_to_refresh",
|
|
128
|
+
"segmented_control",
|
|
129
|
+
"command_palette",
|
|
130
|
+
"inspector_pane",
|
|
131
|
+
"multi_pane_layout",
|
|
132
|
+
"resizable_split",
|
|
133
|
+
"menu_bar",
|
|
134
|
+
"multi_window"
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
export const UI_DESIGN_DENSITIES = new Set(["compact", "comfortable", "spacious"]);
|
|
138
|
+
export const UI_DESIGN_TONES = new Set(["operational", "neutral", "editorial", "playful"]);
|
|
139
|
+
export const UI_DESIGN_RADIUS_SCALES = new Set(["none", "small", "medium", "large"]);
|
|
140
|
+
export const UI_DESIGN_COLOR_ROLES = new Set(["primary", "secondary", "surface", "text", "muted", "danger", "success", "warning", "info"]);
|
|
141
|
+
export const UI_DESIGN_TYPOGRAPHY_ROLES = new Set(["body", "heading", "label", "mono", "numeric"]);
|
|
142
|
+
export const UI_DESIGN_ACTION_ROLES = new Set(["primary", "secondary", "destructive", "contextual", "bulk"]);
|
|
143
|
+
export const UI_DESIGN_ACCESSIBILITY_VALUES = {
|
|
144
|
+
contrast: new Set(["aa", "aaa", "high"]),
|
|
145
|
+
motion: new Set(["standard", "reduced"]),
|
|
146
|
+
focus: new Set(["visible", "required"]),
|
|
147
|
+
min_touch_target: new Set(["compact", "comfortable"])
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const UI_GENERATOR_RENDERED_COMPONENT_PATTERNS = new Set([
|
|
151
|
+
"summary_stats",
|
|
152
|
+
"board_view",
|
|
153
|
+
"calendar_view",
|
|
154
|
+
"resource_table",
|
|
155
|
+
"data_grid_view"
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {string[]} presentations
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
export function collectionPatternFromPresentations(presentations = []) {
|
|
163
|
+
if (presentations.includes("data_grid")) return "data_grid_view";
|
|
164
|
+
if (presentations.includes("table")) return "resource_table";
|
|
165
|
+
if (presentations.includes("cards")) return "resource_cards";
|
|
166
|
+
if (presentations.includes("board")) return "board_view";
|
|
167
|
+
if (presentations.includes("calendar")) return "calendar_view";
|
|
168
|
+
if (presentations.includes("gallery")) return "resource_cards";
|
|
169
|
+
return "search_results";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {string|null|undefined} pattern
|
|
174
|
+
* @returns {string}
|
|
175
|
+
*/
|
|
176
|
+
export function presentationFromPattern(pattern) {
|
|
177
|
+
if (pattern === "data_grid_view") return "data_grid";
|
|
178
|
+
if (pattern === "resource_table") return "table";
|
|
179
|
+
if (pattern === "resource_cards") return "cards";
|
|
180
|
+
if (pattern === "board_view") return "board";
|
|
181
|
+
if (pattern === "calendar_view") return "calendar";
|
|
182
|
+
return "list";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {{ kind?: string|null }} screen
|
|
187
|
+
* @param {{ operation?: string|null, value?: string|null }[]} collectionEntries
|
|
188
|
+
* @returns {string|null}
|
|
189
|
+
*/
|
|
190
|
+
export function defaultPatternForScreen(screen, collectionEntries = []) {
|
|
191
|
+
if (screen.kind === "detail") return "detail_panel";
|
|
192
|
+
if (screen.kind === "form") return "edit_form";
|
|
193
|
+
if (screen.kind === "board") return "board_view";
|
|
194
|
+
if (screen.kind === "calendar") return "calendar_view";
|
|
195
|
+
if (screen.kind === "dashboard" || screen.kind === "analytics" || screen.kind === "report") return "summary_stats";
|
|
196
|
+
if (screen.kind === "feed" || screen.kind === "inbox") return "activity_feed";
|
|
197
|
+
const view = collectionEntries.find((entry) => entry.operation === "view")?.value;
|
|
198
|
+
if (view) return collectionPatternFromPresentations([view]);
|
|
199
|
+
if (screen.kind === "list") return "resource_table";
|
|
200
|
+
return null;
|
|
201
|
+
}
|
package/src/validator/index.js
CHANGED
|
@@ -37,6 +37,16 @@ import {
|
|
|
37
37
|
UI_NAVIGATION_PATTERNS,
|
|
38
38
|
UI_REGION_KINDS,
|
|
39
39
|
UI_PATTERN_KINDS,
|
|
40
|
+
UI_APP_SHELL_KINDS,
|
|
41
|
+
UI_WINDOWING_MODES,
|
|
42
|
+
UI_STATE_KINDS,
|
|
43
|
+
UI_DESIGN_DENSITIES,
|
|
44
|
+
UI_DESIGN_TONES,
|
|
45
|
+
UI_DESIGN_RADIUS_SCALES,
|
|
46
|
+
UI_DESIGN_COLOR_ROLES,
|
|
47
|
+
UI_DESIGN_TYPOGRAPHY_ROLES,
|
|
48
|
+
UI_DESIGN_ACTION_ROLES,
|
|
49
|
+
UI_DESIGN_ACCESSIBILITY_VALUES,
|
|
40
50
|
FIELD_SPECS
|
|
41
51
|
} from "./kinds.js";
|
|
42
52
|
import { validateComponent } from "./per-kind/component.js";
|
|
@@ -78,6 +88,16 @@ export {
|
|
|
78
88
|
UI_NAVIGATION_PATTERNS,
|
|
79
89
|
UI_REGION_KINDS,
|
|
80
90
|
UI_PATTERN_KINDS,
|
|
91
|
+
UI_APP_SHELL_KINDS,
|
|
92
|
+
UI_WINDOWING_MODES,
|
|
93
|
+
UI_STATE_KINDS,
|
|
94
|
+
UI_DESIGN_DENSITIES,
|
|
95
|
+
UI_DESIGN_TONES,
|
|
96
|
+
UI_DESIGN_RADIUS_SCALES,
|
|
97
|
+
UI_DESIGN_COLOR_ROLES,
|
|
98
|
+
UI_DESIGN_TYPOGRAPHY_ROLES,
|
|
99
|
+
UI_DESIGN_ACTION_ROLES,
|
|
100
|
+
UI_DESIGN_ACCESSIBILITY_VALUES,
|
|
81
101
|
FIELD_SPECS
|
|
82
102
|
} from "./kinds.js";
|
|
83
103
|
|
|
@@ -279,7 +299,7 @@ function validateFieldShapes(errors, statement, fieldMap) {
|
|
|
279
299
|
ensureSingleValueField(errors, statement, fieldMap, key, ["list"]);
|
|
280
300
|
}
|
|
281
301
|
|
|
282
|
-
for (const key of ["fields", "props", "events", "slots", "behaviors", "keys", "relations", "invariants", "rename", "overrides", "http", "http_errors", "http_fields", "http_responses", "http_preconditions", "http_idempotency", "http_cache", "http_delete", "http_async", "http_status", "http_download", "http_authz", "http_callbacks", "ui_screens", "ui_collections", "ui_actions", "ui_visibility", "ui_lookups", "ui_routes", "ui_web", "ui_ios", "ui_app_shell", "ui_navigation", "ui_screen_regions", "ui_components", "db_tables", "db_columns", "db_keys", "db_indexes", "db_relations", "db_lifecycle", "generator_defaults"]) {
|
|
302
|
+
for (const key of ["fields", "props", "events", "slots", "behaviors", "keys", "relations", "invariants", "rename", "overrides", "http", "http_errors", "http_fields", "http_responses", "http_preconditions", "http_idempotency", "http_cache", "http_delete", "http_async", "http_status", "http_download", "http_authz", "http_callbacks", "ui_screens", "ui_collections", "ui_actions", "ui_visibility", "ui_lookups", "ui_routes", "ui_web", "ui_ios", "ui_app_shell", "ui_navigation", "ui_screen_regions", "ui_components", "ui_design", "db_tables", "db_columns", "db_keys", "db_indexes", "db_relations", "db_lifecycle", "generator_defaults"]) {
|
|
283
303
|
ensureSingleValueField(errors, statement, fieldMap, key, ["block"]);
|
|
284
304
|
}
|
|
285
305
|
|
|
@@ -2320,18 +2340,113 @@ function validateProjectionUiAppShell(errors, statement, fieldMap) {
|
|
|
2320
2340
|
}
|
|
2321
2341
|
seenKeys.add(key);
|
|
2322
2342
|
|
|
2323
|
-
if (key === "shell" && !
|
|
2343
|
+
if (key === "shell" && !UI_APP_SHELL_KINDS.has(value)) {
|
|
2324
2344
|
pushError(errors, `Projection ${statement.id} ui_app_shell has invalid shell '${value}'`, entry.loc);
|
|
2325
2345
|
}
|
|
2326
2346
|
if (["global_search", "notifications", "account_menu", "workspace_switcher"].includes(key) && !["true", "false"].includes(value)) {
|
|
2327
2347
|
pushError(errors, `Projection ${statement.id} ui_app_shell '${key}' must be true or false`, entry.loc);
|
|
2328
2348
|
}
|
|
2329
|
-
if (key === "windowing" && !
|
|
2349
|
+
if (key === "windowing" && !UI_WINDOWING_MODES.has(value)) {
|
|
2330
2350
|
pushError(errors, `Projection ${statement.id} ui_app_shell has invalid windowing '${value}'`, entry.loc);
|
|
2331
2351
|
}
|
|
2332
2352
|
}
|
|
2333
2353
|
}
|
|
2334
2354
|
|
|
2355
|
+
function validateProjectionUiDesign(errors, statement, fieldMap) {
|
|
2356
|
+
if (statement.kind !== "projection") {
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
const designField = fieldMap.get("ui_design")?.[0];
|
|
2361
|
+
if (!designField || designField.value.type !== "block") {
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
if (symbolValue(getFieldValue(statement, "platform")) !== "ui_shared") {
|
|
2366
|
+
pushError(errors, `Projection ${statement.id} ui_design belongs on shared UI projections; concrete UI projections inherit semantic design intent through 'realizes'`, designField.loc);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
for (const entry of designField.value.entries) {
|
|
2370
|
+
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2371
|
+
const [key, value, extra] = tokens;
|
|
2372
|
+
|
|
2373
|
+
if (key === "density") {
|
|
2374
|
+
if (!UI_DESIGN_DENSITIES.has(value || "")) {
|
|
2375
|
+
pushError(errors, `Projection ${statement.id} ui_design density has invalid value '${value}'`, entry.loc);
|
|
2376
|
+
}
|
|
2377
|
+
if (tokens.length !== 2) {
|
|
2378
|
+
pushError(errors, `Projection ${statement.id} ui_design density accepts exactly one value`, entry.loc);
|
|
2379
|
+
}
|
|
2380
|
+
continue;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
if (key === "tone") {
|
|
2384
|
+
if (!UI_DESIGN_TONES.has(value || "")) {
|
|
2385
|
+
pushError(errors, `Projection ${statement.id} ui_design tone has invalid value '${value}'`, entry.loc);
|
|
2386
|
+
}
|
|
2387
|
+
if (tokens.length !== 2) {
|
|
2388
|
+
pushError(errors, `Projection ${statement.id} ui_design tone accepts exactly one value`, entry.loc);
|
|
2389
|
+
}
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
if (key === "radius_scale") {
|
|
2394
|
+
if (!UI_DESIGN_RADIUS_SCALES.has(value || "")) {
|
|
2395
|
+
pushError(errors, `Projection ${statement.id} ui_design radius_scale has invalid value '${value}'`, entry.loc);
|
|
2396
|
+
}
|
|
2397
|
+
if (tokens.length !== 2) {
|
|
2398
|
+
pushError(errors, `Projection ${statement.id} ui_design radius_scale accepts exactly one value`, entry.loc);
|
|
2399
|
+
}
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
if (key === "color_role") {
|
|
2404
|
+
if (!UI_DESIGN_COLOR_ROLES.has(value || "")) {
|
|
2405
|
+
pushError(errors, `Projection ${statement.id} ui_design color_role has invalid role '${value}'`, entry.loc);
|
|
2406
|
+
}
|
|
2407
|
+
if (tokens.length !== 3) {
|
|
2408
|
+
pushError(errors, `Projection ${statement.id} ui_design color_role must use 'color_role <role> <semantic-token>'`, entry.loc);
|
|
2409
|
+
}
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (key === "typography_role") {
|
|
2414
|
+
if (!UI_DESIGN_TYPOGRAPHY_ROLES.has(value || "")) {
|
|
2415
|
+
pushError(errors, `Projection ${statement.id} ui_design typography_role has invalid role '${value}'`, entry.loc);
|
|
2416
|
+
}
|
|
2417
|
+
if (tokens.length !== 3) {
|
|
2418
|
+
pushError(errors, `Projection ${statement.id} ui_design typography_role must use 'typography_role <role> <semantic-token>'`, entry.loc);
|
|
2419
|
+
}
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
if (key === "action_role") {
|
|
2424
|
+
if (!UI_DESIGN_ACTION_ROLES.has(value || "")) {
|
|
2425
|
+
pushError(errors, `Projection ${statement.id} ui_design action_role has invalid role '${value}'`, entry.loc);
|
|
2426
|
+
}
|
|
2427
|
+
if (tokens.length !== 3) {
|
|
2428
|
+
pushError(errors, `Projection ${statement.id} ui_design action_role must use 'action_role <role> <semantic-token>'`, entry.loc);
|
|
2429
|
+
}
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
if (key === "accessibility") {
|
|
2434
|
+
const values = UI_DESIGN_ACCESSIBILITY_VALUES[value];
|
|
2435
|
+
if (tokens.length !== 3) {
|
|
2436
|
+
pushError(errors, `Projection ${statement.id} ui_design accessibility must use 'accessibility <setting> <value>'`, entry.loc);
|
|
2437
|
+
}
|
|
2438
|
+
if (!values) {
|
|
2439
|
+
pushError(errors, `Projection ${statement.id} ui_design accessibility has invalid setting '${value}'`, entry.loc);
|
|
2440
|
+
} else if (!values.has(extra || "")) {
|
|
2441
|
+
pushError(errors, `Projection ${statement.id} ui_design accessibility '${value}' has invalid value '${extra}'`, entry.loc);
|
|
2442
|
+
}
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
pushError(errors, `Projection ${statement.id} ui_design has unknown key '${key}'`, entry.loc);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2335
2450
|
function validateProjectionUiNavigation(errors, statement, fieldMap, registry) {
|
|
2336
2451
|
if (statement.kind !== "projection") {
|
|
2337
2452
|
return;
|
|
@@ -2458,7 +2573,7 @@ function validateProjectionUiScreenRegions(errors, statement, fieldMap, registry
|
|
|
2458
2573
|
if (directives.has("placement") && !["primary", "secondary", "supporting"].includes(directives.get("placement"))) {
|
|
2459
2574
|
pushError(errors, `Projection ${statement.id} ui_screen_regions for '${screenId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
|
|
2460
2575
|
}
|
|
2461
|
-
if (directives.has("state") && !
|
|
2576
|
+
if (directives.has("state") && !UI_STATE_KINDS.has(directives.get("state"))) {
|
|
2462
2577
|
pushError(errors, `Projection ${statement.id} ui_screen_regions for '${screenId}' has invalid state '${directives.get("state")}'`, entry.loc);
|
|
2463
2578
|
}
|
|
2464
2579
|
}
|
|
@@ -2474,6 +2589,10 @@ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
|
2474
2589
|
return;
|
|
2475
2590
|
}
|
|
2476
2591
|
|
|
2592
|
+
if (symbolValue(getFieldValue(statement, "platform")) !== "ui_shared") {
|
|
2593
|
+
pushError(errors, `Projection ${statement.id} ui_components belongs on shared UI projections; concrete UI projections inherit component placement through 'realizes'`, componentsField.loc);
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2477
2596
|
const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
|
|
2478
2597
|
const availableRegions = collectAvailableUiRegionKeys(statement, registry);
|
|
2479
2598
|
const availableRegionPatterns = collectAvailableUiRegionPatterns(statement, registry);
|
|
@@ -3387,6 +3506,7 @@ export function validateWorkspace(workspaceAst) {
|
|
|
3387
3506
|
validateProjectionUiLookups(errors, statement, fieldMap, registry);
|
|
3388
3507
|
validateProjectionUiRoutes(errors, statement, fieldMap, registry);
|
|
3389
3508
|
validateProjectionUiAppShell(errors, statement, fieldMap);
|
|
3509
|
+
validateProjectionUiDesign(errors, statement, fieldMap);
|
|
3390
3510
|
validateProjectionUiNavigation(errors, statement, fieldMap, registry);
|
|
3391
3511
|
validateProjectionUiScreenRegions(errors, statement, fieldMap, registry);
|
|
3392
3512
|
validateProjectionUiComponents(errors, statement, fieldMap, registry);
|
package/src/validator/kinds.js
CHANGED
|
@@ -74,58 +74,26 @@ export const STATUS_SETS_BY_KIND = {
|
|
|
74
74
|
bug: BUG_STATUSES
|
|
75
75
|
};
|
|
76
76
|
export const VERIFICATION_METHODS = new Set(["smoke", "runtime", "contract", "journey", "manual"]);
|
|
77
|
-
|
|
78
|
-
export
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"aside",
|
|
98
|
-
"related",
|
|
99
|
-
"activity",
|
|
100
|
-
"comments",
|
|
101
|
-
"timeline",
|
|
102
|
-
"tabs",
|
|
103
|
-
"bulk_actions",
|
|
104
|
-
"footer_actions"
|
|
105
|
-
]);
|
|
106
|
-
export const UI_PATTERN_KINDS = new Set([
|
|
107
|
-
"resource_table",
|
|
108
|
-
"data_grid_view",
|
|
109
|
-
"resource_cards",
|
|
110
|
-
"detail_panel",
|
|
111
|
-
"edit_form",
|
|
112
|
-
"lookup_select",
|
|
113
|
-
"action_bar",
|
|
114
|
-
"status_badge",
|
|
115
|
-
"summary_stats",
|
|
116
|
-
"activity_feed",
|
|
117
|
-
"comment_thread",
|
|
118
|
-
"timeline_view",
|
|
119
|
-
"board_view",
|
|
120
|
-
"calendar_view",
|
|
121
|
-
"settings_section",
|
|
122
|
-
"wizard_stepper",
|
|
123
|
-
"audit_log",
|
|
124
|
-
"search_results",
|
|
125
|
-
"empty_state_panel",
|
|
126
|
-
"inspector_pane",
|
|
127
|
-
"master_detail"
|
|
128
|
-
]);
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
UI_APP_SHELL_KINDS,
|
|
80
|
+
UI_WINDOWING_MODES,
|
|
81
|
+
UI_SCREEN_KINDS,
|
|
82
|
+
UI_COLLECTION_PRESENTATIONS,
|
|
83
|
+
UI_NAVIGATION_PATTERNS,
|
|
84
|
+
UI_REGION_KINDS,
|
|
85
|
+
UI_PATTERN_KINDS,
|
|
86
|
+
UI_ACTION_PRESENTATIONS,
|
|
87
|
+
UI_STATE_KINDS,
|
|
88
|
+
UI_PLATFORM_PATTERNS,
|
|
89
|
+
UI_DESIGN_DENSITIES,
|
|
90
|
+
UI_DESIGN_TONES,
|
|
91
|
+
UI_DESIGN_RADIUS_SCALES,
|
|
92
|
+
UI_DESIGN_COLOR_ROLES,
|
|
93
|
+
UI_DESIGN_TYPOGRAPHY_ROLES,
|
|
94
|
+
UI_DESIGN_ACTION_ROLES,
|
|
95
|
+
UI_DESIGN_ACCESSIBILITY_VALUES
|
|
96
|
+
} from "../ui/taxonomy.js";
|
|
129
97
|
|
|
130
98
|
// Kinds that may carry an optional singular `domain dom_x` field. Keep this
|
|
131
99
|
// set in sync with the `allowed[]` arrays in FIELD_SPECS below; the cross-kind
|
|
@@ -221,6 +189,7 @@ export const FIELD_SPECS = {
|
|
|
221
189
|
"ui_navigation",
|
|
222
190
|
"ui_screen_regions",
|
|
223
191
|
"ui_components",
|
|
192
|
+
"ui_design",
|
|
224
193
|
"db_tables",
|
|
225
194
|
"db_columns",
|
|
226
195
|
"db_keys",
|