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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/cli/index.ts +49 -0
  4. package/cli/init.ts +390 -0
  5. package/drift/index.ts +398 -0
  6. package/examples/taskflow/README.md +103 -0
  7. package/examples/taskflow/contracts/README.md +18 -0
  8. package/examples/taskflow/contracts/action_trigger.yaml +7 -0
  9. package/examples/taskflow/contracts/collection.yaml +7 -0
  10. package/examples/taskflow/contracts/data_display.yaml +7 -0
  11. package/examples/taskflow/contracts/feedback.yaml +7 -0
  12. package/examples/taskflow/contracts/input_field.yaml +7 -0
  13. package/examples/taskflow/contracts/nav_container.yaml +7 -0
  14. package/examples/taskflow/contracts/surface.yaml +7 -0
  15. package/examples/taskflow/contracts/x_media_player.yaml +185 -0
  16. package/examples/taskflow/flows/create_task.yaml +171 -0
  17. package/examples/taskflow/flows/edit_task.yaml +131 -0
  18. package/examples/taskflow/locales/en.json +158 -0
  19. package/examples/taskflow/openuispec.yaml +144 -0
  20. package/examples/taskflow/platform/android.yaml +32 -0
  21. package/examples/taskflow/platform/ios.yaml +39 -0
  22. package/examples/taskflow/platform/web.yaml +35 -0
  23. package/examples/taskflow/screens/calendar.yaml +23 -0
  24. package/examples/taskflow/screens/home.yaml +220 -0
  25. package/examples/taskflow/screens/profile_edit.yaml +70 -0
  26. package/examples/taskflow/screens/project_detail.yaml +65 -0
  27. package/examples/taskflow/screens/projects.yaml +142 -0
  28. package/examples/taskflow/screens/settings.yaml +178 -0
  29. package/examples/taskflow/screens/task_detail.yaml +317 -0
  30. package/examples/taskflow/tokens/color.yaml +88 -0
  31. package/examples/taskflow/tokens/elevation.yaml +27 -0
  32. package/examples/taskflow/tokens/icons.yaml +189 -0
  33. package/examples/taskflow/tokens/layout.yaml +156 -0
  34. package/examples/taskflow/tokens/motion.yaml +41 -0
  35. package/examples/taskflow/tokens/spacing.yaml +23 -0
  36. package/examples/taskflow/tokens/themes.yaml +34 -0
  37. package/examples/taskflow/tokens/typography.yaml +61 -0
  38. package/package.json +43 -0
  39. package/schema/custom-contract.schema.json +257 -0
  40. package/schema/defs/action.schema.json +272 -0
  41. package/schema/defs/adaptive.schema.json +13 -0
  42. package/schema/defs/common.schema.json +330 -0
  43. package/schema/defs/data-binding.schema.json +119 -0
  44. package/schema/defs/validation.schema.json +121 -0
  45. package/schema/flow.schema.json +164 -0
  46. package/schema/locale.schema.json +26 -0
  47. package/schema/openuispec.schema.json +287 -0
  48. package/schema/platform.schema.json +95 -0
  49. package/schema/screen.schema.json +346 -0
  50. package/schema/tokens/color.schema.json +104 -0
  51. package/schema/tokens/elevation.schema.json +84 -0
  52. package/schema/tokens/icons.schema.json +149 -0
  53. package/schema/tokens/layout.schema.json +170 -0
  54. package/schema/tokens/motion.schema.json +83 -0
  55. package/schema/tokens/spacing.schema.json +93 -0
  56. package/schema/tokens/themes.schema.json +92 -0
  57. package/schema/tokens/typography.schema.json +106 -0
  58. package/schema/validate.ts +258 -0
  59. 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
+ }