kasy-cli 1.2.1

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 (735) hide show
  1. package/README.md +56 -0
  2. package/bin/kasy.js +529 -0
  3. package/docs/cli-reference.md +251 -0
  4. package/lib/commands/add.js +777 -0
  5. package/lib/commands/check.js +240 -0
  6. package/lib/commands/codemagic.js +161 -0
  7. package/lib/commands/deploy.js +257 -0
  8. package/lib/commands/docs.js +47 -0
  9. package/lib/commands/doctor.js +227 -0
  10. package/lib/commands/features.js +36 -0
  11. package/lib/commands/ios.js +206 -0
  12. package/lib/commands/new.js +1870 -0
  13. package/lib/commands/notifications.js +207 -0
  14. package/lib/commands/remove.js +466 -0
  15. package/lib/commands/run.js +75 -0
  16. package/lib/commands/update.js +382 -0
  17. package/lib/commands/validate.js +131 -0
  18. package/lib/scaffold/CHANGELOG.json +6 -0
  19. package/lib/scaffold/backends/api/.flutter-plugins-dependencies +1 -0
  20. package/lib/scaffold/backends/api/analysis_options.yaml +5 -0
  21. package/lib/scaffold/backends/api/generator.js +76 -0
  22. package/lib/scaffold/backends/api/patch/README.md +85 -0
  23. package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +87 -0
  24. package/lib/scaffold/backends/api/patch/ios/Runner/AppDelegate.swift +61 -0
  25. package/lib/scaffold/backends/api/patch/lib/core/data/api/meta_ads_api.dart +80 -0
  26. package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +117 -0
  27. package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +148 -0
  28. package/lib/scaffold/backends/api/patch/lib/core/data/entities/json_converters.dart +35 -0
  29. package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +48 -0
  30. package/lib/scaffold/backends/api/patch/lib/core/rating/widgets/review_popup.dart +211 -0
  31. package/lib/scaffold/backends/api/patch/lib/environnements.dart +151 -0
  32. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +303 -0
  33. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +80 -0
  34. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +406 -0
  35. package/lib/scaffold/backends/api/patch/lib/features/camera/xfile_extension.dart +6 -0
  36. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/entities/feature_request_entity.dart +24 -0
  37. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/entities/feature_vote_entity.dart +21 -0
  38. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +52 -0
  39. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_vote_api.dart +60 -0
  40. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +58 -0
  41. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_message_entity.dart +29 -0
  42. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +398 -0
  43. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/device_entity.dart +39 -0
  44. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/notifications_entity.dart +23 -0
  45. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/notifications_api.dart +333 -0
  46. package/lib/scaffold/backends/api/patch/lib/features/notifications/providers/models/notification.dart +185 -0
  47. package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/entities/user_info_entity.dart +26 -0
  48. package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/user_infos_api.dart +107 -0
  49. package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +94 -0
  50. package/lib/scaffold/backends/api/patch/lib/features/onboarding/repositories/user_infos_repository.dart +29 -0
  51. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +34 -0
  52. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +66 -0
  53. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +73 -0
  54. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +173 -0
  55. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/widgets/avatar_utils.dart +33 -0
  56. package/lib/scaffold/backends/api/patch/lib/features/subscription/api/entities/subscription_entity.dart +34 -0
  57. package/lib/scaffold/backends/api/patch/lib/features/subscription/api/subscription_api.dart +49 -0
  58. package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +88 -0
  59. package/lib/scaffold/backends/api/patch/lib/main.dart +256 -0
  60. package/lib/scaffold/backends/api/patch/lib/router.dart +133 -0
  61. package/lib/scaffold/backends/api/pubspec.yaml.tpl +108 -0
  62. package/lib/scaffold/backends/firebase/deploy.js +1006 -0
  63. package/lib/scaffold/backends/firebase/generator.js +24 -0
  64. package/lib/scaffold/backends/firebase/setup-from-scratch.js +1008 -0
  65. package/lib/scaffold/backends/firebase/tokens.js +113 -0
  66. package/lib/scaffold/backends/supabase/.flutter-plugins-dependencies +1 -0
  67. package/lib/scaffold/backends/supabase/analysis_options.yaml +5 -0
  68. package/lib/scaffold/backends/supabase/config.toml +34 -0
  69. package/lib/scaffold/backends/supabase/deploy.js +485 -0
  70. package/lib/scaffold/backends/supabase/edge-functions/delete-user-account/README.md +15 -0
  71. package/lib/scaffold/backends/supabase/edge-functions/delete-user-account/index.ts +97 -0
  72. package/lib/scaffold/backends/supabase/edge-functions/llm-chat/index.ts +156 -0
  73. package/lib/scaffold/backends/supabase/edge-functions/meta-track-event/index.ts +292 -0
  74. package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/README.md +30 -0
  75. package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +488 -0
  76. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +35 -0
  77. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +314 -0
  78. package/lib/scaffold/backends/supabase/generator.js +108 -0
  79. package/lib/scaffold/backends/supabase/migrations/20240101000001_initial_schema.sql +149 -0
  80. package/lib/scaffold/backends/supabase/migrations/20240101000002_storage_bucket.sql +40 -0
  81. package/lib/scaffold/backends/supabase/migrations/20240101000005_vote_triggers.sql +34 -0
  82. package/lib/scaffold/backends/supabase/migrations/20240101000006_notification_webhook.sql +36 -0
  83. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +64 -0
  84. package/lib/scaffold/backends/supabase/migrations/20240101000008_user_feature_requests.sql +2 -0
  85. package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +22 -0
  86. package/lib/scaffold/backends/supabase/migrations/20240101000010_notification_image.sql +2 -0
  87. package/lib/scaffold/backends/supabase/patch/README.md +121 -0
  88. package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +87 -0
  89. package/lib/scaffold/backends/supabase/patch/ios/Runner/AppDelegate.swift +61 -0
  90. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/meta_ads_api.dart +75 -0
  91. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/storage_api.dart +83 -0
  92. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +143 -0
  93. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/json_converters.dart +35 -0
  94. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +48 -0
  95. package/lib/scaffold/backends/supabase/patch/lib/core/rating/widgets/review_popup.dart +211 -0
  96. package/lib/scaffold/backends/supabase/patch/lib/environnements.dart +149 -0
  97. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +546 -0
  98. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +410 -0
  99. package/lib/scaffold/backends/supabase/patch/lib/features/camera/xfile_extension.dart +6 -0
  100. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/entities/feature_request_entity.dart +25 -0
  101. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/entities/feature_vote_entity.dart +21 -0
  102. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +59 -0
  103. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_vote_api.dart +85 -0
  104. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/ui/component/add_feature_form.dart +199 -0
  105. package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +47 -0
  106. package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_message_entity.dart +30 -0
  107. package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/providers/llm_chat_notifier.dart +266 -0
  108. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +410 -0
  109. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/entities/device_entity.dart +51 -0
  110. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/entities/notifications_entity.dart +26 -0
  111. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/notifications_api.dart +317 -0
  112. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/providers/models/notification.dart +174 -0
  113. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/entities/user_info_entity.dart +26 -0
  114. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/user_infos_api.dart +90 -0
  115. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +94 -0
  116. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/repositories/user_infos_repository.dart +29 -0
  117. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +34 -0
  118. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +66 -0
  119. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +73 -0
  120. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +173 -0
  121. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/widgets/avatar_utils.dart +35 -0
  122. package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/entities/subscription_entity.dart +35 -0
  123. package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/subscription_api.dart +44 -0
  124. package/lib/scaffold/backends/supabase/patch/lib/features/subscription/shared/maybeshow_premium.dart +85 -0
  125. package/lib/scaffold/backends/supabase/patch/lib/google_auth_options.dart +23 -0
  126. package/lib/scaffold/backends/supabase/patch/lib/main.dart +288 -0
  127. package/lib/scaffold/backends/supabase/patch/lib/router.dart +133 -0
  128. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +110 -0
  129. package/lib/scaffold/backends/supabase/tokens.js +9 -0
  130. package/lib/scaffold/catalog.js +177 -0
  131. package/lib/scaffold/engine.js +230 -0
  132. package/lib/scaffold/features/README.md +152 -0
  133. package/lib/scaffold/features/analytics/lib/core/data/api/analytics_api.dart +124 -0
  134. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.es.md +35 -0
  135. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.md +35 -0
  136. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.pt.md +35 -0
  137. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.es.md +12 -0
  138. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.md +12 -0
  139. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.pt.md +12 -0
  140. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.es.md +17 -0
  141. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.md +17 -0
  142. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.pt.md +17 -0
  143. package/lib/scaffold/features/ci/.github/dependabot.yml +16 -0
  144. package/lib/scaffold/features/ci/.github/workflows/app.yml +20 -0
  145. package/lib/scaffold/features/ci/.gitlab/templates/deploy.yaml +14 -0
  146. package/lib/scaffold/features/ci/.gitlab/templates/dropbox.yaml +19 -0
  147. package/lib/scaffold/features/ci/.gitlab/templates/flutter.yaml +163 -0
  148. package/lib/scaffold/features/ci/.gitlab/templates/mailgun.yaml +28 -0
  149. package/lib/scaffold/features/ci/.gitlab-ci.yml +37 -0
  150. package/lib/scaffold/features/ci/codemagic.yaml +157 -0
  151. package/lib/scaffold/features/facebook/lib/core/data/api/tracking_api.dart +111 -0
  152. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_request_entity.dart +27 -0
  153. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_vote_entity.dart +27 -0
  154. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_request_api.dart +50 -0
  155. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_vote_api.dart +79 -0
  156. package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feature_requests.dart +48 -0
  157. package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feedback_state.dart +42 -0
  158. package/lib/scaffold/features/feedback/lib/features/feedbacks/providers/feedback_page_notifier.dart +147 -0
  159. package/lib/scaffold/features/feedback/lib/features/feedbacks/repositories/feature_request_repository.dart +95 -0
  160. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/component/add_feature_form.dart +199 -0
  161. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/feedback_page.dart +175 -0
  162. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/add_feature_button.dart +76 -0
  163. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/feature_card.dart +279 -0
  164. package/lib/scaffold/features/ios-release/.kasy/apple.env.example +8 -0
  165. package/lib/scaffold/features/ios-release/.kasy/codemagic.env.example +7 -0
  166. package/lib/scaffold/features/ios-release/docs/codemagic-release.en.md +50 -0
  167. package/lib/scaffold/features/ios-release/docs/codemagic-release.es.md +50 -0
  168. package/lib/scaffold/features/ios-release/docs/codemagic-release.pt.md +50 -0
  169. package/lib/scaffold/features/ios-release/docs/ios-release.en.md +41 -0
  170. package/lib/scaffold/features/ios-release/docs/ios-release.es.md +41 -0
  171. package/lib/scaffold/features/ios-release/docs/ios-release.pt.md +41 -0
  172. package/lib/scaffold/features/ios-release/scripts/bump-ios-version.js +38 -0
  173. package/lib/scaffold/features/ios-release/scripts/release-ios.sh +137 -0
  174. package/lib/scaffold/features/llm_chat/lib/features/llm_chat/llm_chat_page.dart +301 -0
  175. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/providers/reminder_notifier.dart +81 -0
  176. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/repositories/reminder_preferences.dart +76 -0
  177. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/ui/reminder_page.dart +282 -0
  178. package/lib/scaffold/features/onboarding/lib/features/onboarding/api/entities/user_info_entity.dart +24 -0
  179. package/lib/scaffold/features/onboarding/lib/features/onboarding/api/user_infos_api.dart +71 -0
  180. package/lib/scaffold/features/onboarding/lib/features/onboarding/models/user_info.dart +92 -0
  181. package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_model.dart +15 -0
  182. package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_provider.dart +78 -0
  183. package/lib/scaffold/features/onboarding/lib/features/onboarding/repositories/user_infos_repository.dart +29 -0
  184. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/animations/page_transitions.dart +30 -0
  185. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_att_setup.dart +66 -0
  186. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_features.dart +72 -0
  187. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_loader.dart +92 -0
  188. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +73 -0
  189. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_questions.dart +89 -0
  190. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/onboarding_page.dart +94 -0
  191. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_background.dart +80 -0
  192. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_feature.dart +139 -0
  193. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +110 -0
  194. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_progress.dart +84 -0
  195. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +173 -0
  196. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_reassurance.dart +45 -0
  197. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +77 -0
  198. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +392 -0
  199. package/lib/scaffold/features/revenuecat/lib/core/data/api/tracking_api.dart +116 -0
  200. package/lib/scaffold/features/revenuecat/lib/core/data/models/subscription.dart +322 -0
  201. package/lib/scaffold/features/revenuecat/lib/core/home_widgets/home_widget_background_task.dart +41 -0
  202. package/lib/scaffold/features/revenuecat/lib/core/states/user_state_notifier.dart +305 -0
  203. package/lib/scaffold/features/widget/lib/core/home_widgets/home_widget_background_task.dart +41 -0
  204. package/lib/scaffold/features/widget/lib/core/home_widgets/home_widget_mywidget_service.dart +57 -0
  205. package/lib/scaffold/features/widget/lib/core/home_widgets/home_widget_service.dart +54 -0
  206. package/lib/scaffold/features/widget/lib/features/settings/ui/components/admin/admin_home_widgets.dart +32 -0
  207. package/lib/scaffold/generate.js +370 -0
  208. package/lib/scaffold/patch.js +395 -0
  209. package/lib/scaffold/shared/backend-config.js +74 -0
  210. package/lib/scaffold/shared/fcm-service-account.js +126 -0
  211. package/lib/scaffold/shared/generator-utils.js +1664 -0
  212. package/lib/scaffold/shared/post-build.js +809 -0
  213. package/lib/scaffold/shared/template-strings.js +112 -0
  214. package/lib/utils/apple-release.js +273 -0
  215. package/lib/utils/checks.js +319 -0
  216. package/lib/utils/codemagic-release.js +127 -0
  217. package/lib/utils/fs.js +122 -0
  218. package/lib/utils/i18n.js +2123 -0
  219. package/lib/utils/license-gate.js +72 -0
  220. package/lib/utils/license.js +64 -0
  221. package/lib/utils/prompts.js +213 -0
  222. package/lib/utils/updates.js +97 -0
  223. package/package.json +55 -0
  224. package/templates/firebase/.env.example +24 -0
  225. package/templates/firebase/.firebaserc +5 -0
  226. package/templates/firebase/.kasy/apple.env.example +8 -0
  227. package/templates/firebase/.kasy/codemagic.env.example +7 -0
  228. package/templates/firebase/.metadata +30 -0
  229. package/templates/firebase/.windsurfrules +95 -0
  230. package/templates/firebase/Makefile +30 -0
  231. package/templates/firebase/README.en.md +180 -0
  232. package/templates/firebase/README.es.md +180 -0
  233. package/templates/firebase/README.md +180 -0
  234. package/templates/firebase/analysis_options.yaml +37 -0
  235. package/templates/firebase/android/app/build.gradle.kts +80 -0
  236. package/templates/firebase/android/app/src/debug/AndroidManifest.xml +7 -0
  237. package/templates/firebase/android/app/src/main/AndroidManifest.xml +93 -0
  238. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +36 -0
  239. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +50 -0
  240. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidgetReceiver.kt +7 -0
  241. package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
  242. package/templates/firebase/android/app/src/main/res/drawable/launch_background.xml +9 -0
  243. package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
  244. package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
  245. package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
  246. package/templates/firebase/android/app/src/main/res/drawable-v21/launch_background.xml +9 -0
  247. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
  248. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
  249. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
  250. package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  251. package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  252. package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  253. package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  254. package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  255. package/templates/firebase/android/app/src/main/res/values/strings.xml +6 -0
  256. package/templates/firebase/android/app/src/main/res/values/styles.xml +22 -0
  257. package/templates/firebase/android/app/src/main/res/values-night/styles.xml +22 -0
  258. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +20 -0
  259. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +20 -0
  260. package/templates/firebase/android/app/src/main/res/xml/locales_config.xml +7 -0
  261. package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +11 -0
  262. package/templates/firebase/android/app/src/profile/AndroidManifest.xml +7 -0
  263. package/templates/firebase/android/build.gradle.kts +28 -0
  264. package/templates/firebase/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  265. package/templates/firebase/android/gradle.properties +2 -0
  266. package/templates/firebase/android/settings.gradle.kts +29 -0
  267. package/templates/firebase/assets/icons/apple.png +0 -0
  268. package/templates/firebase/assets/icons/facebook.png +0 -0
  269. package/templates/firebase/assets/icons/google.png +0 -0
  270. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  271. package/templates/firebase/assets/images/app_icon.png +0 -0
  272. package/templates/firebase/assets/images/empty_notifications.jpg +0 -0
  273. package/templates/firebase/assets/images/icon.png +0 -0
  274. package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
  275. package/templates/firebase/assets/images/onboarding/img1.jpg +0 -0
  276. package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
  277. package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
  278. package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
  279. package/templates/firebase/assets/images/onboarding/onboarding.png +0 -0
  280. package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
  281. package/templates/firebase/assets/images/premium-bg.jpg +0 -0
  282. package/templates/firebase/assets/images/premium-switch-header.png +0 -0
  283. package/templates/firebase/assets/images/review.png +0 -0
  284. package/templates/firebase/assets/images/splashscreen.png +0 -0
  285. package/templates/firebase/assets/images/update.png +0 -0
  286. package/templates/firebase/build.yaml +13 -0
  287. package/templates/firebase/devtools_options.yaml +3 -0
  288. package/templates/firebase/docs/auth-setup.en.md +145 -0
  289. package/templates/firebase/docs/auth-setup.es.md +145 -0
  290. package/templates/firebase/docs/auth-setup.pt.md +145 -0
  291. package/templates/firebase/docs/codemagic-release.en.md +50 -0
  292. package/templates/firebase/docs/codemagic-release.es.md +50 -0
  293. package/templates/firebase/docs/codemagic-release.pt.md +50 -0
  294. package/templates/firebase/docs/ios-release.en.md +41 -0
  295. package/templates/firebase/docs/ios-release.es.md +41 -0
  296. package/templates/firebase/docs/ios-release.pt.md +54 -0
  297. package/templates/firebase/docs/navigation-transitions.md +80 -0
  298. package/templates/firebase/docs/revenuecat-setup.es.md +341 -0
  299. package/templates/firebase/docs/revenuecat-setup.pt.md +341 -0
  300. package/templates/firebase/firebase.json +1 -0
  301. package/templates/firebase/firestore.indexes.json +25 -0
  302. package/templates/firebase/firestore.rules +51 -0
  303. package/templates/firebase/functions/.eslintrc.js +45 -0
  304. package/templates/firebase/functions/jest.config.js +16 -0
  305. package/templates/firebase/functions/package-lock.json +6876 -0
  306. package/templates/firebase/functions/package.json +33 -0
  307. package/templates/firebase/functions/src/authentication/functions.ts +30 -0
  308. package/templates/firebase/functions/src/authentication/triggers.ts +103 -0
  309. package/templates/firebase/functions/src/core/api/meta_ads_api.ts +390 -0
  310. package/templates/firebase/functions/src/core/data/entities/entity_utils.ts +87 -0
  311. package/templates/firebase/functions/src/core/data/entities/notification_entity.ts +74 -0
  312. package/templates/firebase/functions/src/core/data/entities/page.ts +4 -0
  313. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +76 -0
  314. package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +64 -0
  315. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +43 -0
  316. package/templates/firebase/functions/src/core/data/repositories/repositories.ts +16 -0
  317. package/templates/firebase/functions/src/core/data/repositories/subscription_repository.ts +42 -0
  318. package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +31 -0
  319. package/templates/firebase/functions/src/core/data/repositories/user_notifications_repository.ts +58 -0
  320. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +84 -0
  321. package/templates/firebase/functions/src/core/logger/logger.ts +13 -0
  322. package/templates/firebase/functions/src/core/storage/storage_manager.ts +52 -0
  323. package/templates/firebase/functions/src/core/utils/async_utils.ts +5 -0
  324. package/templates/firebase/functions/src/feature_requests/triggers.ts +3 -0
  325. package/templates/firebase/functions/src/index.ts +24 -0
  326. package/templates/firebase/functions/src/llm_chat/index.ts +181 -0
  327. package/templates/firebase/functions/src/notifications/admin_functions.ts +91 -0
  328. package/templates/firebase/functions/src/notifications/models/notification.ts +61 -0
  329. package/templates/firebase/functions/src/notifications/notifications_api.ts +147 -0
  330. package/templates/firebase/functions/src/notifications/triggers.ts +102 -0
  331. package/templates/firebase/functions/src/subscriptions/models/revenuecat_events.ts +162 -0
  332. package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +18 -0
  333. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +173 -0
  334. package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +135 -0
  335. package/templates/firebase/functions/src/subscriptions/triggers.ts +35 -0
  336. package/templates/firebase/functions/src/test/core/data/entities/entity_utils.spec.ts +44 -0
  337. package/templates/firebase/functions/tsconfig.dev.json +5 -0
  338. package/templates/firebase/functions/tsconfig.json +18 -0
  339. package/templates/firebase/home_widget_config.json +30 -0
  340. package/templates/firebase/ios/Flutter/AppFrameworkInfo.plist +24 -0
  341. package/templates/firebase/ios/Flutter/Debug.xcconfig +2 -0
  342. package/templates/firebase/ios/Flutter/Profile.xcconfig +2 -0
  343. package/templates/firebase/ios/Flutter/Release.xcconfig +2 -0
  344. package/templates/firebase/ios/HomeWidgetExtension/HomeWidgetBundle.swift +13 -0
  345. package/templates/firebase/ios/HomeWidgetExtension/Info.plist +29 -0
  346. package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +86 -0
  347. package/templates/firebase/ios/HomeWidgetExtension.entitlements +10 -0
  348. package/templates/firebase/ios/NotificationService/Info.plist +31 -0
  349. package/templates/firebase/ios/NotificationService/NotificationService.swift +76 -0
  350. package/templates/firebase/ios/Podfile +111 -0
  351. package/templates/firebase/ios/Runner/AppDelegate.swift +75 -0
  352. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +1 -0
  353. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  354. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  355. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  356. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  357. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  358. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  359. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  360. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  361. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  362. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  363. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
  364. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
  365. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
  366. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
  367. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  368. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  369. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
  370. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
  371. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  372. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  373. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  374. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json +21 -0
  375. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
  376. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
  377. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  378. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  379. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  380. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
  381. package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +44 -0
  382. package/templates/firebase/ios/Runner/Base.lproj/Main.storyboard +26 -0
  383. package/templates/firebase/ios/Runner/Info.plist +132 -0
  384. package/templates/firebase/ios/Runner/Runner-Bridging-Header.h +1 -0
  385. package/templates/firebase/ios/Runner/Runner.entitlements +14 -0
  386. package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +10 -0
  387. package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +10 -0
  388. package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +10 -0
  389. package/templates/firebase/ios/Runner.xcodeproj/project.pbxproj +1108 -0
  390. package/templates/firebase/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  391. package/templates/firebase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  392. package/templates/firebase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  393. package/templates/firebase/ios/Runner.xcodeproj/xcshareddata/xcschemes/NotificationService.xcscheme +58 -0
  394. package/templates/firebase/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
  395. package/templates/firebase/ios/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  396. package/templates/firebase/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  397. package/templates/firebase/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  398. package/templates/firebase/ios/RunnerTests/RunnerTests.swift +12 -0
  399. package/templates/firebase/lib/components/components.dart +27 -0
  400. package/templates/firebase/lib/components/kasy_accordion.dart +384 -0
  401. package/templates/firebase/lib/components/kasy_alert.dart +266 -0
  402. package/templates/firebase/lib/components/kasy_app_bar.dart +536 -0
  403. package/templates/firebase/lib/components/kasy_avatar.dart +549 -0
  404. package/templates/firebase/lib/components/kasy_avatar_presets.dart +104 -0
  405. package/templates/firebase/lib/components/kasy_badge.dart +278 -0
  406. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +338 -0
  407. package/templates/firebase/lib/components/kasy_button.dart +926 -0
  408. package/templates/firebase/lib/components/kasy_card.dart +128 -0
  409. package/templates/firebase/lib/components/kasy_checkbox.dart +429 -0
  410. package/templates/firebase/lib/components/kasy_chip.dart +83 -0
  411. package/templates/firebase/lib/components/kasy_dialog.dart +789 -0
  412. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +233 -0
  413. package/templates/firebase/lib/components/kasy_skeleton.dart +225 -0
  414. package/templates/firebase/lib/components/kasy_text_area.dart +392 -0
  415. package/templates/firebase/lib/components/kasy_text_field.dart +591 -0
  416. package/templates/firebase/lib/components/kasy_text_field_otp.dart +532 -0
  417. package/templates/firebase/lib/components/kasy_toast.dart +803 -0
  418. package/templates/firebase/lib/core/ads/ads_provider.dart +136 -0
  419. package/templates/firebase/lib/core/ads/ads_state.dart +15 -0
  420. package/templates/firebase/lib/core/animations/bottomfade_anim.dart +37 -0
  421. package/templates/firebase/lib/core/animations/movefade_anim.dart +34 -0
  422. package/templates/firebase/lib/core/animations/slideright_anim.dart +38 -0
  423. package/templates/firebase/lib/core/bottom_menu/bart_inner_paths.dart +5 -0
  424. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +127 -0
  425. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +281 -0
  426. package/templates/firebase/lib/core/bottom_menu/kasy_bart_navigation.dart +22 -0
  427. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +127 -0
  428. package/templates/firebase/lib/core/bottom_menu/notification_bottom_item.dart +48 -0
  429. package/templates/firebase/lib/core/config/app_env.dart +100 -0
  430. package/templates/firebase/lib/core/config/features.dart +15 -0
  431. package/templates/firebase/lib/core/data/api/analytics_api.dart +124 -0
  432. package/templates/firebase/lib/core/data/api/base_api_exceptions.dart +28 -0
  433. package/templates/firebase/lib/core/data/api/firestore_retry.dart +29 -0
  434. package/templates/firebase/lib/core/data/api/http_client.dart +38 -0
  435. package/templates/firebase/lib/core/data/api/image.dart +52 -0
  436. package/templates/firebase/lib/core/data/api/remote_config_api.dart +170 -0
  437. package/templates/firebase/lib/core/data/api/storage_api.dart +115 -0
  438. package/templates/firebase/lib/core/data/api/tracking_api.dart +119 -0
  439. package/templates/firebase/lib/core/data/api/user_api.dart +115 -0
  440. package/templates/firebase/lib/core/data/entities/json_converters.dart +39 -0
  441. package/templates/firebase/lib/core/data/entities/upload_result.dart +48 -0
  442. package/templates/firebase/lib/core/data/entities/user_entity.dart +54 -0
  443. package/templates/firebase/lib/core/data/models/pageable.dart +12 -0
  444. package/templates/firebase/lib/core/data/models/subscription.dart +322 -0
  445. package/templates/firebase/lib/core/data/models/user.dart +107 -0
  446. package/templates/firebase/lib/core/data/repositories/user_repository.dart +130 -0
  447. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +464 -0
  448. package/templates/firebase/lib/core/dev_inspector/dev_inspector_highlight.dart +88 -0
  449. package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +70 -0
  450. package/templates/firebase/lib/core/dev_inspector/dev_inspector_panel.dart +204 -0
  451. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +321 -0
  452. package/templates/firebase/lib/core/extensions/date.ext.dart +32 -0
  453. package/templates/firebase/lib/core/guards/authenticated_guard.dart +37 -0
  454. package/templates/firebase/lib/core/guards/guard.dart +68 -0
  455. package/templates/firebase/lib/core/guards/user_info_guard.dart +55 -0
  456. package/templates/firebase/lib/core/haptics/haptic_feedback_notifier.dart +19 -0
  457. package/templates/firebase/lib/core/haptics/kasy_haptics.dart +27 -0
  458. package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +41 -0
  459. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +57 -0
  460. package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +54 -0
  461. package/templates/firebase/lib/core/icons/kasy_icons.dart +86 -0
  462. package/templates/firebase/lib/core/initializer/models/run_state.dart +14 -0
  463. package/templates/firebase/lib/core/initializer/onstart_service.dart +38 -0
  464. package/templates/firebase/lib/core/initializer/onstart_widget.dart +83 -0
  465. package/templates/firebase/lib/core/initializer/pending_notification_handler.dart +48 -0
  466. package/templates/firebase/lib/core/keyboard_fix/keyboard_flicker_fix.dart +164 -0
  467. package/templates/firebase/lib/core/navigation/kasy_fade_page_transitions_builder.dart +34 -0
  468. package/templates/firebase/lib/core/navigation/kasy_material_page_route.dart +41 -0
  469. package/templates/firebase/lib/core/navigation/kasy_navigation_config.dart +29 -0
  470. package/templates/firebase/lib/core/navigation/kasy_page_transition.dart +40 -0
  471. package/templates/firebase/lib/core/navigation/kasy_route_transition.dart +68 -0
  472. package/templates/firebase/lib/core/navigation/kasy_transition_kind.dart +17 -0
  473. package/templates/firebase/lib/core/navigation/navigation.dart +6 -0
  474. package/templates/firebase/lib/core/rating/api/rating_api.dart +99 -0
  475. package/templates/firebase/lib/core/rating/models/rating.dart +52 -0
  476. package/templates/firebase/lib/core/rating/models/review.dart +70 -0
  477. package/templates/firebase/lib/core/rating/providers/rating_repository.dart +75 -0
  478. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +190 -0
  479. package/templates/firebase/lib/core/rating/widgets/rate_popup.dart +54 -0
  480. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +175 -0
  481. package/templates/firebase/lib/core/security/biometric_copy_slot.dart +20 -0
  482. package/templates/firebase/lib/core/security/biometric_guard.dart +114 -0
  483. package/templates/firebase/lib/core/security/biometric_preference_notifier.dart +17 -0
  484. package/templates/firebase/lib/core/security/biometric_service.dart +79 -0
  485. package/templates/firebase/lib/core/security/biometric_ui_bundle.dart +136 -0
  486. package/templates/firebase/lib/core/security/secured_storage.dart +46 -0
  487. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +70 -0
  488. package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +2021 -0
  489. package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +88 -0
  490. package/templates/firebase/lib/core/states/components/maybe_ask_rating.dart +49 -0
  491. package/templates/firebase/lib/core/states/components/maybe_show_update_bottom_sheet.dart +86 -0
  492. package/templates/firebase/lib/core/states/components/maybeshow_component.dart +110 -0
  493. package/templates/firebase/lib/core/states/events_dispatcher.dart +103 -0
  494. package/templates/firebase/lib/core/states/models/event_model.dart +118 -0
  495. package/templates/firebase/lib/core/states/models/user_state.dart +22 -0
  496. package/templates/firebase/lib/core/states/notifications_dispatcher.dart +81 -0
  497. package/templates/firebase/lib/core/states/translations.dart +11 -0
  498. package/templates/firebase/lib/core/states/user_state_notifier.dart +321 -0
  499. package/templates/firebase/lib/core/theme/colors.dart +262 -0
  500. package/templates/firebase/lib/core/theme/extensions/theme_extension.dart +48 -0
  501. package/templates/firebase/lib/core/theme/providers/kasy_theme.dart +76 -0
  502. package/templates/firebase/lib/core/theme/providers/theme_provider.dart +212 -0
  503. package/templates/firebase/lib/core/theme/radius.dart +33 -0
  504. package/templates/firebase/lib/core/theme/shadows.dart +64 -0
  505. package/templates/firebase/lib/core/theme/spacing.dart +44 -0
  506. package/templates/firebase/lib/core/theme/texts.dart +176 -0
  507. package/templates/firebase/lib/core/theme/theme.dart +24 -0
  508. package/templates/firebase/lib/core/theme/theme_data/theme_data.dart +32 -0
  509. package/templates/firebase/lib/core/theme/theme_data/theme_data_factory.dart +15 -0
  510. package/templates/firebase/lib/core/theme/universal_theme.dart +210 -0
  511. package/templates/firebase/lib/core/toast/toast_service.dart +55 -0
  512. package/templates/firebase/lib/core/ui/app_dialog.dart +26 -0
  513. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +1126 -0
  514. package/templates/firebase/lib/core/widgets/debouncer.dart +31 -0
  515. package/templates/firebase/lib/core/widgets/kasy_hover.dart +166 -0
  516. package/templates/firebase/lib/core/widgets/kasy_pressable.dart +2 -0
  517. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +161 -0
  518. package/templates/firebase/lib/core/widgets/kasy_scroll_behavior.dart +17 -0
  519. package/templates/firebase/lib/core/widgets/keyboard_visibility.dart +75 -0
  520. package/templates/firebase/lib/core/widgets/page_background.dart +71 -0
  521. package/templates/firebase/lib/core/widgets/page_not_found.dart +15 -0
  522. package/templates/firebase/lib/core/widgets/responsive_layout.dart +215 -0
  523. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +192 -0
  524. package/templates/firebase/lib/environnements.dart +160 -0
  525. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +528 -0
  526. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +84 -0
  527. package/templates/firebase/lib/features/authentication/providers/models/email.dart +36 -0
  528. package/templates/firebase/lib/features/authentication/providers/models/password.dart +37 -0
  529. package/templates/firebase/lib/features/authentication/providers/models/phone_signin_state.dart +21 -0
  530. package/templates/firebase/lib/features/authentication/providers/models/recover_state.dart +21 -0
  531. package/templates/firebase/lib/features/authentication/providers/models/signin_state.dart +20 -0
  532. package/templates/firebase/lib/features/authentication/providers/models/signup_state.dart +20 -0
  533. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +132 -0
  534. package/templates/firebase/lib/features/authentication/providers/recover_provider.dart +51 -0
  535. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +166 -0
  536. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +67 -0
  537. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +399 -0
  538. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +85 -0
  539. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +19 -0
  540. package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +19 -0
  541. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +32 -0
  542. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +100 -0
  543. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +109 -0
  544. package/templates/firebase/lib/features/authentication/ui/phone_auth_page.dart +60 -0
  545. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +101 -0
  546. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +321 -0
  547. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +310 -0
  548. package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +35 -0
  549. package/templates/firebase/lib/features/authentication/ui/widgets/auth_page_back_button.dart +149 -0
  550. package/templates/firebase/lib/features/authentication/ui/widgets/otp_input.dart +194 -0
  551. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +62 -0
  552. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +73 -0
  553. package/templates/firebase/lib/features/authentication/ui/widgets/social_separator.dart +26 -0
  554. package/templates/firebase/lib/features/dev/keyboard_test_page.dart +93 -0
  555. package/templates/firebase/lib/features/feedbacks/api/entities/feature_request_entity.dart +27 -0
  556. package/templates/firebase/lib/features/feedbacks/api/entities/feature_vote_entity.dart +27 -0
  557. package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +51 -0
  558. package/templates/firebase/lib/features/feedbacks/api/feature_vote_api.dart +79 -0
  559. package/templates/firebase/lib/features/feedbacks/models/feature_requests.dart +48 -0
  560. package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +43 -0
  561. package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +147 -0
  562. package/templates/firebase/lib/features/feedbacks/repositories/feature_request_repository.dart +92 -0
  563. package/templates/firebase/lib/features/feedbacks/ui/component/add_feature_form.dart +112 -0
  564. package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +184 -0
  565. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +66 -0
  566. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +266 -0
  567. package/templates/firebase/lib/features/home/design_system_page.dart +552 -0
  568. package/templates/firebase/lib/features/home/home_components_page.dart +314 -0
  569. package/templates/firebase/lib/features/home/home_components_preview_page.dart +791 -0
  570. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +7712 -0
  571. package/templates/firebase/lib/features/home/home_features_page.dart +207 -0
  572. package/templates/firebase/lib/features/home/home_page.dart +348 -0
  573. package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +50 -0
  574. package/templates/firebase/lib/features/llm_chat/api/llm_chat_message_entity.dart +35 -0
  575. package/templates/firebase/lib/features/llm_chat/llm_chat_page.dart +352 -0
  576. package/templates/firebase/lib/features/llm_chat/providers/llm_chat_notifier.dart +315 -0
  577. package/templates/firebase/lib/features/llm_chat/ui/widgets/llm_chat_avatars.dart +148 -0
  578. package/templates/firebase/lib/features/llm_chat/ui/widgets/llm_chat_composer.dart +124 -0
  579. package/templates/firebase/lib/features/local_reminder/providers/reminder_notifier.dart +81 -0
  580. package/templates/firebase/lib/features/local_reminder/repositories/reminder_preferences.dart +76 -0
  581. package/templates/firebase/lib/features/local_reminder/ui/reminder_page.dart +288 -0
  582. package/templates/firebase/lib/features/notifications/api/device_api.dart +433 -0
  583. package/templates/firebase/lib/features/notifications/api/entities/device_entity.dart +62 -0
  584. package/templates/firebase/lib/features/notifications/api/entities/notifications_entity.dart +35 -0
  585. package/templates/firebase/lib/features/notifications/api/local_notifier.dart +409 -0
  586. package/templates/firebase/lib/features/notifications/api/notifications_api.dart +319 -0
  587. package/templates/firebase/lib/features/notifications/providers/models/device.dart +27 -0
  588. package/templates/firebase/lib/features/notifications/providers/models/notification.dart +196 -0
  589. package/templates/firebase/lib/features/notifications/providers/models/notification_list.dart +38 -0
  590. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +120 -0
  591. package/templates/firebase/lib/features/notifications/repositories/background_notification_handler.dart +13 -0
  592. package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +137 -0
  593. package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +214 -0
  594. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +75 -0
  595. package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +144 -0
  596. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +169 -0
  597. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +46 -0
  598. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +32 -0
  599. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +106 -0
  600. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +202 -0
  601. package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +84 -0
  602. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +73 -0
  603. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +362 -0
  604. package/templates/firebase/lib/features/onboarding/api/entities/user_info_entity.dart +24 -0
  605. package/templates/firebase/lib/features/onboarding/api/user_infos_api.dart +71 -0
  606. package/templates/firebase/lib/features/onboarding/models/user_info.dart +92 -0
  607. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +15 -0
  608. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +78 -0
  609. package/templates/firebase/lib/features/onboarding/repositories/user_infos_repository.dart +29 -0
  610. package/templates/firebase/lib/features/onboarding/ui/animations/page_transitions.dart +33 -0
  611. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +66 -0
  612. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +76 -0
  613. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +92 -0
  614. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +76 -0
  615. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +89 -0
  616. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +108 -0
  617. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +80 -0
  618. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +145 -0
  619. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +125 -0
  620. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +84 -0
  621. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +173 -0
  622. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_reassurance.dart +44 -0
  623. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +77 -0
  624. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +392 -0
  625. package/templates/firebase/lib/features/settings/settings_page.dart +460 -0
  626. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +301 -0
  627. package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart +38 -0
  628. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +53 -0
  629. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +22 -0
  630. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +366 -0
  631. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notifier.dart +40 -0
  632. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +430 -0
  633. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +28 -0
  634. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +163 -0
  635. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +59 -0
  636. package/templates/firebase/lib/features/settings/ui/widgets/avatar_utils.dart +51 -0
  637. package/templates/firebase/lib/features/settings/ui/widgets/round_progress.dart +151 -0
  638. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +52 -0
  639. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +163 -0
  640. package/templates/firebase/lib/features/subscription/api/entities/subscription_entity.dart +40 -0
  641. package/templates/firebase/lib/features/subscription/api/inapp_subscription_api.dart +389 -0
  642. package/templates/firebase/lib/features/subscription/api/subscription_api.dart +39 -0
  643. package/templates/firebase/lib/features/subscription/providers/models/premium_state.dart +35 -0
  644. package/templates/firebase/lib/features/subscription/providers/premium_page_provider.dart +259 -0
  645. package/templates/firebase/lib/features/subscription/repositories/subscription_repository.dart +177 -0
  646. package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +85 -0
  647. package/templates/firebase/lib/features/subscription/ui/component/active_premium_content.dart +171 -0
  648. package/templates/firebase/lib/features/subscription/ui/component/paywall_minimal.dart +146 -0
  649. package/templates/firebase/lib/features/subscription/ui/component/paywall_row.dart +332 -0
  650. package/templates/firebase/lib/features/subscription/ui/component/paywall_with_switch.dart +207 -0
  651. package/templates/firebase/lib/features/subscription/ui/component/premium_content.dart +157 -0
  652. package/templates/firebase/lib/features/subscription/ui/component/premium_page_factory.dart +161 -0
  653. package/templates/firebase/lib/features/subscription/ui/premium_page.dart +139 -0
  654. package/templates/firebase/lib/features/subscription/ui/widgets/comparison_table.dart +346 -0
  655. package/templates/firebase/lib/features/subscription/ui/widgets/feature_line.dart +131 -0
  656. package/templates/firebase/lib/features/subscription/ui/widgets/paywall_empty_state.dart +68 -0
  657. package/templates/firebase/lib/features/subscription/ui/widgets/premium_background.dart +63 -0
  658. package/templates/firebase/lib/features/subscription/ui/widgets/premium_background_gradient.dart +53 -0
  659. package/templates/firebase/lib/features/subscription/ui/widgets/premium_banner.dart +80 -0
  660. package/templates/firebase/lib/features/subscription/ui/widgets/premium_bottom_menu.dart +102 -0
  661. package/templates/firebase/lib/features/subscription/ui/widgets/premium_card.dart +26 -0
  662. package/templates/firebase/lib/features/subscription/ui/widgets/premium_close_button.dart +61 -0
  663. package/templates/firebase/lib/features/subscription/ui/widgets/premium_feature.dart +39 -0
  664. package/templates/firebase/lib/features/subscription/ui/widgets/selectable_col.dart +377 -0
  665. package/templates/firebase/lib/features/subscription/ui/widgets/selectable_row.dart +436 -0
  666. package/templates/firebase/lib/features/subscription/ui/widgets/trial_switcher.dart +85 -0
  667. package/templates/firebase/lib/features/subscription/ui/widgets/unsubscribe_feedback_popup.dart +123 -0
  668. package/templates/firebase/lib/firebase_options.dart +75 -0
  669. package/templates/firebase/lib/google_auth_options.dart +10 -0
  670. package/templates/firebase/lib/i18n/app_locale_display.dart +17 -0
  671. package/templates/firebase/lib/i18n/en.i18n.json +514 -0
  672. package/templates/firebase/lib/i18n/es.i18n.json +514 -0
  673. package/templates/firebase/lib/i18n/pt.i18n.json +514 -0
  674. package/templates/firebase/lib/i18n/widgets/locale_code_badge.dart +47 -0
  675. package/templates/firebase/lib/main.dart +263 -0
  676. package/templates/firebase/lib/router.dart +226 -0
  677. package/templates/firebase/login-redesign-preview.png +0 -0
  678. package/templates/firebase/pubspec.yaml +177 -0
  679. package/templates/firebase/scripts/bump-ios-version.js +38 -0
  680. package/templates/firebase/scripts/release-ios.sh +137 -0
  681. package/templates/firebase/slang.yaml +7 -0
  682. package/templates/firebase/storage.rules +12 -0
  683. package/templates/firebase/test/core/data/api/analytics_api_fake.dart +25 -0
  684. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +45 -0
  685. package/templates/firebase/test/core/data/api/storage_api_fake.dart +28 -0
  686. package/templates/firebase/test/core/data/models/subscription_test.dart +42 -0
  687. package/templates/firebase/test/core/data/repositories/user_repository_test.dart +56 -0
  688. package/templates/firebase/test/core/guards/guard_test.dart +78 -0
  689. package/templates/firebase/test/core/navigation/kasy_page_transition_test.dart +30 -0
  690. package/templates/firebase/test/core/onstart_service_test.dart +18 -0
  691. package/templates/firebase/test/core/rating/rating_api_fake.dart +58 -0
  692. package/templates/firebase/test/core/rating/rating_banner_test.dart +117 -0
  693. package/templates/firebase/test/core/rating/rating_test.dart +60 -0
  694. package/templates/firebase/test/core/security/secured_storage_fake.dart +33 -0
  695. package/templates/firebase/test/core/states/event_dispatcher_test.dart +269 -0
  696. package/templates/firebase/test/core/states/user_state_notifier_test.dart +244 -0
  697. package/templates/firebase/test/core/widgets/responsive_layout_test.dart +50 -0
  698. package/templates/firebase/test/device_test_utils.dart +98 -0
  699. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +169 -0
  700. package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +86 -0
  701. package/templates/firebase/test/features/authentication/lost_password_page_test.dart +59 -0
  702. package/templates/firebase/test/features/authentication/repositories/authentication_repository_test.dart +88 -0
  703. package/templates/firebase/test/features/authentication/signin_page_test.dart +133 -0
  704. package/templates/firebase/test/features/authentication/signup_page_test.dart +136 -0
  705. package/templates/firebase/test/features/notifications/data/device_api_fake.dart +66 -0
  706. package/templates/firebase/test/features/notifications/data/fake_facebook_api.dart +40 -0
  707. package/templates/firebase/test/features/notifications/data/local_notifier_fake.dart +59 -0
  708. package/templates/firebase/test/features/notifications/data/notifications_api_fake.dart +120 -0
  709. package/templates/firebase/test/features/notifications/data/notifications_settings_fake.dart +34 -0
  710. package/templates/firebase/test/features/notifications/providers/device_repository_test.dart +68 -0
  711. package/templates/firebase/test/features/notifications/providers/notifications_repository_test.dart +74 -0
  712. package/templates/firebase/test/features/notifications/ui/notifications_page_test.dart +69 -0
  713. package/templates/firebase/test/features/subscription/api/fake_inapp_subscription_api.dart +85 -0
  714. package/templates/firebase/test/features/subscription/api/fake_revenuecat_product.dart +186 -0
  715. package/templates/firebase/test/features/subscription/api/fake_subscription_api.dart +11 -0
  716. package/templates/firebase/test/features/subscription/subscription_page_test.dart +119 -0
  717. package/templates/firebase/test/firebase_test_utils.dart +25 -0
  718. package/templates/firebase/test/test_utils.dart +272 -0
  719. package/templates/firebase/web/favicon.png +0 -0
  720. package/templates/firebase/web/firebase-messaging-sw.js +27 -0
  721. package/templates/firebase/web/icons/Icon-192.png +0 -0
  722. package/templates/firebase/web/icons/Icon-512.png +0 -0
  723. package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
  724. package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
  725. package/templates/firebase/web/index.html +110 -0
  726. package/templates/firebase/web/local_notifications.js +31 -0
  727. package/templates/firebase/web/manifest.json +35 -0
  728. package/templates/firebase/web/splash/img/dark-1x.png +0 -0
  729. package/templates/firebase/web/splash/img/dark-2x.png +0 -0
  730. package/templates/firebase/web/splash/img/dark-3x.png +0 -0
  731. package/templates/firebase/web/splash/img/dark-4x.png +0 -0
  732. package/templates/firebase/web/splash/img/light-1x.png +0 -0
  733. package/templates/firebase/web/splash/img/light-2x.png +0 -0
  734. package/templates/firebase/web/splash/img/light-3x.png +0 -0
  735. package/templates/firebase/web/splash/img/light-4x.png +0 -0
