openuispec 0.1.27 → 0.1.28
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 +22 -19
- package/cli/init.ts +7 -7
- package/docs/implementation-notes.md +5 -1
- package/docs/release-notes-v0.1.28.md +25 -0
- package/docs/stress-test-maturity-report.md +1 -1
- package/drift/index.ts +21 -4
- package/examples/taskflow/AGENTS.md +112 -0
- package/examples/taskflow/CLAUDE.md +112 -0
- package/examples/taskflow/generated/android/TaskFlow/README.md +43 -0
- package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +76 -0
- package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +1 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +21 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +19 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +283 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +106 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +57 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +109 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +112 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +61 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +82 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +111 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +77 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +30 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +86 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +57 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +155 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +4 -0
- package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +5 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +12 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle.properties +4 -0
- package/examples/taskflow/generated/android/TaskFlow/gradlew +18 -0
- package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +12 -0
- package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +18 -0
- package/examples/taskflow/generated/ios/TaskFlow/README.md +21 -0
- package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +115 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +24 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +150 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +220 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +122 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +21 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +201 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +48 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +59 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +63 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +85 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +219 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +320 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +41 -0
- package/examples/taskflow/generated/ios/TaskFlow/project.yml +26 -0
- package/examples/taskflow/generated/web/TaskFlow/README.md +19 -0
- package/examples/taskflow/generated/web/TaskFlow/index.html +12 -0
- package/examples/taskflow/generated/web/TaskFlow/package-lock.json +1908 -0
- package/examples/taskflow/generated/web/TaskFlow/package.json +24 -0
- package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +58 -0
- package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +55 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +82 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +191 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +41 -0
- package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +131 -0
- package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +25 -0
- package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +39 -0
- package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +111 -0
- package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +13 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +111 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +82 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +132 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +105 -0
- package/examples/taskflow/generated/web/TaskFlow/src/store.ts +216 -0
- package/examples/taskflow/generated/web/TaskFlow/src/styles.css +617 -0
- package/examples/taskflow/generated/web/TaskFlow/src/types.ts +64 -0
- package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +78 -0
- package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +21 -0
- package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +6 -0
- package/examples/taskflow/openuispec/README.md +49 -0
- package/examples/todo-orbit/AGENTS.md +44 -19
- package/examples/todo-orbit/CLAUDE.md +44 -19
- package/examples/todo-orbit/openuispec/README.md +2 -2
- package/package.json +1 -1
- package/schema/validate.ts +9 -4
- package/status/index.ts +16 -3
- /package/examples/taskflow/{contracts → openuispec/contracts}/README.md +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/action_trigger.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/collection.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/data_display.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/feedback.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/input_field.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/nav_container.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/surface.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/x_media_player.yaml +0 -0
- /package/examples/taskflow/{flows → openuispec/flows}/create_task.yaml +0 -0
- /package/examples/taskflow/{flows → openuispec/flows}/edit_task.yaml +0 -0
- /package/examples/taskflow/{locales → openuispec/locales}/en.json +0 -0
- /package/examples/taskflow/{openuispec.yaml → openuispec/openuispec.yaml} +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/android.yaml +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/ios.yaml +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/web.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/calendar.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/home.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/profile_edit.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/project_detail.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/projects.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/settings.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/task_detail.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/color.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/elevation.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/icons.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/layout.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/motion.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/spacing.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/themes.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/typography.yaml +0 -0
package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import AVKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
struct TaskDetailView: View {
|
|
5
|
+
@Bindable var model: AppModel
|
|
6
|
+
let taskID: UUID
|
|
7
|
+
@State private var showDeletePrompt = false
|
|
8
|
+
|
|
9
|
+
private var task: Task? {
|
|
10
|
+
model.tasks.first(where: { $0.id == taskID })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
var body: some View {
|
|
14
|
+
Group {
|
|
15
|
+
if let task {
|
|
16
|
+
ScrollView {
|
|
17
|
+
TaskDetailContent(model: model, task: task) {
|
|
18
|
+
showDeletePrompt = true
|
|
19
|
+
}
|
|
20
|
+
.padding(24)
|
|
21
|
+
}
|
|
22
|
+
.navigationTitle(task.title)
|
|
23
|
+
.navigationBarTitleDisplayMode(.inline)
|
|
24
|
+
.alert(model.localized("task_detail.delete_title"), isPresented: $showDeletePrompt) {
|
|
25
|
+
Button(model.localized("common.cancel"), role: .cancel) {}
|
|
26
|
+
Button(model.localized("common.delete"), role: .destructive) {
|
|
27
|
+
model.delete(task: task)
|
|
28
|
+
}
|
|
29
|
+
} message: {
|
|
30
|
+
Text(String(format: model.localized("task_detail.delete_message"), task.title))
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
ContentUnavailableView("Task missing", systemImage: "exclamationmark.triangle")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct TaskDetailPanel: View {
|
|
40
|
+
@Bindable var model: AppModel
|
|
41
|
+
let taskID: UUID
|
|
42
|
+
@State private var showDeletePrompt = false
|
|
43
|
+
|
|
44
|
+
private var task: Task? {
|
|
45
|
+
model.tasks.first(where: { $0.id == taskID })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var body: some View {
|
|
49
|
+
Group {
|
|
50
|
+
if let task {
|
|
51
|
+
TaskDetailContent(model: model, task: task) {
|
|
52
|
+
showDeletePrompt = true
|
|
53
|
+
}
|
|
54
|
+
.padding(24)
|
|
55
|
+
.background(.background)
|
|
56
|
+
.clipShape(RoundedRectangle(cornerRadius: 28))
|
|
57
|
+
.alert(model.localized("task_detail.delete_title"), isPresented: $showDeletePrompt) {
|
|
58
|
+
Button(model.localized("common.cancel"), role: .cancel) {}
|
|
59
|
+
Button(model.localized("common.delete"), role: .destructive) {
|
|
60
|
+
model.delete(task: task)
|
|
61
|
+
}
|
|
62
|
+
} message: {
|
|
63
|
+
Text(String(format: model.localized("task_detail.delete_message"), task.title))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private struct TaskDetailContent: View {
|
|
71
|
+
@Bindable var model: AppModel
|
|
72
|
+
let task: Task
|
|
73
|
+
let requestDelete: () -> Void
|
|
74
|
+
|
|
75
|
+
var body: some View {
|
|
76
|
+
VStack(alignment: .leading, spacing: 24) {
|
|
77
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
78
|
+
Text(task.title)
|
|
79
|
+
.font(.system(size: 28, weight: .bold, design: .rounded))
|
|
80
|
+
HStack(spacing: 12) {
|
|
81
|
+
badge(model.statusLabel(task.status), color: statusColor(task.status))
|
|
82
|
+
badge(model.priorityLabel(task.priority), color: priorityColor(task.priority))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
HStack(spacing: 12) {
|
|
87
|
+
statCard(model.localized("task_detail.status"), value: model.statusLabel(task.status), color: statusColor(task.status))
|
|
88
|
+
statCard(model.localized("task_detail.priority"), value: model.priorityLabel(task.priority), color: priorityColor(task.priority))
|
|
89
|
+
statCard(model.localized("task_detail.due"), value: task.dueDate?.formatted(date: .abbreviated, time: .omitted) ?? "No due date", color: AppPalette.info)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if let description = task.description, !description.isEmpty {
|
|
93
|
+
detailBlock(title: model.localized("task_detail.description")) {
|
|
94
|
+
Text(description)
|
|
95
|
+
.foregroundStyle(AppPalette.textSecondary)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if let attachment = task.attachment {
|
|
100
|
+
detailBlock(title: "Media") {
|
|
101
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
102
|
+
Label(attachment.title, systemImage: attachment.mediaType == "video" ? "play.rectangle.fill" : "waveform")
|
|
103
|
+
.font(.headline)
|
|
104
|
+
if attachment.mediaType == "video", let mediaURL = attachment.url {
|
|
105
|
+
VideoPlayer(player: AVPlayer(url: mediaURL))
|
|
106
|
+
.frame(minHeight: 220)
|
|
107
|
+
.clipShape(RoundedRectangle(cornerRadius: 16))
|
|
108
|
+
} else {
|
|
109
|
+
Text(model.localized("media_player.error"))
|
|
110
|
+
.font(.subheadline)
|
|
111
|
+
.foregroundStyle(AppPalette.textSecondary)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
.padding()
|
|
115
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
116
|
+
.background(AppPalette.surfaceSecondary)
|
|
117
|
+
.clipShape(RoundedRectangle(cornerRadius: 20))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
detailBlock(title: model.localized("task_detail.details")) {
|
|
122
|
+
VStack(spacing: 12) {
|
|
123
|
+
detailRow(model.localized("task_detail.project"), value: model.project(for: task)?.name ?? "No project", symbol: "folder")
|
|
124
|
+
detailRow(model.localized("task_detail.assignee"), value: model.assignee(for: task)?.name ?? model.localized("task_detail.unassigned"), symbol: "person")
|
|
125
|
+
detailRow(model.localized("task_detail.tags"), value: task.tags.isEmpty ? "No tags" : task.tags.joined(separator: ", "), symbol: "tag")
|
|
126
|
+
detailRow(model.localized("task_detail.created"), value: task.createdAt.formatted(date: .abbreviated, time: .shortened), symbol: "clock")
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
HStack(spacing: 12) {
|
|
131
|
+
Button(model.localized("task_detail.edit")) {
|
|
132
|
+
model.presentedSheet = .editTask(task.id)
|
|
133
|
+
}
|
|
134
|
+
.buttonStyle(.borderedProminent)
|
|
135
|
+
|
|
136
|
+
Button(model.localized("task_detail.assign_to")) {
|
|
137
|
+
model.presentedSheet = .assignTask(task.id)
|
|
138
|
+
}
|
|
139
|
+
.buttonStyle(.bordered)
|
|
140
|
+
|
|
141
|
+
Button(task.status == .done ? model.localized("task_detail.reopen") : model.localized("task_detail.complete")) {
|
|
142
|
+
model.toggle(task: task)
|
|
143
|
+
}
|
|
144
|
+
.buttonStyle(.bordered)
|
|
145
|
+
|
|
146
|
+
Button(model.localized("task_detail.delete"), role: .destructive) {
|
|
147
|
+
requestDelete()
|
|
148
|
+
}
|
|
149
|
+
.buttonStyle(.bordered)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private func badge(_ text: String, color: Color) -> some View {
|
|
155
|
+
Text(text)
|
|
156
|
+
.font(.subheadline.weight(.semibold))
|
|
157
|
+
.padding(.horizontal, 12)
|
|
158
|
+
.padding(.vertical, 8)
|
|
159
|
+
.background(color.opacity(0.14))
|
|
160
|
+
.foregroundStyle(color)
|
|
161
|
+
.clipShape(Capsule())
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private func statCard(_ title: String, value: String, color: Color) -> some View {
|
|
165
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
166
|
+
Text(title)
|
|
167
|
+
.font(.caption.weight(.semibold))
|
|
168
|
+
.foregroundStyle(AppPalette.textSecondary)
|
|
169
|
+
Text(value)
|
|
170
|
+
.font(.headline)
|
|
171
|
+
Circle()
|
|
172
|
+
.fill(color)
|
|
173
|
+
.frame(width: 10, height: 10)
|
|
174
|
+
}
|
|
175
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
176
|
+
.padding()
|
|
177
|
+
.background(AppPalette.surfaceSecondary)
|
|
178
|
+
.clipShape(RoundedRectangle(cornerRadius: 20))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private func detailBlock<Content: View>(title: String, @ViewBuilder content: () -> Content) -> some View {
|
|
182
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
183
|
+
Text(title.uppercased())
|
|
184
|
+
.font(.caption.weight(.bold))
|
|
185
|
+
.foregroundStyle(AppPalette.textTertiary)
|
|
186
|
+
content()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private func detailRow(_ title: String, value: String, symbol: String) -> some View {
|
|
191
|
+
HStack {
|
|
192
|
+
Label(title, systemImage: symbol)
|
|
193
|
+
.foregroundStyle(AppPalette.textSecondary)
|
|
194
|
+
Spacer()
|
|
195
|
+
Text(value)
|
|
196
|
+
.multilineTextAlignment(.trailing)
|
|
197
|
+
}
|
|
198
|
+
.padding()
|
|
199
|
+
.background(AppPalette.surfaceSecondary)
|
|
200
|
+
.clipShape(RoundedRectangle(cornerRadius: 18))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private func priorityColor(_ priority: TaskPriority) -> Color {
|
|
204
|
+
switch priority {
|
|
205
|
+
case .low: Color(hex: "#9CA3AF")
|
|
206
|
+
case .medium: AppPalette.info
|
|
207
|
+
case .high: AppPalette.warning
|
|
208
|
+
case .urgent: AppPalette.danger
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private func statusColor(_ status: TaskStatus) -> Color {
|
|
213
|
+
switch status {
|
|
214
|
+
case .todo: Color(hex: "#9CA3AF")
|
|
215
|
+
case .inProgress: AppPalette.brandPrimary
|
|
216
|
+
case .done: AppPalette.success
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Observation
|
|
3
|
+
import SwiftUI
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
@Observable
|
|
7
|
+
final class AppModel {
|
|
8
|
+
var selectedSection: AppSection = .home
|
|
9
|
+
var homeFilter: HomeFilter = .today
|
|
10
|
+
var sortOrder: SortOrder = .dueDate
|
|
11
|
+
var searchQuery = ""
|
|
12
|
+
var selectedTaskID: UUID?
|
|
13
|
+
var selectedProjectID: UUID?
|
|
14
|
+
var presentedSheet: PresentedSheet?
|
|
15
|
+
var preferences = Preferences(
|
|
16
|
+
theme: .system,
|
|
17
|
+
defaultPriority: .medium,
|
|
18
|
+
notificationsEnabled: true,
|
|
19
|
+
remindersEnabled: true
|
|
20
|
+
)
|
|
21
|
+
var currentUser: UserProfile
|
|
22
|
+
var team: [UserProfile]
|
|
23
|
+
var projects: [Project]
|
|
24
|
+
var tasks: [Task]
|
|
25
|
+
var toastMessage: String?
|
|
26
|
+
|
|
27
|
+
private let calendar = Calendar.current
|
|
28
|
+
|
|
29
|
+
init() {
|
|
30
|
+
let me = UserProfile(
|
|
31
|
+
id: UUID(),
|
|
32
|
+
name: "Nora Malik",
|
|
33
|
+
firstName: "Nora",
|
|
34
|
+
email: "nora@taskflow.app",
|
|
35
|
+
avatarSymbol: "person.crop.circle.fill"
|
|
36
|
+
)
|
|
37
|
+
currentUser = me
|
|
38
|
+
|
|
39
|
+
let inbox = Project(id: UUID(), name: "Inbox Zero", colorHex: "#5B4FE8", icon: "tray.full")
|
|
40
|
+
let launch = Project(id: UUID(), name: "Product Launch", colorHex: "#E8634F", icon: "rocket")
|
|
41
|
+
let studio = Project(id: UUID(), name: "Studio Refresh", colorHex: "#2D9D5E", icon: "paintbrush.pointed")
|
|
42
|
+
projects = [inbox, launch, studio]
|
|
43
|
+
|
|
44
|
+
let leo = UserProfile(id: UUID(), name: "Leo Park", firstName: "Leo", email: "leo@taskflow.app", avatarSymbol: "person.crop.circle")
|
|
45
|
+
let maya = UserProfile(id: UUID(), name: "Maya Chen", firstName: "Maya", email: "maya@taskflow.app", avatarSymbol: "person.crop.circle.badge.checkmark")
|
|
46
|
+
team = [me, leo, maya]
|
|
47
|
+
|
|
48
|
+
let now = Date()
|
|
49
|
+
tasks = [
|
|
50
|
+
Task(
|
|
51
|
+
id: UUID(),
|
|
52
|
+
title: "Finalize keynote outline",
|
|
53
|
+
description: "Align story arc, confirm metrics slide, and leave time for live demo rehearsal.",
|
|
54
|
+
status: .inProgress,
|
|
55
|
+
priority: .urgent,
|
|
56
|
+
dueDate: calendar.date(byAdding: .hour, value: 6, to: now),
|
|
57
|
+
projectID: launch.id,
|
|
58
|
+
assigneeID: me.id,
|
|
59
|
+
tags: ["launch", "slides"],
|
|
60
|
+
createdAt: calendar.date(byAdding: .day, value: -4, to: now) ?? now,
|
|
61
|
+
updatedAt: now,
|
|
62
|
+
attachment: TaskAttachment(mediaType: "video", title: "Preview reel", url: URL(string: "https://example.com/reel.mp4"))
|
|
63
|
+
),
|
|
64
|
+
Task(
|
|
65
|
+
id: UUID(),
|
|
66
|
+
title: "Book photographer",
|
|
67
|
+
description: "Shortlist candidates and secure availability for the launch event.",
|
|
68
|
+
status: .todo,
|
|
69
|
+
priority: .high,
|
|
70
|
+
dueDate: calendar.date(byAdding: .day, value: 2, to: now),
|
|
71
|
+
projectID: launch.id,
|
|
72
|
+
assigneeID: leo.id,
|
|
73
|
+
tags: ["launch", "vendor"],
|
|
74
|
+
createdAt: calendar.date(byAdding: .day, value: -2, to: now) ?? now,
|
|
75
|
+
updatedAt: now,
|
|
76
|
+
attachment: nil
|
|
77
|
+
),
|
|
78
|
+
Task(
|
|
79
|
+
id: UUID(),
|
|
80
|
+
title: "Refine onboarding checklist",
|
|
81
|
+
description: "Add copy tweaks from user testing and reduce setup friction.",
|
|
82
|
+
status: .done,
|
|
83
|
+
priority: .medium,
|
|
84
|
+
dueDate: calendar.date(byAdding: .day, value: -1, to: now),
|
|
85
|
+
projectID: inbox.id,
|
|
86
|
+
assigneeID: maya.id,
|
|
87
|
+
tags: ["ux", "copy"],
|
|
88
|
+
createdAt: calendar.date(byAdding: .day, value: -5, to: now) ?? now,
|
|
89
|
+
updatedAt: calendar.date(byAdding: .day, value: -1, to: now) ?? now,
|
|
90
|
+
attachment: nil
|
|
91
|
+
),
|
|
92
|
+
Task(
|
|
93
|
+
id: UUID(),
|
|
94
|
+
title: "Source new desk lamps",
|
|
95
|
+
description: "Collect options that fit the warm material palette for the studio.",
|
|
96
|
+
status: .todo,
|
|
97
|
+
priority: .low,
|
|
98
|
+
dueDate: calendar.date(byAdding: .day, value: 5, to: now),
|
|
99
|
+
projectID: studio.id,
|
|
100
|
+
assigneeID: nil,
|
|
101
|
+
tags: ["studio"],
|
|
102
|
+
createdAt: calendar.date(byAdding: .day, value: -3, to: now) ?? now,
|
|
103
|
+
updatedAt: now,
|
|
104
|
+
attachment: nil
|
|
105
|
+
)
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
selectedTaskID = tasks.first?.id
|
|
109
|
+
selectedProjectID = projects.first?.id
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var selectedTask: Task? {
|
|
113
|
+
tasks.first(where: { $0.id == selectedTaskID }) ?? filteredTasks.first
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
var selectedProject: Project? {
|
|
117
|
+
projects.first(where: { $0.id == selectedProjectID }) ?? projects.first
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
var filteredTasks: [Task] {
|
|
121
|
+
let filtered = tasks.filter { task in
|
|
122
|
+
let queryMatches: Bool
|
|
123
|
+
if searchQuery.isEmpty {
|
|
124
|
+
queryMatches = true
|
|
125
|
+
} else {
|
|
126
|
+
let haystack = [
|
|
127
|
+
task.title,
|
|
128
|
+
task.description ?? "",
|
|
129
|
+
project(for: task)?.name ?? "",
|
|
130
|
+
task.tags.joined(separator: " ")
|
|
131
|
+
].joined(separator: " ").localizedCaseInsensitiveContains(searchQuery)
|
|
132
|
+
queryMatches = haystack
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let filterMatches: Bool
|
|
136
|
+
switch homeFilter {
|
|
137
|
+
case .all:
|
|
138
|
+
filterMatches = true
|
|
139
|
+
case .today:
|
|
140
|
+
filterMatches = task.dueDate.map(calendar.isDateInToday) ?? false
|
|
141
|
+
case .upcoming:
|
|
142
|
+
filterMatches = task.status != .done && (task.dueDate.map { $0 > Date() } ?? false)
|
|
143
|
+
case .done:
|
|
144
|
+
filterMatches = task.status == .done
|
|
145
|
+
}
|
|
146
|
+
return queryMatches && filterMatches
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
switch sortOrder {
|
|
150
|
+
case .dueDate:
|
|
151
|
+
return filtered.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
|
|
152
|
+
case .priority:
|
|
153
|
+
return filtered.sorted { $0.priority.rank > $1.priority.rank }
|
|
154
|
+
case .createdAt:
|
|
155
|
+
return filtered.sorted { $0.createdAt > $1.createdAt }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func project(for task: Task) -> Project? {
|
|
160
|
+
guard let projectID = task.projectID else { return nil }
|
|
161
|
+
return projects.first(where: { $0.id == projectID })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func assignee(for task: Task) -> UserProfile? {
|
|
165
|
+
guard let assigneeID = task.assigneeID else { return nil }
|
|
166
|
+
return team.first(where: { $0.id == assigneeID })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func tasks(for project: Project) -> [Task] {
|
|
170
|
+
tasks.filter { $0.projectID == project.id }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func count(for filter: HomeFilter) -> Int {
|
|
174
|
+
switch filter {
|
|
175
|
+
case .all:
|
|
176
|
+
return tasks.count
|
|
177
|
+
case .today:
|
|
178
|
+
return tasks.filter { $0.dueDate.map(calendar.isDateInToday) ?? false }.count
|
|
179
|
+
case .upcoming:
|
|
180
|
+
return tasks.filter { $0.status != .done && ($0.dueDate.map { $0 > Date() } ?? false) }.count
|
|
181
|
+
case .done:
|
|
182
|
+
return tasks.filter { $0.status == .done }.count
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
func greeting() -> String {
|
|
187
|
+
let hour = calendar.component(.hour, from: Date())
|
|
188
|
+
let key = hour < 12 ? "home.greeting.morning" : (hour < 18 ? "home.greeting.afternoon" : "home.greeting.evening")
|
|
189
|
+
return String(format: localized(key), currentUser.firstName)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func todayCountText() -> String {
|
|
193
|
+
let count = count(for: .today)
|
|
194
|
+
if count == 0 { return localized("home.task_count.none") }
|
|
195
|
+
if count == 1 { return String(format: localized("home.task_count.one"), count) }
|
|
196
|
+
return String(format: localized("home.task_count.other"), count)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func statusLabel(_ status: TaskStatus) -> String {
|
|
200
|
+
localized("status.\(status.rawValue)")
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func priorityLabel(_ priority: TaskPriority) -> String {
|
|
204
|
+
localized("priority.\(priority.rawValue)")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func localized(_ key: String) -> String {
|
|
208
|
+
NSLocalizedString(key, comment: "")
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func toggle(task: Task) {
|
|
212
|
+
guard let index = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
|
213
|
+
tasks[index].status = task.status == .done ? .todo : .done
|
|
214
|
+
tasks[index].updatedAt = Date()
|
|
215
|
+
toastMessage = localized("task_detail.task_updated")
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func delete(task: Task) {
|
|
219
|
+
tasks.removeAll { $0.id == task.id }
|
|
220
|
+
if selectedTaskID == task.id {
|
|
221
|
+
selectedTaskID = filteredTasks.first?.id
|
|
222
|
+
}
|
|
223
|
+
toastMessage = localized("task_detail.task_deleted")
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func saveTask(_ draft: TaskDraft, editing taskID: UUID?) {
|
|
227
|
+
let tags = draft.tagsText
|
|
228
|
+
.split(separator: ",")
|
|
229
|
+
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
230
|
+
.filter { !$0.isEmpty }
|
|
231
|
+
let dueDate = draft.dueDateEnabled ? draft.dueDate : nil
|
|
232
|
+
let assignee = draft.assignToSelf ? currentUser.id : nil
|
|
233
|
+
|
|
234
|
+
if let taskID, let index = tasks.firstIndex(where: { $0.id == taskID }) {
|
|
235
|
+
tasks[index].title = draft.title
|
|
236
|
+
tasks[index].description = draft.description.isEmpty ? nil : draft.description
|
|
237
|
+
tasks[index].projectID = draft.projectID
|
|
238
|
+
tasks[index].priority = draft.priority
|
|
239
|
+
tasks[index].dueDate = dueDate
|
|
240
|
+
tasks[index].assigneeID = assignee
|
|
241
|
+
tasks[index].tags = tags
|
|
242
|
+
tasks[index].updatedAt = Date()
|
|
243
|
+
selectedTaskID = taskID
|
|
244
|
+
toastMessage = localized("edit_task.success")
|
|
245
|
+
} else {
|
|
246
|
+
let task = Task(
|
|
247
|
+
id: UUID(),
|
|
248
|
+
title: draft.title,
|
|
249
|
+
description: draft.description.isEmpty ? nil : draft.description,
|
|
250
|
+
status: .todo,
|
|
251
|
+
priority: draft.priority,
|
|
252
|
+
dueDate: dueDate,
|
|
253
|
+
projectID: draft.projectID,
|
|
254
|
+
assigneeID: assignee,
|
|
255
|
+
tags: tags,
|
|
256
|
+
createdAt: Date(),
|
|
257
|
+
updatedAt: Date(),
|
|
258
|
+
attachment: nil
|
|
259
|
+
)
|
|
260
|
+
tasks.insert(task, at: 0)
|
|
261
|
+
selectedTaskID = task.id
|
|
262
|
+
toastMessage = localized("create_task.success")
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
func makeDraft(task: Task?) -> TaskDraft {
|
|
267
|
+
guard let task else { return TaskDraft() }
|
|
268
|
+
return TaskDraft(
|
|
269
|
+
title: task.title,
|
|
270
|
+
description: task.description ?? "",
|
|
271
|
+
projectID: task.projectID,
|
|
272
|
+
priority: task.priority,
|
|
273
|
+
dueDateEnabled: task.dueDate != nil,
|
|
274
|
+
dueDate: task.dueDate ?? Date(),
|
|
275
|
+
tagsText: task.tags.joined(separator: ", "),
|
|
276
|
+
assignToSelf: task.assigneeID == currentUser.id
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
func createProject(_ draft: ProjectDraft) {
|
|
281
|
+
let project = Project(
|
|
282
|
+
id: UUID(),
|
|
283
|
+
name: draft.name,
|
|
284
|
+
colorHex: draft.colorHex,
|
|
285
|
+
icon: "folder"
|
|
286
|
+
)
|
|
287
|
+
projects.append(project)
|
|
288
|
+
selectedProjectID = project.id
|
|
289
|
+
toastMessage = localized("projects.created")
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
func updateProfile(name: String, email: String) {
|
|
293
|
+
currentUser.name = name
|
|
294
|
+
currentUser.firstName = name.split(separator: " ").first.map(String.init) ?? name
|
|
295
|
+
currentUser.email = email
|
|
296
|
+
toastMessage = localized("profile.success")
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
func assignTask(taskID: UUID, userID: UUID) {
|
|
300
|
+
guard let index = tasks.firstIndex(where: { $0.id == taskID }) else { return }
|
|
301
|
+
tasks[index].assigneeID = userID
|
|
302
|
+
tasks[index].updatedAt = Date()
|
|
303
|
+
toastMessage = localized("task_detail.task_updated")
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
func exportData() {
|
|
307
|
+
toastMessage = localized("settings.export_success")
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private extension TaskPriority {
|
|
312
|
+
var rank: Int {
|
|
313
|
+
switch self {
|
|
314
|
+
case .low: 1
|
|
315
|
+
case .medium: 2
|
|
316
|
+
case .high: 3
|
|
317
|
+
case .urgent: 4
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
extension Color {
|
|
4
|
+
init(hex: String) {
|
|
5
|
+
let value = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
|
6
|
+
var int: UInt64 = 0
|
|
7
|
+
Scanner(string: value).scanHexInt64(&int)
|
|
8
|
+
let a, r, g, b: UInt64
|
|
9
|
+
switch value.count {
|
|
10
|
+
case 3:
|
|
11
|
+
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
|
12
|
+
case 6:
|
|
13
|
+
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
|
14
|
+
case 8:
|
|
15
|
+
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
|
16
|
+
default:
|
|
17
|
+
(a, r, g, b) = (255, 91, 79, 232)
|
|
18
|
+
}
|
|
19
|
+
self.init(
|
|
20
|
+
.sRGB,
|
|
21
|
+
red: Double(r) / 255,
|
|
22
|
+
green: Double(g) / 255,
|
|
23
|
+
blue: Double(b) / 255,
|
|
24
|
+
opacity: Double(a) / 255
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
enum AppPalette {
|
|
30
|
+
static let brandPrimary = Color(hex: "#5B4FE8")
|
|
31
|
+
static let brandSecondary = Color(hex: "#E8634F")
|
|
32
|
+
static let surfaceSecondary = Color(hex: "#F7F6F3")
|
|
33
|
+
static let surfaceTertiary = Color(hex: "#EFEEE8")
|
|
34
|
+
static let textSecondary = Color(hex: "#6B6966")
|
|
35
|
+
static let textTertiary = Color(hex: "#9C9A94")
|
|
36
|
+
static let border = Color(hex: "#E5E3DE")
|
|
37
|
+
static let success = Color(hex: "#2D9D5E")
|
|
38
|
+
static let warning = Color(hex: "#D4920E")
|
|
39
|
+
static let danger = Color(hex: "#D43B3B")
|
|
40
|
+
static let info = Color(hex: "#3B82D4")
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: TaskFlow
|
|
2
|
+
options:
|
|
3
|
+
bundleIdPrefix: uz.rsteam
|
|
4
|
+
deploymentTarget:
|
|
5
|
+
iOS: "17.0"
|
|
6
|
+
settings:
|
|
7
|
+
base:
|
|
8
|
+
PRODUCT_NAME: TaskFlow
|
|
9
|
+
targets:
|
|
10
|
+
TaskFlow:
|
|
11
|
+
type: application
|
|
12
|
+
platform: iOS
|
|
13
|
+
deploymentTarget: "17.0"
|
|
14
|
+
sources:
|
|
15
|
+
- path: Sources/TaskFlow
|
|
16
|
+
- path: Resources
|
|
17
|
+
buildPhase: resources
|
|
18
|
+
settings:
|
|
19
|
+
base:
|
|
20
|
+
GENERATE_INFOPLIST_FILE: YES
|
|
21
|
+
INFOPLIST_KEY_CFBundleDisplayName: TaskFlow
|
|
22
|
+
INFOPLIST_KEY_UILaunchScreen_Generation: true
|
|
23
|
+
INFOPLIST_KEY_UIApplicationSceneManifest_Generation: true
|
|
24
|
+
INFOPLIST_KEY_UIUserInterfaceStyle: Automatic
|
|
25
|
+
SWIFT_VERSION: 5.10
|
|
26
|
+
PRODUCT_BUNDLE_IDENTIFIER: uz.rsteam.taskflow
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# TaskFlow Web Target
|
|
2
|
+
|
|
3
|
+
This web target was generated from the local `openuispec/` source in this example only.
|
|
4
|
+
|
|
5
|
+
Included:
|
|
6
|
+
- React + TypeScript + Vite scaffold
|
|
7
|
+
- Adaptive navigation derived from TaskFlow size classes
|
|
8
|
+
- Spec-derived screens: home, task detail, projects, project detail, calendar, settings, profile edit
|
|
9
|
+
- Spec-derived create/edit task and new-project modal flows
|
|
10
|
+
- Local sample state matching the manifest data model
|
|
11
|
+
- English locale values derived from `openuispec/locales/en.json`
|
|
12
|
+
- Token-driven styling based on the TaskFlow color, spacing, typography, and theme specs
|
|
13
|
+
|
|
14
|
+
Run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>TaskFlow</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|