openuispec 0.1.25 → 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.
Files changed (139) hide show
  1. package/README.md +63 -18
  2. package/cli/index.ts +21 -3
  3. package/cli/init.ts +27 -11
  4. package/docs/implementation-notes.md +119 -0
  5. package/docs/release-notes-v0.1.26.md +64 -0
  6. package/docs/release-notes-v0.1.27.md +28 -0
  7. package/docs/release-notes-v0.1.28.md +25 -0
  8. package/docs/stress-test-maturity-report.md +1 -1
  9. package/drift/index.ts +396 -22
  10. package/examples/taskflow/AGENTS.md +112 -0
  11. package/examples/taskflow/CLAUDE.md +112 -0
  12. package/examples/taskflow/generated/android/TaskFlow/README.md +43 -0
  13. package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +76 -0
  14. package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +1 -0
  15. package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +21 -0
  16. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +19 -0
  17. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +283 -0
  18. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +106 -0
  19. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +57 -0
  20. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +109 -0
  21. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +112 -0
  22. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +61 -0
  23. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +82 -0
  24. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +111 -0
  25. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +77 -0
  26. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +30 -0
  27. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +86 -0
  28. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +57 -0
  29. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +155 -0
  30. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +4 -0
  31. package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +5 -0
  32. package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +12 -0
  33. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
  34. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +7 -0
  35. package/examples/taskflow/generated/android/TaskFlow/gradle.properties +4 -0
  36. package/examples/taskflow/generated/android/TaskFlow/gradlew +18 -0
  37. package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +12 -0
  38. package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +18 -0
  39. package/examples/taskflow/generated/ios/TaskFlow/README.md +21 -0
  40. package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +115 -0
  41. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +24 -0
  42. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +150 -0
  43. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +220 -0
  44. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +122 -0
  45. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +21 -0
  46. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +201 -0
  47. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +48 -0
  48. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +59 -0
  49. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +63 -0
  50. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +85 -0
  51. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +219 -0
  52. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +320 -0
  53. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +41 -0
  54. package/examples/taskflow/generated/ios/TaskFlow/project.yml +26 -0
  55. package/examples/taskflow/generated/web/TaskFlow/README.md +19 -0
  56. package/examples/taskflow/generated/web/TaskFlow/index.html +12 -0
  57. package/examples/taskflow/generated/web/TaskFlow/package-lock.json +1908 -0
  58. package/examples/taskflow/generated/web/TaskFlow/package.json +24 -0
  59. package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +58 -0
  60. package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +55 -0
  61. package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +82 -0
  62. package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +191 -0
  63. package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +41 -0
  64. package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +131 -0
  65. package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +25 -0
  66. package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +39 -0
  67. package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +111 -0
  68. package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +13 -0
  69. package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +111 -0
  70. package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +82 -0
  71. package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +132 -0
  72. package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +105 -0
  73. package/examples/taskflow/generated/web/TaskFlow/src/store.ts +216 -0
  74. package/examples/taskflow/generated/web/TaskFlow/src/styles.css +617 -0
  75. package/examples/taskflow/generated/web/TaskFlow/src/types.ts +64 -0
  76. package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +78 -0
  77. package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +21 -0
  78. package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +6 -0
  79. package/examples/taskflow/openuispec/README.md +49 -0
  80. package/examples/todo-orbit/AGENTS.md +46 -14
  81. package/examples/todo-orbit/CLAUDE.md +46 -14
  82. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +69 -18
  83. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +5 -0
  84. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +5 -2
  85. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +1 -0
  86. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +1 -0
  87. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +1 -0
  88. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +1 -0
  89. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +3 -0
  90. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +1 -0
  91. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +2 -0
  92. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +1 -0
  93. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +1 -1
  94. package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +59 -6
  95. package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +40 -0
  96. package/examples/todo-orbit/openuispec/README.md +24 -131
  97. package/examples/todo-orbit/openuispec/flows/create_recurring_rule.yaml +3 -0
  98. package/examples/todo-orbit/openuispec/flows/create_task.yaml +1 -0
  99. package/examples/todo-orbit/openuispec/flows/edit_task.yaml +1 -0
  100. package/examples/todo-orbit/openuispec/locales/en.json +1 -0
  101. package/examples/todo-orbit/openuispec/locales/ru.json +1 -0
  102. package/examples/todo-orbit/openuispec/screens/task_detail.yaml +1 -0
  103. package/examples/todo-orbit/openuispec/tokens/icons.yaml +6 -0
  104. package/package.json +6 -1
  105. package/prepare/index.ts +391 -0
  106. package/schema/semantic-lint.ts +592 -0
  107. package/schema/validate.ts +17 -13
  108. package/status/index.ts +200 -0
  109. /package/examples/taskflow/{contracts → openuispec/contracts}/README.md +0 -0
  110. /package/examples/taskflow/{contracts → openuispec/contracts}/action_trigger.yaml +0 -0
  111. /package/examples/taskflow/{contracts → openuispec/contracts}/collection.yaml +0 -0
  112. /package/examples/taskflow/{contracts → openuispec/contracts}/data_display.yaml +0 -0
  113. /package/examples/taskflow/{contracts → openuispec/contracts}/feedback.yaml +0 -0
  114. /package/examples/taskflow/{contracts → openuispec/contracts}/input_field.yaml +0 -0
  115. /package/examples/taskflow/{contracts → openuispec/contracts}/nav_container.yaml +0 -0
  116. /package/examples/taskflow/{contracts → openuispec/contracts}/surface.yaml +0 -0
  117. /package/examples/taskflow/{contracts → openuispec/contracts}/x_media_player.yaml +0 -0
  118. /package/examples/taskflow/{flows → openuispec/flows}/create_task.yaml +0 -0
  119. /package/examples/taskflow/{flows → openuispec/flows}/edit_task.yaml +0 -0
  120. /package/examples/taskflow/{locales → openuispec/locales}/en.json +0 -0
  121. /package/examples/taskflow/{openuispec.yaml → openuispec/openuispec.yaml} +0 -0
  122. /package/examples/taskflow/{platform → openuispec/platform}/android.yaml +0 -0
  123. /package/examples/taskflow/{platform → openuispec/platform}/ios.yaml +0 -0
  124. /package/examples/taskflow/{platform → openuispec/platform}/web.yaml +0 -0
  125. /package/examples/taskflow/{screens → openuispec/screens}/calendar.yaml +0 -0
  126. /package/examples/taskflow/{screens → openuispec/screens}/home.yaml +0 -0
  127. /package/examples/taskflow/{screens → openuispec/screens}/profile_edit.yaml +0 -0
  128. /package/examples/taskflow/{screens → openuispec/screens}/project_detail.yaml +0 -0
  129. /package/examples/taskflow/{screens → openuispec/screens}/projects.yaml +0 -0
  130. /package/examples/taskflow/{screens → openuispec/screens}/settings.yaml +0 -0
  131. /package/examples/taskflow/{screens → openuispec/screens}/task_detail.yaml +0 -0
  132. /package/examples/taskflow/{tokens → openuispec/tokens}/color.yaml +0 -0
  133. /package/examples/taskflow/{tokens → openuispec/tokens}/elevation.yaml +0 -0
  134. /package/examples/taskflow/{tokens → openuispec/tokens}/icons.yaml +0 -0
  135. /package/examples/taskflow/{tokens → openuispec/tokens}/layout.yaml +0 -0
  136. /package/examples/taskflow/{tokens → openuispec/tokens}/motion.yaml +0 -0
  137. /package/examples/taskflow/{tokens → openuispec/tokens}/spacing.yaml +0 -0
  138. /package/examples/taskflow/{tokens → openuispec/tokens}/themes.yaml +0 -0
  139. /package/examples/taskflow/{tokens → openuispec/tokens}/typography.yaml +0 -0
@@ -0,0 +1,201 @@
1
+ import SwiftUI
2
+
3
+ struct HomeView: View {
4
+ @Bindable var model: AppModel
5
+
6
+ var body: some View {
7
+ GeometryReader { geometry in
8
+ let expanded = geometry.size.width >= 900
9
+ ScrollView {
10
+ VStack(alignment: .leading, spacing: 24) {
11
+ header
12
+ searchField
13
+ filterRow
14
+ if expanded {
15
+ HStack(alignment: .top, spacing: 24) {
16
+ taskList(expanded: true)
17
+ .frame(maxWidth: .infinity)
18
+ detailPanel
19
+ .frame(maxWidth: 420)
20
+ }
21
+ } else {
22
+ taskList(expanded: false)
23
+ }
24
+ }
25
+ .padding(24)
26
+ }
27
+ .background(Color(.systemGroupedBackground))
28
+ .navigationTitle(model.localized("nav.tasks"))
29
+ .toolbar {
30
+ ToolbarItem(placement: .topBarTrailing) {
31
+ Button {
32
+ model.presentedSheet = .createTask
33
+ } label: {
34
+ Label(model.localized("home.new_task"), systemImage: "plus")
35
+ }
36
+ .buttonStyle(.borderedProminent)
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ private var header: some View {
43
+ VStack(alignment: .leading, spacing: 6) {
44
+ Text(model.greeting())
45
+ .font(.system(size: 30, weight: .bold, design: .rounded))
46
+ Text(model.todayCountText())
47
+ .font(.body)
48
+ .foregroundStyle(AppPalette.textSecondary)
49
+ }
50
+ }
51
+
52
+ private var searchField: some View {
53
+ VStack(alignment: .leading, spacing: 8) {
54
+ Text(model.localized("home.search_label"))
55
+ .font(.caption.weight(.semibold))
56
+ .foregroundStyle(AppPalette.textSecondary)
57
+ TextField(model.localized("home.search_placeholder"), text: $model.searchQuery)
58
+ .textFieldStyle(.roundedBorder)
59
+ }
60
+ }
61
+
62
+ private var filterRow: some View {
63
+ ScrollView(.horizontal, showsIndicators: false) {
64
+ HStack(spacing: 10) {
65
+ filterChip(.all)
66
+ filterChip(.today)
67
+ filterChip(.upcoming)
68
+ filterChip(.done)
69
+ Picker("Sort", selection: $model.sortOrder) {
70
+ Text("Due").tag(SortOrder.dueDate)
71
+ Text("Priority").tag(SortOrder.priority)
72
+ Text("Created").tag(SortOrder.createdAt)
73
+ }
74
+ .pickerStyle(.menu)
75
+ }
76
+ }
77
+ }
78
+
79
+ private func filterChip(_ filter: HomeFilter) -> some View {
80
+ let selected = model.homeFilter == filter
81
+ let title: String
82
+ switch filter {
83
+ case .all: title = model.localized("home.filter.all")
84
+ case .today: title = model.localized("home.filter.today")
85
+ case .upcoming: title = model.localized("home.filter.upcoming")
86
+ case .done: title = model.localized("home.filter.done")
87
+ }
88
+ return Button {
89
+ model.homeFilter = filter
90
+ } label: {
91
+ Text("\(title) (\(model.count(for: filter)))")
92
+ .font(.subheadline.weight(.semibold))
93
+ .padding(.horizontal, 12)
94
+ .padding(.vertical, 8)
95
+ .background(selected ? AppPalette.brandPrimary : AppPalette.surfaceSecondary)
96
+ .foregroundStyle(selected ? .white : .primary)
97
+ .clipShape(Capsule())
98
+ }
99
+ .buttonStyle(.plain)
100
+ }
101
+
102
+ private func taskList(expanded: Bool) -> some View {
103
+ VStack(alignment: .leading, spacing: 14) {
104
+ if model.filteredTasks.isEmpty {
105
+ ContentUnavailableView(
106
+ model.localized("home.empty_title"),
107
+ systemImage: "checkmark.circle.fill",
108
+ description: Text(model.localized("home.empty_body"))
109
+ )
110
+ .frame(maxWidth: .infinity)
111
+ .padding(.top, 40)
112
+ } else {
113
+ ForEach(model.filteredTasks) { task in
114
+ if expanded {
115
+ taskRow(task)
116
+ } else {
117
+ NavigationLink {
118
+ TaskDetailView(model: model, taskID: task.id)
119
+ } label: {
120
+ taskCard(task)
121
+ }
122
+ .buttonStyle(.plain)
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ private func taskRow(_ task: Task) -> some View {
130
+ Button {
131
+ model.selectedTaskID = task.id
132
+ } label: {
133
+ taskCard(task)
134
+ .overlay {
135
+ RoundedRectangle(cornerRadius: 24)
136
+ .stroke(model.selectedTaskID == task.id ? AppPalette.brandPrimary : .clear, lineWidth: 2)
137
+ }
138
+ }
139
+ .buttonStyle(.plain)
140
+ }
141
+
142
+ private func taskCard(_ task: Task) -> some View {
143
+ HStack(alignment: .top, spacing: 14) {
144
+ Image(systemName: task.status == .done ? "checkmark.circle.fill" : "circle")
145
+ .font(.title3)
146
+ .foregroundStyle(task.status == .done ? AppPalette.success : AppPalette.textTertiary)
147
+
148
+ VStack(alignment: .leading, spacing: 6) {
149
+ Text(task.title)
150
+ .font(.headline)
151
+ Text([model.project(for: task)?.name, relativeDate(task.dueDate)].compactMap { $0 }.joined(separator: " · "))
152
+ .font(.subheadline)
153
+ .foregroundStyle(AppPalette.textSecondary)
154
+ if !task.tags.isEmpty {
155
+ HStack(spacing: 6) {
156
+ ForEach(task.tags, id: \.self) { tag in
157
+ Text(tag)
158
+ .font(.caption.weight(.medium))
159
+ .padding(.horizontal, 8)
160
+ .padding(.vertical, 4)
161
+ .background(AppPalette.surfaceSecondary)
162
+ .clipShape(Capsule())
163
+ }
164
+ }
165
+ }
166
+ }
167
+ Spacer()
168
+ Circle()
169
+ .fill(priorityColor(task.priority))
170
+ .frame(width: 10, height: 10)
171
+ }
172
+ .padding(18)
173
+ .background(.background)
174
+ .clipShape(RoundedRectangle(cornerRadius: 24))
175
+ .shadow(color: .black.opacity(0.05), radius: 12, y: 4)
176
+ }
177
+
178
+ private var detailPanel: some View {
179
+ Group {
180
+ if let selectedTask = model.selectedTask {
181
+ TaskDetailPanel(model: model, taskID: selectedTask.id)
182
+ } else {
183
+ ContentUnavailableView("Select a task", systemImage: "sidebar.right", description: Text("Choose a task to inspect its details."))
184
+ }
185
+ }
186
+ }
187
+
188
+ private func relativeDate(_ date: Date?) -> String? {
189
+ guard let date else { return nil }
190
+ return date.formatted(.relative(presentation: .named))
191
+ }
192
+
193
+ private func priorityColor(_ priority: TaskPriority) -> Color {
194
+ switch priority {
195
+ case .low: Color(hex: "#9CA3AF")
196
+ case .medium: AppPalette.info
197
+ case .high: AppPalette.warning
198
+ case .urgent: AppPalette.danger
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,48 @@
1
+ import SwiftUI
2
+
3
+ struct ProfileEditView: View {
4
+ @Bindable var model: AppModel
5
+ @Environment(\.dismiss) private var dismiss
6
+ @State private var name = ""
7
+ @State private var email = ""
8
+
9
+ var body: some View {
10
+ Form {
11
+ Section {
12
+ VStack(spacing: 12) {
13
+ Image(systemName: model.currentUser.avatarSymbol ?? "person.crop.circle.fill")
14
+ .font(.system(size: 54))
15
+ .foregroundStyle(AppPalette.brandPrimary)
16
+ Text(model.currentUser.name)
17
+ .font(.headline)
18
+ Text(model.currentUser.email)
19
+ .foregroundStyle(AppPalette.textSecondary)
20
+ Button(model.localized("profile.change_photo")) {}
21
+ }
22
+ .frame(maxWidth: .infinity)
23
+ .padding(.vertical, 12)
24
+ }
25
+
26
+ Section {
27
+ TextField(model.localized("profile.field_name"), text: $name)
28
+ TextField(model.localized("profile.field_email"), text: $email)
29
+ .textInputAutocapitalization(.never)
30
+ .keyboardType(.emailAddress)
31
+ }
32
+
33
+ Section {
34
+ Button(model.localized("profile.save")) {
35
+ model.updateProfile(name: name, email: email)
36
+ dismiss()
37
+ }
38
+ .buttonStyle(.borderedProminent)
39
+ .frame(maxWidth: .infinity, alignment: .center)
40
+ }
41
+ }
42
+ .navigationTitle(model.localized("profile.save"))
43
+ .onAppear {
44
+ name = model.currentUser.name
45
+ email = model.currentUser.email
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,59 @@
1
+ import SwiftUI
2
+
3
+ struct ProjectDetailView: View {
4
+ @Bindable var model: AppModel
5
+ let projectID: UUID
6
+
7
+ private var project: Project? {
8
+ model.projects.first(where: { $0.id == projectID })
9
+ }
10
+
11
+ var body: some View {
12
+ Group {
13
+ if let project {
14
+ List {
15
+ Section {
16
+ VStack(alignment: .leading, spacing: 12) {
17
+ HStack {
18
+ Image(systemName: project.icon)
19
+ .foregroundStyle(Color(hex: project.colorHex))
20
+ Text(project.name)
21
+ .font(.largeTitle.weight(.bold))
22
+ }
23
+ Text("\(model.tasks(for: project).count) tasks")
24
+ .foregroundStyle(AppPalette.textSecondary)
25
+ }
26
+ .padding(.vertical, 8)
27
+ }
28
+
29
+ Section {
30
+ if model.tasks(for: project).isEmpty {
31
+ ContentUnavailableView(
32
+ model.localized("project_detail.empty_title"),
33
+ systemImage: "checkmark.circle.fill",
34
+ description: Text(model.localized("project_detail.empty_body"))
35
+ )
36
+ .padding(.vertical, 24)
37
+ } else {
38
+ ForEach(model.tasks(for: project)) { task in
39
+ NavigationLink {
40
+ TaskDetailView(model: model, taskID: task.id)
41
+ } label: {
42
+ VStack(alignment: .leading, spacing: 4) {
43
+ Text(task.title)
44
+ Text(task.dueDate?.formatted(.relative(presentation: .named)) ?? "No due date")
45
+ .font(.subheadline)
46
+ .foregroundStyle(AppPalette.textSecondary)
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ .navigationTitle(project.name)
54
+ } else {
55
+ ContentUnavailableView("Project missing", systemImage: "folder.badge.questionmark")
56
+ }
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,63 @@
1
+ import SwiftUI
2
+
3
+ struct ProjectsView: View {
4
+ @Bindable var model: AppModel
5
+
6
+ private let gridColumns = [
7
+ GridItem(.adaptive(minimum: 220), spacing: 16)
8
+ ]
9
+
10
+ var body: some View {
11
+ ScrollView {
12
+ LazyVGrid(columns: gridColumns, spacing: 16) {
13
+ ForEach(model.projects) { project in
14
+ NavigationLink {
15
+ ProjectDetailView(model: model, projectID: project.id)
16
+ } label: {
17
+ ProjectCard(project: project, taskCount: model.tasks(for: project).count)
18
+ }
19
+ .buttonStyle(.plain)
20
+ }
21
+ }
22
+ .padding(24)
23
+ }
24
+ .background(Color(.systemGroupedBackground))
25
+ .navigationTitle(model.localized("projects.title"))
26
+ .toolbar {
27
+ ToolbarItem(placement: .topBarTrailing) {
28
+ Button {
29
+ model.presentedSheet = .newProject
30
+ } label: {
31
+ Label(model.localized("projects.new_project"), systemImage: "plus.circle")
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ private struct ProjectCard: View {
39
+ let project: Project
40
+ let taskCount: Int
41
+
42
+ var body: some View {
43
+ VStack(alignment: .leading, spacing: 14) {
44
+ Image(systemName: project.icon)
45
+ .font(.title2.weight(.semibold))
46
+ .foregroundStyle(Color(hex: project.colorHex))
47
+ .frame(width: 48, height: 48)
48
+ .background(Color(hex: project.colorHex).opacity(0.12))
49
+ .clipShape(RoundedRectangle(cornerRadius: 16))
50
+ Text(project.name)
51
+ .font(.headline)
52
+ .multilineTextAlignment(.leading)
53
+ Text(taskCount == 1 ? "1 task" : "\(taskCount) tasks")
54
+ .font(.subheadline)
55
+ .foregroundStyle(AppPalette.textSecondary)
56
+ }
57
+ .frame(maxWidth: .infinity, alignment: .leading)
58
+ .padding(20)
59
+ .background(.background)
60
+ .clipShape(RoundedRectangle(cornerRadius: 24))
61
+ .shadow(color: .black.opacity(0.05), radius: 12, y: 4)
62
+ }
63
+ }
@@ -0,0 +1,85 @@
1
+ import SwiftUI
2
+
3
+ struct SettingsView: View {
4
+ @Bindable var model: AppModel
5
+ @State private var showDeleteAlert = false
6
+
7
+ var body: some View {
8
+ List {
9
+ Section {
10
+ NavigationLink {
11
+ ProfileEditView(model: model)
12
+ } label: {
13
+ HStack(spacing: 12) {
14
+ Image(systemName: model.currentUser.avatarSymbol ?? "person.crop.circle.fill")
15
+ .font(.largeTitle)
16
+ .foregroundStyle(AppPalette.brandPrimary)
17
+ VStack(alignment: .leading) {
18
+ Text(model.currentUser.name)
19
+ .font(.headline)
20
+ Text(model.currentUser.email)
21
+ .font(.subheadline)
22
+ .foregroundStyle(AppPalette.textSecondary)
23
+ }
24
+ }
25
+ .padding(.vertical, 4)
26
+ }
27
+ }
28
+
29
+ Section {
30
+ Picker(model.localized("settings.theme"), selection: $model.preferences.theme) {
31
+ Text(model.localized("settings.theme_system")).tag(ThemePreference.system)
32
+ Text(model.localized("settings.theme_light")).tag(ThemePreference.light)
33
+ Text(model.localized("settings.theme_dark")).tag(ThemePreference.dark)
34
+ Text(model.localized("settings.theme_warm")).tag(ThemePreference.warm)
35
+ }
36
+
37
+ Picker(model.localized("settings.default_priority"), selection: $model.preferences.defaultPriority) {
38
+ Text(model.localized("priority.low")).tag(TaskPriority.low)
39
+ Text(model.localized("priority.medium")).tag(TaskPriority.medium)
40
+ Text(model.localized("priority.high")).tag(TaskPriority.high)
41
+ Text(model.localized("priority.urgent")).tag(TaskPriority.urgent)
42
+ }
43
+
44
+ Toggle(model.localized("settings.notifications"), isOn: $model.preferences.notificationsEnabled)
45
+ Toggle(model.localized("settings.reminders"), isOn: $model.preferences.remindersEnabled)
46
+ } header: {
47
+ Text(model.localized("settings.preferences"))
48
+ } footer: {
49
+ Text(model.localized("settings.reminders_helper"))
50
+ }
51
+
52
+ Section {
53
+ Button(model.localized("settings.export")) {
54
+ model.exportData()
55
+ }
56
+
57
+ Button(model.localized("settings.delete_account"), role: .destructive) {
58
+ showDeleteAlert = true
59
+ }
60
+ } header: {
61
+ Text(model.localized("settings.data"))
62
+ }
63
+
64
+ Section {
65
+ VStack(spacing: 4) {
66
+ Text(model.localized("settings.app_version"))
67
+ Text(model.localized("settings.app_credit"))
68
+ }
69
+ .font(.caption)
70
+ .foregroundStyle(AppPalette.textSecondary)
71
+ .frame(maxWidth: .infinity)
72
+ }
73
+ .listRowBackground(Color.clear)
74
+ }
75
+ .navigationTitle(model.localized("nav.settings"))
76
+ .alert(model.localized("settings.delete_title"), isPresented: $showDeleteAlert) {
77
+ Button(model.localized("common.cancel"), role: .cancel) {}
78
+ Button(model.localized("settings.delete_confirm"), role: .destructive) {
79
+ model.toastMessage = model.localized("settings.delete_title")
80
+ }
81
+ } message: {
82
+ Text(model.localized("settings.delete_message"))
83
+ }
84
+ }
85
+ }
@@ -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
+ }