kasy-cli 1.21.8 → 1.22.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 +86 -38
  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/enable-auth-via-cli.js +14 -6
  31. package/lib/scaffold/backends/firebase/generator.js +5 -5
  32. package/lib/scaffold/backends/firebase/setup-from-scratch.js +69 -45
  33. package/lib/scaffold/backends/firebase/tokens.js +4 -4
  34. package/lib/scaffold/backends/supabase/deploy.js +63 -11
  35. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
  36. package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
  37. package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
  38. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
  39. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
  40. package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
  41. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
  42. package/lib/scaffold/backends/supabase/generator.js +17 -17
  43. package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
  44. package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
  45. package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
  46. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
  47. package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
  48. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
  49. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
  50. 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
  51. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
  52. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
  53. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
  54. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
  55. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  56. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  57. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
  58. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  59. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
  60. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  61. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
  62. package/lib/scaffold/backends/supabase/tokens.js +3 -3
  63. package/lib/scaffold/catalog.js +9 -11
  64. package/lib/scaffold/generate.js +45 -31
  65. package/lib/scaffold/shared/generator-utils.js +188 -81
  66. package/lib/scaffold/shared/sort-imports.js +191 -0
  67. package/lib/scaffold/shared/template-strings.js +3 -3
  68. package/lib/utils/checks.js +2 -2
  69. package/lib/utils/i18n/messages-en.js +50 -35
  70. package/lib/utils/i18n/messages-es.js +50 -35
  71. package/lib/utils/i18n/messages-pt.js +52 -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} +692 -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 +57 -4
  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 +41 -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 +33 -13
  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 +118 -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 +4 -5
  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,12 @@ 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
+ void _navigateTo(int index) {
312
+ setState(() => _activeItemId = '');
313
+ widget.onTapItem!(index);
314
+ }
315
+
217
316
  void _activateItem(String id) {
218
317
  if (id == 'settings') {
219
318
  widget.onSettingsTap?.call();
@@ -222,127 +321,122 @@ class _KasySidebarProState extends State<KasySidebarPro> {
222
321
  }
223
322
  setState(() {
224
323
  _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
324
  if (id != 'income') _activeSubItem = '';
228
325
  });
229
326
  }
230
327
 
231
328
  void _activateSubItem(String label) => setState(() {
232
329
  _activeSubItem = label;
233
- _activeItemId = 'income'; // sub-item belongs to Income → highlight it
330
+ _activeItemId = 'income';
234
331
  });
235
332
 
236
333
  // ── Build ─────────────────────────────────────────────────────────────────
237
334
 
238
335
  @override
239
336
  Widget build(BuildContext context) {
240
- // Recompute effective collapsed state every build so viewport changes
241
- // are picked up automatically without any extra listener.
242
337
  _collapsed = _userChoseCollapsed || _isViewportNarrow(context);
243
338
 
244
339
  final c = _colors;
340
+ final bool anchoredLeft = widget.side == KasySidebarSide.left;
341
+
342
+ // Hairline on the content-facing edge (Figma `separator`).
343
+ final Border edgeBorder = anchoredLeft
344
+ ? Border(right: BorderSide(color: c.border, width: 0.5))
345
+ : Border(left: BorderSide(color: c.border, width: 0.5));
346
+
347
+ // Figma `shadow-surface`: a soft lift in light mode only (dark uses none).
348
+ final List<BoxShadow> shadow = c.isDark
349
+ ? const <BoxShadow>[]
350
+ : const <BoxShadow>[
351
+ BoxShadow(
352
+ color: Color(0x14000000),
353
+ blurRadius: 4,
354
+ offset: Offset(0, 2),
355
+ ),
356
+ BoxShadow(
357
+ color: Color(0x0F000000),
358
+ blurRadius: 2,
359
+ offset: Offset(0, 1),
360
+ ),
361
+ ];
245
362
 
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
363
+ final Widget content = _connected
364
+ ? ValueListenableBuilder<int>(
365
+ valueListenable: widget.currentItem!,
366
+ builder: (_, currentIndex, _) =>
367
+ _buildConnectedContent(context, c, currentIndex),
368
+ )
369
+ : _buildShowcaseContent(context, c);
266
370
 
267
371
  return Material(
268
372
  type: MaterialType.transparency,
269
373
  child: AnimatedContainer(
270
374
  duration: const Duration(milliseconds: 220),
271
375
  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
- ),
376
+ width: _collapsed ? _kWidthCollapsed : _kWidthOpen,
377
+ decoration: BoxDecoration(color: c.bg, boxShadow: shadow),
378
+ foregroundDecoration: BoxDecoration(border: edgeBorder),
379
+ clipBehavior: Clip.hardEdge,
380
+ child: content,
294
381
  ),
295
382
  );
296
383
  }
