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.
Files changed (63) hide show
  1. package/README.md +6 -5
  2. package/cli/index.ts +18 -12
  3. package/cli/init.ts +79 -13
  4. package/docs/cli.md +134 -27
  5. package/docs/file-formats.md +51 -1
  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 +4 -2
  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 +87 -6
  49. package/mcp-server/preview-render.ts +1922 -0
  50. package/mcp-server/preview.ts +292 -0
  51. package/mcp-server/screenshot-shared.ts +41 -4
  52. package/mcp-server/screenshot.ts +283 -97
  53. package/package.json +1 -1
  54. package/prepare/index.ts +1 -1
  55. package/schema/component.schema.json +278 -0
  56. package/schema/openuispec.schema.json +5 -1
  57. package/schema/screen.schema.json +12 -1
  58. package/schema/semantic-lint.ts +29 -5
  59. package/schema/validate.ts +21 -0
  60. package/scripts/regenerate-previews.ts +136 -0
  61. package/spec/{openuispec-v0.1.md → openuispec-v0.2.md} +266 -8
  62. package/examples/taskflow/openuispec/contracts/x_media_player.yaml +0 -185
  63. 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
 
@@ -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"
@@ -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.1"
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.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"
@@ -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"