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
|
@@ -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
|
|
|
@@ -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"
|
|
@@ -3167,6 +3169,8 @@ Use a custom contract when the component:
|
|
|
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
|
|
|
@@ -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"
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
x_task_trend_chart:
|
|
2
|
-
semantic: "Visualizes task creation and completion trends over time for productivity analysis"
|
|
3
|
-
|
|
4
|
-
props:
|
|
5
|
-
series: { type: "list<trend_point>", required: true, description: "Ordered trend points for the selected period" }
|
|
6
|
-
metric:
|
|
7
|
-
type: enum
|
|
8
|
-
values: [completed, created, completion_rate]
|
|
9
|
-
required: true
|
|
10
|
-
description: "Primary metric emphasized in the chart"
|
|
11
|
-
period:
|
|
12
|
-
type: enum
|
|
13
|
-
values: [week, month, quarter]
|
|
14
|
-
required: true
|
|
15
|
-
description: "Time range represented by the chart"
|
|
16
|
-
variant:
|
|
17
|
-
type: enum
|
|
18
|
-
values: [compact, detail]
|
|
19
|
-
default: compact
|
|
20
|
-
description: "Chart presentation density"
|
|
21
|
-
highlighted_index: { type: int, required: false, description: "Optional highlighted datapoint" }
|
|
22
|
-
show_legend: { type: bool, default: true, description: "Whether to render a legend for created/completed series" }
|
|
23
|
-
empty_message: { type: string, required: false, description: "Message to show when series is empty" }
|
|
24
|
-
|
|
25
|
-
states:
|
|
26
|
-
idle:
|
|
27
|
-
semantic: "Chart has not loaded data yet"
|
|
28
|
-
transitions_to: [loading]
|
|
29
|
-
visual: "Reserved chart frame with no plotted data"
|
|
30
|
-
loading:
|
|
31
|
-
semantic: "Trend data is loading"
|
|
32
|
-
transitions_to: [ready, empty, error]
|
|
33
|
-
feedback: "Skeleton chart and loading copy shown"
|
|
34
|
-
visual: "Placeholder bars or line skeleton"
|
|
35
|
-
ready:
|
|
36
|
-
semantic: "Trend data is available and rendered"
|
|
37
|
-
transitions_to: [highlighted, loading, error]
|
|
38
|
-
behavior: "Chart axes, series, and legend are visible"
|
|
39
|
-
highlighted:
|
|
40
|
-
semantic: "One datapoint is emphasized"
|
|
41
|
-
transitions_to: [ready]
|
|
42
|
-
behavior: "Highlighted datapoint shows stronger color and tooltip/callout"
|
|
43
|
-
empty:
|
|
44
|
-
semantic: "No trend points available"
|
|
45
|
-
transitions_to: [loading]
|
|
46
|
-
feedback: "Empty analytics message displayed"
|
|
47
|
-
visual: "Empty state in chart container"
|
|
48
|
-
error:
|
|
49
|
-
semantic: "Trend data failed to load"
|
|
50
|
-
transitions_to: [loading]
|
|
51
|
-
feedback: "Inline error with retry affordance"
|
|
52
|
-
visual: "Error indicator in chart container"
|
|
53
|
-
|
|
54
|
-
a11y:
|
|
55
|
-
role: "img"
|
|
56
|
-
label: "Analytics trend chart"
|
|
57
|
-
traits:
|
|
58
|
-
loading: { announces: "Loading analytics chart" }
|
|
59
|
-
empty: { announces: "No analytics data available" }
|
|
60
|
-
error: { announces: "Analytics chart failed to load" }
|
|
61
|
-
focus:
|
|
62
|
-
keyboard:
|
|
63
|
-
next_point: "ArrowRight"
|
|
64
|
-
previous_point: "ArrowLeft"
|
|
65
|
-
details: "Enter"
|
|
66
|
-
|
|
67
|
-
tokens:
|
|
68
|
-
compact:
|
|
69
|
-
min_height: 220
|
|
70
|
-
padding: "spacing.md"
|
|
71
|
-
background: "color.surface.secondary"
|
|
72
|
-
border: { width: 1, color: "color.border.default" }
|
|
73
|
-
radius: "spacing.sm"
|
|
74
|
-
axis_color: "color.text.tertiary"
|
|
75
|
-
completed_color: "color.brand.primary"
|
|
76
|
-
created_color: "color.brand.secondary"
|
|
77
|
-
grid_color: "color.border.default"
|
|
78
|
-
label_style: "typography.caption"
|
|
79
|
-
detail:
|
|
80
|
-
min_height: 320
|
|
81
|
-
padding: "spacing.lg"
|
|
82
|
-
background: "color.surface.primary"
|
|
83
|
-
border: { width: 1, color: "color.border.default" }
|
|
84
|
-
radius: "spacing.sm"
|
|
85
|
-
axis_color: "color.text.tertiary"
|
|
86
|
-
completed_color: "color.brand.primary"
|
|
87
|
-
created_color: "color.brand.secondary"
|
|
88
|
-
grid_color: "color.border.default"
|
|
89
|
-
label_style: "typography.body_sm"
|
|
90
|
-
|
|
91
|
-
platform_mapping:
|
|
92
|
-
ios:
|
|
93
|
-
compact: { component: "Chart", framework: "Swift Charts" }
|
|
94
|
-
detail: { component: "Chart", framework: "Swift Charts" }
|
|
95
|
-
android:
|
|
96
|
-
compact: { component: "Canvas chart composable", library: "custom compose drawing" }
|
|
97
|
-
detail: { component: "Canvas chart composable", library: "custom compose drawing" }
|
|
98
|
-
web:
|
|
99
|
-
compact: { component: "SVG chart component" }
|
|
100
|
-
detail: { component: "SVG chart component" }
|
|
101
|
-
|
|
102
|
-
dependencies:
|
|
103
|
-
ios:
|
|
104
|
-
frameworks: ["Charts"]
|
|
105
|
-
android:
|
|
106
|
-
libraries: []
|
|
107
|
-
web:
|
|
108
|
-
packages: []
|
|
109
|
-
|
|
110
|
-
generation:
|
|
111
|
-
must_handle:
|
|
112
|
-
- "Render completed and created series for the supplied period"
|
|
113
|
-
- "Respect compact and detail variants with different density"
|
|
114
|
-
- "Provide accessible summary text for the focused datapoint"
|
|
115
|
-
- "Handle loading, empty, and error states in the chart container"
|
|
116
|
-
should_handle:
|
|
117
|
-
- "Animate transitions when highlighted_index changes"
|
|
118
|
-
- "Render a legend when show_legend is true"
|
|
119
|
-
- "Support touch and pointer hover highlighting"
|
|
120
|
-
may_handle:
|
|
121
|
-
- "Tooltips for datapoints"
|
|
122
|
-
- "Trend delta badges and annotations"
|
|
123
|
-
|
|
124
|
-
test_cases:
|
|
125
|
-
- id: ready_state_renders_series
|
|
126
|
-
description: "Chart renders series lines or bars when data is present"
|
|
127
|
-
given: "Chart receives 7 trend points for period week"
|
|
128
|
-
when: "The component enters ready state"
|
|
129
|
-
then: "Both created and completed series are visible with labels and axes"
|
|
130
|
-
- id: empty_state_message
|
|
131
|
-
description: "Chart shows an empty message when no data exists"
|
|
132
|
-
given: "Chart receives an empty series"
|
|
133
|
-
when: "The component enters empty state"
|
|
134
|
-
then: "The empty_message or default empty copy is rendered in the chart frame"
|
|
135
|
-
- id: highlighted_point
|
|
136
|
-
description: "Highlighted point receives emphasis"
|
|
137
|
-
given: "Chart is in ready state with highlighted_index set"
|
|
138
|
-
when: "The highlighted datapoint is rendered"
|
|
139
|
-
then: "That datapoint is visually emphasized and exposed to accessibility"
|