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
@@ -1,11 +1,11 @@
1
- # OpenUISpec v0.1
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.1
7
- **Authors:** Rustam Samandarov
8
- **Last updated:** 2026-03-13
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 behavioral contract with typed props, a state machine, and accessibility requirements. If a state exists in the spec, the generated code must handle it.
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.1"
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 **behavioral family** — a category of UI elements that share the same role, props shape, state machine, and accessibility pattern. The AI maps each contract to the most appropriate native widget per platform.
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` | Finite state machine with valid transitions | Yes |
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 state machine states to data paths. This allows screen-level data to drive contract states declaratively, without requiring an explicit action.
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 machine (e.g., `loading` takes precedence over `disabled`)
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 state machines (see Section 4.7). For non-collection data, screens can use `condition:` to show appropriate UI.
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 a dedicated state machine (e.g., play/pause/seek for media)
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.1"
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 state in the state machine
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, state machines, data bindings), not visual details (padding values, animation curves). Platform-specific polish is expected to diverge from the spec; behavioral contracts are not.
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.1 — Draft specification. Subject to revision.*
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"