@topogram/cli 0.3.49 → 0.3.51

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 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
- (FIS, RNF, DrugTrac, etc.). Identifier prefix `dom_`. Required fields:
57
- `name`, `description`, `status`. Optional: `in_scope`, `out_of_scope`,
58
- `owners`, `parent_domain`, `aliases`. Validator enforces identifier
59
- prefix, scope-list shapes, owner refs (`actor`|`role`), parent_domain
60
- refs, and parent-domain cycle detection.
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 `engine/tests/fixtures/domains/feedlot/` (3 domains,
87
- 10 capabilities, 12 entities, 4 cross-platform projections) and
88
- golden tests at `engine/tests/active/domain-kind.test.js`.
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 private CLI package:
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 private-package consumers call:
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 private catalog index at
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topogram/cli",
3
- "version": "0.3.49",
3
+ "version": "0.3.51",
4
4
  "description": "Topogram CLI for checking Topogram workspaces and generating app bundles.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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: false },
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 demo = plan.runtimeReference.demoEnv;
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=${demo.userId}
122
- TOPOGRAM_DEMO_USER_ID=${demo.userId}
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=${demo.userId}
139
- TOPOGRAM_DEMO_USER_ID=${demo.userId}
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=${demo.userId}
163
- TOPOGRAM_DEMO_USER_ID=${demo.userId}
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.demoEnv.userId}
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.demoEnv.userId}
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 demo = plan.runtimeReference.demoEnv;
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=${demo.userId}
188
- TOPOGRAM_DEMO_USER_ID=${demo.userId}
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=${demo.userId}
208
- TOPOGRAM_DEMO_USER_ID=${demo.userId}
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=${demo.userId}
233
- TOPOGRAM_DEMO_USER_ID=${demo.userId}
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=${demo.userId}
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 || process.env.PUBLIC_TOPOGRAM_DEMO_PROJECT_ID || "",
170
- ${runtimeReference.runtimeCheck.demoPrimaryEnvVar}: process.env.PUBLIC_TOPOGRAM_DEMO_PRIMARY_ID || process.env.PUBLIC_TOPOGRAM_DEMO_TASK_ID || process.env.PUBLIC_TOPOGRAM_DEMO_ISSUE_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.demoEnv.userId}";
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"] = `:root {
484
+ files["src/app.css"] = `${renderDesignIntentCss(contract.design)}
485
+
486
+ :root {
481
487
  font-family: system-ui, sans-serif;
482
- color: #182026;
483
- background: linear-gradient(180deg, #f5f7fb 0%, #edf2f7 100%);
488
+ color: var(--topogram-text-color);
489
+ background: var(--topogram-surface-background);
484
490
  }
485
491
  body { margin: 0; }
486
- a { color: #0f5cc0; text-decoration: none; }
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: 2rem 1.25rem 4rem; }
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: 1rem; 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); }
493
- .app-nav { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 1rem; 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); }
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: #607284; }
500
- .command-palette-button { background: #182026; color: white; border: none; border-radius: 999px; padding: 0.6rem 0.9rem; font: inherit; cursor: pointer; }
501
- .app-footer { max-width: 72rem; margin: 0 auto; padding: 0 1.25rem 2rem; color: #607284; }
502
- .card { background: white; border-radius: 16px; padding: 1.25rem; box-shadow: 0 12px 30px rgba(24, 32, 38, 0.08); }
503
- .hero, .stack, .grid, .filters, .task-meta, .resource-meta, .definition-list { display: grid; gap: 1rem; }
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: 12px; padding: 0.7rem 0.85rem; background: white; }
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: 999px; padding: 0.7rem 1rem; background: #0f5cc0; color: white; font-weight: 600; cursor: pointer; }
511
- .button-link.secondary { background: #e9eef6; color: #182026; }
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
- .task-list, .resource-list { list-style: none; padding: 0; margin: 1rem 0 0; display: grid; gap: 0.75rem; }
514
- .task-list li, .resource-list li { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; padding: 1rem; border: 1px solid #e0e8f1; border-radius: 14px; background: #fbfcfe; }
515
- .table-wrap { margin-top: 1rem; overflow-x: auto; border: 1px solid #d7e1ec; border-radius: 14px; background: white; }
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: #607284; font-size: 0.92rem; }
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: 999px; background: #eef4ff; color: #0f5cc0; font-size: 0.85rem; font-weight: 600; }
529
- .muted { color: #607284; }
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 #d7e1ec; border-radius: 14px; background: #fbfcfe; padding: 1rem; margin-top: 1rem; }
533
- .component-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
534
- .component-eyebrow { margin: 0 0 0.25rem; color: #607284; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
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: 12px; background: white; padding: 0.85rem; }
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: #607284; font-size: 0.9rem; }
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: 10px; background: #f8fbff; padding: 0.75rem; }
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; } .task-list li, .resource-list li { flex-direction: column; } .resource-table { min-width: 36rem; } .app-nav { flex-wrap: wrap; } }
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 demoTaskEnvVar = webReference.home.demoPrimaryEnvVar;
323
- const ownerEnvVar = webReference.createPrimary.defaultAssigneeEnvVar;
324
- const projectEnvVar = webReference.createPrimary.defaultContainerEnvVar;
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
- ":root {\n font-family: system-ui, sans-serif;\n color: #182026;\n background: linear-gradient(180deg, #f5f7fb 0%, #edf2f7 100%);\n}\nbody {\n margin: 0;\n}\na {\n color: #0f5cc0;\n text-decoration: none;\n}\na:hover {\n text-decoration: underline;\n}\nmain {\n max-width: 72rem;\n margin: 0 auto;\n padding: 2rem 1.25rem 4rem;\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: 1rem;\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: 1rem;\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: #607284;\n}\n.command-palette-button {\n background: #182026;\n color: white;\n border: none;\n border-radius: 999px;\n padding: 0.6rem 0.9rem;\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: #607284;\n}\n.card {\n background: white;\n border-radius: 16px;\n padding: 1.25rem;\n box-shadow: 0 12px 30px rgba(24, 32, 38, 0.08);\n}\n.hero {\n display: grid;\n gap: 1rem;\n}\n.grid {\n display: grid;\n gap: 1rem;\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: 12px;\n padding: 0.7rem 0.85rem;\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: 999px;\n padding: 0.7rem 1rem;\n background: #0f5cc0;\n color: white;\n font-weight: 600;\n cursor: pointer;\n}\n.button-link.secondary {\n background: #e9eef6;\n color: #182026;\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: 1rem;\n}\n.task-list,\n.resource-list {\n list-style: none;\n padding: 0;\n margin: 1rem 0 0;\n display: grid;\n gap: 0.75rem;\n}\n.task-list li,\n.resource-list li {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid #e0e8f1;\n border-radius: 14px;\n background: #fbfcfe;\n}\n.table-wrap {\n margin-top: 1rem;\n overflow-x: auto;\n border: 1px solid #d7e1ec;\n border-radius: 14px;\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.task-meta,\n.definition-list {\n display: grid;\n gap: 0.5rem;\n}\n.cell-secondary {\n color: #607284;\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: 999px;\n background: #eef4ff;\n color: #0f5cc0;\n font-size: 0.85rem;\n font-weight: 600;\n}\n.muted {\n color: #607284;\n}\n.empty-state {\n padding: 1rem 0;\n}\n.component-card {\n border: 1px solid #d7e1ec;\n border-radius: 14px;\n background: #fbfcfe;\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: 1rem;\n flex-wrap: wrap;\n}\n.component-eyebrow {\n margin: 0 0 0.25rem;\n color: #607284;\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: 12px;\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: #607284;\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: 10px;\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: #607284;\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 .task-list li,\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";
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: demoTaskEnvVar,
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 taskList = contract.screens.find((screen) => screen.id === webScreenReference.listScreenId);
406
- const taskDetail = contract.screens.find((screen) => screen.id === webScreenReference.detailScreenId);
407
- const taskCreate = contract.screens.find((screen) => screen.id === webScreenReference.createScreenId);
408
- const taskEdit = contract.screens.find((screen) => screen.id === webScreenReference.editScreenId);
409
- const taskExports = webScreenReference.exportsScreenId
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 taskListLookups = Object.fromEntries((taskList?.lookups || []).map((lookup) => [lookup.field, lookupDescriptor(lookup)]));
413
- const taskCreateLookups = Object.fromEntries((taskCreate?.lookups || []).map((lookup) => [lookup.field, lookupDescriptor(lookup)]));
414
- const taskEditLookups = Object.fromEntries((taskEdit?.lookups || []).map((lookup) => [lookup.field, lookupDescriptor(lookup)]));
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 (taskList?.route && taskDetail?.route && taskCreate?.route && taskEdit?.route) {
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
- taskList,
427
- taskDetail,
428
- taskCreate,
429
- taskEdit,
430
- taskExports,
431
- taskListLookups,
432
- taskCreateLookups,
433
- taskEditLookups,
434
- projectEnvVar,
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 { getProjection } from "../shared.js";
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(graph, screenId) {
13
- for (const projection of graph.byKind.projection || []) {
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 `:root {
60
- color: #182026;
61
- background: #f6f8fb;
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: 1rem;
70
+ gap: var(--topogram-space-unit);
74
71
  padding: 1rem 1.25rem;
75
- border-bottom: 1px solid #d8e0ea;
76
- background: #ffffff;
72
+ border-bottom: 1px solid var(--topogram-border-color);
73
+ background: var(--topogram-surface-card);
77
74
  }
