kasy-cli 1.21.9 → 1.23.0

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 (269) hide show
  1. package/lib/commands/add.js +93 -80
  2. package/lib/commands/configure.js +100 -32
  3. package/lib/commands/doctor.js +28 -2
  4. package/lib/commands/new.js +80 -37
  5. package/lib/commands/notifications.js +1 -1
  6. package/lib/commands/remove.js +43 -15
  7. package/lib/commands/run.js +2 -2
  8. package/lib/commands/update.js +2 -2
  9. package/lib/scaffold/CHANGELOG.json +14 -0
  10. package/lib/scaffold/backends/api/generator.js +14 -14
  11. package/lib/scaffold/backends/api/patch/README.md +83 -0
  12. package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
  13. package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
  14. package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
  15. package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
  16. package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
  17. package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +5 -5
  18. package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
  19. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
  20. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
  21. package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
  22. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  23. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  24. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
  25. package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  26. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
  27. package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  28. package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
  29. package/lib/scaffold/backends/firebase/deploy.js +87 -13
  30. package/lib/scaffold/backends/firebase/generator.js +5 -5
  31. package/lib/scaffold/backends/firebase/tokens.js +4 -4
  32. package/lib/scaffold/backends/supabase/deploy.js +63 -11
  33. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
  34. package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
  35. package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
  36. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
  37. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
  38. package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
  39. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
  40. package/lib/scaffold/backends/supabase/generator.js +17 -17
  41. package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
  42. package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
  43. package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
  44. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
  45. package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
  46. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
  47. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
  48. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
  49. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
  50. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
  51. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
  52. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
  53. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  54. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  55. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
  56. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  57. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
  58. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  59. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
  60. package/lib/scaffold/backends/supabase/tokens.js +3 -3
  61. package/lib/scaffold/catalog.js +9 -11
  62. package/lib/scaffold/generate.js +45 -31
  63. package/lib/scaffold/shared/generator-utils.js +188 -81
  64. package/lib/scaffold/shared/sort-imports.js +191 -0
  65. package/lib/scaffold/shared/template-strings.js +3 -3
  66. package/lib/utils/checks.js +22 -6
  67. package/lib/utils/env-tools.js +7 -0
  68. package/lib/utils/flutter-install.js +114 -0
  69. package/lib/utils/i18n/messages-en.js +52 -35
  70. package/lib/utils/i18n/messages-es.js +52 -35
  71. package/lib/utils/i18n/messages-pt.js +54 -37
  72. package/lib/utils/updates.js +15 -15
  73. package/package.json +1 -1
  74. package/templates/firebase/.env.example +2 -2
  75. package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
  76. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  77. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  78. package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
  79. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
  80. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
  81. package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
  82. package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
  83. package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
  84. package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
  85. package/templates/firebase/firestore.rules +24 -5
  86. package/templates/firebase/functions/package-lock.json +22 -1
  87. package/templates/firebase/functions/package.json +2 -1
  88. package/templates/firebase/functions/src/admin/functions.ts +113 -0
  89. package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
  90. package/templates/firebase/functions/src/index.ts +8 -2
  91. package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
  92. package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
  93. package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
  94. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
  95. package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
  96. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
  97. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  98. package/templates/firebase/lib/components/components.dart +4 -1
  99. package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
  100. package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
  101. package/templates/firebase/lib/components/kasy_button.dart +23 -99
  102. package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
  103. package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
  104. package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +702 -425
  105. package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
  106. package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
  107. package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
  108. package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
  109. package/templates/firebase/lib/components/kasy_toast.dart +2 -2
  110. package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
  111. package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
  112. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +136 -23
  113. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
  114. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
  115. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +54 -0
  116. package/templates/firebase/lib/core/config/app_env.dart +5 -11
  117. package/templates/firebase/lib/core/config/features.dart +5 -4
  118. package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
  119. package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
  120. package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
  121. package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
  122. package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
  123. package/templates/firebase/lib/core/data/models/user.dart +11 -0
  124. package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
  125. package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
  126. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
  127. package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
  128. package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
  129. package/templates/firebase/lib/core/states/logout_action.dart +25 -0
  130. package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
  131. package/templates/firebase/lib/core/theme/colors.dart +488 -188
  132. package/templates/firebase/lib/core/theme/radius.dart +22 -11
  133. package/templates/firebase/lib/core/theme/shadows.dart +66 -0
  134. package/templates/firebase/lib/core/theme/texts.dart +75 -41
  135. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
  136. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
  137. package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
  138. package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
  139. package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
  140. package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
  141. package/templates/firebase/lib/core/widgets/kasy_hover.dart +53 -14
  142. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
  143. package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
  144. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
  145. package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
  146. package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
  147. package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
  148. package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
  149. package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
  150. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
  151. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
  152. package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
  153. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
  154. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
  155. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
  156. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
  157. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
  158. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
  159. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
  160. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
  161. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +128 -0
  162. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
  163. package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
  164. package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
  165. package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
  166. package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
  167. package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
  168. package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
  169. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
  170. package/templates/firebase/lib/features/home/home_feed.dart +289 -0
  171. package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
  172. package/templates/firebase/lib/features/home/home_page.dart +11 -250
  173. package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
  174. package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
  175. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
  176. package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
  177. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
  178. package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
  179. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  180. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
  181. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
  182. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
  183. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
  184. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
  185. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
  186. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
  187. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
  188. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
  189. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
  190. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  191. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
  192. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
  193. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
  194. package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
  195. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
  196. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
  197. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
  198. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
  199. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
  200. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
  201. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
  202. package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
  203. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
  204. package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
  205. package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
  206. package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
  207. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
  208. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
  209. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
  210. package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  211. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
  212. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
  213. package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
  214. package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
  215. package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
  216. package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
  217. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
  218. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
  219. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
  220. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
  221. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
  222. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
  223. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
  224. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
  225. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
  226. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
  227. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
  228. package/templates/firebase/lib/i18n/en.i18n.json +171 -46
  229. package/templates/firebase/lib/i18n/es.i18n.json +175 -50
  230. package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
  231. package/templates/firebase/lib/main.dart +6 -3
  232. package/templates/firebase/lib/router.dart +15 -23
  233. package/templates/firebase/pubspec.yaml +5 -6
  234. package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
  235. package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
  236. package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
  237. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
  238. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
  239. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
  240. package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
  241. package/templates/firebase/test/test_utils.dart +6 -6
  242. package/templates/firebase/web/index.html +5 -2
  243. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
  244. package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
  245. package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
  246. package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
  247. package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
  248. package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
  249. package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
  250. package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
  251. package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
  252. package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
  253. package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
  254. package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
  255. package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
  256. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
  257. package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
  258. /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
  259. /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
  260. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
  261. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
  262. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
  263. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
  264. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
  265. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
  266. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
  267. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
  268. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
  269. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
