openuispec 0.2.12 → 0.2.14

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.
Files changed (64) hide show
  1. package/README.md +8 -7
  2. package/cli/index.ts +18 -12
  3. package/cli/init.ts +78 -13
  4. package/docs/cli.md +81 -27
  5. package/docs/file-formats.md +52 -2
  6. package/drift/index.ts +7 -2
  7. package/examples/social-app/openuispec/README.md +2 -1
  8. package/examples/social-app/openuispec/mock/chat_detail.yaml +25 -0
  9. package/examples/social-app/openuispec/mock/discover.yaml +17 -0
  10. package/examples/social-app/openuispec/mock/edit_profile.yaml +9 -0
  11. package/examples/social-app/openuispec/mock/home_feed.yaml +32 -0
  12. package/examples/social-app/openuispec/mock/messages_inbox.yaml +15 -0
  13. package/examples/social-app/openuispec/mock/notifications.yaml +30 -0
  14. package/examples/social-app/openuispec/mock/post_detail.yaml +26 -0
  15. package/examples/social-app/openuispec/mock/profile_self.yaml +28 -0
  16. package/examples/social-app/openuispec/mock/profile_user.yaml +32 -0
  17. package/examples/social-app/openuispec/mock/search_results.yaml +17 -0
  18. package/examples/social-app/openuispec/mock/settings.yaml +7 -0
  19. package/examples/social-app/openuispec/openuispec.yaml +3 -2
  20. package/examples/taskflow/README.md +5 -3
  21. package/examples/taskflow/openuispec/README.md +2 -1
  22. package/examples/taskflow/openuispec/components/media_player.yaml +92 -0
  23. package/examples/taskflow/openuispec/contracts/README.md +2 -2
  24. package/examples/taskflow/openuispec/locales/en.json +1 -0
  25. package/examples/taskflow/openuispec/mock/home.yaml +64 -0
  26. package/examples/taskflow/openuispec/mock/profile_edit.yaml +6 -0
  27. package/examples/taskflow/openuispec/mock/project_detail.yaml +33 -0
  28. package/examples/taskflow/openuispec/mock/settings.yaml +13 -0
  29. package/examples/taskflow/openuispec/mock/task_detail.yaml +18 -0
  30. package/examples/taskflow/openuispec/openuispec.yaml +3 -4
  31. package/examples/taskflow/openuispec/platform/ios.yaml +0 -4
  32. package/examples/taskflow/openuispec/screens/task_detail.yaml +5 -8
  33. package/examples/taskflow/openuispec/tokens/icons.yaml +16 -0
  34. package/examples/todo-orbit/README.md +3 -2
  35. package/examples/todo-orbit/openuispec/README.md +2 -1
  36. package/examples/todo-orbit/openuispec/components/task_trend_chart.yaml +85 -0
  37. package/examples/todo-orbit/openuispec/locales/en.json +3 -0
  38. package/examples/todo-orbit/openuispec/locales/ru.json +3 -0
  39. package/examples/todo-orbit/openuispec/mock/analytics.yaml +26 -0
  40. package/examples/todo-orbit/openuispec/mock/home.yaml +33 -0
  41. package/examples/todo-orbit/openuispec/mock/settings.yaml +7 -0
  42. package/examples/todo-orbit/openuispec/mock/task_detail.yaml +14 -0
  43. package/examples/todo-orbit/openuispec/openuispec.yaml +3 -3
  44. package/examples/todo-orbit/openuispec/platform/android.yaml +0 -3
  45. package/examples/todo-orbit/openuispec/platform/ios.yaml +0 -3
  46. package/examples/todo-orbit/openuispec/platform/web.yaml +0 -3
  47. package/examples/todo-orbit/openuispec/screens/analytics.yaml +1 -4
  48. package/mcp-server/index.ts +80 -3
  49. package/mcp-server/preview-render.ts +1922 -0
  50. package/mcp-server/preview.ts +292 -0
  51. package/mcp-server/screenshot-shared.ts +38 -0
  52. package/mcp-server/screenshot.ts +3 -32
  53. package/package.json +1 -1
  54. package/prepare/index.ts +1 -1
  55. package/schema/component.schema.json +278 -0
  56. package/schema/custom-contract.schema.json +2 -2
  57. package/schema/openuispec.schema.json +18 -8
  58. package/schema/screen.schema.json +12 -1
  59. package/schema/semantic-lint.ts +24 -2
  60. package/schema/validate.ts +21 -0
  61. package/scripts/regenerate-previews.ts +136 -0
  62. package/spec/{openuispec-v0.1.md → openuispec-v0.2.md} +275 -17
  63. package/examples/taskflow/openuispec/contracts/x_media_player.yaml +0 -185
  64. package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +0 -139