297
384
 
298
- // ── Scrollable content ────────────────────────────────────────────────────
385
+ // ── Showcase layout (HeroUI Figma 1:1) ──────────────────────────────────────
299
386
 
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.
387
+ Widget _buildShowcaseContent(BuildContext context, _SidebarColors c) {
306
388
  return SizedBox.expand(
307
389
  child: Column(
308
- crossAxisAlignment: CrossAxisAlignment.start,
390
+ crossAxisAlignment: CrossAxisAlignment.stretch,
309
391
  children: [
310
- // ── Scrollable nav area ──────────────────────────────────────────
392
+ // Logo band 68px tall so the divider below aligns with the web
393
+ // header's bottom border (one continuous line across the chrome).
394
+ _buildTopBand(c),
395
+ _buildDivider(c),
396
+ // Nav: workspace selector + segmented tabs + the layers list.
311
397
  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
- ],
398
+ child: Padding(
399
+ padding: const EdgeInsets.fromLTRB(_kPadH, _kDividerGap, _kPadH, 0),
400
+ child: SingleChildScrollView(
401
+ child: Column(
402
+ crossAxisAlignment: CrossAxisAlignment.start,
403
+ children: [
404
+ if (!_collapsed) ...[
405
+ _buildWorkspaceSelector(c),
406
+ const SizedBox(height: _kHeaderGap),
407
+ _buildTabs(c),
408
+ const SizedBox(height: _kNavGap),
409
+ ],
410
+ for (final item in _kShowcaseItems)
411
+ _buildNavItem(context, item, c),
412
+ ],
413
+ ),
333
414
  ),
334
415
  ),
335
416
  ),
336
- // ── Bottom items pinned ──────────────────────────────────────────
417
+ // Pinned ⌘K search row + profile block.
418
+ _buildDivider(c),
337
419
  Padding(
338
420
  padding: const EdgeInsets.fromLTRB(
339
- _kPad, _kItemGap, _kPad, _kPad,
421
+ _kPadH,
422
+ _kFooterGap,
423
+ _kPadH,
424
+ _kPadBottom,
340
425
  ),
341
426
  child: Column(
342
427
  crossAxisAlignment: CrossAxisAlignment.start,
343
- children: _kBottomItems
344
- .map((item) => _buildNavItem(context, item, c))
345
- .toList(),
428
+ children: [
429
+ _buildItemRow(
430
+ c,
431
+ icon: KasyIcons.search,
432
+ label: 'Search',
433
+ isActive: false,
434
+ onTap: () {},
435
+ bottomGap: 0,
436
+ trailing: [_buildKbd(c)],
437
+ ),
438
+ if (widget.showProfile) _buildProfile(c),
439
+ ],
346
440
  ),
347
441
  ),
348
442
  ],
@@ -350,161 +444,459 @@ class _KasySidebarProState extends State<KasySidebarPro> {
350
444
  );
351
445
  }
352
446
 