@@ -0,0 +1,91 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:kasy_kit/core/theme/theme.dart';
3
+
4
+ /// Accent tone for [KasyStatusTag], resolved against the current theme.
5
+ enum KasyStatusTagTone { success, neutral, primary, warning, danger }
6
+
7
+ /// Compact, content-sized status pill: a soft-tinted rounded badge with an
8
+ /// optional leading dot and a colored label. Built for table cells, list rows
9
+ /// and detail headers where you want a quiet status indicator (active/inactive,
10
+ /// paid/free, visible/hidden, …) — not a loud solid badge.
11
+ ///
12
+ /// It sizes to its content. Inside a tight parent (e.g. a table column wrapped
13
+ /// in [Expanded]) wrap it in an [Align] so it doesn't stretch:
14
+ ///
15
+ /// ```dart
16
+ /// Expanded(
17
+ /// child: Align(
18
+ /// alignment: Alignment.centerLeft,
19
+ /// child: KasyStatusTag(label: 'Active', tone: KasyStatusTagTone.success),
20
+ /// ),
21
+ /// )
22
+ /// ```
23
+ class KasyStatusTag extends StatelessWidget {
24
+ /// The text shown in the pill.
25
+ final String label;
26
+
27
+ /// Semantic tone (maps to theme colors). Ignored when [color] is set.
28
+ final KasyStatusTagTone tone;
29
+
30
+ /// Overrides the tone's accent color (used for both the dot and the label).
31
+ final Color? color;
32
+
33
+ /// Whether to show the small leading status dot.
34
+ final bool showDot;
35
+
36
+ const KasyStatusTag({
37
+ super.key,
38
+ required this.label,
39
+ this.tone = KasyStatusTagTone.neutral,
40
+ this.color,
41
+ this.showDot = true,
42
+ });
43
+
44
+ @override
45
+ Widget build(BuildContext context) {
46
+ final Color accent = color ?? _toneColor(context, tone);
47
+ return Container(
48
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
49
+ decoration: BoxDecoration(
50
+ color: accent.withValues(alpha: context.isDark ? 0.16 : 0.1),
51
+ borderRadius: BorderRadius.circular(KasyRadius.full),
52
+ ),
53
+ child: Row(
54
+ mainAxisSize: MainAxisSize.min,
55
+ children: [
56
+ if (showDot) ...[
57
+ Container(
58
+ width: 6,
59
+ height: 6,
60
+ decoration: BoxDecoration(color: accent, shape: BoxShape.circle),
61
+ ),
62
+ const SizedBox(width: 6),
63
+ ],
64
+ Flexible(
65
+ child: Text(
66
+ label,
67
+ maxLines: 1,
68
+ overflow: TextOverflow.ellipsis,
69
+ style: context.textTheme.labelSmall?.copyWith(
70
+ color: accent,
71
+ fontWeight: FontWeight.w600,
72
+ fontSize: 11.5,
73
+ ),
74
+ ),
75
+ ),
76
+ ],
77
+ ),
78
+ );
79
+ }
80
+
81
+ Color _toneColor(BuildContext context, KasyStatusTagTone tone) {
82
+ final c = context.colors;
83
+ return switch (tone) {
84
+ KasyStatusTagTone.success => c.success,
85
+ KasyStatusTagTone.neutral => c.muted,
86
+ KasyStatusTagTone.primary => c.primary,
87
+ KasyStatusTagTone.warning => c.warning,
88
+ KasyStatusTagTone.danger => c.error,
89
+ };
90
+ }
91
+ }
@@ -152,9 +152,7 @@ class _KasyTextAreaState extends State<KasyTextArea> {
152
152
  final bool isApplePlatform =
153
153
  platform == TargetPlatform.iOS || platform == TargetPlatform.macOS;
154
154
  final double focusedBorderWidth = isApplePlatform ? 1.7 : 1.4;
155
- final Color focusedBorderColor = isApplePlatform
156
- ? const Color(0xFF0A84FF)
157
- : context.colors.primary;
155
+ final Color focusedBorderColor = context.colors.primary;
158
156
  final Color resolvedFocusedBorderColor = hasInvalidState
159
157
  ? context.colors.error
160
158
  : focusedBorderColor;
@@ -67,8 +67,9 @@ class KasyTextField extends StatefulWidget {
67
67
  /// on secondary/embedded/web). Pass an empty list to render no shadow.
68
68
  final List<BoxShadow>? boxShadow;
69
69
 
70
- /// When true (default), the field shows the design-system blue focus border
71
- /// while focused. Set to false to keep the resting border in every state —
70
+ /// When true (default), the field shows the theme primary-colored focus
71
+ /// border while focused. Set to false to keep the resting border in every
72
+ /// state —
72
73
  /// useful for read-only triggers or compact contexts where the focus
73
74
  /// affordance would feel noisy.
74
75
  final bool focusBorder;
@@ -331,9 +332,7 @@ class _KasyTextFieldState extends State<KasyTextField> {
331
332
  final bool isApplePlatform =
332
333
  platform == TargetPlatform.iOS || platform == TargetPlatform.macOS;
333
334
  final double focusedBorderWidth = isApplePlatform ? 1.7 : 1.4;
334
- final Color focusedBorderColor = isApplePlatform
335
- ? const Color(0xFF0A84FF)
336
- : context.colors.primary;
335
+ final Color focusedBorderColor = context.colors.primary;
337
336
  final Color resolvedFocusedBorderColor = hasInvalidState
338
337
  ? context.colors.error
339
338
  : focusedBorderColor;
@@ -538,7 +537,7 @@ class _KasyTextFieldState extends State<KasyTextField> {
538
537
  decoration: decoration,
539
538
  enableInteractiveSelection: widget.enableInteractiveSelection,
540
539
  );
541
- // TESTE: sombra desabilitada para investigar diferença visual com DatePicker
540
+ // TEST: shadow disabled to investigate the visual difference with DatePicker
542
541
  final List<BoxShadow>? effectiveBoxShadow = widget.boxShadow ??
543
542
  (shouldShowShadow ? <BoxShadow>[resolvedShadow] : null);
544
543
  final Widget decoratedField = DecoratedBox(
@@ -142,9 +142,7 @@ class _KasyTextFieldOTPState extends State<KasyTextFieldOTP> {
142
142
  final bool isApple =
143
143
  Theme.of(context).platform == TargetPlatform.iOS ||
144
144
  Theme.of(context).platform == TargetPlatform.macOS;
145
- final Color focusColor = isApple
146
- ? const Color(0xFF0A84FF)
147
- : context.colors.primary;
145
+ final Color focusColor = context.colors.primary;
148
146
  final double focusBorderWidth = isApple ? 1.7 : 1.4;
149
147
  final Color errorColor = context.colors.error;
150
148
 
@@ -501,8 +501,8 @@ class _KasyToastLayerState extends State<_KasyToastLayer>
501
501
  final int fromFront = visible.indexOf(item);
502
502
  final bool isFront = fromFront == 0;
503
503
 
504
- // Para top: usa altura medida para que o peek seja sempre _peekOffset px.
505
- // Para bottom: offset negativo fixo garante peek consistente.
504
+ // For top: use the measured height so the peek is always _peekOffset px.
505
+ // For bottom: a fixed negative offset already guarantees a consistent peek.
506
506
  double yOffset;
507
507
  if (isTop && frontHeight != null) {
508
508
  final double cardH = _heights[item] ?? frontHeight;
@@ -0,0 +1,209 @@
1
+ /// Application top header for the **web desktop breakpoint** (viewport ≥ 1024px).
2
+ ///
3
+ /// This is the *application* chrome (global search, quick-create, notifications,
4
+ /// profile) — distinct from [KasyAppBar], which is *page* chrome (title / back /
5
+ /// theme) used on phone and tablet. On desktop the sidebar handles navigation,
6
+ /// so this header carries global actions instead of a title or back button.
7
+ ///
8
+ /// Composed entirely from existing kit widgets: a [KasyTextField] search box
9
+ /// (fixed 220px), a ghost [KasyButton.iconOnly] for notifications, a neutral
10
+ /// pill [KasyButton] for create, and a gradient [KasyAvatar].
11
+ ///
12
+ /// Barrel: [components.dart].
13
+ library;
14
+
15
+ import 'package:flutter/material.dart';
16
+ import 'package:kasy_kit/components/kasy_avatar.dart';
17
+ import 'package:kasy_kit/components/kasy_avatar_presets.dart';
18
+ import 'package:kasy_kit/components/kasy_button.dart';
19
+ import 'package:kasy_kit/components/kasy_text_field.dart';
20
+ import 'package:kasy_kit/core/theme/theme.dart';
21
+
22
+ /// Total header height (matches the design: 36px content band + 16px top/bottom).
23
+ const double kasyWebHeaderHeight = 68;
24
+
25
+ /// Fixed width of the leading search field (matches the design).
26
+ const double kasyWebHeaderSearchWidth = 220;
27
+
28
+ /// Desktop application header. Place it at the top of the content area (to the
29
+ /// right of the sidebar) on viewports ≥ 1024px.
30
+ class KasyWebHeader extends StatelessWidget {
31
+ /// Controller for the search field. Optional — omit for a display-only header.
32
+ final TextEditingController? searchController;
33
+
34
+ /// Placeholder shown in the search field.
35
+ final String searchHint;
36
+
37
+ /// Called as the user types in the search field.
38
+ final ValueChanged<String>? onSearchChanged;
39
+
40
+ /// Called when the search field is submitted (Enter).
41
+ final ValueChanged<String>? onSearchSubmitted;
42
+
43
+ /// Notifications (bell) action. When null the bell is disabled.
44
+ final VoidCallback? onNotifications;
45
+
46
+ /// Shows the unread dot on the notifications bell.
47
+ final bool showNotificationBadge;
48
+
49
+ /// Primary quick-create action. When null the button is disabled.
50
+ final VoidCallback? onCreate;
51
+
52
+ /// Label for the create button.
53
+ final String createLabel;
54
+
55
+ /// Gradient used for the profile avatar fallback (when [avatar] is null).
56
+ final KasyAvatarGradientData avatarGradient;
57
+
58
+ /// Custom avatar widget — pass the signed-in user's avatar (e.g.
59
+ /// `KasyUserAvatar`) to show their real photo. When null (and [showAvatar] is
60
+ /// true), a gradient-fill avatar is shown instead.
61
+ final Widget? avatar;
62
+
63
+ /// Whether the profile avatar is shown at all. Set false for a header that
64
+ /// carries no account chip (e.g. when the sidebar already owns the profile).
65
+ final bool showAvatar;
66
+
67
+ /// Profile avatar tap (open menu / profile). When null the avatar is inert.
68
+ final VoidCallback? onAvatarTap;
69
+
70
+ /// Theme toggle. When set, a sun/moon ghost button is shown before the bell
71
+ /// (on desktop the web header replaces the app bar's theme toggle).
72
+ final VoidCallback? onToggleTheme;
73
+
74
+ const KasyWebHeader({
75
+ super.key,
76
+ this.searchController,
77
+ this.searchHint = 'Search...',
78
+ this.onSearchChanged,
79
+ this.onSearchSubmitted,
80
+ this.onNotifications,
81
+ this.showNotificationBadge = false,
82
+ this.onCreate,
83
+ this.createLabel = 'Create',
84
+ this.avatarGradient = KasyAvatarGradients.orange,
85
+ this.avatar,
86
+ this.showAvatar = true,
87
+ this.onAvatarTap,
88
+ this.onToggleTheme,
89
+ });
90
+
91
+ @override
92
+ Widget build(BuildContext context) {
93
+ final KasyColors c = context.colors;
94
+ return DecoratedBox(
95
+ decoration: BoxDecoration(
96
+ // Matches KasySidebar exactly: surface fill + the same `border` hairline
97
+ // used by the sidebar's vertical edge line, at the same 0.5px width — so
98
+ // the header's bottom border and the sidebar's top divider read as one
99
+ // continuous line across the whole chrome in light and dark.
100
+ color: c.surface,
101
+ border: Border(
102
+ bottom: BorderSide(color: c.border, width: 0.5),
103
+ ),
104
+ ),
105
+ // minHeight (not a fixed height) keeps the band at 68 while letting the
106
+ // search field size naturally, so it never overflows the toolbar row.
107
+ child: ConstrainedBox(
108
+ constraints: const BoxConstraints(minHeight: kasyWebHeaderHeight),
109
+ child: Padding(
110
+ // Design gutters: 20px horizontal; vertical breathes around the field.
111
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
112
+ child: Row(
113
+ children: [
114
+ SizedBox(
115
+ width: kasyWebHeaderSearchWidth,
116
+ child: _buildSearch(context),
117
+ ),
118
+ const Spacer(),
119
+ if (onToggleTheme != null) ...[
120
+ _buildThemeToggle(context),
121
+ const SizedBox(width: KasySpacing.md),
122
+ ],
123
+ _buildNotifications(context),
124
+ const SizedBox(width: KasySpacing.md),
125
+ KasyButton(
126
+ label: createLabel,
127
+ variant: KasyButtonVariant.neutral,
128
+ size: KasyButtonSize.small,
129
+ onPressed: onCreate,
130
+ ),
131
+ if (showAvatar) ...[
132
+ const SizedBox(width: KasySpacing.md),
133
+ avatar ??
134
+ KasyAvatar.gradientFill(
135
+ size: KasyAvatarSize.small,
136
+ diameter: 36,
137
+ gradient: avatarGradient,
138
+ showShadow: false,
139
+ onTap: onAvatarTap,
140
+ ),
141
+ ],
142
+ ],
143
+ ),
144
+ ),
145
+ ),
146
+ );
147
+ }
148
+
149
+ Widget _buildSearch(BuildContext context) {
150
+ return KasyTextField(
151
+ controller: searchController,
152
+ hint: searchHint,
153
+ onChanged: onSearchChanged,
154
+ onSubmitted: onSearchSubmitted,
155
+ prefix: Icon(
156
+ KasyIcons.search,
157
+ size: 17,
158
+ color: context.colors.muted,
159
+ ),
160
+ contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
161
+ );
162
+ }
163
+
164
+ Widget _buildThemeToggle(BuildContext context) {
165
+ final bool isDark = Theme.of(context).brightness == Brightness.dark;
166
+ return KasyButton.iconOnly(
167
+ icon: isDark ? KasyIcons.lightMode : KasyIcons.darkMode,
168
+ variant: KasyButtonVariant.ghost,
169
+ size: KasyButtonSize.small,
170
+ iconOnlyLayoutExtent: 36,
171
+ iconGlyphSize: 18,
172
+ onPressed: onToggleTheme,
173
+ semanticLabel: isDark ? 'Light mode' : 'Dark mode',
174
+ );
175
+ }
176
+
177
+ Widget _buildNotifications(BuildContext context) {
178
+ final KasyColors c = context.colors;
179
+ final Widget bell = KasyButton.iconOnly(
180
+ icon: KasyIcons.notification,
181
+ variant: KasyButtonVariant.ghost,
182
+ size: KasyButtonSize.small,
183
+ iconOnlyLayoutExtent: 36,
184
+ iconGlyphSize: 18,
185
+ onPressed: onNotifications,
186
+ semanticLabel: 'Notifications',
187
+ );
188
+ if (!showNotificationBadge) return bell;
189
+ return Stack(
190
+ clipBehavior: Clip.none,
191
+ children: [
192
+ bell,
193
+ Positioned(
194
+ top: 8,
195
+ right: 8,
196
+ child: Container(
197
+ width: 8,
198
+ height: 8,
199
+ decoration: BoxDecoration(
200
+ color: c.error,
201
+ shape: BoxShape.circle,
202
+ border: Border.all(color: c.background, width: 1.5),
203
+ ),
204
+ ),
205
+ ),
206
+ ],
207
+ );
208
+ }
209
+ }
@@ -1,7 +1,7 @@
1
1
  // ignore_for_file: unused_element, unused_element_parameter
2
2
 
3
3
  import 'package:kasy_kit/core/ads/ads_state.dart';
4
- import 'package:kasy_kit/environnements.dart';
4
+ import 'package:kasy_kit/environments.dart';
5
5
  import 'package:logger/logger.dart';
6
6
  import 'package:riverpod_annotation/riverpod_annotation.dart';
7
7
  import 'package:universal_io/io.dart';
@@ -2,11 +2,17 @@ import 'package:bart/bart.dart' as bart;
2
2
  import 'package:flutter/foundation.dart';
3
3
  import 'package:flutter/material.dart';
4
4
  import 'package:flutter/services.dart';
5
- import 'package:kasy_kit/components/kasy_sidebar_pro.dart';
5
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
6
+ import 'package:kasy_kit/components/kasy_sidebar.dart';
6
7
  import 'package:kasy_kit/core/bottom_menu/bottom_router.dart';
7
8
  import 'package:kasy_kit/core/bottom_menu/kasy_bottom_bar_factory.dart';
9
+ import 'package:kasy_kit/core/data/models/user.dart';
10
+ import 'package:kasy_kit/core/states/logout_action.dart';
11
+ import 'package:kasy_kit/core/states/user_state_notifier.dart';
8
12
  import 'package:kasy_kit/core/theme/theme.dart';
9
13
  import 'package:kasy_kit/core/widgets/responsive_layout.dart';
14
+ import 'package:kasy_kit/features/settings/ui/widgets/kasy_user_avatar.dart';
15
+ import 'package:kasy_kit/i18n/translations.g.dart';
10
16
 
11
17
  /// Bottom navigation host powered by Bart (https://pub.dev/packages/bart).
12
18
  ///
@@ -27,8 +33,64 @@ class BottomMenu extends StatelessWidget {
27
33
  );
28
34
  final scaffoldOptions = bart.ScaffoldOptions(
29
35
  backgroundColor: context.colors.background,
36
+ // Let tab content scroll behind the floating bar; the page-level scrolls
37
+ // reserve the bar's height via the bottom inset Scaffold injects here.
38
+ extendBody: true,
30
39
  );
31
40
 
41
+ // Desktop/tablet sidebar, marked as the FIRST stop in the keyboard Tab
42
+ // order. Together with the header/content orders set in WebContentWrapper
43
+ // and the OrderedTraversalPolicy below, every screen tabs the same way:
44
+ // sidebar → header → content. (Touch/mobile has no Tab, so the `small`
45
+ // layout below is left untouched.)
46
+ bart.CustomSideBarOptions connectedSidebar() => bart.CustomSideBarOptions(
47
+ sideBarBuilder: (routes, onTap, current) => FocusTraversalOrder(
48
+ order: const NumericFocusOrder(1),
49
+ child: _FocusableSidebar(
50
+ currentItem: current,
51
+ child: Consumer(
52
+ builder: (context, ref, _) {
53
+ final User user =
54
+ ref.watch(userStateNotifierProvider).user;
55
+ final (String name, String email) = switch (user) {
56
+ final AuthenticatedUserData u => (
57
+ (u.name?.isNotEmpty ?? false)
58
+ ? u.name!
59
+ : u.email.split('@').first,
60
+ u.email,
61
+ ),
62
+ _ => (context.t.settings.my_account, ''),
63
+ };
64
+ return KasySidebar(
65
+ routes: routes,
66
+ onTapItem: onTap,
67
+ currentItem: current,
68
+ onLogout: () => confirmLogout(context, ref),
69
+ profileName: name,
70
+ profileEmail: email,
71
+ profileAvatar: const KasyUserAvatar(),
72
+ );
73
+ },
74
+ ),
75
+ ),
76
+ ),
77
+ );
78
+
79
+ // medium and large are identical here — the sidebar collapses itself by
80
+ // viewport width. The OrderedTraversalPolicy turns the numbered orders
81
+ // (sidebar=1, header=2, content=3) into the actual Tab sequence.
82
+ Widget connectedScaffold() => FocusTraversalGroup(
83
+ policy: OrderedTraversalPolicy(),
84
+ child: bart.BartScaffold(
85
+ routesBuilder: subRoutes,
86
+ bottomBar: kasyPaddedSurfaceBottomBar(),
87
+ initialRoute: resolvedInitialRoute,
88
+ showBottomBarOnStart: showBottomBarOnStart,
89
+ scaffoldOptions: scaffoldOptions,
90
+ sideBarOptions: connectedSidebar(),
91
+ ),
92
+ );
93
+
32
94
  return AnnotatedRegion<SystemUiOverlayStyle>(
33
95
  value: switch (Theme.brightnessOf(context)) {
34
96
  Brightness.dark => SystemUiOverlayStyle.light,
@@ -42,28 +104,8 @@ class BottomMenu extends StatelessWidget {
42
104
  showBottomBarOnStart: showBottomBarOnStart,
43
105
  scaffoldOptions: scaffoldOptions,
44
106
  ),
45
- // medium (768–1024 px): KasySidebarPro auto-collapses at this width
46
- medium: bart.BartScaffold(
47
- routesBuilder: subRoutes,
48
- bottomBar: kasyPaddedSurfaceBottomBar(),
49
- initialRoute: resolvedInitialRoute,
50
- showBottomBarOnStart: showBottomBarOnStart,
51
- scaffoldOptions: scaffoldOptions,
52
- sideBarOptions: bart.CustomSideBarOptions(
53
- sideBarBuilder: (routes, onTap, current) => const KasySidebarPro(),
54
- ),
55
- ),
56
- // large (1024 px+): full expanded sidebar
57
- large: bart.BartScaffold(
58
- routesBuilder: subRoutes,
59
- bottomBar: kasyPaddedSurfaceBottomBar(),
60
- initialRoute: resolvedInitialRoute,
61
- showBottomBarOnStart: showBottomBarOnStart,
62
- scaffoldOptions: scaffoldOptions,
63
- sideBarOptions: bart.CustomSideBarOptions(
64
- sideBarBuilder: (routes, onTap, current) => const KasySidebarPro(),
65
- ),
66
- ),
107
+ medium: connectedScaffold(),
108
+ large: connectedScaffold(),
67
109
  ),
68
110
  );
69
111
  }
@@ -109,3 +151,74 @@ class BottomMenu extends StatelessWidget {
109
151
  return segments.length < 2;
110
152
  }
111
153
  }
154
+
155
+ /// Owns the sidebar's keyboard [FocusScope] and keeps the initial Tab focus
156
+ /// anchored on the sidebar — on every screen, like Stripe/Linear.
157
+ ///
158
+ /// Why this exists: Bart renders each page inside a nested [Navigator]
159
+ /// (see bart's nested_navigator.dart), which has its OWN FocusScope and claims
160
+ /// the primary focus the moment a route mounts. A plain `autofocus` on a sidebar
161
+ /// item loses that race — the Navigator overwrites it in the same frame.
162
+ ///
163
+ /// The fix is a post-frame callback (runs AFTER the Navigator has claimed focus)
164
+ /// that hands focus back to this sidebar scope. We focus the SCOPE, not a
165
+ /// specific item, so the very first Tab lands on the first item (no item is
166
+ /// skipped). It re-runs whenever [currentItem] changes (a tab navigation) so a
167
+ /// fresh screen also starts at the sidebar. The ring still only paints during
168
+ /// keyboard navigation, so this is invisible to mouse/touch users.
169
+ class _FocusableSidebar extends StatefulWidget {
170
+ final Widget child;
171
+ final ValueNotifier<int> currentItem;
172
+
173
+ const _FocusableSidebar({required this.child, required this.currentItem});
174
+
175
+ @override
176
+ State<_FocusableSidebar> createState() => _FocusableSidebarState();
177
+ }
178
+
179
+ class _FocusableSidebarState extends State<_FocusableSidebar> {
180
+ final FocusScopeNode _scope = FocusScopeNode(debugLabel: 'sidebarScope');
181
+
182
+ @override
183
+ void initState() {
184
+ super.initState();
185
+ widget.currentItem.addListener(_anchorFocus);
186
+ _anchorFocus();
187
+ }
188
+
189
+ @override
190
+ void didUpdateWidget(_FocusableSidebar oldWidget) {
191
+ super.didUpdateWidget(oldWidget);
192
+ if (oldWidget.currentItem != widget.currentItem) {
193
+ oldWidget.currentItem.removeListener(_anchorFocus);
194
+ widget.currentItem.addListener(_anchorFocus);
195
+ }
196
+ }
197
+
198
+ // Defer to after the frame so we win the race against the nested Navigator,
199
+ // which claims focus for its own scope while the route is mounting.
200
+ void _anchorFocus() {
201
+ WidgetsBinding.instance.addPostFrameCallback((_) {
202
+ if (!mounted) return;
203
+ // Only seize focus when nothing inside the sidebar already holds it, so a
204
+ // keyboard user mid-navigation in the sidebar isn't yanked back to start.
205
+ if (_scope.hasFocus) return;
206
+ _scope.requestFocus();
207
+ });
208
+ }
209
+
210
+ @override
211
+ void dispose() {
212
+ widget.currentItem.removeListener(_anchorFocus);
213
+ _scope.dispose();
214
+ super.dispose();
215
+ }
216
+
217
+ @override
218
+ Widget build(BuildContext context) {
219
+ return FocusScope(
220
+ node: _scope,
221
+ child: FocusTraversalGroup(child: widget.child),
222
+ );
223
+ }
224
+ }