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,77 +1,106 @@
1
1
  import 'dart:async';
2
2
 
3
+ import 'package:bart/bart/bart_model.dart';
4
+ import 'package:bart/bart/widgets/side_bar/custom_sidebar.dart';
3
5
  import 'package:flutter/material.dart';
4
- import 'package:kasy_kit/core/icons/kasy_icons.dart';
6
+ import 'package:kasy_kit/components/kasy_avatar.dart';
7
+ import 'package:kasy_kit/components/kasy_avatar_presets.dart';
8
+ import 'package:kasy_kit/core/theme/theme.dart';
5
9
  import 'package:kasy_kit/core/widgets/kasy_hover.dart';
10
+ import 'package:kasy_kit/i18n/translations.g.dart';
6
11
 
7
12
  // ─────────────────────────────────────────────────────────────────────────────
8
- // Design tokens — from Figma (node 2:1331 / 6:114)
13
+ // Design tokens — HeroUI Figma Kit (node 4678:41088 light / 21964:53880 dark)
9
14
  // ─────────────────────────────────────────────────────────────────────────────
10
15
 
11
- const double _kWidthOpen = 256.0;
12
- const double _kWidthCollapsed = 92.0;
13
- const double _kPad = 24.0;
14
- const double _kItemRadius = 8.0;
15
- const double _kItemVPad = 10.0;
16
- const double _kItemHPad = 12.0;
17
- const double _kItemGap = 8.0;
16
+ // Figma sidebar is 223 wide; we run a touch wider for breathing room.
17
+ const double _kWidthOpen = 248.0;
18
+ const double _kWidthCollapsed = 78.0;
19
+ const double _kPadH = 16.0; // px-4
20
+ const double _kPadBottom = 16.0; // pb-4
21
+ // Top band that holds the logo. Equals the web header height (kasyWebHeaderHeight
22
+ // = 68) so the sidebar's first divider lines up with the header's bottom border.
23
+ const double _kTopBandHeight = 68.0;
24
+ const double _kItemRadius = 14.0; // rounded item / active pill
25
+ const double _kItemMinH = 36.0;
26
+ const double _kItemHPad = 12.0; // px-3
27
+ const double _kItemVPad = 6.0; // py-1.5
28
+ const double _kIconSize = 16.0;
29
+ const double _kIconGap = 12.0; // gap-3 (icon → label)
30
+ const double _kItemGap = 8.0; // gap between items (spacing/2)
31
+ const double _kHeaderGap = 24.0; // logo row → workspace selector (spacing/6)
32
+ const double _kDividerGap = 20.0; // gap around the section dividers (spacing/5)
33
+ const double _kNavGap = 20.0; // tabs → list (spacing/5)
34
+ const double _kFooterGap = 16.0; // bottom divider → search (spacing/4)
35
+ const double _kToggleSize = 36.0; // header panel-toggle button
36
+
37
+ // Submenu tree tokens (kept from the previous sidebar — still used by the
38
+ // connected/Income dropdown and its collapsed hover popup).
18
39
  const double _kSubItemH = 32.0;
19
40
  const double _kSubItemGap = 4.0;
20
41
  const double _kSubIndent = 36.0;
21
42
  const double _kTreeConnectorW = 13.0;
22
- const double _kCollapseButtonSize = 28.0;
23
43
 
24
44
  /// Returns the height of the vertical tree line for [n] sub-items.
25
45
  double _treeLineHeight(int n) =>
26
46
  (n * (_kSubItemH + _kSubItemGap) - _kSubItemGap) * 0.8627;
27
47
 
28
48
  // ─────────────────────────────────────────────────────────────────────────────
29
- // Color palette
49
+ // Color palette — every value derives from the global Kasy theme so the sidebar
50
+ // follows light/dark automatically (nothing hardcoded).
30
51
  // ─────────────────────────────────────────────────────────────────────────────
31
52
 
32
- enum _SidebarColors {
33
- light(
34
- bg: Color(0xFFFFFFFF),
35
- border: Color(0x1A000000),
36
- divider: Color(0xFFF6F6F6),
37
- activeBg: Color(0xFFF6F6F6),
38
- textMuted: Color(0xFF757575),
39
- textActive: Color(0xFF000000),
40
- logout: Color(0xFFD55F5A),
41
- ),
42
- dark(
43
- bg: Color(0xFF161A23),
44
- border: Color(0x1AFFFFFF),
45
- divider: Color(0xFF2D2F39),
46
- activeBg: Color(0xFF2D2F39),
47
- textMuted: Color(0x80FFFFFF),
48
- textActive: Color(0xCCFFFFFF),
49
- logout: Color(0xFFCC8889),
50
- );
51
-
53
+ class _SidebarColors {
52
54
  const _SidebarColors({
53
55
  required this.bg,
54
56
  required this.border,
55
57
  required this.divider,
56
58
  required this.activeBg,
59
+ required this.segmentThumb,
57
60
  required this.textMuted,
58
61
  required this.textActive,
59
62
  required this.logout,
63
+ required this.isDark,
60
64
  });
61
65
 
66
+ /// Maps the HeroUI Figma tokens onto the global [KasyColors]:
67
+ /// `surface → surface`, `foreground → onSurface`, `foreground/muted → muted`,
68
+ /// `border → border`, `separator → separator`, `default → surfaceNeutralSoft`
69
+ /// (hover/active fill + tabs track), and the selected-tab thumb to the
70
+ /// dedicated `segment` token.
71
+ factory _SidebarColors.fromContext(BuildContext context) {
72
+ final c = context.colors;
73
+ final bool dark = context.isDark;
74
+ return _SidebarColors(
75
+ bg: c.surface,
76
+ border: c.border,
77
+ // Same token as the vertical edge line + the web header's bottom border,
78
+ // so the top divider continues that line seamlessly across the chrome.
79
+ divider: c.border,
80
+ // Hover / active item fill + tabs track + kbd chip (default/default).
81
+ activeBg: c.surfaceNeutralSoft,
82
+ // Selected segment thumb (HeroUI `segment`): lifts off the track.
83
+ segmentThumb: c.segment,
84
+ textMuted: c.muted,
85
+ textActive: c.onSurface,
86
+ logout: c.error,
87
+ isDark: dark,
88
+ );
89
+ }
90
+
62
91
  final Color bg;
63
92
  final Color border;
64
93
  final Color divider;
65
94
  final Color activeBg;
95
+ final Color segmentThumb;
66
96
  final Color textMuted;
67
97
  final Color textActive;
68
98
  final Color logout;
69
99
 
70
- /// True when this palette is the dark-mode variant.
71
- /// Used by overlay widgets (tooltips, popups) that cannot rely on
72
- /// Theme.of(overlayContext) since overlay contexts may not inherit the
73
- /// app theme correctly.
74
- bool get isDark => identical(this, dark) || bg == dark.bg;
100
+ /// True when the current theme is dark. Used by overlay widgets (tooltips,
101
+ /// popups) that cannot rely on Theme.of(overlayContext) since overlay
102
+ /// contexts may not inherit the app theme correctly.
103
+ final bool isDark;
75
104
  }
76
105
 
77
106
  // ─────────────────────────────────────────────────────────────────────────────
@@ -94,20 +123,44 @@ class _NavItem {
94
123
  required this.icon,
95
124
  required this.label,
96
125
  this.subItems = const [],
97
- this.isLogout = false,
126
+ this.trailingControls = false,
98
127
  });
99
128
 
100
129
  final String id;
101
130
  final IconData icon;
102
131
  final String label;
103
132
  final List<String> subItems;
104
- final bool isLogout;
133
+
134
+ /// When true, the expanded row shows the lock + eye trailing controls
135
+ /// (matches the active "Object 2" layer item in the Figma reference).
136
+ final bool trailingControls;
105
137
 
