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.
- package/README.md +52 -34
- package/cli/index.ts +1 -1
- package/docs/stress-test-maturity-report.md +97 -0
- package/examples/taskflow/screens/profile_edit.yaml +2 -0
- package/examples/todo-orbit/AGENTS.md +127 -0
- package/examples/todo-orbit/CLAUDE.md +127 -0
- package/examples/todo-orbit/README.md +62 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/README.md +14 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +58 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +1 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +20 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +14 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +345 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +231 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +169 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +8 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +185 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +193 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +102 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +342 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +344 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +59 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +148 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +154 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +4 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +4 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +248 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +93 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +18 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +29 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +118 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +118 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +50 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +204 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +126 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +70 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +123 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +60 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +238 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +94 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +74 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +363 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +324 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +408 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +79 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +24 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/index.html +16 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +1087 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/package.json +24 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +2114 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +13 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +886 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +19 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +6 -0
- package/examples/todo-orbit/openuispec/README.md +158 -0
- package/examples/todo-orbit/openuispec/contracts/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/contracts/action_trigger.yaml +28 -0
- package/examples/todo-orbit/openuispec/contracts/collection.yaml +32 -0
- package/examples/todo-orbit/openuispec/contracts/data_display.yaml +38 -0
- package/examples/todo-orbit/openuispec/contracts/feedback.yaml +32 -0
- package/examples/todo-orbit/openuispec/contracts/input_field.yaml +52 -0
- package/examples/todo-orbit/openuispec/contracts/nav_container.yaml +47 -0
- package/examples/todo-orbit/openuispec/contracts/surface.yaml +28 -0
- package/examples/todo-orbit/openuispec/contracts/x_schedule_preview.yaml +134 -0
- package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +139 -0
- package/examples/todo-orbit/openuispec/flows/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/flows/create_recurring_rule.yaml +253 -0
- package/examples/todo-orbit/openuispec/flows/create_task.yaml +118 -0
- package/examples/todo-orbit/openuispec/flows/edit_task.yaml +126 -0
- package/examples/todo-orbit/openuispec/locales/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/locales/en.json +150 -0
- package/examples/todo-orbit/openuispec/locales/ru.json +150 -0
- package/examples/todo-orbit/openuispec/openuispec.yaml +122 -0
- package/examples/todo-orbit/openuispec/platform/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/platform/android.yaml +19 -0
- package/examples/todo-orbit/openuispec/platform/ios.yaml +20 -0
- package/examples/todo-orbit/openuispec/platform/web.yaml +22 -0
- package/examples/todo-orbit/openuispec/screens/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/screens/analytics.yaml +139 -0
- package/examples/todo-orbit/openuispec/screens/home.yaml +172 -0
- package/examples/todo-orbit/openuispec/screens/settings.yaml +148 -0
- package/examples/todo-orbit/openuispec/screens/task_detail.yaml +223 -0
- package/examples/todo-orbit/openuispec/tokens/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/tokens/color.yaml +93 -0
- package/examples/todo-orbit/openuispec/tokens/elevation.yaml +25 -0
- package/examples/todo-orbit/openuispec/tokens/icons.yaml +92 -0
- package/examples/todo-orbit/openuispec/tokens/layout.yaml +107 -0
- package/examples/todo-orbit/openuispec/tokens/motion.yaml +39 -0
- package/examples/todo-orbit/openuispec/tokens/spacing.yaml +18 -0
- package/examples/todo-orbit/openuispec/tokens/themes.yaml +23 -0
- package/examples/todo-orbit/openuispec/tokens/typography.yaml +52 -0
- package/package.json +1 -1
- package/schema/validate.ts +271 -4
- package/spec/openuispec-v0.1.md +80 -13
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
struct SchedulePreviewResult {
|
|
4
|
+
enum State {
|
|
5
|
+
case invalid
|
|
6
|
+
case empty
|
|
7
|
+
case ready
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let state: State
|
|
11
|
+
let dates: [Date]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
struct SchedulePreviewView: View {
|
|
15
|
+
let model: AppModel
|
|
16
|
+
let title: String
|
|
17
|
+
let result: SchedulePreviewResult
|
|
18
|
+
|
|
19
|
+
var body: some View {
|
|
20
|
+
VStack(alignment: .leading, spacing: 14) {
|
|
21
|
+
Text("x_schedule_preview.detail")
|
|
22
|
+
.font(.caption.weight(.bold))
|
|
23
|
+
.foregroundStyle(.secondary)
|
|
24
|
+
.textCase(.uppercase)
|
|
25
|
+
Text(title)
|
|
26
|
+
.font(.title3.weight(.semibold))
|
|
27
|
+
|
|
28
|
+
switch result.state {
|
|
29
|
+
case .invalid:
|
|
30
|
+
Text(model.string("recurring_preview.invalid"))
|
|
31
|
+
.font(.footnote.weight(.medium))
|
|
32
|
+
.foregroundStyle(.red)
|
|
33
|
+
.padding(14)
|
|
34
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
35
|
+
.background(Color.red.opacity(0.08), in: CutCornerShape(cut: 12))
|
|
36
|
+
case .empty:
|
|
37
|
+
Text(model.string("recurring_preview.empty"))
|
|
38
|
+
.foregroundStyle(.secondary)
|
|
39
|
+
case .ready:
|
|
40
|
+
VStack(spacing: 10) {
|
|
41
|
+
ForEach(Array(result.dates.enumerated()), id: \.offset) { index, date in
|
|
42
|
+
HStack {
|
|
43
|
+
Text(index == 0 ? "Next" : "+\(index)")
|
|
44
|
+
.font(.caption.weight(.bold))
|
|
45
|
+
.foregroundStyle(index == 0 ? .teal : .secondary)
|
|
46
|
+
Spacer()
|
|
47
|
+
Text(model.formatDate(date))
|
|
48
|
+
.font(.subheadline.weight(.semibold))
|
|
49
|
+
}
|
|
50
|
+
.padding(14)
|
|
51
|
+
.background(
|
|
52
|
+
CutCornerShape(cut: 12)
|
|
53
|
+
.fill(index == 0 ? Color.teal.opacity(0.08) : Color(uiColor: .secondarySystemBackground))
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
.orbitCard(fill: Color(uiColor: .systemBackground), stroke: Color.orange.opacity(0.22))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static func compute(from draft: RecurringRuleDraft) -> SchedulePreviewResult {
|
|
63
|
+
guard let cadence = draft.cadence, let interval = Int(draft.interval), interval >= 1 else {
|
|
64
|
+
return .init(state: .invalid, dates: [])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if draft.hasEndDate, draft.endDate < draft.startDate {
|
|
68
|
+
return .init(state: .invalid, dates: [])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if cadence == .weekly, draft.weekday == nil {
|
|
72
|
+
return .init(state: .invalid, dates: [])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if cadence == .monthly {
|
|
76
|
+
guard let day = Int(draft.monthDay), (1...28).contains(day) else {
|
|
77
|
+
return .init(state: .invalid, dates: [])
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let calendar = Calendar.current
|
|
82
|
+
var dates: [Date] = []
|
|
83
|
+
var cursor = draft.startDate
|
|
84
|
+
let endDate = draft.hasEndDate ? draft.endDate : nil
|
|
85
|
+
|
|
86
|
+
for _ in 0..<4 {
|
|
87
|
+
let next: Date?
|
|
88
|
+
switch cadence {
|
|
89
|
+
case .daily:
|
|
90
|
+
next = cursor
|
|
91
|
+
cursor = calendar.date(byAdding: .day, value: interval, to: cursor) ?? cursor
|
|
92
|
+
case .weekly:
|
|
93
|
+
next = nextWeeklyDate(startingAt: cursor, weekday: draft.weekday!, interval: interval)
|
|
94
|
+
cursor = calendar.date(byAdding: .day, value: interval * 7, to: next ?? cursor) ?? cursor
|
|
95
|
+
case .monthly:
|
|
96
|
+
next = nextMonthlyDate(startingAt: cursor, day: Int(draft.monthDay)!, interval: interval)
|
|
97
|
+
cursor = calendar.date(byAdding: .month, value: interval, to: next ?? cursor) ?? cursor
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
guard let next else { break }
|
|
101
|
+
if let endDate, next > endDate { break }
|
|
102
|
+
dates.append(next)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return dates.isEmpty ? .init(state: .empty, dates: []) : .init(state: .ready, dates: dates)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private static func nextWeeklyDate(startingAt start: Date, weekday: Weekday, interval: Int) -> Date? {
|
|
109
|
+
let calendar = Calendar.current
|
|
110
|
+
var next = start
|
|
111
|
+
while calendar.component(.weekday, from: next) != weekday.calendarIndex {
|
|
112
|
+
guard let candidate = calendar.date(byAdding: .day, value: 1, to: next) else { return nil }
|
|
113
|
+
next = candidate
|
|
114
|
+
}
|
|
115
|
+
return next
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private static func nextMonthlyDate(startingAt start: Date, day: Int, interval: Int) -> Date? {
|
|
119
|
+
let calendar = Calendar.current
|
|
120
|
+
var components = calendar.dateComponents([.year, .month], from: start)
|
|
121
|
+
components.day = day
|
|
122
|
+
guard let initial = calendar.date(from: components) else { return nil }
|
|
123
|
+
if initial >= start { return initial }
|
|
124
|
+
return calendar.date(byAdding: .month, value: interval, to: initial)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import Charts
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
struct TrendChartView: View {
|
|
5
|
+
let title: String
|
|
6
|
+
let subtitle: String
|
|
7
|
+
let emptyMessage: String
|
|
8
|
+
let legendCompleted: String
|
|
9
|
+
let legendCreated: String
|
|
10
|
+
let points: [TrendPoint]
|
|
11
|
+
|
|
12
|
+
var body: some View {
|
|
13
|
+
VStack(alignment: .leading, spacing: 14) {
|
|
14
|
+
Text(subtitle)
|
|
15
|
+
.font(.caption.weight(.bold))
|
|
16
|
+
.foregroundStyle(.secondary)
|
|
17
|
+
.textCase(.uppercase)
|
|
18
|
+
Text(title)
|
|
19
|
+
.font(.title3.weight(.semibold))
|
|
20
|
+
|
|
21
|
+
if points.isEmpty {
|
|
22
|
+
Text(emptyMessage)
|
|
23
|
+
.font(.body)
|
|
24
|
+
.foregroundStyle(.secondary)
|
|
25
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
26
|
+
.padding(.vertical, 40)
|
|
27
|
+
} else {
|
|
28
|
+
Chart(points) { point in
|
|
29
|
+
LineMark(
|
|
30
|
+
x: .value("Label", point.label),
|
|
31
|
+
y: .value(legendCreated, point.created)
|
|
32
|
+
)
|
|
33
|
+
.foregroundStyle(.orange)
|
|
34
|
+
.interpolationMethod(.catmullRom)
|
|
35
|
+
|
|
36
|
+
LineMark(
|
|
37
|
+
x: .value("Label", point.label),
|
|
38
|
+
y: .value(legendCompleted, point.completed)
|
|
39
|
+
)
|
|
40
|
+
.foregroundStyle(.teal)
|
|
41
|
+
.interpolationMethod(.catmullRom)
|
|
42
|
+
|
|
43
|
+
AreaMark(
|
|
44
|
+
x: .value("Label", point.label),
|
|
45
|
+
y: .value(legendCompleted, point.completed)
|
|
46
|
+
)
|
|
47
|
+
.foregroundStyle(.teal.opacity(0.08))
|
|
48
|
+
}
|
|
49
|
+
.frame(height: 280)
|
|
50
|
+
|
|
51
|
+
HStack(spacing: 18) {
|
|
52
|
+
legend(.teal, title: legendCompleted)
|
|
53
|
+
legend(.orange, title: legendCreated)
|
|
54
|
+
}
|
|
55
|
+
.font(.footnote)
|
|
56
|
+
.foregroundStyle(.secondary)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
.orbitCard(fill: Color(uiColor: .systemBackground), stroke: Color.teal.opacity(0.18))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private func legend(_ color: Color, title: String) -> some View {
|
|
63
|
+
HStack(spacing: 8) {
|
|
64
|
+
Circle()
|
|
65
|
+
.fill(color)
|
|
66
|
+
.frame(width: 10, height: 10)
|
|
67
|
+
Text(title)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
struct RecurringRuleSheet: View {
|
|
4
|
+
@Environment(\.dismiss) private var dismiss
|
|
5
|
+
@ObservedObject var model: AppModel
|
|
6
|
+
|
|
7
|
+
@State private var draft = RecurringRuleDraft()
|
|
8
|
+
@State private var errors: [String: String] = [:]
|
|
9
|
+
|
|
10
|
+
var body: some View {
|
|
11
|
+
NavigationStack {
|
|
12
|
+
ScrollView {
|
|
13
|
+
VStack(alignment: .leading, spacing: 18) {
|
|
14
|
+
if !errors.isEmpty {
|
|
15
|
+
Text(model.string("validation.fix_errors"))
|
|
16
|
+
.font(.footnote.weight(.medium))
|
|
17
|
+
.foregroundStyle(.orange)
|
|
18
|
+
.padding(14)
|
|
19
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
20
|
+
.background(Color.orange.opacity(0.1), in: CutCornerShape(cut: 12))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
VStack(spacing: 14) {
|
|
24
|
+
TextField(model.string("recurring_rule.field_name"), text: $draft.name)
|
|
25
|
+
errorText("name")
|
|
26
|
+
|
|
27
|
+
TextField(model.string("recurring_rule.field_confirm_name"), text: $draft.confirmName)
|
|
28
|
+
errorText("confirmName")
|
|
29
|
+
|
|
30
|
+
Picker(model.string("recurring_rule.field_cadence"), selection: $draft.cadence) {
|
|
31
|
+
Text("—").tag(RecurrenceCadence?.none)
|
|
32
|
+
ForEach(RecurrenceCadence.allCases) { cadence in
|
|
33
|
+
Text(model.string("recurring_rule.cadence_\(cadence.rawValue)"))
|
|
34
|
+
.tag(RecurrenceCadence?.some(cadence))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
errorText("cadence")
|
|
38
|
+
|
|
39
|
+
TextField(model.string("recurring_rule.field_interval"), text: $draft.interval)
|
|
40
|
+
.keyboardType(.numberPad)
|
|
41
|
+
errorText("interval")
|
|
42
|
+
|
|
43
|
+
if draft.cadence == .weekly {
|
|
44
|
+
Picker(model.string("recurring_rule.field_weekday"), selection: $draft.weekday) {
|
|
45
|
+
Text("—").tag(Weekday?.none)
|
|
46
|
+
ForEach(Weekday.allCases) { weekday in
|
|
47
|
+
Text(model.label(for: weekday)).tag(Weekday?.some(weekday))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
errorText("weekday")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if draft.cadence == .monthly {
|
|
54
|
+
TextField(model.string("recurring_rule.field_month_day"), text: $draft.monthDay)
|
|
55
|
+
.keyboardType(.numberPad)
|
|
56
|
+
errorText("monthDay")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
DatePicker(model.string("recurring_rule.field_start_date"), selection: $draft.startDate, displayedComponents: .date)
|
|
60
|
+
|
|
61
|
+
Toggle(model.string("recurring_rule.field_has_end_date"), isOn: $draft.hasEndDate)
|
|
62
|
+
if draft.hasEndDate {
|
|
63
|
+
DatePicker(model.string("recurring_rule.field_end_date"), selection: $draft.endDate, displayedComponents: .date)
|
|
64
|
+
errorText("endDate")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if model.preferences.remindersEnabled {
|
|
68
|
+
TextField(model.string("recurring_rule.field_remind_at"), text: $draft.remindAt)
|
|
69
|
+
.textInputAutocapitalization(.never)
|
|
70
|
+
errorText("remindAt")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Toggle(model.string("recurring_rule.field_enable_summary"), isOn: $draft.enableSummary)
|
|
74
|
+
if draft.enableSummary {
|
|
75
|
+
Picker(model.string("recurring_rule.field_summary_channel"), selection: $draft.summaryChannel) {
|
|
76
|
+
Text("—").tag(SummaryChannel?.none)
|
|
77
|
+
ForEach(SummaryChannel.allCases) { channel in
|
|
78
|
+
Text(model.string("recurring_rule.summary_\(channel.rawValue)"))
|
|
79
|
+
.tag(SummaryChannel?.some(channel))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
errorText("summaryChannel")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
.textFieldStyle(.roundedBorder)
|
|
86
|
+
.orbitCard(fill: Color(uiColor: .secondarySystemGroupedBackground), stroke: Color.teal.opacity(0.18))
|
|
87
|
+
|
|
88
|
+
SchedulePreviewView(
|
|
89
|
+
model: model,
|
|
90
|
+
title: model.string("recurring_preview.title"),
|
|
91
|
+
result: SchedulePreviewView.compute(from: draft)
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
.padding()
|
|
95
|
+
}
|
|
96
|
+
.navigationTitle(model.string("recurring_rule.title"))
|
|
97
|
+
.toolbar {
|
|
98
|
+
ToolbarItem(placement: .topBarLeading) {
|
|
99
|
+
Button(model.string("common.cancel")) { dismiss() }
|
|
100
|
+
}
|
|
101
|
+
ToolbarItem(placement: .topBarTrailing) {
|
|
102
|
+
Button(model.string("recurring_rule.save")) {
|
|
103
|
+
errors = model.addRecurringRule(from: draft)
|
|
104
|
+
if errors.isEmpty {
|
|
105
|
+
dismiss()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
.presentationDetents([.large])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@ViewBuilder
|
|
115
|
+
private func errorText(_ key: String) -> some View {
|
|
116
|
+
if let message = errors[key] {
|
|
117
|
+
Text(message)
|
|
118
|
+
.font(.footnote)
|
|
119
|
+
.foregroundStyle(.red)
|
|
120
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
struct TaskEditorSheet: View {
|
|
4
|
+
@Environment(\.dismiss) private var dismiss
|
|
5
|
+
@ObservedObject var model: AppModel
|
|
6
|
+
let editingTaskID: UUID?
|
|
7
|
+
|
|
8
|
+
@State private var draft = TaskEditorDraft(title: "", notes: "", priority: .medium, dueDate: nil)
|
|
9
|
+
@State private var validationError: String?
|
|
10
|
+
|
|
11
|
+
var body: some View {
|
|
12
|
+
NavigationStack {
|
|
13
|
+
Form {
|
|
14
|
+
if let validationError {
|
|
15
|
+
Section {
|
|
16
|
+
Text(validationError)
|
|
17
|
+
.font(.footnote.weight(.medium))
|
|
18
|
+
.foregroundStyle(.red)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Section {
|
|
23
|
+
TextField(model.string(editingTaskID == nil ? "create_task.field_title" : "edit_task.field_title"), text: $draft.title)
|
|
24
|
+
TextField(model.string(editingTaskID == nil ? "create_task.field_notes" : "edit_task.field_notes"), text: $draft.notes, axis: .vertical)
|
|
25
|
+
Picker(model.string(editingTaskID == nil ? "create_task.field_priority" : "edit_task.field_priority"), selection: $draft.priority) {
|
|
26
|
+
ForEach(TaskPriority.allCases) { priority in
|
|
27
|
+
Text(model.label(for: priority)).tag(priority)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
DatePicker(
|
|
31
|
+
model.string(editingTaskID == nil ? "create_task.field_due_date" : "edit_task.field_due_date"),
|
|
32
|
+
selection: Binding(
|
|
33
|
+
get: { draft.dueDate ?? .now },
|
|
34
|
+
set: { draft.dueDate = $0 }
|
|
35
|
+
),
|
|
36
|
+
displayedComponents: .date
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
.navigationTitle(model.string(editingTaskID == nil ? "create_task.title" : "edit_task.title"))
|
|
41
|
+
.toolbar {
|
|
42
|
+
ToolbarItem(placement: .topBarLeading) {
|
|
43
|
+
Button(model.string("common.cancel")) { dismiss() }
|
|
44
|
+
}
|
|
45
|
+
ToolbarItem(placement: .topBarTrailing) {
|
|
46
|
+
Button(model.string(editingTaskID == nil ? "create_task.save" : "edit_task.save")) {
|
|
47
|
+
validationError = model.submitTask(draft, editing: editingTaskID)
|
|
48
|
+
if validationError == nil {
|
|
49
|
+
dismiss()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
.onAppear {
|
|
56
|
+
draft = model.makeTaskDraft(for: model.task(id: editingTaskID))
|
|
57
|
+
}
|
|
58
|
+
.presentationDetents([.medium, .large])
|
|
59
|
+
}
|
|
60
|
+
}
|
package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
enum AppLocale: String, CaseIterable, Identifiable {
|
|
5
|
+
case en
|
|
6
|
+
case ru
|
|
7
|
+
|
|
8
|
+
var id: String { rawValue }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
enum ThemePreference: String, CaseIterable, Identifiable {
|
|
12
|
+
case light
|
|
13
|
+
case dark
|
|
14
|
+
|
|
15
|
+
var id: String { rawValue }
|
|
16
|
+
|
|
17
|
+
var colorScheme: ColorScheme {
|
|
18
|
+
switch self {
|
|
19
|
+
case .light: .light
|
|
20
|
+
case .dark: .dark
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
enum TaskStatus: String, CaseIterable, Identifiable {
|
|
26
|
+
case open
|
|
27
|
+
case done
|
|
28
|
+
|
|
29
|
+
var id: String { rawValue }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
enum TaskPriority: String, CaseIterable, Identifiable {
|
|
33
|
+
case low
|
|
34
|
+
case medium
|
|
35
|
+
case high
|
|
36
|
+
|
|
37
|
+
var id: String { rawValue }
|
|
38
|
+
|
|
39
|
+
var tint: Color {
|
|
40
|
+
switch self {
|
|
41
|
+
case .low: Color(red: 148 / 255, green: 163 / 255, blue: 184 / 255)
|
|
42
|
+
case .medium: Color(red: 37 / 255, green: 99 / 255, blue: 235 / 255)
|
|
43
|
+
case .high: Color(red: 217 / 255, green: 119 / 255, blue: 6 / 255)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
enum TaskFilter: String, CaseIterable, Identifiable {
|
|
49
|
+
case all
|
|
50
|
+
case open
|
|
51
|
+
case done
|
|
52
|
+
|
|
53
|
+
var id: String { rawValue }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
enum AnalyticsPeriod: String, CaseIterable, Identifiable {
|
|
57
|
+
case week
|
|
58
|
+
case month
|
|
59
|
+
case quarter
|
|
60
|
+
|
|
61
|
+
var id: String { rawValue }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
enum RecurrenceCadence: String, CaseIterable, Identifiable {
|
|
65
|
+
case daily
|
|
66
|
+
case weekly
|
|
67
|
+
case monthly
|
|
68
|
+
|
|
69
|
+
var id: String { rawValue }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
enum Weekday: String, CaseIterable, Identifiable {
|
|
73
|
+
case mon
|
|
74
|
+
case tue
|
|
75
|
+
case wed
|
|
76
|
+
case thu
|
|
77
|
+
case fri
|
|
78
|
+
case sat
|
|
79
|
+
case sun
|
|
80
|
+
|
|
81
|
+
var id: String { rawValue }
|
|
82
|
+
|
|
83
|
+
var calendarIndex: Int {
|
|
84
|
+
switch self {
|
|
85
|
+
case .sun: 1
|
|
86
|
+
case .mon: 2
|
|
87
|
+
case .tue: 3
|
|
88
|
+
case .wed: 4
|
|
89
|
+
case .thu: 5
|
|
90
|
+
case .fri: 6
|
|
91
|
+
case .sat: 7
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
enum SummaryChannel: String, CaseIterable, Identifiable {
|
|
97
|
+
case push
|
|
98
|
+
case email
|
|
99
|
+
|
|
100
|
+
var id: String { rawValue }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
struct Preferences: Equatable {
|
|
104
|
+
var locale: AppLocale
|
|
105
|
+
var theme: ThemePreference
|
|
106
|
+
var remindersEnabled: Bool
|
|
107
|
+
var dailySummaryEnabled: Bool
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
struct Task: Identifiable, Equatable {
|
|
111
|
+
let id: UUID
|
|
112
|
+
var title: String
|
|
113
|
+
var notes: String
|
|
114
|
+
var status: TaskStatus
|
|
115
|
+
var priority: TaskPriority
|
|
116
|
+
var dueDate: Date?
|
|
117
|
+
let createdAt: Date
|
|
118
|
+
var updatedAt: Date
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
struct RecurringRule: Identifiable, Equatable {
|
|
122
|
+
let id: UUID
|
|
123
|
+
var name: String
|
|
124
|
+
var cadence: RecurrenceCadence
|
|
125
|
+
var interval: Int
|
|
126
|
+
var weekday: Weekday?
|
|
127
|
+
var monthDay: Int?
|
|
128
|
+
var startDate: Date
|
|
129
|
+
var endDate: Date?
|
|
130
|
+
var remindAt: String?
|
|
131
|
+
var summaryChannel: SummaryChannel?
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
struct TrendPoint: Identifiable, Equatable {
|
|
135
|
+
let id = UUID()
|
|
136
|
+
let label: String
|
|
137
|
+
let completed: Int
|
|
138
|
+
let created: Int
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
struct AnalyticsSnapshot: Equatable {
|
|
142
|
+
let completedToday: Int
|
|
143
|
+
let openTasks: Int
|
|
144
|
+
let overdueTasks: Int
|
|
145
|
+
let completionRate: Int
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
struct ToastMessage: Identifiable, Equatable {
|
|
149
|
+
enum Level {
|
|
150
|
+
case success
|
|
151
|
+
case warning
|
|
152
|
+
case error
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let id = UUID()
|
|
156
|
+
let level: Level
|
|
157
|
+
let text: String
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
struct TaskEditorDraft {
|
|
161
|
+
var title: String
|
|
162
|
+
var notes: String
|
|
163
|
+
var priority: TaskPriority
|
|
164
|
+
var dueDate: Date?
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
struct RecurringRuleDraft {
|
|
168
|
+
var name: String = ""
|
|
169
|
+
var confirmName: String = ""
|
|
170
|
+
var cadence: RecurrenceCadence?
|
|
171
|
+
var interval: String = "1"
|
|
172
|
+
var weekday: Weekday?
|
|
173
|
+
var monthDay: String = ""
|
|
174
|
+
var startDate: Date = .now
|
|
175
|
+
var hasEndDate: Bool = false
|
|
176
|
+
var endDate: Date = .now
|
|
177
|
+
var remindAt: String = ""
|
|
178
|
+
var enableSummary: Bool = false
|
|
179
|
+
var summaryChannel: SummaryChannel?
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
extension Task {
|
|
183
|
+
static func seed(referenceDate: Date = .now) -> [Task] {
|
|
184
|
+
let calendar = Calendar.current
|
|
185
|
+
return [
|
|
186
|
+
Task(
|
|
187
|
+
id: UUID(uuidString: "A6AC1D32-DF3E-4A87-9E11-6E9C2A52CF11")!,
|
|
188
|
+
title: "Prepare bilingual launch notes",
|
|
189
|
+
notes: "Document the web, iOS, and Android behavior differences before review.",
|
|
190
|
+
status: .open,
|
|
191
|
+
priority: .high,
|
|
192
|
+
dueDate: calendar.date(byAdding: .day, value: 2, to: referenceDate),
|
|
193
|
+
createdAt: calendar.date(byAdding: .day, value: -6, to: referenceDate)!,
|
|
194
|
+
updatedAt: calendar.date(byAdding: .day, value: -1, to: referenceDate)!
|
|
195
|
+
),
|
|
196
|
+
Task(
|
|
197
|
+
id: UUID(uuidString: "A6AC1D32-DF3E-4A87-9E11-6E9C2A52CF12")!,
|
|
198
|
+
title: "Review recurring-rule validation",
|
|
199
|
+
notes: "Confirm async uniqueness checks and cross-field constraints.",
|
|
200
|
+
status: .done,
|
|
201
|
+
priority: .medium,
|
|
202
|
+
dueDate: calendar.date(byAdding: .day, value: -1, to: referenceDate),
|
|
203
|
+
createdAt: calendar.date(byAdding: .day, value: -5, to: referenceDate)!,
|
|
204
|
+
updatedAt: referenceDate
|
|
205
|
+
),
|
|
206
|
+
Task(
|
|
207
|
+
id: UUID(uuidString: "A6AC1D32-DF3E-4A87-9E11-6E9C2A52CF13")!,
|
|
208
|
+
title: "Polish analytics empty states",
|
|
209
|
+
notes: "Ensure chart and overdue list degrade gracefully on zero-data snapshots.",
|
|
210
|
+
status: .open,
|
|
211
|
+
priority: .medium,
|
|
212
|
+
dueDate: calendar.date(byAdding: .day, value: 5, to: referenceDate),
|
|
213
|
+
createdAt: calendar.date(byAdding: .day, value: -4, to: referenceDate)!,
|
|
214
|
+
updatedAt: calendar.date(byAdding: .day, value: -2, to: referenceDate)!
|
|
215
|
+
),
|
|
216
|
+
Task(
|
|
217
|
+
id: UUID(uuidString: "A6AC1D32-DF3E-4A87-9E11-6E9C2A52CF14")!,
|
|
218
|
+
title: "Regenerate drift snapshots",
|
|
219
|
+
notes: "Refresh ios, android, and web state after spec edits.",
|
|
220
|
+
status: .open,
|
|
221
|
+
priority: .low,
|
|
222
|
+
dueDate: calendar.date(byAdding: .day, value: -3, to: referenceDate),
|
|
223
|
+
createdAt: calendar.date(byAdding: .day, value: -3, to: referenceDate)!,
|
|
224
|
+
updatedAt: calendar.date(byAdding: .day, value: -3, to: referenceDate)!
|
|
225
|
+
),
|
|
226
|
+
Task(
|
|
227
|
+
id: UUID(uuidString: "A6AC1D32-DF3E-4A87-9E11-6E9C2A52CF15")!,
|
|
228
|
+
title: "Prototype schedule preview contract",
|
|
229
|
+
notes: "Use derived occurrences to prove custom-contract generation.",
|
|
230
|
+
status: .done,
|
|
231
|
+
priority: .high,
|
|
232
|
+
dueDate: calendar.date(byAdding: .day, value: 1, to: referenceDate),
|
|
233
|
+
createdAt: calendar.date(byAdding: .day, value: -8, to: referenceDate)!,
|
|
234
|
+
updatedAt: calendar.date(byAdding: .day, value: -1, to: referenceDate)!
|
|
235
|
+
)
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
}
|