@@ -0,0 +1,292 @@
1
+ /**
2
+ * preview.ts — Orchestrates spec loading, mock data, and HTML rendering
3
+ * for the openuispec_preview tool.
4
+ */
5
+
6
+ import { readFileSync, existsSync, readdirSync } from "node:fs";
7
+ import { join, resolve } from "node:path";
8
+ import YAML from "yaml";
9
+ import { findProjectDir } from "../drift/index.js";
10
+ import { getBrowser, type ScreenshotResult } from "./screenshot-shared.js";
11
+ import { renderPage, type PreviewContext } from "./preview-render.js";
12
+
13
+ // ── types ───────────────────────────────────────────────────────────
14
+
15
+ export interface PreviewOptions {
16
+ screen: string;
17
+ size_class?: "compact" | "regular" | "expanded";
18
+ theme?: "light" | "dark";
19
+ locale?: string;
20
+ viewport?: { width: number; height: number };
21
+ include_html?: boolean;
22
+ }
23
+
24
+ // ── viewport defaults per size class ────────────────────────────────
25
+
26
+ const SIZE_CLASS_VIEWPORTS: Record<string, { width: number; height: number }> = {
27
+ compact: { width: 390, height: 844 },
28
+ regular: { width: 820, height: 1180 },
29
+ expanded: { width: 1280, height: 800 },
30
+ };
31
+
32
+ // ── spec loading helpers ────────────────────────────────────────────
33
+
34
+ function loadManifest(projectDir: string): any {
35
+ const manifestPath = join(projectDir, "openuispec.yaml");
36
+ if (!existsSync(manifestPath)) {
37
+ throw new Error(`Manifest not found at ${manifestPath}`);
38
+ }
39
+ return YAML.parse(readFileSync(manifestPath, "utf-8"));
40
+ }
41
+
42
+ function loadScreen(projectDir: string, manifest: any, screenName: string): any {
43
+ const screensDir = resolve(projectDir, manifest.includes?.screens ?? "./screens/");
44
+
45
+ // Try exact filename first
46
+ const candidates = [
47
+ join(screensDir, `${screenName}.yaml`),
48
+ join(screensDir, `${screenName}.yml`),
49
+ ];
50
+
51
+ for (const candidate of candidates) {
52
+ if (existsSync(candidate)) {
53
+ return YAML.parse(readFileSync(candidate, "utf-8"));
54
+ }
55
+ }
56
+
57
+ // Scan all screen files for matching screen name key
58
+ if (existsSync(screensDir)) {
59
+ for (const file of readdirSync(screensDir)) {
60
+ if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
61
+ const content = YAML.parse(readFileSync(join(screensDir, file), "utf-8"));
62
+ if (content && typeof content === "object" && screenName in content) {
63
+ return content;
64
+ }
65
+ }
66
+ }
67
+
68
+ throw new Error(
69
+ `Screen "${screenName}" not found in ${screensDir}. ` +
70
+ `Available: ${existsSync(screensDir) ? readdirSync(screensDir).filter(f => f.endsWith(".yaml")).map(f => f.replace(".yaml", "")).join(", ") : "none"}`,
71
+ );
72
+ }
73
+
74
+ function loadAllTokens(projectDir: string, manifest: any): Record<string, any> {
75
+ const tokensDir = resolve(projectDir, manifest.includes?.tokens ?? "./tokens/");
76
+ const tokens: Record<string, any> = {};
77
+
78
+ if (!existsSync(tokensDir)) return tokens;
79
+
80
+ for (const file of readdirSync(tokensDir)) {
81
+ if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
82
+ const category = file.replace(/\.ya?ml$/, "");
83
+ try {
84
+ tokens[category] = YAML.parse(readFileSync(join(tokensDir, file), "utf-8"));
85
+ } catch {
86
+ // Skip malformed token files
87
+ }
88
+ }
89
+
90
+ return tokens;
91
+ }
92
+
93
+ function loadLocale(projectDir: string, manifest: any, localeName: string): Record<string, string> {
94
+ const localesDir = resolve(projectDir, manifest.includes?.locales ?? "./locales/");
95
+
96
+ const candidates = [
97
+ join(localesDir, `${localeName}.json`),
98
+ join(localesDir, `${localeName}.yaml`),
99
+ join(localesDir, `${localeName}.yml`),
100
+ ];
101
+
102
+ for (const candidate of candidates) {
103
+ if (existsSync(candidate)) {
104
+ const content = readFileSync(candidate, "utf-8");
105
+ if (candidate.endsWith(".json")) {
106
+ return JSON.parse(content);
107
+ }
108
+ return YAML.parse(content) ?? {};
109
+ }
110
+ }
111
+
112
+ // Fallback to default locale
113
+ const defaultLocale = manifest.i18n?.default_locale ?? "en";
114
+ if (localeName !== defaultLocale) {
115
+ return loadLocale(projectDir, manifest, defaultLocale);
116
+ }
117
+
118
+ return {};
119
+ }
120
+
121
+ function loadContractDefs(projectDir: string, manifest: any): Record<string, any> {
122
+ const contractsDir = resolve(projectDir, manifest.includes?.contracts ?? "./contracts/");
123
+ const defs: Record<string, any> = {};
124
+
125
+ if (!existsSync(contractsDir)) return defs;
126
+
127
+ for (const file of readdirSync(contractsDir)) {
128
+ if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
129
+ try {
130
+ const content = YAML.parse(readFileSync(join(contractsDir, file), "utf-8"));
131
+ if (content && typeof content === "object") {
132
+ const name = file.replace(/\.ya?ml$/, "");
133
+ defs[name] = content;
134
+ }
135
+ } catch {
136
+ // Skip malformed contract files
137
+ }
138
+ }
139
+
140
+ return defs;
141
+ }
142
+
143
+ function loadComponentDefs(projectDir: string, manifest: any): Record<string, any> {
144
+ const componentsDir = resolve(projectDir, manifest.includes?.components ?? "./components/");
145
+ const defs: Record<string, any> = {};
146
+
147
+ if (!existsSync(componentsDir)) return defs;
148
+
149
+ for (const file of readdirSync(componentsDir)) {
150
+ if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
151
+ try {
152
+ const content = YAML.parse(readFileSync(join(componentsDir, file), "utf-8"));
153
+ if (content && typeof content === "object") {
154
+ for (const [name, def] of Object.entries(content)) {
155
+ defs[name] = def;
156
+ }
157
+ }
158
+ } catch {
159
+ // Skip malformed component files
160
+ }
161
+ }
162
+
163
+ return defs;
164
+ }
165
+
166
+ function loadMockData(projectDir: string, screenName: string): { data: Record<string, any>; params: Record<string, any> } {
167
+ const mockDir = join(projectDir, "mock");
168
+
169
+ const candidates = [
170
+ join(mockDir, `${screenName}.yaml`),
171
+ join(mockDir, `${screenName}.yml`),
172
+ join(mockDir, `${screenName}.json`),
173
+ ];
174
+
175
+ for (const candidate of candidates) {
176
+ if (existsSync(candidate)) {
177
+ const content = readFileSync(candidate, "utf-8");
178
+ let parsed: any;
179
+ if (candidate.endsWith(".json")) {
180
+ parsed = JSON.parse(content);
181
+ } else {
182
+ parsed = YAML.parse(content);
183
+ }
184
+ return {
185
+ data: parsed?.data ?? parsed ?? {},
186
+ params: parsed?.params ?? {},
187
+ };
188
+ }
189
+ }
190
+
191
+ return { data: {}, params: {} };
192
+ }
193
+
194
+ // ── main preview function ───────────────────────────────────────────
195
+
196
+ export async function renderPreview(
197
+ projectCwd: string,
198
+ options: PreviewOptions,
199
+ ): Promise<ScreenshotResult> {
200
+ const {
201
+ screen,
202
+ size_class = "compact",
203
+ theme = "light",
204
+ locale: localeName = "en",
205
+ viewport,
206
+ include_html = false,
207
+ } = options;
208
+
209
+ // 1. Find project directory
210
+ const projectDir = findProjectDir(projectCwd);
211
+
212
+ // 2. Load specs
213
+ const manifest = loadManifest(projectDir);
214
+ const screenSpec = loadScreen(projectDir, manifest, screen);
215
+ const tokens = loadAllTokens(projectDir, manifest);
216
+ const locale = loadLocale(projectDir, manifest, localeName);
217
+
218
+ // 3. Load contract definitions (project extensions) and component definitions
219
+ const contractDefs = loadContractDefs(projectDir, manifest);
220
+ manifest._contractDefs = contractDefs;
221
+ const componentDefs = loadComponentDefs(projectDir, manifest);
222
+ manifest._componentDefs = componentDefs;
223
+
224
+ // 4. Load mock data
225
+ const { data: mockData, params: mockParams } = loadMockData(projectDir, screen);
226
+
227
+ // 5. Build render context
228
+ const ctx: PreviewContext = {
229
+ manifest,
230
+ screen: screenSpec,
231
+ screenName: screen,
232
+ tokens,
233
+ locale,
234
+ mockData,
235
+ mockParams,
236
+ sizeClass: size_class,
237
+ theme,
238
+ };
239
+
240
+ // 6. Render HTML
241
+ const html = renderPage(ctx);
242
+
243
+ // 7. Screenshot with Puppeteer
244
+ const vp = viewport ?? SIZE_CLASS_VIEWPORTS[size_class];
245
+ const browser = await getBrowser();
246
+ const page = await browser.newPage();
247
+
248
+ try {
249
+ await page.setViewport({
250
+ width: vp.width,
251
+ height: vp.height,
252
+ deviceScaleFactor: 2,
253
+ });
254
+
255
+ if (theme === "dark") {
256
+ await page.emulateMediaFeatures([
257
+ { name: "prefers-color-scheme", value: "dark" },
258
+ ]);
259
+ }
260
+
261
+ await page.setContent(html, { waitUntil: "load", timeout: 10_000 });
262
+
263
+ // Small delay for CSS to settle
264
+ await new Promise((r) => setTimeout(r, 200));
265
+
266
+ const buffer = await page.screenshot({ type: "png", fullPage: true });
267
+ const base64 = buffer.toString("base64");
268
+
269
+ const content: ScreenshotResult["content"] = [
270
+ { type: "image" as const, data: base64, mimeType: "image/png" },
271
+ {
272
+ type: "text" as const,
273
+ text: JSON.stringify({
274
+ screen,
275
+ size_class,
276
+ theme,
277
+ locale: localeName,
278
+ viewport: vp,
279
+ mock_data_loaded: Object.keys(mockData).length > 0,
280
+ }, null, 2),
281
+ },
282
+ ];
283
+
284
+ if (include_html) {
285
+ content.push({ type: "text" as const, text: html });
286
+ }
287
+
288
+ return { content };
289
+ } finally {
290
+ await page.close();
291
+ }
292
+ }
@@ -8,6 +8,44 @@ import { createHash } from "node:crypto";
8
8
  import YAML from "yaml";
