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
@@ -1,21 +1,32 @@
1
1
  import 'package:flutter/material.dart';
2
2
 
3
- /// Kasy Design System — Border Radius Tokens
3
+ /// Kasy Design System — Border Radius Tokens (HeroUI / Tailwind scale)
4
4
  ///
5
- /// Single source of truth for all border radius values in the app.
6
- /// Usage: KasyRadius.md 8.0
7
- /// BorderRadius.circular(KasyRadius.md)
5
+ /// Radius is a system-level variable based on Tailwind's base radius values.
6
+ /// The full HeroUI scale is exposed as `rounded*` tokens; the existing semantic
7
+ /// names (xs/sm/md/lg/xl/full) are kept for backward compatibility and already
8
+ /// land on points of this scale:
9
+ /// xs (4) = roundedSm · sm (8) = roundedLg · md (12) = roundedXl
10
+ /// lg (16) = rounded2xl · xl (24) = rounded3xl
8
11
  ///
9
- /// Scale reference:
10
- /// xs → 4
11
- /// sm → 8
12
- /// md → 12
13
- /// lg → 16
14
- /// xl → 24
15
- /// full → 999 (pill shape / fully rounded)
12
+ /// Usage: KasyRadius.md → 12.0 · BorderRadius.circular(KasyRadius.md)
16
13
  class KasyRadius {
17
14
  KasyRadius._();
18
15
 
16
+ // --- HeroUI / Tailwind scale (canonical) ---
17
+ static const double roundedNone = 0;
18
+ static const double roundedXs = 2;
19
+ static const double roundedSm = 4;
20
+ static const double roundedMd = 6;
21
+ static const double roundedLg = 8;
22
+ static const double roundedXl = 12;
23
+ static const double rounded2xl = 16;
24
+ static const double rounded2_5xl = 20;
25
+ static const double rounded3xl = 24;
26
+ static const double rounded4xl = 32;
27
+ static const double roundedFull = 9999;
28
+
29
+ // --- Semantic names (backward compatible — map onto the scale above) ---
19
30
  static const double xs = 4;
20
31
  static const double sm = 8;
21
32
  static const double md = 12;
@@ -61,4 +61,70 @@ class KasyShadows {
61
61
  blurRadius: base.blurRadius - 0.3,
62
62
  );
63
63
  }
64
+
65
+ // -------------------------------------------------------------------------
66
+ // HeroUI Effect Styles — exact Figma specs (drop-shadow layers).
67
+ //
68
+ // The Figma effects also include inner-shadow and background-blur layers.
69
+ // Flutter has no native inner shadow and blur needs a [BackdropFilter], so
70
+ // these lists model the drop-shadow stack — the part expressible as
71
+ // [BoxShadow]. Blur radii and the inner-shadow tint are exposed separately.
72
+ // -------------------------------------------------------------------------
73
+
74
+ /// Surface elevation (cards, panels, modals).
75
+ static const List<BoxShadow> surface = [
76
+ BoxShadow(color: Color(0x0F000000), blurRadius: 1),
77
+ BoxShadow(color: Color(0x0F000000), offset: Offset(0, 1), blurRadius: 2),
78
+ BoxShadow(color: Color(0x0A000000), offset: Offset(0, 2), blurRadius: 4),
79
+ ];
80
+
81
+ /// Form field resting elevation (same drop stack as [surface]).
82
+ static const List<BoxShadow> field = surface;
83
+
84
+ /// Switch thumb.
85
+ static const List<BoxShadow> switchControl = [
86
+ BoxShadow(color: Color(0x05000000), blurRadius: 5),
87
+ BoxShadow(color: Color(0x0F000000), offset: Offset(0, 2), blurRadius: 10),
88
+ BoxShadow(color: Color(0x4D000000), blurRadius: 1),
89
+ ];
90
+
91
+ /// Segmented control / tab selected thumb.
92
+ static const List<BoxShadow> tab = [
93
+ BoxShadow(color: Color(0x0F000000), offset: Offset(0, 2), blurRadius: 8),
94
+ ];
95
+
96
+ /// Floating overlays (menus, popovers, dropdowns).
97
+ static const List<BoxShadow> overlay = [
98
+ BoxShadow(color: Color(0x14000000), offset: Offset(0, 14), blurRadius: 28),
99
+ BoxShadow(color: Color(0x08000000), offset: Offset(0, -6), blurRadius: 12),
100
+ BoxShadow(color: Color(0x0F000000), offset: Offset(0, 2), blurRadius: 8),
101
+ ];
102
+
103
+ /// Inner-shadow tint used on insets (apply via custom painter / overlay).
104
+ static const Color inner = Color(0x4D000000);
105
+
106
+ /// Background-blur radius for translucent surfaces (Figma `blur`), logical px.
107
+ static const double blurRadius = 64;
108
+
109
+ /// Background-blur radius for backdrops (Figma `blur-backdrop`), logical px.
110
+ static const double backdropBlurRadius = 12;
111
+
112
+ /// Focus ring thickness (HeroUI `ring-focus-width`).
113
+ static const double ringFocusWidth = 4;
114
+
115
+ /// Gap between the element and its focus ring (HeroUI `ring-offset-width`).
116
+ static const double ringOffsetWidth = 2;
117
+
118
+ /// HeroUI focus ring as box-shadows: an accent ring with a background-colored
119
+ /// offset gap. Pass `ring` = accent and `gap` = the surrounding background.
120
+ static List<BoxShadow> focusRing({required Color ring, required Color gap}) =>
121
+ [
122
+ BoxShadow(color: ring, spreadRadius: ringFocusWidth),
123
+ BoxShadow(color: gap, spreadRadius: ringOffsetWidth),
124
+ ];
125
+
126
+ /// Focus ring for form fields: accent ring with no offset gap.
127
+ static List<BoxShadow> focusRingField({required Color ring}) => [
128
+ BoxShadow(color: ring, spreadRadius: ringOffsetWidth),
129
+ ];
64
130
  }