353
- // ── Profile ───────────────────────────────────────────────────────────────
447
+ // ── Connected layout (real navigation) ──────────────────────────────────────
354
448
 
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
- );
449
+ Widget _buildConnectedContent(
450
+ BuildContext context,
451
+ _SidebarColors c,
452
+ int currentIndex,
453
+ ) {
454
+ final int settingsIndex = widget.routes!.length - 1;
455
+ final nav = context.t.navigation;
456
+ final List<({IconData icon, String label})> meta = [
457
+ (icon: KasyIcons.home, label: nav.home),
458
+ (icon: KasyIcons.help, label: nav.support),
459
+ (icon: KasyIcons.notification, label: nav.notifications),
460
+ ];
461
+ final int mainCount = widget.routes!.length - 1; // exclude settings (last)
366
462
 
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,
463
+ return SizedBox.expand(
464
+ child: Column(
465
+ crossAxisAlignment: CrossAxisAlignment.stretch,
466
+ children: [
467
+ _buildTopBand(c),
468
+ _buildDivider(c),
469
+ Expanded(
470
+ child: Padding(
471
+ padding: const EdgeInsets.fromLTRB(_kPadH, _kDividerGap, _kPadH, 0),
472
+ child: SingleChildScrollView(
473
+ child: Column(
474
+ crossAxisAlignment: CrossAxisAlignment.start,
377
475
  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
- ],
476
+ if (!_collapsed) ...[
477
+ _buildSectionLabel('MAIN', c),
478
+ const SizedBox(height: _kItemGap),
479
+ ],
480
+ // Real, navigable screens.
481
+ for (int i = 0; i < mainCount; i++)
482
+ _buildItemRow(
483
+ c,
484
+ icon: i < meta.length
485
+ ? meta[i].icon
486
+ : (widget.routes![i].icon ?? KasyIcons.note),
487
+ label: i < meta.length
488
+ ? meta[i].label
489
+ : (widget.routes![i].label ?? ''),
490
+ isActive: _activeItemId.isEmpty && currentIndex == i,
491
+ onTap: () => _navigateTo(i),
403
492
  ),
493
+ // Static showcase extras (incl. the Income submenu).
494
+ for (final item in _kMainItems.skip(1))
495
+ _buildNavItem(context, item, c),
496
+ const SizedBox(height: _kDividerGap),
497
+ if (!_collapsed) ...[
498
+ _buildSectionLabel('SETTINGS', c),
499
+ const SizedBox(height: _kItemGap),
500
+ ],
501
+ _buildItemRow(
502
+ c,
503
+ icon: KasyIcons.settings,
504
+ label: nav.settings,
505
+ isActive:
506
+ _activeItemId.isEmpty && currentIndex == settingsIndex,
507
+ onTap: () => _navigateTo(settingsIndex),
404
508
  ),
405
509
  ],
406
510
  ),
511
+ ),
512
+ ),
513
+ ),
514
+ _buildDivider(c),
515
+ const SizedBox(height: _kFooterGap),
516
+ Padding(
517
+ padding: const EdgeInsets.fromLTRB(_kPadH, 0, _kPadH, _kPadBottom),
518
+ child: Column(
519
+ crossAxisAlignment: CrossAxisAlignment.start,
520
+ children: [
521
+ _buildNavItem(context, _kHelpItem, c),
522
+ _buildItemRow(
523
+ c,
524
+ icon: KasyIcons.logout,
525
+ label: nav.logout,
526
+ isActive: false,
527
+ isLogout: true,
528
+ bottomGap: 0,
529
+ onTap: () => widget.onLogout?.call(),
530
+ ),
531
+ if (widget.showProfile) _buildProfile(c),
532
+ ],
533
+ ),
534
+ ),
535
+ ],
536
+ ),
537
+ );
538
+ }
539
+
540
+ // ── Top band (logo + panel toggle) ──────────────────────────────────────────
541
+
542
+ /// The brand band at the top of the rail. Fixed to [_kTopBandHeight] (= web
543
+ /// header height) so the divider underneath lines up with the header's bottom
544
+ /// border. Content is vertically centred, mirroring the header's toolbar row.
545
+ Widget _buildTopBand(_SidebarColors c) {
546
+ return Padding(
547
+ padding: const EdgeInsets.symmetric(horizontal: _kPadH),
548
+ child: SizedBox(
549
+ height: _kTopBandHeight,
550
+ child: _collapsed
551
+ ? Center(child: _buildToggleButton(c))
552
+ : Row(
553
+ children: [
554
+ // Brand wordmark — same artwork as the splash screen.
555
+ Image.asset(
556
+ c.isDark
557
+ ? 'assets/images/logo_wordmark_dark.png'
558
+ : 'assets/images/logo_wordmark_light.png',
559
+ height: 32,
560
+ fit: BoxFit.contain,
561
+ ),
562
+ const Spacer(),
563
+ _buildToggleButton(c),
564
+ ],
565
+ ),
566
+ ),
567
+ );
568
+ }
569
+
570
+ Widget _buildToggleButton(_SidebarColors c) {
571
+ return KasyHover(
572
+ borderRadius: BorderRadius.circular(_kToggleSize / 2),
573
+ hoverColor: c.activeBg,
574
+ pressColor: c.textActive,
575
+ onTap: _toggleCollapse,
576
+ child: Container(
577
+ width: _kToggleSize,
578
+ height: _kToggleSize,
579
+ alignment: Alignment.center,
580
+ decoration: BoxDecoration(
581
+ borderRadius: BorderRadius.circular(_kToggleSize / 2),
582
+ ),
583
+ child: Icon(KasyIcons.panelLeft, size: _kIconSize, color: c.textMuted),
584
+ ),
585
+ );
586
+ }
587
+
588
+ // ── Workspace selector ──────────────────────────────────────────────────────
589
+
590
+ Widget _buildWorkspaceSelector(_SidebarColors c) {
591
+ return Column(
592
+ crossAxisAlignment: CrossAxisAlignment.start,
593
+ mainAxisSize: MainAxisSize.min,
594
+ children: [
595
+ Row(
596
+ mainAxisSize: MainAxisSize.min,
597
+ children: [
598
+ Flexible(
599
+ child: Text(
600
+ '3D Dog Character',
601
+ maxLines: 1,
602
+ overflow: TextOverflow.ellipsis,
603
+ style: TextStyle(
604
+ fontSize: 14,
605
+ height: 20 / 14,
606
+ fontWeight: FontWeight.w500,
607
+ color: c.textActive,
608
+ ),
609
+ ),
610
+ ),
611
+ const SizedBox(width: 16),
612
+ Icon(KasyIcons.chevronDown, size: _kIconSize, color: c.textMuted),
613
+ ],
614
+ ),
615
+ const SizedBox(height: 4),
616
+ Text(
617
+ '3D Design Project',
618
+ maxLines: 1,
619
+ overflow: TextOverflow.ellipsis,
620
+ style: TextStyle(
621
+ fontSize: 12,
622
+ height: 16 / 12,
623
+ fontWeight: FontWeight.w400,
624
+ color: c.textMuted,
625
+ ),
407
626
  ),
408
627
  ],
409
628
  );
410
629
  }