78
75
 
79
76
  .brand {
80
- color: #182026;
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: #0f5cc0;
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: 1rem;
95
+ gap: var(--topogram-space-unit);
99
96
  max-width: 56rem;
100
97
  margin: 0 auto;
101
- padding: 2rem 1.25rem 4rem;
98
+ padding: var(--topogram-page-padding);
102
99
  }
103
100
 
104
101
  .panel {
105
- border: 1px solid #d8e0ea;
106
- border-radius: 8px;
107
- background: #ffffff;
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: #607284;
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 projection = getProjection(graph, options.projectionId);
196
- const routeEntries = projection.uiRoutes?.length
197
- ? projection.uiRoutes
198
- : [{ screenId: "home", path: "/" }];
199
- const routes = routeEntries.map((route) => ({
200
- screenId: route.screenId,
201
- path: route.path,
202
- title: titleForScreen(graph, route.screenId),
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
  }
@@ -2352,6 +2352,47 @@ function validateProjectionUiAppShell(errors, statement, fieldMap) {
2352
2352
  }
2353
2353
  }
2354
2354
 
2355
+ const SHARED_UI_SEMANTIC_BLOCKS = [
2356
+ "ui_screens",
2357
+ "ui_collections",
2358
+ "ui_actions",
2359
+ "ui_visibility",
2360
+ "ui_lookups",
2361
+ "ui_app_shell",
2362
+ "ui_navigation",
2363
+ "ui_screen_regions"
2364
+ ];
2365
+
2366
+ function validateProjectionUiOwnership(errors, statement, fieldMap) {
2367
+ if (statement.kind !== "projection") {
2368
+ return;
2369
+ }
2370
+
2371
+ const platform = symbolValue(getFieldValue(statement, "platform"));
2372
+ for (const key of SHARED_UI_SEMANTIC_BLOCKS) {
2373
+ const field = fieldMap.get(key)?.[0];
2374
+ if (!field || field.value.type !== "block") {
2375
+ continue;
2376
+ }
2377
+ if (platform !== "ui_shared") {
2378
+ pushError(
2379
+ errors,
2380
+ `Projection ${statement.id} ${key} belongs on shared UI projections; concrete UI projections may define ui_routes and platform surface hints only`,
2381
+ field.loc
2382
+ );
2383
+ }
2384
+ }
2385
+
2386
+ const routesField = fieldMap.get("ui_routes")?.[0];
2387
+ if (routesField?.value.type === "block" && !["ui_web", "ui_ios"].includes(platform || "")) {
2388
+ pushError(
2389
+ errors,
2390
+ `Projection ${statement.id} ui_routes belongs on concrete UI projections; shared UI projections own semantic screens and regions`,
2391
+ routesField.loc
2392
+ );
2393
+ }
2394
+ }
2395
+
2355
2396
  function validateProjectionUiDesign(errors, statement, fieldMap) {
2356
2397
  if (statement.kind !== "projection") {
2357
2398
  return;
@@ -3499,6 +3540,7 @@ export function validateWorkspace(workspaceAst) {
3499
3540
  validateProjectionHttpDownload(errors, statement, fieldMap, registry);
3500
3541
  validateProjectionHttpAuthz(errors, statement, fieldMap, registry);
3501
3542
  validateProjectionHttpCallbacks(errors, statement, fieldMap, registry);
3543
+ validateProjectionUiOwnership(errors, statement, fieldMap);
3502
3544
  validateProjectionUiScreens(errors, statement, fieldMap, registry);
3503
3545
  validateProjectionUiCollections(errors, statement, fieldMap, registry);
3504
3546
  validateProjectionUiActions(errors, statement, fieldMap, registry);