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.
- package/README.md +8 -7
- package/cli/index.ts +18 -12
- package/cli/init.ts +78 -13
- package/docs/cli.md +81 -27
- package/docs/file-formats.md +52 -2
- 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 +5 -3
- 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 +80 -3
- package/mcp-server/preview-render.ts +1922 -0
- package/mcp-server/preview.ts +292 -0
- package/mcp-server/screenshot-shared.ts +38 -0
- package/mcp-server/screenshot.ts +3 -32
- package/package.json +1 -1
- package/prepare/index.ts +1 -1
- package/schema/component.schema.json +278 -0
- package/schema/custom-contract.schema.json +2 -2
- package/schema/openuispec.schema.json +18 -8
- package/schema/screen.schema.json +12 -1
- package/schema/semantic-lint.ts +24 -2
- package/schema/validate.ts +21 -0
- package/scripts/regenerate-previews.ts +136 -0
- package/spec/{openuispec-v0.1.md → openuispec-v0.2.md} +275 -17
- package/examples/taskflow/openuispec/contracts/x_media_player.yaml +0 -185
- package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +0 -139
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# OpenUISpec v0.
|
|
1
|
+
# OpenUISpec v0.2
|
|
2
2
|
|
|
3
3
|
> A single source of truth design language for AI-native, platform-native app development.
|
|
4
4
|
|
|
5
|
-
**Status:** Draft
|
|
6
|
-
**Version:** 0.
|
|
7
|
-
**Authors:** Rustam Samandarov
|
|
8
|
-
**Last updated:** 2026-03-
|
|
5
|
+
**Status:** Draft
|
|
6
|
+
**Version:** 0.2
|
|
7
|
+
**Authors:** Rustam Samandarov
|
|
8
|
+
**Last updated:** 2026-03-19
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -19,7 +19,7 @@ OpenUISpec is a shared UI sync language for native products, optimized for solo
|
|
|
19
19
|
|
|
20
20
|
1. **Semantic over visual.** The spec defines behavioral intent, not pixel layouts. A "primary action trigger" maps to `Button` in SwiftUI, `Button` in Compose, and `<button>` in HTML — the spec never says "button."
|
|
21
21
|
2. **Constrained freedom.** Tokens use ranges, not exact values. Close enough to be recognizably the same brand; loose enough for each platform to feel native.
|
|
22
|
-
3. **Contract-driven.** Every component is a
|
|
22
|
+
3. **Contract-driven.** Every UI component is a reusable contract with typed props, UI interaction states (pressed, disabled, loading, error — not business logic states), and accessibility requirements. If a UI state exists in the spec, the generated code must handle it.
|
|
23
23
|
4. **AI-first authoring.** The spec is structured for machine consumption: strongly typed, validatable, with generation hints that tell AI what it must, should, and may produce.
|
|
24
24
|
5. **Platform respect.** iOS should feel like iOS. Android should feel like Android. Web should feel like the web. The spec preserves platform identity; it does not erase it.
|
|
25
25
|
|
|
@@ -49,6 +49,8 @@ project/
|
|
|
49
49
|
│ ├── surface.yaml
|
|
50
50
|
│ ├── collection.yaml
|
|
51
51
|
│ └── x_media_player.yaml # Custom contract (Section 12)
|
|
52
|
+
├── components/
|
|
53
|
+
│ └── media_player.yaml # Component composition (Section 15)
|
|
52
54
|
├── screens/
|
|
53
55
|
│ ├── home.yaml
|
|
54
56
|
│ ├── order_detail.yaml
|
|
@@ -68,7 +70,7 @@ project/
|
|
|
68
70
|
|
|
69
71
|
```yaml
|
|
70
72
|
# openuispec.yaml
|
|
71
|
-
spec_version: "0.
|
|
73
|
+
spec_version: "0.2"
|
|
72
74
|
project:
|
|
73
75
|
name: "MyApp"
|
|
74
76
|
description: "A sample application defined in OpenUISpec"
|
|
@@ -555,7 +557,7 @@ The `custom` section follows the same shape as `registry` categories. App-specif
|
|
|
555
557
|
|
|
556
558
|
## 4. Component contracts
|
|
557
559
|
|
|
558
|
-
Each contract defines a **
|
|
560
|
+
Each contract defines a **reusable UI component family** — a category of UI elements that share the same role, props shape, UI interaction states, and accessibility pattern. The AI maps each contract to the most appropriate native widget per platform. Contracts describe how components look and respond to user interaction — they are UI rendering specifications, not business logic or domain state machines.
|
|
559
561
|
|
|
560
562
|
### Contract anatomy
|
|
561
563
|
|
|
@@ -565,7 +567,7 @@ Every contract contains:
|
|
|
565
567
|
|---------|---------|----------|
|
|
566
568
|
| `semantic` | Human-readable description of what this family does | Yes |
|
|
567
569
|
| `props` | Typed inputs the component accepts | Yes |
|
|
568
|
-
| `states` |
|
|
570
|
+
| `states` | UI interaction states (e.g. idle, pressed, disabled, loading, error) with valid transitions | Yes |
|
|
569
571
|
| `a11y` | Accessibility role, label pattern, focus behavior | Yes |
|
|
570
572
|
| `tokens` | Visual token bindings per variant | Yes |
|
|
571
573
|
| `platform_mapping` | Default native widget per platform | Yes |
|
|
@@ -1765,7 +1767,7 @@ When omitted, the item contract's default variant is used.
|
|
|
1765
1767
|
|
|
1766
1768
|
#### `state_binding`
|
|
1767
1769
|
|
|
1768
|
-
Binds a contract's
|
|
1770
|
+
Binds a contract's UI interaction states to data paths. This allows screen-level data to drive contract states declaratively, without requiring an explicit action.
|
|
1769
1771
|
|
|
1770
1772
|
```yaml
|
|
1771
1773
|
- contract: action_trigger
|
|
@@ -1783,7 +1785,7 @@ Binds a contract's state machine states to data paths. This allows screen-level
|
|
|
1783
1785
|
- Values must be data paths that resolve to `bool`
|
|
1784
1786
|
- When the bound value is `true`, the contract transitions to that state
|
|
1785
1787
|
- When the bound value returns to `false`, the contract transitions back to `default`
|
|
1786
|
-
- If multiple state bindings are `true` simultaneously, priority follows the contract's state
|
|
1788
|
+
- If multiple state bindings are `true` simultaneously, priority follows the contract's state priority order (e.g., `loading` takes precedence over `disabled`)
|
|
1787
1789
|
|
|
1788
1790
|
---
|
|
1789
1791
|
|
|
@@ -2966,7 +2968,7 @@ props:
|
|
|
2966
2968
|
condition: "tasks.$empty"
|
|
2967
2969
|
```
|
|
2968
2970
|
|
|
2969
|
-
Collection contracts handle `$loading`, `$error`, and `$empty` automatically via their
|
|
2971
|
+
Collection contracts handle `$loading`, `$error`, and `$empty` automatically via their built-in UI states (see Section 4.7). For non-collection data, screens can use `condition:` to show appropriate UI.
|
|
2970
2972
|
|
|
2971
2973
|
### 10.9 AI generation requirements
|
|
2972
2974
|
|
|
@@ -3161,12 +3163,14 @@ Custom contracts allow spec authors to define domain-specific component families
|
|
|
3161
3163
|
Use a custom contract when the component:
|
|
3162
3164
|
|
|
3163
3165
|
- Has domain-specific behavior that doesn't map cleanly to any built-in family
|
|
3164
|
-
- Requires
|
|
3166
|
+
- Requires dedicated UI interaction states (e.g., play/pause/seek for media)
|
|
3165
3167
|
- Needs platform-specific libraries or frameworks not covered by core contracts
|
|
3166
3168
|
- Would clutter the core spec if included as a built-in
|
|
3167
3169
|
|
|
3168
3170
|
Do **not** use a custom contract when a built-in family with the right variant already covers the use case. A data card is `data_display`, not `x_data_card`.
|
|
3169
3171
|
|
|
3172
|
+
> **Prefer components over custom contracts** when the UI block is a composition of multiple contracts (e.g., a media player with play button, scrubber, and time label). Components (Section 15) provide named slots, states, variants, and screen-level overrides. Reserve `x_` custom contracts for truly atomic, domain-specific widgets that don't decompose into smaller contracts.
|
|
3173
|
+
|
|
3170
3174
|
### 12.2 Naming
|
|
3171
3175
|
|
|
3172
3176
|
Custom contract names **MUST**:
|
|
@@ -3271,7 +3275,7 @@ Custom contracts are registered in the root manifest via the `custom_contracts`
|
|
|
3271
3275
|
|
|
3272
3276
|
```yaml
|
|
3273
3277
|
# openuispec.yaml
|
|
3274
|
-
spec_version: "0.
|
|
3278
|
+
spec_version: "0.2"
|
|
3275
3279
|
project:
|
|
3276
3280
|
name: "MyApp"
|
|
3277
3281
|
|
|
@@ -3327,7 +3331,7 @@ ios:
|
|
|
3327
3331
|
|
|
3328
3332
|
**MUST:**
|
|
3329
3333
|
- Read and parse all registered custom contract definitions before generating code
|
|
3330
|
-
- Handle every declared
|
|
3334
|
+
- Handle every declared UI interaction state
|
|
3331
3335
|
- Apply `platform_mapping` to select the correct native component
|
|
3332
3336
|
- Include all `dependencies` in the generated project configuration (Package.swift, build.gradle, package.json)
|
|
3333
3337
|
- Implement all items listed in `generation.must_handle`
|
|
@@ -3822,7 +3826,7 @@ A drift detector compares:
|
|
|
3822
3826
|
- **Spec → Code**: Does the generated code match what the spec describes? (e.g., a button's action type, a screen's data sources, a flow's step order)
|
|
3823
3827
|
- **Code → Spec**: Does the current platform code contain UI decisions not reflected in the spec? (e.g., a new field added to a form, a navigation path changed)
|
|
3824
3828
|
|
|
3825
|
-
Drift detection is scoped to the semantic layer — it compares behavioral intent (contracts, props,
|
|
3829
|
+
Drift detection is scoped to the semantic layer — it compares behavioral intent (contracts, props, UI interaction states, data bindings), not visual details (padding values, animation curves). Platform-specific polish is expected to diverge from the spec; behavioral contracts are not.
|
|
3826
3830
|
|
|
3827
3831
|
Resolution strategies:
|
|
3828
3832
|
- **Update spec** — The code change is intentional; update the spec to match
|
|
@@ -3841,6 +3845,259 @@ This is the spec's primary value beyond code generation: it gives cross-platform
|
|
|
3841
3845
|
|
|
3842
3846
|
---
|
|
3843
3847
|
|
|
3848
|
+
## 15. Component composition
|
|
3849
|
+
|
|
3850
|
+
Components fill the gap between atomic contracts and full-page screens. A component is a **reusable composition of contracts with named slots** — think of a media player composed of a play button, scrubber, time label, and volume control, each backed by a base contract.
|
|
3851
|
+
|
|
3852
|
+
```
|
|
3853
|
+
Tokens → Contracts → Components → Screens → Flows
|
|
3854
|
+
(atomic) (composed) (full page)
|
|
3855
|
+
```
|
|
3856
|
+
|
|
3857
|
+
Components live in the `components/` directory referenced by `includes.components` in the manifest.
|
|
3858
|
+
|
|
3859
|
+
### 15.1 Component definition format
|
|
3860
|
+
|
|
3861
|
+
Each YAML file contains a single root key — the component name — mapping to a `component_def`:
|
|
3862
|
+
|
|
3863
|
+
```yaml
|
|
3864
|
+
# components/media_player.yaml
|
|
3865
|
+
media_player:
|
|
3866
|
+
semantic: "Plays audio and video media with transport controls"
|
|
3867
|
+
|
|
3868
|
+
props:
|
|
3869
|
+
source: { type: string, required: true }
|
|
3870
|
+
media_type: { type: enum, values: [audio, video], required: true }
|
|
3871
|
+
title: { type: string }
|
|
3872
|
+
|
|
3873
|
+
slots:
|
|
3874
|
+
play_button:
|
|
3875
|
+
contract: action_trigger
|
|
3876
|
+
variant: icon
|
|
3877
|
+
props: { label: "$t:media_player.play", icon: play }
|
|
3878
|
+
hideable: true
|
|
3879
|
+
scrubber:
|
|
3880
|
+
contract: input_field
|
|
3881
|
+
input_type: slider
|
|
3882
|
+
props: { label: "$t:media_player.progress" }
|
|
3883
|
+
hideable: true
|
|
3884
|
+
time_label:
|
|
3885
|
+
contract: data_display
|
|
3886
|
+
variant: caption
|
|
3887
|
+
hideable: true
|
|
3888
|
+
volume_control:
|
|
3889
|
+
contract: input_field
|
|
3890
|
+
input_type: slider
|
|
3891
|
+
hideable: true
|
|
3892
|
+
|
|
3893
|
+
layout:
|
|
3894
|
+
type: stack
|
|
3895
|
+
spacing: "spacing.sm"
|
|
3896
|
+
sections:
|
|
3897
|
+
- slot: play_button
|
|
3898
|
+
- slot: scrubber
|
|
3899
|
+
- layout:
|
|
3900
|
+
type: row
|
|
3901
|
+
sections:
|
|
3902
|
+
- slot: time_label
|
|
3903
|
+
- slot: volume_control
|
|
3904
|
+
|
|
3905
|
+
states:
|
|
3906
|
+
idle: { semantic: "No media loaded" }
|
|
3907
|
+
loading:
|
|
3908
|
+
semantic: "Buffering"
|
|
3909
|
+
hide_slots: [scrubber, volume_control]
|
|
3910
|
+
playing:
|
|
3911
|
+
semantic: "Actively playing"
|
|
3912
|
+
slot_overrides:
|
|
3913
|
+
play_button: { props: { icon: pause } }
|
|
3914
|
+
paused:
|
|
3915
|
+
semantic: "Paused at position"
|
|
3916
|
+
slot_overrides:
|
|
3917
|
+
play_button: { props: { icon: play } }
|
|
3918
|
+
|
|
3919
|
+
variants:
|
|
3920
|
+
mini:
|
|
3921
|
+
semantic: "Compact player for persistent bottom bar"
|
|
3922
|
+
hide_slots: [volume_control]
|
|
3923
|
+
layout:
|
|
3924
|
+
type: row
|
|
3925
|
+
sections:
|
|
3926
|
+
- slot: play_button
|
|
3927
|
+
- slot: scrubber
|
|
3928
|
+
- slot: time_label
|
|
3929
|
+
fullscreen:
|
|
3930
|
+
semantic: "Full-screen immersive player"
|
|
3931
|
+
tokens: { background: "#000000" }
|
|
3932
|
+
|
|
3933
|
+
tokens:
|
|
3934
|
+
background: "color.surface.secondary"
|
|
3935
|
+
radius: "spacing.md"
|
|
3936
|
+
padding: "spacing.md"
|
|
3937
|
+
|
|
3938
|
+
a11y:
|
|
3939
|
+
role: "group"
|
|
3940
|
+
label: "props.title"
|
|
3941
|
+
|
|
3942
|
+
platform_mapping:
|
|
3943
|
+
ios: { component: "VideoPlayer", framework: "AVKit" }
|
|
3944
|
+
android: { component: "PlayerView", library: "androidx.media3" }
|
|
3945
|
+
web: { element: "div", role: "region" }
|
|
3946
|
+
|
|
3947
|
+
generation:
|
|
3948
|
+
must_handle: ["All slots must render with correct contract types"]
|
|
3949
|
+
```
|
|
3950
|
+
|
|
3951
|
+
### 15.2 Anatomy
|
|
3952
|
+
|
|
3953
|
+
| Section | Purpose | Required |
|
|
3954
|
+
|---------|---------|----------|
|
|
3955
|
+
| `semantic` | Human-readable description of what this component does | Yes |
|
|
3956
|
+
| `slots` | Named contract instances that make up the component | Yes |
|
|
3957
|
+
| `props` | Typed inputs the component accepts (passed via data binding) | No |
|
|
3958
|
+
| `layout` | Spatial arrangement of slots using layout primitives | No |
|
|
3959
|
+
| `states` | Composite states that control slot visibility and props | No |
|
|
3960
|
+
| `variants` | Named presets that hide slots, change layout, or override tokens | No |
|
|
3961
|
+
| `tokens` | Visual token bindings for the component container | No |
|
|
3962
|
+
| `a11y` | Accessibility role and label pattern | No |
|
|
3963
|
+
| `platform_mapping` | Per-platform native component hints | No |
|
|
3964
|
+
| `dependencies` | Platform-specific library requirements | No |
|
|
3965
|
+
| `generation` | AI generation hints (must_handle, should_handle, may_handle) | No |
|
|
3966
|
+
| `test_cases` | Behavioral verification scenarios | No |
|
|
3967
|
+
|
|
3968
|
+
### 15.3 Slots
|
|
3969
|
+
|
|
3970
|
+
A **slot** is a named position within a component that renders a base contract. Each slot specifies:
|
|
3971
|
+
|
|
3972
|
+
| Field | Type | Required | Description |
|
|
3973
|
+
|-------|------|----------|-------------|
|
|
3974
|
+
| `contract` | `contract_ref` | Yes | The base contract family (e.g. `action_trigger`, `data_display`) |
|
|
3975
|
+
| `variant` | `string` | No | Default variant for the contract |
|
|
3976
|
+
| `input_type` | `string` | No | Input type (for `input_field` contracts) |
|
|
3977
|
+
| `props` | `object` | No | Default props passed to the contract |
|
|
3978
|
+
| `hideable` | `bool` | No | Whether this slot can be hidden from screens or by states |
|
|
3979
|
+
| `tokens_override` | `object` | No | Token overrides for this slot |
|
|
3980
|
+
|
|
3981
|
+
Slots reference **base contracts only** — components cannot nest other components (v1 keeps it flat).
|
|
3982
|
+
|
|
3983
|
+
### 15.4 States
|
|
3984
|
+
|
|
3985
|
+
Component states are **composite states** that control slot visibility and override slot props. They differ from contract states (Section 4): contract states describe UI interaction states of a single widget (pressed, disabled, loading); component states describe the state of the entire composition (playing, paused, buffering).
|
|
3986
|
+
|
|
3987
|
+
Each state can specify:
|
|
3988
|
+
|
|
3989
|
+
| Field | Type | Description |
|
|
3990
|
+
|-------|------|-------------|
|
|
3991
|
+
| `semantic` | `string` | What this state means |
|
|
3992
|
+
| `hide_slots` | `string[]` | Slots to hide when this state is active |
|
|
3993
|
+
| `slot_overrides` | `object` | Per-slot prop/variant overrides |
|
|
3994
|
+
| `transitions_to` | `string[]` | Valid next states |
|
|
3995
|
+
|
|
3996
|
+
### 15.5 Variants
|
|
3997
|
+
|
|
3998
|
+
Variants are named presets that change the component's appearance or slot arrangement:
|
|
3999
|
+
|
|
4000
|
+
```yaml
|
|
4001
|
+
variants:
|
|
4002
|
+
mini:
|
|
4003
|
+
semantic: "Compact player for persistent bottom bar"
|
|
4004
|
+
hide_slots: [volume_control]
|
|
4005
|
+
layout:
|
|
4006
|
+
type: row
|
|
4007
|
+
sections:
|
|
4008
|
+
- slot: play_button
|
|
4009
|
+
- slot: scrubber
|
|
4010
|
+
- slot: time_label
|
|
4011
|
+
```
|
|
4012
|
+
|
|
4013
|
+
A variant can specify:
|
|
4014
|
+
- `semantic` — What this variant is for
|
|
4015
|
+
- `hide_slots` — Slots to remove in this variant
|
|
4016
|
+
- `layout` — Alternative slot arrangement
|
|
4017
|
+
- `tokens` — Token overrides for this variant
|
|
4018
|
+
- `slot_overrides` — Per-slot prop/variant overrides
|
|
4019
|
+
|
|
4020
|
+
### 15.6 Slot resolution order
|
|
4021
|
+
|
|
4022
|
+
When a component is rendered, slot properties are resolved by layering overrides from most general to most specific:
|
|
4023
|
+
|
|
4024
|
+
```
|
|
4025
|
+
slot default → variant override → state override → screen-level override
|
|
4026
|
+
```
|
|
4027
|
+
|
|
4028
|
+
Most specific wins. Screen-level overrides always have final say. If a slot is hidden at any level (variant `hide_slots`, state `hide_slots`, or screen-level `hidden: true`), it is not rendered.
|
|
4029
|
+
|
|
4030
|
+
### 15.7 Usage in screens
|
|
4031
|
+
|
|
4032
|
+
Components are used in screens via the `component` key (instead of `contract`):
|
|
4033
|
+
|
|
4034
|
+
```yaml
|
|
4035
|
+
# screens/task_detail.yaml
|
|
4036
|
+
- component: media_player
|
|
4037
|
+
variant: mini
|
|
4038
|
+
props:
|
|
4039
|
+
source: "{task.attachment.url}"
|
|
4040
|
+
media_type: "{task.attachment.media_type}"
|
|
4041
|
+
slots:
|
|
4042
|
+
volume_control: { hidden: true }
|
|
4043
|
+
play_button:
|
|
4044
|
+
variant: branded
|
|
4045
|
+
tokens_override: { background: "color.brand.primary" }
|
|
4046
|
+
```
|
|
4047
|
+
|
|
4048
|
+
**Screen-level slot overrides** can:
|
|
4049
|
+
- Change the slot's `variant`
|
|
4050
|
+
- Override `props`
|
|
4051
|
+
- Apply `tokens_override`
|
|
4052
|
+
- Set `hidden: true` to suppress the slot (only if the slot is declared `hideable: true`)
|
|
4053
|
+
|
|
4054
|
+
### 15.8 Components vs. custom contracts
|
|
4055
|
+
|
|
4056
|
+
| Aspect | Component | Custom contract (`x_`) |
|
|
4057
|
+
|--------|-----------|----------------------|
|
|
4058
|
+
| Structure | Composition of base contracts via slots | Single atomic widget |
|
|
4059
|
+
| Customization | Slots can be restyled, repositioned, swapped, or hidden | Props and tokens only |
|
|
4060
|
+
| States | Composite states controlling slot visibility | UI interaction states (pressed, loading, error) |
|
|
4061
|
+
| Layout | Defines spatial arrangement of slots | Opaque — platform decides layout |
|
|
4062
|
+
| Use when | The UI block decomposes into smaller contracts | The widget is truly atomic and domain-specific |
|
|
4063
|
+
| Examples | Media player, wizard, conversation timeline | Status badge, SLA indicator, sparkline chart |
|
|
4064
|
+
|
|
4065
|
+
### 15.9 Registration
|
|
4066
|
+
|
|
4067
|
+
Components are registered in the manifest's `includes` section:
|
|
4068
|
+
|
|
4069
|
+
```yaml
|
|
4070
|
+
# openuispec.yaml
|
|
4071
|
+
includes:
|
|
4072
|
+
tokens: "./tokens/"
|
|
4073
|
+
contracts: "./contracts/"
|
|
4074
|
+
components: "./components/"
|
|
4075
|
+
screens: "./screens/"
|
|
4076
|
+
# ...
|
|
4077
|
+
```
|
|
4078
|
+
|
|
4079
|
+
Each `.yaml` file in the components directory defines one component. The file must contain exactly one root key matching the component name (no `x_` prefix — that is reserved for custom contracts).
|
|
4080
|
+
|
|
4081
|
+
### 15.10 AI generation requirements
|
|
4082
|
+
|
|
4083
|
+
**MUST:**
|
|
4084
|
+
- Read all component definitions before generating code
|
|
4085
|
+
- Render every non-hidden slot with the correct base contract type
|
|
4086
|
+
- Apply the slot resolution order (Section 15.6) correctly
|
|
4087
|
+
- Support variant selection from screens
|
|
4088
|
+
- Include `dependencies` in generated project configuration
|
|
4089
|
+
|
|
4090
|
+
**SHOULD:**
|
|
4091
|
+
- Implement component states with correct slot visibility transitions
|
|
4092
|
+
- Apply component-level and slot-level token overrides
|
|
4093
|
+
- Generate accessibility support matching the `a11y` definition
|
|
4094
|
+
|
|
4095
|
+
**MAY:**
|
|
4096
|
+
- Generate test code based on `test_cases`
|
|
4097
|
+
- Add platform-specific enhancements via `platform_mapping`
|
|
4098
|
+
|
|
4099
|
+
---
|
|
4100
|
+
|
|
3844
4101
|
## Appendix A: Type reference
|
|
3845
4102
|
|
|
3846
4103
|
| Type | Description | Example |
|
|
@@ -3853,6 +4110,7 @@ This is the spec's primary value beyond code generation: it gives cross-platform
|
|
|
3853
4110
|
| `media_ref` | Image/video reference | `"assets/hero.jpg"` |
|
|
3854
4111
|
| `color_ref` | Token path | `"color.brand.primary"` |
|
|
3855
4112
|
| `component_ref` | Inline contract instance | `{ contract: data_display, ... }` |
|
|
4113
|
+
| `composed_component_ref` | Component name from `components/` | `"media_player"` |
|
|
3856
4114
|
| `contract_ref` | Contract family name | `"action_trigger"` |
|
|
3857
4115
|
| `screen_ref` | Screen identifier | `"screens/order_detail"` |
|
|
3858
4116
|
| `action` | Action definition (see Section 9) | `{ type: navigate, destination: "..." }` |
|
|
@@ -3884,4 +4142,4 @@ This is the spec's primary value beyond code generation: it gives cross-platform
|
|
|
3884
4142
|
|
|
3885
4143
|
---
|
|
3886
4144
|
|
|
3887
|
-
*OpenUISpec v0.
|
|
4145
|
+
*OpenUISpec v0.2 — Draft specification. Subject to revision.*
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
# ============================================================
|
|
2
|
-
# Custom Contract: x_media_player
|
|
3
|
-
# ============================================================
|
|
4
|
-
# A media playback contract for audio and video content.
|
|
5
|
-
# Demonstrates the custom contract extension mechanism
|
|
6
|
-
# (see spec Section 12).
|
|
7
|
-
# ============================================================
|
|
8
|
-
|
|
9
|
-
x_media_player:
|
|
10
|
-
semantic: "Plays audio and video media with transport controls, state management, and platform-native playback"
|
|
11
|
-
|
|
12
|
-
props:
|
|
13
|
-
source: { type: string, required: true, description: "Media URL or asset path" }
|
|
14
|
-
media_type:
|
|
15
|
-
type: enum
|
|
16
|
-
values: [audio, video]
|
|
17
|
-
required: true
|
|
18
|
-
description: "Type of media content"
|
|
19
|
-
variant:
|
|
20
|
-
type: enum
|
|
21
|
-
values: [inline, fullscreen, mini]
|
|
22
|
-
default: inline
|
|
23
|
-
description: "Player presentation style"
|
|
24
|
-
autoplay: { type: bool, default: false, description: "Begin playback automatically when source loads" }
|
|
25
|
-
loop: { type: bool, default: false, description: "Restart playback when media ends" }
|
|
26
|
-
show_controls: { type: bool, default: true, description: "Display transport controls" }
|
|
27
|
-
poster: { type: media_ref, required: false, description: "Preview image shown before playback (video only)" }
|
|
28
|
-
title: { type: string, required: false, description: "Media title for display and accessibility" }
|
|
29
|
-
subtitle: { type: string, required: false, description: "Secondary text (artist, channel, etc.)" }
|
|
30
|
-
playback_rate: { type: enum, values: ["0.5", "0.75", "1", "1.25", "1.5", "2"], default: "1", description: "Playback speed multiplier" }
|
|
31
|
-
muted: { type: bool, default: false, description: "Start with audio muted" }
|
|
32
|
-
|
|
33
|
-
states:
|
|
34
|
-
idle:
|
|
35
|
-
semantic: "No media loaded or playback not started"
|
|
36
|
-
transitions_to: [loading]
|
|
37
|
-
visual: "Shows poster image or placeholder; controls hidden or minimal"
|
|
38
|
-
loading:
|
|
39
|
-
semantic: "Media source is buffering"
|
|
40
|
-
transitions_to: [playing, error]
|
|
41
|
-
duration: "motion.standard"
|
|
42
|
-
feedback: "Loading indicator visible"
|
|
43
|
-
visual: "Spinner or progress bar overlaid on poster"
|
|
44
|
-
playing:
|
|
45
|
-
semantic: "Media is actively playing"
|
|
46
|
-
transitions_to: [paused, ended, loading, error]
|
|
47
|
-
behavior: "Progress bar advances, elapsed time updates"
|
|
48
|
-
visual: "Active playback with visible progress and controls"
|
|
49
|
-
paused:
|
|
50
|
-
semantic: "Playback is paused at current position"
|
|
51
|
-
transitions_to: [playing, loading]
|
|
52
|
-
visual: "Frozen frame (video) or paused waveform (audio); play button prominent"
|
|
53
|
-
ended:
|
|
54
|
-
semantic: "Playback reached the end of the media"
|
|
55
|
-
transitions_to: [playing, loading]
|
|
56
|
-
behavior: "If loop is true, transitions to playing automatically"
|
|
57
|
-
visual: "Replay button visible; poster or last frame shown"
|
|
58
|
-
error:
|
|
59
|
-
semantic: "Media failed to load or playback encountered an error"
|
|
60
|
-
transitions_to: [loading]
|
|
61
|
-
feedback: "Error message displayed with retry option"
|
|
62
|
-
visual: "Error icon and message; retry button visible"
|
|
63
|
-
|
|
64
|
-
a11y:
|
|
65
|
-
role: "media"
|
|
66
|
-
label: "props.title"
|
|
67
|
-
traits:
|
|
68
|
-
playing: { announces: "Playing" }
|
|
69
|
-
paused: { announces: "Paused" }
|
|
70
|
-
loading: { announces: "Loading media" }
|
|
71
|
-
error: { announces: "Media playback error" }
|
|
72
|
-
focus:
|
|
73
|
-
keyboard:
|
|
74
|
-
play_pause: "Space"
|
|
75
|
-
seek_forward: "ArrowRight"
|
|
76
|
-
seek_backward: "ArrowLeft"
|
|
77
|
-
volume_up: "ArrowUp"
|
|
78
|
-
volume_down: "ArrowDown"
|
|
79
|
-
fullscreen: "f"
|
|
80
|
-
mute: "m"
|
|
81
|
-
|
|
82
|
-
tokens:
|
|
83
|
-
inline:
|
|
84
|
-
min_height: [200, 280]
|
|
85
|
-
radius: "spacing.md"
|
|
86
|
-
background: "color.surface.secondary"
|
|
87
|
-
controls_background: "rgba(0, 0, 0, 0.5)"
|
|
88
|
-
controls_color: "#FFFFFF"
|
|
89
|
-
progress_active: "color.brand.primary"
|
|
90
|
-
progress_inactive: "rgba(255, 255, 255, 0.3)"
|
|
91
|
-
progress_height: [3, 4]
|
|
92
|
-
title_style: "typography.caption"
|
|
93
|
-
subtitle_style: "typography.small"
|
|
94
|
-
fullscreen:
|
|
95
|
-
background: "#000000"
|
|
96
|
-
controls_background: "rgba(0, 0, 0, 0.6)"
|
|
97
|
-
controls_color: "#FFFFFF"
|
|
98
|
-
progress_active: "color.brand.primary"
|
|
99
|
-
progress_inactive: "rgba(255, 255, 255, 0.3)"
|
|
100
|
-
progress_height: [4, 6]
|
|
101
|
-
title_style: "typography.subtitle"
|
|
102
|
-
subtitle_style: "typography.body"
|
|
103
|
-
mini:
|
|
104
|
-
height: [48, 64]
|
|
105
|
-
radius: "spacing.sm"
|
|
106
|
-
background: "color.surface.secondary"
|
|
107
|
-
progress_active: "color.brand.primary"
|
|
108
|
-
progress_height: [2, 3]
|
|
109
|
-
title_style: "typography.caption"
|
|
110
|
-
|
|
111
|
-
platform_mapping:
|
|
112
|
-
ios:
|
|
113
|
-
inline: { component: "VideoPlayer", framework: "AVKit" }
|
|
114
|
-
fullscreen: { component: "AVPlayerViewController", framework: "AVKit" }
|
|
115
|
-
mini: { component: "Custom mini player view", framework: "AVKit" }
|
|
116
|
-
audio: { component: "Custom audio player", framework: "AVFoundation" }
|
|
117
|
-
android:
|
|
118
|
-
inline: { component: "PlayerView", library: "androidx.media3" }
|
|
119
|
-
fullscreen: { component: "PlayerView (fullscreen activity)", library: "androidx.media3" }
|
|
120
|
-
mini: { component: "Custom mini player composable", library: "androidx.media3" }
|
|
121
|
-
audio: { component: "PlayerView (audio mode)", library: "androidx.media3" }
|
|
122
|
-
web:
|
|
123
|
-
inline: { element: "video", fallback: "audio" }
|
|
124
|
-
fullscreen: { element: "video", api: "Fullscreen API" }
|
|
125
|
-
mini: { element: "Custom mini player component" }
|
|
126
|
-
audio: { element: "audio" }
|
|
127
|
-
|
|
128
|
-
dependencies:
|
|
129
|
-
ios:
|
|
130
|
-
frameworks: [AVKit, AVFoundation]
|
|
131
|
-
android:
|
|
132
|
-
libraries: ["androidx.media3:media3-ui", "androidx.media3:media3-exoplayer"]
|
|
133
|
-
web:
|
|
134
|
-
packages: []
|
|
135
|
-
|
|
136
|
-
generation:
|
|
137
|
-
must_handle:
|
|
138
|
-
- "All 6 states (idle, loading, playing, paused, ended, error) with correct transitions"
|
|
139
|
-
- "Keyboard shortcuts for accessibility (Space, arrows, f, m)"
|
|
140
|
-
- "Poster/placeholder display in idle state"
|
|
141
|
-
- "Error state with retry action"
|
|
142
|
-
- "Platform-native player component per platform_mapping"
|
|
143
|
-
should_handle:
|
|
144
|
-
- "Playback rate selection UI"
|
|
145
|
-
- "Progress bar with seek interaction"
|
|
146
|
-
- "Volume control"
|
|
147
|
-
- "Elapsed / remaining time display"
|
|
148
|
-
- "Mini variant with compact controls"
|
|
149
|
-
may_handle:
|
|
150
|
-
- "Picture-in-picture support (iOS, web)"
|
|
151
|
-
- "AirPlay / Cast integration"
|
|
152
|
-
- "Subtitle / closed caption support"
|
|
153
|
-
- "Background audio playback"
|
|
154
|
-
- "Gesture controls (swipe to seek, pinch to zoom)"
|
|
155
|
-
|
|
156
|
-
test_cases:
|
|
157
|
-
- id: play_pause_toggle
|
|
158
|
-
description: "Tapping play starts playback; tapping pause freezes it"
|
|
159
|
-
given: "Player is in idle state with a valid source"
|
|
160
|
-
when: "User taps the play button"
|
|
161
|
-
then: "State transitions to loading, then playing; tapping pause transitions to paused"
|
|
162
|
-
|
|
163
|
-
- id: error_retry
|
|
164
|
-
description: "Failed media shows error with retry"
|
|
165
|
-
given: "Player source URL is unreachable"
|
|
166
|
-
when: "Player attempts to load the media"
|
|
167
|
-
then: "State transitions to error; retry button is visible; tapping retry transitions to loading"
|
|
168
|
-
|
|
169
|
-
- id: ended_loop
|
|
170
|
-
description: "Loop restarts playback at end"
|
|
171
|
-
given: "Player is playing with loop=true"
|
|
172
|
-
when: "Media reaches the end"
|
|
173
|
-
then: "State transitions to playing from the beginning without user interaction"
|
|
174
|
-
|
|
175
|
-
- id: keyboard_controls
|
|
176
|
-
description: "Keyboard shortcuts control playback"
|
|
177
|
-
given: "Player is focused and in playing state"
|
|
178
|
-
when: "User presses Space"
|
|
179
|
-
then: "State transitions to paused; pressing Space again transitions to playing"
|
|
180
|
-
|
|
181
|
-
- id: fullscreen_variant
|
|
182
|
-
description: "Fullscreen variant fills the viewport"
|
|
183
|
-
given: "Player variant is fullscreen"
|
|
184
|
-
when: "Player renders"
|
|
185
|
-
then: "Player fills the screen with black background; controls overlay the content"
|