9
9
  import { findProjectDir } from "../drift/index.js";
10
10
 
11
+ // ── shared browser manager ──────────────────────────────────────────
12
+
13
+ let browserInstance: any = null;
14
+ let launchPromise: Promise<any> | null = null;
15
+
16
+ export async function getBrowser(): Promise<any> {
17
+ if (browserInstance?.connected) return browserInstance;
18
+
19
+ if (!launchPromise) {
20
+ launchPromise = (async () => {
21
+ let puppeteer: any;
22
+ try {
23
+ puppeteer = await import("puppeteer");
24
+ } catch {
25
+ throw new Error(
26
+ "puppeteer is not installed. Run:\n npm install -g puppeteer\n" +
27
+ "or add it to your project's devDependencies.",
28
+ );
29
+ }
30
+
31
+ browserInstance = await puppeteer.launch({
32
+ headless: true,
33
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
34
+ });
35
+ return browserInstance;
36
+ })();
37
+ }
38
+ return launchPromise;
39
+ }
40
+
41
+ export async function closeBrowser(): Promise<void> {
42
+ launchPromise = null;
43
+ if (browserInstance) {
44
+ try { await browserInstance.close(); } catch { /* ignore */ }
45
+ browserInstance = null;
46
+ }
47
+ }
48
+
11
49
  // ── shared result type ──────────────────────────────────────────────
