openuispec 0.2.10 → 0.2.12

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 (237) hide show
  1. package/README.md +3 -1
  2. package/check/index.ts +17 -0
  3. package/cli/index.ts +21 -3
  4. package/cli/init.ts +224 -10
  5. package/docs/cli.md +13 -8
  6. package/docs/file-formats.md +36 -0
  7. package/docs/implementation-notes.md +7 -0
  8. package/drift/index.ts +281 -40
  9. package/mcp-server/index.ts +179 -119
  10. package/mcp-server/screenshot.ts +19 -4
  11. package/package.json +5 -2
  12. package/prepare/index.ts +155 -18
  13. package/schema/openuispec.schema.json +59 -0
  14. package/schema/semantic-lint.ts +25 -1
  15. package/scripts/take-all-screenshots.ts +507 -0
  16. package/spec/openuispec-v0.1.md +13 -0
  17. package/status/index.ts +72 -2
  18. package/examples/social-app/.mcp.json +0 -10
  19. package/examples/social-app/AGENTS.md +0 -124
  20. package/examples/social-app/CLAUDE.md +0 -124
  21. package/examples/social-app/backend/.gitkeep +0 -1
  22. package/examples/social-app/generated/android/social-app/app/.paparazzi-hashes.json +0 -3
  23. package/examples/social-app/generated/android/social-app/app/build.gradle.kts +0 -94
  24. package/examples/social-app/generated/android/social-app/app/src/main/AndroidManifest.xml +0 -26
  25. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/AppContainer.kt +0 -20
  26. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/MainActivity.kt +0 -35
  27. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/SocialAppApplication.kt +0 -13
  28. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/MockData.kt +0 -98
  29. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/AppPreferences.kt +0 -19
  30. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/DataStorePreferencesRepository.kt +0 -68
  31. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/PreferencesRepository.kt +0 -15
  32. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/model/Models.kt +0 -34
  33. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/MainShell.kt +0 -390
  34. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/Components.kt +0 -234
  35. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/ContractPrimitives.kt +0 -641
  36. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/navigation/RootComponent.kt +0 -113
  37. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ChatDetailScreen.kt +0 -212
  38. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/CreatePostScreen.kt +0 -113
  39. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/DiscoverScreen.kt +0 -137
  40. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/EditProfileScreen.kt +0 -180
  41. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +0 -169
  42. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/MessagesInboxScreen.kt +0 -85
  43. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/NotificationsScreen.kt +0 -74
  44. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/PostDetailScreen.kt +0 -293
  45. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ProfileSelfScreen.kt +0 -116
  46. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SearchResultsScreen.kt +0 -161
  47. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsScreen.kt +0 -164
  48. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsStore.kt +0 -95
  49. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/UserProfileScreen.kt +0 -123
  50. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Color.kt +0 -33
  51. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Shape.kt +0 -41
  52. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Spacing.kt +0 -20
  53. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Theme.kt +0 -82
  54. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Type.kt +0 -60
  55. package/examples/social-app/generated/android/social-app/app/src/main/res/drawable/ic_launcher_foreground.xml +0 -9
  56. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -5
  57. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -5
  58. package/examples/social-app/generated/android/social-app/app/src/main/res/values/strings.xml +0 -91
  59. package/examples/social-app/generated/android/social-app/app/src/main/res/values/themes.xml +0 -10
  60. package/examples/social-app/generated/android/social-app/app/src/main/res/values-ru/strings.xml +0 -79
  61. package/examples/social-app/generated/android/social-app/app/src/main/res/values-uz/strings.xml +0 -79
  62. package/examples/social-app/generated/android/social-app/app/src/main/xml/AndroidManifest.xml +0 -23
  63. package/examples/social-app/generated/android/social-app/app/src/test/kotlin/com/social/app/screenshots/HomeFeedScreenshotTest.kt +0 -34
  64. package/examples/social-app/generated/android/social-app/build.gradle.kts +0 -7
  65. package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +0 -50
  66. package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.jar +0 -0
  67. package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.properties +0 -8
  68. package/examples/social-app/generated/android/social-app/gradle.properties +0 -11
  69. package/examples/social-app/generated/android/social-app/gradlew +0 -248
  70. package/examples/social-app/generated/android/social-app/settings.gradle.kts +0 -27
  71. package/examples/social-app/generated/web/social-app/index.html +0 -12
  72. package/examples/social-app/generated/web/social-app/package-lock.json +0 -2517
  73. package/examples/social-app/generated/web/social-app/package.json +0 -27
  74. package/examples/social-app/generated/web/social-app/src/app/App.tsx +0 -58
  75. package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +0 -259
  76. package/examples/social-app/generated/web/social-app/src/components/cards.tsx +0 -317
  77. package/examples/social-app/generated/web/social-app/src/components/ui.tsx +0 -340
  78. package/examples/social-app/generated/web/social-app/src/flows/CreatePostFlow.tsx +0 -86
  79. package/examples/social-app/generated/web/social-app/src/i18n.tsx +0 -59
  80. package/examples/social-app/generated/web/social-app/src/lib/icons.tsx +0 -85
  81. package/examples/social-app/generated/web/social-app/src/lib/tokens.ts +0 -70
  82. package/examples/social-app/generated/web/social-app/src/lib/utils.ts +0 -97
  83. package/examples/social-app/generated/web/social-app/src/locales/en.json +0 -67
  84. package/examples/social-app/generated/web/social-app/src/locales/ru.json +0 -67
  85. package/examples/social-app/generated/web/social-app/src/locales/uz.json +0 -67
  86. package/examples/social-app/generated/web/social-app/src/main.tsx +0 -16
  87. package/examples/social-app/generated/web/social-app/src/screens/ChatDetailScreen.tsx +0 -90
  88. package/examples/social-app/generated/web/social-app/src/screens/DiscoverScreen.tsx +0 -86
  89. package/examples/social-app/generated/web/social-app/src/screens/EditProfileScreen.tsx +0 -57
  90. package/examples/social-app/generated/web/social-app/src/screens/HomeFeedScreen.tsx +0 -103
  91. package/examples/social-app/generated/web/social-app/src/screens/MessagesInboxScreen.tsx +0 -52
  92. package/examples/social-app/generated/web/social-app/src/screens/NotificationsScreen.tsx +0 -41
  93. package/examples/social-app/generated/web/social-app/src/screens/PostDetailScreen.tsx +0 -115
  94. package/examples/social-app/generated/web/social-app/src/screens/ProfileSelfScreen.tsx +0 -57
  95. package/examples/social-app/generated/web/social-app/src/screens/ProfileUserScreen.tsx +0 -76
  96. package/examples/social-app/generated/web/social-app/src/screens/SearchResultsScreen.tsx +0 -96
  97. package/examples/social-app/generated/web/social-app/src/screens/SettingsScreen.tsx +0 -79
  98. package/examples/social-app/generated/web/social-app/src/state/store.ts +0 -592
  99. package/examples/social-app/generated/web/social-app/src/styles.css +0 -125
  100. package/examples/social-app/generated/web/social-app/src/vite-env.d.ts +0 -1
  101. package/examples/social-app/generated/web/social-app/tsconfig.json +0 -22
  102. package/examples/social-app/generated/web/social-app/tsconfig.node.json +0 -13
  103. package/examples/social-app/generated/web/social-app/tsconfig.node.tsbuildinfo +0 -1
  104. package/examples/social-app/generated/web/social-app/tsconfig.tsbuildinfo +0 -1
  105. package/examples/social-app/generated/web/social-app/vite.config.d.ts +0 -2
  106. package/examples/social-app/generated/web/social-app/vite.config.js +0 -6
  107. package/examples/social-app/generated/web/social-app/vite.config.ts +0 -7
  108. package/examples/social-app/package.json +0 -13
  109. package/examples/social-app/take-web-screenshots.ts +0 -97
  110. package/examples/taskflow/.codex/config.toml +0 -4
  111. package/examples/taskflow/.mcp.json +0 -10
  112. package/examples/taskflow/AGENTS.md +0 -124
  113. package/examples/taskflow/CLAUDE.md +0 -124
  114. package/examples/taskflow/backend/.gitkeep +0 -1
  115. package/examples/taskflow/generated/android/TaskFlow/README.md +0 -43
  116. package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +0 -76
  117. package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +0 -1
  118. package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +0 -21
  119. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +0 -19
  120. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +0 -283
  121. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +0 -106
  122. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +0 -57
  123. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +0 -109
  124. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +0 -112
  125. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +0 -61
  126. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +0 -82
  127. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +0 -111
  128. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +0 -77
  129. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +0 -30
  130. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +0 -86
  131. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +0 -57
  132. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +0 -155
  133. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +0 -4
  134. package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +0 -5
  135. package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +0 -12
  136. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
  137. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +0 -7
  138. package/examples/taskflow/generated/android/TaskFlow/gradle.properties +0 -4
  139. package/examples/taskflow/generated/android/TaskFlow/gradlew +0 -18
  140. package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +0 -12
  141. package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +0 -18
  142. package/examples/taskflow/generated/ios/TaskFlow/README.md +0 -21
  143. package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +0 -115
  144. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +0 -24
  145. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +0 -150
  146. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +0 -220
  147. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +0 -122
  148. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +0 -21
  149. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +0 -201
  150. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +0 -48
  151. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +0 -59
  152. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +0 -63
  153. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +0 -85
  154. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +0 -219
  155. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +0 -320
  156. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +0 -41
  157. package/examples/taskflow/generated/ios/TaskFlow/project.yml +0 -31
  158. package/examples/taskflow/generated/web/TaskFlow/README.md +0 -19
  159. package/examples/taskflow/generated/web/TaskFlow/index.html +0 -12
  160. package/examples/taskflow/generated/web/TaskFlow/package-lock.json +0 -1908
  161. package/examples/taskflow/generated/web/TaskFlow/package.json +0 -24
  162. package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +0 -58
  163. package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +0 -55
  164. package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +0 -82
  165. package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +0 -191
  166. package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +0 -41
  167. package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +0 -131
  168. package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +0 -25
  169. package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +0 -39
  170. package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +0 -111
  171. package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +0 -13
  172. package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +0 -111
  173. package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +0 -82
  174. package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +0 -132
  175. package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +0 -105
  176. package/examples/taskflow/generated/web/TaskFlow/src/store.ts +0 -216
  177. package/examples/taskflow/generated/web/TaskFlow/src/styles.css +0 -617
  178. package/examples/taskflow/generated/web/TaskFlow/src/types.ts +0 -64
  179. package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +0 -78
  180. package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +0 -21
  181. package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +0 -6
  182. package/examples/todo-orbit/.codex/config.toml +0 -4
  183. package/examples/todo-orbit/.mcp.json +0 -10
  184. package/examples/todo-orbit/AGENTS.md +0 -124
  185. package/examples/todo-orbit/CLAUDE.md +0 -124
  186. package/examples/todo-orbit/backend/.gitkeep +0 -1
  187. package/examples/todo-orbit/generated/android/Todo Orbit/README.md +0 -14
  188. package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +0 -58
  189. package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +0 -1
  190. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +0 -20
  191. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +0 -14
  192. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +0 -345
  193. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +0 -231
  194. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +0 -169
  195. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +0 -8
  196. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +0 -236
  197. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +0 -193
  198. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +0 -102
  199. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +0 -347
  200. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +0 -347
  201. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +0 -59
  202. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +0 -149
  203. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +0 -155
  204. package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +0 -4
  205. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
  206. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +0 -7
  207. package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +0 -4
  208. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +0 -248
  209. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +0 -93
  210. package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +0 -18
  211. package/examples/todo-orbit/generated/ios/Todo Orbit/.screenshot-uitest/Sources/ScreenshotUITest.swift +0 -36
  212. package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +0 -29
  213. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +0 -119
  214. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +0 -119
  215. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +0 -50
  216. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +0 -204
  217. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +0 -126
  218. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +0 -70
  219. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +0 -126
  220. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +0 -61
  221. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +0 -238
  222. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +0 -94
  223. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +0 -76
  224. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +0 -364
  225. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +0 -324
  226. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +0 -439
  227. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  228. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +0 -89
  229. package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +0 -32
  230. package/examples/todo-orbit/generated/web/Todo Orbit/index.html +0 -16
  231. package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +0 -1087
  232. package/examples/todo-orbit/generated/web/Todo Orbit/package.json +0 -24
  233. package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +0 -2167
  234. package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +0 -13
  235. package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +0 -926
  236. package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +0 -19
  237. package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +0 -6
@@ -1,364 +0,0 @@
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
- .navigationTitle(model.string("task_detail.title"))
316
- }
317
-
318
- private func stat(_ title: String, value: String) -> some View {
319
- VStack(alignment: .leading, spacing: 10) {
320
- Text(title)
321
- .font(.caption.weight(.semibold))
322
- .foregroundStyle(.secondary)
323
- Text(value)
324
- .font(.title3.weight(.bold))
325
- }
326
- .frame(maxWidth: .infinity, alignment: .leading)
327
- .orbitCard()
328
- }
329
- }
330
-
331
- private struct TaskMetaSheet: View {
332
- @Environment(\.dismiss) private var dismiss
333
- @ObservedObject var model: AppModel
334
- let taskID: UUID
335
-
336
- var body: some View {
337
- NavigationStack {
338
- if let task = model.task(id: taskID) {
339
- List {
340
- row(model.string("task_detail.status"), model.label(for: task.status))
341
- row(model.string("task_detail.priority"), model.label(for: task.priority))
342
- row(model.string("task_detail.created"), model.formatDate(task.createdAt))
343
- row(model.string("task_detail.updated"), model.formatDate(task.updatedAt))
344
- }
345
- .navigationTitle(model.string("task_detail.more_info"))
346
- .toolbar {
347
- ToolbarItem(placement: .topBarTrailing) {
348
- Button(model.string("common.cancel")) { dismiss() }
349
- }
350
- }
351
- }
352
- }
353
- .presentationDetents([.medium])
354
- }
355
-
356
- private func row(_ title: String, _ value: String) -> some View {
357
- HStack {
358
- Text(title)
359
- Spacer()
360
- Text(value)
361
- .foregroundStyle(.secondary)
362
- }
363
- }
364
- }
@@ -1,324 +0,0 @@
1
- import Foundation
2
- import SwiftUI
3
-
4
- @MainActor
5
- final class AppModel: ObservableObject {
6
- @Published var preferences = Preferences(
7
- locale: .en,
8
- theme: .light,
9
- remindersEnabled: true,
10
- dailySummaryEnabled: false
11
- )
12
- @Published var tasks: [Task] = Task.seed()
13
- @Published var rules: [RecurringRule] = []
14
- @Published var selectedTaskID: UUID?
15
- @Published var toast: ToastMessage?
16
-
17
- init() {
18
- selectedTaskID = tasks.first?.id
19
- }
20
-
21
- var locale: Locale { Locale(identifier: preferences.locale.rawValue) }
22
-
23
- func string(_ key: String) -> String {
24
- let bundle = Bundle.main.path(
25
- forResource: preferences.locale.rawValue,
26
- ofType: "lproj"
27
- ).flatMap { Bundle(path: $0) } ?? .main
28
- return bundle.localizedString(forKey: key, value: key, table: nil)
29
- }
30
-
31
- func format(_ key: String, _ arguments: CVarArg...) -> String {
32
- let format = string(key)
33
- return String(format: format, locale: locale, arguments: arguments)
34
- }
35
-
36
- func homeSummary() -> String {
37
- let open = tasks.filter { $0.status == .open }.count
38
- let total = tasks.count
39
- if preferences.locale == .ru {
40
- if open == 0 { return string("home.summary.done") }
41
- return format("home.summary.remaining", open, total)
42
- }
43
-
44
- if open == 0 { return string("home.summary.done") }
45
- return format("home.summary.remaining", open, total)
46
- }
47
-
48
- func taskCount(for filter: TaskFilter) -> Int {
49
- switch filter {
50
- case .all:
51
- return tasks.count
52
- case .open:
53
- return tasks.filter { $0.status == .open }.count
54
- case .done:
55
- return tasks.filter { $0.status == .done }.count
56
- }
57
- }
58
-
59
- func filteredTasks(filter: TaskFilter, search: String) -> [Task] {
60
- tasks.filter { task in
61
- let matchesFilter: Bool
62
- switch filter {
63
- case .all: matchesFilter = true
64
- case .open: matchesFilter = task.status == .open
65
- case .done: matchesFilter = task.status == .done
66
- }
67
-
68
- let query = search.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
69
- let matchesSearch = query.isEmpty || "\(task.title) \(task.notes)".lowercased().contains(query)
70
- return matchesFilter && matchesSearch
71
- }
72
- }
73
-
74
- func task(id: UUID?) -> Task? {
75
- guard let id else { return nil }
76
- return tasks.first(where: { $0.id == id })
77
- }
78
-
79
- func binding(for taskID: UUID) -> Binding<Task>? {
80
- guard let index = tasks.firstIndex(where: { $0.id == taskID }) else { return nil }
81
- return Binding(
82
- get: { self.tasks[index] },
83
- set: { self.tasks[index] = $0 }
84
- )
85
- }
86
-
87
- func savePreferences(_ draft: Preferences) {
88
- preferences = draft
89
- showToast(level: .success, text: string("settings.saved"))
90
- }
91
-
92
- func makeTaskDraft(for task: Task?) -> TaskEditorDraft {
93
- TaskEditorDraft(
94
- title: task?.title ?? "",
95
- notes: task?.notes ?? "",
96
- priority: task?.priority ?? .medium,
97
- dueDate: task?.dueDate
98
- )
99
- }
100
-
101
- func submitTask(_ draft: TaskEditorDraft, editing taskID: UUID?) -> String? {
102
- let title = draft.title.trimmingCharacters(in: .whitespacesAndNewlines)
103
- guard title.count >= 2 else {
104
- return format("validation.min_length", 2)
105
- }
106
-
107
- if let taskID, let index = tasks.firstIndex(where: { $0.id == taskID }) {
108
- tasks[index].title = title
109
- tasks[index].notes = draft.notes.trimmingCharacters(in: .whitespacesAndNewlines)
110
- tasks[index].priority = draft.priority
111
- tasks[index].dueDate = draft.dueDate
112
- tasks[index].updatedAt = .now
113
- showToast(level: .success, text: string("edit_task.success"))
114
- } else {
115
- let next = Task(
116
- id: UUID(),
117
- title: title,
118
- notes: draft.notes.trimmingCharacters(in: .whitespacesAndNewlines),
119
- status: .open,
120
- priority: draft.priority,
121
- dueDate: draft.dueDate,
122
- createdAt: .now,
123
- updatedAt: .now
124
- )
125
- tasks.insert(next, at: 0)
126
- selectedTaskID = next.id
127
- showToast(level: .success, text: string("create_task.success"))
128
- }
129
-
130
- return nil
131
- }
132
-
133
- func toggleTaskStatus(_ taskID: UUID) {
134
- guard let index = tasks.firstIndex(where: { $0.id == taskID }) else { return }
135
- tasks[index].status = tasks[index].status == .done ? .open : .done
136
- tasks[index].updatedAt = .now
137
- showToast(level: .success, text: string("task_detail.updated_feedback"))
138
- }
139
-
140
- func deleteTask(_ taskID: UUID) {
141
- tasks.removeAll { $0.id == taskID }
142
- if selectedTaskID == taskID {
143
- selectedTaskID = tasks.first?.id
144
- }
145
- showToast(level: .success, text: string("task_detail.deleted_feedback"))
146
- }
147
-
148
- func addRecurringRule(from draft: RecurringRuleDraft) -> [String: String] {
149
- var errors: [String: String] = [:]
150
-
151
- let trimmedName = draft.name.trimmingCharacters(in: .whitespacesAndNewlines)
152
- if trimmedName.count < 4 {
153
- errors["name"] = format("validation.rule_name_min_length", 4)
154
- } else if trimmedName == "Default" {
155
- errors["name"] = string("validation.rule_name_reserved")
156
- } else if rules.contains(where: { $0.name.caseInsensitiveCompare(trimmedName) == .orderedSame }) {
157
- errors["name"] = string("validation.rule_name_taken")
158
- }
159
-
160
- if draft.confirmName.trimmingCharacters(in: .whitespacesAndNewlines) != trimmedName {
161
- errors["confirmName"] = string("validation.match_field")
162
- }
163
-
164
- guard let cadence = draft.cadence else {
165
- errors["cadence"] = string("validation.required")
166
- return errors
167
- }
168
-
169
- guard let interval = Int(draft.interval), interval >= 1 else {
170
- errors["interval"] = format("validation.min_value", 1)
171
- return errors
172
- }
173
-
174
- if interval > 30 {
175
- errors["interval"] = format("validation.max_value", 30)
176
- }
177
-
178
- if cadence == .weekly, draft.weekday == nil {
179
- errors["weekday"] = string("validation.required")
180
- }
181
-
182
- var monthDayValue: Int?
183
- if cadence == .monthly {
184
- guard let day = Int(draft.monthDay), day >= 1 else {
185
- errors["monthDay"] = format("validation.min_value", 1)
186
- return errors
187
- }
188
- if day > 28 {
189
- errors["monthDay"] = string("validation.month_day_max")
190
- } else {
191
- monthDayValue = day
192
- }
193
- }
194
-
195
- if draft.hasEndDate && draft.endDate < draft.startDate {
196
- errors["endDate"] = string("validation.end_date_after_start")
197
- }
198
-
199
- if preferences.remindersEnabled {
200
- let regex = try? NSRegularExpression(pattern: "^([01]\\d|2[0-3]):[0-5]\\d$")
201
- let range = NSRange(location: 0, length: draft.remindAt.utf16.count)
202
- let matches = regex?.firstMatch(in: draft.remindAt, options: [], range: range) != nil
203
- if !matches {
204
- errors["remindAt"] = string("validation.time_format")
205
- }
206
- }
207
-
208
- if draft.enableSummary && draft.summaryChannel == nil {
209
- errors["summaryChannel"] = string("validation.required")
210
- }
211
-
212
- guard errors.isEmpty else { return errors }
213
-
214
- let rule = RecurringRule(
215
- id: UUID(),
216
- name: trimmedName,
217
- cadence: cadence,
218
- interval: interval,
219
- weekday: draft.weekday,
220
- monthDay: monthDayValue,
221
- startDate: draft.startDate,
222
- endDate: draft.hasEndDate ? draft.endDate : nil,
223
- remindAt: preferences.remindersEnabled ? draft.remindAt : nil,
224
- summaryChannel: draft.enableSummary ? draft.summaryChannel : nil
225
- )
226
- rules.insert(rule, at: 0)
227
- showToast(level: .success, text: string("recurring_rule.success"))
228
- return [:]
229
- }
230
-
231
- func analyticsSnapshot() -> AnalyticsSnapshot {
232
- let calendar = Calendar.current
233
- let startOfToday = calendar.startOfDay(for: .now)
234
- let completedToday = tasks.filter {
235
- $0.status == .done && $0.updatedAt >= startOfToday
236
- }.count
237
- let openTasks = tasks.filter { $0.status == .open }.count
238
- let overdueTasks = overdueTasks().count
239
- let completionRate = tasks.isEmpty ? 0 : Int(((Double(tasks.count - openTasks) / Double(tasks.count)) * 100).rounded())
240
- return AnalyticsSnapshot(
241
- completedToday: completedToday,
242
- openTasks: openTasks,
243
- overdueTasks: overdueTasks,
244
- completionRate: completionRate
245
- )
246
- }
247
-
248
- func trendSeries(period: AnalyticsPeriod) -> [TrendPoint] {
249
- let calendar = Calendar.current
250
- let formatter = DateFormatter()
251
- formatter.locale = locale
252
- formatter.dateFormat = period == .week ? "E" : "MMM d"
253
- let length: Int = switch period {
254
- case .week: 7
255
- case .month: 6
256
- case .quarter: 8
257
- }
258
- let strideDays: Int = period == .week ? 1 : 5
259
-
260
- return (0..<length).map { index in
261
- let offset = length - index - 1
262
- let pointDate = calendar.date(byAdding: .day, value: -(offset * strideDays), to: .now) ?? .now
263
- let completed = tasks.filter { $0.status == .done && $0.updatedAt <= pointDate }.count
264
- let created = tasks.filter { $0.createdAt <= pointDate }.count
265
- return TrendPoint(label: formatter.string(from: pointDate), completed: completed, created: created)
266
- }
267
- }
268
-
269
- func overdueTasks() -> [Task] {
270
- let startOfToday = Calendar.current.startOfDay(for: .now)
271
- return tasks.filter { task in
272
- task.status == .open && (task.dueDate.map { $0 < startOfToday } ?? false)
273
- }
274
- }
275
-
276
- func formatDate(_ date: Date?) -> String {
277
- guard let date else { return string("task_detail.no_due_date") }
278
- let formatter = DateFormatter()
279
- formatter.locale = locale
280
- formatter.dateStyle = .medium
281
- formatter.timeStyle = .none
282
- return formatter.string(from: date)
283
- }
284
-
285
- func formatRelativeDueDate(_ date: Date?) -> String {
286
- guard let date else { return string("task_detail.no_due_date") }
287
- let formatter = RelativeDateTimeFormatter()
288
- formatter.locale = locale
289
- formatter.unitsStyle = .abbreviated
290
- return formatter.localizedString(for: date, relativeTo: .now)
291
- }
292
-
293
- func label(for priority: TaskPriority) -> String { string("priority.\(priority.rawValue)") }
294
- func label(for status: TaskStatus) -> String { string("status.\(status.rawValue)") }
295
- func label(for weekday: Weekday) -> String { string("weekday.\(weekday.rawValue)") }
296
-
297
- func describe(rule: RecurringRule) -> String {
298
- switch rule.cadence {
299
- case .daily:
300
- return "\(string("recurring_rule.cadence_daily")) · \(formatDate(rule.startDate))"
301
- case .weekly:
302
- return "\(string("recurring_rule.cadence_weekly")) · \(label(for: rule.weekday ?? .mon))"
303
- case .monthly:
304
- return "\(string("recurring_rule.cadence_monthly")) · \(rule.monthDay ?? 1)"
305
- }
306
- }
307
-
308
- func showToast(level: ToastMessage.Level, text: String) {
309
- withAnimation(.spring(duration: 0.32)) {
310
- toast = ToastMessage(level: level, text: text)
311
- }
312
-
313
- _Concurrency.Task { [weak self] in
314
- try? await _Concurrency.Task.sleep(for: .seconds(2.5))
315
- guard let self else { return }
316
- await MainActor.run {
317
- withAnimation(.easeOut(duration: 0.2)) {
318
- self.toast = nil
319
- }
320
- }
321
- }
322
- }
323
-
324
- }