411
630
 
412
- // ── Divider ───────────────────────────────────────────────────────────────
631
+ // ── Segmented tabs (Layers / Assets) ────────────────────────────────────────
413
632
 
414
- Widget _buildDivider(_SidebarColors c) {
633
+ Widget _buildTabs(_SidebarColors c) {
415
634
  return Container(
416
- height: 2,
635
+ width: double.infinity,
636
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
417
637
  decoration: BoxDecoration(
418
- color: c.divider,
419
- borderRadius: BorderRadius.circular(2),
638
+ color: c.activeBg,
639
+ borderRadius: BorderRadius.circular(24),
640
+ ),
641
+ child: Row(
642
+ children: [
643
+ _buildTab(c, 'Layers', 0),
644
+ const SizedBox(width: 2),
645
+ _buildTab(c, 'Assets', 1),
646
+ ],
420
647
  ),
421
648
  );
422
649
  }
423
650
 
424
- // ── Section label ─────────────────────────────────────────────────────────
651
+ Widget _buildTab(_SidebarColors c, String label, int index) {
652
+ final bool selected = _showcaseTab == index;
653
+ return Expanded(
654
+ child: MouseRegion(
655
+ cursor: SystemMouseCursors.click,
656
+ child: GestureDetector(
657
+ behavior: HitTestBehavior.opaque,
658
+ onTap: () => setState(() => _showcaseTab = index),
659
+ child: AnimatedContainer(
660
+ duration: const Duration(milliseconds: 180),
661
+ curve: Curves.easeOut,
662
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
663
+ alignment: Alignment.center,
664
+ decoration: BoxDecoration(
665
+ color: selected ? c.segmentThumb : Colors.transparent,
666
+ borderRadius: BorderRadius.circular(24),
667
+ boxShadow: selected
668
+ ? const [
669
+ BoxShadow(
670
+ color: Color(0x0F000000),
671
+ blurRadius: 8,
672
+ offset: Offset(0, 2),
673
+ ),
674
+ ]
675
+ : null,
676
+ ),
677
+ child: Text(
678
+ label,
679
+ style: TextStyle(
680
+ fontSize: 14,
681
+ height: 20 / 14,
682
+ fontWeight: FontWeight.w500,
683
+ color: selected ? c.textActive : c.textMuted,
684
+ ),
685
+ ),
686
+ ),
687
+ ),
688
+ ),
689
+ );
690
+ }
425
691
 
426
- Widget _buildSectionLabel(String label, _SidebarColors c) {
692
+ // ── ⌘K keyboard chip ─────────────────────────────────────────────────────────
693
+
694
+ Widget _buildKbd(_SidebarColors c) {
427
695
  final TextStyle style = TextStyle(
428
- fontSize: 10,
696
+ fontSize: 14,
697
+ height: 20 / 14,
429
698
  fontWeight: FontWeight.w500,
430
699
  color: c.textMuted,
431
- letterSpacing: 0.4,
432
700
  );
701
+ return Container(
702
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
703
+ decoration: BoxDecoration(
704
+ color: c.activeBg,
705
+ borderRadius: BorderRadius.circular(8),
706
+ ),
707
+ child: Row(
708
+ mainAxisSize: MainAxisSize.min,
709
+ children: [
710
+ Text('⌘', style: style),
711
+ const SizedBox(width: 2),
712
+ Text('K', style: style),
713
+ ],
714
+ ),
715
+ );
716
+ }
717
+
718
+ // ── Profile block (bottom) ──────────────────────────────────────────────────
719
+
720
+ /// Account row pinned at the bottom: the [KasySidebar.profileAvatar] (the
721
+ /// signed-in user's photo) when provided, otherwise a gradient-fill avatar +
722
+ /// name + email. Collapses to just the avatar on the narrow rail.
723
+ Widget _buildProfile(_SidebarColors c) {
724
+ final Widget avatar =
725
+ widget.profileAvatar ??
726
+ KasyAvatar(diameter: 36, backgroundGradient: widget.profileGradient);
433
727
 
434
728
  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
- ),
729
+ return Padding(
730
+ padding: const EdgeInsets.only(top: _kItemGap),
731
+ child: Center(child: avatar),
444
732
  );
445
733
  }
446
734
 
735
+ return Padding(
736
+ padding: const EdgeInsets.only(top: _kItemGap),
737
+ child: KasyHover(
738
+ borderRadius: BorderRadius.circular(_kItemRadius),
739
+ hoverColor: c.activeBg,
740
+ pressColor: c.textActive,
741
+ onTap: widget.onProfileTap ?? () {},
742
+ child: Padding(
743
+ padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
744
+ child: Row(
745
+ children: [
746
+ avatar,
747
+ const SizedBox(width: _kIconGap),
748
+ Expanded(
749
+ child: Column(
750
+ crossAxisAlignment: CrossAxisAlignment.start,
751
+ mainAxisSize: MainAxisSize.min,
752
+ children: [
753
+ Text(
754
+ widget.profileName,
755
+ maxLines: 1,
756
+ overflow: TextOverflow.ellipsis,
757
+ style: TextStyle(
758
+ fontSize: 14,
759
+ height: 20 / 14,
760
+ fontWeight: FontWeight.w500,
761
+ color: c.textActive,
762
+ ),
763
+ ),
764
+ Text(
765
+ widget.profileEmail,
766
+ maxLines: 1,
767
+ overflow: TextOverflow.ellipsis,
768
+ style: TextStyle(
769
+ fontSize: 12,
770
+ height: 16 / 12,
771
+ fontWeight: FontWeight.w500,
772
+ color: c.textMuted,
773
+ ),
774
+ ),
775
+ ],
776
+ ),
777
+ ),
778
+ ],
779
+ ),
780
+ ),
781
+ ),
782
+ );
783
+ }
784
+
785
+ // ── Divider ───────────────────────────────────────────────────────────────
786
+
787
+ // 0.5px hairline — same width + colour as the vertical edge line and the web
788
+ // header's bottom border, so all the chrome lines read identically.
789
+ Widget _buildDivider(_SidebarColors c) =>
790
+ Container(height: 0.5, color: c.divider);
791
+
792
+ // ── Section label ─────────────────────────────────────────────────────────
793
+
794
+ Widget _buildSectionLabel(String label, _SidebarColors c) {
447
795
  return Padding(
448
796
  padding: const EdgeInsets.only(left: _kItemHPad),
449
- child: Text(label, style: style),
797
+ child: Text(
798
+ label,
799
+ style: TextStyle(
800
+ fontSize: 11,
801
+ fontWeight: FontWeight.w600,
802
+ color: c.textMuted,
803
+ letterSpacing: 0.6,
804
+ ),
805
+ ),
450
806
  );
451
807
  }