106
138
  bool get hasSubmenu => subItems.isNotEmpty;
107
139
  }
108
140
 
109
141
  // ─────────────────────────────────────────────────────────────────────────────
110
- // Static nav data (mirrors the Figma)
142
+ // Showcase nav data (mirrors the HeroUI Figma layers list 1:1)
143
+ // ─────────────────────────────────────────────────────────────────────────────
144
+
145
+ const List<_NavItem> _kShowcaseItems = [
146
+ _NavItem(id: 'camera1', icon: KasyIcons.settings, label: 'Camera 1'),
147
+ _NavItem(id: 'domelight', icon: KasyIcons.lightMode, label: 'Dome Light'),
148
+ _NavItem(id: 'keylight', icon: KasyIcons.idea, label: 'Key Light'),
149
+ _NavItem(id: 'arealight', icon: KasyIcons.widgets, label: 'Area Light'),
150
+ _NavItem(
151
+ id: 'object2',
152
+ icon: KasyIcons.packageOutline,
153
+ label: 'Object 2',
154
+ trailingControls: true,
155
+ ),
156
+ _NavItem(id: 'bg2', icon: KasyIcons.packageOutline, label: 'Background 2'),
157
+ _NavItem(id: 'character', icon: KasyIcons.packageOutline, label: 'Character'),
158
+ _NavItem(id: 'bg1', icon: KasyIcons.packageOutline, label: 'Background 1'),
159
+ ];
160
+
161
+ // ─────────────────────────────────────────────────────────────────────────────
162
+ // Connected (real navigation) nav data — kept so the live app keeps working,
163
+ // including the Income submenu the host still relies on.
111
164
  // ─────────────────────────────────────────────────────────────────────────────
112
165
 
113
166
  const List<_NavItem> _kMainItems = [
@@ -123,61 +176,93 @@ const List<_NavItem> _kMainItems = [
123
176
  ),
124
177
  ];
125
178
 
