@topogram/cli 0.3.49 → 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/registry.js +1 -1
- 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/web/design-intent.js +308 -0
- package/src/generator/surfaces/web/react.js +35 -28
- package/src/generator/surfaces/web/sveltekit.js +28 -23
- package/src/generator/surfaces/web/vanilla.js +102 -36
package/CHANGELOG.md
CHANGED
|
@@ -53,11 +53,12 @@
|
|
|
53
53
|
## 0.3.11 - 2026-05-01
|
|
54
54
|
|
|
55
55
|
- Add `domain` statement kind for grouping the spec by business slice
|
|
56
|
-
(
|
|
57
|
-
`name`, `description`, `status`.
|
|
58
|
-
`
|
|
59
|
-
prefix, scope-list shapes,
|
|
60
|
-
refs, and parent-domain
|
|
56
|
+
(order fulfillment, billing, support, reporting, etc.). Identifier
|
|
57
|
+
prefix `dom_`. Required fields: `name`, `description`, `status`.
|
|
58
|
+
Optional: `in_scope`, `out_of_scope`, `owners`, `parent_domain`,
|
|
59
|
+
`aliases`. Validator enforces identifier prefix, scope-list shapes,
|
|
60
|
+
owner refs (`actor`|`role`), parent_domain refs, and parent-domain
|
|
61
|
+
cycle detection.
|
|
61
62
|
- Add optional singular `domain` field on `capability`, `entity`, `rule`,
|
|
62
63
|
`verification`, `orchestration`, `operation`, and `decision`. Cross-kind
|
|
63
64
|
validator hard-errors on unknown ids and wrong-kind references.
|
|
@@ -83,9 +84,9 @@
|
|
|
83
84
|
with the `domain` row and the optional-field paragraph;
|
|
84
85
|
`docs/topogram-workspace-layout.md` appends a "Domain organization"
|
|
85
86
|
subsection.
|
|
86
|
-
- New fixture
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
- New multi-domain fixture (3 domains, 10 capabilities, 12 entities, 4
|
|
88
|
+
cross-platform projections) and golden tests at
|
|
89
|
+
`engine/tests/active/domain-kind.test.js`.
|
|
89
90
|
|
|
90
91
|
### SDLC layer (Phase 2)
|
|
91
92
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ The active product workflow is authoring-to-generated-app. Engine development sh
|
|
|
6
6
|
|
|
7
7
|
## Package Shape
|
|
8
8
|
|
|
9
|
-
The engine is the publishable
|
|
9
|
+
The engine is the publishable CLI package:
|
|
10
10
|
|
|
11
11
|
```json
|
|
12
12
|
{
|
|
@@ -17,7 +17,7 @@ The engine is the publishable private CLI package:
|
|
|
17
17
|
}
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
This lets source checkouts and
|
|
20
|
+
This lets source checkouts and package consumers call:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
topogram new ../my-app
|
|
@@ -111,7 +111,7 @@ topogram new ../todo-demo --template @topogram/template-todo
|
|
|
111
111
|
topogram new ../todo-demo --template todo
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
Catalog aliases resolve through the
|
|
114
|
+
Catalog aliases resolve through the public catalog index at
|
|
115
115
|
`github:attebury/topograms/topograms.catalog.json`. The catalog is package
|
|
116
116
|
backed; executable starter content still lives in template packages. Use
|
|
117
117
|
`topogram catalog show <id>` to inspect an entry and get the correct `new` or
|
|
@@ -131,7 +131,7 @@ template update metadata while keeping normal check/generate behavior.
|
|
|
131
131
|
Do not create generated projects under `engine/`. The CLI refuses paths inside the engine directory.
|
|
132
132
|
|
|
133
133
|
Template pack authoring and trust policy are documented in `../docs/template-authoring.md`.
|
|
134
|
-
Catalog layout and private access are documented in `../docs/catalog.md`.
|
|
134
|
+
Catalog layout and optional private-source access are documented in `../docs/catalog.md`.
|
|
135
135
|
Projects created from executable templates include `.topogram-template-trust.json`;
|
|
136
136
|
regenerate it with `topogram trust template` after reviewing copied
|
|
137
137
|
`implementation/` code. Use `topogram template status` for the lifecycle
|
package/package.json
CHANGED
|
@@ -63,7 +63,7 @@ export const GENERATOR_MANIFESTS = [
|
|
|
63
63
|
inputs: ["ui-web-contract"],
|
|
64
64
|
outputs: ["web-app", "generation-coverage"],
|
|
65
65
|
stack: { runtime: "browser", framework: "vanilla", language: "javascript" },
|
|
66
|
-
capabilities: { routes: true, components: false, coverage:
|
|
66
|
+
capabilities: { routes: true, components: false, coverage: true },
|
|
67
67
|
componentSupport: { patterns: [], behaviors: [], unsupported: "contract-only" },
|
|
68
68
|
source: "bundled",
|
|
69
69
|
profile: "vanilla"
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
buildVerificationSummary,
|
|
18
18
|
getDefaultEnvironmentProjections,
|
|
19
19
|
resolveRuntimeTopology,
|
|
20
|
+
runtimeDemoUserId,
|
|
20
21
|
runtimePorts,
|
|
21
22
|
runtimeUrls
|
|
22
23
|
} from "./shared.js";
|
|
@@ -104,7 +105,7 @@ function buildAppBundlePlan(graph, options = {}) {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function renderAppBundleEnvExample(plan) {
|
|
107
|
-
const
|
|
108
|
+
const demoUserId = runtimeDemoUserId(plan.runtimeReference);
|
|
108
109
|
const databaseName = plan.runtimeReference.environment.databaseName || "topogram_app";
|
|
109
110
|
const topology = {
|
|
110
111
|
primaryApi: { port: plan.topology.components.find((component) => component.type === "api")?.port },
|
|
@@ -118,8 +119,8 @@ TOPOGRAM_ENVIRONMENT_PROFILE=${plan.profiles.environment}
|
|
|
118
119
|
TOPOGRAM_DEPLOY_PROFILE=${plan.profiles.deployment}
|
|
119
120
|
|
|
120
121
|
# Local runtime defaults
|
|
121
|
-
${plan.projections.api ? `SERVER_PORT=${ports.server}\n` : ""}${plan.projections.ui ? `WEB_PORT=${ports.web}\n` : ""}${plan.projections.api && plan.projections.ui ? `PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}PUBLIC_TOPOGRAM_DEMO_USER_ID=${
|
|
122
|
-
TOPOGRAM_DEMO_USER_ID=${
|
|
122
|
+
${plan.projections.api ? `SERVER_PORT=${ports.server}\n` : ""}${plan.projections.ui ? `WEB_PORT=${ports.web}\n` : ""}${plan.projections.api && plan.projections.ui ? `PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
123
|
+
TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
123
124
|
${plan.runtimeReference.environment.envExample || ""}
|
|
124
125
|
|
|
125
126
|
# Smoke-test defaults
|
|
@@ -135,8 +136,8 @@ SERVER_PORT=${ports.server}
|
|
|
135
136
|
WEB_PORT=${ports.web}
|
|
136
137
|
DATABASE_URL=file:./var/${databaseName}.sqlite
|
|
137
138
|
PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
|
|
138
|
-
PUBLIC_TOPOGRAM_DEMO_USER_ID=${
|
|
139
|
-
TOPOGRAM_DEMO_USER_ID=${
|
|
139
|
+
PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
140
|
+
TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
140
141
|
${plan.runtimeReference.environment.envExample || ""}
|
|
141
142
|
TOPOGRAM_SEED_DEMO=true
|
|
142
143
|
|
|
@@ -159,8 +160,8 @@ POSTGRES_PASSWORD=postgres
|
|
|
159
160
|
DATABASE_URL=postgresql://\${POSTGRES_USER}@localhost:5432/${databaseName}
|
|
160
161
|
DATABASE_ADMIN_URL=postgresql://\${POSTGRES_USER}@localhost:5432/postgres
|
|
161
162
|
PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
|
|
162
|
-
PUBLIC_TOPOGRAM_DEMO_USER_ID=${
|
|
163
|
-
TOPOGRAM_DEMO_USER_ID=${
|
|
163
|
+
PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
164
|
+
TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
164
165
|
${plan.runtimeReference.environment.envExample || ""}
|
|
165
166
|
TOPOGRAM_SEED_DEMO=true
|
|
166
167
|
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
generateWebBundle,
|
|
4
4
|
getDefaultEnvironmentProjections,
|
|
5
5
|
resolveRuntimeTopology,
|
|
6
|
+
runtimeDemoUserId,
|
|
6
7
|
runtimeUrls
|
|
7
8
|
} from "./shared.js";
|
|
8
9
|
import { getExampleImplementation } from "../../example-implementation.js";
|
|
@@ -75,13 +76,13 @@ function renderCompileCheckEnvExample(graph, options = {}) {
|
|
|
75
76
|
if (dbProjection?.platform === "db_sqlite") {
|
|
76
77
|
return `DATABASE_URL=./var/${runtimeReference.environment.databaseName || "topogram_app"}.sqlite
|
|
77
78
|
PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
|
|
78
|
-
PUBLIC_TOPOGRAM_DEMO_USER_ID=${runtimeReference
|
|
79
|
+
PUBLIC_TOPOGRAM_DEMO_USER_ID=${runtimeDemoUserId(runtimeReference)}
|
|
79
80
|
${runtimeReference.environment.envExample || ""}
|
|
80
81
|
`;
|
|
81
82
|
}
|
|
82
83
|
return `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/${runtimeReference.environment.databaseName || "topogram_app"}?schema=public
|
|
83
84
|
PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
|
|
84
|
-
PUBLIC_TOPOGRAM_DEMO_USER_ID=${runtimeReference
|
|
85
|
+
PUBLIC_TOPOGRAM_DEMO_USER_ID=${runtimeDemoUserId(runtimeReference)}
|
|
85
86
|
${runtimeReference.environment.envExample || ""}
|
|
86
87
|
`;
|
|
87
88
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
dbEnvVarsForComponent,
|
|
8
8
|
getDefaultEnvironmentProjections,
|
|
9
9
|
resolveRuntimeTopology,
|
|
10
|
+
runtimeDemoUserId,
|
|
10
11
|
runtimePorts,
|
|
11
12
|
runtimeUrls
|
|
12
13
|
} from "./shared.js";
|
|
@@ -158,7 +159,7 @@ function buildEnvironmentPlan(graph, options = {}) {
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
function renderEnvironmentEnvExample(plan) {
|
|
161
|
-
const
|
|
162
|
+
const demoUserId = runtimeDemoUserId(plan.runtimeReference);
|
|
162
163
|
const databaseName = plan.runtimeReference.environment.databaseName || "topogram_app";
|
|
163
164
|
const urls = runtimeUrls(plan.runtimeReference, {
|
|
164
165
|
primaryApi: { port: plan.ports.server },
|
|
@@ -184,8 +185,8 @@ function renderEnvironmentEnvExample(plan) {
|
|
|
184
185
|
TOPOGRAM_ENVIRONMENT_PROFILE=${plan.environment.profile}
|
|
185
186
|
|
|
186
187
|
# Local stack ports
|
|
187
|
-
${plan.components.apis.length ? `SERVER_PORT=${plan.ports.server}\n` : ""}${plan.components.webs.length ? `WEB_PORT=${plan.ports.web}\n` : ""}${plan.components.webs.length && plan.components.apis.length ? `PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}${plan.components.webs.length ? `TOPOGRAM_CORS_ORIGINS=${urls.web},http://127.0.0.1:${plan.ports.web}\n` : ""}PUBLIC_TOPOGRAM_DEMO_USER_ID=${
|
|
188
|
-
TOPOGRAM_DEMO_USER_ID=${
|
|
188
|
+
${plan.components.apis.length ? `SERVER_PORT=${plan.ports.server}\n` : ""}${plan.components.webs.length ? `WEB_PORT=${plan.ports.web}\n` : ""}${plan.components.webs.length && plan.components.apis.length ? `PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}${plan.components.webs.length ? `TOPOGRAM_CORS_ORIGINS=${urls.web},http://127.0.0.1:${plan.ports.web}\n` : ""}PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
189
|
+
TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
189
190
|
${plan.runtimeReference.environment.envExample || ""}
|
|
190
191
|
TOPOGRAM_SEED_DEMO=true
|
|
191
192
|
`;
|
|
@@ -204,8 +205,8 @@ WEB_PORT=${plan.ports.web}
|
|
|
204
205
|
DATABASE_URL=file:./var/${databaseName}.sqlite
|
|
205
206
|
${extraDatabaseLines ? `${extraDatabaseLines}\n` : ""}PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
|
|
206
207
|
TOPOGRAM_CORS_ORIGINS=${urls.web},http://127.0.0.1:${plan.ports.web}
|
|
207
|
-
PUBLIC_TOPOGRAM_DEMO_USER_ID=${
|
|
208
|
-
TOPOGRAM_DEMO_USER_ID=${
|
|
208
|
+
PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
209
|
+
TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
209
210
|
${plan.runtimeReference.environment.envExample || ""}
|
|
210
211
|
TOPOGRAM_SEED_DEMO=true
|
|
211
212
|
`;
|
|
@@ -229,8 +230,8 @@ DATABASE_URL=postgresql://\${POSTGRES_USER}@localhost:${plan.ports.database || 5
|
|
|
229
230
|
DATABASE_ADMIN_URL=postgresql://\${POSTGRES_USER}@localhost:${plan.ports.database || 5432}/postgres
|
|
230
231
|
${extraDatabaseLines ? `${extraDatabaseLines}\n` : ""}PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
|
|
231
232
|
TOPOGRAM_CORS_ORIGINS=${urls.web},http://127.0.0.1:${plan.ports.web}
|
|
232
|
-
PUBLIC_TOPOGRAM_DEMO_USER_ID=${
|
|
233
|
-
TOPOGRAM_DEMO_USER_ID=${
|
|
233
|
+
PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
234
|
+
TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
234
235
|
${plan.runtimeReference.environment.envExample || ""}
|
|
235
236
|
TOPOGRAM_SEED_DEMO=true
|
|
236
237
|
`;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
buildVerificationSummary,
|
|
4
4
|
getDefaultEnvironmentProjections,
|
|
5
5
|
resolveRuntimeTopology,
|
|
6
|
+
runtimeDemoUserId,
|
|
6
7
|
selectChecksByVerification,
|
|
7
8
|
runtimePorts,
|
|
8
9
|
runtimeUrls
|
|
@@ -74,11 +75,10 @@ function buildRuntimeCheckPlan(graph, options = {}) {
|
|
|
74
75
|
|
|
75
76
|
function renderRuntimeCheckEnvExample(graph, options = {}) {
|
|
76
77
|
const runtimeReference = getExampleImplementation(graph, options).runtime.reference;
|
|
77
|
-
const demo = runtimeReference.demoEnv;
|
|
78
78
|
const urls = runtimeUrls(runtimeReference, resolveRuntimeTopology(graph, options));
|
|
79
79
|
return `TOPOGRAM_API_BASE_URL=${urls.api}
|
|
80
80
|
TOPOGRAM_WEB_BASE_URL=${urls.web}
|
|
81
|
-
TOPOGRAM_DEMO_USER_ID=${
|
|
81
|
+
TOPOGRAM_DEMO_USER_ID=${runtimeDemoUserId(runtimeReference)}
|
|
82
82
|
${runtimeReference.environment.envExample || ""}
|
|
83
83
|
`;
|
|
84
84
|
}
|
|
@@ -166,9 +166,9 @@ function envValue(name) {
|
|
|
166
166
|
const fallbackMap = {
|
|
167
167
|
TOPOGRAM_API_BASE_URL: process.env.PUBLIC_TOPOGRAM_API_BASE_URL || "http://localhost:${ports.server}",
|
|
168
168
|
TOPOGRAM_WEB_BASE_URL: process.env.PUBLIC_TOPOGRAM_WEB_BASE_URL || \`http://localhost:\${process.env.WEB_PORT || "${ports.web}"}\`,
|
|
169
|
-
${runtimeReference.runtimeCheck.demoContainerEnvVar}: process.env.PUBLIC_TOPOGRAM_DEMO_CONTAINER_ID ||
|
|
170
|
-
${runtimeReference.runtimeCheck.demoPrimaryEnvVar}: process.env.PUBLIC_TOPOGRAM_DEMO_PRIMARY_ID ||
|
|
171
|
-
TOPOGRAM_DEMO_USER_ID: process.env.PUBLIC_TOPOGRAM_DEMO_USER_ID || "",
|
|
169
|
+
${runtimeReference.runtimeCheck.demoContainerEnvVar}: process.env.PUBLIC_TOPOGRAM_DEMO_CONTAINER_ID || "",
|
|
170
|
+
${runtimeReference.runtimeCheck.demoPrimaryEnvVar}: process.env.PUBLIC_TOPOGRAM_DEMO_PRIMARY_ID || "",
|
|
171
|
+
TOPOGRAM_DEMO_USER_ID: process.env.PUBLIC_TOPOGRAM_AUTH_USER_ID || process.env.PUBLIC_TOPOGRAM_DEMO_USER_ID || "",
|
|
172
172
|
TOPOGRAM_AUTH_USER_ID: process.env.TOPOGRAM_DEMO_USER_ID || ""
|
|
173
173
|
};
|
|
174
174
|
|
|
@@ -513,3 +513,12 @@ export function runtimeUrls(runtimeReference, topology = null) {
|
|
|
513
513
|
web: `http://localhost:${ports.web}`
|
|
514
514
|
};
|
|
515
515
|
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* @param {Record<string, any>|null|undefined} runtimeReference
|
|
519
|
+
* @returns {string}
|
|
520
|
+
*/
|
|
521
|
+
export function runtimeDemoUserId(runtimeReference) {
|
|
522
|
+
const demo = runtimeReference?.demoEnv || {};
|
|
523
|
+
return demo.userId || demo.memberId || demo.ownerId || demo.primaryActorId || "";
|
|
524
|
+
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
buildVerificationSummary,
|
|
3
3
|
getDefaultEnvironmentProjections,
|
|
4
4
|
resolveRuntimeTopology,
|
|
5
|
+
runtimeDemoUserId,
|
|
5
6
|
runtimeUrls,
|
|
6
7
|
selectChecksByVerification
|
|
7
8
|
} from "./shared.js";
|
|
@@ -126,7 +127,7 @@ process.on("unhandledRejection", reportFatal);
|
|
|
126
127
|
const apiBase = process.env.TOPOGRAM_API_BASE_URL || "";
|
|
127
128
|
const webBase = process.env.TOPOGRAM_WEB_BASE_URL || "";
|
|
128
129
|
const demoContainerId = process.env.${runtimeReference.smoke.defaultContainerEnvVar} || "${runtimeReference.demoEnv.containerId}";
|
|
129
|
-
const demoUserId = process.env.TOPOGRAM_DEMO_USER_ID || "${runtimeReference
|
|
130
|
+
const demoUserId = process.env.TOPOGRAM_AUTH_USER_ID || process.env.TOPOGRAM_DEMO_USER_ID || "${runtimeDemoUserId(runtimeReference)}";
|
|
130
131
|
const authToken = process.env.TOPOGRAM_AUTH_TOKEN || "";
|
|
131
132
|
|
|
132
133
|
if (!apiBase || !webBase) {
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
const DEFAULT_DESIGN_INTENT = Object.freeze({
|
|
4
|
+
density: "comfortable",
|
|
5
|
+
tone: "neutral",
|
|
6
|
+
radiusScale: "medium",
|
|
7
|
+
colorRoles: Object.freeze({
|
|
8
|
+
primary: "accent"
|
|
9
|
+
}),
|
|
10
|
+
typographyRoles: Object.freeze({
|
|
11
|
+
body: "readable",
|
|
12
|
+
heading: "prominent"
|
|
13
|
+
}),
|
|
14
|
+
actionRoles: Object.freeze({
|
|
15
|
+
primary: "prominent"
|
|
16
|
+
}),
|
|
17
|
+
accessibility: Object.freeze({
|
|
18
|
+
contrast: "aa",
|
|
19
|
+
focus: "visible"
|
|
20
|
+
})
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const DENSITY_VALUES = {
|
|
24
|
+
compact: {
|
|
25
|
+
spaceUnit: "0.75rem",
|
|
26
|
+
pagePadding: "1.5rem 1rem 3rem",
|
|
27
|
+
controlPadding: "0.55rem 0.75rem"
|
|
28
|
+
},
|
|
29
|
+
comfortable: {
|
|
30
|
+
spaceUnit: "1rem",
|
|
31
|
+
pagePadding: "2rem 1.25rem 4rem",
|
|
32
|
+
controlPadding: "0.7rem 1rem"
|
|
33
|
+
},
|
|
34
|
+
spacious: {
|
|
35
|
+
spaceUnit: "1.25rem",
|
|
36
|
+
pagePadding: "2.5rem 1.5rem 5rem",
|
|
37
|
+
controlPadding: "0.85rem 1.15rem"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const RADIUS_VALUES = {
|
|
42
|
+
none: {
|
|
43
|
+
card: "0",
|
|
44
|
+
control: "0",
|
|
45
|
+
pill: "0"
|
|
46
|
+
},
|
|
47
|
+
small: {
|
|
48
|
+
card: "8px",
|
|
49
|
+
control: "8px",
|
|
50
|
+
pill: "999px"
|
|
51
|
+
},
|
|
52
|
+
medium: {
|
|
53
|
+
card: "14px",
|
|
54
|
+
control: "12px",
|
|
55
|
+
pill: "999px"
|
|
56
|
+
},
|
|
57
|
+
large: {
|
|
58
|
+
card: "18px",
|
|
59
|
+
control: "16px",
|
|
60
|
+
pill: "999px"
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const COLOR_VALUES = {
|
|
65
|
+
accent: "#0f5cc0",
|
|
66
|
+
critical: "#b42318",
|
|
67
|
+
danger: "#b42318",
|
|
68
|
+
success: "#027a48",
|
|
69
|
+
warning: "#b54708",
|
|
70
|
+
neutral: "#516173",
|
|
71
|
+
muted: "#607284"
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const TONE_VALUES = {
|
|
75
|
+
neutral: {
|
|
76
|
+
text: "#182026",
|
|
77
|
+
muted: "#607284",
|
|
78
|
+
background: "linear-gradient(180deg, #f5f7fb 0%, #edf2f7 100%)",
|
|
79
|
+
surface: "#ffffff",
|
|
80
|
+
surfaceSubtle: "#fbfcfe",
|
|
81
|
+
border: "#d7e1ec"
|
|
82
|
+
},
|
|
83
|
+
operational: {
|
|
84
|
+
text: "#182026",
|
|
85
|
+
muted: "#607284",
|
|
86
|
+
background: "linear-gradient(180deg, #f5f7fb 0%, #edf2f7 100%)",
|
|
87
|
+
surface: "#ffffff",
|
|
88
|
+
surfaceSubtle: "#fbfcfe",
|
|
89
|
+
border: "#d7e1ec"
|
|
90
|
+
},
|
|
91
|
+
editorial: {
|
|
92
|
+
text: "#1f2933",
|
|
93
|
+
muted: "#5c6670",
|
|
94
|
+
background: "linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%)",
|
|
95
|
+
surface: "#ffffff",
|
|
96
|
+
surfaceSubtle: "#f8fafc",
|
|
97
|
+
border: "#d8dee8"
|
|
98
|
+
},
|
|
99
|
+
playful: {
|
|
100
|
+
text: "#1f2937",
|
|
101
|
+
muted: "#5b6472",
|
|
102
|
+
background: "linear-gradient(180deg, #f7fbff 0%, #eef6ff 100%)",
|
|
103
|
+
surface: "#ffffff",
|
|
104
|
+
surfaceSubtle: "#f7fbff",
|
|
105
|
+
border: "#d6e4f5"
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string|null|undefined} value
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
function cssToken(value) {
|
|
114
|
+
return String(value || "default").replace(/[^A-Za-z0-9_-]/g, "_");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {Record<string, string>|null|undefined} source
|
|
119
|
+
* @param {Record<string, string>} fallback
|
|
120
|
+
* @returns {Record<string, string>}
|
|
121
|
+
*/
|
|
122
|
+
function mergeStringMap(source, fallback) {
|
|
123
|
+
return {
|
|
124
|
+
...fallback,
|
|
125
|
+
...(source && typeof source === "object" ? source : {})
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {any} design
|
|
131
|
+
* @returns {{
|
|
132
|
+
* density: string,
|
|
133
|
+
* tone: string,
|
|
134
|
+
* radiusScale: string,
|
|
135
|
+
* colorRoles: Record<string, string>,
|
|
136
|
+
* typographyRoles: Record<string, string>,
|
|
137
|
+
* actionRoles: Record<string, string>,
|
|
138
|
+
* accessibility: Record<string, string>
|
|
139
|
+
* }}
|
|
140
|
+
*/
|
|
141
|
+
export function normalizeDesignIntent(design) {
|
|
142
|
+
const value = design && typeof design === "object" ? design : {};
|
|
143
|
+
return {
|
|
144
|
+
density: typeof value.density === "string" ? value.density : DEFAULT_DESIGN_INTENT.density,
|
|
145
|
+
tone: typeof value.tone === "string" ? value.tone : DEFAULT_DESIGN_INTENT.tone,
|
|
146
|
+
radiusScale: typeof value.radiusScale === "string" ? value.radiusScale : DEFAULT_DESIGN_INTENT.radiusScale,
|
|
147
|
+
colorRoles: mergeStringMap(value.colorRoles, DEFAULT_DESIGN_INTENT.colorRoles),
|
|
148
|
+
typographyRoles: mergeStringMap(value.typographyRoles, DEFAULT_DESIGN_INTENT.typographyRoles),
|
|
149
|
+
actionRoles: mergeStringMap(value.actionRoles, DEFAULT_DESIGN_INTENT.actionRoles),
|
|
150
|
+
accessibility: mergeStringMap(value.accessibility, DEFAULT_DESIGN_INTENT.accessibility)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {Record<string, string>} map
|
|
156
|
+
* @param {string} prefix
|
|
157
|
+
* @returns {string[]}
|
|
158
|
+
*/
|
|
159
|
+
function tokenMapLines(map, prefix) {
|
|
160
|
+
return Object.entries(map)
|
|
161
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
162
|
+
.map(([role, value]) => ` --topogram-design-${prefix}-${cssToken(role)}: ${cssToken(value)};`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {any} design
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
export function renderDesignIntentCss(design) {
|
|
170
|
+
const normalized = normalizeDesignIntent(design);
|
|
171
|
+
const tone = TONE_VALUES[normalized.tone] || TONE_VALUES.neutral;
|
|
172
|
+
const density = DENSITY_VALUES[normalized.density] || DENSITY_VALUES.comfortable;
|
|
173
|
+
const radius = RADIUS_VALUES[normalized.radiusScale] || RADIUS_VALUES.medium;
|
|
174
|
+
const primaryColor = COLOR_VALUES[normalized.colorRoles.primary] || COLOR_VALUES.accent;
|
|
175
|
+
const dangerColor = COLOR_VALUES[normalized.colorRoles.danger] || COLOR_VALUES.critical;
|
|
176
|
+
const focusColor = primaryColor;
|
|
177
|
+
|
|
178
|
+
return `/* Topogram semantic design intent. Generators map normalized UI tokens to stack CSS here. */
|
|
179
|
+
:root {
|
|
180
|
+
--topogram-design-density: ${cssToken(normalized.density)};
|
|
181
|
+
--topogram-design-tone: ${cssToken(normalized.tone)};
|
|
182
|
+
--topogram-design-radius-scale: ${cssToken(normalized.radiusScale)};
|
|
183
|
+
${tokenMapLines(normalized.colorRoles, "color").join("\n")}
|
|
184
|
+
${tokenMapLines(normalized.typographyRoles, "typography").join("\n")}
|
|
185
|
+
${tokenMapLines(normalized.actionRoles, "action").join("\n")}
|
|
186
|
+
${tokenMapLines(normalized.accessibility, "accessibility").join("\n")}
|
|
187
|
+
--topogram-space-unit: ${density.spaceUnit};
|
|
188
|
+
--topogram-page-padding: ${density.pagePadding};
|
|
189
|
+
--topogram-control-padding: ${density.controlPadding};
|
|
190
|
+
--topogram-radius-card: ${radius.card};
|
|
191
|
+
--topogram-radius-control: ${radius.control};
|
|
192
|
+
--topogram-radius-pill: ${radius.pill};
|
|
193
|
+
--topogram-text-color: ${tone.text};
|
|
194
|
+
--topogram-muted-color: ${tone.muted};
|
|
195
|
+
--topogram-surface-background: ${tone.background};
|
|
196
|
+
--topogram-surface-card: ${tone.surface};
|
|
197
|
+
--topogram-surface-subtle: ${tone.surfaceSubtle};
|
|
198
|
+
--topogram-border-color: ${tone.border};
|
|
199
|
+
--topogram-action-primary-background: ${primaryColor};
|
|
200
|
+
--topogram-action-primary-color: #ffffff;
|
|
201
|
+
--topogram-action-danger-background: ${dangerColor};
|
|
202
|
+
--topogram-focus-outline: 3px solid ${focusColor};
|
|
203
|
+
}
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {ReturnType<typeof normalizeDesignIntent>} design
|
|
209
|
+
* @returns {Array<{ category: string, role: string|null, value: string, marker: string }>}
|
|
210
|
+
*/
|
|
211
|
+
function requiredDesignMarkers(design) {
|
|
212
|
+
return [
|
|
213
|
+
{
|
|
214
|
+
category: "density",
|
|
215
|
+
role: null,
|
|
216
|
+
value: design.density,
|
|
217
|
+
marker: "--topogram-design-density"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
category: "tone",
|
|
221
|
+
role: null,
|
|
222
|
+
value: design.tone,
|
|
223
|
+
marker: "--topogram-design-tone"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
category: "radius_scale",
|
|
227
|
+
role: null,
|
|
228
|
+
value: design.radiusScale,
|
|
229
|
+
marker: "--topogram-design-radius-scale"
|
|
230
|
+
},
|
|
231
|
+
...Object.entries(design.colorRoles).map(([role, value]) => ({
|
|
232
|
+
category: "color_roles",
|
|
233
|
+
role,
|
|
234
|
+
value,
|
|
235
|
+
marker: `--topogram-design-color-${cssToken(role)}`
|
|
236
|
+
})),
|
|
237
|
+
...Object.entries(design.typographyRoles).map(([role, value]) => ({
|
|
238
|
+
category: "typography_roles",
|
|
239
|
+
role,
|
|
240
|
+
value,
|
|
241
|
+
marker: `--topogram-design-typography-${cssToken(role)}`
|
|
242
|
+
})),
|
|
243
|
+
...Object.entries(design.actionRoles).map(([role, value]) => ({
|
|
244
|
+
category: "action_roles",
|
|
245
|
+
role,
|
|
246
|
+
value,
|
|
247
|
+
marker: `--topogram-design-action-${cssToken(role)}`
|
|
248
|
+
})),
|
|
249
|
+
...Object.entries(design.accessibility).map(([role, value]) => ({
|
|
250
|
+
category: "accessibility",
|
|
251
|
+
role,
|
|
252
|
+
value,
|
|
253
|
+
marker: `--topogram-design-accessibility-${cssToken(role)}`
|
|
254
|
+
}))
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @param {any} contract
|
|
260
|
+
* @param {Record<string, string>} files
|
|
261
|
+
* @param {string} cssPath
|
|
262
|
+
* @returns {{ coverage: any, diagnostics: any[] }}
|
|
263
|
+
*/
|
|
264
|
+
export function buildDesignIntentCoverage(contract, files, cssPath) {
|
|
265
|
+
const design = normalizeDesignIntent(contract?.design);
|
|
266
|
+
const css = files[cssPath] || "";
|
|
267
|
+
const markers = requiredDesignMarkers(design);
|
|
268
|
+
const mapped = markers.filter((item) => css.includes(item.marker));
|
|
269
|
+
const missing = markers.filter((item) => !css.includes(item.marker));
|
|
270
|
+
const coverage = {
|
|
271
|
+
status: missing.length === 0 ? "mapped" : "unmapped",
|
|
272
|
+
css_path: cssPath,
|
|
273
|
+
tokens: {
|
|
274
|
+
density: design.density,
|
|
275
|
+
tone: design.tone,
|
|
276
|
+
radius_scale: design.radiusScale,
|
|
277
|
+
color_roles: design.colorRoles,
|
|
278
|
+
typography_roles: design.typographyRoles,
|
|
279
|
+
action_roles: design.actionRoles,
|
|
280
|
+
accessibility: design.accessibility
|
|
281
|
+
},
|
|
282
|
+
mapped: mapped.map((item) => ({
|
|
283
|
+
category: item.category,
|
|
284
|
+
role: item.role,
|
|
285
|
+
value: item.value,
|
|
286
|
+
marker: item.marker
|
|
287
|
+
})),
|
|
288
|
+
missing: missing.map((item) => ({
|
|
289
|
+
category: item.category,
|
|
290
|
+
role: item.role,
|
|
291
|
+
value: item.value,
|
|
292
|
+
marker: item.marker
|
|
293
|
+
}))
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
coverage,
|
|
297
|
+
diagnostics: missing.map((item) => ({
|
|
298
|
+
code: "design_intent_not_mapped",
|
|
299
|
+
severity: "error",
|
|
300
|
+
category: item.category,
|
|
301
|
+
role: item.role,
|
|
302
|
+
value: item.value,
|
|
303
|
+
marker: item.marker,
|
|
304
|
+
message: `UI design intent token '${item.category}${item.role ? `.${item.role}` : ""}' was not mapped into ${cssPath}.`,
|
|
305
|
+
suggested_fix: "Render Topogram semantic design variables with renderDesignIntentCss before writing the web stylesheet."
|
|
306
|
+
}))
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
reactComponentUsageSupport,
|
|
5
5
|
renderReactComponentRegion
|
|
6
6
|
} from "./react-components.js";
|
|
7
|
+
import { buildDesignIntentCoverage, renderDesignIntentCss } from "./design-intent.js";
|
|
7
8
|
import { renderApiClientModule, renderLookupModule, renderVisibilityModule } from "./shared.js";
|
|
8
9
|
|
|
9
10
|
function componentNameForScreen(screenId) {
|
|
@@ -197,6 +198,8 @@ function screenPagePath(screen) {
|
|
|
197
198
|
|
|
198
199
|
function buildReactGenerationCoverage(contract, files, routeScreens) {
|
|
199
200
|
const diagnostics = [];
|
|
201
|
+
const designIntent = buildDesignIntentCoverage(contract, files, "src/app.css");
|
|
202
|
+
diagnostics.push(...designIntent.diagnostics);
|
|
200
203
|
const routeScreenIds = new Set(routeScreens.map((screen) => screen.id));
|
|
201
204
|
const screens = (contract.screens || [])
|
|
202
205
|
.filter((screen) => routeScreenIds.has(screen.id))
|
|
@@ -286,6 +289,7 @@ function buildReactGenerationCoverage(contract, files, routeScreens) {
|
|
|
286
289
|
errors: diagnostics.filter((diagnostic) => diagnostic.severity === "error").length,
|
|
287
290
|
warnings: diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length
|
|
288
291
|
},
|
|
292
|
+
design_intent: designIntent.coverage,
|
|
289
293
|
screens,
|
|
290
294
|
diagnostics
|
|
291
295
|
};
|
|
@@ -477,42 +481,45 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
|
477
481
|
);
|
|
478
482
|
`;
|
|
479
483
|
files["src/vite-env.d.ts"] = `/// <reference types="vite/client" />\n`;
|
|
480
|
-
files["src/app.css"] =
|
|
484
|
+
files["src/app.css"] = `${renderDesignIntentCss(contract.design)}
|
|
485
|
+
|
|
486
|
+
:root {
|
|
481
487
|
font-family: system-ui, sans-serif;
|
|
482
|
-
color:
|
|
483
|
-
background:
|
|
488
|
+
color: var(--topogram-text-color);
|
|
489
|
+
background: var(--topogram-surface-background);
|
|
484
490
|
}
|
|
485
491
|
body { margin: 0; }
|
|
486
|
-
a { color:
|
|
492
|
+
a { color: var(--topogram-action-primary-background); text-decoration: none; }
|
|
487
493
|
a:hover { text-decoration: underline; }
|
|
488
|
-
main { max-width: 72rem; margin: 0 auto; padding:
|
|
494
|
+
main { max-width: 72rem; margin: 0 auto; padding: var(--topogram-page-padding); }
|
|
489
495
|
.app-shell { min-height: 100vh; }
|
|
490
496
|
.app-workspace { display: grid; grid-template-columns: 18rem minmax(0, 1fr); min-height: 100vh; }
|
|
491
497
|
.app-main-shell { min-width: 0; }
|
|
492
|
-
.app-sidebar { position: sticky; top: 0; align-self: start; min-height: 100vh; display: grid; align-content: start; gap:
|
|
493
|
-
.app-nav { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap:
|
|
498
|
+
.app-sidebar { position: sticky; top: 0; align-self: start; min-height: 100vh; display: grid; align-content: start; gap: var(--topogram-space-unit); padding: 1.25rem 1rem; border-right: 1px solid rgba(24, 32, 38, 0.08); background: rgba(255, 255, 255, 0.86); backdrop-filter: blur(12px); }
|
|
499
|
+
.app-nav { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: var(--topogram-space-unit); padding: 1rem 1.25rem; border-bottom: 1px solid rgba(24, 32, 38, 0.08); background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(12px); }
|
|
494
500
|
.app-nav-links, .app-nav nav, .app-tabbar { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
|
495
501
|
.app-nav.menu-bar { border-bottom-style: dashed; }
|
|
496
502
|
.app-nav.compact { justify-content: flex-end; }
|
|
497
503
|
.app-tabbar { position: sticky; bottom: 0; z-index: 10; justify-content: space-around; padding: 0.85rem 1rem calc(0.85rem + env(safe-area-inset-bottom, 0px)); border-top: 1px solid rgba(24, 32, 38, 0.08); background: rgba(255, 255, 255, 0.92); backdrop-filter: blur(12px); }
|
|
498
504
|
.brand { font-weight: 700; letter-spacing: 0.01em; }
|
|
499
|
-
.brand-mark { font-weight: 700; color:
|
|
500
|
-
.command-palette-button { background:
|
|
501
|
-
.app-footer { max-width: 72rem; margin: 0 auto; padding: 0 1.25rem 2rem; color:
|
|
502
|
-
.card { background:
|
|
503
|
-
.hero, .stack, .grid, .filters, .
|
|
505
|
+
.brand-mark { font-weight: 700; color: var(--topogram-muted-color); }
|
|
506
|
+
.command-palette-button { background: var(--topogram-text-color); color: white; border: none; border-radius: var(--topogram-radius-pill); padding: var(--topogram-control-padding); font: inherit; cursor: pointer; }
|
|
507
|
+
.app-footer { max-width: 72rem; margin: 0 auto; padding: 0 1.25rem 2rem; color: var(--topogram-muted-color); }
|
|
508
|
+
.card { background: var(--topogram-surface-card); border-radius: var(--topogram-radius-card); padding: 1.25rem; box-shadow: 0 12px 30px rgba(24, 32, 38, 0.08); }
|
|
509
|
+
.hero, .stack, .grid, .filters, .resource-meta, .definition-list { display: grid; gap: var(--topogram-space-unit); }
|
|
504
510
|
.grid.two { grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); }
|
|
505
511
|
.filters { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); margin: 1rem 0 1.25rem; }
|
|
506
512
|
label { display: grid; gap: 0.35rem; font-size: 0.95rem; }
|
|
507
513
|
input, textarea, button, select { font: inherit; }
|
|
508
|
-
input, textarea, select { width: 100%; box-sizing: border-box; border: 1px solid #c9d4e2; border-radius:
|
|
514
|
+
input, textarea, select { width: 100%; box-sizing: border-box; border: 1px solid #c9d4e2; border-radius: var(--topogram-radius-control); padding: var(--topogram-control-padding); background: white; }
|
|
509
515
|
textarea { min-height: 8rem; resize: vertical; }
|
|
510
|
-
button, .button-link { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; border: none; border-radius:
|
|
511
|
-
.button-link
|
|
516
|
+
button, .button-link { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; border: none; border-radius: var(--topogram-radius-pill); padding: var(--topogram-control-padding); background: var(--topogram-action-primary-background); color: var(--topogram-action-primary-color); font-weight: 600; cursor: pointer; }
|
|
517
|
+
button:focus-visible, .button-link:focus-visible, a:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { outline: var(--topogram-focus-outline); outline-offset: 2px; }
|
|
518
|
+
.button-link.secondary { background: #e9eef6; color: var(--topogram-text-color); }
|
|
512
519
|
.button-row { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; }
|
|
513
|
-
.
|
|
514
|
-
.
|
|
515
|
-
.table-wrap { margin-top: 1rem; overflow-x: auto; border: 1px solid
|
|
520
|
+
.resource-list { list-style: none; padding: 0; margin: 1rem 0 0; display: grid; gap: 0.75rem; }
|
|
521
|
+
.resource-list li { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--topogram-space-unit); padding: 1rem; border: 1px solid #e0e8f1; border-radius: var(--topogram-radius-card); background: var(--topogram-surface-subtle); }
|
|
522
|
+
.table-wrap { margin-top: 1rem; overflow-x: auto; border: 1px solid var(--topogram-border-color); border-radius: var(--topogram-radius-card); background: white; }
|
|
516
523
|
.resource-table { width: 100%; border-collapse: collapse; min-width: 42rem; }
|
|
517
524
|
.resource-table th, .resource-table td { padding: 0.85rem 1rem; text-align: left; border-bottom: 1px solid #e7edf5; vertical-align: top; }
|
|
518
525
|
.resource-table th { font-size: 0.85rem; letter-spacing: 0.04em; text-transform: uppercase; color: #516173; background: #f8fbff; }
|
|
@@ -521,28 +528,28 @@ button, .button-link { display: inline-flex; align-items: center; justify-conten
|
|
|
521
528
|
.data-grid thead th { position: sticky; top: 0; z-index: 1; background: #eef5ff; }
|
|
522
529
|
.data-grid-shell { box-shadow: inset 0 0 0 1px rgba(15, 92, 192, 0.04); }
|
|
523
530
|
.cell-stack { display: grid; gap: 0.35rem; }
|
|
524
|
-
.cell-secondary { color:
|
|
531
|
+
.cell-secondary { color: var(--topogram-muted-color); font-size: 0.92rem; }
|
|
525
532
|
.definition-list { grid-template-columns: minmax(8rem, 12rem) 1fr; align-items: start; }
|
|
526
533
|
.definition-list dt { font-weight: 600; color: #516173; }
|
|
527
534
|
.definition-list dd { margin: 0; }
|
|
528
|
-
.badge { display: inline-flex; align-items: center; padding: 0.25rem 0.6rem; border-radius:
|
|
529
|
-
.muted { color:
|
|
535
|
+
.badge { display: inline-flex; align-items: center; padding: 0.25rem 0.6rem; border-radius: var(--topogram-radius-pill); background: #eef4ff; color: var(--topogram-action-primary-background); font-size: 0.85rem; font-weight: 600; }
|
|
536
|
+
.muted { color: var(--topogram-muted-color); }
|
|
530
537
|
.empty-state { padding: 1rem 0; }
|
|
531
538
|
.error-text { color: #b42318; }
|
|
532
|
-
.component-card { border: 1px solid
|
|
533
|
-
.component-header { display: flex; align-items: center; justify-content: space-between; gap:
|
|
534
|
-
.component-eyebrow { margin: 0 0 0.25rem; color:
|
|
539
|
+
.component-card { border: 1px solid var(--topogram-border-color); border-radius: var(--topogram-radius-card); background: var(--topogram-surface-subtle); padding: 1rem; margin-top: 1rem; }
|
|
540
|
+
.component-header { display: flex; align-items: center; justify-content: space-between; gap: var(--topogram-space-unit); flex-wrap: wrap; }
|
|
541
|
+
.component-eyebrow { margin: 0 0 0.25rem; color: var(--topogram-muted-color); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
535
542
|
.component-card h2, .component-card h3 { margin: 0; }
|
|
536
543
|
.component-table-wrap { margin-top: 1rem; }
|
|
537
544
|
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: 0.75rem; }
|
|
538
|
-
.summary-grid div, .board-column { border: 1px solid #e0e8f1; border-radius:
|
|
545
|
+
.summary-grid div, .board-column { border: 1px solid #e0e8f1; border-radius: var(--topogram-radius-control); background: white; padding: 0.85rem; }
|
|
539
546
|
.summary-grid strong { display: block; font-size: 1.5rem; }
|
|
540
|
-
.summary-grid span, .calendar-list span { color:
|
|
547
|
+
.summary-grid span, .calendar-list span { color: var(--topogram-muted-color); font-size: 0.9rem; }
|
|
541
548
|
.board-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: 0.75rem; margin-top: 1rem; }
|
|
542
|
-
.board-card, .calendar-card { display: grid; gap: 0.25rem; border: 1px solid #e0e8f1; border-radius:
|
|
549
|
+
.board-card, .calendar-card { display: grid; gap: 0.25rem; border: 1px solid #e0e8f1; border-radius: var(--topogram-radius-control); background: #f8fbff; padding: 0.75rem; }
|
|
543
550
|
.calendar-list { display: grid; gap: 0.75rem; margin-top: 1rem; }
|
|
544
551
|
@media (max-width: 900px) { .app-workspace { grid-template-columns: 1fr; } .app-sidebar { position: static; min-height: auto; border-right: none; border-bottom: 1px solid rgba(24, 32, 38, 0.08); } }
|
|
545
|
-
@media (max-width: 640px) { .definition-list { grid-template-columns: 1fr; } .
|
|
552
|
+
@media (max-width: 640px) { .definition-list { grid-template-columns: 1fr; } .resource-list li { flex-direction: column; } .resource-table { min-width: 36rem; } .app-nav { flex-wrap: wrap; } }
|
|
546
553
|
`;
|
|
547
554
|
files["src/App.tsx"] = buildAppTsx(contract, webReferenceWithDefaults);
|
|
548
555
|
files["src/lib/topogram/api-contracts.json"] = `${JSON.stringify(realization.apiContracts, null, 2)}\n`;
|
|
@@ -2,6 +2,7 @@ import { buildWebRealization } from "../../../realization/ui/index.js";
|
|
|
2
2
|
import { lookupRouteSegment } from "../services/runtime-helpers.js";
|
|
3
3
|
import { getExampleImplementation } from "../../../example-implementation.js";
|
|
4
4
|
import { renderApiClientModule, renderLookupModule, renderVisibilityModule } from "./shared.js";
|
|
5
|
+
import { buildDesignIntentCoverage, renderDesignIntentCss } from "./design-intent.js";
|
|
5
6
|
import {
|
|
6
7
|
renderSvelteKitComponentRegion,
|
|
7
8
|
svelteKitComponentUsageSupport
|
|
@@ -165,6 +166,8 @@ ${renderedRegions || ` ${defaultCollection}`}
|
|
|
165
166
|
|
|
166
167
|
function buildSvelteKitGenerationCoverage(contract, files, implementationScreenIds) {
|
|
167
168
|
const diagnostics = [];
|
|
169
|
+
const designIntent = buildDesignIntentCoverage(contract, files, "src/app.css");
|
|
170
|
+
diagnostics.push(...designIntent.diagnostics);
|
|
168
171
|
const screens = (contract.screens || [])
|
|
169
172
|
.filter((screen) => Boolean(screen.route) && screen.route !== "/")
|
|
170
173
|
.map((screen) => {
|
|
@@ -258,6 +261,7 @@ function buildSvelteKitGenerationCoverage(contract, files, implementationScreenI
|
|
|
258
261
|
errors: diagnostics.filter((diagnostic) => diagnostic.severity === "error").length,
|
|
259
262
|
warnings: diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length
|
|
260
263
|
},
|
|
264
|
+
design_intent: designIntent.coverage,
|
|
261
265
|
screens,
|
|
262
266
|
diagnostics
|
|
263
267
|
};
|
|
@@ -319,9 +323,9 @@ function buildSvelteKitScaffold(contract, apiContracts, options = {}) {
|
|
|
319
323
|
const navigationPatterns = (contract.navigation?.patterns || []).join(" ");
|
|
320
324
|
const hasCommandPalette = (contract.navigation?.patterns || []).includes("command_palette");
|
|
321
325
|
const homeDescription = webReference.home.heroDescriptionTemplate.replace("PROFILE", `\`${profile}\``);
|
|
322
|
-
const
|
|
323
|
-
const ownerEnvVar = webReference.createPrimary.defaultAssigneeEnvVar;
|
|
324
|
-
const
|
|
326
|
+
const demoPrimaryEnvVar = webReference.home.demoPrimaryEnvVar;
|
|
327
|
+
const ownerEnvVar = webReference.createPrimary.defaultOwnerEnvVar || webReference.createPrimary.defaultAssigneeEnvVar;
|
|
328
|
+
const containerEnvVar = webReference.createPrimary.defaultContainerEnvVar;
|
|
325
329
|
files["package.json"] = JSON.stringify(
|
|
326
330
|
{
|
|
327
331
|
name: contract.projection.id,
|
|
@@ -370,7 +374,8 @@ function buildSvelteKitScaffold(contract, apiContracts, options = {}) {
|
|
|
370
374
|
files["src/app.html"] =
|
|
371
375
|
"<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n %sveltekit.head%\n </head>\n <body data-sveltekit-preload-data=\"hover\">\n <div style=\"display: contents\">%sveltekit.body%</div>\n </body>\n</html>\n";
|
|
372
376
|
files["src/app.css"] =
|
|
373
|
-
|
|
377
|
+
`${renderDesignIntentCss(contract.design)}\n` +
|
|
378
|
+
":root {\n font-family: system-ui, sans-serif;\n color: var(--topogram-text-color);\n background: var(--topogram-surface-background);\n}\nbody {\n margin: 0;\n}\na {\n color: var(--topogram-action-primary-background);\n text-decoration: none;\n}\na:hover {\n text-decoration: underline;\n}\nmain {\n max-width: 72rem;\n margin: 0 auto;\n padding: var(--topogram-page-padding);\n}\n.app-shell {\n min-height: 100vh;\n}\n.app-workspace {\n display: grid;\n grid-template-columns: 18rem minmax(0, 1fr);\n min-height: 100vh;\n}\n.app-main-shell {\n min-width: 0;\n}\n.app-sidebar {\n position: sticky;\n top: 0;\n align-self: start;\n min-height: 100vh;\n display: grid;\n align-content: start;\n gap: var(--topogram-space-unit);\n padding: 1.25rem 1rem;\n border-right: 1px solid rgba(24, 32, 38, 0.08);\n background: rgba(255, 255, 255, 0.86);\n backdrop-filter: blur(12px);\n}\n.app-nav {\n position: sticky;\n top: 0;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--topogram-space-unit);\n padding: 1rem 1.25rem;\n border-bottom: 1px solid rgba(24, 32, 38, 0.08);\n background: rgba(255, 255, 255, 0.9);\n backdrop-filter: blur(12px);\n}\n.app-nav-links,\n.app-nav nav,\n.app-tabbar {\n display: flex;\n gap: 0.75rem;\n flex-wrap: wrap;\n}\n.app-nav.menu-bar {\n border-bottom-style: dashed;\n}\n.app-nav.compact {\n justify-content: flex-end;\n}\n.app-tabbar {\n position: sticky;\n bottom: 0;\n z-index: 10;\n justify-content: space-around;\n padding: 0.85rem 1rem calc(0.85rem + env(safe-area-inset-bottom, 0px));\n border-top: 1px solid rgba(24, 32, 38, 0.08);\n background: rgba(255, 255, 255, 0.92);\n backdrop-filter: blur(12px);\n}\n.brand {\n font-weight: 700;\n letter-spacing: 0.01em;\n}\n.brand-mark {\n font-weight: 700;\n color: var(--topogram-muted-color);\n}\n.command-palette-button {\n background: var(--topogram-text-color);\n color: white;\n border: none;\n border-radius: var(--topogram-radius-pill);\n padding: var(--topogram-control-padding);\n font: inherit;\n cursor: pointer;\n}\n.app-footer {\n max-width: 72rem;\n margin: 0 auto;\n padding: 0 1.25rem 2rem;\n color: var(--topogram-muted-color);\n}\n.card {\n background: var(--topogram-surface-card);\n border-radius: var(--topogram-radius-card);\n padding: 1.25rem;\n box-shadow: 0 12px 30px rgba(24, 32, 38, 0.08);\n}\n.hero {\n display: grid;\n gap: var(--topogram-space-unit);\n}\n.grid {\n display: grid;\n gap: var(--topogram-space-unit);\n}\n.grid.two {\n grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));\n}\n.filters {\n display: grid;\n gap: 0.75rem;\n grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));\n margin: 1rem 0 1.25rem;\n}\nlabel {\n display: grid;\n gap: 0.35rem;\n font-size: 0.95rem;\n}\ninput,\ntextarea,\nbutton,\nselect {\n font: inherit;\n}\ninput,\ntextarea,\nselect {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid #c9d4e2;\n border-radius: var(--topogram-radius-control);\n padding: var(--topogram-control-padding);\n background: white;\n}\ntextarea {\n min-height: 8rem;\n resize: vertical;\n}\nbutton,\n.button-link {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 0.35rem;\n border: none;\n border-radius: var(--topogram-radius-pill);\n padding: var(--topogram-control-padding);\n background: var(--topogram-action-primary-background);\n color: var(--topogram-action-primary-color);\n font-weight: 600;\n cursor: pointer;\n}\nbutton:focus-visible,\n.button-link:focus-visible,\na:focus-visible,\ninput:focus-visible,\ntextarea:focus-visible,\nselect:focus-visible {\n outline: var(--topogram-focus-outline);\n outline-offset: 2px;\n}\n.button-link.secondary {\n background: #e9eef6;\n color: var(--topogram-text-color);\n}\n.button-row {\n display: flex;\n gap: 0.75rem;\n flex-wrap: wrap;\n align-items: center;\n}\n.stack {\n display: grid;\n gap: var(--topogram-space-unit);\n}\n\n.resource-list {\n list-style: none;\n padding: 0;\n margin: 1rem 0 0;\n display: grid;\n gap: 0.75rem;\n}\n\n.resource-list li {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: var(--topogram-space-unit);\n padding: 1rem;\n border: 1px solid #e0e8f1;\n border-radius: var(--topogram-radius-card);\n background: var(--topogram-surface-subtle);\n}\n.table-wrap {\n margin-top: 1rem;\n overflow-x: auto;\n border: 1px solid var(--topogram-border-color);\n border-radius: var(--topogram-radius-card);\n background: white;\n}\n.resource-table {\n width: 100%;\n border-collapse: collapse;\n min-width: 42rem;\n}\n.resource-table th,\n.resource-table td {\n padding: 0.85rem 1rem;\n text-align: left;\n border-bottom: 1px solid #e7edf5;\n vertical-align: top;\n}\n.resource-table th {\n font-size: 0.85rem;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: #516173;\n background: #f8fbff;\n}\n.resource-table tbody tr:hover {\n background: #fbfdff;\n}\n.data-grid {\n min-width: 64rem;\n font-size: 0.95rem;\n}\n.data-grid thead th {\n position: sticky;\n top: 0;\n z-index: 1;\n background: #eef5ff;\n}\n.data-grid-shell {\n box-shadow: inset 0 0 0 1px rgba(15, 92, 192, 0.04);\n}\n.cell-stack,\n.resource-meta,\n.definition-list {\n display: grid;\n gap: 0.5rem;\n}\n.cell-secondary {\n color: var(--topogram-muted-color);\n font-size: 0.92rem;\n}\n.definition-list {\n grid-template-columns: minmax(8rem, 12rem) 1fr;\n align-items: start;\n}\n.definition-list dt {\n font-weight: 600;\n color: #516173;\n}\n.definition-list dd {\n margin: 0;\n}\n.badge {\n display: inline-flex;\n align-items: center;\n padding: 0.25rem 0.6rem;\n border-radius: var(--topogram-radius-pill);\n background: #eef4ff;\n color: var(--topogram-action-primary-background);\n font-size: 0.85rem;\n font-weight: 600;\n}\n.muted {\n color: var(--topogram-muted-color);\n}\n.empty-state {\n padding: 1rem 0;\n}\n.component-card {\n border: 1px solid var(--topogram-border-color);\n border-radius: var(--topogram-radius-card);\n background: var(--topogram-surface-subtle);\n padding: 1rem;\n margin-top: 1rem;\n}\n.component-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--topogram-space-unit);\n flex-wrap: wrap;\n}\n.component-eyebrow {\n margin: 0 0 0.25rem;\n color: var(--topogram-muted-color);\n font-size: 0.75rem;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n}\n.component-card h2,\n.component-card h3 {\n margin: 0;\n}\n.component-table-wrap {\n margin-top: 1rem;\n}\n.summary-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));\n gap: 0.75rem;\n}\n.summary-grid div,\n.board-column {\n border: 1px solid #e0e8f1;\n border-radius: var(--topogram-radius-control);\n background: white;\n padding: 0.85rem;\n}\n.summary-grid strong {\n display: block;\n font-size: 1.5rem;\n}\n.summary-grid span,\n.calendar-list span {\n color: var(--topogram-muted-color);\n font-size: 0.9rem;\n}\n.board-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));\n gap: 0.75rem;\n margin-top: 1rem;\n}\n.board-card,\n.calendar-card {\n display: grid;\n gap: 0.25rem;\n border: 1px solid #e0e8f1;\n border-radius: var(--topogram-radius-control);\n background: #f8fbff;\n padding: 0.75rem;\n}\n.calendar-list {\n display: grid;\n gap: 0.75rem;\n margin-top: 1rem;\n}\nsmall.route-hint {\n display: block;\n color: var(--topogram-muted-color);\n margin-top: 0.25rem;\n}\n@media (max-width: 900px) {\n .app-workspace {\n grid-template-columns: 1fr;\n }\n .app-sidebar {\n position: static;\n min-height: auto;\n border-right: none;\n border-bottom: 1px solid rgba(24, 32, 38, 0.08);\n }\n}\n@media (max-width: 640px) {\n .definition-list {\n grid-template-columns: 1fr;\n }\n .resource-list li {\n flex-direction: column;\n }\n .resource-table {\n min-width: 36rem;\n }\n .app-nav {\n flex-wrap: wrap;\n }\n}\n";
|
|
374
379
|
const navMarkup = navLinks.map((link) => ` <a href="${link.route}">${link.label}</a>`).join("\n");
|
|
375
380
|
const shellLayout =
|
|
376
381
|
shellMode === "split_view"
|
|
@@ -381,7 +386,7 @@ function buildSvelteKitScaffold(contract, apiContracts, options = {}) {
|
|
|
381
386
|
files["src/routes/+layout.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>\n import "../app.css";\n</script>\n\n<div class="app-shell" data-shell="${shellMode}" data-windowing="${windowingMode}" data-navigation-patterns="${navigationPatterns}">\n ${shellLayout}\n${footerEnabled ? `\n <footer class="app-footer">\n <span>Generated from Topogram</span>\n </footer>` : ""}\n</div>\n`;
|
|
382
387
|
files["src/routes/+page.svelte"] = webRenderers.renderHomePage({
|
|
383
388
|
useTypescript,
|
|
384
|
-
demoPrimaryEnvVar
|
|
389
|
+
demoPrimaryEnvVar,
|
|
385
390
|
screens: contract.screens.map((screen) => ({
|
|
386
391
|
id: screen.id,
|
|
387
392
|
title: screen.title || screen.id,
|
|
@@ -402,16 +407,16 @@ function buildSvelteKitScaffold(contract, apiContracts, options = {}) {
|
|
|
402
407
|
Object.assign(files, buildGenericSvelteKitScreenFiles(screen, contract, useTypescript));
|
|
403
408
|
}
|
|
404
409
|
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
+
const primaryList = contract.screens.find((screen) => screen.id === webScreenReference.listScreenId);
|
|
411
|
+
const primaryDetail = contract.screens.find((screen) => screen.id === webScreenReference.detailScreenId);
|
|
412
|
+
const primaryCreate = contract.screens.find((screen) => screen.id === webScreenReference.createScreenId);
|
|
413
|
+
const primaryEdit = contract.screens.find((screen) => screen.id === webScreenReference.editScreenId);
|
|
414
|
+
const primaryExports = webScreenReference.exportsScreenId
|
|
410
415
|
? contract.screens.find((screen) => screen.id === webScreenReference.exportsScreenId)
|
|
411
416
|
: null;
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
const
|
|
417
|
+
const primaryListLookups = Object.fromEntries((primaryList?.lookups || []).map((lookup) => [lookup.field, lookupDescriptor(lookup)]));
|
|
418
|
+
const primaryCreateLookups = Object.fromEntries((primaryCreate?.lookups || []).map((lookup) => [lookup.field, lookupDescriptor(lookup)]));
|
|
419
|
+
const primaryEditLookups = Object.fromEntries((primaryEdit?.lookups || []).map((lookup) => [lookup.field, lookupDescriptor(lookup)]));
|
|
415
420
|
const routePageScreenIds = new Map(
|
|
416
421
|
(contract.screens || [])
|
|
417
422
|
.filter((screen) => screen.route && screen.route !== "/")
|
|
@@ -419,19 +424,19 @@ function buildSvelteKitScaffold(contract, apiContracts, options = {}) {
|
|
|
419
424
|
);
|
|
420
425
|
const implementationScreenIds = new Set();
|
|
421
426
|
|
|
422
|
-
if (
|
|
427
|
+
if (primaryList?.route && primaryDetail?.route && primaryCreate?.route && primaryEdit?.route) {
|
|
423
428
|
for (const [relativePath, contents] of Object.entries(webRenderers.renderRoutes({
|
|
424
429
|
useTypescript,
|
|
425
430
|
contract,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
431
|
+
primaryList,
|
|
432
|
+
primaryDetail,
|
|
433
|
+
primaryCreate,
|
|
434
|
+
primaryEdit,
|
|
435
|
+
primaryExports,
|
|
436
|
+
primaryListLookups,
|
|
437
|
+
primaryCreateLookups,
|
|
438
|
+
primaryEditLookups,
|
|
439
|
+
containerEnvVar,
|
|
435
440
|
ownerEnvVar,
|
|
436
441
|
webReference,
|
|
437
442
|
prettyScreenKind
|
|
@@ -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
|
}
|