@@ -1,32 +1,68 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:google_fonts/google_fonts.dart';
3
3
 
4
- /// Kasy Design System — Typography Tokens
4
+ /// Kasy Design System — Typography Tokens (HeroUI type scale)
5
5
  ///
6
- /// Single source of truth for all text styles in the app.
7
- /// To change the font family or any style globally, change it here.
6
+ /// Single source of truth for all text styles in the app. Mirrors the HeroUI
7
+ /// typography, which is based on Tailwind's default scale: Inter, neutral
8
+ /// tracking (letter-spacing 0), predictable sizes and line-heights.
8
9
  ///
9
- /// Usage via context extension:
10
- /// context.textTheme.bodyMedium
11
- /// context.textTheme.titleLarge
10
+ /// HeroUI roles (heading1…buttonSm) are exposed as static getters and are the
11
+ /// canonical styles. The Material slots (displayLarge…labelSmall) map onto
12
+ /// those roles so existing `context.textTheme.*` call sites keep working.
12
13
  ///
13
- /// Font family is defined once in [KasyTextTheme._fontFamily].
14
- /// Change it here all text in the app updates automatically.
14
+ /// Usage:
15
+ /// context.textTheme.bodyMedium Body sm (Material slot)
16
+ /// KasyTextTheme.heading1 → HeroUI Heading 1
15
17
  class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
16
18
  // -----------------------------------------------------------------------
17
- // Font family — change here to update the entire app
19
+ // Font family — Inter. Change here to update the entire app.
18
20
  // -----------------------------------------------------------------------
19
- static TextStyle get _base => GoogleFonts.plusJakartaSans();
21
+ static TextStyle _inter(
22
+ FontWeight weight,
23
+ double size,
24
+ double lineHeight, {
25
+ TextDecoration? decoration,
26
+ }) =>
27
+ GoogleFonts.inter(
28
+ fontSize: size,
29
+ fontWeight: weight,
30
+ height: lineHeight / size,
31
+ letterSpacing: 0,
32
+ decoration: decoration,
33
+ );
20
34
 
21
35
  // -----------------------------------------------------------------------
