openuispec 0.1.17 → 0.1.19

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 (97) hide show
  1. package/README.md +52 -34
  2. package/cli/index.ts +1 -1
  3. package/docs/stress-test-maturity-report.md +97 -0
  4. package/examples/taskflow/screens/profile_edit.yaml +2 -0
  5. package/examples/todo-orbit/AGENTS.md +127 -0
  6. package/examples/todo-orbit/CLAUDE.md +127 -0
  7. package/examples/todo-orbit/README.md +62 -0
  8. package/examples/todo-orbit/generated/android/Todo Orbit/README.md +14 -0
  9. package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +58 -0
  10. package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +1 -0
  11. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +20 -0
  12. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +14 -0
  13. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +345 -0
  14. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +231 -0
  15. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +169 -0
  16. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +8 -0
  17. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +185 -0
  18. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +193 -0
  19. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +102 -0
  20. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +342 -0
  21. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +344 -0
  22. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +59 -0
  23. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +148 -0
  24. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +154 -0
  25. package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +4 -0
  26. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
  27. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +7 -0
  28. package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +4 -0
  29. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +248 -0
  30. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +93 -0
  31. package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +18 -0
  32. package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +29 -0
  33. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +118 -0
  34. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +118 -0
  35. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +50 -0
  36. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +204 -0
  37. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +126 -0
  38. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +70 -0
  39. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +123 -0
  40. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +60 -0
  41. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +238 -0
  42. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +94 -0
  43. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +74 -0
  44. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +363 -0
  45. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +324 -0
  46. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +408 -0
  47. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  48. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +79 -0
  49. package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +24 -0
  50. package/examples/todo-orbit/generated/web/Todo Orbit/index.html +16 -0
  51. package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +1087 -0
  52. package/examples/todo-orbit/generated/web/Todo Orbit/package.json +24 -0
  53. package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +2114 -0
  54. package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +13 -0
  55. package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +886 -0
  56. package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +19 -0
  57. package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +6 -0
  58. package/examples/todo-orbit/openuispec/README.md +158 -0
  59. package/examples/todo-orbit/openuispec/contracts/.gitkeep +0 -0
  60. package/examples/todo-orbit/openuispec/contracts/action_trigger.yaml +28 -0
  61. package/examples/todo-orbit/openuispec/contracts/collection.yaml +32 -0
  62. package/examples/todo-orbit/openuispec/contracts/data_display.yaml +38 -0
  63. package/examples/todo-orbit/openuispec/contracts/feedback.yaml +32 -0
  64. package/examples/todo-orbit/openuispec/contracts/input_field.yaml +52 -0
  65. package/examples/todo-orbit/openuispec/contracts/nav_container.yaml +47 -0
  66. package/examples/todo-orbit/openuispec/contracts/surface.yaml +28 -0
  67. package/examples/todo-orbit/openuispec/contracts/x_schedule_preview.yaml +134 -0
  68. package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +139 -0
  69. package/examples/todo-orbit/openuispec/flows/.gitkeep +0 -0
  70. package/examples/todo-orbit/openuispec/flows/create_recurring_rule.yaml +253 -0
  71. package/examples/todo-orbit/openuispec/flows/create_task.yaml +118 -0
  72. package/examples/todo-orbit/openuispec/flows/edit_task.yaml +126 -0
  73. package/examples/todo-orbit/openuispec/locales/.gitkeep +0 -0
  74. package/examples/todo-orbit/openuispec/locales/en.json +150 -0
  75. package/examples/todo-orbit/openuispec/locales/ru.json +150 -0
  76. package/examples/todo-orbit/openuispec/openuispec.yaml +122 -0
  77. package/examples/todo-orbit/openuispec/platform/.gitkeep +0 -0
  78. package/examples/todo-orbit/openuispec/platform/android.yaml +19 -0
  79. package/examples/todo-orbit/openuispec/platform/ios.yaml +20 -0
  80. package/examples/todo-orbit/openuispec/platform/web.yaml +22 -0
  81. package/examples/todo-orbit/openuispec/screens/.gitkeep +0 -0
  82. package/examples/todo-orbit/openuispec/screens/analytics.yaml +139 -0
  83. package/examples/todo-orbit/openuispec/screens/home.yaml +172 -0
  84. package/examples/todo-orbit/openuispec/screens/settings.yaml +148 -0
  85. package/examples/todo-orbit/openuispec/screens/task_detail.yaml +223 -0
  86. package/examples/todo-orbit/openuispec/tokens/.gitkeep +0 -0
  87. package/examples/todo-orbit/openuispec/tokens/color.yaml +93 -0
  88. package/examples/todo-orbit/openuispec/tokens/elevation.yaml +25 -0
  89. package/examples/todo-orbit/openuispec/tokens/icons.yaml +92 -0
  90. package/examples/todo-orbit/openuispec/tokens/layout.yaml +107 -0
  91. package/examples/todo-orbit/openuispec/tokens/motion.yaml +39 -0
  92. package/examples/todo-orbit/openuispec/tokens/spacing.yaml +18 -0
  93. package/examples/todo-orbit/openuispec/tokens/themes.yaml +23 -0
  94. package/examples/todo-orbit/openuispec/tokens/typography.yaml +52 -0
  95. package/package.json +1 -1
  96. package/schema/validate.ts +271 -4
  97. package/spec/openuispec-v0.1.md +80 -13
