openuispec 0.1.0
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/LICENSE +21 -0
- package/README.md +214 -0
- package/cli/index.ts +49 -0
- package/cli/init.ts +390 -0
- package/drift/index.ts +398 -0
- package/examples/taskflow/README.md +103 -0
- package/examples/taskflow/contracts/README.md +18 -0
- package/examples/taskflow/contracts/action_trigger.yaml +7 -0
- package/examples/taskflow/contracts/collection.yaml +7 -0
- package/examples/taskflow/contracts/data_display.yaml +7 -0
- package/examples/taskflow/contracts/feedback.yaml +7 -0
- package/examples/taskflow/contracts/input_field.yaml +7 -0
- package/examples/taskflow/contracts/nav_container.yaml +7 -0
- package/examples/taskflow/contracts/surface.yaml +7 -0
- package/examples/taskflow/contracts/x_media_player.yaml +185 -0
- package/examples/taskflow/flows/create_task.yaml +171 -0
- package/examples/taskflow/flows/edit_task.yaml +131 -0
- package/examples/taskflow/locales/en.json +158 -0
- package/examples/taskflow/openuispec.yaml +144 -0
- package/examples/taskflow/platform/android.yaml +32 -0
- package/examples/taskflow/platform/ios.yaml +39 -0
- package/examples/taskflow/platform/web.yaml +35 -0
- package/examples/taskflow/screens/calendar.yaml +23 -0
- package/examples/taskflow/screens/home.yaml +220 -0
- package/examples/taskflow/screens/profile_edit.yaml +70 -0
- package/examples/taskflow/screens/project_detail.yaml +65 -0
- package/examples/taskflow/screens/projects.yaml +142 -0
- package/examples/taskflow/screens/settings.yaml +178 -0
- package/examples/taskflow/screens/task_detail.yaml +317 -0
- package/examples/taskflow/tokens/color.yaml +88 -0
- package/examples/taskflow/tokens/elevation.yaml +27 -0
- package/examples/taskflow/tokens/icons.yaml +189 -0
- package/examples/taskflow/tokens/layout.yaml +156 -0
- package/examples/taskflow/tokens/motion.yaml +41 -0
- package/examples/taskflow/tokens/spacing.yaml +23 -0
- package/examples/taskflow/tokens/themes.yaml +34 -0
- package/examples/taskflow/tokens/typography.yaml +61 -0
- package/package.json +43 -0
- package/schema/custom-contract.schema.json +257 -0
- package/schema/defs/action.schema.json +272 -0
- package/schema/defs/adaptive.schema.json +13 -0
- package/schema/defs/common.schema.json +330 -0
- package/schema/defs/data-binding.schema.json +119 -0
- package/schema/defs/validation.schema.json +121 -0
- package/schema/flow.schema.json +164 -0
- package/schema/locale.schema.json +26 -0
- package/schema/openuispec.schema.json +287 -0
- package/schema/platform.schema.json +95 -0
- package/schema/screen.schema.json +346 -0
- package/schema/tokens/color.schema.json +104 -0
- package/schema/tokens/elevation.schema.json +84 -0
- package/schema/tokens/icons.schema.json +149 -0
- package/schema/tokens/layout.schema.json +170 -0
- package/schema/tokens/motion.schema.json +83 -0
- package/schema/tokens/spacing.schema.json +93 -0
- package/schema/tokens/themes.schema.json +92 -0
- package/schema/tokens/typography.schema.json +106 -0
- package/schema/validate.ts +258 -0
- package/spec/openuispec-v0.1.md +3677 -0
|
@@ -0,0 +1,185 @@
|
|
|
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"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# TaskFlow — Create Task Flow
|
|
3
|
+
# ============================================================
|
|
4
|
+
# Exercises: input_field (text, multiline, select, date, toggle),
|
|
5
|
+
# action_trigger, feedback (toast, banner), surface
|
|
6
|
+
# ============================================================
|
|
7
|
+
|
|
8
|
+
create_task:
|
|
9
|
+
semantic: "Multi-step flow to create a new task"
|
|
10
|
+
|
|
11
|
+
entry: "task_form"
|
|
12
|
+
|
|
13
|
+
# Single-screen flow presented as a sheet
|
|
14
|
+
screens:
|
|
15
|
+
task_form:
|
|
16
|
+
screen_inline:
|
|
17
|
+
semantic: "Task creation form with all input types"
|
|
18
|
+
|
|
19
|
+
state:
|
|
20
|
+
is_submitting: { type: bool, default: false }
|
|
21
|
+
|
|
22
|
+
layout:
|
|
23
|
+
type: scroll_vertical
|
|
24
|
+
safe_area: true
|
|
25
|
+
padding: "spacing.page_margin"
|
|
26
|
+
|
|
27
|
+
sections:
|
|
28
|
+
# ---------- Sheet header ----------
|
|
29
|
+
- id: header
|
|
30
|
+
layout: { type: row, justify: "space-between", align: "center" }
|
|
31
|
+
children:
|
|
32
|
+
- contract: action_trigger
|
|
33
|
+
variant: ghost
|
|
34
|
+
size: sm
|
|
35
|
+
props: { label: "$t:common.cancel" }
|
|
36
|
+
action: { type: dismiss }
|
|
37
|
+
|
|
38
|
+
- contract: data_display
|
|
39
|
+
variant: inline
|
|
40
|
+
props:
|
|
41
|
+
title: "$t:create_task.title"
|
|
42
|
+
tokens_override:
|
|
43
|
+
title_style: "typography.heading_sm"
|
|
44
|
+
|
|
45
|
+
- contract: action_trigger
|
|
46
|
+
variant: primary
|
|
47
|
+
size: sm
|
|
48
|
+
props:
|
|
49
|
+
label: "$t:create_task.save"
|
|
50
|
+
loading_label: "$t:create_task.saving"
|
|
51
|
+
state_binding:
|
|
52
|
+
loading: "state.is_submitting"
|
|
53
|
+
action:
|
|
54
|
+
type: submit_form
|
|
55
|
+
form_id: "task_form"
|
|
56
|
+
|
|
57
|
+
# ---------- Form fields ----------
|
|
58
|
+
- id: form
|
|
59
|
+
form_id: "task_form"
|
|
60
|
+
margin_top: "spacing.lg"
|
|
61
|
+
layout: { type: stack, spacing: "spacing.md" }
|
|
62
|
+
children:
|
|
63
|
+
# Title (text)
|
|
64
|
+
- contract: input_field
|
|
65
|
+
input_type: text
|
|
66
|
+
props:
|
|
67
|
+
label: "$t:create_task.field_title"
|
|
68
|
+
placeholder: "$t:create_task.field_title_placeholder"
|
|
69
|
+
required: true
|
|
70
|
+
max_length: 200
|
|
71
|
+
validate:
|
|
72
|
+
min_length: { value: 3, message: "$t:validation.min_length" }
|
|
73
|
+
data_binding: "form.title"
|
|
74
|
+
|
|
75
|
+
# Description (multiline)
|
|
76
|
+
- contract: input_field
|
|
77
|
+
input_type: multiline
|
|
78
|
+
props:
|
|
79
|
+
label: "$t:create_task.field_description"
|
|
80
|
+
placeholder: "$t:create_task.field_description_placeholder"
|
|
81
|
+
required: false
|
|
82
|
+
data_binding: "form.description"
|
|
83
|
+
tokens_override:
|
|
84
|
+
min_height: 100
|
|
85
|
+
|
|
86
|
+
# Project (select)
|
|
87
|
+
- contract: input_field
|
|
88
|
+
input_type: select
|
|
89
|
+
props:
|
|
90
|
+
label: "$t:create_task.field_project"
|
|
91
|
+
placeholder: "$t:create_task.field_project_placeholder"
|
|
92
|
+
options:
|
|
93
|
+
source: "api.projects.list"
|
|
94
|
+
value_key: "id"
|
|
95
|
+
label_key: "name"
|
|
96
|
+
icon_key: "icon"
|
|
97
|
+
required: false
|
|
98
|
+
data_binding: "form.project_id"
|
|
99
|
+
|
|
100
|
+
# Priority (select)
|
|
101
|
+
- contract: input_field
|
|
102
|
+
input_type: select
|
|
103
|
+
props:
|
|
104
|
+
label: "$t:create_task.field_priority"
|
|
105
|
+
options:
|
|
106
|
+
- { value: "low", label: "$t:priority.low", icon: "flag", color: "color.priority.low" }
|
|
107
|
+
- { value: "medium", label: "$t:priority.medium", icon: "flag_fill", color: "color.priority.medium" }
|
|
108
|
+
- { value: "high", label: "$t:priority.high", icon: "flag_fill", color: "color.priority.high" }
|
|
109
|
+
- { value: "urgent", label: "$t:priority.urgent", icon: "exclamationmark_triangle", color: "color.priority.urgent" }
|
|
110
|
+
required: true
|
|
111
|
+
data_binding: "form.priority"
|
|
112
|
+
|
|
113
|
+
# Due date (date picker)
|
|
114
|
+
- contract: input_field
|
|
115
|
+
input_type: date
|
|
116
|
+
props:
|
|
117
|
+
label: "$t:create_task.field_due_date"
|
|
118
|
+
placeholder: "$t:create_task.field_due_date_placeholder"
|
|
119
|
+
required: false
|
|
120
|
+
data_binding: "form.due_date"
|
|
121
|
+
|
|
122
|
+
# Tags (text with tokenization)
|
|
123
|
+
- contract: input_field
|
|
124
|
+
input_type: text
|
|
125
|
+
props:
|
|
126
|
+
label: "$t:create_task.field_tags"
|
|
127
|
+
placeholder: "$t:create_task.field_tags_placeholder"
|
|
128
|
+
helper_text: "$t:create_task.field_tags_helper"
|
|
129
|
+
required: false
|
|
130
|
+
validate:
|
|
131
|
+
pattern: { regex: "^[a-zA-Z0-9_,\\-\\s]+$", message: "$t:validation.tag_format" }
|
|
132
|
+
data_binding: "form.tags"
|
|
133
|
+
behavior:
|
|
134
|
+
tokenize_on: [",", "Enter"]
|
|
135
|
+
display_as: chips
|
|
136
|
+
|
|
137
|
+
# Assign to me toggle
|
|
138
|
+
- contract: input_field
|
|
139
|
+
input_type: toggle
|
|
140
|
+
props:
|
|
141
|
+
label: "$t:create_task.field_assign_to_me"
|
|
142
|
+
value: true
|
|
143
|
+
data_binding: "form.assign_to_self"
|
|
144
|
+
|
|
145
|
+
# ---------- Form submission ----------
|
|
146
|
+
on_submit:
|
|
147
|
+
type: sequence
|
|
148
|
+
actions:
|
|
149
|
+
- { type: set_state, is_submitting: true }
|
|
150
|
+
- type: api_call
|
|
151
|
+
endpoint: "api.tasks.create"
|
|
152
|
+
body: "form"
|
|
153
|
+
on_success:
|
|
154
|
+
type: sequence
|
|
155
|
+
actions:
|
|
156
|
+
- { type: set_state, is_submitting: false }
|
|
157
|
+
- { type: feedback, variant: toast, message: "$t:create_task.success", severity: success, icon: "checkmark_circle", duration: 3000 }
|
|
158
|
+
- { type: dismiss }
|
|
159
|
+
- { type: refresh, target: "screens/home.tasks" }
|
|
160
|
+
on_error:
|
|
161
|
+
type: sequence
|
|
162
|
+
actions:
|
|
163
|
+
- { type: set_state, is_submitting: false }
|
|
164
|
+
- { type: feedback, variant: banner, title: "$t:create_task.error_title", message: "{error.message}", severity: error }
|
|
165
|
+
|
|
166
|
+
transitions: {} # single screen, no transitions
|
|
167
|
+
|
|
168
|
+
platform_hints:
|
|
169
|
+
ios: { presentation: "sheet", detents: [large] }
|
|
170
|
+
android: { presentation: "bottom_sheet", fullscreen: false }
|
|
171
|
+
web: { presentation: "modal", size: "md" }
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# TaskFlow — Edit Task Flow (stub)
|
|
3
|
+
# ============================================================
|
|
4
|
+
# Referenced by task_detail.yaml. Same form as create_task
|
|
5
|
+
# but pre-populated with existing task data.
|
|
6
|
+
# ============================================================
|
|
7
|
+
|
|
8
|
+
edit_task:
|
|
9
|
+
semantic: "Edit an existing task"
|
|
10
|
+
status: stub
|
|
11
|
+
|
|
12
|
+
params:
|
|
13
|
+
task_id: { type: string, required: true }
|
|
14
|
+
|
|
15
|
+
entry: "task_form"
|
|
16
|
+
|
|
17
|
+
screens:
|
|
18
|
+
task_form:
|
|
19
|
+
screen_inline:
|
|
20
|
+
semantic: "Task edit form pre-populated with existing data"
|
|
21
|
+
|
|
22
|
+
data:
|
|
23
|
+
task:
|
|
24
|
+
source: "api.tasks.getById"
|
|
25
|
+
params: { id: "params.task_id" }
|
|
26
|
+
|
|
27
|
+
state:
|
|
28
|
+
is_submitting: { type: bool, default: false }
|
|
29
|
+
|
|
30
|
+
layout:
|
|
31
|
+
type: scroll_vertical
|
|
32
|
+
safe_area: true
|
|
33
|
+
padding: "spacing.page_margin"
|
|
34
|
+
|
|
35
|
+
sections:
|
|
36
|
+
- id: header
|
|
37
|
+
layout: { type: row, justify: "space-between", align: "center" }
|
|
38
|
+
children:
|
|
39
|
+
- contract: action_trigger
|
|
40
|
+
variant: ghost
|
|
41
|
+
size: sm
|
|
42
|
+
props: { label: "$t:common.cancel" }
|
|
43
|
+
action: { type: dismiss }
|
|
44
|
+
|
|
45
|
+
- contract: data_display
|
|
46
|
+
variant: inline
|
|
47
|
+
props:
|
|
48
|
+
title: "$t:edit_task.title"
|
|
49
|
+
tokens_override:
|
|
50
|
+
title_style: "typography.heading_sm"
|
|
51
|
+
|
|
52
|
+
- contract: action_trigger
|
|
53
|
+
variant: primary
|
|
54
|
+
size: sm
|
|
55
|
+
props:
|
|
56
|
+
label: "$t:edit_task.save"
|
|
57
|
+
loading_label: "$t:edit_task.saving"
|
|
58
|
+
state_binding:
|
|
59
|
+
loading: "state.is_submitting"
|
|
60
|
+
action:
|
|
61
|
+
type: submit_form
|
|
62
|
+
form_id: "edit_task_form"
|
|
63
|
+
|
|
64
|
+
- id: form
|
|
65
|
+
form_id: "edit_task_form"
|
|
66
|
+
margin_top: "spacing.lg"
|
|
67
|
+
layout: { type: stack, spacing: "spacing.md" }
|
|
68
|
+
children:
|
|
69
|
+
- contract: input_field
|
|
70
|
+
input_type: text
|
|
71
|
+
props:
|
|
72
|
+
label: "$t:edit_task.field_title"
|
|
73
|
+
value: "task.title"
|
|
74
|
+
required: true
|
|
75
|
+
max_length: 200
|
|
76
|
+
data_binding: "form.title"
|
|
77
|
+
|
|
78
|
+
- contract: input_field
|
|
79
|
+
input_type: multiline
|
|
80
|
+
props:
|
|
81
|
+
label: "$t:edit_task.field_description"
|
|
82
|
+
value: "task.description"
|
|
83
|
+
data_binding: "form.description"
|
|
84
|
+
|
|
85
|
+
- contract: input_field
|
|
86
|
+
input_type: select
|
|
87
|
+
props:
|
|
88
|
+
label: "$t:edit_task.field_priority"
|
|
89
|
+
value: "task.priority"
|
|
90
|
+
options:
|
|
91
|
+
- { value: "low", label: "$t:priority.low", icon: "flag", color: "color.priority.low" }
|
|
92
|
+
- { value: "medium", label: "$t:priority.medium", icon: "flag_fill", color: "color.priority.medium" }
|
|
93
|
+
- { value: "high", label: "$t:priority.high", icon: "flag_fill", color: "color.priority.high" }
|
|
94
|
+
- { value: "urgent", label: "$t:priority.urgent", icon: "exclamationmark_triangle", color: "color.priority.urgent" }
|
|
95
|
+
required: true
|
|
96
|
+
data_binding: "form.priority"
|
|
97
|
+
|
|
98
|
+
- contract: input_field
|
|
99
|
+
input_type: date
|
|
100
|
+
props:
|
|
101
|
+
label: "$t:edit_task.field_due_date"
|
|
102
|
+
value: "task.due_date"
|
|
103
|
+
data_binding: "form.due_date"
|
|
104
|
+
|
|
105
|
+
on_submit:
|
|
106
|
+
type: sequence
|
|
107
|
+
actions:
|
|
108
|
+
- { type: set_state, is_submitting: true }
|
|
109
|
+
- type: api_call
|
|
110
|
+
endpoint: "api.tasks.update"
|
|
111
|
+
params: { id: "params.task_id" }
|
|
112
|
+
body: "form"
|
|
113
|
+
on_success:
|
|
114
|
+
type: sequence
|
|
115
|
+
actions:
|
|
116
|
+
- { type: set_state, is_submitting: false }
|
|
117
|
+
- { type: feedback, variant: toast, message: "$t:edit_task.success", severity: success }
|
|
118
|
+
- { type: dismiss }
|
|
119
|
+
- { type: refresh, target: "screens/task_detail.task" }
|
|
120
|
+
on_error:
|
|
121
|
+
type: sequence
|
|
122
|
+
actions:
|
|
123
|
+
- { type: set_state, is_submitting: false }
|
|
124
|
+
- { type: feedback, variant: banner, title: "$t:edit_task.error_title", message: "{error.message}", severity: error }
|
|
125
|
+
|
|
126
|
+
transitions: {}
|
|
127
|
+
|
|
128
|
+
platform_hints:
|
|
129
|
+
ios: { presentation: "sheet", detents: [large] }
|
|
130
|
+
android: { presentation: "bottom_sheet", fullscreen: false }
|
|
131
|
+
web: { presentation: "modal", size: "md" }
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$locale": "en",
|
|
3
|
+
"$direction": "ltr",
|
|
4
|
+
|
|
5
|
+
"nav.tasks": "Tasks",
|
|
6
|
+
"nav.projects": "Projects",
|
|
7
|
+
"nav.calendar": "Calendar",
|
|
8
|
+
"nav.settings": "Settings",
|
|
9
|
+
|
|
10
|
+
"home.greeting.morning": "Good morning, {name}",
|
|
11
|
+
"home.greeting.afternoon": "Good afternoon, {name}",
|
|
12
|
+
"home.greeting.evening": "Good evening, {name}",
|
|
13
|
+
"home.task_count": "{count, plural, =0 {No tasks today} one {# task today} other {# tasks today}}",
|
|
14
|
+
"home.search_label": "Search tasks",
|
|
15
|
+
"home.search_placeholder": "Search by title, tag, or project...",
|
|
16
|
+
"home.filter.all": "All",
|
|
17
|
+
"home.filter.today": "Today",
|
|
18
|
+
"home.filter.upcoming": "Upcoming",
|
|
19
|
+
"home.filter.done": "Done",
|
|
20
|
+
"home.empty_title": "All caught up!",
|
|
21
|
+
"home.empty_body": "No tasks match this filter. Tap + to add one.",
|
|
22
|
+
"home.new_task": "New task",
|
|
23
|
+
"home.mark_complete": "Mark {title} complete",
|
|
24
|
+
|
|
25
|
+
"task_detail.status": "Status",
|
|
26
|
+
"task_detail.priority": "Priority",
|
|
27
|
+
"task_detail.due": "Due",
|
|
28
|
+
"task_detail.description": "Description",
|
|
29
|
+
"task_detail.details": "Details",
|
|
30
|
+
"task_detail.project": "Project",
|
|
31
|
+
"task_detail.assignee": "Assignee",
|
|
32
|
+
"task_detail.unassigned": "Unassigned",
|
|
33
|
+
"task_detail.tags": "Tags",
|
|
34
|
+
"task_detail.created": "Created",
|
|
35
|
+
"task_detail.edit": "Edit task",
|
|
36
|
+
"task_detail.toggle_status": "{is_done, select, true {Reopen task} other {Mark complete}}",
|
|
37
|
+
"task_detail.delete": "Delete task",
|
|
38
|
+
"task_detail.delete_title": "Delete task?",
|
|
39
|
+
"task_detail.delete_message": "This action cannot be undone. The task \"{title}\" will be permanently removed.",
|
|
40
|
+
"task_detail.task_updated": "Task updated",
|
|
41
|
+
"task_detail.update_error": "Couldn't update status",
|
|
42
|
+
"task_detail.task_deleted": "Task deleted",
|
|
43
|
+
"task_detail.assign_to": "Assign to",
|
|
44
|
+
"task_detail.search_people": "Search people",
|
|
45
|
+
"task_detail.search_people_placeholder": "Name or email...",
|
|
46
|
+
|
|
47
|
+
"create_task.title": "New task",
|
|
48
|
+
"create_task.save": "Save",
|
|
49
|
+
"create_task.saving": "Saving...",
|
|
50
|
+
"create_task.field_title": "Title",
|
|
51
|
+
"create_task.field_title_placeholder": "What needs to be done?",
|
|
52
|
+
"create_task.field_description": "Description",
|
|
53
|
+
"create_task.field_description_placeholder": "Add details, notes, or context...",
|
|
54
|
+
"create_task.field_project": "Project",
|
|
55
|
+
"create_task.field_project_placeholder": "Select a project",
|
|
56
|
+
"create_task.field_priority": "Priority",
|
|
57
|
+
"create_task.field_due_date": "Due date",
|
|
58
|
+
"create_task.field_due_date_placeholder": "No due date",
|
|
59
|
+
"create_task.field_tags": "Tags",
|
|
60
|
+
"create_task.field_tags_placeholder": "Add tags separated by commas",
|
|
61
|
+
"create_task.field_tags_helper": "Press comma or Enter to add a tag",
|
|
62
|
+
"create_task.field_assign_to_me": "Assign to me",
|
|
63
|
+
"create_task.success": "Task created",
|
|
64
|
+
"create_task.error_title": "Couldn't create task",
|
|
65
|
+
|
|
66
|
+
"edit_task.title": "Edit task",
|
|
67
|
+
"edit_task.save": "Save",
|
|
68
|
+
"edit_task.saving": "Saving...",
|
|
69
|
+
"edit_task.field_title": "Title",
|
|
70
|
+
"edit_task.field_description": "Description",
|
|
71
|
+
"edit_task.field_priority": "Priority",
|
|
72
|
+
"edit_task.field_due_date": "Due date",
|
|
73
|
+
"edit_task.success": "Task updated",
|
|
74
|
+
"edit_task.error_title": "Couldn't update task",
|
|
75
|
+
|
|
76
|
+
"projects.title": "Projects",
|
|
77
|
+
"projects.new_project": "New project",
|
|
78
|
+
"projects.task_count": "{count, plural, =0 {No tasks} one {# task} other {# tasks}}",
|
|
79
|
+
"projects.empty_title": "No projects yet",
|
|
80
|
+
"projects.empty_body": "Create a project to organize your tasks.",
|
|
81
|
+
"projects.dialog_title": "New project",
|
|
82
|
+
"projects.field_name": "Project name",
|
|
83
|
+
"projects.field_name_placeholder": "e.g., Product Launch",
|
|
84
|
+
"projects.field_color": "Color",
|
|
85
|
+
"projects.field_icon": "Icon",
|
|
86
|
+
"projects.created": "Project created",
|
|
87
|
+
|
|
88
|
+
"project_detail.task_count": "{count, plural, =0 {No tasks} one {# task} other {# tasks}}",
|
|
89
|
+
"project_detail.empty_title": "No tasks in this project",
|
|
90
|
+
"project_detail.empty_body": "Add a task to get started.",
|
|
91
|
+
|
|
92
|
+
"settings.preferences": "Preferences",
|
|
93
|
+
"settings.theme": "Theme",
|
|
94
|
+
"settings.theme_system": "System",
|
|
95
|
+
"settings.theme_light": "Light",
|
|
96
|
+
"settings.theme_dark": "Dark",
|
|
97
|
+
"settings.theme_warm": "Warm",
|
|
98
|
+
"settings.default_priority": "Default priority",
|
|
99
|
+
"settings.notifications": "Push notifications",
|
|
100
|
+
"settings.reminders": "Due date reminders",
|
|
101
|
+
"settings.reminders_helper": "Get notified 1 hour before a task is due",
|
|
102
|
+
"settings.data": "Data",
|
|
103
|
+
"settings.export": "Export data",
|
|
104
|
+
"settings.export_success": "Export sent to your email",
|
|
105
|
+
"settings.delete_account": "Delete account",
|
|
106
|
+
"settings.delete_title": "Delete your account?",
|
|
107
|
+
"settings.delete_message": "This will permanently delete your account and all your data. This action cannot be undone.",
|
|
108
|
+
"settings.delete_confirm": "Delete my account",
|
|
109
|
+
"settings.app_version": "TaskFlow v1.0.0",
|
|
110
|
+
"settings.app_credit": "Built with OpenUISpec",
|
|
111
|
+
|
|
112
|
+
"profile.change_photo": "Change photo",
|
|
113
|
+
"profile.field_name": "Name",
|
|
114
|
+
"profile.field_email": "Email",
|
|
115
|
+
"profile.save": "Save changes",
|
|
116
|
+
"profile.success": "Profile updated",
|
|
117
|
+
|
|
118
|
+
"calendar.title": "Calendar",
|
|
119
|
+
"calendar.coming_soon": "Coming in a future version",
|
|
120
|
+
|
|
121
|
+
"priority.low": "Low",
|
|
122
|
+
"priority.medium": "Medium",
|
|
123
|
+
"priority.high": "High",
|
|
124
|
+
"priority.urgent": "Urgent",
|
|
125
|
+
|
|
126
|
+
"status.todo": "To do",
|
|
127
|
+
"status.in_progress": "In progress",
|
|
128
|
+
"status.done": "Done",
|
|
129
|
+
|
|
130
|
+
"media_player.loading": "Loading media…",
|
|
131
|
+
"media_player.play": "Play",
|
|
132
|
+
"media_player.pause": "Pause",
|
|
133
|
+
"media_player.replay": "Replay",
|
|
134
|
+
"media_player.retry": "Retry",
|
|
135
|
+
"media_player.error": "Unable to play media",
|
|
136
|
+
"media_player.fullscreen": "Full screen",
|
|
137
|
+
"media_player.exit_fullscreen": "Exit full screen",
|
|
138
|
+
"media_player.mute": "Mute",
|
|
139
|
+
"media_player.unmute": "Unmute",
|
|
140
|
+
"media_player.playback_rate": "Playback speed",
|
|
141
|
+
|
|
142
|
+
"validation.required": "This field is required",
|
|
143
|
+
"validation.min_length": "Must be at least {min} characters",
|
|
144
|
+
"validation.max_length": "Must be no more than {max} characters",
|
|
145
|
+
"validation.min_value": "Must be at least {min}",
|
|
146
|
+
"validation.max_value": "Must be no more than {max}",
|
|
147
|
+
"validation.pattern": "Invalid format",
|
|
148
|
+
"validation.format.email": "Enter a valid email address",
|
|
149
|
+
"validation.format.url": "Enter a valid URL",
|
|
150
|
+
"validation.format.phone": "Enter a valid phone number",
|
|
151
|
+
"validation.match_field": "Fields do not match",
|
|
152
|
+
"validation.tag_format": "Tags may only contain letters, numbers, and hyphens",
|
|
153
|
+
"validation.checking": "Checking…",
|
|
154
|
+
|
|
155
|
+
"common.cancel": "Cancel",
|
|
156
|
+
"common.delete": "Delete",
|
|
157
|
+
"common.create": "Create"
|
|
158
|
+
}
|