22
- // Displayfor large, prominent text (hero sections, splash screens)
36
+ // HeroUI roles canonical styles (color applied per-theme downstream)
37
+ // -----------------------------------------------------------------------
38
+ static TextStyle get heading1 => _inter(FontWeight.w800, 36, 40);
39
+ static TextStyle get heading2 => _inter(FontWeight.w700, 24, 32);
40
+ static TextStyle get heading3 => _inter(FontWeight.w600, 20, 28);
41
+ static TextStyle get heading4 => _inter(FontWeight.w600, 16, 24);
42
+ static TextStyle get bodyBase => _inter(FontWeight.w400, 16, 24);
43
+ static TextStyle get bodyBaseMedium => _inter(FontWeight.w500, 16, 24);
44
+ static TextStyle get bodySm => _inter(FontWeight.w400, 14, 20);
45
+ static TextStyle get bodySmMedium => _inter(FontWeight.w500, 14, 20);
46
+ static TextStyle get bodyXs => _inter(FontWeight.w400, 12, 16);
47
+ static TextStyle get bodyXsMedium => _inter(FontWeight.w500, 12, 16);
48
+ static TextStyle get linkBase =>
49
+ _inter(FontWeight.w500, 16, 24, decoration: TextDecoration.underline);
50
+ static TextStyle get linkSm =>
51
+ _inter(FontWeight.w500, 14, 20, decoration: TextDecoration.underline);
52
+ static TextStyle get textFieldBase => _inter(FontWeight.w400, 16, 24);
53
+ static TextStyle get textFieldSm => _inter(FontWeight.w400, 14, 20);
54
+ static TextStyle get buttonBase => _inter(FontWeight.w500, 16, 24);
55
+ static TextStyle get buttonSm => _inter(FontWeight.w500, 14, 20);
56
+
57
+ // -----------------------------------------------------------------------
58
+ // Display — large hero text (extends the HeroUI scale upward in Inter)
23
59
  // -----------------------------------------------------------------------
24
60
  final TextStyle displayLarge;
25
61
  final TextStyle displayMedium;
26
62
  final TextStyle displaySmall;
27
63
 
28
64
  // -----------------------------------------------------------------------
29
- // Headline — section headings
65
+ // Headline — section headings (Heading 1–3)
30
66
  // -----------------------------------------------------------------------
31
67
  final TextStyle headlineLarge;
32
68
  final TextStyle headlineMedium;
@@ -76,36 +112,34 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
76
112
  });
77
113
 
78
114
  factory KasyTextTheme.build() {
79
- final base = _base;
80
-
81
115
  return KasyTextTheme(
82
- // Display
83
- displayLarge: base.copyWith(fontSize: 57, fontWeight: FontWeight.w300, letterSpacing: -0.25),
84
- displayMedium: base.copyWith(fontSize: 45, fontWeight: FontWeight.w300),
85
- displaySmall: base.copyWith(fontSize: 36, fontWeight: FontWeight.w400),
86
-
87
- // Headline
88
- headlineLarge: base.copyWith(fontSize: 32, fontWeight: FontWeight.w600),
89
- headlineMedium: base.copyWith(fontSize: 26, fontWeight: FontWeight.w600),
90
- headlineSmall: base.copyWith(fontSize: 22, fontWeight: FontWeight.w500),
91
-
92
- // Title
93
- titleLarge: base.copyWith(fontSize: 20, fontWeight: FontWeight.w500),
94
- titleMedium: base.copyWith(fontSize: 15, fontWeight: FontWeight.w600, letterSpacing: 0.1),
95
- titleSmall: base.copyWith(fontSize: 14, fontWeight: FontWeight.w500, letterSpacing: 0.1),
96
-
97
- // Body
98
- bodyLarge: base.copyWith(fontSize: 16, fontWeight: FontWeight.w400, letterSpacing: 0.15),
99
- bodyMedium: base.copyWith(fontSize: 14, fontWeight: FontWeight.w400, letterSpacing: 0.1),
100
- bodySmall: base.copyWith(fontSize: 12, fontWeight: FontWeight.w400, letterSpacing: 0.2),
101
-
102
- // Label
103
- labelLarge: base.copyWith(fontSize: 14, fontWeight: FontWeight.w600, letterSpacing: 0.05),
104
- labelMedium: base.copyWith(fontSize: 12, fontWeight: FontWeight.w500, letterSpacing: 0.4),
105
- labelSmall: base.copyWith(fontSize: 11, fontWeight: FontWeight.w500, letterSpacing: 0.4),
106
-
107
- // Legacy primary
108
- primary: base.copyWith(fontSize: 16, fontWeight: FontWeight.w400),
116
+ // Display — hero text in Inter, neutral tracking.
117
+ displayLarge: _inter(FontWeight.w800, 57, 64),
118
+ displayMedium: _inter(FontWeight.w800, 45, 52),
119
+ displaySmall: heading1, // 36/40
120
+
121
+ // Headline → HeroUI Heading 1–3.
122
+ headlineLarge: heading1,
123
+ headlineMedium: heading2,
124
+ headlineSmall: heading3,
125
+
126
+ // Title → HeroUI Heading 3 / Heading 4 / Body sm medium.
127
+ titleLarge: heading3,
128
+ titleMedium: heading4,
129
+ titleSmall: bodySmMedium,
130
+
131
+ // Body → HeroUI Body base / Body sm / Body xs.
132
+ bodyLarge: bodyBase,
133
+ bodyMedium: bodySm,
134
+ bodySmall: bodyXs,
135
+
136
+ // Label → HeroUI Button sm / Body xs medium.
137
+ labelLarge: buttonSm,
138
+ labelMedium: bodyXsMedium,
139
+ labelSmall: bodyXsMedium,
140
+
141
+ // Legacy primary.
142
+ primary: bodyBase,
109
143
  );
110
144
  }