126
- const _NavItem _kSettingsItem = _NavItem(
127
- id: 'settings',
128
- icon: KasyIcons.settings,
129
- label: 'Settings',
179
+ const _NavItem _kHelpItem = _NavItem(
180
+ id: 'help',
181
+ icon: KasyIcons.help,
182
+ label: 'Help',
130
183
  );
131
184
 
132
- const List<_NavItem> _kBottomItems = [
133
- _NavItem(id: 'help', icon: KasyIcons.help, label: 'Help'),
134
- _NavItem(
135
- id: 'logout',
136
- icon: KasyIcons.logout,
137
- label: 'Logout Account',
138
- isLogout: true,
139
- ),
140
- ];
141
-
142
185
  // ─────────────────────────────────────────────────────────────────────────────
143
- // KasySidebarPro — public widget
186
+ // KasySidebar — public widget
144
187
  // ─────────────────────────────────────────────────────────────────────────────
145
188
 
146
189
  /// Which screen edge the sidebar is anchored to.
147
190
  ///
148
- /// Controls which corners receive [BorderRadius]: the edge touching the screen
149
- /// has flat (zero) corners; the opposite edge gets the rounded corners.
150
- enum KasySidebarProSide { left, right }
191
+ /// Controls which edge receives the content-facing hairline + shadow nudge.
192
+ enum KasySidebarSide { left, right }
151
193
 
152
- /// A pro-style collapsible sidebar with expandable submenu tree, hover popups
153
- /// in collapsed mode, and full light / dark mode support.
194
+ /// A SaaS-style sidebar modelled on the HeroUI Figma kit: brand logo + panel
195
+ /// toggle, a workspace selector, a segmented control, a navigable list with an
196
+ /// active pill, and a pinned ⌘K search row. Collapses to an icon rail (with
197
+ /// tooltips and a hover submenu popup) on narrow viewports or via the toggle.
154
198
  ///
155
199
  /// Pass [onSettingsTap] to respond when the user taps Settings.
156
- /// The sidebar manages its own collapsed state and sub-menu expansion.
157
- class KasySidebarPro extends StatefulWidget {
158
- const KasySidebarPro({
200
+ class KasySidebar extends StatefulWidget {
201
+ const KasySidebar({
159
202
  super.key,
160
203
  this.onSettingsTap,
204
+ this.onLogout,
161
205
  this.initiallyCollapsed = false,
162
- this.side = KasySidebarProSide.left,
206
+ this.side = KasySidebarSide.left,
207
+ this.routes,
208
+ this.onTapItem,
209
+ this.currentItem,
210
+ this.showProfile = true,
211
+ this.profileName = 'Calvin Rice',
212
+ this.profileEmail = 'calvin@email.com',
213
+ this.profileAvatar,
214
+ this.profileGradient = KasyAvatarGradients.indigo,
215
+ this.onProfileTap,
163
216
  });
164
217
 
165
218
  final VoidCallback? onSettingsTap;
166
219
 
220
+ /// Whether the profile block is shown at the bottom of the rail. Set false to
221
+ /// drop it entirely (e.g. when the web header already carries the avatar).
222
+ final bool showProfile;
223
+
224
+ /// Profile block (bottom of the rail) — display name + email.
225
+ final String profileName;
226
+ final String profileEmail;
227
+
228
+ /// Custom avatar widget for the profile block — pass the signed-in user's
229
+ /// avatar (e.g. `KasyUserAvatar`) to show their real photo. When null, a
230
+ /// gradient-fill avatar ([profileGradient]) is shown instead.
231
+ final Widget? profileAvatar;
232
+
233
+ /// Gradient used for the profile avatar when no [profileImage] is given.
234
+ final KasyAvatarGradientData profileGradient;
235
+
236
+ /// Tap on the profile block (open account menu / profile).
237
+ final VoidCallback? onProfileTap;
238
+
239
+ /// Called when the user taps the Logout row in connected mode. The component
240
+ /// is purely presentational — the host (feature) owns the actual logout flow
241
+ /// (confirm dialog + sign-out). When null, the Logout row does nothing real.
242
+ final VoidCallback? onLogout;
243
+
167
244
  /// Whether the sidebar starts in the narrow (icon-only) mode.
168
245
  final bool initiallyCollapsed;
169
246
 
170
247
  /// The screen edge this sidebar is anchored to.
171
- ///
172
- /// [KasySidebarProSide.left] → flat left corners, rounded right corners.
173
- /// [KasySidebarProSide.right] flat right corners, rounded left corners.
174
- final KasySidebarProSide side;
248
+ final KasySidebarSide side;
249
+
250
+ /// Bart bottom-bar routes to wire into the sidebar. When provided (along with
251
+ /// [onTapItem] and [currentItem]) the sidebar runs in "connected" mode with
252
+ /// real, navigable items. When null, it shows the HeroUI showcase items.
253
+ final List<BartMenuRoute>? routes;
254
+
255
+ /// Called with the route index when a real nav item is tapped.
256
+ final OnTapItem? onTapItem;
257
+
258
+ /// Bart's active-tab index notifier, used to highlight the current screen.
259
+ final ValueNotifier<int>? currentItem;
175
260
 
176
261
  @override
177
- State<KasySidebarPro> createState() => _KasySidebarProState();
262
+ State<KasySidebar> createState() => _KasySidebarState();
178
263
  }
179
264
 
180
- class _KasySidebarProState extends State<KasySidebarPro> {
265
+ class _KasySidebarState extends State<KasySidebar> {
181
266
  // User's explicit open/close preference (set by tapping the toggle button).
182
267
  late bool _userChoseCollapsed;
183
268
 
@@ -186,27 +271,35 @@ class _KasySidebarProState extends State<KasySidebarPro> {
186
271
  bool _collapsed = false;
187
272
 
188
273
  bool _incomeExpanded = false;
189
- String _activeItemId = 'dashboard';
274
+
275
+ // Showcase state.
276
+ int _showcaseTab = 0; // 0 = Layers, 1 = Assets
277
+ late String _activeItemId;
190
278
  String _activeSubItem = '';
191
279
 
192
280
  /// Viewport width below which the sidebar auto-collapses (tablet breakpoint).
193
281
  static const double _kBreakpoint = 1024.0;
194
282
 
283
+ /// True when wired to Bart's navigation (real, tappable screens).
284
+ bool get _connected =>
285
+ widget.routes != null &&
286
+ widget.routes!.length >= 2 &&
287
+ widget.onTapItem != null &&
288
+ widget.currentItem != null;
289
+
195
290
  @override
196
291
  void initState() {
197
292
  super.initState();
198
293
  _userChoseCollapsed = widget.initiallyCollapsed;
294
+ // Connected mode follows Bart's currentItem (empty highlight here); the
295
+ // showcase defaults to the active layer from the Figma reference.
296
+ _activeItemId = _connected ? '' : 'object2';
199
297
  }
200
298
 
201
299
  bool _isViewportNarrow(BuildContext context) =>
202
300
  MediaQuery.sizeOf(context).width < _kBreakpoint;
203
301
 
204
- // ── Color helpers ──────────────────────────────────────────────────────────
205
-
206
- _SidebarColors get _colors =>
207
- Theme.of(context).brightness == Brightness.dark
208
- ? _SidebarColors.dark
209
- : _SidebarColors.light;
302
+ _SidebarColors get _colors => _SidebarColors.fromContext(context);
210
303
 
211
304
  // ── Actions ───────────────────────────────────────────────────────────────
212
305
 
@@ -214,6 +307,19 @@ class _KasySidebarProState extends State<KasySidebarPro> {
214
307
  void _toggleCollapse() =>
215
308
  setState(() => _userChoseCollapsed = !_userChoseCollapsed);
216
309
 
310
+ /// Navigates to a real route via Bart and clears any static-item highlight.
311
+ /// Moving to another screen also collapses an open submenu (e.g. Income) and
312
+ /// drops its selected sub-item, so the sidebar always reflects the active
313
+ /// screen rather than a left-over expanded menu.
314
+ void _navigateTo(int index) {
315
+ setState(() {
316
+ _activeItemId = '';
317
+ _incomeExpanded = false;
318
+ _activeSubItem = '';
319
+ });
320
+ widget.onTapItem!(index);
321
+ }
322
+
217
323
  void _activateItem(String id) {
218
324
  if (id == 'settings') {
219
325
  widget.onSettingsTap?.call();
@@ -222,127 +328,122 @@ class _KasySidebarProState extends State<KasySidebarPro> {
222
328
  }
223
329
  setState(() {
224
330
  _activeItemId = id;
225
- // Leaving Income → clear the active sub-item so it doesn't stay
226
- // highlighted when the user is on a completely different page.
227
331
  if (id != 'income') _activeSubItem = '';
228
332
  });
229
333
  }
230
334
 
231
335
  void _activateSubItem(String label) => setState(() {
232
336
  _activeSubItem = label;
233
- _activeItemId = 'income'; // sub-item belongs to Income → highlight it
337
+ _activeItemId = 'income';
234
338
  });
235
339
 
236
340
  // ── Build ─────────────────────────────────────────────────────────────────
237
341
 
238
342
  @override
239
343
  Widget build(BuildContext context) {
240
- // Recompute effective collapsed state every build so viewport changes
241
- // are picked up automatically without any extra listener.
242
344
  _collapsed = _userChoseCollapsed || _isViewportNarrow(context);
243
345
 
244
346
  final c = _colors;
347
+ final bool anchoredLeft = widget.side == KasySidebarSide.left;
348
+
349
+ // Hairline on the content-facing edge (Figma `separator`).
350
+ final Border edgeBorder = anchoredLeft
351
+ ? Border(right: BorderSide(color: c.border, width: 0.5))
352
+ : Border(left: BorderSide(color: c.border, width: 0.5));
353
+
354
+ // Figma `shadow-surface`: a soft lift in light mode only (dark uses none).
355
+ final List<BoxShadow> shadow = c.isDark
356
+ ? const <BoxShadow>[]
357
+ : const <BoxShadow>[
358
+ BoxShadow(
359
+ color: Color(0x14000000),
360
+ blurRadius: 4,
361
+ offset: Offset(0, 2),
362
+ ),
363
+ BoxShadow(
364
+ color: Color(0x0F000000),
365
+ blurRadius: 2,
366
+ offset: Offset(0, 1),
367
+ ),
368
+ ];
245
369
 
246
- final bool anchoredLeft = widget.side == KasySidebarProSide.left;
247
- final Border border = anchoredLeft
248
- ? Border(right: BorderSide(color: c.border))
249
- : Border(left: BorderSide(color: c.border));
250
-
251
- // The outer AnimatedContainer is extended by half the button width (14px)
252
- // on the side where the collapse button protrudes. This makes the button
253
- // visually straddle the edge AND stay within parent bounds — Flutter only
254
- // delivers hit tests to widgets inside their parent's bounds.
255
- //
256
- // Layout: left-anchored example
257
- // ┌── outer (256+14) ─────────────────────────┐
258
- // │ ┌── sidebar box (256) ──────────┐ │
259
- // │ │ content │ [button] │
260
- // │ └───────────────────────────────┘ │
261
- // └────────────────────────────────────────────┘
262
- //
263
- // The 14px transparent strip on the right holds the outer half of the
264
- // button, giving it a full 28×28 hittable area.
265
- const double halfBtn = _kCollapseButtonSize / 2; // 14px
370
+ final Widget content = _connected
371
+ ? ValueListenableBuilder<int>(
372
+ valueListenable: widget.currentItem!,
373
+ builder: (_, currentIndex, _) =>
374
+ _buildConnectedContent(context, c, currentIndex),
375
+ )
376
+ : _buildShowcaseContent(context, c);
266
377
 
267
378
  return Material(
268
379
  type: MaterialType.transparency,
269
380
  child: AnimatedContainer(
270
381
  duration: const Duration(milliseconds: 220),
271
382
  curve: Curves.easeInOut,
272
- width: (_collapsed ? _kWidthCollapsed : _kWidthOpen) + halfBtn,
273
- child: Stack(
274
- children: [
275
- // Sidebar background + content (leaves halfBtn space for button).
276
- Positioned(
277
- left: anchoredLeft ? 0 : halfBtn,
278
- right: anchoredLeft ? halfBtn : 0,
279
- top: 0,
280
- bottom: 0,
281
- child: DecoratedBox(
282
- decoration: BoxDecoration(
283
- color: c.bg,
284
- border: border,
285
- ),
286
- child: _buildScrollableContent(context, c),
287
- ),
288
- ),
289
- // Collapse button — centered on the sidebar edge, fully within
290
- // the outer container so hit tests work correctly.
291
- _buildCollapseButton(context, c),
292
- ],
293
- ),
383
+ width: _collapsed ? _kWidthCollapsed : _kWidthOpen,
384
+ decoration: BoxDecoration(color: c.bg, boxShadow: shadow),
385
+ foregroundDecoration: BoxDecoration(border: edgeBorder),
386
+ clipBehavior: Clip.hardEdge,
387
+ child: content,
294
388
  ),
295
389
  );
296
390
  }
297
391
 
298
- // ── Scrollable content ────────────────────────────────────────────────────
392
+ // ── Showcase layout (HeroUI Figma 1:1) ──────────────────────────────────────
299
393
 
300
- Widget _buildScrollableContent(BuildContext context, _SidebarColors c) {
301
- // Use a Column that fills the sidebar height.
302
- // Top section scrolls if content overflows; bottom items (Help + Logout)
303
- // stay pinned to the bottom via Spacer in the outer Column.
304
- // SizedBox.expand (not Positioned.fill) because the parent here is a
305
- // DecoratedBox, not a Stack — Positioned only works as a direct Stack child.
394
+ Widget _buildShowcaseContent(BuildContext context, _SidebarColors c) {
306
395
  return SizedBox.expand(
307
396
  child: Column(
308
- crossAxisAlignment: CrossAxisAlignment.start,
397
+ crossAxisAlignment: CrossAxisAlignment.stretch,
309
398
  children: [
310
- // ── Scrollable nav area ──────────────────────────────────────────
399
+ // Logo band 68px tall so the divider below aligns with the web
400
+ // header's bottom border (one continuous line across the chrome).
401
+ _buildTopBand(c),
402
+ _buildDivider(c),
403
+ // Nav: workspace selector + segmented tabs + the layers list.
311
404
  Expanded(
312
- child: SingleChildScrollView(
313
- padding: const EdgeInsets.fromLTRB(
314
- _kPad, _kPad, _kPad, 0,
315
- ),
316
- child: Column(
317
- crossAxisAlignment: CrossAxisAlignment.start,
318
- children: [
319
- _buildProfile(context, c),
320
- const SizedBox(height: _kPad),
321
- _buildDivider(c),
322
- const SizedBox(height: _kPad),
323
- _buildSectionLabel('MAIN', c),
324
- const SizedBox(height: _kItemGap),
325
- ..._kMainItems.map((item) => _buildNavItem(context, item, c)),
326
- const SizedBox(height: _kPad),
327
- _buildDivider(c),
328
- const SizedBox(height: _kPad),
329
- _buildSectionLabel('SETTINGS', c),
330
- const SizedBox(height: _kItemGap),
331
- _buildNavItem(context, _kSettingsItem, c),
332
- ],
405
+ child: Padding(
406
+ padding: const EdgeInsets.fromLTRB(_kPadH, _kDividerGap, _kPadH, 0),
407
+ child: SingleChildScrollView(
408
+ child: Column(
409
+ crossAxisAlignment: CrossAxisAlignment.start,
410
+ children: [
411
+ if (!_collapsed) ...[
412
+ _buildWorkspaceSelector(c),
413
+ const SizedBox(height: _kHeaderGap),
414
+ _buildTabs(c),
415
+ const SizedBox(height: _kNavGap),
416
+ ],
417
+ for (final item in _kShowcaseItems)
418
+ _buildNavItem(context, item, c),
419
+ ],
420
+ ),
333
421
  ),
334
422
  ),
335
423
  ),
336
- // ── Bottom items pinned ──────────────────────────────────────────
424
+ // Pinned ⌘K search row + profile block.
425
+ _buildDivider(c),
337
426
  Padding(
338
427
  padding: const EdgeInsets.fromLTRB(
339
- _kPad, _kItemGap, _kPad, _kPad,
428
+ _kPadH,
429
+ _kFooterGap,
430
+ _kPadH,
431
+ _kPadBottom,
340
432
  ),
341
433
  child: Column(
342
434
  crossAxisAlignment: CrossAxisAlignment.start,
343
- children: _kBottomItems
344
- .map((item) => _buildNavItem(context, item, c))
345
- .toList(),
435
+ children: [
436
+ _buildItemRow(
437
+ c,
438
+ icon: KasyIcons.search,
439
+ label: 'Search',
440
+ isActive: false,
441
+ onTap: () {},
442
+ bottomGap: 0,
443
+ trailing: [_buildKbd(c)],
444
+ ),
445
+ if (widget.showProfile) _buildProfile(c),
446
+ ],
346
447
  ),
347
448
  ),
348
449
  ],
@@ -350,161 +451,461 @@ class _KasySidebarProState extends State<KasySidebarPro> {
350
451
  );
351
452
  }
352
453
 
353
- // ── Profile ───────────────────────────────────────────────────────────────
454
+ // ── Connected layout (real navigation) ──────────────────────────────────────
354
455
 
355
- Widget _buildProfile(BuildContext context, _SidebarColors c) {
356
- const double avatarSize = 44;
357
- final Widget avatar = Container(
358
- width: avatarSize,
359
- height: avatarSize,
360
- decoration: BoxDecoration(
361
- color: c.divider,
362
- shape: BoxShape.circle,
363
- ),
364
- child: Icon(KasyIcons.person, size: 20, color: c.textMuted),
365
- );
456
+ Widget _buildConnectedContent(
457
+ BuildContext context,
458
+ _SidebarColors c,
459
+ int currentIndex,
460
+ ) {
461
+ final int settingsIndex = widget.routes!.length - 1;
462
+ final nav = context.t.navigation;
463
+ final List<({IconData icon, String label})> meta = [
464
+ (icon: KasyIcons.home, label: nav.home),
465
+ (icon: KasyIcons.help, label: nav.support),
466
+ (icon: KasyIcons.notification, label: nav.notifications),
467
+ ];
468
+ final int mainCount = widget.routes!.length - 1; // exclude settings (last)
366
469
 
367
- return Row(
368
- children: [
369
- avatar,
370
- AnimatedSize(
371
- duration: const Duration(milliseconds: 220),
372
- curve: Curves.easeInOut,
373
- child: _collapsed
374
- ? const SizedBox.shrink()
375
- : Row(
376
- mainAxisSize: MainAxisSize.min,
470
+ return SizedBox.expand(
471
+ child: Column(
472
+ crossAxisAlignment: CrossAxisAlignment.stretch,
473
+ children: [
474
+ _buildTopBand(c),
475
+ _buildDivider(c),
476
+ Expanded(
477
+ child: Padding(
478
+ padding: const EdgeInsets.fromLTRB(_kPadH, _kDividerGap, _kPadH, 0),
479
+ child: SingleChildScrollView(
480
+ child: Column(
481
+ crossAxisAlignment: CrossAxisAlignment.start,
377
482
  children: [
378
- const SizedBox(width: 12),
379
- Flexible(
380
- child: Column(
381
- crossAxisAlignment: CrossAxisAlignment.start,
382
- mainAxisSize: MainAxisSize.min,
383
- children: [
384
- Text(
385
- 'PRODUCT MANAGER',
386
- style: TextStyle(
387
- fontSize: 10,
388
- fontWeight: FontWeight.w500,
389
- color: c.textMuted,
390
- letterSpacing: 0.4,
391
- ),
392
- ),
393
- const SizedBox(height: 4),
394
- Text(
395
- 'Andrew Smith',
396
- style: TextStyle(
397
- fontSize: 14,
398
- fontWeight: FontWeight.w500,
399
- color: c.textActive,
400
- ),
401
- ),
402
- ],
483
+ if (!_collapsed) ...[
484
+ _buildSectionLabel('MAIN', c),
485
+ const SizedBox(height: _kItemGap),
486
+ ],
487
+ // Real, navigable screens.
488
+ for (int i = 0; i < mainCount; i++)
489
+ _buildItemRow(
490
+ c,
491
+ icon: i < meta.length
492
+ ? meta[i].icon
493
+ : (widget.routes![i].icon ?? KasyIcons.note),
494
+ label: i < meta.length
495
+ ? meta[i].label
496
+ : (widget.routes![i].label ?? ''),
497
+ isActive: _activeItemId.isEmpty && currentIndex == i,
498
+ onTap: () => _navigateTo(i),
403
499
  ),
500
+ // Static showcase extras (incl. the Income submenu).
501
+ for (final item in _kMainItems.skip(1))
502
+ _buildNavItem(context, item, c),
503
+ const SizedBox(height: _kDividerGap),
504
+ if (!_collapsed) ...[
505
+ _buildSectionLabel('SETTINGS', c),
506
+ const SizedBox(height: _kItemGap),
507
+ ],
508
+ _buildItemRow(
509
+ c,
510
+ icon: KasyIcons.settings,
511
+ label: nav.settings,
512
+ isActive:
513
+ _activeItemId.isEmpty && currentIndex == settingsIndex,
514
+ onTap: () => _navigateTo(settingsIndex),
404
515
  ),
405
516
  ],
406
517
  ),
518
+ ),
519
+ ),
520
+ ),
521
+ _buildDivider(c),
522
+ const SizedBox(height: _kFooterGap),
523
+ Padding(
524
+ padding: const EdgeInsets.fromLTRB(_kPadH, 0, _kPadH, _kPadBottom),
525
+ child: Column(
526
+ crossAxisAlignment: CrossAxisAlignment.start,
527
+ children: [
528
+ _buildNavItem(context, _kHelpItem, c),
529
+ _buildItemRow(
530
+ c,
531
+ icon: KasyIcons.logout,
532
+ label: nav.logout,
533
+ isActive: false,
534
+ isLogout: true,
535
+ bottomGap: 0,
536
+ onTap: () => widget.onLogout?.call(),
537
+ ),
538
+ if (widget.showProfile) _buildProfile(c),
539
+ ],
540
+ ),
541
+ ),
542
+ ],
543
+ ),
544
+ );
545
+ }
546
+
547
+ // ── Top band (logo + panel toggle) ──────────────────────────────────────────
548
+
549
+ /// The brand band at the top of the rail. Fixed to [_kTopBandHeight] (= web
550
+ /// header height) so the divider underneath lines up with the header's bottom
551
+ /// border. Content is vertically centred, mirroring the header's toolbar row.
552
+ Widget _buildTopBand(_SidebarColors c) {
553
+ return Padding(
554
+ padding: const EdgeInsets.symmetric(horizontal: _kPadH),
555
+ child: SizedBox(
556
+ height: _kTopBandHeight,
557
+ child: _collapsed
558
+ ? Center(child: _buildToggleButton(c))
559
+ : Row(
560
+ children: [
561
+ // Brand wordmark — same artwork as the splash screen.
562
+ Image.asset(
563
+ c.isDark
564
+ ? 'assets/images/logo_wordmark_dark.png'
565
+ : 'assets/images/logo_wordmark_light.png',
566
+ height: 32,
567
+ fit: BoxFit.contain,
568
+ ),
569
+ const Spacer(),
570
+ _buildToggleButton(c),
571
+ ],
572
+ ),
573
+ ),
574
+ );
575
+ }
576
+
577
+ Widget _buildToggleButton(_SidebarColors c) {
578
+ return KasyHover(
579
+ borderRadius: BorderRadius.circular(_kToggleSize / 2),
580
+ hoverColor: c.activeBg,
581
+ pressColor: c.textActive,
582
+ onTap: _toggleCollapse,
583
+ child: Container(
584
+ width: _kToggleSize,
585
+ height: _kToggleSize,
586
+ alignment: Alignment.center,
587
+ decoration: BoxDecoration(
588
+ borderRadius: BorderRadius.circular(_kToggleSize / 2),
589
+ ),
590
+ child: Icon(KasyIcons.panelLeft, size: _kIconSize, color: c.textMuted),
591
+ ),
592
+ );
593
+ }
594
+
595
+ // ── Workspace selector ──────────────────────────────────────────────────────
596
+
597
+ Widget _buildWorkspaceSelector(_SidebarColors c) {
598
+ return Column(
599
+ crossAxisAlignment: CrossAxisAlignment.start,
600
+ mainAxisSize: MainAxisSize.min,
601
+ children: [
602
+ Row(
603
+ mainAxisSize: MainAxisSize.min,
604
+ children: [
605
+ Flexible(
606
+ child: Text(
607
+ '3D Dog Character',
608
+ maxLines: 1,
609
+ overflow: TextOverflow.ellipsis,
610
+ style: TextStyle(
611
+ fontSize: 14,
612
+ height: 20 / 14,
613
+ fontWeight: FontWeight.w500,
614
+ color: c.textActive,
615
+ ),
616
+ ),
617
+ ),
618
+ const SizedBox(width: 16),
619
+ Icon(KasyIcons.chevronDown, size: _kIconSize, color: c.textMuted),
620
+ ],
621
+ ),
622
+ const SizedBox(height: 4),
623
+ Text(
624
+ '3D Design Project',
625
+ maxLines: 1,
626
+ overflow: TextOverflow.ellipsis,
627
+ style: TextStyle(
628
+ fontSize: 12,
629
+ height: 16 / 12,
630
+ fontWeight: FontWeight.w400,
631
+ color: c.textMuted,
632
+ ),
407
633
  ),
408
634
  ],
409
635
  );
410
636
  }
411
637
 
412
- // ── Divider ───────────────────────────────────────────────────────────────
638
+ // ── Segmented tabs (Layers / Assets) ────────────────────────────────────────
413
639
 
414
- Widget _buildDivider(_SidebarColors c) {
640
+ Widget _buildTabs(_SidebarColors c) {
415
641
  return Container(
416
- height: 2,
642
+ width: double.infinity,
643
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
417
644
  decoration: BoxDecoration(
418
- color: c.divider,
419
- borderRadius: BorderRadius.circular(2),
645
+ color: c.activeBg,
646
+ borderRadius: BorderRadius.circular(24),
647
+ ),
648
+ child: Row(
649
+ children: [
650
+ _buildTab(c, 'Layers', 0),
651
+ const SizedBox(width: 2),
652
+ _buildTab(c, 'Assets', 1),
653
+ ],
420
654
  ),
421
655
  );
422
656
  }
423
657
 
424
- // ── Section label ─────────────────────────────────────────────────────────
658
+ Widget _buildTab(_SidebarColors c, String label, int index) {
659
+ final bool selected = _showcaseTab == index;
660
+ return Expanded(
661
+ child: MouseRegion(
662
+ cursor: SystemMouseCursors.click,
663
+ child: GestureDetector(
664
+ behavior: HitTestBehavior.opaque,
665
+ onTap: () => setState(() => _showcaseTab = index),
666
+ child: AnimatedContainer(
667
+ duration: const Duration(milliseconds: 180),
668
+ curve: Curves.easeOut,
669
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
670
+ alignment: Alignment.center,
671
+ decoration: BoxDecoration(
672
+ color: selected ? c.segmentThumb : Colors.transparent,
673
+ borderRadius: BorderRadius.circular(24),
674
+ boxShadow: selected
675
+ ? const [
676
+ BoxShadow(
677
+ color: Color(0x0F000000),
678
+ blurRadius: 8,
679
+ offset: Offset(0, 2),
680
+ ),
681
+ ]
682
+ : null,
683
+ ),
684
+ child: Text(
685
+ label,
686
+ style: TextStyle(
687
+ fontSize: 14,
688
+ height: 20 / 14,
689
+ fontWeight: FontWeight.w500,
690
+ color: selected ? c.textActive : c.textMuted,
691
+ ),
692
+ ),
693
+ ),
694
+ ),
695
+ ),
696
+ );
697
+ }
425
698
 
426
- Widget _buildSectionLabel(String label, _SidebarColors c) {
699
+ // ── ⌘K keyboard chip ─────────────────────────────────────────────────────────
700
+
701
+ Widget _buildKbd(_SidebarColors c) {
427
702
  final TextStyle style = TextStyle(
428
- fontSize: 10,
703
+ fontSize: 14,
704
+ height: 20 / 14,
429
705
  fontWeight: FontWeight.w500,
430
706
  color: c.textMuted,
431
- letterSpacing: 0.4,
432
707
  );
708
+ return Container(
709
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
710
+ decoration: BoxDecoration(
711
+ color: c.activeBg,
712
+ borderRadius: BorderRadius.circular(8),
713
+ ),
714
+ child: Row(
715
+ mainAxisSize: MainAxisSize.min,
716
+ children: [
717
+ Text('⌘', style: style),
718
+ const SizedBox(width: 2),
719
+ Text('K', style: style),
720
+ ],
721
+ ),
722
+ );
723
+ }
724
+
725
+ // ── Profile block (bottom) ──────────────────────────────────────────────────
726
+
727
+ /// Account row pinned at the bottom: the [KasySidebar.profileAvatar] (the
728
+ /// signed-in user's photo) when provided, otherwise a gradient-fill avatar +
729
+ /// name + email. Collapses to just the avatar on the narrow rail.
730
+ Widget _buildProfile(_SidebarColors c) {
731
+ final Widget avatar =
732
+ widget.profileAvatar ??
733
+ KasyAvatar(diameter: 36, backgroundGradient: widget.profileGradient);
433
734
 
434
735
  if (_collapsed) {
435
- // Centered + clipped so long labels (e.g. SETTINGS) don't overflow.
436
- return Center(
437
- child: Text(
438
- label,
439
- style: style,
440
- overflow: TextOverflow.clip,
441
- softWrap: false,
442
- textAlign: TextAlign.center,
443
- ),
736
+ return Padding(
737
+ padding: const EdgeInsets.only(top: _kItemGap),
738
+ child: Center(child: avatar),
444
739
  );
445
740
  }
446
741
 
742
+ return Padding(
743
+ padding: const EdgeInsets.only(top: _kItemGap),
744
+ child: KasyHover(
745
+ borderRadius: BorderRadius.circular(_kItemRadius),
746
+ hoverColor: c.activeBg,
747
+ pressColor: c.textActive,
748
+ focusable: true,
749
+ onTap: widget.onProfileTap ?? () {},
750
+ child: Padding(
751
+ padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
752
+ child: Row(
753
+ children: [
754
+ avatar,
755
+ const SizedBox(width: _kIconGap),
756
+ Expanded(
757
+ child: Column(
758
+ crossAxisAlignment: CrossAxisAlignment.start,
759
+ mainAxisSize: MainAxisSize.min,
760
+ children: [
761
+ Text(
762
+ widget.profileName,
763
+ maxLines: 1,
764
+ overflow: TextOverflow.ellipsis,
765
+ style: TextStyle(
766
+ fontSize: 14,
767
+ height: 20 / 14,
768
+ fontWeight: FontWeight.w500,
769
+ color: c.textActive,
770
+ ),
771
+ ),
772
+ Text(
773
+ widget.profileEmail,
774
+ maxLines: 1,
775
+ overflow: TextOverflow.ellipsis,
776
+ style: TextStyle(
777
+ fontSize: 12,
778
+ height: 16 / 12,
779
+ fontWeight: FontWeight.w500,
780
+ color: c.textMuted,
781
+ ),
782
+ ),
783
+ ],
784
+ ),
785
+ ),
786
+ ],
787
+ ),
788
+ ),
789
+ ),
790
+ );
791
+ }
792
+
793
+ // ── Divider ───────────────────────────────────────────────────────────────
794
+
795
+ // 0.5px hairline — same width + colour as the vertical edge line and the web
796
+ // header's bottom border, so all the chrome lines read identically.
797
+ Widget _buildDivider(_SidebarColors c) =>
798
+ Container(height: 0.5, color: c.divider);
799
+
800
+ // ── Section label ─────────────────────────────────────────────────────────
801
+
802
+ Widget _buildSectionLabel(String label, _SidebarColors c) {
447
803
  return Padding(
448
804
  padding: const EdgeInsets.only(left: _kItemHPad),
449
- child: Text(label, style: style),
805
+ child: Text(
806
+ label,
807
+ style: TextStyle(
808
+ fontSize: 11,
809
+ fontWeight: FontWeight.w600,
810
+ color: c.textMuted,
811
+ letterSpacing: 0.6,
812
+ ),
813
+ ),
450
814
  );
451
815
  }
452
816
 
453
- // ── Nav item ──────────────────────────────────────────────────────────────
817
+ // ── Nav item dispatch ───────────────────────────────────────────────────────
454
818
 
455
- Widget _buildNavItem(
456
- BuildContext context,
457
- _NavItem item,
458
- _SidebarColors c,
459
- ) {
819
+ Widget _buildNavItem(BuildContext context, _NavItem item, _SidebarColors c) {
460
820
  if (item.hasSubmenu) {
461
821
  return _buildDropdownItem(context, item, c);
462
822
  }
463
-
464
823
  final bool isActive = _activeItemId == item.id;
465
- // Active bg is explicit; hover bg comes from KasyHover.hoverColor.
466
- final Color bg = isActive ? c.activeBg : Colors.transparent;
467
- final Color iconColor = item.isLogout
824
+ return _buildItemRow(
825
+ c,
826
+ icon: item.icon,
827
+ label: item.label,
828
+ isActive: isActive,
829
+ onTap: () => _activateItem(item.id),
830
+ trailing: item.trailingControls
831
+ ? [
832
+ Icon(KasyIcons.security, size: _kIconSize, color: c.textMuted),
833
+ const SizedBox(width: 12),
834
+ Icon(KasyIcons.eye, size: _kIconSize, color: c.textMuted),
835
+ ]
836
+ : const [],
837
+ );
838
+ }
839
+
840
+ // ── Generic row (expanded) / icon+tooltip (collapsed) ────────────────────────
841
+
842
+ Widget _buildItemRow(
843
+ _SidebarColors c, {
844
+ required IconData icon,
845
+ required String label,
846
+ required bool isActive,
847
+ required VoidCallback onTap,
848
+ bool isLogout = false,
849
+ List<Widget> trailing = const [],
850
+ double bottomGap = _kItemGap,
851
+ }) {
852
+ final Color fill = isActive ? c.activeBg : Colors.transparent;
853
+ final Color iconColor = isLogout
468
854
  ? c.logout
469
855
  : (isActive ? c.textActive : c.textMuted);
470
- final Color textColor = item.isLogout ? c.logout : iconColor;
856
+ final Color labelColor = isLogout ? c.logout : c.textActive;
471
857
 
472
- // Collapsed: icon only, optionally with hover popup for sub-items
473
858
  if (_collapsed) {
474
- return _buildCollapsedIcon(context, item, bg, iconColor, c);
859
+ return Padding(
860
+ padding: EdgeInsets.only(bottom: bottomGap),
861
+ child: _ProTooltipIcon(
862
+ icon: icon,
863
+ label: label,
864
+ iconBg: fill,
865
+ iconColor: iconColor,
866
+ activeBg: c.activeBg,
867
+ colors: c,
868
+ onTap: onTap,
869
+ ),
870
+ );
475
871
  }
476
872
 
477
873
  return Padding(
478
- padding: const EdgeInsets.only(bottom: _kItemGap),
874
+ padding: EdgeInsets.only(bottom: bottomGap),
479
875
  child: KasyHover(
480
876
  borderRadius: BorderRadius.circular(_kItemRadius),
481
877
  hoverColor: c.activeBg,
482
- pressColor: c.activeBg,
483
- onTap: () => _activateItem(item.id),
878
+ pressColor: c.textActive,
879
+ focusable: true,
880
+ onTap: onTap,
484
881
  child: Container(
882
+ constraints: const BoxConstraints(minHeight: _kItemMinH),
485
883
  padding: const EdgeInsets.symmetric(
486
884
  horizontal: _kItemHPad,
487
885
  vertical: _kItemVPad,
488
886
  ),
489
887
  decoration: BoxDecoration(
490
- color: bg,
888
+ color: fill,
491
889
  borderRadius: BorderRadius.circular(_kItemRadius),
492
890
  ),
493
891
  child: Row(
494
892
  children: [
495
- _ProIcon(icon: item.icon, color: iconColor, active: isActive),
496
- const SizedBox(width: 12),
893
+ Icon(icon, size: _kIconSize, color: iconColor),
894
+ const SizedBox(width: _kIconGap),
497
895
  Expanded(
498
896
  child: Text(
499
- item.label,
897
+ label,
898
+ maxLines: 1,
899
+ overflow: TextOverflow.ellipsis,
500
900
  style: TextStyle(
501
901
  fontSize: 14,
502
- fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
503
- color: textColor,
504
- letterSpacing: -0.28,
902
+ height: 20 / 14,
903
+ fontWeight: FontWeight.w500,
904
+ color: labelColor,
505
905
  ),
506
906
  ),
507
907
  ),
908
+ ...trailing,
508
909
  ],
509
910
  ),
510
911
  ),
@@ -512,30 +913,7 @@ class _KasySidebarProState extends State<KasySidebarPro> {
512
913
  );
513
914
  }
514
915
 
515
- // ── Collapsed icon button ─────────────────────────────────────────────────
516
-
517
- Widget _buildCollapsedIcon(
518
- BuildContext context,
519
- _NavItem item,
520
- Color bg,
521
- Color iconColor,
522
- _SidebarColors c,
523
- ) {
524
- return Padding(
525
- padding: const EdgeInsets.only(bottom: _kItemGap),
526
- child: _ProTooltipIcon(
527
- icon: item.icon,
528
- label: item.label,
529
- iconBg: bg,
530
- iconColor: iconColor,
531
- activeBg: c.activeBg,
532
- colors: c,
533
- onTap: () => _activateItem(item.id),
534
- ),
535
- );
536
- }
537
-
538
- // ── Dropdown item (Income) ────────────────────────────────────────────────
916
+ // ── Dropdown item (submenu — used by connected Income) ───────────────────────
539
917
 
540
918
  Widget _buildDropdownItem(
541
919
  BuildContext context,
@@ -547,7 +925,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
547
925
  final Color iconColor = isActive ? c.textActive : c.textMuted;
548
926
 
549
927
  if (_collapsed) {
550
- // In collapsed mode: icon that shows a floating popup on hover
551
928
  return Padding(
552
929
  padding: const EdgeInsets.only(bottom: _kItemGap),
553
930
  child: _ProHoverPopupIcon(
@@ -565,53 +942,50 @@ class _KasySidebarProState extends State<KasySidebarPro> {
565
942
  return Column(
566
943
  crossAxisAlignment: CrossAxisAlignment.start,
567
944
  children: [
568
- // Header row — no bottom padding here; the SizedBox at the bottom of
569
- // this Column provides the single trailing gap (same as other items).
570
945
  KasyHover(
946
+ borderRadius: BorderRadius.circular(_kItemRadius),
947
+ hoverColor: c.activeBg,
948
+ pressColor: c.textActive,
949
+ focusable: true,
950
+ onTap: () => _activateItem(item.id),
951
+ child: Container(
952
+ constraints: const BoxConstraints(minHeight: _kItemMinH),
953
+ padding: const EdgeInsets.symmetric(
954
+ horizontal: _kItemHPad,
955
+ vertical: _kItemVPad,
956
+ ),
957
+ decoration: BoxDecoration(
958
+ color: bg,
571
959
  borderRadius: BorderRadius.circular(_kItemRadius),
572
- hoverColor: c.activeBg,
573
- pressColor: c.activeBg,
574
- onTap: () => _activateItem(item.id),
575
- child: Container(
576
- padding: const EdgeInsets.symmetric(
577
- horizontal: _kItemHPad,
578
- vertical: _kItemVPad,
579
- ),
580
- decoration: BoxDecoration(
581
- color: bg,
582
- borderRadius: BorderRadius.circular(_kItemRadius),
583
- ),
584
- child: Row(
585
- children: [
586
- _ProIcon(icon: item.icon, color: iconColor, active: isActive),
587
- const SizedBox(width: 12),
588
- Expanded(
589
- child: Text(
590
- item.label,
591
- style: TextStyle(
592
- fontSize: 14,
593
- fontWeight: isActive
594
- ? FontWeight.w600
595
- : FontWeight.w500,
596
- color: iconColor,
597
- letterSpacing: -0.28,
598
- ),
599
- ),
600
- ),
601
- AnimatedRotation(
602
- turns: _incomeExpanded ? 0.5 : 0,
603
- duration: const Duration(milliseconds: 200),
604
- child: Icon(
605
- KasyIcons.chevronDown,
606
- size: 16,
607
- color: iconColor,
608
- ),
960
+ ),
961
+ child: Row(
962
+ children: [
963
+ Icon(item.icon, size: _kIconSize, color: iconColor),
964
+ const SizedBox(width: _kIconGap),
965
+ Expanded(
966
+ child: Text(
967
+ item.label,
968
+ style: TextStyle(
969
+ fontSize: 14,
970
+ height: 20 / 14,
971
+ fontWeight: FontWeight.w500,
972
+ color: c.textActive,
609
973
  ),
610
- ],
974
+ ),
611
975
  ),
612
- ),
976
+ AnimatedRotation(
977
+ turns: _incomeExpanded ? 0.5 : 0,
978
+ duration: const Duration(milliseconds: 200),
979
+ child: Icon(
980
+ KasyIcons.chevronDown,
981
+ size: _kIconSize,
982
+ color: iconColor,
983
+ ),
984
+ ),
985
+ ],
986
+ ),
987
+ ),
613
988
  ),
614
- // Expandable sub-items tree
615
989
  AnimatedCrossFade(
616
990
  duration: const Duration(milliseconds: 200),
617
991
  crossFadeState: _incomeExpanded
@@ -628,9 +1002,7 @@ class _KasySidebarProState extends State<KasySidebarPro> {
628
1002
  // ── Sub-items tree ────────────────────────────────────────────────────────
629
1003
 
630
1004
  Widget _buildSubItemsTree(_SidebarColors c) {
631
- final subItems = _kMainItems
632
- .firstWhere((i) => i.id == 'income')
633
- .subItems;
1005
+ final subItems = _kMainItems.firstWhere((i) => i.id == 'income').subItems;
634
1006
  final double lineH = _treeLineHeight(subItems.length);
635
1007
 
636
1008
  return Padding(
@@ -640,12 +1012,11 @@ class _KasySidebarProState extends State<KasySidebarPro> {
640
1012
  child: Stack(
641
1013
  clipBehavior: Clip.none,
642
1014
  children: [
643
- // Vertical tree line
644
1015
  Positioned(
645
1016
  left: -_kTreeConnectorW,
646
1017
  top: 0,
647
1018
  child: Container(
648
- width: 2,
1019
+ width: 1.5,
649
1020
  height: lineH,
650
1021
  decoration: BoxDecoration(
651
1022
  color: c.divider,
@@ -653,7 +1024,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
653
1024
  ),
654
1025
  ),
655
1026
  ),
656
- // Sub-items
657
1027
  Column(
658
1028
  children: subItems.asMap().entries.map((entry) {
659
1029
  final i = entry.key;
@@ -670,9 +1040,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
670
1040
 
671
1041
  Widget _buildSubItem(String label, bool isLast, _SidebarColors c) {
672
1042
  final bool isActive = _activeSubItem == label;
673
- // Sub-items: active state = bolder text only, no background fill.
674
- // Hover still shows the fill via KasyHover.hoverColor for interactivity.
675
- const Color bg = Colors.transparent;
676
1043
  final Color textColor = isActive ? c.textActive : c.textMuted;
677
1044
 
678
1045
  return Padding(
@@ -680,7 +1047,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
680
1047
  child: Stack(
681
1048
  clipBehavior: Clip.none,
682
1049
  children: [
683
- // L-connector: bottom-left rounded border
684
1050
  Positioned(
685
1051
  left: -_kTreeConnectorW,
686
1052
  top: _kSubItemH / 2 - 4,
@@ -689,8 +1055,8 @@ class _KasySidebarProState extends State<KasySidebarPro> {
689
1055
  height: 8,
690
1056
  decoration: BoxDecoration(
691
1057
  border: Border(
692
- left: BorderSide(color: c.divider, width: 2),
693
- bottom: BorderSide(color: c.divider, width: 2),
1058
+ left: BorderSide(color: c.divider, width: 1.5),
1059
+ bottom: BorderSide(color: c.divider, width: 1.5),
694
1060
  ),
695
1061
  borderRadius: const BorderRadius.only(
696
1062
  bottomLeft: Radius.circular(8),
@@ -698,11 +1064,10 @@ class _KasySidebarProState extends State<KasySidebarPro> {
698
1064
  ),
699
1065
  ),
700
1066
  ),
701
- // Item row
702
1067
  KasyHover(
703
1068
  borderRadius: BorderRadius.circular(_kItemRadius),
704
1069
  hoverColor: c.activeBg,
705
- pressColor: c.activeBg,
1070
+ pressColor: c.textActive,
706
1071
  onTap: () => _activateSubItem(label),
707
1072
  child: Container(
708
1073
  height: _kSubItemH,
@@ -711,7 +1076,7 @@ class _KasySidebarProState extends State<KasySidebarPro> {
711
1076
  vertical: 8,
712
1077
  ),
713
1078
  decoration: BoxDecoration(
714
- color: bg,
1079
+ color: Colors.transparent,
715
1080
  borderRadius: BorderRadius.circular(_kItemRadius),
716
1081
  ),
717
1082
  child: Align(
@@ -732,95 +1097,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
732
1097
  ),
733
1098
  );
734
1099
  }
735
-
736
- // ── Collapse toggle button ────────────────────────────────────────────────
737
-
738
- Widget _buildCollapseButton(BuildContext context, _SidebarColors c) {
739
- final bool anchoredLeft = widget.side == KasySidebarProSide.left;
740
-
741
- // Left sidebar: button at the right edge; chevron ← when open, → when collapsed.
742
- // Right sidebar: button at the left edge; chevron → when open, ← when collapsed.
743
- // The button sits inside the AnimatedContainer bounds so it receives full
744
- // hit tests (Flutter clips hit testing to the parent's bounds).
745
- final double turns = anchoredLeft
746
- ? (_collapsed ? -0.25 : 0.25) // ← open, → collapsed
747
- : (_collapsed ? 0.25 : -0.25); // → open, ← collapsed
748
-
749
- return Positioned(
750
- // The outer container is sidebar_width + halfBtn wide.
751
- // right: 0 → button right edge at outer container right edge.
752
- // Button spans outer_width-28 to outer_width, center = sidebar_width. ✓
753
- // Same logic mirrored for right-anchored sidebar.
754
- left: anchoredLeft ? null : 0,
755
- right: anchoredLeft ? 0 : null,
756
- top: 34,
757
- child: GestureDetector(
758
- onTap: _toggleCollapse,
759
- child: AnimatedContainer(
760
- duration: const Duration(milliseconds: 220),
761
- width: _kCollapseButtonSize,
762
- height: _kCollapseButtonSize,
763
- decoration: BoxDecoration(
764
- color: c.bg,
765
- border: Border.all(color: c.divider),
766
- borderRadius: BorderRadius.circular(8),
767
- boxShadow: [
768
- BoxShadow(
769
- color: const Color(0xFF000000).withValues(alpha: 0.06),
770
- blurRadius: 4,
771
- offset: const Offset(0, 1),
772
- ),
773
- ],
774
- ),
775
- child: Center(
776
- child: AnimatedRotation(
777
- turns: turns,
778
- duration: const Duration(milliseconds: 220),
779
- child: Icon(
780
- KasyIcons.chevronDown,
781
- size: 16,
782
- color: c.textMuted,
783
- ),
784
- ),
785
- ),
786
- ),
787
- ),
788
- );
789
- }
790
- }
791
-
792
- // ─────────────────────────────────────────────────────────────────────────────
793
- // _ProIcon — nav icon with subtle shadow weight when active
794
- // ─────────────────────────────────────────────────────────────────────────────
795
-
796
- /// Renders a nav icon at 20px. When [active], adds a soft shadow using the
797
- /// same [color] to simulate a slightly heavier stroke — consistent with the
798
- /// bolder text weight used on selected items.
799
- class _ProIcon extends StatelessWidget {
800
- const _ProIcon({
801
- required this.icon,
802
- required this.color,
803
- required this.active,
804
- });
805
-
806
- final IconData icon;
807
- final Color color;
808
- final bool active;
809
-
810
- @override
811
- Widget build(BuildContext context) {
812
- return Icon(
813
- icon,
814
- size: 20,
815
- color: color,
816
- shadows: active
817
- ? [
818
- Shadow(color: color.withValues(alpha: 0.55), blurRadius: 0.6),
819
- Shadow(color: color.withValues(alpha: 0.35), blurRadius: 1.4),
820
- ]
821
- : null,
822
- );
823
- }
824
1100
  }
825
1101
 
826
1102
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1069,12 +1345,11 @@ class _TooltipCard extends StatelessWidget {
1069
1345
  @override
1070
1346
  Widget build(BuildContext context) {
1071
1347
  // Use colors.isDark instead of Theme.of(context) — the overlay context
1072
- // does not always inherit the app theme correctly.
1073
- // Dark → bg: colors.divider (#2D2F39), text: rgba(255,255,255,0.8)
1074
- // Light → bg: black, text: white inverted tooltip (Figma spec / industry standard)
1075
- final Color bg = colors.isDark ? colors.divider : Colors.black;
1076
- final Color textColor =
1077
- colors.isDark ? const Color(0xCCFFFFFF) : Colors.white;
1348
+ // does not always inherit the app theme correctly. Colors come from the
1349
+ // global theme: an inverted tooltip in light mode (dark surface / light
1350
+ // text — Figma spec / industry standard), neutral surface in dark mode.
1351
+ final Color bg = colors.isDark ? colors.divider : colors.textActive;
1352
+ final Color textColor = colors.isDark ? colors.textActive : colors.bg;
1078
1353
 
1079
1354
  return Material(
1080
1355
  color: Colors.transparent,
@@ -1128,7 +1403,9 @@ class _TooltipArrowPainter extends CustomPainter {
1128
1403
 
1129
1404
  @override
1130
1405
  void paint(Canvas canvas, Size size) {
1131
- final paint = Paint()..color = color..style = PaintingStyle.fill;
1406
+ final paint = Paint()
1407
+ ..color = color
1408
+ ..style = PaintingStyle.fill;
1132
1409
  final path = Path()
1133
1410
  ..moveTo(size.width, 0)
1134
1411
  ..lineTo(0, size.height / 2)