452
808
 
453
- // ── Nav item ──────────────────────────────────────────────────────────────
809
+ // ── Nav item dispatch ───────────────────────────────────────────────────────
454
810
 
455
- Widget _buildNavItem(
456
- BuildContext context,
457
- _NavItem item,
458
- _SidebarColors c,
459
- ) {
811
+ Widget _buildNavItem(BuildContext context, _NavItem item, _SidebarColors c) {
460
812
  if (item.hasSubmenu) {
461
813
  return _buildDropdownItem(context, item, c);
462
814
  }
463
-
464
815
  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
816
+ return _buildItemRow(
817
+ c,
818
+ icon: item.icon,
819
+ label: item.label,
820
+ isActive: isActive,
821
+ onTap: () => _activateItem(item.id),
822
+ trailing: item.trailingControls
823
+ ? [
824
+ Icon(KasyIcons.security, size: _kIconSize, color: c.textMuted),
825
+ const SizedBox(width: 12),
826
+ Icon(KasyIcons.eye, size: _kIconSize, color: c.textMuted),
827
+ ]
828
+ : const [],
829
+ );
830
+ }
831
+
832
+ // ── Generic row (expanded) / icon+tooltip (collapsed) ────────────────────────
833
+
834
+ Widget _buildItemRow(
835
+ _SidebarColors c, {
836
+ required IconData icon,
837
+ required String label,
838
+ required bool isActive,
839
+ required VoidCallback onTap,
840
+ bool isLogout = false,
841
+ List<Widget> trailing = const [],
842
+ double bottomGap = _kItemGap,
843
+ }) {
844
+ final Color fill = isActive ? c.activeBg : Colors.transparent;
845
+ final Color iconColor = isLogout
468
846
  ? c.logout
469
847
  : (isActive ? c.textActive : c.textMuted);
470
- final Color textColor = item.isLogout ? c.logout : iconColor;
848
+ final Color labelColor = isLogout ? c.logout : c.textActive;
471
849
 
472
- // Collapsed: icon only, optionally with hover popup for sub-items
473
850
  if (_collapsed) {
474
- return _buildCollapsedIcon(context, item, bg, iconColor, c);
851
+ return Padding(
852
+ padding: EdgeInsets.only(bottom: bottomGap),
853
+ child: _ProTooltipIcon(
854
+ icon: icon,
855
+ label: label,
856
+ iconBg: fill,
857
+ iconColor: iconColor,
858
+ activeBg: c.activeBg,
859
+ colors: c,
860
+ onTap: onTap,
861
+ ),
862
+ );
475
863
  }
476
864
 
477
865
  return Padding(
478
- padding: const EdgeInsets.only(bottom: _kItemGap),
866
+ padding: EdgeInsets.only(bottom: bottomGap),
479
867
  child: KasyHover(
480
868
  borderRadius: BorderRadius.circular(_kItemRadius),
481
869
  hoverColor: c.activeBg,
482
- pressColor: c.activeBg,
483
- onTap: () => _activateItem(item.id),
870
+ pressColor: c.textActive,
871
+ onTap: onTap,
484
872
  child: Container(
873
+ constraints: const BoxConstraints(minHeight: _kItemMinH),
485
874
  padding: const EdgeInsets.symmetric(
486
875
  horizontal: _kItemHPad,
487
876
  vertical: _kItemVPad,
488
877
  ),
489
878
  decoration: BoxDecoration(
490
- color: bg,
879
+ color: fill,
491
880
  borderRadius: BorderRadius.circular(_kItemRadius),
492
881
  ),
493
882
  child: Row(
494
883
  children: [
495
- _ProIcon(icon: item.icon, color: iconColor, active: isActive),
496
- const SizedBox(width: 12),
884
+ Icon(icon, size: _kIconSize, color: iconColor),
885
+ const SizedBox(width: _kIconGap),
497
886
  Expanded(
498
887
  child: Text(
499
- item.label,
888
+ label,
889
+ maxLines: 1,
890
+ overflow: TextOverflow.ellipsis,
500
891
  style: TextStyle(
501
892
  fontSize: 14,
502
- fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
503
- color: textColor,
504
- letterSpacing: -0.28,
893
+ height: 20 / 14,
894
+ fontWeight: FontWeight.w500,
895
+ color: labelColor,
505
896
  ),
506
897
  ),
507
898
  ),
899
+ ...trailing,
508
900
  ],
509
901
  ),
510
902
  ),
@@ -512,30 +904,7 @@ class _KasySidebarProState extends State<KasySidebarPro> {
512
904
  );
513
905
  }
514
906
 
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) ────────────────────────────────────────────────
907
+ // ── Dropdown item (submenu — used by connected Income) ───────────────────────
539
908
 
540
909
  Widget _buildDropdownItem(
541
910
  BuildContext context,
@@ -547,7 +916,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
547
916
  final Color iconColor = isActive ? c.textActive : c.textMuted;
548
917
 
549
918
  if (_collapsed) {
550
- // In collapsed mode: icon that shows a floating popup on hover
551
919
  return Padding(
552
920
  padding: const EdgeInsets.only(bottom: _kItemGap),
553
921
  child: _ProHoverPopupIcon(
@@ -565,53 +933,49 @@ class _KasySidebarProState extends State<KasySidebarPro> {
565
933
  return Column(
566
934
  crossAxisAlignment: CrossAxisAlignment.start,
567
935
  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
936
  KasyHover(
937
+ borderRadius: BorderRadius.circular(_kItemRadius),
938
+ hoverColor: c.activeBg,
939
+ pressColor: c.textActive,
940
+ onTap: () => _activateItem(item.id),
941
+ child: Container(
942
+ constraints: const BoxConstraints(minHeight: _kItemMinH),
943
+ padding: const EdgeInsets.symmetric(
944
+ horizontal: _kItemHPad,
945
+ vertical: _kItemVPad,
946
+ ),
947
+ decoration: BoxDecoration(
948
+ color: bg,
571
949
  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
- ),
950
+ ),
951
+ child: Row(
952
+ children: [
953
+ Icon(item.icon, size: _kIconSize, color: iconColor),
954
+ const SizedBox(width: _kIconGap),
955
+ Expanded(
956
+ child: Text(
957
+ item.label,
958
+ style: TextStyle(
959
+ fontSize: 14,
960
+ height: 20 / 14,
961
+ fontWeight: FontWeight.w500,
962
+ color: c.textActive,
609
963
  ),
610
- ],
964
+ ),
611
965
  ),
612
- ),
966
+ AnimatedRotation(
967
+ turns: _incomeExpanded ? 0.5 : 0,
968
+ duration: const Duration(milliseconds: 200),
969
+ child: Icon(
970
+ KasyIcons.chevronDown,
971
+ size: _kIconSize,
972
+ color: iconColor,
973
+ ),
974
+ ),
975
+ ],
976
+ ),
977
+ ),
613
978
  ),
614
- // Expandable sub-items tree
615
979
  AnimatedCrossFade(
616
980
  duration: const Duration(milliseconds: 200),
617
981
  crossFadeState: _incomeExpanded
@@ -628,9 +992,7 @@ class _KasySidebarProState extends State<KasySidebarPro> {
628
992
  // ── Sub-items tree ────────────────────────────────────────────────────────
629
993
 
630
994
  Widget _buildSubItemsTree(_SidebarColors c) {
631
- final subItems = _kMainItems
632
- .firstWhere((i) => i.id == 'income')
633
- .subItems;
995
+ final subItems = _kMainItems.firstWhere((i) => i.id == 'income').subItems;
634
996
  final double lineH = _treeLineHeight(subItems.length);
635
997
 
636
998
  return Padding(
@@ -640,12 +1002,11 @@ class _KasySidebarProState extends State<KasySidebarPro> {
640
1002
  child: Stack(
641
1003
  clipBehavior: Clip.none,
642
1004
  children: [
643
- // Vertical tree line
644
1005
  Positioned(
645
1006
  left: -_kTreeConnectorW,
646
1007
  top: 0,
647
1008
  child: Container(
648
- width: 2,
1009
+ width: 1.5,
649
1010
  height: lineH,
650
1011
  decoration: BoxDecoration(
651
1012
  color: c.divider,
@@ -653,7 +1014,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
653
1014
  ),
654
1015
  ),
655
1016
  ),
656
- // Sub-items
657
1017
  Column(
658
1018
  children: subItems.asMap().entries.map((entry) {
659
1019
  final i = entry.key;
@@ -670,9 +1030,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
670
1030
 
671
1031
  Widget _buildSubItem(String label, bool isLast, _SidebarColors c) {
672
1032
  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
1033
  final Color textColor = isActive ? c.textActive : c.textMuted;
677
1034
 
678
1035
  return Padding(
@@ -680,7 +1037,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
680
1037
  child: Stack(
681
1038
  clipBehavior: Clip.none,
682
1039
  children: [
683
- // L-connector: bottom-left rounded border
684
1040
  Positioned(
685
1041
  left: -_kTreeConnectorW,
686
1042
  top: _kSubItemH / 2 - 4,
@@ -689,8 +1045,8 @@ class _KasySidebarProState extends State<KasySidebarPro> {
689
1045
  height: 8,
690
1046
  decoration: BoxDecoration(
691
1047
  border: Border(
692
- left: BorderSide(color: c.divider, width: 2),
693
- bottom: BorderSide(color: c.divider, width: 2),
1048
+ left: BorderSide(color: c.divider, width: 1.5),
1049
+ bottom: BorderSide(color: c.divider, width: 1.5),
694
1050
  ),
695
1051
  borderRadius: const BorderRadius.only(
696
1052
  bottomLeft: Radius.circular(8),
@@ -698,11 +1054,10 @@ class _KasySidebarProState extends State<KasySidebarPro> {
698
1054
  ),
699
1055
  ),
700
1056
  ),
701
- // Item row
702
1057
  KasyHover(
703
1058
  borderRadius: BorderRadius.circular(_kItemRadius),
704
1059
  hoverColor: c.activeBg,
705
- pressColor: c.activeBg,
1060
+ pressColor: c.textActive,
706
1061
  onTap: () => _activateSubItem(label),
707
1062
  child: Container(
708
1063
  height: _kSubItemH,
@@ -711,7 +1066,7 @@ class _KasySidebarProState extends State<KasySidebarPro> {
711
1066
  vertical: 8,
712
1067
  ),
713
1068
  decoration: BoxDecoration(
714
- color: bg,
1069
+ color: Colors.transparent,
715
1070
  borderRadius: BorderRadius.circular(_kItemRadius),
716
1071
  ),
717
1072
  child: Align(
@@ -732,95 +1087,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
732
1087
  ),
733
1088
  );
734
1089
  }
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
1090
  }
825
1091
 
826
1092
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1069,12 +1335,11 @@ class _TooltipCard extends StatelessWidget {
1069
1335
  @override
1070
1336
  Widget build(BuildContext context) {
1071
1337
  // 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;
1338
+ // does not always inherit the app theme correctly. Colors come from the
1339
+ // global theme: an inverted tooltip in light mode (dark surface / light
1340
+ // text — Figma spec / industry standard), neutral surface in dark mode.
1341
+ final Color bg = colors.isDark ? colors.divider : colors.textActive;
1342
+ final Color textColor = colors.isDark ? colors.textActive : colors.bg;
1078
1343
 
1079
1344
  return Material(
1080
1345
  color: Colors.transparent,
@@ -1128,7 +1393,9 @@ class _TooltipArrowPainter extends CustomPainter {
1128
1393
 
1129
1394
  @override
1130
1395
  void paint(Canvas canvas, Size size) {
1131
- final paint = Paint()..color = color..style = PaintingStyle.fill;
1396
+ final paint = Paint()
1397
+ ..color = color
1398
+ ..style = PaintingStyle.fill;
1132
1399
  final path = Path()
1133
1400
  ..moveTo(size.width, 0)
1134
1401
  ..lineTo(0, size.height / 2)