12
50
 
13
51
  export interface ScreenshotResult {
@@ -11,6 +11,7 @@ import { join, resolve } from "node:path";
11
11
  import { createServer, type AddressInfo } from "node:net";
12
12
  import YAML from "yaml";
13
13
  import { findProjectDir } from "../drift/index.js";
14
+ import { getBrowser, closeBrowser, type ScreenshotResult } from "./screenshot-shared.js";
14
15
 
15
16
  // ── types ───────────────────────────────────────────────────────────
16
17
 
@@ -25,11 +26,6 @@ export interface ScreenshotOptions {
25
26
  output_dir?: string;
26
27
  }
27
28
 
28
- export interface ScreenshotResult {
29
- content: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }>;
30
- isError?: true;
31
- }
32
-
33
29
  // ── free port finder ────────────────────────────────────────────────
34
30
 
35
31
  function findFreePort(): Promise<number> {
@@ -145,29 +141,7 @@ async function startDevServer(webDir: string): Promise<ServerInstance> {
145
141
  return instance;
146
142
  }
147
143
 
148
- // ── browser manager ─────────────────────────────────────────────────
149
-
150
- let browserInstance: any = null;
151
-
152
- async function getBrowser(): Promise<any> {
153
- if (browserInstance?.connected) return browserInstance;
154
-
155
- let puppeteer: any;
156
- try {
157
- puppeteer = await import("puppeteer");
158
- } catch {
159
- throw new Error(
160
- "puppeteer is not installed. Run:\n npm install -g puppeteer\n" +
161
- "or add it to your project's devDependencies.",
162
- );
163
- }
164
-
165
- browserInstance = await puppeteer.launch({
166
- headless: true,
167
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
168
- });
169
- return browserInstance;
170
- }
144
+ // ── browser manager (imported from screenshot-shared.ts) ────────────
171
145
 
172
146
  // ── screenshot capture ──────────────────────────────────────────────
173
147
 
@@ -360,10 +334,7 @@ export async function shutdownAll() {
360
334
  try { instance.process.kill(); } catch { /* already dead */ }
361
335
  }
362
336
  servers.clear();
363
- if (browserInstance) {
364
- try { await browserInstance.close(); } catch { /* ignore */ }
365
- browserInstance = null;
366
- }
337
+ await closeBrowser();
367
338
  }