111
145
 
@@ -64,15 +64,20 @@ class UniversalThemeFactory extends KasyThemeDataFactory {
64
64
  textTheme: defaultTextStyle,
65
65
  ),
66
66
  appBarTheme: AppBarTheme(
67
- backgroundColor: colors.background.computeLuminance() < 0.5
68
- ? const Color(0xFF161618)
69
- : const Color(0xFFF7F7F7),
70
- foregroundColor: colors.onSurface,
67
+ backgroundColor: colors.surface,
68
+ foregroundColor: colors.foreground,
71
69
  elevation: colors.background.computeLuminance() < 0.5 ? 0 : 1.4,
72
70
  shadowColor: colors.background.computeLuminance() < 0.5
73
71
  ? Colors.transparent
74
72
  : Colors.black.withValues(alpha: 0.05),
75
73
  ),
74
+ // Modal bottom sheets span the full available width on every breakpoint.
75
+ // Material 3 otherwise caps them at 640 logical px, so on web/desktop the
76
+ // sheet floats at tablet width with empty gutters on each side. An empty
77
+ // BoxConstraints() removes that cap; phones are unaffected (already < 640).
78
+ bottomSheetTheme: const BottomSheetThemeData(
79
+ constraints: BoxConstraints(),
80
+ ),
76
81
  ),
77
82
  );
78
83
  }
