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.
- package/lib/commands/add.js +93 -80
- package/lib/commands/configure.js +100 -32
- package/lib/commands/doctor.js +28 -2
- package/lib/commands/new.js +80 -37
- package/lib/commands/notifications.js +1 -1
- package/lib/commands/remove.js +43 -15
- package/lib/commands/run.js +2 -2
- package/lib/commands/update.js +2 -2
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/api/generator.js +14 -14
- package/lib/scaffold/backends/api/patch/README.md +83 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
- package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
- 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
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/firebase/deploy.js +87 -13
- package/lib/scaffold/backends/firebase/generator.js +5 -5
- package/lib/scaffold/backends/firebase/tokens.js +4 -4
- package/lib/scaffold/backends/supabase/deploy.js +63 -11
- package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
- package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
- package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
- package/lib/scaffold/backends/supabase/generator.js +17 -17
- package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
- package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
- 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
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/supabase/tokens.js +3 -3
- package/lib/scaffold/catalog.js +9 -11
- package/lib/scaffold/generate.js +45 -31
- package/lib/scaffold/shared/generator-utils.js +188 -81
- package/lib/scaffold/shared/sort-imports.js +191 -0
- package/lib/scaffold/shared/template-strings.js +3 -3
- package/lib/utils/checks.js +22 -6
- package/lib/utils/env-tools.js +7 -0
- package/lib/utils/flutter-install.js +114 -0
- package/lib/utils/i18n/messages-en.js +52 -35
- package/lib/utils/i18n/messages-es.js +52 -35
- package/lib/utils/i18n/messages-pt.js +54 -37
- package/lib/utils/updates.js +15 -15
- package/package.json +1 -1
- package/templates/firebase/.env.example +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
- package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
- package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
- package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
- package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
- package/templates/firebase/firestore.rules +24 -5
- package/templates/firebase/functions/package-lock.json +22 -1
- package/templates/firebase/functions/package.json +2 -1
- package/templates/firebase/functions/src/admin/functions.ts +113 -0
- package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
- package/templates/firebase/functions/src/index.ts +8 -2
- package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
- package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
- package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
- package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
- package/templates/firebase/lib/components/components.dart +4 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
- package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
- package/templates/firebase/lib/components/kasy_button.dart +23 -99
- package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
- package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
- package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +702 -425
- package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
- package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
- package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
- package/templates/firebase/lib/components/kasy_toast.dart +2 -2
- package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
- package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +136 -23
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +54 -0
- package/templates/firebase/lib/core/config/app_env.dart +5 -11
- package/templates/firebase/lib/core/config/features.dart +5 -4
- package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
- package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
- package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
- package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
- package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
- package/templates/firebase/lib/core/data/models/user.dart +11 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
- package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
- package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
- package/templates/firebase/lib/core/states/logout_action.dart +25 -0
- package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
- package/templates/firebase/lib/core/theme/colors.dart +488 -188
- package/templates/firebase/lib/core/theme/radius.dart +22 -11
- package/templates/firebase/lib/core/theme/shadows.dart +66 -0
- package/templates/firebase/lib/core/theme/texts.dart +75 -41
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
- package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
- package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +53 -14
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
- package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
- package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
- package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
- package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +128 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
- package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
- package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
- package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
- package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
- package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
- package/templates/firebase/lib/features/home/home_feed.dart +289 -0
- package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
- package/templates/firebase/lib/features/home/home_page.dart +11 -250
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
- package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
- package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
- package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
- package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
- package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
- package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
- package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
- package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
- package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
- package/templates/firebase/lib/i18n/en.i18n.json +171 -46
- package/templates/firebase/lib/i18n/es.i18n.json +175 -50
- package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
- package/templates/firebase/lib/main.dart +6 -3
- package/templates/firebase/lib/router.dart +15 -23
- package/templates/firebase/pubspec.yaml +5 -6
- package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
- package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
- package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
- package/templates/firebase/test/test_utils.dart +6 -6
- package/templates/firebase/web/index.html +5 -2
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
- package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
- package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
- package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
- package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
- package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
- package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
- package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
- package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
- /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
- /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/
|
|
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 —
|
|
13
|
+
// Design tokens — HeroUI Figma Kit (node 4678:41088 light / 21964:53880 dark)
|
|
9
14
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
const double
|
|
13
|
-
const double
|
|
14
|
-
const double
|
|
15
|
-
const double
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
71
|
-
///
|
|
72
|
-
///
|
|
73
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
127
|
-
id: '
|
|
128
|
-
icon: KasyIcons.
|
|
129
|
-
label: '
|
|
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
|
-
//
|
|
186
|
+
// KasySidebar — public widget
|
|
144
187
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
145
188
|
|
|
146
189
|
/// Which screen edge the sidebar is anchored to.
|
|
147
190
|
///
|
|
148
|
-
/// Controls which
|
|
149
|
-
|
|
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
|
|
153
|
-
///
|
|
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
|
-
|
|
157
|
-
|
|
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 =
|
|
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
|
-
|
|
173
|
-
///
|
|
174
|
-
|
|
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<
|
|
262
|
+
State<KasySidebar> createState() => _KasySidebarState();
|
|
178
263
|
}
|
|
179
264
|
|
|
180
|
-
class
|
|
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
|
-
|
|
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
|
-
|
|
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';
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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:
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
// ──
|
|
392
|
+
// ── Showcase layout (HeroUI Figma 1:1) ──────────────────────────────────────
|
|
299
393
|
|
|
300
|
-
Widget
|
|
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.
|
|
397
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
309
398
|
children: [
|
|
310
|
-
//
|
|
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:
|
|
313
|
-
padding: const EdgeInsets.fromLTRB(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
//
|
|
424
|
+
// Pinned ⌘K search row + profile block.
|
|
425
|
+
_buildDivider(c),
|
|
337
426
|
Padding(
|
|
338
427
|
padding: const EdgeInsets.fromLTRB(
|
|
339
|
-
|
|
428
|
+
_kPadH,
|
|
429
|
+
_kFooterGap,
|
|
430
|
+
_kPadH,
|
|
431
|
+
_kPadBottom,
|
|
340
432
|
),
|
|
341
433
|
child: Column(
|
|
342
434
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
343
|
-
children:
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
// ──
|
|
454
|
+
// ── Connected layout (real navigation) ──────────────────────────────────────
|
|
354
455
|
|
|
355
|
-
Widget
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
),
|
|
364
|
-
|
|
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
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
:
|
|
376
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
),
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
// ──
|
|
638
|
+
// ── Segmented tabs (Layers / Assets) ────────────────────────────────────────
|
|
413
639
|
|
|
414
|
-
Widget
|
|
640
|
+
Widget _buildTabs(_SidebarColors c) {
|
|
415
641
|
return Container(
|
|
416
|
-
|
|
642
|
+
width: double.infinity,
|
|
643
|
+
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
417
644
|
decoration: BoxDecoration(
|
|
418
|
-
color: c.
|
|
419
|
-
borderRadius: BorderRadius.circular(
|
|
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
|
-
|
|
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
|
-
|
|
699
|
+
// ── ⌘K keyboard chip ─────────────────────────────────────────────────────────
|
|
700
|
+
|
|
701
|
+
Widget _buildKbd(_SidebarColors c) {
|
|
427
702
|
final TextStyle style = TextStyle(
|
|
428
|
-
fontSize:
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
child:
|
|
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(
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|
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
|
|
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:
|
|
874
|
+
padding: EdgeInsets.only(bottom: bottomGap),
|
|
479
875
|
child: KasyHover(
|
|
480
876
|
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
481
877
|
hoverColor: c.activeBg,
|
|
482
|
-
pressColor: c.
|
|
483
|
-
|
|
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:
|
|
888
|
+
color: fill,
|
|
491
889
|
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
492
890
|
),
|
|
493
891
|
child: Row(
|
|
494
892
|
children: [
|
|
495
|
-
|
|
496
|
-
const SizedBox(width:
|
|
893
|
+
Icon(icon, size: _kIconSize, color: iconColor),
|
|
894
|
+
const SizedBox(width: _kIconGap),
|
|
497
895
|
Expanded(
|
|
498
896
|
child: Text(
|
|
499
|
-
|
|
897
|
+
label,
|
|
898
|
+
maxLines: 1,
|
|
899
|
+
overflow: TextOverflow.ellipsis,
|
|
500
900
|
style: TextStyle(
|
|
501
901
|
fontSize: 14,
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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:
|
|
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:
|
|
693
|
-
bottom: BorderSide(color: c.divider, width:
|
|
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.
|
|
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:
|
|
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
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
final Color bg = colors.isDark ? colors.divider :
|
|
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()
|
|
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)
|