368
339
 
369
340
  process.on("exit", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",
package/prepare/index.ts CHANGED
@@ -935,7 +935,7 @@ function referenceExamples(): string[] {
935
935
  const packageRoot = resolvePackageRoot();
936
936
  const candidates = [
937
937
  join(packageRoot, "README.md"),
938
- join(packageRoot, "spec", "openuispec-v0.1.md"),
938
+ join(packageRoot, "spec", "openuispec-v0.2.md"),
939
939
  join(packageRoot, "examples", "taskflow", "openuispec"),
940
940
  join(packageRoot, "schema"),
941
941
  ];
@@ -0,0 +1,278 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://openuispec.rsteam.uz/schema/component.schema.json",
4
+ "title": "OpenUISpec Component",
5
+ "description": "Reusable composition of contracts with named slots — root key is the component name",
6
+ "type": "object",
7
+ "minProperties": 1,
8
+ "maxProperties": 1,
9
+ "propertyNames": {
10
+ "pattern": "^[a-z][a-z0-9_]*$"
11
+ },
12
+ "additionalProperties": {
13
+ "$ref": "#/$defs/component_def"
14
+ },
15
+ "$defs": {
16
+ "component_def": {
17
+ "type": "object",
18
+ "description": "A component definition — composition of contracts with named slots, states, variants, and layout",
19
+ "properties": {
20
+ "semantic": {
21
+ "type": "string",
22
+ "description": "Human-readable description of this component's purpose"
23
+ },
24
+ "props": {
25
+ "type": "object",
26
+ "description": "Typed property definitions for this component",
27
+ "additionalProperties": {
28
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/prop_def"
29
+ }
30
+ },
31
+ "slots": {
32
+ "type": "object",
33
+ "description": "Named slots — each slot wraps a contract instance",
34
+ "additionalProperties": {
35
+ "$ref": "#/$defs/slot_def"
36
+ }
37
+ },
38
+ "layout": {
39
+ "$ref": "#/$defs/component_layout"
40
+ },
41
+ "states": {
42
+ "type": "object",
43
+ "description": "Composite states that control slot visibility and props",
44
+ "additionalProperties": {
45
+ "$ref": "#/$defs/component_state_def"
46
+ }
47
+ },
48
+ "variants": {
49
+ "type": "object",
50
+ "description": "Named component variants with layout and slot overrides",
51
+ "additionalProperties": {
52
+ "$ref": "#/$defs/component_variant_def"
53
+ }
54
+ },
55
+ "tokens": {
56
+ "type": "object",
57
+ "description": "Token bindings for the component container",
58
+ "additionalProperties": true
59
+ },
60
+ "a11y": {
61
+ "type": "object",
62
+ "description": "Accessibility requirements",
63
+ "properties": {
64
+ "role": {
65
+ "type": "string",
66
+ "description": "ARIA/accessibility role"
67
+ },
68
+ "label": {
69
+ "type": "string",
70
+ "description": "Accessibility label source"
71
+ }
72
+ },
73
+ "additionalProperties": true
74
+ },
75
+ "platform_mapping": {
76
+ "type": "object",
77
+ "description": "Per-platform native component mapping",
78
+ "properties": {
79
+ "ios": { "type": "object", "additionalProperties": true },
80
+ "android": { "type": "object", "additionalProperties": true },
81
+ "web": { "type": "object", "additionalProperties": true }
82
+ },
83
+ "additionalProperties": {
84
+ "type": "object",
85
+ "additionalProperties": true
86
+ }
87
+ },
88
+ "dependencies": {
89
+ "type": "object",
90
+ "description": "Per-platform library/framework requirements",
91
+ "additionalProperties": {
92
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/dependency_def"
93
+ }
94
+ },
95
+ "generation": {
96
+ "type": "object",
97
+ "description": "AI generation compliance hints",
98
+ "properties": {
99
+ "must_handle": {
100
+ "type": "array",
101
+ "items": { "type": "string" }
102
+ },
103
+ "should_handle": {
104
+ "type": "array",
105
+ "items": { "type": "string" }
106
+ },
107
+ "may_handle": {
108
+ "type": "array",
109
+ "items": { "type": "string" }
110
+ }
111
+ },
112
+ "additionalProperties": false
113
+ },
114
+ "test_cases": {
115
+ "type": "array",
116
+ "description": "Behavioral verification scenarios",
117
+ "items": {
118
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/test_case"
119
+ }
120
+ }
121
+ },
122
+ "required": ["semantic", "slots"],
123
+ "additionalProperties": false
124
+ },
125
+ "slot_def": {
126
+ "type": "object",
127
+ "description": "A named slot wrapping a contract instance",
128
+ "properties": {
129
+ "contract": {
130
+ "type": "string",
131
+ "description": "Base contract family (e.g. action_trigger, input_field, data_display)"
132
+ },
133
+ "variant": {
134
+ "type": "string",
135
+ "description": "Contract variant"
136
+ },
137
+ "input_type": {
138
+ "type": "string",
139
+ "description": "Input type for input_field contracts (e.g. slider, text)"
140
+ },
141
+ "props": {
142
+ "type": "object",
143
+ "description": "Default props passed to the contract",
144
+ "additionalProperties": true
145
+ },
146
+ "hideable": {
147
+ "type": "boolean",
148
+ "description": "Whether this slot can be hidden by states or screen overrides"
149
+ },
150
+ "tokens_override": {
151
+ "type": "object",
152
+ "description": "Token overrides for this slot",
153
+ "additionalProperties": true
154
+ }
155
+ },
156
+ "required": ["contract"],
157
+ "additionalProperties": false
158
+ },
159
+ "component_layout": {
160
+ "type": "object",
161
+ "description": "Layout definition for the component",
162
+ "properties": {
163
+ "type": {
164
+ "type": "string",
165
+ "description": "Layout type (e.g. stack, row, grid)"
166
+ },
167
+ "spacing": {
168
+ "type": "string"
169
+ },
170
+ "align": {
171
+ "type": "string"
172
+ },
173
+ "justify": {
174
+ "type": "string"
175
+ },
176
+ "sections": {
177
+ "type": "array",
178
+ "items": {
179
+ "$ref": "#/$defs/component_layout_item"
180
+ }
181
+ }
182
+ },
183
+ "additionalProperties": true
184
+ },
185
+ "component_layout_item": {
186
+ "description": "A layout item — either a slot reference or a nested layout",
187
+ "type": "object",
188
+ "properties": {
189
+ "slot": {
190
+ "type": "string",
191
+ "description": "Reference to a named slot"
192
+ },
193
+ "layout": {
194
+ "$ref": "#/$defs/component_layout"
195
+ }
196
+ },
197
+ "additionalProperties": true
198
+ },
199
+ "component_state_def": {
200
+ "type": "object",
201
+ "description": "A composite state affecting slot visibility and props",
202
+ "properties": {
203
+ "semantic": {
204
+ "type": "string"
205
+ },
206
+ "hide_slots": {
207
+ "type": "array",
208
+ "items": { "type": "string" },
209
+ "description": "Slots to hide in this state"
210
+ },
211
+ "slot_overrides": {
212
+ "type": "object",
213
+ "description": "Per-slot prop/variant overrides in this state",
214
+ "additionalProperties": {
215
+ "$ref": "#/$defs/slot_override"
216
+ }
217
+ },
218
+ "transitions_to": {
219
+ "type": "array",
220
+ "items": { "type": "string" },
221
+ "description": "States this state can transition to"
222
+ }
223
+ },
224
+ "additionalProperties": false
225
+ },
226
+ "component_variant_def": {
227
+ "type": "object",
228
+ "description": "A named component variant with layout and slot overrides",
229
+ "properties": {
230
+ "semantic": {
231
+ "type": "string"
232
+ },
233
+ "hide_slots": {
234
+ "type": "array",
235
+ "items": { "type": "string" },
236
+ "description": "Slots to hide in this variant"
237
+ },
238
+ "layout": {
239
+ "$ref": "#/$defs/component_layout"
240
+ },
241
+ "tokens": {
242
+ "type": "object",
243
+ "description": "Token overrides for this variant",
244
+ "additionalProperties": true
245
+ },
246
+ "slot_overrides": {
247
+ "type": "object",
248
+ "description": "Per-slot prop/variant overrides in this variant",
249
+ "additionalProperties": {
250
+ "$ref": "#/$defs/slot_override"
251
+ }
252
+ }
253
+ },
254
+ "additionalProperties": false
255
+ },
256
+ "slot_override": {
257
+ "type": "object",
258
+ "description": "Override for a specific slot",
259
+ "properties": {
260
+ "variant": {
261
+ "type": "string"
262
+ },
263
+ "props": {
264
+ "type": "object",
265
+ "additionalProperties": true
266
+ },
267
+ "tokens_override": {
268
+ "type": "object",
269
+ "additionalProperties": true
270
+ },
271
+ "hidden": {
272
+ "type": "boolean"
273
+ }
274
+ },
275
+ "additionalProperties": false
276
+ }
277
+ }
278
+ }