@@ -203,10 +203,11 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
203
203
  Future<void> _bootstrap() async {
204
204
  final prefs = await SharedPreferences.getInstance();
205
205
 
206
- // Default ON for the first run: most devs hitting `kasy run --web` want to
207
- // preview their mobile app inside a device frame. Closing the chrome
208
- // persists the choice, so subsequent launches respect what the user left.
209
- final savedEnabled = prefs.getBool(webDevicePreviewEnabledPrefKey) ?? true;
206
+ // Default OFF: the web app renders at its real desktop proportions instead
207
+ // of inside a simulated device frame (which distorts scale/proportion).
208
+ // Devs who want to preview the mobile app in a device frame toggle it on
209
+ // (shortcut), and that choice persists for subsequent launches.
210
+ final savedEnabled = prefs.getBool(webDevicePreviewEnabledPrefKey) ?? false;
210
211
  final savedPlatform = prefs.getInt(_platformPrefKey);
211
212
  final savedIosIndex = prefs.getInt(_iosIndexPrefKey);
212
213
  final savedAndroidIndex = prefs.getInt(_androidIndexPrefKey);
@@ -0,0 +1,52 @@
1
+ import 'package:flutter/foundation.dart' show kIsWeb;
2
+ import 'package:flutter/widgets.dart';
3
+
4
+ /// Global render scale applied to the app on web.
5
+ ///
6
+ /// Flutter web tends to render ~10% larger than equivalent HTML apps at the
7
+ /// browser's 100% zoom, so the whole UI feels oversized on desktop. `0.9` brings
8
+ /// it to the proportion the design targets (i.e. what 90% zoom looked like)
9
+ /// without the user having to touch the browser zoom.
10
+ const double kWebViewportScale = 0.9;
11
+
12
+ /// Renders [child] uniformly scaled by [scale] on web (no-op elsewhere).
13
+ ///
14
+ /// This is a *true* viewport scale, not a visual-only [Transform]: it enlarges
15
+ /// the logical [MediaQuery] size by `1/scale` and fits it back into the real
16
+ /// viewport with a [FittedBox], so layout, scrolling and hit-testing all stay
17
+ /// correct — exactly as if the browser were zoomed out, but baked into the app.
18
+ class WebViewportScale extends StatelessWidget {
19
+ final Widget child;
20
+ final double scale;
21
+
22
+ const WebViewportScale({
23
+ super.key,
24
+ required this.child,
25
+ this.scale = kWebViewportScale,
26
+ });
27
+
28
+ /// Wraps [child] on web; returns it untouched on every other platform.
29
+ static Widget wrap(Widget child) =>
30
+ kIsWeb ? WebViewportScale(child: child) : child;
31
+
32
+ @override
33
+ Widget build(BuildContext context) {
34
+ if (!kIsWeb || scale == 1.0) return child;
35
+ final MediaQueryData mq = MediaQuery.of(context);
36
+ final Size logicalSize = Size(
37
+ mq.size.width / scale,
38
+ mq.size.height / scale,
39
+ );
40
+ return MediaQuery(
41
+ data: mq.copyWith(size: logicalSize),
42
+ child: FittedBox(
43
+ fit: BoxFit.fill,
44
+ child: SizedBox(
45
+ width: logicalSize.width,
46
+ height: logicalSize.height,
47
+ child: child,
48
+ ),
49
+ ),
50
+ );
51
+ }
52
+ }
@@ -0,0 +1,118 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:kasy_kit/core/theme/theme.dart';
3
+
4
+ /// Premium brand mark: a brand‑gradient rounded squircle holding an icon (or any
5
+ /// child), optionally floating on a soft radial brand glow.
6
+ ///
7
+ /// This is the kit's single "hero badge" primitive. Auth screens and permission
8
+ /// prompts all build their top mark from it, so rebranding is a one‑file change:
9
+ /// swap the [icon] (or pass a logo as [child]), or retint via [gradient] /
10
+ /// [foregroundColor]. Colors default to the active theme's brand color, so it
11
+ /// adapts to light/dark automatically.
12
+ class KasyBrandBadge extends StatelessWidget {
13
+ const KasyBrandBadge({
14
+ super.key,
15
+ this.icon,
16
+ this.child,
17
+ this.size = 64,
18
+ this.glyphSize = 30,
19
+ this.glow = true,
20
+ this.gradient,
21
+ this.foregroundColor,
22
+ }) : assert(
23
+ icon != null || child != null,
24
+ 'KasyBrandBadge needs an icon or a child',
25
+ );
26
+
27
+ /// Icon drawn inside the badge. Ignored when [child] is provided.
28
+ final IconData? icon;
29
+
30
+ /// Custom content (e.g. an `Image` logo). Overrides [icon] when set.
31
+ final Widget? child;
32
+
33
+ /// Badge edge length.
34
+ final double size;
35
+
36
+ /// Icon size inside the badge.
37
+ final double glyphSize;
38
+
39
+ /// Soft radial brand glow behind the badge.
40
+ final bool glow;
41
+
42
+ /// Overrides the default subtle brand gradient fill.
43
+ final Gradient? gradient;
44
+
45
+ /// Icon color. Defaults to [KasyColors.onPrimary].
46
+ final Color? foregroundColor;
47
+
48
+ @override
49
+ Widget build(BuildContext context) {
50
+ final KasyColors colors = context.colors;
51
+
52
+ final Widget badge = Container(
53
+ width: size,
54
+ height: size,
55
+ alignment: Alignment.center,
56
+ decoration: BoxDecoration(
57
+ gradient:
58
+ gradient ??
59
+ LinearGradient(
60
+ begin: Alignment.topLeft,
61
+ end: Alignment.bottomRight,
62
+ colors: [
63
+ colors.primary,
64
+ Color.lerp(colors.primary, Colors.black, 0.18)!,
65
+ ],
66
+ ),
67
+ borderRadius: BorderRadius.circular(size * 0.3),
68
+ boxShadow: [
69
+ BoxShadow(
70
+ color: colors.primary.withValues(
71
+ alpha: context.isDark ? 0.34 : 0.26,
72
+ ),
73
+ blurRadius: 22,
74
+ spreadRadius: -6,
75
+ offset: const Offset(0, 10),
76
+ ),
77
+ ],
78
+ ),
79
+ child:
80
+ child ??
81
+ Icon(
82
+ icon,
83
+ color: foregroundColor ?? colors.onPrimary,
84
+ size: glyphSize,
85
+ ),
86
+ );
87
+
88
+ if (!glow) return badge;
89
+
90
+ final double glowSize = size * 2.2;
91
+ return SizedBox(
92
+ width: glowSize,
93
+ height: glowSize,
94
+ child: Stack(
95
+ alignment: Alignment.center,
96
+ clipBehavior: Clip.none,
97
+ children: [
98
+ IgnorePointer(
99
+ child: Container(
100
+ decoration: BoxDecoration(
101
+ shape: BoxShape.circle,
102
+ gradient: RadialGradient(
103
+ colors: [
104
+ colors.primary.withValues(
105
+ alpha: context.isDark ? 0.20 : 0.14,
106
+ ),
107
+ colors.primary.withValues(alpha: 0),
108
+ ],
109
+ ),
110
+ ),
111
+ ),
112
+ ),
113
+ badge,
114
+ ],
115
+ ),
116
+ );
117
+ }
118
+ }
@@ -0,0 +1,40 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:kasy_kit/core/theme/theme.dart';
3
+
4
+ /// The app's brand logo — the SAME artwork shown on the native splash screen,
5
+ /// so the launch → app handoff feels seamless.
6
+ ///
7
+ /// It auto‑swaps between the light and dark splash logos based on the active
8
+ /// theme. To rebrand, replace the two PNGs (or point [lightAsset] / [darkAsset]
9
+ /// at your own) — keep them in sync with `flutter_native_splash` in
10
+ /// `pubspec.yaml` so the splash and the in‑app logo always match.
11
+ class KasyBrandLogo extends StatelessWidget {
12
+ const KasyBrandLogo({
13
+ super.key,
14
+ this.height = 48,
15
+ this.lightAsset = 'assets/images/splash_logo_light.png',
16
+ this.darkAsset = 'assets/images/splash_logo_dark.png',
17
+ this.semanticLabel,
18
+ });
19
+
20
+ /// Rendered logo height. Width follows the artwork's aspect ratio.
21
+ final double height;
22
+
23
+ /// Logo used in light mode (matches the splash `image`).
24
+ final String lightAsset;
25
+
26
+ /// Logo used in dark mode (matches the splash `image_dark`).
27
+ final String darkAsset;
28
+
29
+ final String? semanticLabel;
30
+
31
+ @override
32
+ Widget build(BuildContext context) {
33
+ return Image.asset(
34
+ context.isDark ? darkAsset : lightAsset,
35
+ height: height,
36
+ fit: BoxFit.contain,
37
+ semanticLabel: semanticLabel,
38
+ );
39
+ }
40
+ }
@@ -0,0 +1,125 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter/services.dart';
3
+ import 'package:kasy_kit/core/theme/theme.dart';
4
+
5
+ /// The kit's single keyboard focus indicator: a thin primary-coloured ring shown
6
+ /// ONLY during keyboard navigation (never on pointer/touch focus), lifted off the
7
+ /// control by a hair-line gap so it reads as a crisp outline, not a glow.
8
+ ///
9
+ /// Visibility follows [FocusManager]'s highlight mode via
10
+ /// [FocusableActionDetector.onShowFocusHighlight], so clicking/tapping a control
11
+ /// (or it merely receiving pointer focus) never paints the ring.
12
+ ///
13
+ /// Two ways to use it:
14
+ /// - Controls without their own keyboard activation: pass [onActivate]
15
+ /// (Enter/Space) and let this widget own focus.
16
+ /// - Controls that already activate from their own focusable (e.g. an
17
+ /// [InkWell]): set that focusable's `canRequestFocus: false` and wrap it here
18
+ /// with [onActivate] so focus + ring live in one place.
19
+ class KasyFocusRing extends StatefulWidget {
20
+ final Widget child;
21
+ final BorderRadius borderRadius;
22
+
23
+ /// Invoked on Enter/Space while focused. When null the ring still appears but
24
+ /// no keyboard activation is wired (use when a child handles its own keys).
25
+ final VoidCallback? onActivate;
26
+
27
+ /// When false the ring never takes focus or paints (disabled controls).
28
+ final bool enabled;
29
+
30
+ const KasyFocusRing({
31
+ super.key,
32
+ required this.child,
33
+ required this.borderRadius,
34
+ this.onActivate,
35
+ this.enabled = true,
36
+ });
37
+
38
+ @override
39
+ State<KasyFocusRing> createState() => _KasyFocusRingState();
40
+ }
41
+
42
+ class _KasyFocusRingState extends State<KasyFocusRing> {
43
+ bool _showFocusRing = false;
44
+
45
+ static const Map<ShortcutActivator, Intent> _activateShortcuts =
46
+ <ShortcutActivator, Intent>{
47
+ SingleActivator(LogicalKeyboardKey.enter): ActivateIntent(),
48
+ SingleActivator(LogicalKeyboardKey.numpadEnter): ActivateIntent(),
49
+ SingleActivator(LogicalKeyboardKey.space): ActivateIntent(),
50
+ };
51
+
52
+ @override
53
+ Widget build(BuildContext context) {
54
+ if (!widget.enabled) return widget.child;
55
+
56
+ final Color ringColor = context.colors.primary;
57
+ // A hair-line gap in the canvas colour lifts the ring just off the control
58
+ // so it reads as a crisp outline (same focus language as KasyTextField) and
59
+ // stays visible even on primary-coloured buttons (e.g. the chat send orb).
60
+ final Color gapColor = context.colors.background;
61
+ final VoidCallback? onActivate = widget.onActivate;
62
+
63
+ return FocusableActionDetector(
64
+ shortcuts: onActivate != null
65
+ ? _activateShortcuts
66
+ : const <ShortcutActivator, Intent>{},
67
+ actions: <Type, Action<Intent>>{
68
+ if (onActivate != null)
69
+ ActivateIntent: CallbackAction<ActivateIntent>(
70
+ onInvoke: (_) {
71
+ onActivate();
72
+ return null;
73
+ },
74
+ ),
75
+ },
76
+ onShowFocusHighlight: (show) {
77
+ if (mounted && show != _showFocusRing) {
78
+ setState(() => _showFocusRing = show);
79
+ }
80
+ },
81
+ // Two concentric layers instead of one box with two shadows: a [BoxShadow]
82
+ // inflates its box by [spreadRadius] but keeps the box's corner radius, so
83
+ // on a circular control the inflated ring squares off at the corners and
84
+ // reads as a rounded rectangle. Giving each layer its own radius — matched
85
+ // to its inflated edge — keeps the ring a true contour of any shape.
86
+ // Outer: thin solid primary ring (~1.5px). Inner: a 1.5px gap so the ring
87
+ // floats just off the control instead of hugging it.
88
+ child: AnimatedContainer(
89
+ duration: const Duration(milliseconds: 120),
90
+ decoration: _showFocusRing
91
+ ? BoxDecoration(
92
+ borderRadius: _inflate(widget.borderRadius, 3),
93
+ boxShadow: <BoxShadow>[
94
+ BoxShadow(color: ringColor, spreadRadius: 3),
95
+ ],
96
+ )
97
+ : const BoxDecoration(),
98
+ child: AnimatedContainer(
99
+ duration: const Duration(milliseconds: 120),
100
+ decoration: _showFocusRing
101
+ ? BoxDecoration(
102
+ borderRadius: _inflate(widget.borderRadius, 1.5),
103
+ boxShadow: <BoxShadow>[
104
+ BoxShadow(color: gapColor, spreadRadius: 1.5),
105
+ ],
106
+ )
107
+ : const BoxDecoration(),
108
+ child: widget.child,
109
+ ),
110
+ ),
111
+ );
112
+ }
113
+
114
+ /// Grows every corner of [radius] by [delta] so a layer inflated by the same
115
+ /// amount stays a perfect contour (a circle stays a circle, not a squircle).
116
+ BorderRadius _inflate(BorderRadius radius, double delta) {
117
+ final Radius bump = Radius.circular(delta);
118
+ return BorderRadius.only(
119
+ topLeft: radius.topLeft + bump,
120
+ topRight: radius.topRight + bump,
121
+ bottomLeft: radius.bottomLeft + bump,
122
+ bottomRight: radius.bottomRight + bump,
123
+ );
124
+ }
125
+ }