@@ -0,0 +1,2021 @@
1
+ import 'dart:math' show min;
2
+
3
+ import 'package:bart/bart/bart_model.dart';
4
+ import 'package:bart/bart/widgets/side_bar/custom_sidebar.dart';
5
+ import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform;
6
+ import 'package:flutter/material.dart';
7
+ import 'package:flutter/services.dart';
8
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
9
+ import 'package:kasy_kit/components/components.dart';
10
+ import 'package:kasy_kit/core/data/models/user.dart';
11
+ import 'package:kasy_kit/core/states/user_state_notifier.dart';
12
+ import 'package:kasy_kit/core/theme/theme.dart';
13
+ import 'package:kasy_kit/core/widgets/kasy_hover.dart';
14
+ import 'package:kasy_kit/features/notifications/providers/notifications_provider.dart';
15
+ import 'package:kasy_kit/features/notifications/repositories/notifications_repository.dart';
16
+
17
+ const double _kWidth = 260.0;
18
+ const double _kItemHeight = 44.0;
19
+ const double _kItemRadius = 8.0;
20
+
21
+ /// Entry point — pass to [CustomSideBarOptions.sideBarBuilder].
22
+ Widget kasySidebarBuilder(
23
+ List<BartMenuRoute> routes,
24
+ OnTapItem onTapItem,
25
+ ValueNotifier<int> currentItem,
26
+ ) {
27
+ return _KasySidebarRoot(
28
+ routes: routes,
29
+ onTapItem: onTapItem,
30
+ currentItem: currentItem,
31
+ );
32
+ }
33
+
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ // Root — reacts to Bart's tab-index changes without recreating State
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+
38
+ class _KasySidebarRoot extends StatelessWidget {
39
+ final List<BartMenuRoute> routes;
40
+ final OnTapItem onTapItem;
41
+ final ValueNotifier<int> currentItem;
42
+
43
+ const _KasySidebarRoot({
44
+ required this.routes,
45
+ required this.onTapItem,
46
+ required this.currentItem,
47
+ });
48
+
49
+ @override
50
+ Widget build(BuildContext context) {
51
+ return ValueListenableBuilder<int>(
52
+ valueListenable: currentItem,
53
+ builder: (_, currentIndex, child) => _KasySidebar(
54
+ routes: routes,
55
+ currentIndex: currentIndex,
56
+ onTapItem: onTapItem,
57
+ ),
58
+ );
59
+ }
60
+ }
61
+
62
+ // ─────────────────────────────────────────────────────────────────────────────
63
+ // Sidebar — stateful: holds project expansion + keyboard shortcut ⌘K / Ctrl+K
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+
66
+ class _KasySidebar extends StatefulWidget {
67
+ final List<BartMenuRoute> routes;
68
+ final int currentIndex;
69
+ final OnTapItem onTapItem;
70
+
71
+ const _KasySidebar({
72
+ required this.routes,
73
+ required this.currentIndex,
74
+ required this.onTapItem,
75
+ });
76
+
77
+ @override
78
+ State<_KasySidebar> createState() => _KasySidebarState();
79
+ }
80
+
81
+ class _KasySidebarState extends State<_KasySidebar> {
82
+ int? _expandedProject;
83
+ bool _searchOpen = false;
84
+
85
+ static const _projects = [
86
+ (color: Color(0xFF34C759), name: 'Kasy Studio'),
87
+ (color: Color(0xFF1E88E5), name: 'Design System'),
88
+ (color: Color(0xFFF57F17), name: 'Components'),
89
+ ];
90
+
91
+ static const _placeholders = [
92
+ (icon: KasyIcons.flash, label: 'Activity'),
93
+ (icon: KasyIcons.note, label: 'Tasks'),
94
+ (icon: KasyIcons.book, label: 'Reports'),
95
+ (icon: KasyIcons.person, label: 'Team'),
96
+ ];
97
+
98
+ // ── Keyboard shortcut ────────────────────────────────────────────────────
99
+
100
+ @override
101
+ void initState() {
102
+ super.initState();
103
+ HardwareKeyboard.instance.addHandler(_onKeyEvent);
104
+ }
105
+
106
+ @override
107
+ void dispose() {
108
+ HardwareKeyboard.instance.removeHandler(_onKeyEvent);
109
+ super.dispose();
110
+ }
111
+
112
+ bool _onKeyEvent(KeyEvent event) {
113
+ if (!mounted || event is! KeyDownEvent) return false;
114
+ if (event.logicalKey != LogicalKeyboardKey.keyK) return false;
115
+ final bool trigger = HardwareKeyboard.instance.isMetaPressed ||
116
+ HardwareKeyboard.instance.isControlPressed;
117
+ if (trigger) {
118
+ _openSearch();
119
+ return true;
120
+ }
121
+ return false;
122
+ }
123
+
124
+ // ── Search modal ─────────────────────────────────────────────────────────
125
+
126
+ void _openSearch() {
127
+ if (_searchOpen || !mounted) return;
128
+ setState(() => _searchOpen = true);
129
+ showGeneralDialog<void>(
130
+ context: context,
131
+ barrierDismissible: true,
132
+ barrierLabel: 'Dismiss',
133
+ barrierColor: Colors.black.withValues(alpha: 0.32),
134
+ transitionDuration: const Duration(milliseconds: 220),
135
+ pageBuilder: (ctx, anim, secAnim) => Align(
136
+ alignment: const Alignment(0, -0.18),
137
+ child: _SearchModal(
138
+ routes: widget.routes,
139
+ onNavigate: (index) {
140
+ Navigator.of(ctx, rootNavigator: true).pop();
141
+ widget.onTapItem(index);
142
+ },
143
+ ),
144
+ ),
145
+ transitionBuilder: (ctx, anim, secAnim, child) {
146
+ final curved =
147
+ CurvedAnimation(parent: anim, curve: Curves.easeOutCubic);
148
+ return FadeTransition(
149
+ opacity: curved,
150
+ child: ScaleTransition(
151
+ scale: Tween<double>(begin: 0.95, end: 1.0).animate(curved),
152
+ child: child,
153
+ ),
154
+ );
155
+ },
156
+ ).then((_) {
157
+ if (mounted) setState(() => _searchOpen = false);
158
+ });
159
+ }
160
+
161
+ // ── Projects expand/collapse ─────────────────────────────────────────────
162
+
163
+ void _toggleProject(int index) {
164
+ setState(() {
165
+ _expandedProject = _expandedProject == index ? null : index;
166
+ });
167
+ }
168
+
169
+ // ── Build ─────────────────────────────────────────────────────────────────
170
+
171
+ @override
172
+ Widget build(BuildContext context) {
173
+ final colors = context.colors;
174
+
175
+ return Container(
176
+ width: _kWidth,
177
+ decoration: BoxDecoration(
178
+ color: colors.surface,
179
+ border: Border(
180
+ right: BorderSide(color: colors.outline.withValues(alpha: 0.4)),
181
+ ),
182
+ ),
183
+ child: SafeArea(
184
+ right: false,
185
+ child: Column(
186
+ crossAxisAlignment: CrossAxisAlignment.stretch,
187
+ children: [
188
+ // ── Scrollable content ──────────────────────────────────────────
189
+ Expanded(
190
+ child: ScrollConfiguration(
191
+ // Hide scrollbar for a cleaner desktop/web look
192
+ behavior: ScrollConfiguration.of(context)
193
+ .copyWith(scrollbars: false),
194
+ child: SingleChildScrollView(
195
+ physics: const ClampingScrollPhysics(),
196
+ padding: const EdgeInsets.fromLTRB(12, 16, 12, 8),
197
+ child: Column(
198
+ crossAxisAlignment: CrossAxisAlignment.start,
199
+ children: [
200
+ // Brand header
201
+ const _Header(),
202
+ const SizedBox(height: 16),
203
+
204
+ // Search bar — opens modal on tap / ⌘K
205
+ _SearchBar(onTap: _openSearch),
206
+ const SizedBox(height: 20),
207
+
208
+ // ── MENU section ─────────────────────────────────────
209
+ const _SectionLabel('Menu'),
210
+ const SizedBox(height: 4),
211
+ ...widget.routes.asMap().entries.map(
212
+ (e) => _NavItem(
213
+ route: e.value,
214
+ isActive: widget.currentIndex == e.key,
215
+ onTap: () => widget.onTapItem(e.key),
216
+ ),
217
+ ),
218
+ const SizedBox(height: 8),
219
+
220
+ const _SidebarDivider(),
221
+ const SizedBox(height: 8),
222
+
223
+ // ── WORKSPACE section (placeholder items) ─────────────
224
+ const _SectionLabel('Workspace'),
225
+ const SizedBox(height: 4),
226
+ ..._placeholders.map(
227
+ (p) =>
228
+ _PlaceholderItem(icon: p.icon, label: p.label),
229
+ ),
230
+ const SizedBox(height: 16),
231
+
232
+ const _SidebarDivider(),
233
+ const SizedBox(height: 12),
234
+
235
+ // ── PROJECTS section ──────────────────────────────────
236
+ Row(
237
+ children: [
238
+ const Expanded(child: _SectionLabel('Projects')),
239
+ Icon(KasyIcons.add, size: 14, color: colors.muted),
240
+ const SizedBox(width: 4),
241
+ ],
242
+ ),
243
+ const SizedBox(height: 4),
244
+ ..._projects.asMap().entries.map(
245
+ (e) => _ProjectItem(
246
+ color: e.value.color,
247
+ name: e.value.name,
248
+ isExpanded: _expandedProject == e.key,
249
+ onToggle: () => _toggleProject(e.key),
250
+ ),
251
+ ),
252
+
253
+ // Add new project
254
+ _AddProjectButton(),
255
+ const SizedBox(height: 8),
256
+ ],
257
+ ),
258
+ ),
259
+ ),
260
+ ),
261
+
262
+ // ── Theme toggle + sticky footer ─────────────────────────────
263
+ const _ThemeToggle(),
264
+ const SizedBox(height: 8),
265
+ const _SidebarDivider(),
266
+ const _UserFooter(),
267
+ ],
268
+ ),
269
+ ),
270
+ );
271
+ }
272
+ }
273
+
274
+ // ─────────────────────────────────────────────────────────────────────────────
275
+ // Header — brand logo + workspace name + dropdown hint
276
+ // ─────────────────────────────────────────────────────────────────────────────
277
+
278
+ class _Header extends StatelessWidget {
279
+ const _Header();
280
+
281
+ @override
282
+ Widget build(BuildContext context) {
283
+ final colors = context.colors;
284
+ return KasyHover(
285
+ onTap: () {},
286
+ hapticEnabled: false,
287
+ borderRadius: BorderRadius.circular(10),
288
+ child: Padding(
289
+ padding: const EdgeInsets.symmetric(vertical: 4),
290
+ child: Row(
291
+ children: [
292
+ // Brand icon
293
+ Container(
294
+ width: 34,
295
+ height: 34,
296
+ decoration: BoxDecoration(
297
+ color: colors.primary,
298
+ borderRadius: BorderRadius.circular(8),
299
+ ),
300
+ child: const Icon(KasyIcons.assistant, size: 18, color: Colors.white),
301
+ ),
302
+ const SizedBox(width: 10),
303
+ // Name + plan
304
+ Expanded(
305
+ child: Column(
306
+ crossAxisAlignment: CrossAxisAlignment.start,
307
+ mainAxisSize: MainAxisSize.min,
308
+ children: [
309
+ Text(
310
+ 'Kasy',
311
+ style: context.textTheme.titleMedium?.copyWith(
312
+ fontWeight: FontWeight.w700,
313
+ height: 1.15,
314
+ ),
315
+ ),
316
+ Text(
317
+ 'Design System',
318
+ style: context.textTheme.labelSmall?.copyWith(
319
+ color: colors.muted,
320
+ ),
321
+ ),
322
+ ],
323
+ ),
324
+ ),
325
+ Icon(KasyIcons.chevronDown, size: 14, color: colors.muted),
326
+ ],
327
+ ),
328
+ ),
329
+ );
330
+ }
331
+ }
332
+
333
+ // ─────────────────────────────────────────────────────────────────────────────
334
+ // Search bar — tappable, opens command-palette modal
335
+ // ─────────────────────────────────────────────────────────────────────────────
336
+
337
+ class _SearchBar extends StatelessWidget {
338
+ final VoidCallback onTap;
339
+ const _SearchBar({required this.onTap});
340
+
341
+ @override
342
+ Widget build(BuildContext context) {
343
+ final colors = context.colors;
344
+ return KasyHover(
345
+ onTap: onTap,
346
+ semanticLabel: 'Search',
347
+ borderRadius: BorderRadius.circular(8),
348
+ child: Container(
349
+ height: 36,
350
+ decoration: BoxDecoration(
351
+ color: colors.surfaceNeutralSoft,
352
+ borderRadius: BorderRadius.circular(8),
353
+ border: Border.all(
354
+ color: colors.outline.withValues(alpha: 0.5),
355
+ width: 0.6,
356
+ ),
357
+ ),
358
+ child: Row(
359
+ children: [
360
+ const SizedBox(width: 10),
361
+ Icon(KasyIcons.search, size: 15, color: colors.muted),
362
+ const SizedBox(width: 8),
363
+ Expanded(
364
+ child: Text(
365
+ 'Search...',
366
+ style: context.textTheme.bodySmall?.copyWith(
367
+ color: colors.muted,
368
+ ),
369
+ ),
370
+ ),
371
+ // Keyboard shortcut badge
372
+ Container(
373
+ margin: const EdgeInsets.only(right: 8),
374
+ padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
375
+ decoration: BoxDecoration(
376
+ color: colors.outline.withValues(alpha: 0.35),
377
+ borderRadius: BorderRadius.circular(4),
378
+ ),
379
+ child: Text(
380
+ defaultTargetPlatform == TargetPlatform.macOS ? '⌘K' : 'Ctrl K',
381
+ style: context.textTheme.labelSmall?.copyWith(
382
+ color: colors.muted,
383
+ fontSize: 10,
384
+ ),
385
+ ),
386
+ ),
387
+ ],
388
+ ),
389
+ ),
390
+ );
391
+ }
392
+ }
393
+
394
+ // ─────────────────────────────────────────────────────────────────────────────
395
+ // Section label — ALL CAPS, subtle
396
+ // ─────────────────────────────────────────────────────────────────────────────
397
+
398
+ class _SectionLabel extends StatelessWidget {
399
+ final String text;
400
+ const _SectionLabel(this.text);
401
+
402
+ @override
403
+ Widget build(BuildContext context) {
404
+ return Padding(
405
+ padding: const EdgeInsets.only(left: 4, bottom: 2),
406
+ child: Text(
407
+ text,
408
+ style: context.textTheme.labelSmall?.copyWith(
409
+ color: context.colors.muted,
410
+ fontWeight: FontWeight.w600,
411
+ letterSpacing: 0.4,
412
+ fontSize: 11,
413
+ ),
414
+ ),
415
+ );
416
+ }
417
+ }
418
+
419
+ // ─────────────────────────────────────────────────────────────────────────────
420
+ // Divider
421
+ // ─────────────────────────────────────────────────────────────────────────────
422
+
423
+ class _SidebarDivider extends StatelessWidget {
424
+ const _SidebarDivider();
425
+
426
+ @override
427
+ Widget build(BuildContext context) {
428
+ return Divider(
429
+ height: 1,
430
+ thickness: 0.8,
431
+ color: context.colors.outline.withValues(alpha: 0.4),
432
+ );
433
+ }
434
+ }
435
+
436
+ // ─────────────────────────────────────────────────────────────────────────────
437
+ // Nav item — real, tappable, animated active state
438
+ // ─────────────────────────────────────────────────────────────────────────────
439
+
440
+ class _NavItem extends StatelessWidget {
441
+ final BartMenuRoute route;
442
+ final bool isActive;
443
+ final VoidCallback onTap;
444
+
445
+ const _NavItem({
446
+ required this.route,
447
+ required this.isActive,
448
+ required this.onTap,
449
+ });
450
+
451
+ @override
452
+ Widget build(BuildContext context) {
453
+ final colors = context.colors;
454
+ final Color iconColor =
455
+ isActive ? colors.primary : colors.onSurface.withValues(alpha: 0.6);
456
+ final Color textColor = isActive ? colors.primary : colors.onSurface;
457
+ final bool hasIconBuilder = route.iconBuilder != null;
458
+ final IconData icon = route.icon ?? KasyIcons.notification;
459
+
460
+ return Padding(
461
+ padding: const EdgeInsets.symmetric(vertical: 1.5),
462
+ child: KasyHover(
463
+ onTap: onTap,
464
+ semanticLabel: route.label,
465
+ borderRadius: BorderRadius.circular(_kItemRadius),
466
+ child: AnimatedContainer(
467
+ duration: const Duration(milliseconds: 160),
468
+ curve: Curves.easeOut,
469
+ height: _kItemHeight,
470
+ decoration: BoxDecoration(
471
+ color: isActive
472
+ ? colors.surfacePrimarySoft
473
+ : Colors.transparent,
474
+ borderRadius: BorderRadius.circular(_kItemRadius),
475
+ ),
476
+ child: Row(
477
+ children: [
478
+ // Active accent line on the left
479
+ AnimatedContainer(
480
+ duration: const Duration(milliseconds: 160),
481
+ width: 3,
482
+ height: 20,
483
+ margin: const EdgeInsets.only(left: 4, right: 8),
484
+ decoration: BoxDecoration(
485
+ color: isActive ? colors.primary : Colors.transparent,
486
+ borderRadius: BorderRadius.circular(2),
487
+ ),
488
+ ),
489
+ // Icon
490
+ Icon(icon, size: 18, color: iconColor),
491
+ const SizedBox(width: 10),
492
+ // Label
493
+ Expanded(
494
+ child: Text(
495
+ route.label ?? '',
496
+ style: context.textTheme.bodyMedium?.copyWith(
497
+ color: textColor,
498
+ fontWeight:
499
+ isActive ? FontWeight.w600 : FontWeight.w500,
500
+ ),
501
+ ),
502
+ ),
503
+ // Notification count badge (right side)
504
+ if (hasIconBuilder) ...[
505
+ const _NotificationCountBadge(),
506
+ const SizedBox(width: 10),
507
+ ],
508
+ ],
509
+ ),
510
+ ),
511
+ ),
512
+ );
513
+ }
514
+ }
515
+
516
+ // ─────────────────────────────────────────────────────────────────────────────
517
+ // Notification count badge — right-aligned pill, reads live count
518
+ // ─────────────────────────────────────────────────────────────────────────────
519
+
520
+ class _NotificationCountBadge extends ConsumerWidget {
521
+ const _NotificationCountBadge();
522
+
523
+ @override
524
+ Widget build(BuildContext context, WidgetRef ref) {
525
+ final String? userId = ref.watch(userStateNotifierProvider).user.idOrNull;
526
+ if (userId == null) return const SizedBox.shrink();
527
+
528
+ final int count = ref.watch(notificationsProvider).maybeWhen(
529
+ data: (list) => list.data.where((n) => !n.seen).length,
530
+ orElse: () => 0,
531
+ );
532
+ if (count == 0) return const SizedBox.shrink();
533
+
534
+ return AnimatedSwitcher(
535
+ duration: const Duration(milliseconds: 220),
536
+ transitionBuilder: (child, anim) => ScaleTransition(
537
+ scale: Tween<double>(begin: 0.55, end: 1.0).animate(
538
+ CurvedAnimation(parent: anim, curve: Curves.easeOutBack),
539
+ ),
540
+ child: FadeTransition(opacity: anim, child: child),
541
+ ),
542
+ child: Container(
543
+ key: ValueKey(count),
544
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
545
+ decoration: BoxDecoration(
546
+ color: context.colors.error,
547
+ borderRadius: BorderRadius.circular(10),
548
+ ),
549
+ child: Text(
550
+ count > 99 ? '99+' : '$count',
551
+ style: context.textTheme.labelSmall?.copyWith(
552
+ color: Colors.white,
553
+ fontWeight: FontWeight.w700,
554
+ fontSize: 10,
555
+ height: 1.2,
556
+ ),
557
+ ),
558
+ ),
559
+ );
560
+ }
561
+ }
562
+
563
+ // ─────────────────────────────────────────────────────────────────────────────
564
+ // Placeholder item — hover feedback, no navigation (coming soon)
565
+ // ─────────────────────────────────────────────────────────────────────────────
566
+
567
+ class _PlaceholderItem extends StatelessWidget {
568
+ final IconData icon;
569
+ final String label;
570
+
571
+ const _PlaceholderItem({required this.icon, required this.label});
572
+
573
+ @override
574
+ Widget build(BuildContext context) {
575
+ final colors = context.colors;
576
+ return Padding(
577
+ padding: const EdgeInsets.symmetric(vertical: 1.5),
578
+ child: KasyHover(
579
+ onTap: () {
580
+ showKasyToast(
581
+ context,
582
+ title: label,
583
+ message: 'Coming soon',
584
+ duration: const Duration(seconds: 2),
585
+ );
586
+ },
587
+ hapticEnabled: false,
588
+ borderRadius: BorderRadius.circular(_kItemRadius),
589
+ child: SizedBox(
590
+ height: _kItemHeight,
591
+ child: Row(
592
+ children: [
593
+ const SizedBox(width: 15), // aligns with nav items (3+4+8)
594
+ Icon(
595
+ icon,
596
+ size: 18,
597
+ color: colors.onSurface.withValues(alpha: 0.35),
598
+ ),
599
+ const SizedBox(width: 10),
600
+ Expanded(
601
+ child: Text(
602
+ label,
603
+ style: context.textTheme.bodyMedium?.copyWith(
604
+ color: colors.muted,
605
+ fontWeight: FontWeight.w500,
606
+ ),
607
+ ),
608
+ ),
609
+ ],
610
+ ),
611
+ ),
612
+ ),
613
+ );
614
+ }
615
+ }
616
+
617
+ // ─────────────────────────────────────────────────────────────────────────────
618
+ // Project item — expandable with animated chevron + sub-items
619
+ // ─────────────────────────────────────────────────────────────────────────────
620
+
621
+ class _ProjectItem extends StatelessWidget {
622
+ final Color color;
623
+ final String name;
624
+ final bool isExpanded;
625
+ final VoidCallback onToggle;
626
+
627
+ const _ProjectItem({
628
+ required this.color,
629
+ required this.name,
630
+ required this.isExpanded,
631
+ required this.onToggle,
632
+ });
633
+
634
+ static const _subItems = [
635
+ (icon: KasyIcons.home, label: 'Overview'),
636
+ (icon: KasyIcons.person, label: 'Members'),
637
+ (icon: KasyIcons.settings, label: 'Settings'),
638
+ ];
639
+
640
+ @override
641
+ Widget build(BuildContext context) {
642
+ final colors = context.colors;
643
+
644
+ return Column(
645
+ crossAxisAlignment: CrossAxisAlignment.start,
646
+ mainAxisSize: MainAxisSize.min,
647
+ children: [
648
+ // ── Header row ────────────────────────────────────────────────────
649
+ KasyHover(
650
+ onTap: onToggle,
651
+ borderRadius: BorderRadius.circular(_kItemRadius),
652
+ child: SizedBox(
653
+ height: 36,
654
+ child: Row(
655
+ children: [
656
+ const SizedBox(width: 15),
657
+ // Colored dot
658
+ Container(
659
+ width: 10,
660
+ height: 10,
661
+ decoration: BoxDecoration(
662
+ color: color,
663
+ shape: BoxShape.circle,
664
+ boxShadow: [
665
+ BoxShadow(
666
+ color: color.withValues(alpha: 0.45),
667
+ blurRadius: 5,
668
+ offset: const Offset(0, 1),
669
+ ),
670
+ ],
671
+ ),
672
+ ),
673
+ const SizedBox(width: 12),
674
+ Expanded(
675
+ child: Text(
676
+ name,
677
+ style: context.textTheme.bodyMedium?.copyWith(
678
+ color: colors.onSurface.withValues(alpha: 0.8),
679
+ fontWeight: FontWeight.w500,
680
+ ),
681
+ ),
682
+ ),
683
+ // Chevron rotates when expanded
684
+ AnimatedRotation(
685
+ duration: const Duration(milliseconds: 200),
686
+ curve: Curves.easeInOut,
687
+ turns: isExpanded ? 0.25 : 0.0,
688
+ child: Icon(
689
+ KasyIcons.chevronRight,
690
+ size: 13,
691
+ color: colors.muted.withValues(alpha: 0.6),
692
+ ),
693
+ ),
694
+ const SizedBox(width: 10),
695
+ ],
696
+ ),
697
+ ),
698
+ ),
699
+
700
+ // ── Sub-items — animated expand/collapse ──────────────────────────
701
+ AnimatedSize(
702
+ duration: const Duration(milliseconds: 200),
703
+ curve: Curves.easeInOut,
704
+ child: isExpanded
705
+ ? Column(
706
+ crossAxisAlignment: CrossAxisAlignment.start,
707
+ children: _subItems
708
+ .map((s) =>
709
+ _ProjectSubItem(icon: s.icon, label: s.label))
710
+ .toList(),
711
+ )
712
+ : const SizedBox.shrink(),
713
+ ),
714
+ ],
715
+ );
716
+ }
717
+ }
718
+
719
+ class _ProjectSubItem extends StatelessWidget {
720
+ final IconData icon;
721
+ final String label;
722
+
723
+ const _ProjectSubItem({required this.icon, required this.label});
724
+
725
+ @override
726
+ Widget build(BuildContext context) {
727
+ final colors = context.colors;
728
+ return KasyHover(
729
+ onTap: () {},
730
+ hapticEnabled: false,
731
+ borderRadius: BorderRadius.circular(6),
732
+ child: SizedBox(
733
+ height: 32,
734
+ child: Row(
735
+ children: [
736
+ const SizedBox(width: 35), // deeper indent
737
+ Icon(icon, size: 14, color: colors.muted),
738
+ const SizedBox(width: 9),
739
+ Text(
740
+ label,
741
+ style: context.textTheme.bodySmall?.copyWith(
742
+ color: colors.onSurface.withValues(alpha: 0.6),
743
+ fontWeight: FontWeight.w500,
744
+ ),
745
+ ),
746
+ ],
747
+ ),
748
+ ),
749
+ );
750
+ }
751
+ }
752
+
753
+ // ─────────────────────────────────────────────────────────────────────────────
754
+ // "Add New Project" button
755
+ // ─────────────────────────────────────────────────────────────────────────────
756
+
757
+ class _AddProjectButton extends StatelessWidget {
758
+ @override
759
+ Widget build(BuildContext context) {
760
+ final colors = context.colors;
761
+ return KasyHover(
762
+ onTap: () {},
763
+ hapticEnabled: false,
764
+ borderRadius: BorderRadius.circular(_kItemRadius),
765
+ child: SizedBox(
766
+ height: 34,
767
+ child: Row(
768
+ children: [
769
+ const SizedBox(width: 15),
770
+ Container(
771
+ width: 20,
772
+ height: 20,
773
+ decoration: BoxDecoration(
774
+ color: colors.surfacePrimarySoft,
775
+ borderRadius: BorderRadius.circular(6),
776
+ ),
777
+ child: Icon(KasyIcons.add, size: 13, color: colors.primary),
778
+ ),
779
+ const SizedBox(width: 10),
780
+ Text(
781
+ 'New Project',
782
+ style: context.textTheme.bodySmall?.copyWith(
783
+ color: colors.primary.withValues(alpha: 0.75),
784
+ fontWeight: FontWeight.w500,
785
+ ),
786
+ ),
787
+ ],
788
+ ),
789
+ ),
790
+ );
791
+ }
792
+ }
793
+
794
+ // ─────────────────────────────────────────────────────────────────────────────
795
+ // User footer — real data via Riverpod, KasyAvatar initials
796
+ // Logout icon only visible on hover to avoid visual clutter.
797
+ // ─────────────────────────────────────────────────────────────────────────────
798
+
799
+ class _UserFooter extends ConsumerStatefulWidget {
800
+ const _UserFooter();
801
+
802
+ @override
803
+ ConsumerState<_UserFooter> createState() => _UserFooterState();
804
+ }
805
+
806
+ class _UserFooterState extends ConsumerState<_UserFooter> {
807
+ bool _hovered = false;
808
+
809
+ @override
810
+ Widget build(BuildContext context) {
811
+ final user = ref.watch(userStateNotifierProvider).user;
812
+
813
+ final String displayName;
814
+ final String subtitle;
815
+
816
+ if (user is AuthenticatedUserData) {
817
+ displayName = user.name ?? user.email.split('@').first;
818
+ subtitle = user.email;
819
+ } else {
820
+ displayName = 'Guest';
821
+ subtitle = 'Not signed in';
822
+ }
823
+
824
+ final String initials =
825
+ displayName.isNotEmpty ? displayName[0].toUpperCase() : 'G';
826
+
827
+ return MouseRegion(
828
+ onEnter: (_) => setState(() => _hovered = true),
829
+ onExit: (_) => setState(() => _hovered = false),
830
+ child: KasyHover(
831
+ onTap: () {},
832
+ hapticEnabled: false,
833
+ child: Padding(
834
+ padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
835
+ child: Row(
836
+ children: [
837
+ // Avatar
838
+ KasyAvatar(
839
+ size: KasyAvatarSize.small,
840
+ diameter: 32,
841
+ initials: initials,
842
+ ),
843
+ const SizedBox(width: 10),
844
+ // Name + email
845
+ Expanded(
846
+ child: Column(
847
+ crossAxisAlignment: CrossAxisAlignment.start,
848
+ mainAxisSize: MainAxisSize.min,
849
+ children: [
850
+ Text(
851
+ displayName,
852
+ style: context.textTheme.labelMedium?.copyWith(
853
+ fontWeight: FontWeight.w600,
854
+ ),
855
+ overflow: TextOverflow.ellipsis,
856
+ maxLines: 1,
857
+ ),
858
+ Text(
859
+ subtitle,
860
+ style: context.textTheme.labelSmall?.copyWith(
861
+ color: context.colors.muted,
862
+ ),
863
+ overflow: TextOverflow.ellipsis,
864
+ maxLines: 1,
865
+ ),
866
+ ],
867
+ ),
868
+ ),
869
+ // Logout icon — only visible on hover
870
+ AnimatedOpacity(
871
+ opacity: _hovered ? 1.0 : 0.0,
872
+ duration: const Duration(milliseconds: 150),
873
+ child: Padding(
874
+ padding: const EdgeInsets.only(left: 8),
875
+ child: Icon(
876
+ KasyIcons.logout,
877
+ size: 16,
878
+ color: context.colors.muted,
879
+ ),
880
+ ),
881
+ ),
882
+ ],
883
+ ),
884
+ ),
885
+ ),
886
+ );
887
+ }
888
+ }
889
+
890
+ // ─────────────────────────────────────────────────────────────────────────────
891
+ // Search modal — centered command palette with KasyTextField + live filter
892
+ // ─────────────────────────────────────────────────────────────────────────────
893
+
894
+ class _SearchModal extends StatefulWidget {
895
+ final List<BartMenuRoute> routes;
896
+ final void Function(int index) onNavigate;
897
+
898
+ const _SearchModal({required this.routes, required this.onNavigate});
899
+
900
+ @override
901
+ State<_SearchModal> createState() => _SearchModalState();
902
+ }
903
+
904
+ class _SearchModalState extends State<_SearchModal> {
905
+ final _controller = TextEditingController();
906
+ late final FocusNode _focusNode;
907
+ final ScrollController _scrollController = ScrollController();
908
+ String _query = '';
909
+ int _selectedIndex = 0;
910
+
911
+ // Approximate height of each result tile (34px icon + 12px vertical padding)
912
+ static const double _kTileHeight = 47.0;
913
+
914
+ static const _placeholders = [
915
+ (icon: KasyIcons.flash, label: 'Activity'),
916
+ (icon: KasyIcons.note, label: 'Tasks'),
917
+ (icon: KasyIcons.book, label: 'Reports'),
918
+ (icon: KasyIcons.person, label: 'Team'),
919
+ ];
920
+
921
+ @override
922
+ void initState() {
923
+ super.initState();
924
+ _focusNode = FocusNode(onKeyEvent: _onFocusKeyEvent);
925
+ }
926
+
927
+ @override
928
+ void dispose() {
929
+ _controller.dispose();
930
+ _focusNode.dispose();
931
+ _scrollController.dispose();
932
+ super.dispose();
933
+ }
934
+
935
+ KeyEventResult _onFocusKeyEvent(FocusNode node, KeyEvent event) {
936
+ if (event is! KeyDownEvent && event is! KeyRepeatEvent) {
937
+ return KeyEventResult.ignored;
938
+ }
939
+
940
+ final results = _results;
941
+
942
+ if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
943
+ if (results.isNotEmpty) {
944
+ setState(() {
945
+ _selectedIndex = (_selectedIndex + 1).clamp(0, results.length - 1);
946
+ });
947
+ _scrollToSelected();
948
+ }
949
+ return KeyEventResult.handled;
950
+ }
951
+
952
+ if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
953
+ if (results.isNotEmpty) {
954
+ setState(() {
955
+ _selectedIndex = (_selectedIndex - 1).clamp(0, results.length - 1);
956
+ });
957
+ _scrollToSelected();
958
+ }
959
+ return KeyEventResult.handled;
960
+ }
961
+
962
+ if (event.logicalKey == LogicalKeyboardKey.enter) {
963
+ if (_selectedIndex < results.length) {
964
+ final item = results[_selectedIndex];
965
+ if (item.navIndex != null) {
966
+ widget.onNavigate(item.navIndex!);
967
+ }
968
+ }
969
+ return KeyEventResult.handled;
970
+ }
971
+
972
+ if (event.logicalKey == LogicalKeyboardKey.escape) {
973
+ Navigator.of(context, rootNavigator: true).pop();
974
+ return KeyEventResult.handled;
975
+ }
976
+
977
+ return KeyEventResult.ignored;
978
+ }
979
+
980
+ void _scrollToSelected() {
981
+ if (!_scrollController.hasClients) return;
982
+ final offset = (_selectedIndex * _kTileHeight)
983
+ .clamp(0.0, _scrollController.position.maxScrollExtent);
984
+ _scrollController.animateTo(
985
+ offset,
986
+ duration: const Duration(milliseconds: 120),
987
+ curve: Curves.easeOut,
988
+ );
989
+ }
990
+
991
+ List<({IconData icon, String label, int? navIndex})> get _results {
992
+ final q = _query.trim().toLowerCase();
993
+ final out = <({IconData icon, String label, int? navIndex})>[];
994
+
995
+ // Real nav routes
996
+ for (var i = 0; i < widget.routes.length; i++) {
997
+ final r = widget.routes[i];
998
+ final label = r.label ?? '';
999
+ if (q.isEmpty || label.toLowerCase().contains(q)) {
1000
+ out.add((
1001
+ icon: r.icon ?? KasyIcons.notification,
1002
+ label: label,
1003
+ navIndex: i,
1004
+ ));
1005
+ }
1006
+ }
1007
+
1008
+ // Placeholder workspace items
1009
+ for (final p in _placeholders) {
1010
+ if (q.isEmpty || p.label.toLowerCase().contains(q)) {
1011
+ out.add((icon: p.icon, label: p.label, navIndex: null));
1012
+ }
1013
+ }
1014
+
1015
+ return out;
1016
+ }
1017
+
1018
+ @override
1019
+ Widget build(BuildContext context) {
1020
+ final colors = context.colors;
1021
+ final results = _results;
1022
+ final double maxW = min(560.0, MediaQuery.of(context).size.width - 48);
1023
+
1024
+ return Material(
1025
+ color: Colors.transparent,
1026
+ child: Container(
1027
+ width: maxW,
1028
+ constraints: BoxConstraints(
1029
+ maxHeight: MediaQuery.of(context).size.height * 0.62,
1030
+ ),
1031
+ decoration: BoxDecoration(
1032
+ color: colors.surface,
1033
+ borderRadius: BorderRadius.circular(16),
1034
+ border: Border.all(color: colors.outline.withValues(alpha: 0.25)),
1035
+ boxShadow: [
1036
+ BoxShadow(
1037
+ color: Colors.black.withValues(alpha: 0.18),
1038
+ blurRadius: 40,
1039
+ offset: const Offset(0, 12),
1040
+ ),
1041
+ BoxShadow(
1042
+ color: Colors.black.withValues(alpha: 0.06),
1043
+ blurRadius: 8,
1044
+ offset: const Offset(0, 2),
1045
+ ),
1046
+ ],
1047
+ ),
1048
+ child: ClipRRect(
1049
+ borderRadius: BorderRadius.circular(16),
1050
+ child: Column(
1051
+ mainAxisSize: MainAxisSize.min,
1052
+ children: [
1053
+ // ── Search input ──────────────────────────────────────────────
1054
+ Padding(
1055
+ padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
1056
+ child: KasyTextField(
1057
+ controller: _controller,
1058
+ focusNode: _focusNode,
1059
+ hint: 'Search pages, settings, projects...',
1060
+ autofocus: true,
1061
+ onChanged: (v) => setState(() {
1062
+ _query = v;
1063
+ _selectedIndex = 0;
1064
+ }),
1065
+ ),
1066
+ ),
1067
+
1068
+ // ── Results ───────────────────────────────────────────────────
1069
+ if (results.isEmpty && _query.isNotEmpty)
1070
+ Padding(
1071
+ padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
1072
+ child: Row(
1073
+ children: [
1074
+ Icon(KasyIcons.search, size: 16, color: colors.muted),
1075
+ const SizedBox(width: 8),
1076
+ Text(
1077
+ 'No results for "$_query"',
1078
+ style: context.textTheme.bodyMedium
1079
+ ?.copyWith(color: colors.muted),
1080
+ ),
1081
+ ],
1082
+ ),
1083
+ )
1084
+ else ...[
1085
+ Divider(
1086
+ height: 1,
1087
+ color: colors.outline.withValues(alpha: 0.4)),
1088
+ Flexible(
1089
+ child: ListView.separated(
1090
+ controller: _scrollController,
1091
+ shrinkWrap: true,
1092
+ padding: const EdgeInsets.symmetric(vertical: 6),
1093
+ itemCount: results.length,
1094
+ separatorBuilder: (_, idx) => const SizedBox(height: 1),
1095
+ itemBuilder: (_, i) {
1096
+ final item = results[i];
1097
+ return _SearchResultTile(
1098
+ icon: item.icon,
1099
+ label: item.label,
1100
+ isNavigable: item.navIndex != null,
1101
+ isSelected: i == _selectedIndex,
1102
+ onTap: item.navIndex != null
1103
+ ? () => widget.onNavigate(item.navIndex!)
1104
+ : null,
1105
+ onHover: () => setState(() => _selectedIndex = i),
1106
+ );
1107
+ },
1108
+ ),
1109
+ ),
1110
+ ],
1111
+
1112
+ // ── Footer hint ───────────────────────────────────────────────
1113
+ Container(
1114
+ decoration: BoxDecoration(
1115
+ color: colors.surfaceNeutralSoft,
1116
+ border: Border(
1117
+ top: BorderSide(
1118
+ color: colors.outline.withValues(alpha: 0.3)),
1119
+ ),
1120
+ ),
1121
+ padding: const EdgeInsets.symmetric(
1122
+ horizontal: 16, vertical: 8),
1123
+ child: const Row(
1124
+ children: [
1125
+ _KbdHint(label: '↑↓', tooltip: 'Navigate'),
1126
+ SizedBox(width: 12),
1127
+ _KbdHint(label: '↵', tooltip: 'Select'),
1128
+ SizedBox(width: 12),
1129
+ _KbdHint(label: 'Esc', tooltip: 'Close'),
1130
+ ],
1131
+ ),
1132
+ ),
1133
+ ],
1134
+ ),
1135
+ ),
1136
+ ),
1137
+ );
1138
+ }
1139
+ }
1140
+
1141
+ /// Keyboard shortcut hint chip shown in the modal footer.
1142
+ class _KbdHint extends StatelessWidget {
1143
+ final String label;
1144
+ final String tooltip;
1145
+
1146
+ const _KbdHint({required this.label, required this.tooltip});
1147
+
1148
+ @override
1149
+ Widget build(BuildContext context) {
1150
+ final colors = context.colors;
1151
+ return Row(
1152
+ mainAxisSize: MainAxisSize.min,
1153
+ children: [
1154
+ Container(
1155
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
1156
+ decoration: BoxDecoration(
1157
+ color: colors.surface,
1158
+ borderRadius: BorderRadius.circular(4),
1159
+ border:
1160
+ Border.all(color: colors.outline.withValues(alpha: 0.5)),
1161
+ ),
1162
+ child: Text(
1163
+ label,
1164
+ style: context.textTheme.labelSmall?.copyWith(
1165
+ fontSize: 10,
1166
+ color: colors.muted,
1167
+ fontWeight: FontWeight.w600,
1168
+ ),
1169
+ ),
1170
+ ),
1171
+ const SizedBox(width: 4),
1172
+ Text(
1173
+ tooltip,
1174
+ style: context.textTheme.labelSmall?.copyWith(
1175
+ fontSize: 10,
1176
+ color: colors.muted,
1177
+ ),
1178
+ ),
1179
+ ],
1180
+ );
1181
+ }
1182
+ }
1183
+
1184
+ // ─────────────────────────────────────────────────────────────────────────────
1185
+ // Search result tile
1186
+ // ─────────────────────────────────────────────────────────────────────────────
1187
+
1188
+ class _SearchResultTile extends StatelessWidget {
1189
+ final IconData icon;
1190
+ final String label;
1191
+ final bool isNavigable;
1192
+ final bool isSelected;
1193
+ final VoidCallback? onTap;
1194
+ final VoidCallback? onHover;
1195
+
1196
+ const _SearchResultTile({
1197
+ required this.icon,
1198
+ required this.label,
1199
+ required this.isNavigable,
1200
+ required this.isSelected,
1201
+ this.onTap,
1202
+ this.onHover,
1203
+ });
1204
+
1205
+ @override
1206
+ Widget build(BuildContext context) {
1207
+ final colors = context.colors;
1208
+
1209
+ final Widget content = AnimatedContainer(
1210
+ duration: const Duration(milliseconds: 100),
1211
+ color: isSelected
1212
+ ? colors.surfacePrimarySoft.withValues(alpha: 0.5)
1213
+ : Colors.transparent,
1214
+ child: Padding(
1215
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
1216
+ child: Row(
1217
+ children: [
1218
+ // Icon badge
1219
+ Container(
1220
+ width: 34,
1221
+ height: 34,
1222
+ decoration: BoxDecoration(
1223
+ color: isNavigable
1224
+ ? colors.surfacePrimarySoft
1225
+ : colors.surfaceNeutralSoft,
1226
+ borderRadius: BorderRadius.circular(8),
1227
+ ),
1228
+ child: Icon(
1229
+ icon,
1230
+ size: 16,
1231
+ color: isNavigable ? colors.primary : colors.muted,
1232
+ ),
1233
+ ),
1234
+ const SizedBox(width: 12),
1235
+ // Label
1236
+ Expanded(
1237
+ child: Text(
1238
+ label,
1239
+ style: context.textTheme.bodyMedium?.copyWith(
1240
+ color: isNavigable ? colors.onSurface : colors.muted,
1241
+ fontWeight:
1242
+ isNavigable ? FontWeight.w500 : FontWeight.w400,
1243
+ ),
1244
+ ),
1245
+ ),
1246
+ // Right indicator
1247
+ if (!isNavigable)
1248
+ Container(
1249
+ padding:
1250
+ const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
1251
+ decoration: BoxDecoration(
1252
+ color: colors.surfaceNeutralSoft,
1253
+ borderRadius: BorderRadius.circular(6),
1254
+ border: Border.all(
1255
+ color: colors.outline.withValues(alpha: 0.4)),
1256
+ ),
1257
+ child: Text(
1258
+ 'Soon',
1259
+ style: context.textTheme.labelSmall?.copyWith(
1260
+ color: colors.muted,
1261
+ fontSize: 10,
1262
+ ),
1263
+ ),
1264
+ )
1265
+ else
1266
+ Icon(KasyIcons.arrowForwardIos,
1267
+ size: 13, color: colors.muted.withValues(alpha: 0.6)),
1268
+ ],
1269
+ ),
1270
+ ),
1271
+ );
1272
+
1273
+ return MouseRegion(
1274
+ onEnter: (_) => onHover?.call(),
1275
+ child: onTap != null
1276
+ ? KasyHover(onTap: onTap!, child: content)
1277
+ : content,
1278
+ );
1279
+ }
1280
+ }
1281
+
1282
+ // ─────────────────────────────────────────────────────────────────────────────
1283
+ // Theme toggle — pill switcher Light / Dark in the sidebar footer area
1284
+ // ─────────────────────────────────────────────────────────────────────────────
1285
+
1286
+ class _ThemeToggle extends StatelessWidget {
1287
+ const _ThemeToggle();
1288
+
1289
+ @override
1290
+ Widget build(BuildContext context) {
1291
+ final theme = ThemeProvider.of(context);
1292
+ final isDark = theme.mode == ThemeMode.dark;
1293
+ final colors = context.colors;
1294
+
1295
+ return Padding(
1296
+ padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
1297
+ child: Container(
1298
+ height: 46,
1299
+ padding: const EdgeInsets.all(4),
1300
+ decoration: BoxDecoration(
1301
+ color: colors.surfaceNeutralSoft,
1302
+ borderRadius: BorderRadius.circular(12),
1303
+ border: Border.all(color: colors.outline.withValues(alpha: 0.35)),
1304
+ ),
1305
+ child: Row(
1306
+ children: [
1307
+ Expanded(
1308
+ child: _ThemeOption(
1309
+ icon: KasyIcons.lightMode,
1310
+ label: 'Light',
1311
+ isActive: !isDark,
1312
+ isDarkMode: isDark,
1313
+ onTap: isDark ? theme.toggle : null,
1314
+ ),
1315
+ ),
1316
+ Expanded(
1317
+ child: _ThemeOption(
1318
+ icon: KasyIcons.darkMode,
1319
+ label: 'Dark',
1320
+ isActive: isDark,
1321
+ isDarkMode: isDark,
1322
+ onTap: isDark ? null : theme.toggle,
1323
+ ),
1324
+ ),
1325
+ ],
1326
+ ),
1327
+ ),
1328
+ );
1329
+ }
1330
+ }
1331
+
1332
+ class _ThemeOption extends StatelessWidget {
1333
+ final IconData icon;
1334
+ final String label;
1335
+ final bool isActive;
1336
+ final bool isDarkMode;
1337
+ final VoidCallback? onTap;
1338
+
1339
+ const _ThemeOption({
1340
+ required this.icon,
1341
+ required this.label,
1342
+ required this.isActive,
1343
+ required this.isDarkMode,
1344
+ this.onTap,
1345
+ });
1346
+
1347
+ @override
1348
+ Widget build(BuildContext context) {
1349
+ final colors = context.colors;
1350
+
1351
+ // Active pill — white card on light, distinct dark surface on dark
1352
+ final Color activeBg =
1353
+ isDarkMode ? colors.onSurface.withValues(alpha: 0.18) : colors.surface;
1354
+ final Color activeContentColor =
1355
+ isDarkMode ? Colors.white : colors.onSurface;
1356
+ final Color inactiveColor = colors.muted;
1357
+
1358
+ return KasyHover(
1359
+ onTap: onTap ?? () {},
1360
+ hapticEnabled: false,
1361
+ borderRadius: BorderRadius.circular(8),
1362
+ child: AnimatedContainer(
1363
+ duration: const Duration(milliseconds: 200),
1364
+ curve: Curves.easeOut,
1365
+ decoration: BoxDecoration(
1366
+ color: isActive ? activeBg : Colors.transparent,
1367
+ borderRadius: BorderRadius.circular(8),
1368
+ boxShadow: isActive && !isDarkMode
1369
+ ? [
1370
+ BoxShadow(
1371
+ color: Colors.black.withValues(alpha: 0.12),
1372
+ blurRadius: 8,
1373
+ offset: const Offset(0, 2),
1374
+ ),
1375
+ ]
1376
+ : null,
1377
+ ),
1378
+ child: Row(
1379
+ mainAxisAlignment: MainAxisAlignment.center,
1380
+ children: [
1381
+ Icon(
1382
+ icon,
1383
+ size: 15,
1384
+ color: isActive ? activeContentColor : inactiveColor,
1385
+ ),
1386
+ const SizedBox(width: 6),
1387
+ Text(
1388
+ label,
1389
+ style: context.textTheme.bodySmall?.copyWith(
1390
+ color: isActive ? activeContentColor : inactiveColor,
1391
+ fontWeight: isActive ? FontWeight.w700 : FontWeight.w400,
1392
+ ),
1393
+ ),
1394
+ ],
1395
+ ),
1396
+ ),
1397
+ );
1398
+ }
1399
+ }
1400
+
1401
+ // ─────────────────────────────────────────────────────────────────────────────
1402
+ // Rail theme toggle — single icon button in the collapsed rail
1403
+ // ─────────────────────────────────────────────────────────────────────────────
1404
+
1405
+ class _RailThemeToggle extends StatelessWidget {
1406
+ const _RailThemeToggle();
1407
+
1408
+ @override
1409
+ Widget build(BuildContext context) {
1410
+ final theme = ThemeProvider.of(context);
1411
+ final isDark = theme.mode == ThemeMode.dark;
1412
+ return _RailIconSlot(
1413
+ child: _RailIconBtn(
1414
+ icon: isDark ? KasyIcons.lightMode : KasyIcons.darkMode,
1415
+ tooltip: isDark ? 'Light mode' : 'Dark mode',
1416
+ onTap: theme.toggle,
1417
+ hapticEnabled: false,
1418
+ ),
1419
+ );
1420
+ }
1421
+ }
1422
+
1423
+ // ═════════════════════════════════════════════════════════════════════════════
1424
+ // COLLAPSED SIDEBAR — icon-only rail for medium breakpoint (768–1024 px)
1425
+ // ═════════════════════════════════════════════════════════════════════════════
1426
+
1427
+ const double _kCollapsedWidth = 64.0;
1428
+ const double _kRailIconSize = 40.0;
1429
+
1430
+ /// Entry point for the collapsed icon-only sidebar.
1431
+ Widget kasySidebarCollapsedBuilder(
1432
+ List<BartMenuRoute> routes,
1433
+ OnTapItem onTapItem,
1434
+ ValueNotifier<int> currentItem,
1435
+ ) {
1436
+ return _KasySidebarCollapsedRoot(
1437
+ routes: routes,
1438
+ onTapItem: onTapItem,
1439
+ currentItem: currentItem,
1440
+ );
1441
+ }
1442
+
1443
+ // ─────────────────────────────────────────────────────────────────────────────
1444
+ // Root
1445
+ // ─────────────────────────────────────────────────────────────────────────────
1446
+
1447
+ class _KasySidebarCollapsedRoot extends StatelessWidget {
1448
+ final List<BartMenuRoute> routes;
1449
+ final OnTapItem onTapItem;
1450
+ final ValueNotifier<int> currentItem;
1451
+
1452
+ const _KasySidebarCollapsedRoot({
1453
+ required this.routes,
1454
+ required this.onTapItem,
1455
+ required this.currentItem,
1456
+ });
1457
+
1458
+ @override
1459
+ Widget build(BuildContext context) {
1460
+ return ValueListenableBuilder<int>(
1461
+ valueListenable: currentItem,
1462
+ builder: (_, currentIndex, child) => _KasySidebarCollapsed(
1463
+ routes: routes,
1464
+ currentIndex: currentIndex,
1465
+ onTapItem: onTapItem,
1466
+ ),
1467
+ );
1468
+ }
1469
+ }
1470
+
1471
+ // ─────────────────────────────────────────────────────────────────────────────
1472
+ // Collapsed sidebar — stateful for ⌘K shortcut
1473
+ // ─────────────────────────────────────────────────────────────────────────────
1474
+
1475
+ class _KasySidebarCollapsed extends StatefulWidget {
1476
+ final List<BartMenuRoute> routes;
1477
+ final int currentIndex;
1478
+ final OnTapItem onTapItem;
1479
+
1480
+ const _KasySidebarCollapsed({
1481
+ required this.routes,
1482
+ required this.currentIndex,
1483
+ required this.onTapItem,
1484
+ });
1485
+
1486
+ @override
1487
+ State<_KasySidebarCollapsed> createState() => _KasySidebarCollapsedState();
1488
+ }
1489
+
1490
+ class _KasySidebarCollapsedState extends State<_KasySidebarCollapsed> {
1491
+ bool _searchOpen = false;
1492
+
1493
+ static const _projects = [
1494
+ (color: Color(0xFF34C759), name: 'Kasy Studio'),
1495
+ (color: Color(0xFF1E88E5), name: 'Design System'),
1496
+ (color: Color(0xFFF57F17), name: 'Components'),
1497
+ ];
1498
+
1499
+ static const _placeholders = [
1500
+ (icon: KasyIcons.flash, label: 'Activity'),
1501
+ (icon: KasyIcons.note, label: 'Tasks'),
1502
+ (icon: KasyIcons.book, label: 'Reports'),
1503
+ (icon: KasyIcons.person, label: 'Team'),
1504
+ ];
1505
+
1506
+ // ── Keyboard shortcut ────────────────────────────────────────────────────
1507
+
1508
+ @override
1509
+ void initState() {
1510
+ super.initState();
1511
+ HardwareKeyboard.instance.addHandler(_onKeyEvent);
1512
+ }
1513
+
1514
+ @override
1515
+ void dispose() {
1516
+ HardwareKeyboard.instance.removeHandler(_onKeyEvent);
1517
+ super.dispose();
1518
+ }
1519
+
1520
+ bool _onKeyEvent(KeyEvent event) {
1521
+ if (!mounted || event is! KeyDownEvent) return false;
1522
+ if (event.logicalKey != LogicalKeyboardKey.keyK) return false;
1523
+ final bool trigger = HardwareKeyboard.instance.isMetaPressed ||
1524
+ HardwareKeyboard.instance.isControlPressed;
1525
+ if (trigger) {
1526
+ _openSearch();
1527
+ return true;
1528
+ }
1529
+ return false;
1530
+ }
1531
+
1532
+ void _openSearch() {
1533
+ if (_searchOpen || !mounted) return;
1534
+ setState(() => _searchOpen = true);
1535
+ showGeneralDialog<void>(
1536
+ context: context,
1537
+ barrierDismissible: true,
1538
+ barrierLabel: 'Dismiss',
1539
+ barrierColor: Colors.black.withValues(alpha: 0.32),
1540
+ transitionDuration: const Duration(milliseconds: 220),
1541
+ pageBuilder: (ctx, anim, secAnim) => Align(
1542
+ alignment: const Alignment(0, -0.18),
1543
+ child: _SearchModal(
1544
+ routes: widget.routes,
1545
+ onNavigate: (index) {
1546
+ Navigator.of(ctx, rootNavigator: true).pop();
1547
+ widget.onTapItem(index);
1548
+ },
1549
+ ),
1550
+ ),
1551
+ transitionBuilder: (ctx, anim, secAnim, child) {
1552
+ final curved =
1553
+ CurvedAnimation(parent: anim, curve: Curves.easeOutCubic);
1554
+ return FadeTransition(
1555
+ opacity: curved,
1556
+ child: ScaleTransition(
1557
+ scale: Tween<double>(begin: 0.95, end: 1.0).animate(curved),
1558
+ child: child,
1559
+ ),
1560
+ );
1561
+ },
1562
+ ).then((_) {
1563
+ if (mounted) setState(() => _searchOpen = false);
1564
+ });
1565
+ }
1566
+
1567
+ // ── Build ─────────────────────────────────────────────────────────────────
1568
+
1569
+ @override
1570
+ Widget build(BuildContext context) {
1571
+ final colors = context.colors;
1572
+
1573
+ return Container(
1574
+ width: _kCollapsedWidth,
1575
+ decoration: BoxDecoration(
1576
+ color: colors.surface,
1577
+ border: Border(
1578
+ right: BorderSide(color: colors.outline.withValues(alpha: 0.4)),
1579
+ ),
1580
+ ),
1581
+ child: SafeArea(
1582
+ right: false,
1583
+ child: Column(
1584
+ children: [
1585
+ // ── Scrollable area ────────────────────────────────────────────
1586
+ Expanded(
1587
+ child: ScrollConfiguration(
1588
+ behavior: ScrollConfiguration.of(context)
1589
+ .copyWith(scrollbars: false),
1590
+ child: SingleChildScrollView(
1591
+ physics: const ClampingScrollPhysics(),
1592
+ child: Column(
1593
+ children: [
1594
+ const SizedBox(height: 16),
1595
+
1596
+ // Brand icon
1597
+ _RailIconSlot(
1598
+ child: Container(
1599
+ width: 34,
1600
+ height: 34,
1601
+ decoration: BoxDecoration(
1602
+ color: colors.primary,
1603
+ borderRadius: BorderRadius.circular(8),
1604
+ ),
1605
+ child: const Icon(
1606
+ KasyIcons.assistant,
1607
+ size: 18,
1608
+ color: Colors.white,
1609
+ ),
1610
+ ),
1611
+ ),
1612
+ const SizedBox(height: 12),
1613
+
1614
+ // Search
1615
+ _RailIconSlot(
1616
+ child: _RailIconBtn(
1617
+ icon: KasyIcons.search,
1618
+ tooltip: defaultTargetPlatform == TargetPlatform.macOS
1619
+ ? 'Search ⌘K'
1620
+ : 'Search Ctrl K',
1621
+ onTap: _openSearch,
1622
+ ),
1623
+ ),
1624
+ const SizedBox(height: 8),
1625
+ const _SidebarDivider(),
1626
+ const SizedBox(height: 8),
1627
+
1628
+ // ── Nav items ──────────────────────────────────────
1629
+ ...widget.routes.asMap().entries.map((e) {
1630
+ final hasBuilder = e.value.iconBuilder != null;
1631
+ final icon = e.value.icon ?? KasyIcons.notification;
1632
+ final label = e.value.label ?? '';
1633
+ final isActive = widget.currentIndex == e.key;
1634
+
1635
+ if (hasBuilder) {
1636
+ // Notification item — show dot badge
1637
+ return Padding(
1638
+ padding: const EdgeInsets.symmetric(vertical: 2),
1639
+ child: _CollapsedNotificationBtn(
1640
+ isActive: isActive,
1641
+ label: label,
1642
+ onTap: () => widget.onTapItem(e.key),
1643
+ ),
1644
+ );
1645
+ }
1646
+ return Padding(
1647
+ padding: const EdgeInsets.symmetric(vertical: 2),
1648
+ child: _RailNavBtn(
1649
+ icon: icon,
1650
+ tooltip: label,
1651
+ isActive: isActive,
1652
+ onTap: () => widget.onTapItem(e.key),
1653
+ ),
1654
+ );
1655
+ }),
1656
+ const SizedBox(height: 8),
1657
+ const _SidebarDivider(),
1658
+ const SizedBox(height: 8),
1659
+
1660
+ // ── Workspace icons ────────────────────────────────
1661
+ ..._placeholders.map(
1662
+ (p) => _RailIconSlot(
1663
+ child: _RailIconBtn(
1664
+ icon: p.icon,
1665
+ tooltip: p.label,
1666
+ onTap: () {},
1667
+ hapticEnabled: false,
1668
+ ),
1669
+ ),
1670
+ ),
1671
+ const SizedBox(height: 12),
1672
+ const _SidebarDivider(),
1673
+ const SizedBox(height: 10),
1674
+
1675
+ // ── Project dots ───────────────────────────────────
1676
+ ..._projects.map(
1677
+ (p) => _RailProjectDot(
1678
+ color: p.color,
1679
+ name: p.name,
1680
+ ),
1681
+ ),
1682
+ const SizedBox(height: 12),
1683
+ ],
1684
+ ),
1685
+ ),
1686
+ ),
1687
+ ),
1688
+
1689
+ // ── Theme toggle + user avatar footer ─────────────────────────
1690
+ const _RailThemeToggle(),
1691
+ const SizedBox(height: 4),
1692
+ const _SidebarDivider(),
1693
+ const _CollapsedUserFooter(),
1694
+ ],
1695
+ ),
1696
+ ),
1697
+ );
1698
+ }
1699
+ }
1700
+
1701
+ // ─────────────────────────────────────────────────────────────────────────────
1702
+ // Rail helpers
1703
+ // ─────────────────────────────────────────────────────────────────────────────
1704
+
1705
+ /// Centers a child horizontally within the 64 px rail column.
1706
+ class _RailIconSlot extends StatelessWidget {
1707
+ final Widget child;
1708
+ const _RailIconSlot({required this.child});
1709
+
1710
+ @override
1711
+ Widget build(BuildContext context) {
1712
+ return Padding(
1713
+ padding: const EdgeInsets.symmetric(vertical: 2),
1714
+ child: Center(child: child),
1715
+ );
1716
+ }
1717
+ }
1718
+
1719
+ /// Generic icon-only button: tooltip + hover.
1720
+ class _RailIconBtn extends StatelessWidget {
1721
+ final IconData icon;
1722
+ final String tooltip;
1723
+ final VoidCallback onTap;
1724
+ final bool hapticEnabled;
1725
+
1726
+ const _RailIconBtn({
1727
+ required this.icon,
1728
+ required this.tooltip,
1729
+ required this.onTap,
1730
+ this.hapticEnabled = true,
1731
+ });
1732
+
1733
+ @override
1734
+ Widget build(BuildContext context) {
1735
+ final colors = context.colors;
1736
+ return Tooltip(
1737
+ message: tooltip,
1738
+ preferBelow: false,
1739
+ waitDuration: const Duration(milliseconds: 400),
1740
+ child: KasyHover(
1741
+ onTap: onTap,
1742
+ hapticEnabled: hapticEnabled,
1743
+ borderRadius: BorderRadius.circular(_kItemRadius),
1744
+ child: SizedBox(
1745
+ width: _kRailIconSize,
1746
+ height: _kRailIconSize,
1747
+ child: Icon(
1748
+ icon,
1749
+ size: 19,
1750
+ color: colors.onSurface.withValues(alpha: 0.6),
1751
+ ),
1752
+ ),
1753
+ ),
1754
+ );
1755
+ }
1756
+ }
1757
+
1758
+ /// Nav button with a left accent bar that appears when active.
1759
+ class _RailNavBtn extends StatelessWidget {
1760
+ final IconData icon;
1761
+ final String tooltip;
1762
+ final bool isActive;
1763
+ final VoidCallback onTap;
1764
+
1765
+ const _RailNavBtn({
1766
+ required this.icon,
1767
+ required this.tooltip,
1768
+ required this.isActive,
1769
+ required this.onTap,
1770
+ });
1771
+
1772
+ @override
1773
+ Widget build(BuildContext context) {
1774
+ final colors = context.colors;
1775
+ const double slotHeight = _kRailIconSize + 4;
1776
+
1777
+ return Tooltip(
1778
+ message: tooltip,
1779
+ preferBelow: false,
1780
+ waitDuration: const Duration(milliseconds: 400),
1781
+ child: SizedBox(
1782
+ width: _kCollapsedWidth,
1783
+ height: slotHeight,
1784
+ child: Stack(
1785
+ alignment: Alignment.center,
1786
+ children: [
1787
+ // Left accent bar
1788
+ Positioned(
1789
+ left: 0,
1790
+ top: (slotHeight - 20) / 2,
1791
+ child: AnimatedContainer(
1792
+ duration: const Duration(milliseconds: 160),
1793
+ width: 3,
1794
+ height: 20,
1795
+ decoration: BoxDecoration(
1796
+ color: isActive ? colors.primary : Colors.transparent,
1797
+ borderRadius: const BorderRadius.only(
1798
+ topRight: Radius.circular(2),
1799
+ bottomRight: Radius.circular(2),
1800
+ ),
1801
+ ),
1802
+ ),
1803
+ ),
1804
+ // Icon with hover
1805
+ KasyHover(
1806
+ onTap: onTap,
1807
+ borderRadius: BorderRadius.circular(_kItemRadius),
1808
+ child: AnimatedContainer(
1809
+ duration: const Duration(milliseconds: 160),
1810
+ curve: Curves.easeOut,
1811
+ width: _kRailIconSize,
1812
+ height: _kRailIconSize,
1813
+ decoration: BoxDecoration(
1814
+ color: isActive
1815
+ ? colors.surfacePrimarySoft
1816
+ : Colors.transparent,
1817
+ borderRadius: BorderRadius.circular(_kItemRadius),
1818
+ ),
1819
+ child: Icon(
1820
+ icon,
1821
+ size: 19,
1822
+ color: isActive
1823
+ ? colors.primary
1824
+ : colors.onSurface.withValues(alpha: 0.6),
1825
+ ),
1826
+ ),
1827
+ ),
1828
+ ],
1829
+ ),
1830
+ ),
1831
+ );
1832
+ }
1833
+ }
1834
+
1835
+ /// Notification button with a live unread dot indicator.
1836
+ class _CollapsedNotificationBtn extends ConsumerWidget {
1837
+ final bool isActive;
1838
+ final String label;
1839
+ final VoidCallback onTap;
1840
+
1841
+ const _CollapsedNotificationBtn({
1842
+ required this.isActive,
1843
+ required this.label,
1844
+ required this.onTap,
1845
+ });
1846
+
1847
+ @override
1848
+ Widget build(BuildContext context, WidgetRef ref) {
1849
+ final colors = context.colors;
1850
+ final String? userId =
1851
+ ref.watch(userStateNotifierProvider).user.idOrNull;
1852
+ const double slotHeight = _kRailIconSize + 4;
1853
+
1854
+ return Tooltip(
1855
+ message: label,
1856
+ preferBelow: false,
1857
+ waitDuration: const Duration(milliseconds: 400),
1858
+ child: SizedBox(
1859
+ width: _kCollapsedWidth,
1860
+ height: slotHeight,
1861
+ child: Stack(
1862
+ alignment: Alignment.center,
1863
+ children: [
1864
+ // Left accent bar
1865
+ Positioned(
1866
+ left: 0,
1867
+ top: (slotHeight - 20) / 2,
1868
+ child: AnimatedContainer(
1869
+ duration: const Duration(milliseconds: 160),
1870
+ width: 3,
1871
+ height: 20,
1872
+ decoration: BoxDecoration(
1873
+ color: isActive ? colors.primary : Colors.transparent,
1874
+ borderRadius: const BorderRadius.only(
1875
+ topRight: Radius.circular(2),
1876
+ bottomRight: Radius.circular(2),
1877
+ ),
1878
+ ),
1879
+ ),
1880
+ ),
1881
+ // Icon + dot badge
1882
+ KasyHover(
1883
+ onTap: onTap,
1884
+ borderRadius: BorderRadius.circular(_kItemRadius),
1885
+ child: AnimatedContainer(
1886
+ duration: const Duration(milliseconds: 160),
1887
+ curve: Curves.easeOut,
1888
+ width: _kRailIconSize,
1889
+ height: _kRailIconSize,
1890
+ decoration: BoxDecoration(
1891
+ color:
1892
+ isActive ? colors.surfacePrimarySoft : Colors.transparent,
1893
+ borderRadius: BorderRadius.circular(_kItemRadius),
1894
+ ),
1895
+ child: Stack(
1896
+ clipBehavior: Clip.none,
1897
+ alignment: Alignment.center,
1898
+ children: [
1899
+ Icon(
1900
+ KasyIcons.notification,
1901
+ size: 19,
1902
+ color: isActive
1903
+ ? colors.primary
1904
+ : colors.onSurface.withValues(alpha: 0.6),
1905
+ ),
1906
+ if (userId != null)
1907
+ StreamBuilder<int>(
1908
+ stream: ref
1909
+ .read(notificationRepositoryProvider)
1910
+ .listenToUnreadNotificationsCount(userId),
1911
+ builder: (_, snapshot) {
1912
+ final int count = snapshot.data ?? 0;
1913
+ if (count == 0) return const SizedBox.shrink();
1914
+ return Positioned(
1915
+ top: 7,
1916
+ right: 7,
1917
+ child: Container(
1918
+ width: 7,
1919
+ height: 7,
1920
+ decoration: BoxDecoration(
1921
+ color: colors.error,
1922
+ shape: BoxShape.circle,
1923
+ border: Border.all(
1924
+ color: colors.surface,
1925
+ width: 1.2,
1926
+ ),
1927
+ ),
1928
+ ),
1929
+ );
1930
+ },
1931
+ ),
1932
+ ],
1933
+ ),
1934
+ ),
1935
+ ),
1936
+ ],
1937
+ ),
1938
+ ),
1939
+ );
1940
+ }
1941
+ }
1942
+
1943
+ /// Small colored dot for the projects section in collapsed mode.
1944
+ class _RailProjectDot extends StatelessWidget {
1945
+ final Color color;
1946
+ final String name;
1947
+
1948
+ const _RailProjectDot({required this.color, required this.name});
1949
+
1950
+ @override
1951
+ Widget build(BuildContext context) {
1952
+ return _RailIconSlot(
1953
+ child: Tooltip(
1954
+ message: name,
1955
+ preferBelow: false,
1956
+ waitDuration: const Duration(milliseconds: 400),
1957
+ child: KasyHover(
1958
+ onTap: () {},
1959
+ hapticEnabled: false,
1960
+ borderRadius: BorderRadius.circular(_kItemRadius),
1961
+ child: SizedBox(
1962
+ width: _kRailIconSize,
1963
+ height: _kRailIconSize,
1964
+ child: Center(
1965
+ child: Container(
1966
+ width: 10,
1967
+ height: 10,
1968
+ decoration: BoxDecoration(
1969
+ color: color,
1970
+ shape: BoxShape.circle,
1971
+ boxShadow: [
1972
+ BoxShadow(
1973
+ color: color.withValues(alpha: 0.5),
1974
+ blurRadius: 5,
1975
+ offset: const Offset(0, 1),
1976
+ ),
1977
+ ],
1978
+ ),
1979
+ ),
1980
+ ),
1981
+ ),
1982
+ ),
1983
+ ),
1984
+ );
1985
+ }
1986
+ }
1987
+
1988
+ /// User avatar in the collapsed rail footer.
1989
+ class _CollapsedUserFooter extends ConsumerWidget {
1990
+ const _CollapsedUserFooter();
1991
+
1992
+ @override
1993
+ Widget build(BuildContext context, WidgetRef ref) {
1994
+ final user = ref.watch(userStateNotifierProvider).user;
1995
+ final String displayName = user is AuthenticatedUserData
1996
+ ? (user.name ?? user.email.split('@').first)
1997
+ : 'Guest';
1998
+ final String initials =
1999
+ displayName.isNotEmpty ? displayName[0].toUpperCase() : 'G';
2000
+
2001
+ return KasyHover(
2002
+ onTap: () {},
2003
+ hapticEnabled: false,
2004
+ child: Padding(
2005
+ padding: const EdgeInsets.symmetric(vertical: 10),
2006
+ child: Center(
2007
+ child: Tooltip(
2008
+ message: displayName,
2009
+ preferBelow: false,
2010
+ waitDuration: const Duration(milliseconds: 400),
2011
+ child: KasyAvatar(
2012
+ size: KasyAvatarSize.small,
2013
+ diameter: 32,
2014
+ initials: initials,
2015
+ ),
2016
+ ),
2017
+ ),
2018
+ ),
2019
+ );
2020
+ }
2021
+ }