openuispec 0.2.13 → 0.2.15
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/README.md +6 -5
- package/cli/index.ts +18 -12
- package/cli/init.ts +79 -13
- package/docs/cli.md +134 -27
- package/docs/file-formats.md +51 -1
- package/drift/index.ts +7 -2
- package/examples/social-app/openuispec/README.md +2 -1
- package/examples/social-app/openuispec/mock/chat_detail.yaml +25 -0
- package/examples/social-app/openuispec/mock/discover.yaml +17 -0
- package/examples/social-app/openuispec/mock/edit_profile.yaml +9 -0
- package/examples/social-app/openuispec/mock/home_feed.yaml +32 -0
- package/examples/social-app/openuispec/mock/messages_inbox.yaml +15 -0
- package/examples/social-app/openuispec/mock/notifications.yaml +30 -0
- package/examples/social-app/openuispec/mock/post_detail.yaml +26 -0
- package/examples/social-app/openuispec/mock/profile_self.yaml +28 -0
- package/examples/social-app/openuispec/mock/profile_user.yaml +32 -0
- package/examples/social-app/openuispec/mock/search_results.yaml +17 -0
- package/examples/social-app/openuispec/mock/settings.yaml +7 -0
- package/examples/social-app/openuispec/openuispec.yaml +3 -2
- package/examples/taskflow/README.md +4 -2
- package/examples/taskflow/openuispec/README.md +2 -1
- package/examples/taskflow/openuispec/components/media_player.yaml +92 -0
- package/examples/taskflow/openuispec/contracts/README.md +2 -2
- package/examples/taskflow/openuispec/locales/en.json +1 -0
- package/examples/taskflow/openuispec/mock/home.yaml +64 -0
- package/examples/taskflow/openuispec/mock/profile_edit.yaml +6 -0
- package/examples/taskflow/openuispec/mock/project_detail.yaml +33 -0
- package/examples/taskflow/openuispec/mock/settings.yaml +13 -0
- package/examples/taskflow/openuispec/mock/task_detail.yaml +18 -0
- package/examples/taskflow/openuispec/openuispec.yaml +3 -4
- package/examples/taskflow/openuispec/platform/ios.yaml +0 -4
- package/examples/taskflow/openuispec/screens/task_detail.yaml +5 -8
- package/examples/taskflow/openuispec/tokens/icons.yaml +16 -0
- package/examples/todo-orbit/README.md +3 -2
- package/examples/todo-orbit/openuispec/README.md +2 -1
- package/examples/todo-orbit/openuispec/components/task_trend_chart.yaml +85 -0
- package/examples/todo-orbit/openuispec/locales/en.json +3 -0
- package/examples/todo-orbit/openuispec/locales/ru.json +3 -0
- package/examples/todo-orbit/openuispec/mock/analytics.yaml +26 -0
- package/examples/todo-orbit/openuispec/mock/home.yaml +33 -0
- package/examples/todo-orbit/openuispec/mock/settings.yaml +7 -0
- package/examples/todo-orbit/openuispec/mock/task_detail.yaml +14 -0
- package/examples/todo-orbit/openuispec/openuispec.yaml +3 -3
- package/examples/todo-orbit/openuispec/platform/android.yaml +0 -3
- package/examples/todo-orbit/openuispec/platform/ios.yaml +0 -3
- package/examples/todo-orbit/openuispec/platform/web.yaml +0 -3
- package/examples/todo-orbit/openuispec/screens/analytics.yaml +1 -4
- package/mcp-server/index.ts +87 -6
- package/mcp-server/preview-render.ts +1922 -0
- package/mcp-server/preview.ts +292 -0
- package/mcp-server/screenshot-shared.ts +41 -4
- package/mcp-server/screenshot.ts +283 -97
- package/package.json +1 -1
- package/prepare/index.ts +1 -1
- package/schema/component.schema.json +278 -0
- package/schema/openuispec.schema.json +5 -1
- package/schema/screen.schema.json +12 -1
- package/schema/semantic-lint.ts +29 -5
- package/schema/validate.ts +21 -0
- package/scripts/regenerate-previews.ts +136 -0
- package/spec/{openuispec-v0.1.md → openuispec-v0.2.md} +266 -8
- package/examples/taskflow/openuispec/contracts/x_media_player.yaml +0 -185
- package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +0 -139
|
@@ -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
|
+
}
|
|
@@ -60,6 +60,10 @@
|
|
|
60
60
|
"locales": {
|
|
61
61
|
"type": "string",
|
|
62
62
|
"description": "Localization string files — one JSON file per supported locale containing translated UI text."
|
|
63
|
+
},
|
|
64
|
+
"components": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Reusable component compositions — each YAML file defines a component as a composition of contracts with named slots, states, and variants."
|
|
63
67
|
}
|
|
64
68
|
},
|
|
65
69
|
"required": [
|
|
@@ -184,7 +188,7 @@
|
|
|
184
188
|
"description": "Spec categories this shared layer tracks for hash-based drift detection. Optional — when omitted, the layer relies on scope alone. Categories: manifest (project config, data models, API endpoints), tokens (visual design tokens — UI only), contracts (reusable UI component definitions — UI only, not business logic), screens (screen layouts — UI only), flows (navigation journeys — UI only), platform (per-target overrides — UI only), locales (translated UI strings).",
|
|
185
189
|
"items": {
|
|
186
190
|
"type": "string",
|
|
187
|
-
"enum": ["manifest", "tokens", "contracts", "screens", "flows", "platform", "locales"]
|
|
191
|
+
"enum": ["manifest", "tokens", "contracts", "components", "screens", "flows", "platform", "locales"]
|
|
188
192
|
}
|
|
189
193
|
},
|
|
190
194
|
"scope": {
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"additionalProperties": false
|
|
172
172
|
},
|
|
173
173
|
"section_item": {
|
|
174
|
-
"description": "A section item \u2014
|
|
174
|
+
"description": "A section item \u2014 a contract instance, component instance, or section group with children",
|
|
175
175
|
"type": "object",
|
|
176
176
|
"properties": {
|
|
177
177
|
"id": {
|
|
@@ -180,6 +180,17 @@
|
|
|
180
180
|
"contract": {
|
|
181
181
|
"type": "string"
|
|
182
182
|
},
|
|
183
|
+
"component": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"description": "Component name (alternative to contract). References a component defined in components/*.yaml."
|
|
186
|
+
},
|
|
187
|
+
"slots": {
|
|
188
|
+
"type": "object",
|
|
189
|
+
"description": "Slot overrides when using a component. Each key is a slot name.",
|
|
190
|
+
"additionalProperties": {
|
|
191
|
+
"$ref": "https://openuispec.rsteam.uz/schema/component.schema.json#/$defs/slot_override"
|
|
192
|
+
}
|
|
193
|
+
},
|
|
183
194
|
"variant": {
|
|
184
195
|
"type": "string"
|
|
185
196
|
},
|
package/schema/semantic-lint.ts
CHANGED
|
@@ -25,6 +25,7 @@ function collectLocaleKeys(data: unknown, prefix = ""): string[] {
|
|
|
25
25
|
export interface Includes {
|
|
26
26
|
tokens: string;
|
|
27
27
|
contracts: string;
|
|
28
|
+
components: string;
|
|
28
29
|
screens: string;
|
|
29
30
|
flows: string;
|
|
30
31
|
platform: string;
|
|
@@ -43,6 +44,7 @@ interface SemanticContext {
|
|
|
43
44
|
formatterNames: Set<string>;
|
|
44
45
|
mapperNames: Set<string>;
|
|
45
46
|
contractNames: Set<string>;
|
|
47
|
+
componentNames: Set<string>;
|
|
46
48
|
tokenRefs: Set<string>;
|
|
47
49
|
iconRefs: Set<string>;
|
|
48
50
|
iconVariantSuffixes: string[];
|
|
@@ -182,7 +184,8 @@ function collectIconRefs(filePath: string): { refs: Set<string>; suffixes: strin
|
|
|
182
184
|
if (!isRecord(data) || !isRecord(data.icons)) return { refs, suffixes };
|
|
183
185
|
|
|
184
186
|
const icons = data.icons as UnknownRecord;
|
|
185
|
-
const
|
|
187
|
+
const variants = isRecord(icons.variants) ? icons.variants : {};
|
|
188
|
+
const variantSuffixes = isRecord(variants.suffixes) ? variants.suffixes : {};
|
|
186
189
|
for (const suffix of Object.keys(variantSuffixes)) {
|
|
187
190
|
if (suffix.trim()) suffixes.push(suffix);
|
|
188
191
|
}
|
|
@@ -215,8 +218,9 @@ function collectIconRefs(filePath: string): { refs: Set<string>; suffixes: strin
|
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
|
|
219
|
-
|
|
221
|
+
const fallback = isRecord(icons.fallback) ? icons.fallback : {};
|
|
222
|
+
if (typeof fallback.missing_icon === "string") {
|
|
223
|
+
refs.add(fallback.missing_icon);
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
return { refs, suffixes };
|
|
@@ -253,6 +257,15 @@ function buildContext(projectDir: string, includes: Includes, manifest: UnknownR
|
|
|
253
257
|
}
|
|
254
258
|
}
|
|
255
259
|
|
|
260
|
+
// Components are also valid references in screen sections (via "component:" key)
|
|
261
|
+
const componentNames = new Set<string>();
|
|
262
|
+
const componentsDir = resolve(projectDir, includes.components);
|
|
263
|
+
for (const filePath of listFiles(componentsDir, ".yaml")) {
|
|
264
|
+
for (const key of rootKeys(filePath)) {
|
|
265
|
+
componentNames.add(key);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
256
269
|
const tokenRefs = new Set<string>();
|
|
257
270
|
const tokensDir = resolve(projectDir, includes.tokens);
|
|
258
271
|
for (const filePath of listFiles(tokensDir, ".yaml")) {
|
|
@@ -300,6 +313,7 @@ function buildContext(projectDir: string, includes: Includes, manifest: UnknownR
|
|
|
300
313
|
formatterNames,
|
|
301
314
|
mapperNames,
|
|
302
315
|
contractNames,
|
|
316
|
+
componentNames,
|
|
303
317
|
tokenRefs,
|
|
304
318
|
iconRefs: iconData.refs,
|
|
305
319
|
iconVariantSuffixes: iconData.suffixes,
|
|
@@ -429,6 +443,10 @@ function validateStringValue(
|
|
|
429
443
|
errors.push({ path, message: `unknown contract "${value}"` });
|
|
430
444
|
}
|
|
431
445
|
|
|
446
|
+
if (key === "component" && !path.includes("platform_mapping") && !context.componentNames.has(value)) {
|
|
447
|
+
errors.push({ path, message: `unknown component "${value}"` });
|
|
448
|
+
}
|
|
449
|
+
|
|
432
450
|
if (
|
|
433
451
|
(key === "icon" || key === "icon_active") &&
|
|
434
452
|
!isDynamicReference(value) &&
|
|
@@ -617,6 +635,7 @@ export function collectSemanticLint(projectDir: string, includes: Includes): Usa
|
|
|
617
635
|
const manifest = readManifest(projectDir) as UnknownRecord;
|
|
618
636
|
const context = buildContext(projectDir, includes, manifest);
|
|
619
637
|
const contractsDir = resolve(projectDir, includes.contracts);
|
|
638
|
+
const componentsDir = resolve(projectDir, includes.components);
|
|
620
639
|
|
|
621
640
|
const allErrors: UsageLint[] = [
|
|
622
641
|
...lintLocaleCoverage(context),
|
|
@@ -629,11 +648,13 @@ export function collectSemanticLint(projectDir: string, includes: Includes): Usa
|
|
|
629
648
|
...listFiles(resolve(projectDir, includes.flows), ".yaml"),
|
|
630
649
|
...listFiles(resolve(projectDir, includes.platform), ".yaml"),
|
|
631
650
|
...listFiles(resolve(projectDir, includes.contracts), ".yaml"),
|
|
651
|
+
...listFiles(componentsDir, ".yaml"),
|
|
632
652
|
];
|
|
633
653
|
|
|
634
654
|
for (const filePath of files) {
|
|
655
|
+
const isContractOrComponent = filePath.startsWith(contractsDir) || filePath.startsWith(componentsDir);
|
|
635
656
|
allErrors.push(
|
|
636
|
-
...lintFile(filePath, context, { validateTokens: !
|
|
657
|
+
...lintFile(filePath, context, { validateTokens: !isContractOrComponent })
|
|
637
658
|
);
|
|
638
659
|
}
|
|
639
660
|
|
|
@@ -645,6 +666,7 @@ export function runSemanticLint(projectDir: string, includes: Includes): number
|
|
|
645
666
|
const context = buildContext(projectDir, includes, manifest);
|
|
646
667
|
let total = 0;
|
|
647
668
|
const contractsDir = resolve(projectDir, includes.contracts);
|
|
669
|
+
const componentsDir = resolve(projectDir, includes.components);
|
|
648
670
|
|
|
649
671
|
total += printSemanticErrors("locales", lintLocaleCoverage(context));
|
|
650
672
|
total += printSemanticErrors("openuispec.yaml", lintManifestGenerationContext(projectDir, context.manifest));
|
|
@@ -655,12 +677,14 @@ export function runSemanticLint(projectDir: string, includes: Includes): number
|
|
|
655
677
|
...listFiles(resolve(projectDir, includes.flows), ".yaml"),
|
|
656
678
|
...listFiles(resolve(projectDir, includes.platform), ".yaml"),
|
|
657
679
|
...listFiles(resolve(projectDir, includes.contracts), ".yaml"),
|
|
680
|
+
...listFiles(componentsDir, ".yaml"),
|
|
658
681
|
];
|
|
659
682
|
|
|
660
683
|
for (const filePath of files) {
|
|
684
|
+
const isContractOrComponent = filePath.startsWith(contractsDir) || filePath.startsWith(componentsDir);
|
|
661
685
|
total += printSemanticErrors(
|
|
662
686
|
basename(filePath),
|
|
663
|
-
lintFile(filePath, context, { validateTokens: !
|
|
687
|
+
lintFile(filePath, context, { validateTokens: !isContractOrComponent })
|
|
664
688
|
);
|
|
665
689
|
}
|
|
666
690
|
|
package/schema/validate.ts
CHANGED
|
@@ -478,6 +478,7 @@ function validateFile(
|
|
|
478
478
|
const DEFAULT_INCLUDES: Includes = {
|
|
479
479
|
tokens: "./tokens/",
|
|
480
480
|
contracts: "./contracts/",
|
|
481
|
+
components: "./components/",
|
|
481
482
|
screens: "./screens/",
|
|
482
483
|
flows: "./flows/",
|
|
483
484
|
platform: "./platform/",
|
|
@@ -702,6 +703,26 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
702
703
|
},
|
|
703
704
|
},
|
|
704
705
|
|
|
706
|
+
components: {
|
|
707
|
+
label: "Components",
|
|
708
|
+
run(ajv, projectDir, includes) {
|
|
709
|
+
let errors = 0;
|
|
710
|
+
const dir = resolveInclude(projectDir, includes.components);
|
|
711
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
712
|
+
errors += validateFile(ajv, f, `${BASE}component.schema.json`);
|
|
713
|
+
}
|
|
714
|
+
return errors;
|
|
715
|
+
},
|
|
716
|
+
collectJson(ajv, projectDir, includes, groupKey) {
|
|
717
|
+
const errors: JsonError[] = [];
|
|
718
|
+
const dir = resolveInclude(projectDir, includes.components);
|
|
719
|
+
for (const f of listFiles(dir, ".yaml")) {
|
|
720
|
+
errors.push(...collectValidateFile(ajv, f, `${BASE}component.schema.json`));
|
|
721
|
+
}
|
|
722
|
+
return { group: groupKey, errors };
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
|
|
705
726
|
semantic: {
|
|
706
727
|
label: "Semantic",
|
|
707
728
|
run(_ajv, projectDir, includes) {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Regenerates all preview PNGs for the 3 example projects using the
|
|
4
|
+
* preview renderer (preview-render.ts → preview.ts → Puppeteer).
|
|
5
|
+
*
|
|
6
|
+
* Outputs to examples/<project>/previews/<screen>_<sizeClass>[_<theme>].png
|
|
7
|
+
*
|
|
8
|
+
* Usage: npx tsx scripts/regenerate-previews.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
import { renderPreview } from "../mcp-server/preview.js";
|
|
14
|
+
import { closeBrowser } from "../mcp-server/screenshot-shared.js";
|
|
15
|
+
|
|
16
|
+
const ROOT = resolve(import.meta.dirname!, "..");
|
|
17
|
+
|
|
18
|
+
interface Capture {
|
|
19
|
+
screen: string;
|
|
20
|
+
sizeClass: "compact" | "regular" | "expanded";
|
|
21
|
+
theme?: "light" | "dark";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ProjectDef {
|
|
25
|
+
name: string;
|
|
26
|
+
captures: Capture[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function allSizes(screen: string): Capture[] {
|
|
30
|
+
return [
|
|
31
|
+
{ screen, sizeClass: "compact" },
|
|
32
|
+
{ screen, sizeClass: "expanded" },
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function allSizesWithThemes(screen: string): Capture[] {
|
|
37
|
+
return [
|
|
38
|
+
{ screen, sizeClass: "compact" },
|
|
39
|
+
{ screen, sizeClass: "expanded" },
|
|
40
|
+
{ screen, sizeClass: "compact", theme: "light" },
|
|
41
|
+
{ screen, sizeClass: "compact", theme: "dark" },
|
|
42
|
+
{ screen, sizeClass: "expanded", theme: "light" },
|
|
43
|
+
{ screen, sizeClass: "expanded", theme: "dark" },
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PROJECTS: ProjectDef[] = [
|
|
48
|
+
{
|
|
49
|
+
name: "social-app",
|
|
50
|
+
captures: [
|
|
51
|
+
...allSizes("home_feed"),
|
|
52
|
+
...allSizes("discover"),
|
|
53
|
+
...allSizes("notifications"),
|
|
54
|
+
...allSizes("messages_inbox"),
|
|
55
|
+
...allSizes("profile_self"),
|
|
56
|
+
...allSizes("profile_user"),
|
|
57
|
+
...allSizes("settings"),
|
|
58
|
+
...allSizes("post_detail"),
|
|
59
|
+
...allSizes("chat_detail"),
|
|
60
|
+
...allSizes("search_results"),
|
|
61
|
+
...allSizes("edit_profile"),
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "taskflow",
|
|
66
|
+
captures: [
|
|
67
|
+
...allSizesWithThemes("home"),
|
|
68
|
+
...allSizes("projects"),
|
|
69
|
+
...allSizes("calendar"),
|
|
70
|
+
...allSizesWithThemes("settings"),
|
|
71
|
+
...allSizesWithThemes("task_detail"),
|
|
72
|
+
...allSizes("project_detail"),
|
|
73
|
+
...allSizes("profile_edit"),
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "todo-orbit",
|
|
78
|
+
captures: [
|
|
79
|
+
...allSizes("home"),
|
|
80
|
+
...allSizes("analytics"),
|
|
81
|
+
...allSizes("settings"),
|
|
82
|
+
...allSizes("task_detail"),
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function log(msg: string) { console.log(`\x1b[36m▸\x1b[0m ${msg}`); }
|
|
88
|
+
function logOk(msg: string) { console.log(`\x1b[32m✔\x1b[0m ${msg}`); }
|
|
89
|
+
function logErr(msg: string) { console.error(`\x1b[31m✖\x1b[0m ${msg}`); }
|
|
90
|
+
|
|
91
|
+
async function main() {
|
|
92
|
+
let total = 0;
|
|
93
|
+
let failures = 0;
|
|
94
|
+
|
|
95
|
+
for (const project of PROJECTS) {
|
|
96
|
+
console.log(`\n\x1b[1m=== ${project.name} ===\x1b[0m\n`);
|
|
97
|
+
const projectCwd = join(ROOT, "examples", project.name);
|
|
98
|
+
const outDir = join(projectCwd, "previews");
|
|
99
|
+
mkdirSync(outDir, { recursive: true });
|
|
100
|
+
|
|
101
|
+
for (const cap of project.captures) {
|
|
102
|
+
const theme = cap.theme ?? "light";
|
|
103
|
+
const suffix = cap.theme ? `_${cap.theme}` : "";
|
|
104
|
+
const filename = `${cap.screen}_${cap.sizeClass}${suffix}.png`;
|
|
105
|
+
log(`${filename}...`);
|
|
106
|
+
total++;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const result = await renderPreview(projectCwd, {
|
|
110
|
+
screen: cap.screen,
|
|
111
|
+
size_class: cap.sizeClass,
|
|
112
|
+
theme,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
for (const item of result.content) {
|
|
116
|
+
if (item.type === "image" && "data" in item) {
|
|
117
|
+
writeFileSync(join(outDir, filename), Buffer.from(item.data, "base64"));
|
|
118
|
+
logOk(filename);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (err: any) {
|
|
122
|
+
logErr(`${filename}: ${err.message}`);
|
|
123
|
+
failures++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await closeBrowser();
|
|
129
|
+
console.log(`\n\x1b[${failures ? "31" : "32"}m${total - failures}/${total} previews generated, ${failures} failures\x1b[0m\n`);
|
|
130
|
+
process.exit(failures > 0 ? 1 : 0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main().catch((err) => {
|
|
134
|
+
console.error(err);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
});
|