openuispec 0.2.9 → 0.2.11

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 (232) hide show
  1. package/README.md +3 -1
  2. package/cli/index.ts +80 -2
  3. package/cli/init.ts +23 -8
  4. package/docs/cli.md +42 -7
  5. package/drift/index.ts +41 -15
  6. package/mcp-server/index.ts +247 -117
  7. package/mcp-server/screenshot-android.ts +185 -44
  8. package/mcp-server/screenshot-ios.ts +242 -30
  9. package/mcp-server/screenshot.ts +96 -1
  10. package/package.json +5 -2
  11. package/prepare/index.ts +16 -0
  12. package/scripts/take-all-screenshots.ts +507 -0
  13. package/status/index.ts +2 -2
  14. package/examples/social-app/.mcp.json +0 -10
  15. package/examples/social-app/AGENTS.md +0 -124
  16. package/examples/social-app/CLAUDE.md +0 -124
  17. package/examples/social-app/backend/.gitkeep +0 -1
  18. package/examples/social-app/generated/android/social-app/app/.paparazzi-hashes.json +0 -3
  19. package/examples/social-app/generated/android/social-app/app/build.gradle.kts +0 -94
  20. package/examples/social-app/generated/android/social-app/app/src/main/AndroidManifest.xml +0 -26
  21. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/AppContainer.kt +0 -20
  22. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/MainActivity.kt +0 -35
  23. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/SocialAppApplication.kt +0 -13
  24. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/MockData.kt +0 -98
  25. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/AppPreferences.kt +0 -19
  26. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/DataStorePreferencesRepository.kt +0 -68
  27. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/PreferencesRepository.kt +0 -15
  28. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/model/Models.kt +0 -34
  29. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/MainShell.kt +0 -390
  30. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/Components.kt +0 -234
  31. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/ContractPrimitives.kt +0 -641
  32. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/navigation/RootComponent.kt +0 -113
  33. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ChatDetailScreen.kt +0 -212
  34. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/CreatePostScreen.kt +0 -113
  35. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/DiscoverScreen.kt +0 -137
  36. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/EditProfileScreen.kt +0 -180
  37. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +0 -169
  38. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/MessagesInboxScreen.kt +0 -85
  39. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/NotificationsScreen.kt +0 -74
  40. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/PostDetailScreen.kt +0 -293
  41. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ProfileSelfScreen.kt +0 -116
  42. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SearchResultsScreen.kt +0 -161
  43. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsScreen.kt +0 -164
  44. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsStore.kt +0 -95
  45. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/UserProfileScreen.kt +0 -123
  46. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Color.kt +0 -33
  47. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Shape.kt +0 -41
  48. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Spacing.kt +0 -20
  49. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Theme.kt +0 -82
  50. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Type.kt +0 -60
  51. package/examples/social-app/generated/android/social-app/app/src/main/res/drawable/ic_launcher_foreground.xml +0 -9
  52. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -5
  53. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -5
  54. package/examples/social-app/generated/android/social-app/app/src/main/res/values/strings.xml +0 -91
  55. package/examples/social-app/generated/android/social-app/app/src/main/res/values/themes.xml +0 -10
  56. package/examples/social-app/generated/android/social-app/app/src/main/res/values-ru/strings.xml +0 -79
  57. package/examples/social-app/generated/android/social-app/app/src/main/res/values-uz/strings.xml +0 -79
  58. package/examples/social-app/generated/android/social-app/app/src/main/xml/AndroidManifest.xml +0 -23
  59. package/examples/social-app/generated/android/social-app/app/src/test/kotlin/com/social/app/screenshots/HomeFeedScreenshotTest.kt +0 -34
  60. package/examples/social-app/generated/android/social-app/build.gradle.kts +0 -7
  61. package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +0 -50
  62. package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.jar +0 -0
  63. package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.properties +0 -8
  64. package/examples/social-app/generated/android/social-app/gradle.properties +0 -11
  65. package/examples/social-app/generated/android/social-app/gradlew +0 -248
  66. package/examples/social-app/generated/android/social-app/settings.gradle.kts +0 -27
  67. package/examples/social-app/generated/web/social-app/index.html +0 -12
  68. package/examples/social-app/generated/web/social-app/package-lock.json +0 -2517
  69. package/examples/social-app/generated/web/social-app/package.json +0 -27
  70. package/examples/social-app/generated/web/social-app/src/app/App.tsx +0 -58
  71. package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +0 -259
  72. package/examples/social-app/generated/web/social-app/src/components/cards.tsx +0 -317
  73. package/examples/social-app/generated/web/social-app/src/components/ui.tsx +0 -340
  74. package/examples/social-app/generated/web/social-app/src/flows/CreatePostFlow.tsx +0 -86
  75. package/examples/social-app/generated/web/social-app/src/i18n.tsx +0 -59
  76. package/examples/social-app/generated/web/social-app/src/lib/icons.tsx +0 -85
  77. package/examples/social-app/generated/web/social-app/src/lib/tokens.ts +0 -70
  78. package/examples/social-app/generated/web/social-app/src/lib/utils.ts +0 -97
  79. package/examples/social-app/generated/web/social-app/src/locales/en.json +0 -67
  80. package/examples/social-app/generated/web/social-app/src/locales/ru.json +0 -67
  81. package/examples/social-app/generated/web/social-app/src/locales/uz.json +0 -67
  82. package/examples/social-app/generated/web/social-app/src/main.tsx +0 -16
  83. package/examples/social-app/generated/web/social-app/src/screens/ChatDetailScreen.tsx +0 -90
  84. package/examples/social-app/generated/web/social-app/src/screens/DiscoverScreen.tsx +0 -86
  85. package/examples/social-app/generated/web/social-app/src/screens/EditProfileScreen.tsx +0 -57
  86. package/examples/social-app/generated/web/social-app/src/screens/HomeFeedScreen.tsx +0 -103
  87. package/examples/social-app/generated/web/social-app/src/screens/MessagesInboxScreen.tsx +0 -52
  88. package/examples/social-app/generated/web/social-app/src/screens/NotificationsScreen.tsx +0 -41
  89. package/examples/social-app/generated/web/social-app/src/screens/PostDetailScreen.tsx +0 -115
  90. package/examples/social-app/generated/web/social-app/src/screens/ProfileSelfScreen.tsx +0 -57
  91. package/examples/social-app/generated/web/social-app/src/screens/ProfileUserScreen.tsx +0 -76
  92. package/examples/social-app/generated/web/social-app/src/screens/SearchResultsScreen.tsx +0 -96
  93. package/examples/social-app/generated/web/social-app/src/screens/SettingsScreen.tsx +0 -79
  94. package/examples/social-app/generated/web/social-app/src/state/store.ts +0 -592
  95. package/examples/social-app/generated/web/social-app/src/styles.css +0 -125
  96. package/examples/social-app/generated/web/social-app/src/vite-env.d.ts +0 -1
  97. package/examples/social-app/generated/web/social-app/tsconfig.json +0 -22
  98. package/examples/social-app/generated/web/social-app/tsconfig.node.json +0 -13
  99. package/examples/social-app/generated/web/social-app/tsconfig.node.tsbuildinfo +0 -1
  100. package/examples/social-app/generated/web/social-app/tsconfig.tsbuildinfo +0 -1
  101. package/examples/social-app/generated/web/social-app/vite.config.d.ts +0 -2
  102. package/examples/social-app/generated/web/social-app/vite.config.js +0 -6
  103. package/examples/social-app/generated/web/social-app/vite.config.ts +0 -7
  104. package/examples/social-app/package.json +0 -13
  105. package/examples/social-app/take-web-screenshots.ts +0 -97
  106. package/examples/taskflow/.codex/config.toml +0 -4
  107. package/examples/taskflow/.mcp.json +0 -10
  108. package/examples/taskflow/AGENTS.md +0 -124
  109. package/examples/taskflow/CLAUDE.md +0 -124
  110. package/examples/taskflow/backend/.gitkeep +0 -1
  111. package/examples/taskflow/generated/android/TaskFlow/README.md +0 -43
  112. package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +0 -76
  113. package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +0 -1
  114. package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +0 -21
  115. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +0 -19
  116. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +0 -283
  117. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +0 -106
  118. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +0 -57
  119. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +0 -109
  120. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +0 -112
  121. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +0 -61
  122. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +0 -82
  123. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +0 -111
  124. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +0 -77
  125. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +0 -30
  126. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +0 -86
  127. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +0 -57
  128. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +0 -155
  129. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +0 -4
  130. package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +0 -5
  131. package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +0 -12
  132. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
  133. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +0 -7
  134. package/examples/taskflow/generated/android/TaskFlow/gradle.properties +0 -4
  135. package/examples/taskflow/generated/android/TaskFlow/gradlew +0 -18
  136. package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +0 -12
  137. package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +0 -18
  138. package/examples/taskflow/generated/ios/TaskFlow/README.md +0 -21
  139. package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +0 -115
  140. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +0 -24
  141. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +0 -150
  142. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +0 -220
  143. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +0 -122
  144. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +0 -21
  145. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +0 -201
  146. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +0 -48
  147. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +0 -59
  148. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +0 -63
  149. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +0 -85
  150. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +0 -219
  151. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +0 -320
  152. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +0 -41
  153. package/examples/taskflow/generated/ios/TaskFlow/project.yml +0 -26
  154. package/examples/taskflow/generated/web/TaskFlow/README.md +0 -19
  155. package/examples/taskflow/generated/web/TaskFlow/index.html +0 -12
  156. package/examples/taskflow/generated/web/TaskFlow/package-lock.json +0 -1908
  157. package/examples/taskflow/generated/web/TaskFlow/package.json +0 -24
  158. package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +0 -58
  159. package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +0 -55
  160. package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +0 -82
  161. package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +0 -191
  162. package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +0 -41
  163. package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +0 -131
  164. package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +0 -25
  165. package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +0 -39
  166. package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +0 -111
  167. package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +0 -13
  168. package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +0 -111
  169. package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +0 -82
  170. package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +0 -132
  171. package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +0 -105
  172. package/examples/taskflow/generated/web/TaskFlow/src/store.ts +0 -216
  173. package/examples/taskflow/generated/web/TaskFlow/src/styles.css +0 -617
  174. package/examples/taskflow/generated/web/TaskFlow/src/types.ts +0 -64
  175. package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +0 -78
  176. package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +0 -21
  177. package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +0 -6
  178. package/examples/todo-orbit/.codex/config.toml +0 -4
  179. package/examples/todo-orbit/.mcp.json +0 -10
  180. package/examples/todo-orbit/AGENTS.md +0 -124
  181. package/examples/todo-orbit/CLAUDE.md +0 -124
  182. package/examples/todo-orbit/backend/.gitkeep +0 -1
  183. package/examples/todo-orbit/generated/android/Todo Orbit/README.md +0 -14
  184. package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +0 -58
  185. package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +0 -1
  186. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +0 -20
  187. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +0 -14
  188. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +0 -345
  189. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +0 -231
  190. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +0 -169
  191. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +0 -8
  192. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +0 -236
  193. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +0 -193
  194. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +0 -102
  195. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +0 -347
  196. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +0 -347
  197. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +0 -59
  198. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +0 -149
  199. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +0 -155
  200. package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +0 -4
  201. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
  202. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +0 -7
  203. package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +0 -4
  204. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +0 -248
  205. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +0 -93
  206. package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +0 -18
  207. package/examples/todo-orbit/generated/ios/Todo Orbit/.screenshot-uitest/Sources/ScreenshotUITest.swift +0 -36
  208. package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +0 -29
  209. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +0 -119
  210. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +0 -119
  211. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +0 -50
  212. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +0 -204
  213. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +0 -126
  214. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +0 -70
  215. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +0 -126
  216. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +0 -61
  217. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +0 -238
  218. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +0 -94
  219. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +0 -76
  220. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +0 -364
  221. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +0 -324
  222. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +0 -400
  223. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  224. package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +0 -25
  225. package/examples/todo-orbit/generated/web/Todo Orbit/index.html +0 -16
  226. package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +0 -1087
  227. package/examples/todo-orbit/generated/web/Todo Orbit/package.json +0 -24
  228. package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +0 -2167
  229. package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +0 -13
  230. package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +0 -926
  231. package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +0 -19
  232. 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
- }