@@ -0,0 +1,324 @@
1
+ import Foundation
2
+ import SwiftUI
3
+
4
+ @MainActor
5
+ final class AppModel: ObservableObject {
6
+ @Published var preferences = Preferences(
7
+ locale: .en,
8
+ theme: .light,
9
+ remindersEnabled: true,
10
+ dailySummaryEnabled: false
11
+ )
12
+ @Published var tasks: [Task] = Task.seed()
13
+ @Published var rules: [RecurringRule] = []
14
+ @Published var selectedTaskID: UUID?
15
+ @Published var toast: ToastMessage?
16
+
17
+ init() {
18
+ selectedTaskID = tasks.first?.id
19
+ }
20
+
21
+ var locale: Locale { Locale(identifier: preferences.locale.rawValue) }
22
+
23
+ func string(_ key: String) -> String {
24
+ let bundle = Bundle.main.path(
25
+ forResource: preferences.locale.rawValue,
26
+ ofType: "lproj"
27
+ ).flatMap { Bundle(path: $0) } ?? .main
28
+ return bundle.localizedString(forKey: key, value: key, table: nil)
29
+ }
30
+
31
+ func format(_ key: String, _ arguments: CVarArg...) -> String {
32
+ let format = string(key)
33
+ return String(format: format, locale: locale, arguments: arguments)
34
+ }
35
+
36
+ func homeSummary() -> String {
37
+ let open = tasks.filter { $0.status == .open }.count
38
+ let total = tasks.count
39
+ if preferences.locale == .ru {
40
+ if open == 0 { return string("home.summary.done") }
41
+ return format("home.summary.remaining", open, total)
42
+ }
43
+
44
+ if open == 0 { return string("home.summary.done") }
45
+ return format("home.summary.remaining", open, total)
46
+ }
47
+
48
+ func taskCount(for filter: TaskFilter) -> Int {
49
+ switch filter {
50
+ case .all:
51
+ return tasks.count
52
+ case .open:
53
+ return tasks.filter { $0.status == .open }.count
54
+ case .done:
55
+ return tasks.filter { $0.status == .done }.count
56
+ }
57
+ }
58
+
59
+ func filteredTasks(filter: TaskFilter, search: String) -> [Task] {
60
+ tasks.filter { task in
61
+ let matchesFilter: Bool
62
+ switch filter {
63
+ case .all: matchesFilter = true
64
+ case .open: matchesFilter = task.status == .open
65
+ case .done: matchesFilter = task.status == .done
66
+ }
67
+
68
+ let query = search.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
69
+ let matchesSearch = query.isEmpty || "\(task.title) \(task.notes)".lowercased().contains(query)
70
+ return matchesFilter && matchesSearch
71
+ }
72
+ }
73
+
74
+ func task(id: UUID?) -> Task? {
75
+ guard let id else { return nil }
76
+ return tasks.first(where: { $0.id == id })
77
+ }
78
+
79
+ func binding(for taskID: UUID) -> Binding<Task>? {
80
+ guard let index = tasks.firstIndex(where: { $0.id == taskID }) else { return nil }
81
+ return Binding(
82
+ get: { self.tasks[index] },
83
+ set: { self.tasks[index] = $0 }
84
+ )
85
+ }
86
+
87
+ func savePreferences(_ draft: Preferences) {
88
+ preferences = draft
89
+ showToast(level: .success, text: string("settings.saved"))
90
+ }
91
+
92
+ func makeTaskDraft(for task: Task?) -> TaskEditorDraft {
93
+ TaskEditorDraft(
94
+ title: task?.title ?? "",
95
+ notes: task?.notes ?? "",
96
+ priority: task?.priority ?? .medium,
97
+ dueDate: task?.dueDate
98
+ )
99
+ }
100
+
101
+ func submitTask(_ draft: TaskEditorDraft, editing taskID: UUID?) -> String? {
102
+ let title = draft.title.trimmingCharacters(in: .whitespacesAndNewlines)
103
+ guard title.count >= 2 else {
104
+ return format("validation.min_length", 2)
105
+ }
106
+
107
+ if let taskID, let index = tasks.firstIndex(where: { $0.id == taskID }) {
108
+ tasks[index].title = title
109
+ tasks[index].notes = draft.notes.trimmingCharacters(in: .whitespacesAndNewlines)
110
+ tasks[index].priority = draft.priority
111
+ tasks[index].dueDate = draft.dueDate
112
+ tasks[index].updatedAt = .now
113
+ showToast(level: .success, text: string("edit_task.success"))
114
+ } else {
115
+ let next = Task(
116
+ id: UUID(),
117
+ title: title,
118
+ notes: draft.notes.trimmingCharacters(in: .whitespacesAndNewlines),
119
+ status: .open,
120
+ priority: draft.priority,
121
+ dueDate: draft.dueDate,
122
+ createdAt: .now,
123
+ updatedAt: .now
124
+ )
125
+ tasks.insert(next, at: 0)
126
+ selectedTaskID = next.id
127
+ showToast(level: .success, text: string("create_task.success"))
128
+ }
129
+
130
+ return nil
131
+ }
132
+
133
+ func toggleTaskStatus(_ taskID: UUID) {
134
+ guard let index = tasks.firstIndex(where: { $0.id == taskID }) else { return }
135
+ tasks[index].status = tasks[index].status == .done ? .open : .done
136
+ tasks[index].updatedAt = .now
137
+ showToast(level: .success, text: string("task_detail.updated_feedback"))
138
+ }
139
+
140
+ func deleteTask(_ taskID: UUID) {
141
+ tasks.removeAll { $0.id == taskID }
142
+ if selectedTaskID == taskID {
143
+ selectedTaskID = tasks.first?.id
144
+ }
145
+ showToast(level: .success, text: string("task_detail.deleted_feedback"))
146
+ }
147
+
148
+ func addRecurringRule(from draft: RecurringRuleDraft) -> [String: String] {
149
+ var errors: [String: String] = [:]
150
+
151
+ let trimmedName = draft.name.trimmingCharacters(in: .whitespacesAndNewlines)
152
+ if trimmedName.count < 4 {
153
+ errors["name"] = format("validation.rule_name_min_length", 4)
154
+ } else if trimmedName == "Default" {
155
+ errors["name"] = string("validation.rule_name_reserved")
156
+ } else if rules.contains(where: { $0.name.caseInsensitiveCompare(trimmedName) == .orderedSame }) {
157
+ errors["name"] = string("validation.rule_name_taken")
158
+ }
159
+
160
+ if draft.confirmName.trimmingCharacters(in: .whitespacesAndNewlines) != trimmedName {
161
+ errors["confirmName"] = string("validation.match_field")
162
+ }
163
+
164
+ guard let cadence = draft.cadence else {
165
+ errors["cadence"] = string("validation.required")
166
+ return errors
167
+ }
168
+
169
+ guard let interval = Int(draft.interval), interval >= 1 else {
170
+ errors["interval"] = format("validation.min_value", 1)
171
+ return errors
172
+ }
173
+
174
+ if interval > 30 {
175
+ errors["interval"] = format("validation.max_value", 30)
176
+ }
177
+
178
+ if cadence == .weekly, draft.weekday == nil {
179
+ errors["weekday"] = string("validation.required")
180
+ }
181
+
182
+ var monthDayValue: Int?
183
+ if cadence == .monthly {
184
+ guard let day = Int(draft.monthDay), day >= 1 else {
185
+ errors["monthDay"] = format("validation.min_value", 1)
186
+ return errors
187
+ }
188
+ if day > 28 {
189
+ errors["monthDay"] = string("validation.month_day_max")
190
+ } else {
191
+ monthDayValue = day
192
+ }
193
+ }
194
+
195
+ if draft.hasEndDate && draft.endDate < draft.startDate {
196
+ errors["endDate"] = string("validation.end_date_after_start")
197
+ }
198
+
199
+ if preferences.remindersEnabled {
200
+ let regex = try? NSRegularExpression(pattern: "^([01]\\d|2[0-3]):[0-5]\\d$")
201
+ let range = NSRange(location: 0, length: draft.remindAt.utf16.count)
202
+ let matches = regex?.firstMatch(in: draft.remindAt, options: [], range: range) != nil
203
+ if !matches {
204
+ errors["remindAt"] = string("validation.time_format")
205
+ }
206
+ }
207
+
208
+ if draft.enableSummary && draft.summaryChannel == nil {
209
+ errors["summaryChannel"] = string("validation.required")
210
+ }
211
+
212
+ guard errors.isEmpty else { return errors }
213
+
214
+ let rule = RecurringRule(
215
+ id: UUID(),
216
+ name: trimmedName,
217
+ cadence: cadence,
218
+ interval: interval,
219
+ weekday: draft.weekday,
220
+ monthDay: monthDayValue,
221
+ startDate: draft.startDate,
222
+ endDate: draft.hasEndDate ? draft.endDate : nil,
223
+ remindAt: preferences.remindersEnabled ? draft.remindAt : nil,
224
+ summaryChannel: draft.enableSummary ? draft.summaryChannel : nil
225
+ )
226
+ rules.insert(rule, at: 0)
227
+ showToast(level: .success, text: string("recurring_rule.success"))
228
+ return [:]
229
+ }
230
+
231
+ func analyticsSnapshot() -> AnalyticsSnapshot {
232
+ let calendar = Calendar.current
233
+ let startOfToday = calendar.startOfDay(for: .now)
234
+ let completedToday = tasks.filter {
235
+ $0.status == .done && $0.updatedAt >= startOfToday
236
+ }.count
237
+ let openTasks = tasks.filter { $0.status == .open }.count
238
+ let overdueTasks = overdueTasks().count
239
+ let completionRate = tasks.isEmpty ? 0 : Int(((Double(tasks.count - openTasks) / Double(tasks.count)) * 100).rounded())
240
+ return AnalyticsSnapshot(
241
+ completedToday: completedToday,
242
+ openTasks: openTasks,
243
+ overdueTasks: overdueTasks,
244
+ completionRate: completionRate
245
+ )
246
+ }
247
+
248
+ func trendSeries(period: AnalyticsPeriod) -> [TrendPoint] {
249
+ let calendar = Calendar.current
250
+ let formatter = DateFormatter()
251
+ formatter.locale = locale
252
+ formatter.dateFormat = period == .week ? "E" : "MMM d"
253
+ let length: Int = switch period {
254
+ case .week: 7
255
+ case .month: 6
256
+ case .quarter: 8
257
+ }
258
+ let strideDays: Int = period == .week ? 1 : 5
259
+
260
+ return (0..<length).map { index in
261
+ let offset = length - index - 1
262
+ let pointDate = calendar.date(byAdding: .day, value: -(offset * strideDays), to: .now) ?? .now
263
+ let completed = tasks.filter { $0.status == .done && $0.updatedAt <= pointDate }.count
264
+ let created = tasks.filter { $0.createdAt <= pointDate }.count
265
+ return TrendPoint(label: formatter.string(from: pointDate), completed: completed, created: created)
266
+ }
267
+ }
268
+
269
+ func overdueTasks() -> [Task] {
270
+ let startOfToday = Calendar.current.startOfDay(for: .now)
271
+ return tasks.filter { task in
272
+ task.status == .open && (task.dueDate.map { $0 < startOfToday } ?? false)
273
+ }
274
+ }
275
+
276
+ func formatDate(_ date: Date?) -> String {
277
+ guard let date else { return string("task_detail.no_due_date") }
278
+ let formatter = DateFormatter()
279
+ formatter.locale = locale
280
+ formatter.dateStyle = .medium
281
+ formatter.timeStyle = .none
282
+ return formatter.string(from: date)
283
+ }
284
+
285
+ func formatRelativeDueDate(_ date: Date?) -> String {
286
+ guard let date else { return string("task_detail.no_due_date") }
287
+ let formatter = RelativeDateTimeFormatter()
288
+ formatter.locale = locale
289
+ formatter.unitsStyle = .full
290
+ return formatter.localizedString(for: date, relativeTo: .now)
291
+ }
292
+
293
+ func label(for priority: TaskPriority) -> String { string("priority.\(priority.rawValue)") }
294
+ func label(for status: TaskStatus) -> String { string("status.\(status.rawValue)") }
295
+ func label(for weekday: Weekday) -> String { string("weekday.\(weekday.rawValue)") }
296
+
297
+ func describe(rule: RecurringRule) -> String {
298
+ switch rule.cadence {
299
+ case .daily:
300
+ return "\(string("recurring_rule.cadence_daily")) · \(formatDate(rule.startDate))"
301
+ case .weekly:
302
+ return "\(string("recurring_rule.cadence_weekly")) · \(label(for: rule.weekday ?? .mon))"
303
+ case .monthly:
304
+ return "\(string("recurring_rule.cadence_monthly")) · \(rule.monthDay ?? 1)"
305
+ }
306
+ }
307
+
308
+ func showToast(level: ToastMessage.Level, text: String) {
309
+ withAnimation(.spring(duration: 0.32)) {
310
+ toast = ToastMessage(level: level, text: text)
311
+ }
312
+
313
+ _Concurrency.Task { [weak self] in
314
+ try? await _Concurrency.Task.sleep(for: .seconds(2.5))
315
+ guard let self else { return }
316
+ await MainActor.run {
317
+ withAnimation(.easeOut(duration: 0.2)) {
318
+ self.toast = nil
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ }