openuispec 0.1.18 → 0.1.20

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 (98) hide show
  1. package/README.md +52 -34
  2. package/cli/index.ts +1 -1
  3. package/cli/init.ts +48 -211
  4. package/docs/stress-test-maturity-report.md +97 -0
  5. package/examples/todo-orbit/AGENTS.md +127 -0
  6. package/examples/todo-orbit/CLAUDE.md +75 -0
  7. package/examples/todo-orbit/README.md +62 -0
  8. package/examples/todo-orbit/generated/android/Todo Orbit/README.md +14 -0
  9. package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +58 -0
  10. package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +1 -0
  11. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +20 -0
  12. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +14 -0
  13. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +345 -0
  14. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +231 -0
  15. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +169 -0
  16. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +8 -0
  17. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +185 -0
  18. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +193 -0
  19. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +102 -0
  20. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +342 -0
  21. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +344 -0
  22. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +59 -0
  23. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +148 -0
  24. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +154 -0
  25. package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +4 -0
  26. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
  27. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +7 -0
  28. package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +4 -0
  29. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +248 -0
  30. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +93 -0
  31. package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +18 -0
  32. package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +29 -0
  33. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +118 -0
  34. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +118 -0
  35. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +50 -0
  36. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +204 -0
  37. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +126 -0
  38. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +70 -0
  39. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +123 -0
  40. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +60 -0
  41. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +238 -0
  42. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +94 -0
  43. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +74 -0
  44. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +363 -0
  45. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +324 -0
  46. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +408 -0
  47. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  48. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +79 -0
  49. package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +24 -0
  50. package/examples/todo-orbit/generated/web/Todo Orbit/index.html +16 -0
  51. package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +1087 -0
  52. package/examples/todo-orbit/generated/web/Todo Orbit/package.json +24 -0
  53. package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +2114 -0
  54. package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +13 -0
  55. package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +886 -0
  56. package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +19 -0
  57. package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +6 -0
  58. package/examples/todo-orbit/openuispec/README.md +158 -0
  59. package/examples/todo-orbit/openuispec/contracts/.gitkeep +0 -0
  60. package/examples/todo-orbit/openuispec/contracts/action_trigger.yaml +28 -0
  61. package/examples/todo-orbit/openuispec/contracts/collection.yaml +32 -0
  62. package/examples/todo-orbit/openuispec/contracts/data_display.yaml +38 -0
  63. package/examples/todo-orbit/openuispec/contracts/feedback.yaml +32 -0
  64. package/examples/todo-orbit/openuispec/contracts/input_field.yaml +52 -0
  65. package/examples/todo-orbit/openuispec/contracts/nav_container.yaml +47 -0
  66. package/examples/todo-orbit/openuispec/contracts/surface.yaml +28 -0
  67. package/examples/todo-orbit/openuispec/contracts/x_schedule_preview.yaml +134 -0
  68. package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +139 -0
  69. package/examples/todo-orbit/openuispec/flows/.gitkeep +0 -0
  70. package/examples/todo-orbit/openuispec/flows/create_recurring_rule.yaml +253 -0
  71. package/examples/todo-orbit/openuispec/flows/create_task.yaml +118 -0
  72. package/examples/todo-orbit/openuispec/flows/edit_task.yaml +126 -0
  73. package/examples/todo-orbit/openuispec/locales/.gitkeep +0 -0
  74. package/examples/todo-orbit/openuispec/locales/en.json +150 -0
  75. package/examples/todo-orbit/openuispec/locales/ru.json +150 -0
  76. package/examples/todo-orbit/openuispec/openuispec.yaml +122 -0
  77. package/examples/todo-orbit/openuispec/platform/.gitkeep +0 -0
  78. package/examples/todo-orbit/openuispec/platform/android.yaml +19 -0
  79. package/examples/todo-orbit/openuispec/platform/ios.yaml +20 -0
  80. package/examples/todo-orbit/openuispec/platform/web.yaml +22 -0
  81. package/examples/todo-orbit/openuispec/screens/.gitkeep +0 -0
  82. package/examples/todo-orbit/openuispec/screens/analytics.yaml +140 -0
  83. package/examples/todo-orbit/openuispec/screens/home.yaml +173 -0
  84. package/examples/todo-orbit/openuispec/screens/settings.yaml +149 -0
  85. package/examples/todo-orbit/openuispec/screens/task_detail.yaml +223 -0
  86. package/examples/todo-orbit/openuispec/tokens/.gitkeep +0 -0
  87. package/examples/todo-orbit/openuispec/tokens/color.yaml +93 -0
  88. package/examples/todo-orbit/openuispec/tokens/elevation.yaml +25 -0
  89. package/examples/todo-orbit/openuispec/tokens/icons.yaml +92 -0
  90. package/examples/todo-orbit/openuispec/tokens/layout.yaml +107 -0
  91. package/examples/todo-orbit/openuispec/tokens/motion.yaml +39 -0
  92. package/examples/todo-orbit/openuispec/tokens/spacing.yaml +18 -0
  93. package/examples/todo-orbit/openuispec/tokens/themes.yaml +23 -0
  94. package/examples/todo-orbit/openuispec/tokens/typography.yaml +52 -0
  95. package/package.json +1 -1
  96. package/schema/screen.schema.json +9 -0
  97. package/schema/validate.ts +0 -2
  98. package/spec/openuispec-v0.1.md +129 -27
@@ -0,0 +1,94 @@
1
+ import SwiftUI
2
+
3
+ struct AnalyticsView: View {
4
+ @ObservedObject var model: AppModel
5
+ @State private var period: AnalyticsPeriod = .week
6
+
7
+ private let grid = [
8
+ GridItem(.flexible(), spacing: 12),
9
+ GridItem(.flexible(), spacing: 12)
10
+ ]
11
+
12
+ var body: some View {
13
+ let snapshot = model.analyticsSnapshot()
14
+ let points = model.trendSeries(period: period)
15
+ let overdue = model.overdueTasks()
16
+
17
+ ScrollView {
18
+ VStack(alignment: .leading, spacing: 18) {
19
+ VStack(alignment: .leading, spacing: 6) {
20
+ Text(model.string("analytics.title"))
21
+ .font(.largeTitle.weight(.bold))
22
+ Text(model.string("analytics.subtitle"))
23
+ .foregroundStyle(.secondary)
24
+ }
25
+
26
+ Picker("Period", selection: $period) {
27
+ ForEach(AnalyticsPeriod.allCases) { period in
28
+ Text(model.string("analytics.period_\(period.rawValue)")).tag(period)
29
+ }
30
+ }
31
+ .pickerStyle(.segmented)
32
+
33
+ LazyVGrid(columns: grid, spacing: 12) {
34
+ statCard(model.string("analytics.completed_today"), value: "\(snapshot.completedToday)")
35
+ statCard(model.string("analytics.open_tasks"), value: "\(snapshot.openTasks)")
36
+ statCard(model.string("analytics.overdue_tasks"), value: "\(snapshot.overdueTasks)")
37
+ statCard(model.string("analytics.completion_rate"), value: "\(snapshot.completionRate)%")
38
+ }
39
+
40
+ TrendChartView(
41
+ title: model.string("analytics.title"),
42
+ subtitle: model.string("analytics.trend_subtitle"),
43
+ emptyMessage: model.string("analytics.empty_trend"),
44
+ legendCompleted: model.string("analytics.legend_completed"),
45
+ legendCreated: model.string("analytics.legend_created"),
46
+ points: points
47
+ )
48
+
49
+ VStack(alignment: .leading, spacing: 14) {
50
+ Text(model.string("analytics.overdue_section"))
51
+ .font(.title3.weight(.semibold))
52
+ Text(model.string("analytics.overdue_subtitle"))
53
+ .foregroundStyle(.secondary)
54
+
55
+ if overdue.isEmpty {
56
+ Text(model.string("analytics.empty_overdue_body"))
57
+ .foregroundStyle(.secondary)
58
+ } else {
59
+ ForEach(overdue) { task in
60
+ HStack {
61
+ VStack(alignment: .leading, spacing: 4) {
62
+ Text(task.title)
63
+ .font(.headline)
64
+ Text(model.label(for: task.priority))
65
+ .foregroundStyle(.secondary)
66
+ }
67
+ Spacer()
68
+ Text(model.formatDate(task.dueDate))
69
+ .font(.subheadline.weight(.semibold))
70
+ }
71
+ .padding(.vertical, 10)
72
+ Divider()
73
+ }
74
+ }
75
+ }
76
+ .orbitCard()
77
+ }
78
+ .padding()
79
+ }
80
+ .navigationTitle(model.string("nav.analytics"))
81
+ }
82
+
83
+ private func statCard(_ title: String, value: String) -> some View {
84
+ VStack(alignment: .leading, spacing: 10) {
85
+ Text(title)
86
+ .font(.caption.weight(.semibold))
87
+ .foregroundStyle(.secondary)
88
+ Text(value)
89
+ .font(.title.weight(.bold))
90
+ }
91
+ .frame(maxWidth: .infinity, alignment: .leading)
92
+ .orbitCard()
93
+ }
94
+ }
@@ -0,0 +1,74 @@
1
+ import SwiftUI
2
+
3
+ struct SettingsView: View {
4
+ @ObservedObject var model: AppModel
5
+ @State private var draft = Preferences(locale: .en, theme: .light, remindersEnabled: true, dailySummaryEnabled: false)
6
+ @State private var showRecurringSheet = false
7
+
8
+ var body: some View {
9
+ ScrollView {
10
+ VStack(alignment: .leading, spacing: 18) {
11
+ VStack(alignment: .leading, spacing: 6) {
12
+ Text(model.string("settings.title"))
13
+ .font(.largeTitle.weight(.bold))
14
+ Text(model.string("settings.subtitle"))
15
+ .foregroundStyle(.secondary)
16
+ }
17
+
18
+ VStack(spacing: 14) {
19
+ Picker(model.string("settings.language"), selection: $draft.locale) {
20
+ Text(model.string("settings.language_en")).tag(AppLocale.en)
21
+ Text(model.string("settings.language_ru")).tag(AppLocale.ru)
22
+ }
23
+
24
+ Picker(model.string("settings.theme"), selection: $draft.theme) {
25
+ Text(model.string("settings.theme_light")).tag(ThemePreference.light)
26
+ Text(model.string("settings.theme_dark")).tag(ThemePreference.dark)
27
+ }
28
+
29
+ Toggle(model.string("settings.reminders"), isOn: $draft.remindersEnabled)
30
+ Toggle(model.string("settings.daily_summary"), isOn: $draft.dailySummaryEnabled)
31
+
32
+ Button(model.string("settings.save")) {
33
+ model.savePreferences(draft)
34
+ }
35
+ .buttonStyle(OrbitPrimaryButtonStyle())
36
+ .frame(maxWidth: .infinity, alignment: .leading)
37
+ }
38
+ .orbitCard()
39
+
40
+ VStack(alignment: .leading, spacing: 14) {
41
+ Text(model.string("settings.automation_title"))
42
+ .font(.title3.weight(.semibold))
43
+ Text(model.string("settings.automation_subtitle"))
44
+ .foregroundStyle(.secondary)
45
+
46
+ Button(model.string("settings.automation_create_rule")) {
47
+ showRecurringSheet = true
48
+ }
49
+ .buttonStyle(OrbitPrimaryButtonStyle())
50
+
51
+ if !model.rules.isEmpty {
52
+ ForEach(model.rules) { rule in
53
+ VStack(alignment: .leading, spacing: 6) {
54
+ Text(rule.name)
55
+ .font(.headline)
56
+ Text(model.describe(rule: rule))
57
+ .foregroundStyle(.secondary)
58
+ }
59
+ .frame(maxWidth: .infinity, alignment: .leading)
60
+ .padding(.vertical, 8)
61
+ }
62
+ }
63
+ }
64
+ .orbitCard()
65
+ }
66
+ .padding()
67
+ }
68
+ .navigationTitle(model.string("nav.settings"))
69
+ .onAppear { draft = model.preferences }
70
+ .sheet(isPresented: $showRecurringSheet) {
71
+ RecurringRuleSheet(model: model)
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,363 @@
1
+ import SwiftUI
2
+
3
+ struct TasksHomeView: View {
4
+ @Environment(\.horizontalSizeClass) private var horizontalSizeClass
5
+ @ObservedObject var model: AppModel
6
+ @State private var searchQuery = ""
7
+ @State private var filter: TaskFilter = .all
8
+ @State private var taskSheetMode: TaskSheetMode?
9
+ @State private var showDeleteDialog = false
10
+ @State private var showMetaSheet = false
11
+
12
+ var body: some View {
13
+ Group {
14
+ if horizontalSizeClass == .compact {
15
+ compactLayout
16
+ } else {
17
+ splitLayout
18
+ }
19
+ }
20
+ .sheet(item: $taskSheetMode) { mode in
21
+ switch mode {
22
+ case .create:
23
+ TaskEditorSheet(model: model, editingTaskID: nil)
24
+ case .edit(let id):
25
+ TaskEditorSheet(model: model, editingTaskID: id)
26
+ }
27
+ }
28
+ .sheet(isPresented: $showMetaSheet) {
29
+ if let taskID = model.selectedTaskID {
30
+ TaskMetaSheet(model: model, taskID: taskID)
31
+ }
32
+ }
33
+ .confirmationDialog(
34
+ model.string("task_detail.delete_title"),
35
+ isPresented: $showDeleteDialog,
36
+ titleVisibility: .visible
37
+ ) {
38
+ Button(model.string("common.delete"), role: .destructive) {
39
+ if let taskID = model.selectedTaskID {
40
+ model.deleteTask(taskID)
41
+ }
42
+ }
43
+ Button(model.string("common.cancel"), role: .cancel) {}
44
+ } message: {
45
+ Text(model.string("task_detail.delete_message"))
46
+ }
47
+ }
48
+
49
+ private var splitLayout: some View {
50
+ NavigationSplitView {
51
+ tasksCanvas(selectionMode: .split)
52
+ .navigationTitle(model.string("nav.tasks"))
53
+ } detail: {
54
+ if let task = model.task(id: model.selectedTaskID) {
55
+ TaskDetailPanel(
56
+ model: model,
57
+ task: task,
58
+ onEdit: { taskSheetMode = .edit(task.id) },
59
+ onMeta: { showMetaSheet = true },
60
+ onDelete: { showDeleteDialog = true }
61
+ )
62
+ } else {
63
+ ContentUnavailableView(model.string("home.empty_title"), systemImage: "checkmark.circle")
64
+ }
65
+ }
66
+ }
67
+
68
+ private var compactLayout: some View {
69
+ tasksCanvas(selectionMode: .compact)
70
+ .navigationTitle(model.string("nav.tasks"))
71
+ }
72
+
73
+ private enum SelectionMode {
74
+ case compact
75
+ case split
76
+ }
77
+
78
+ private func tasksCanvas(selectionMode: SelectionMode) -> some View {
79
+ ScrollView {
80
+ VStack(alignment: .leading, spacing: 18) {
81
+ taskHeader
82
+ searchField
83
+ filterChips
84
+ taskListContent(selectionMode: selectionMode)
85
+ }
86
+ .padding(.horizontal, horizontalSizeClass == .compact ? 16 : 20)
87
+ .padding(.top, 12)
88
+ .padding(.bottom, 104)
89
+ }
90
+ .background(Color(uiColor: .systemGroupedBackground).ignoresSafeArea())
91
+ .overlay(alignment: .bottomTrailing) {
92
+ createButton
93
+ .padding(.trailing, horizontalSizeClass == .compact ? 18 : 24)
94
+ .padding(.bottom, horizontalSizeClass == .compact ? 20 : 24)
95
+ }
96
+ }
97
+
98
+ private var taskHeader: some View {
99
+ VStack(alignment: .leading, spacing: 6) {
100
+ Text(model.string("home.title"))
101
+ .font(.largeTitle.weight(.bold))
102
+ Text(model.homeSummary())
103
+ .foregroundStyle(.secondary)
104
+ }
105
+ }
106
+
107
+ private var searchField: some View {
108
+ HStack(spacing: 12) {
109
+ Image(systemName: "magnifyingglass")
110
+ .foregroundStyle(.secondary)
111
+
112
+ TextField(model.string("home.search_placeholder"), text: $searchQuery)
113
+ .textFieldStyle(.plain)
114
+
115
+ if !searchQuery.isEmpty {
116
+ Button {
117
+ searchQuery = ""
118
+ } label: {
119
+ Image(systemName: "xmark.circle.fill")
120
+ .foregroundStyle(.secondary)
121
+ }
122
+ .buttonStyle(.plain)
123
+ }
124
+ }
125
+ .orbitInputShell(
126
+ fill: Color(uiColor: .systemBackground),
127
+ stroke: Color.teal.opacity(0.3),
128
+ lineWidth: 1.5
129
+ )
130
+ }
131
+
132
+ private var filterChips: some View {
133
+ ScrollView(.horizontal, showsIndicators: false) {
134
+ HStack(spacing: 10) {
135
+ ForEach(TaskFilter.allCases) { item in
136
+ Button {
137
+ filter = item
138
+ } label: {
139
+ Text("\(model.string("home.filter_\(item.rawValue)")) (\(model.taskCount(for: item)))")
140
+ }
141
+ .buttonStyle(OrbitChipButtonStyle(selected: filter == item))
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ @ViewBuilder
148
+ private func taskListContent(selectionMode: SelectionMode) -> some View {
149
+ let filtered = model.filteredTasks(filter: filter, search: searchQuery)
150
+
151
+ if filtered.isEmpty {
152
+ ContentUnavailableView(
153
+ model.string("home.empty_title"),
154
+ systemImage: "checkmark.circle"
155
+ )
156
+ .frame(maxWidth: .infinity, minHeight: 220)
157
+ } else {
158
+ LazyVStack(spacing: 12) {
159
+ ForEach(filtered) { task in
160
+ taskRowCard(task, selectionMode: selectionMode)
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ @ViewBuilder
167
+ private func taskRowCard(_ task: Task, selectionMode: SelectionMode) -> some View {
168
+ let selected = selectionMode == .split && model.selectedTaskID == task.id
169
+ let card = taskRow(task, selected: selected)
170
+
171
+ switch selectionMode {
172
+ case .compact:
173
+ NavigationLink {
174
+ TaskDetailPanel(
175
+ model: model,
176
+ task: task,
177
+ onEdit: { taskSheetMode = .edit(task.id) },
178
+ onMeta: {
179
+ model.selectedTaskID = task.id
180
+ showMetaSheet = true
181
+ },
182
+ onDelete: {
183
+ model.selectedTaskID = task.id
184
+ showDeleteDialog = true
185
+ }
186
+ )
187
+ } label: {
188
+ card
189
+ }
190
+ .buttonStyle(.plain)
191
+ case .split:
192
+ Button {
193
+ model.selectedTaskID = task.id
194
+ } label: {
195
+ card
196
+ }
197
+ .buttonStyle(.plain)
198
+ }
199
+ }
200
+
201
+ private func taskRow(_ task: Task, selected: Bool) -> some View {
202
+ HStack(spacing: 12) {
203
+ Button {
204
+ model.toggleTaskStatus(task.id)
205
+ } label: {
206
+ Image(systemName: task.status == .done ? "checkmark.circle.fill" : "circle")
207
+ .font(.title3)
208
+ .foregroundStyle(task.status == .done ? Color.green : Color.secondary)
209
+ }
210
+ .buttonStyle(.plain)
211
+
212
+ VStack(alignment: .leading, spacing: 4) {
213
+ Text(task.title)
214
+ .font(.headline)
215
+ .foregroundStyle(.primary)
216
+ .multilineTextAlignment(.leading)
217
+ Text(model.formatRelativeDueDate(task.dueDate))
218
+ .font(.subheadline)
219
+ .foregroundStyle(.secondary)
220
+ }
221
+
222
+ Spacer(minLength: 12)
223
+ PriorityDot(priority: task.priority)
224
+
225
+ if horizontalSizeClass == .compact {
226
+ Image(systemName: "chevron.right")
227
+ .font(.footnote.weight(.bold))
228
+ .foregroundStyle(.tertiary)
229
+ }
230
+ }
231
+ .frame(maxWidth: .infinity, alignment: .leading)
232
+ .orbitSurface(
233
+ cut: 14,
234
+ fill: selected ? Color.teal.opacity(0.12) : Color(uiColor: .systemBackground),
235
+ stroke: selected ? Color.teal.opacity(0.34) : Color(uiColor: .separator).opacity(0.28),
236
+ lineWidth: selected ? 1.5 : 1,
237
+ contentPadding: 16
238
+ )
239
+ }
240
+
241
+ private var createButton: some View {
242
+ Button {
243
+ taskSheetMode = .create
244
+ } label: {
245
+ Label(model.string("home.new_task"), systemImage: "plus")
246
+ }
247
+ .buttonStyle(OrbitFloatingActionButtonStyle())
248
+ }
249
+ }
250
+
251
+ private enum TaskSheetMode: Identifiable {
252
+ case create
253
+ case edit(UUID)
254
+
255
+ var id: String {
256
+ switch self {
257
+ case .create: "create"
258
+ case .edit(let id): "edit-\(id.uuidString)"
259
+ }
260
+ }
261
+ }
262
+
263
+ private struct TaskDetailPanel: View {
264
+ @ObservedObject var model: AppModel
265
+ let task: Task
266
+ let onEdit: () -> Void
267
+ let onMeta: () -> Void
268
+ let onDelete: () -> Void
269
+
270
+ var body: some View {
271
+ ScrollView {
272
+ VStack(alignment: .leading, spacing: 18) {
273
+ VStack(alignment: .leading, spacing: 8) {
274
+ Text(task.title)
275
+ .font(.largeTitle.weight(.bold))
276
+ Text(model.formatDate(task.dueDate))
277
+ .foregroundStyle(.secondary)
278
+ Label(model.label(for: task.status), systemImage: task.status == .done ? "checkmark.circle.fill" : "circle")
279
+ .foregroundStyle(task.status == .done ? .green : .blue)
280
+ }
281
+ .orbitCard(fill: Color(uiColor: .systemBackground), stroke: task.priority.tint.opacity(0.25))
282
+
283
+ LazyVGrid(columns: [.init(.flexible()), .init(.flexible())], spacing: 12) {
284
+ stat(model.string("task_detail.status"), value: model.label(for: task.status))
285
+ stat(model.string("task_detail.priority"), value: model.label(for: task.priority))
286
+ stat(model.string("task_detail.created"), value: model.formatDate(task.createdAt))
287
+ stat(model.string("task_detail.updated"), value: model.formatDate(task.updatedAt))
288
+ }
289
+
290
+ if !task.notes.isEmpty {
291
+ VStack(alignment: .leading, spacing: 10) {
292
+ Text(model.string("task_detail.notes"))
293
+ .font(.headline)
294
+ Text(task.notes)
295
+ .foregroundStyle(.secondary)
296
+ }
297
+ .orbitCard()
298
+ }
299
+
300
+ VStack(spacing: 12) {
301
+ Button(model.string("task_detail.edit"), action: onEdit)
302
+ .buttonStyle(OrbitPrimaryButtonStyle())
303
+ Button(model.string("task_detail.toggle_status")) {
304
+ model.toggleTaskStatus(task.id)
305
+ }
306
+ .buttonStyle(OrbitGhostButtonStyle())
307
+ Button(model.string("task_detail.more_info"), action: onMeta)
308
+ .buttonStyle(OrbitGhostButtonStyle())
309
+ Button(model.string("task_detail.delete"), role: .destructive, action: onDelete)
310
+ .buttonStyle(OrbitGhostButtonStyle())
311
+ }
312
+ }
313
+ .padding()
314
+ }
315
+ }
316
+
317
+ private func stat(_ title: String, value: String) -> some View {
318
+ VStack(alignment: .leading, spacing: 10) {
319
+ Text(title)
320
+ .font(.caption.weight(.semibold))
321
+ .foregroundStyle(.secondary)
322
+ Text(value)
323
+ .font(.title3.weight(.bold))
324
+ }
325
+ .frame(maxWidth: .infinity, alignment: .leading)
326
+ .orbitCard()
327
+ }
328
+ }
329
+
330
+ private struct TaskMetaSheet: View {
331
+ @Environment(\.dismiss) private var dismiss
332
+ @ObservedObject var model: AppModel
333
+ let taskID: UUID
334
+
335
+ var body: some View {
336
+ NavigationStack {
337
+ if let task = model.task(id: taskID) {
338
+ List {
339
+ row(model.string("task_detail.status"), model.label(for: task.status))
340
+ row(model.string("task_detail.priority"), model.label(for: task.priority))
341
+ row(model.string("task_detail.created"), model.formatDate(task.createdAt))
342
+ row(model.string("task_detail.updated"), model.formatDate(task.updatedAt))
343
+ }
344
+ .navigationTitle(model.string("task_detail.more_info"))
345
+ .toolbar {
346
+ ToolbarItem(placement: .topBarTrailing) {
347
+ Button(model.string("common.cancel")) { dismiss() }
348
+ }
349
+ }
350
+ }
351
+ }
352
+ .presentationDetents([.medium])
353
+ }
354
+
355
+ private func row(_ title: String, _ value: String) -> some View {
356
+ HStack {
357
+ Text(title)
358
+ Spacer()
359
+ Text(value)
360
+ .foregroundStyle(.secondary)
361
+ }
362
+ }
363
+ }