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
|
@@ -10,13 +10,14 @@ Todo Orbit is a multi-platform sample project for OpenUISpec. It combines a sour
|
|
|
10
10
|
- Create and edit task flows
|
|
11
11
|
- Recurring-rule creation with conditional fields and validation
|
|
12
12
|
- Bilingual localization with English and Russian
|
|
13
|
-
-
|
|
13
|
+
- Components for reusable composed UI (task trend chart)
|
|
14
|
+
- Custom contracts for schedule preview
|
|
14
15
|
|
|
15
16
|
## Project layout
|
|
16
17
|
|
|
17
18
|
| Path | Purpose |
|
|
18
19
|
|------|---------|
|
|
19
|
-
| [`openuispec/`](./openuispec/) | Source OpenUISpec project: manifest, tokens, screens, flows, contracts, locales |
|
|
20
|
+
| [`openuispec/`](./openuispec/) | Source OpenUISpec project: manifest, tokens, screens, flows, contracts, components, locales |
|
|
20
21
|
| [`generated/web/Todo Orbit/`](./generated/web/Todo%20Orbit/) | Generated React + Vite web app |
|
|
21
22
|
| [`generated/ios/Todo Orbit/`](./generated/ios/Todo%20Orbit/) | Generated SwiftUI iOS target |
|
|
22
23
|
| [`generated/android/Todo Orbit/`](./generated/android/Todo%20Orbit/) | Generated Jetpack Compose Android target |
|
|
@@ -12,6 +12,7 @@ This directory contains the **OpenUISpec** semantic UI specification for **todo-
|
|
|
12
12
|
| `screens/` | Screen definitions — one YAML file per screen |
|
|
13
13
|
| `flows/` | Navigation flows — multi-step user journeys |
|
|
14
14
|
| `contracts/` | Component contracts — standard extensions and custom (`x_` prefixed) |
|
|
15
|
+
| `components/` | Reusable compositions — contract slot compositions with states and variants |
|
|
15
16
|
| `platform/` | Platform overrides — per-target (iOS, Android, Web) behaviors |
|
|
16
17
|
| `locales/` | Localization — i18n strings (JSON, ICU MessageFormat) |
|
|
17
18
|
|
|
@@ -28,7 +29,7 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
|
|
|
28
29
|
|
|
29
30
|
**Reference files inside the package (read in this order):**
|
|
30
31
|
1. `README.md` — schema tables, file format reference, root wrapper keys
|
|
31
|
-
2. `spec/openuispec-v0.
|
|
32
|
+
2. `spec/openuispec-v0.2.md` — full specification (contracts, layout, expressions, etc.)
|
|
32
33
|
3. `examples/taskflow/openuispec/` — complete working example with all file types
|
|
33
34
|
4. `schema/` — JSON Schemas for validation
|
|
34
35
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# Component: task_trend_chart
|
|
3
|
+
# ============================================================
|
|
4
|
+
# Visualizes task creation and completion trends as a composed
|
|
5
|
+
# component with chart area, legend, and period selector slots.
|
|
6
|
+
# ============================================================
|
|
7
|
+
|
|
8
|
+
task_trend_chart:
|
|
9
|
+
semantic: "Visualizes task creation and completion trends over time for productivity analysis"
|
|
10
|
+
|
|
11
|
+
props:
|
|
12
|
+
metric:
|
|
13
|
+
type: enum
|
|
14
|
+
values: [completed, created, completion_rate]
|
|
15
|
+
required: true
|
|
16
|
+
period:
|
|
17
|
+
type: enum
|
|
18
|
+
values: [week, month, quarter]
|
|
19
|
+
required: true
|
|
20
|
+
show_legend: { type: bool, default: true }
|
|
21
|
+
|
|
22
|
+
slots:
|
|
23
|
+
chart_area:
|
|
24
|
+
contract: data_display
|
|
25
|
+
variant: card
|
|
26
|
+
props: { title: "$t:analytics.trend_chart" }
|
|
27
|
+
period_selector:
|
|
28
|
+
contract: input_field
|
|
29
|
+
input_type: select
|
|
30
|
+
props: { label: "$t:analytics.period", options: [week, month, quarter] }
|
|
31
|
+
hideable: true
|
|
32
|
+
legend:
|
|
33
|
+
contract: data_display
|
|
34
|
+
variant: inline
|
|
35
|
+
props: { title: "$t:analytics.legend" }
|
|
36
|
+
hideable: true
|
|
37
|
+
|
|
38
|
+
layout:
|
|
39
|
+
type: stack
|
|
40
|
+
spacing: "spacing.sm"
|
|
41
|
+
sections:
|
|
42
|
+
- slot: period_selector
|
|
43
|
+
- slot: chart_area
|
|
44
|
+
- slot: legend
|
|
45
|
+
|
|
46
|
+
states:
|
|
47
|
+
idle: { semantic: "Chart has not loaded data yet" }
|
|
48
|
+
loading:
|
|
49
|
+
semantic: "Trend data is loading"
|
|
50
|
+
hide_slots: [legend]
|
|
51
|
+
ready:
|
|
52
|
+
semantic: "Trend data is available and rendered"
|
|
53
|
+
empty:
|
|
54
|
+
semantic: "No trend points available"
|
|
55
|
+
hide_slots: [legend]
|
|
56
|
+
error:
|
|
57
|
+
semantic: "Trend data failed to load"
|
|
58
|
+
hide_slots: [legend]
|
|
59
|
+
|
|
60
|
+
variants:
|
|
61
|
+
compact:
|
|
62
|
+
semantic: "Compact chart for dashboard cards"
|
|
63
|
+
hide_slots: [period_selector]
|
|
64
|
+
tokens:
|
|
65
|
+
background: "color.surface.secondary"
|
|
66
|
+
radius: "spacing.sm"
|
|
67
|
+
|
|
68
|
+
tokens:
|
|
69
|
+
background: "color.surface.primary"
|
|
70
|
+
radius: "spacing.sm"
|
|
71
|
+
padding: "spacing.md"
|
|
72
|
+
|
|
73
|
+
a11y:
|
|
74
|
+
role: "img"
|
|
75
|
+
label: "Analytics trend chart"
|
|
76
|
+
|
|
77
|
+
platform_mapping:
|
|
78
|
+
ios: { component: "Chart", framework: "Swift Charts" }
|
|
79
|
+
android: { component: "Canvas chart composable" }
|
|
80
|
+
web: { component: "SVG chart component" }
|
|
81
|
+
|
|
82
|
+
generation:
|
|
83
|
+
must_handle:
|
|
84
|
+
- "Render chart area with completed and created series"
|
|
85
|
+
- "Handle loading, empty, and error states"
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"analytics.completion_rate": "Completion rate",
|
|
27
27
|
"analytics.overdue_section": "Overdue review",
|
|
28
28
|
"analytics.overdue_subtitle": "Tasks that need attention first.",
|
|
29
|
+
"analytics.trend_chart": "Trend chart",
|
|
30
|
+
"analytics.period": "Period",
|
|
31
|
+
"analytics.legend": "Legend",
|
|
29
32
|
"analytics.trend_subtitle": "Completion trend",
|
|
30
33
|
"analytics.legend_completed": "Completed",
|
|
31
34
|
"analytics.legend_created": "Created",
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"analytics.completion_rate": "Процент выполнения",
|
|
27
27
|
"analytics.overdue_section": "Просроченные задачи",
|
|
28
28
|
"analytics.overdue_subtitle": "Задачи, которым нужно уделить внимание в первую очередь.",
|
|
29
|
+
"analytics.trend_chart": "График тренда",
|
|
30
|
+
"analytics.period": "Период",
|
|
31
|
+
"analytics.legend": "Легенда",
|
|
29
32
|
"analytics.trend_subtitle": "Динамика выполнения",
|
|
30
33
|
"analytics.legend_completed": "Выполнено",
|
|
31
34
|
"analytics.legend_created": "Создано",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Mock data for the analytics screen
|
|
2
|
+
data:
|
|
3
|
+
overview:
|
|
4
|
+
completed_today: 3
|
|
5
|
+
open_tasks: 8
|
|
6
|
+
overdue_tasks: 2
|
|
7
|
+
completion_rate: 67
|
|
8
|
+
|
|
9
|
+
trend:
|
|
10
|
+
- { label: "Mon", completed: 4, created: 2 }
|
|
11
|
+
- { label: "Tue", completed: 3, created: 5 }
|
|
12
|
+
- { label: "Wed", completed: 6, created: 3 }
|
|
13
|
+
- { label: "Thu", completed: 2, created: 4 }
|
|
14
|
+
- { label: "Fri", completed: 5, created: 1 }
|
|
15
|
+
- { label: "Sat", completed: 1, created: 0 }
|
|
16
|
+
- { label: "Sun", completed: 0, created: 2 }
|
|
17
|
+
|
|
18
|
+
overdue:
|
|
19
|
+
- id: "t6"
|
|
20
|
+
title: "Submit tax forms"
|
|
21
|
+
priority: high
|
|
22
|
+
due_date: "2026-03-15"
|
|
23
|
+
- id: "t7"
|
|
24
|
+
title: "Renew gym membership"
|
|
25
|
+
priority: low
|
|
26
|
+
due_date: "2026-03-14"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Mock data for the home screen
|
|
2
|
+
data:
|
|
3
|
+
tasks:
|
|
4
|
+
- id: "t1"
|
|
5
|
+
title: "Buy groceries"
|
|
6
|
+
due_date: "2026-03-18"
|
|
7
|
+
status: open
|
|
8
|
+
priority: high
|
|
9
|
+
- id: "t2"
|
|
10
|
+
title: "Finish project proposal"
|
|
11
|
+
due_date: "2026-03-19"
|
|
12
|
+
status: open
|
|
13
|
+
priority: medium
|
|
14
|
+
- id: "t3"
|
|
15
|
+
title: "Call dentist"
|
|
16
|
+
due_date: "2026-03-20"
|
|
17
|
+
status: done
|
|
18
|
+
priority: low
|
|
19
|
+
- id: "t4"
|
|
20
|
+
title: "Review pull request"
|
|
21
|
+
due_date: "2026-03-18"
|
|
22
|
+
status: open
|
|
23
|
+
priority: high
|
|
24
|
+
- id: "t5"
|
|
25
|
+
title: "Clean apartment"
|
|
26
|
+
due_date: "2026-03-21"
|
|
27
|
+
status: open
|
|
28
|
+
priority: medium
|
|
29
|
+
|
|
30
|
+
task_counts:
|
|
31
|
+
all: 12
|
|
32
|
+
open: 8
|
|
33
|
+
done: 4
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Mock data for the task_detail screen
|
|
2
|
+
data:
|
|
3
|
+
task:
|
|
4
|
+
id: "t1"
|
|
5
|
+
title: "Buy groceries"
|
|
6
|
+
status: open
|
|
7
|
+
priority: high
|
|
8
|
+
due_date: "2026-03-18"
|
|
9
|
+
notes: "Get milk, eggs, bread, and vegetables. Check the weekly deals at the store."
|
|
10
|
+
created_at: "2026-03-10"
|
|
11
|
+
updated_at: "2026-03-17"
|
|
12
|
+
|
|
13
|
+
params:
|
|
14
|
+
task_id: "t1"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# openuispec-sample — OpenUISpec v0.
|
|
2
|
-
spec_version: "0.
|
|
1
|
+
# openuispec-sample — OpenUISpec v0.2
|
|
2
|
+
spec_version: "0.2"
|
|
3
3
|
|
|
4
4
|
project:
|
|
5
5
|
name: "Todo Orbit"
|
|
@@ -9,13 +9,13 @@ project:
|
|
|
9
9
|
includes:
|
|
10
10
|
tokens: "./tokens/"
|
|
11
11
|
contracts: "./contracts/"
|
|
12
|
+
components: "./components/"
|
|
12
13
|
screens: "./screens/"
|
|
13
14
|
flows: "./flows/"
|
|
14
15
|
platform: "./platform/"
|
|
15
16
|
locales: "./locales/"
|
|
16
17
|
|
|
17
18
|
custom_contracts:
|
|
18
|
-
- "./contracts/x_task_trend_chart.yaml"
|
|
19
19
|
- "./contracts/x_schedule_preview.yaml"
|
|
20
20
|
|
|
21
21
|
i18n:
|
|
@@ -4,9 +4,6 @@ android:
|
|
|
4
4
|
min_sdk: 26
|
|
5
5
|
|
|
6
6
|
overrides:
|
|
7
|
-
x_task_trend_chart:
|
|
8
|
-
compact: { uses_canvas_rendering: true }
|
|
9
|
-
detail: { uses_canvas_rendering: true, supports_pointer_highlight: true }
|
|
10
7
|
x_schedule_preview:
|
|
11
8
|
compact: { uses_lazy_column: true }
|
|
12
9
|
detail: { uses_lazy_column: true, supports_animated_content: true }
|
|
@@ -3,9 +3,6 @@ ios:
|
|
|
3
3
|
min_version: "17.0"
|
|
4
4
|
|
|
5
5
|
overrides:
|
|
6
|
-
x_task_trend_chart:
|
|
7
|
-
compact: { uses_native_chart_framework: true }
|
|
8
|
-
detail: { uses_native_chart_framework: true, supports_rule_mark: true }
|
|
9
6
|
x_schedule_preview:
|
|
10
7
|
compact: { uses_timeline_view: true }
|
|
11
8
|
detail: { uses_timeline_view: true, supports_content_transition: true }
|
|
@@ -3,9 +3,6 @@ web:
|
|
|
3
3
|
language: typescript
|
|
4
4
|
|
|
5
5
|
overrides:
|
|
6
|
-
x_task_trend_chart:
|
|
7
|
-
compact: { implementation: "svg", responsive: true }
|
|
8
|
-
detail: { implementation: "svg", responsive: true, supports_tooltip: true }
|
|
9
6
|
x_schedule_preview:
|
|
10
7
|
compact: { implementation: "semantic_list", responsive: true }
|
|
11
8
|
detail: { implementation: "semantic_list", responsive: true, supports_staggered_updates: true }
|
|
@@ -102,14 +102,11 @@ analytics:
|
|
|
102
102
|
|
|
103
103
|
- id: trend_chart
|
|
104
104
|
margin_top: "spacing.lg"
|
|
105
|
-
|
|
106
|
-
variant: detail
|
|
105
|
+
component: task_trend_chart
|
|
107
106
|
props:
|
|
108
|
-
series: "trend"
|
|
109
107
|
metric: completed
|
|
110
108
|
period: "state.period"
|
|
111
109
|
show_legend: true
|
|
112
|
-
empty_message: "$t:analytics.empty_trend"
|
|
113
110
|
|
|
114
111
|
- id: overdue_header
|
|
115
112
|
margin_top: "spacing.xl"
|
package/mcp-server/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import YAML from "yaml";
|
|
|
25
25
|
import { takeScreenshot, takeScreenshotBatch } from "./screenshot.js";
|
|
26
26
|
import { takeAndroidScreenshot, takeAndroidScreenshotBatch } from "./screenshot-android.js";
|
|
27
27
|
import { takeIOSScreenshot, takeIOSScreenshotBatch } from "./screenshot-ios.js";
|
|
28
|
+
import { renderPreview } from "./preview.js";
|
|
28
29
|
|
|
29
30
|
// ── resolve project cwd ──────────────────────────────────────────────
|
|
30
31
|
|
|
@@ -121,8 +122,9 @@ WORKFLOW — each tool response includes a next_tool hint, follow it:
|
|
|
121
122
|
5. Remind the user to baseline when satisfied: openuispec drift --snapshot --target <t>
|
|
122
123
|
Do not baseline on your own initiative — the user decides when output is accepted.
|
|
123
124
|
|
|
124
|
-
FOCUSED GETTERS (prefer for incremental edits): get_screen, get_contract, get_tokens, get_locale
|
|
125
|
+
FOCUSED GETTERS (prefer for incremental edits): get_screen, get_contract, get_component, get_tokens, get_locale
|
|
125
126
|
SPEC AUTHORING: spec_types → spec_schema(type, summary?) → write YAML
|
|
127
|
+
PREVIEW: openuispec_preview(screen) → render spec as HTML with mock data, returns screenshot (no app needed)
|
|
126
128
|
SCREENSHOTS: screenshot (web), screenshot_android, screenshot_ios — single + batch variants
|
|
127
129
|
|
|
128
130
|
Skip only for purely non-UI requests.`,
|
|
@@ -387,10 +389,10 @@ server.registerTool(
|
|
|
387
389
|
server.registerTool(
|
|
388
390
|
"openuispec_validate",
|
|
389
391
|
{
|
|
390
|
-
description: "Validate spec files against JSON schemas. Returns validation errors grouped by type (manifest, tokens, screens, flows, platform, locales, contracts, semantic).",
|
|
392
|
+
description: "Validate spec files against JSON schemas. Returns validation errors grouped by type (manifest, tokens, screens, flows, platform, locales, contracts, components, semantic).",
|
|
391
393
|
inputSchema: {
|
|
392
394
|
groups: z
|
|
393
|
-
.array(z.enum(["manifest", "tokens", "screens", "flows", "platform", "locales", "contracts", "semantic"]))
|
|
395
|
+
.array(z.enum(["manifest", "tokens", "screens", "flows", "platform", "locales", "contracts", "components", "semantic"]))
|
|
394
396
|
.optional()
|
|
395
397
|
.describe("Specific groups to validate. If omitted, validates all groups."),
|
|
396
398
|
},
|
|
@@ -491,6 +493,7 @@ const SCHEMA_CATALOG: Record<string, { file: string; title: string; description:
|
|
|
491
493
|
platform: { file: "platform.schema.json", title: "Platform", description: "Platform-specific generation config: architecture, naming, CSS framework, component mapping" },
|
|
492
494
|
contract: { file: "contract.schema.json", title: "Contract", description: "Built-in UI contract definitions: variants, props, must_handle states, generation hints" },
|
|
493
495
|
"custom-contract":{ file: "custom-contract.schema.json", title: "Custom Contract", description: "User-defined UI contract definitions (x_ prefixed)" },
|
|
496
|
+
component: { file: "component.schema.json", title: "Component", description: "Reusable composition of contracts with named slots, states, variants, and layout" },
|
|
494
497
|
locale: { file: "locale.schema.json", title: "Locale", description: "Locale translation files: flat key-value string maps" },
|
|
495
498
|
"tokens/color": { file: "tokens/color.schema.json", title: "Color Tokens", description: "Color tokens: brand, surface, text, semantic, border groups with HSL ranges and contrast" },
|
|
496
499
|
"tokens/typography": { file: "tokens/typography.schema.json", title: "Typography Tokens", description: "Typography tokens: font families, sizes, weights, line heights, letter spacing" },
|
|
@@ -634,6 +637,53 @@ server.registerTool(
|
|
|
634
637
|
}
|
|
635
638
|
);
|
|
636
639
|
|
|
640
|
+
// ── tool: openuispec_get_component ──────────────────────────────────
|
|
641
|
+
|
|
642
|
+
server.registerTool(
|
|
643
|
+
"openuispec_get_component",
|
|
644
|
+
{
|
|
645
|
+
description: "Get a single component spec. Components are reusable compositions of contracts with named slots.",
|
|
646
|
+
inputSchema: {
|
|
647
|
+
name: z.string().describe("Component name, e.g. 'media_player'"),
|
|
648
|
+
variant: z.string().optional().describe("Optional variant name. If given, returns only that variant's definition."),
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
async ({ name, variant }) => {
|
|
652
|
+
try {
|
|
653
|
+
const projectDir = findProjectDir(projectCwd);
|
|
654
|
+
const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
655
|
+
const componentsDir = resolveSpecDir(projectDir, manifest, "components");
|
|
656
|
+
|
|
657
|
+
if (!existsSync(componentsDir)) {
|
|
658
|
+
return toolError(`Components directory not found: ${componentsDir}`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (const file of readdirSync(componentsDir).filter(f => f.endsWith(".yaml")).sort()) {
|
|
662
|
+
const filePath = join(componentsDir, file);
|
|
663
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
664
|
+
const content = YAML.parse(raw);
|
|
665
|
+
const componentName = Object.keys(content)[0];
|
|
666
|
+
if (componentName !== name) continue;
|
|
667
|
+
|
|
668
|
+
if (variant) {
|
|
669
|
+
const component = content[componentName];
|
|
670
|
+
const variantDef = component?.variants?.[variant];
|
|
671
|
+
if (!variantDef) {
|
|
672
|
+
return toolError(`Variant "${variant}" not found in component "${name}". Available variants: ${Object.keys(component?.variants ?? {}).join(", ")}`);
|
|
673
|
+
}
|
|
674
|
+
return toolResult({ name, variant, definition: variantDef });
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return toolResult({ name, path: relative(projectDir, filePath), content: raw });
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return toolError(`Component "${name}" not found in ${componentsDir}`);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
return toolError(err);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
);
|
|
686
|
+
|
|
637
687
|
// ── tool: openuispec_get_tokens ─────────────────────────────────────
|
|
638
688
|
|
|
639
689
|
server.registerTool(
|
|
@@ -763,9 +813,10 @@ server.registerTool(
|
|
|
763
813
|
full_page: z.boolean().optional().default(false).describe("Capture the full scrollable page instead of just the viewport"),
|
|
764
814
|
selector: z.string().optional().describe("CSS selector to screenshot a specific element instead of the full page"),
|
|
765
815
|
output_dir: z.string().optional().describe("Directory to save the screenshot PNG (relative to web app root). E.g. 'screenshots'. If omitted, only returns base64 in response."),
|
|
816
|
+
init_script: z.string().optional().describe("JavaScript to run before the page renders. Passed to the app via ?__ous_init=<base64> query param. The app's bootstrapper decodes and executes it — use for auth injection, role switching, or session setup."),
|
|
766
817
|
},
|
|
767
818
|
},
|
|
768
|
-
async ({ route, viewport, scale, theme, wait_for, full_page, selector, output_dir }) => {
|
|
819
|
+
async ({ route, viewport, scale, theme, wait_for, full_page, selector, output_dir, init_script }) => {
|
|
769
820
|
try {
|
|
770
821
|
return await takeScreenshot(projectCwd, {
|
|
771
822
|
route,
|
|
@@ -776,6 +827,7 @@ server.registerTool(
|
|
|
776
827
|
full_page,
|
|
777
828
|
selector,
|
|
778
829
|
output_dir,
|
|
830
|
+
init_script,
|
|
779
831
|
});
|
|
780
832
|
} catch (err) {
|
|
781
833
|
return toolError(err);
|
|
@@ -844,6 +896,7 @@ const webBatchCaptureSchema = z.object({
|
|
|
844
896
|
selector: z.string().optional().describe("CSS selector to screenshot a specific element"),
|
|
845
897
|
full_page: z.boolean().optional().describe("Capture full scrollable page"),
|
|
846
898
|
wait_for: z.number().optional().describe("Per-capture wait time in ms"),
|
|
899
|
+
init_script: z.string().optional().describe("Per-capture init script (overrides shared init_script for this capture)"),
|
|
847
900
|
});
|
|
848
901
|
|
|
849
902
|
server.registerTool(
|
|
@@ -856,11 +909,12 @@ server.registerTool(
|
|
|
856
909
|
scale: z.number().optional().default(2).describe("Device pixel ratio for all captures (default 2)"),
|
|
857
910
|
theme: z.enum(["light", "dark"]).optional().describe("Force color scheme for all captures"),
|
|
858
911
|
output_dir: z.string().optional().describe("Directory to save all PNGs (relative to web app root)"),
|
|
912
|
+
init_script: z.string().optional().describe("Shared init script for all captures. Passed via ?__ous_init=<base64>. Per-capture init_script overrides this."),
|
|
859
913
|
},
|
|
860
914
|
},
|
|
861
|
-
async ({ captures, viewport, scale, theme, output_dir }) => {
|
|
915
|
+
async ({ captures, viewport, scale, theme, output_dir, init_script }) => {
|
|
862
916
|
try {
|
|
863
|
-
return await takeScreenshotBatch(projectCwd, { captures, viewport, scale, theme, output_dir });
|
|
917
|
+
return await takeScreenshotBatch(projectCwd, { captures, viewport, scale, theme, output_dir, init_script });
|
|
864
918
|
} catch (err) {
|
|
865
919
|
return toolError(err);
|
|
866
920
|
}
|
|
@@ -928,6 +982,33 @@ server.registerTool(
|
|
|
928
982
|
}
|
|
929
983
|
);
|
|
930
984
|
|
|
985
|
+
// ── tool: openuispec_preview ────────────────────────────────────────────
|
|
986
|
+
|
|
987
|
+
server.registerTool(
|
|
988
|
+
"openuispec_preview",
|
|
989
|
+
{
|
|
990
|
+
description: "Render a screen spec as an HTML preview with mock data and return a screenshot. Uses token values, locale strings, and contract-to-HTML mapping to produce a visual approximation without generating a full app. Mock data should be placed in openuispec/mock/<screen>.yaml.",
|
|
991
|
+
inputSchema: {
|
|
992
|
+
screen: z.string().describe("Screen name (e.g. 'home', 'settings', 'task_detail')"),
|
|
993
|
+
size_class: z.enum(["compact", "regular", "expanded"]).optional().default("compact").describe("Adaptive size class — compact (phone), regular (tablet), expanded (desktop)"),
|
|
994
|
+
theme: z.enum(["light", "dark"]).optional().default("light").describe("Color theme"),
|
|
995
|
+
locale: z.string().optional().default("en").describe("Locale code for i18n strings"),
|
|
996
|
+
viewport: z.object({
|
|
997
|
+
width: z.number(),
|
|
998
|
+
height: z.number(),
|
|
999
|
+
}).optional().describe("Custom viewport size (overrides size_class default)"),
|
|
1000
|
+
include_html: z.boolean().optional().default(false).describe("Also return the rendered HTML string in the response"),
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
async ({ screen, size_class, theme, locale, viewport, include_html }) => {
|
|
1004
|
+
try {
|
|
1005
|
+
return await renderPreview(projectCwd, { screen, size_class, theme, locale, viewport, include_html });
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
return toolError(err);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
);
|
|
1011
|
+
|
|
931
1012
|
// ── start server ─────────────────────────────────────────────────────
|
|
932
1013
|
|
|
933
1014
|
export async function startMcpServer() {
|