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.
- 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 +86 -38
- 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/enable-auth-via-cli.js +14 -6
- package/lib/scaffold/backends/firebase/generator.js +5 -5
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +69 -45
- 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 +2 -2
- package/lib/utils/i18n/messages-en.js +50 -35
- package/lib/utils/i18n/messages-es.js +50 -35
- package/lib/utils/i18n/messages-pt.js +52 -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} +692 -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 +57 -4
- 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 +41 -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 +33 -13
- 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 +118 -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 +4 -5
- 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,2021 +0,0 @@
|
|
|
1
|
-
import 'dart:math' show min;
|
|
2
|
-
|
|
3
|
-
import 'package:bart/bart/bart_model.dart';
|
|
4
|
-
import 'package:bart/bart/widgets/side_bar/custom_sidebar.dart';
|
|
5
|
-
import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform;
|
|
6
|
-
import 'package:flutter/material.dart';
|
|
7
|
-
import 'package:flutter/services.dart';
|
|
8
|
-
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
9
|
-
import 'package:kasy_kit/components/components.dart';
|
|
10
|
-
import 'package:kasy_kit/core/data/models/user.dart';
|
|
11
|
-
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
12
|
-
import 'package:kasy_kit/core/theme/theme.dart';
|
|
13
|
-
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
14
|
-
import 'package:kasy_kit/features/notifications/providers/notifications_provider.dart';
|
|
15
|
-
import 'package:kasy_kit/features/notifications/repositories/notifications_repository.dart';
|
|
16
|
-
|
|
17
|
-
const double _kWidth = 260.0;
|
|
18
|
-
const double _kItemHeight = 44.0;
|
|
19
|
-
const double _kItemRadius = 8.0;
|
|
20
|
-
|
|
21
|
-
/// Entry point — pass to [CustomSideBarOptions.sideBarBuilder].
|
|
22
|
-
Widget kasySidebarBuilder(
|
|
23
|
-
List<BartMenuRoute> routes,
|
|
24
|
-
OnTapItem onTapItem,
|
|
25
|
-
ValueNotifier<int> currentItem,
|
|
26
|
-
) {
|
|
27
|
-
return _KasySidebarRoot(
|
|
28
|
-
routes: routes,
|
|
29
|
-
onTapItem: onTapItem,
|
|
30
|
-
currentItem: currentItem,
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
-
// Root — reacts to Bart's tab-index changes without recreating State
|
|
36
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
class _KasySidebarRoot extends StatelessWidget {
|
|
39
|
-
final List<BartMenuRoute> routes;
|
|
40
|
-
final OnTapItem onTapItem;
|
|
41
|
-
final ValueNotifier<int> currentItem;
|
|
42
|
-
|
|
43
|
-
const _KasySidebarRoot({
|
|
44
|
-
required this.routes,
|
|
45
|
-
required this.onTapItem,
|
|
46
|
-
required this.currentItem,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
@override
|
|
50
|
-
Widget build(BuildContext context) {
|
|
51
|
-
return ValueListenableBuilder<int>(
|
|
52
|
-
valueListenable: currentItem,
|
|
53
|
-
builder: (_, currentIndex, child) => _KasySidebar(
|
|
54
|
-
routes: routes,
|
|
55
|
-
currentIndex: currentIndex,
|
|
56
|
-
onTapItem: onTapItem,
|
|
57
|
-
),
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
-
// Sidebar — stateful: holds project expansion + keyboard shortcut ⌘K / Ctrl+K
|
|
64
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
65
|
-
|
|
66
|
-
class _KasySidebar extends StatefulWidget {
|
|
67
|
-
final List<BartMenuRoute> routes;
|
|
68
|
-
final int currentIndex;
|
|
69
|
-
final OnTapItem onTapItem;
|
|
70
|
-
|
|
71
|
-
const _KasySidebar({
|
|
72
|
-
required this.routes,
|
|
73
|
-
required this.currentIndex,
|
|
74
|
-
required this.onTapItem,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
@override
|
|
78
|
-
State<_KasySidebar> createState() => _KasySidebarState();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
class _KasySidebarState extends State<_KasySidebar> {
|
|
82
|
-
int? _expandedProject;
|
|
83
|
-
bool _searchOpen = false;
|
|
84
|
-
|
|
85
|
-
static const _projects = [
|
|
86
|
-
(color: Color(0xFF34C759), name: 'Kasy Studio'),
|
|
87
|
-
(color: Color(0xFF1E88E5), name: 'Design System'),
|
|
88
|
-
(color: Color(0xFFF57F17), name: 'Components'),
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
static const _placeholders = [
|
|
92
|
-
(icon: KasyIcons.flash, label: 'Activity'),
|
|
93
|
-
(icon: KasyIcons.note, label: 'Tasks'),
|
|
94
|
-
(icon: KasyIcons.book, label: 'Reports'),
|
|
95
|
-
(icon: KasyIcons.person, label: 'Team'),
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
// ── Keyboard shortcut ────────────────────────────────────────────────────
|
|
99
|
-
|
|
100
|
-
@override
|
|
101
|
-
void initState() {
|
|
102
|
-
super.initState();
|
|
103
|
-
HardwareKeyboard.instance.addHandler(_onKeyEvent);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
@override
|
|
107
|
-
void dispose() {
|
|
108
|
-
HardwareKeyboard.instance.removeHandler(_onKeyEvent);
|
|
109
|
-
super.dispose();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
bool _onKeyEvent(KeyEvent event) {
|
|
113
|
-
if (!mounted || event is! KeyDownEvent) return false;
|
|
114
|
-
if (event.logicalKey != LogicalKeyboardKey.keyK) return false;
|
|
115
|
-
final bool trigger = HardwareKeyboard.instance.isMetaPressed ||
|
|
116
|
-
HardwareKeyboard.instance.isControlPressed;
|
|
117
|
-
if (trigger) {
|
|
118
|
-
_openSearch();
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ── Search modal ─────────────────────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
void _openSearch() {
|
|
127
|
-
if (_searchOpen || !mounted) return;
|
|
128
|
-
setState(() => _searchOpen = true);
|
|
129
|
-
showGeneralDialog<void>(
|
|
130
|
-
context: context,
|
|
131
|
-
barrierDismissible: true,
|
|
132
|
-
barrierLabel: 'Dismiss',
|
|
133
|
-
barrierColor: Colors.black.withValues(alpha: 0.32),
|
|
134
|
-
transitionDuration: const Duration(milliseconds: 220),
|
|
135
|
-
pageBuilder: (ctx, anim, secAnim) => Align(
|
|
136
|
-
alignment: const Alignment(0, -0.18),
|
|
137
|
-
child: _SearchModal(
|
|
138
|
-
routes: widget.routes,
|
|
139
|
-
onNavigate: (index) {
|
|
140
|
-
Navigator.of(ctx, rootNavigator: true).pop();
|
|
141
|
-
widget.onTapItem(index);
|
|
142
|
-
},
|
|
143
|
-
),
|
|
144
|
-
),
|
|
145
|
-
transitionBuilder: (ctx, anim, secAnim, child) {
|
|
146
|
-
final curved =
|
|
147
|
-
CurvedAnimation(parent: anim, curve: Curves.easeOutCubic);
|
|
148
|
-
return FadeTransition(
|
|
149
|
-
opacity: curved,
|
|
150
|
-
child: ScaleTransition(
|
|
151
|
-
scale: Tween<double>(begin: 0.95, end: 1.0).animate(curved),
|
|
152
|
-
child: child,
|
|
153
|
-
),
|
|
154
|
-
);
|
|
155
|
-
},
|
|
156
|
-
).then((_) {
|
|
157
|
-
if (mounted) setState(() => _searchOpen = false);
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ── Projects expand/collapse ─────────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
void _toggleProject(int index) {
|
|
164
|
-
setState(() {
|
|
165
|
-
_expandedProject = _expandedProject == index ? null : index;
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ── Build ─────────────────────────────────────────────────────────────────
|
|
170
|
-
|
|
171
|
-
@override
|
|
172
|
-
Widget build(BuildContext context) {
|
|
173
|
-
final colors = context.colors;
|
|
174
|
-
|
|
175
|
-
return Container(
|
|
176
|
-
width: _kWidth,
|
|
177
|
-
decoration: BoxDecoration(
|
|
178
|
-
color: colors.surface,
|
|
179
|
-
border: Border(
|
|
180
|
-
right: BorderSide(color: colors.outline.withValues(alpha: 0.4)),
|
|
181
|
-
),
|
|
182
|
-
),
|
|
183
|
-
child: SafeArea(
|
|
184
|
-
right: false,
|
|
185
|
-
child: Column(
|
|
186
|
-
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
187
|
-
children: [
|
|
188
|
-
// ── Scrollable content ──────────────────────────────────────────
|
|
189
|
-
Expanded(
|
|
190
|
-
child: ScrollConfiguration(
|
|
191
|
-
// Hide scrollbar for a cleaner desktop/web look
|
|
192
|
-
behavior: ScrollConfiguration.of(context)
|
|
193
|
-
.copyWith(scrollbars: false),
|
|
194
|
-
child: SingleChildScrollView(
|
|
195
|
-
physics: const ClampingScrollPhysics(),
|
|
196
|
-
padding: const EdgeInsets.fromLTRB(12, 16, 12, 8),
|
|
197
|
-
child: Column(
|
|
198
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
199
|
-
children: [
|
|
200
|
-
// Brand header
|
|
201
|
-
const _Header(),
|
|
202
|
-
const SizedBox(height: 16),
|
|
203
|
-
|
|
204
|
-
// Search bar — opens modal on tap / ⌘K
|
|
205
|
-
_SearchBar(onTap: _openSearch),
|
|
206
|
-
const SizedBox(height: 20),
|
|
207
|
-
|
|
208
|
-
// ── MENU section ─────────────────────────────────────
|
|
209
|
-
const _SectionLabel('Menu'),
|
|
210
|
-
const SizedBox(height: 4),
|
|
211
|
-
...widget.routes.asMap().entries.map(
|
|
212
|
-
(e) => _NavItem(
|
|
213
|
-
route: e.value,
|
|
214
|
-
isActive: widget.currentIndex == e.key,
|
|
215
|
-
onTap: () => widget.onTapItem(e.key),
|
|
216
|
-
),
|
|
217
|
-
),
|
|
218
|
-
const SizedBox(height: 8),
|
|
219
|
-
|
|
220
|
-
const _SidebarDivider(),
|
|
221
|
-
const SizedBox(height: 8),
|
|
222
|
-
|
|
223
|
-
// ── WORKSPACE section (placeholder items) ─────────────
|
|
224
|
-
const _SectionLabel('Workspace'),
|
|
225
|
-
const SizedBox(height: 4),
|
|
226
|
-
..._placeholders.map(
|
|
227
|
-
(p) =>
|
|
228
|
-
_PlaceholderItem(icon: p.icon, label: p.label),
|
|
229
|
-
),
|
|
230
|
-
const SizedBox(height: 16),
|
|
231
|
-
|
|
232
|
-
const _SidebarDivider(),
|
|
233
|
-
const SizedBox(height: 12),
|
|
234
|
-
|
|
235
|
-
// ── PROJECTS section ──────────────────────────────────
|
|
236
|
-
Row(
|
|
237
|
-
children: [
|
|
238
|
-
const Expanded(child: _SectionLabel('Projects')),
|
|
239
|
-
Icon(KasyIcons.add, size: 14, color: colors.muted),
|
|
240
|
-
const SizedBox(width: 4),
|
|
241
|
-
],
|
|
242
|
-
),
|
|
243
|
-
const SizedBox(height: 4),
|
|
244
|
-
..._projects.asMap().entries.map(
|
|
245
|
-
(e) => _ProjectItem(
|
|
246
|
-
color: e.value.color,
|
|
247
|
-
name: e.value.name,
|
|
248
|
-
isExpanded: _expandedProject == e.key,
|
|
249
|
-
onToggle: () => _toggleProject(e.key),
|
|
250
|
-
),
|
|
251
|
-
),
|
|
252
|
-
|
|
253
|
-
// Add new project
|
|
254
|
-
_AddProjectButton(),
|
|
255
|
-
const SizedBox(height: 8),
|
|
256
|
-
],
|
|
257
|
-
),
|
|
258
|
-
),
|
|
259
|
-
),
|
|
260
|
-
),
|
|
261
|
-
|
|
262
|
-
// ── Theme toggle + sticky footer ─────────────────────────────
|
|
263
|
-
const _ThemeToggle(),
|
|
264
|
-
const SizedBox(height: 8),
|
|
265
|
-
const _SidebarDivider(),
|
|
266
|
-
const _UserFooter(),
|
|
267
|
-
],
|
|
268
|
-
),
|
|
269
|
-
),
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
275
|
-
// Header — brand logo + workspace name + dropdown hint
|
|
276
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
277
|
-
|
|
278
|
-
class _Header extends StatelessWidget {
|
|
279
|
-
const _Header();
|
|
280
|
-
|
|
281
|
-
@override
|
|
282
|
-
Widget build(BuildContext context) {
|
|
283
|
-
final colors = context.colors;
|
|
284
|
-
return KasyHover(
|
|
285
|
-
onTap: () {},
|
|
286
|
-
hapticEnabled: false,
|
|
287
|
-
borderRadius: BorderRadius.circular(10),
|
|
288
|
-
child: Padding(
|
|
289
|
-
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
290
|
-
child: Row(
|
|
291
|
-
children: [
|
|
292
|
-
// Brand icon
|
|
293
|
-
Container(
|
|
294
|
-
width: 34,
|
|
295
|
-
height: 34,
|
|
296
|
-
decoration: BoxDecoration(
|
|
297
|
-
color: colors.primary,
|
|
298
|
-
borderRadius: BorderRadius.circular(8),
|
|
299
|
-
),
|
|
300
|
-
child: const Icon(KasyIcons.assistant, size: 18, color: Colors.white),
|
|
301
|
-
),
|
|
302
|
-
const SizedBox(width: 10),
|
|
303
|
-
// Name + plan
|
|
304
|
-
Expanded(
|
|
305
|
-
child: Column(
|
|
306
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
307
|
-
mainAxisSize: MainAxisSize.min,
|
|
308
|
-
children: [
|
|
309
|
-
Text(
|
|
310
|
-
'Kasy',
|
|
311
|
-
style: context.textTheme.titleMedium?.copyWith(
|
|
312
|
-
fontWeight: FontWeight.w700,
|
|
313
|
-
height: 1.15,
|
|
314
|
-
),
|
|
315
|
-
),
|
|
316
|
-
Text(
|
|
317
|
-
'Design System',
|
|
318
|
-
style: context.textTheme.labelSmall?.copyWith(
|
|
319
|
-
color: colors.muted,
|
|
320
|
-
),
|
|
321
|
-
),
|
|
322
|
-
],
|
|
323
|
-
),
|
|
324
|
-
),
|
|
325
|
-
Icon(KasyIcons.chevronDown, size: 14, color: colors.muted),
|
|
326
|
-
],
|
|
327
|
-
),
|
|
328
|
-
),
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
334
|
-
// Search bar — tappable, opens command-palette modal
|
|
335
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
336
|
-
|
|
337
|
-
class _SearchBar extends StatelessWidget {
|
|
338
|
-
final VoidCallback onTap;
|
|
339
|
-
const _SearchBar({required this.onTap});
|
|
340
|
-
|
|
341
|
-
@override
|
|
342
|
-
Widget build(BuildContext context) {
|
|
343
|
-
final colors = context.colors;
|
|
344
|
-
return KasyHover(
|
|
345
|
-
onTap: onTap,
|
|
346
|
-
semanticLabel: 'Search',
|
|
347
|
-
borderRadius: BorderRadius.circular(8),
|
|
348
|
-
child: Container(
|
|
349
|
-
height: 36,
|
|
350
|
-
decoration: BoxDecoration(
|
|
351
|
-
color: colors.surfaceNeutralSoft,
|
|
352
|
-
borderRadius: BorderRadius.circular(8),
|
|
353
|
-
border: Border.all(
|
|
354
|
-
color: colors.outline.withValues(alpha: 0.5),
|
|
355
|
-
width: 0.6,
|
|
356
|
-
),
|
|
357
|
-
),
|
|
358
|
-
child: Row(
|
|
359
|
-
children: [
|
|
360
|
-
const SizedBox(width: 10),
|
|
361
|
-
Icon(KasyIcons.search, size: 15, color: colors.muted),
|
|
362
|
-
const SizedBox(width: 8),
|
|
363
|
-
Expanded(
|
|
364
|
-
child: Text(
|
|
365
|
-
'Search...',
|
|
366
|
-
style: context.textTheme.bodySmall?.copyWith(
|
|
367
|
-
color: colors.muted,
|
|
368
|
-
),
|
|
369
|
-
),
|
|
370
|
-
),
|
|
371
|
-
// Keyboard shortcut badge
|
|
372
|
-
Container(
|
|
373
|
-
margin: const EdgeInsets.only(right: 8),
|
|
374
|
-
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
|
|
375
|
-
decoration: BoxDecoration(
|
|
376
|
-
color: colors.outline.withValues(alpha: 0.35),
|
|
377
|
-
borderRadius: BorderRadius.circular(4),
|
|
378
|
-
),
|
|
379
|
-
child: Text(
|
|
380
|
-
defaultTargetPlatform == TargetPlatform.macOS ? '⌘K' : 'Ctrl K',
|
|
381
|
-
style: context.textTheme.labelSmall?.copyWith(
|
|
382
|
-
color: colors.muted,
|
|
383
|
-
fontSize: 10,
|
|
384
|
-
),
|
|
385
|
-
),
|
|
386
|
-
),
|
|
387
|
-
],
|
|
388
|
-
),
|
|
389
|
-
),
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
395
|
-
// Section label — ALL CAPS, subtle
|
|
396
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
397
|
-
|
|
398
|
-
class _SectionLabel extends StatelessWidget {
|
|
399
|
-
final String text;
|
|
400
|
-
const _SectionLabel(this.text);
|
|
401
|
-
|
|
402
|
-
@override
|
|
403
|
-
Widget build(BuildContext context) {
|
|
404
|
-
return Padding(
|
|
405
|
-
padding: const EdgeInsets.only(left: 4, bottom: 2),
|
|
406
|
-
child: Text(
|
|
407
|
-
text,
|
|
408
|
-
style: context.textTheme.labelSmall?.copyWith(
|
|
409
|
-
color: context.colors.muted,
|
|
410
|
-
fontWeight: FontWeight.w600,
|
|
411
|
-
letterSpacing: 0.4,
|
|
412
|
-
fontSize: 11,
|
|
413
|
-
),
|
|
414
|
-
),
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
420
|
-
// Divider
|
|
421
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
422
|
-
|
|
423
|
-
class _SidebarDivider extends StatelessWidget {
|
|
424
|
-
const _SidebarDivider();
|
|
425
|
-
|
|
426
|
-
@override
|
|
427
|
-
Widget build(BuildContext context) {
|
|
428
|
-
return Divider(
|
|
429
|
-
height: 1,
|
|
430
|
-
thickness: 0.8,
|
|
431
|
-
color: context.colors.outline.withValues(alpha: 0.4),
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
437
|
-
// Nav item — real, tappable, animated active state
|
|
438
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
439
|
-
|
|
440
|
-
class _NavItem extends StatelessWidget {
|
|
441
|
-
final BartMenuRoute route;
|
|
442
|
-
final bool isActive;
|
|
443
|
-
final VoidCallback onTap;
|
|
444
|
-
|
|
445
|
-
const _NavItem({
|
|
446
|
-
required this.route,
|
|
447
|
-
required this.isActive,
|
|
448
|
-
required this.onTap,
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
@override
|
|
452
|
-
Widget build(BuildContext context) {
|
|
453
|
-
final colors = context.colors;
|
|
454
|
-
final Color iconColor =
|
|
455
|
-
isActive ? colors.primary : colors.onSurface.withValues(alpha: 0.6);
|
|
456
|
-
final Color textColor = isActive ? colors.primary : colors.onSurface;
|
|
457
|
-
final bool hasIconBuilder = route.iconBuilder != null;
|
|
458
|
-
final IconData icon = route.icon ?? KasyIcons.notification;
|
|
459
|
-
|
|
460
|
-
return Padding(
|
|
461
|
-
padding: const EdgeInsets.symmetric(vertical: 1.5),
|
|
462
|
-
child: KasyHover(
|
|
463
|
-
onTap: onTap,
|
|
464
|
-
semanticLabel: route.label,
|
|
465
|
-
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
466
|
-
child: AnimatedContainer(
|
|
467
|
-
duration: const Duration(milliseconds: 160),
|
|
468
|
-
curve: Curves.easeOut,
|
|
469
|
-
height: _kItemHeight,
|
|
470
|
-
decoration: BoxDecoration(
|
|
471
|
-
color: isActive
|
|
472
|
-
? colors.surfacePrimarySoft
|
|
473
|
-
: Colors.transparent,
|
|
474
|
-
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
475
|
-
),
|
|
476
|
-
child: Row(
|
|
477
|
-
children: [
|
|
478
|
-
// Active accent line on the left
|
|
479
|
-
AnimatedContainer(
|
|
480
|
-
duration: const Duration(milliseconds: 160),
|
|
481
|
-
width: 3,
|
|
482
|
-
height: 20,
|
|
483
|
-
margin: const EdgeInsets.only(left: 4, right: 8),
|
|
484
|
-
decoration: BoxDecoration(
|
|
485
|
-
color: isActive ? colors.primary : Colors.transparent,
|
|
486
|
-
borderRadius: BorderRadius.circular(2),
|
|
487
|
-
),
|
|
488
|
-
),
|
|
489
|
-
// Icon
|
|
490
|
-
Icon(icon, size: 18, color: iconColor),
|
|
491
|
-
const SizedBox(width: 10),
|
|
492
|
-
// Label
|
|
493
|
-
Expanded(
|
|
494
|
-
child: Text(
|
|
495
|
-
route.label ?? '',
|
|
496
|
-
style: context.textTheme.bodyMedium?.copyWith(
|
|
497
|
-
color: textColor,
|
|
498
|
-
fontWeight:
|
|
499
|
-
isActive ? FontWeight.w600 : FontWeight.w500,
|
|
500
|
-
),
|
|
501
|
-
),
|
|
502
|
-
),
|
|
503
|
-
// Notification count badge (right side)
|
|
504
|
-
if (hasIconBuilder) ...[
|
|
505
|
-
const _NotificationCountBadge(),
|
|
506
|
-
const SizedBox(width: 10),
|
|
507
|
-
],
|
|
508
|
-
],
|
|
509
|
-
),
|
|
510
|
-
),
|
|
511
|
-
),
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
517
|
-
// Notification count badge — right-aligned pill, reads live count
|
|
518
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
519
|
-
|
|
520
|
-
class _NotificationCountBadge extends ConsumerWidget {
|
|
521
|
-
const _NotificationCountBadge();
|
|
522
|
-
|
|
523
|
-
@override
|
|
524
|
-
Widget build(BuildContext context, WidgetRef ref) {
|
|
525
|
-
final String? userId = ref.watch(userStateNotifierProvider).user.idOrNull;
|
|
526
|
-
if (userId == null) return const SizedBox.shrink();
|
|
527
|
-
|
|
528
|
-
final int count = ref.watch(notificationsProvider).maybeWhen(
|
|
529
|
-
data: (list) => list.data.where((n) => !n.seen).length,
|
|
530
|
-
orElse: () => 0,
|
|
531
|
-
);
|
|
532
|
-
if (count == 0) return const SizedBox.shrink();
|
|
533
|
-
|
|
534
|
-
return AnimatedSwitcher(
|
|
535
|
-
duration: const Duration(milliseconds: 220),
|
|
536
|
-
transitionBuilder: (child, anim) => ScaleTransition(
|
|
537
|
-
scale: Tween<double>(begin: 0.55, end: 1.0).animate(
|
|
538
|
-
CurvedAnimation(parent: anim, curve: Curves.easeOutBack),
|
|
539
|
-
),
|
|
540
|
-
child: FadeTransition(opacity: anim, child: child),
|
|
541
|
-
),
|
|
542
|
-
child: Container(
|
|
543
|
-
key: ValueKey(count),
|
|
544
|
-
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
545
|
-
decoration: BoxDecoration(
|
|
546
|
-
color: context.colors.error,
|
|
547
|
-
borderRadius: BorderRadius.circular(10),
|
|
548
|
-
),
|
|
549
|
-
child: Text(
|
|
550
|
-
count > 99 ? '99+' : '$count',
|
|
551
|
-
style: context.textTheme.labelSmall?.copyWith(
|
|
552
|
-
color: Colors.white,
|
|
553
|
-
fontWeight: FontWeight.w700,
|
|
554
|
-
fontSize: 10,
|
|
555
|
-
height: 1.2,
|
|
556
|
-
),
|
|
557
|
-
),
|
|
558
|
-
),
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
564
|
-
// Placeholder item — hover feedback, no navigation (coming soon)
|
|
565
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
566
|
-
|
|
567
|
-
class _PlaceholderItem extends StatelessWidget {
|
|
568
|
-
final IconData icon;
|
|
569
|
-
final String label;
|
|
570
|
-
|
|
571
|
-
const _PlaceholderItem({required this.icon, required this.label});
|
|
572
|
-
|
|
573
|
-
@override
|
|
574
|
-
Widget build(BuildContext context) {
|
|
575
|
-
final colors = context.colors;
|
|
576
|
-
return Padding(
|
|
577
|
-
padding: const EdgeInsets.symmetric(vertical: 1.5),
|
|
578
|
-
child: KasyHover(
|
|
579
|
-
onTap: () {
|
|
580
|
-
showKasyToast(
|
|
581
|
-
context,
|
|
582
|
-
title: label,
|
|
583
|
-
message: 'Coming soon',
|
|
584
|
-
duration: const Duration(seconds: 2),
|
|
585
|
-
);
|
|
586
|
-
},
|
|
587
|
-
hapticEnabled: false,
|
|
588
|
-
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
589
|
-
child: SizedBox(
|
|
590
|
-
height: _kItemHeight,
|
|
591
|
-
child: Row(
|
|
592
|
-
children: [
|
|
593
|
-
const SizedBox(width: 15), // aligns with nav items (3+4+8)
|
|
594
|
-
Icon(
|
|
595
|
-
icon,
|
|
596
|
-
size: 18,
|
|
597
|
-
color: colors.onSurface.withValues(alpha: 0.35),
|
|
598
|
-
),
|
|
599
|
-
const SizedBox(width: 10),
|
|
600
|
-
Expanded(
|
|
601
|
-
child: Text(
|
|
602
|
-
label,
|
|
603
|
-
style: context.textTheme.bodyMedium?.copyWith(
|
|
604
|
-
color: colors.muted,
|
|
605
|
-
fontWeight: FontWeight.w500,
|
|
606
|
-
),
|
|
607
|
-
),
|
|
608
|
-
),
|
|
609
|
-
],
|
|
610
|
-
),
|
|
611
|
-
),
|
|
612
|
-
),
|
|
613
|
-
);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
618
|
-
// Project item — expandable with animated chevron + sub-items
|
|
619
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
620
|
-
|
|
621
|
-
class _ProjectItem extends StatelessWidget {
|
|
622
|
-
final Color color;
|
|
623
|
-
final String name;
|
|
624
|
-
final bool isExpanded;
|
|
625
|
-
final VoidCallback onToggle;
|
|
626
|
-
|
|
627
|
-
const _ProjectItem({
|
|
628
|
-
required this.color,
|
|
629
|
-
required this.name,
|
|
630
|
-
required this.isExpanded,
|
|
631
|
-
required this.onToggle,
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
static const _subItems = [
|
|
635
|
-
(icon: KasyIcons.home, label: 'Overview'),
|
|
636
|
-
(icon: KasyIcons.person, label: 'Members'),
|
|
637
|
-
(icon: KasyIcons.settings, label: 'Settings'),
|
|
638
|
-
];
|
|
639
|
-
|
|
640
|
-
@override
|
|
641
|
-
Widget build(BuildContext context) {
|
|
642
|
-
final colors = context.colors;
|
|
643
|
-
|
|
644
|
-
return Column(
|
|
645
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
646
|
-
mainAxisSize: MainAxisSize.min,
|
|
647
|
-
children: [
|
|
648
|
-
// ── Header row ────────────────────────────────────────────────────
|
|
649
|
-
KasyHover(
|
|
650
|
-
onTap: onToggle,
|
|
651
|
-
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
652
|
-
child: SizedBox(
|
|
653
|
-
height: 36,
|
|
654
|
-
child: Row(
|
|
655
|
-
children: [
|
|
656
|
-
const SizedBox(width: 15),
|
|
657
|
-
// Colored dot
|
|
658
|
-
Container(
|
|
659
|
-
width: 10,
|
|
660
|
-
height: 10,
|
|
661
|
-
decoration: BoxDecoration(
|
|
662
|
-
color: color,
|
|
663
|
-
shape: BoxShape.circle,
|
|
664
|
-
boxShadow: [
|
|
665
|
-
BoxShadow(
|
|
666
|
-
color: color.withValues(alpha: 0.45),
|
|
667
|
-
blurRadius: 5,
|
|
668
|
-
offset: const Offset(0, 1),
|
|
669
|
-
),
|
|
670
|
-
],
|
|
671
|
-
),
|
|
672
|
-
),
|
|
673
|
-
const SizedBox(width: 12),
|
|
674
|
-
Expanded(
|
|
675
|
-
child: Text(
|
|
676
|
-
name,
|
|
677
|
-
style: context.textTheme.bodyMedium?.copyWith(
|
|
678
|
-
color: colors.onSurface.withValues(alpha: 0.8),
|
|
679
|
-
fontWeight: FontWeight.w500,
|
|
680
|
-
),
|
|
681
|
-
),
|
|
682
|
-
),
|
|
683
|
-
// Chevron rotates when expanded
|
|
684
|
-
AnimatedRotation(
|
|
685
|
-
duration: const Duration(milliseconds: 200),
|
|
686
|
-
curve: Curves.easeInOut,
|
|
687
|
-
turns: isExpanded ? 0.25 : 0.0,
|
|
688
|
-
child: Icon(
|
|
689
|
-
KasyIcons.chevronRight,
|
|
690
|
-
size: 13,
|
|
691
|
-
color: colors.muted.withValues(alpha: 0.6),
|
|
692
|
-
),
|
|
693
|
-
),
|
|
694
|
-
const SizedBox(width: 10),
|
|
695
|
-
],
|
|
696
|
-
),
|
|
697
|
-
),
|
|
698
|
-
),
|
|
699
|
-
|
|
700
|
-
// ── Sub-items — animated expand/collapse ──────────────────────────
|
|
701
|
-
AnimatedSize(
|
|
702
|
-
duration: const Duration(milliseconds: 200),
|
|
703
|
-
curve: Curves.easeInOut,
|
|
704
|
-
child: isExpanded
|
|
705
|
-
? Column(
|
|
706
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
707
|
-
children: _subItems
|
|
708
|
-
.map((s) =>
|
|
709
|
-
_ProjectSubItem(icon: s.icon, label: s.label))
|
|
710
|
-
.toList(),
|
|
711
|
-
)
|
|
712
|
-
: const SizedBox.shrink(),
|
|
713
|
-
),
|
|
714
|
-
],
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
class _ProjectSubItem extends StatelessWidget {
|
|
720
|
-
final IconData icon;
|
|
721
|
-
final String label;
|
|
722
|
-
|
|
723
|
-
const _ProjectSubItem({required this.icon, required this.label});
|
|
724
|
-
|
|
725
|
-
@override
|
|
726
|
-
Widget build(BuildContext context) {
|
|
727
|
-
final colors = context.colors;
|
|
728
|
-
return KasyHover(
|
|
729
|
-
onTap: () {},
|
|
730
|
-
hapticEnabled: false,
|
|
731
|
-
borderRadius: BorderRadius.circular(6),
|
|
732
|
-
child: SizedBox(
|
|
733
|
-
height: 32,
|
|
734
|
-
child: Row(
|
|
735
|
-
children: [
|
|
736
|
-
const SizedBox(width: 35), // deeper indent
|
|
737
|
-
Icon(icon, size: 14, color: colors.muted),
|
|
738
|
-
const SizedBox(width: 9),
|
|
739
|
-
Text(
|
|
740
|
-
label,
|
|
741
|
-
style: context.textTheme.bodySmall?.copyWith(
|
|
742
|
-
color: colors.onSurface.withValues(alpha: 0.6),
|
|
743
|
-
fontWeight: FontWeight.w500,
|
|
744
|
-
),
|
|
745
|
-
),
|
|
746
|
-
],
|
|
747
|
-
),
|
|
748
|
-
),
|
|
749
|
-
);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
754
|
-
// "Add New Project" button
|
|
755
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
756
|
-
|
|
757
|
-
class _AddProjectButton extends StatelessWidget {
|
|
758
|
-
@override
|
|
759
|
-
Widget build(BuildContext context) {
|
|
760
|
-
final colors = context.colors;
|
|
761
|
-
return KasyHover(
|
|
762
|
-
onTap: () {},
|
|
763
|
-
hapticEnabled: false,
|
|
764
|
-
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
765
|
-
child: SizedBox(
|
|
766
|
-
height: 34,
|
|
767
|
-
child: Row(
|
|
768
|
-
children: [
|
|
769
|
-
const SizedBox(width: 15),
|
|
770
|
-
Container(
|
|
771
|
-
width: 20,
|
|
772
|
-
height: 20,
|
|
773
|
-
decoration: BoxDecoration(
|
|
774
|
-
color: colors.surfacePrimarySoft,
|
|
775
|
-
borderRadius: BorderRadius.circular(6),
|
|
776
|
-
),
|
|
777
|
-
child: Icon(KasyIcons.add, size: 13, color: colors.primary),
|
|
778
|
-
),
|
|
779
|
-
const SizedBox(width: 10),
|
|
780
|
-
Text(
|
|
781
|
-
'New Project',
|
|
782
|
-
style: context.textTheme.bodySmall?.copyWith(
|
|
783
|
-
color: colors.primary.withValues(alpha: 0.75),
|
|
784
|
-
fontWeight: FontWeight.w500,
|
|
785
|
-
),
|
|
786
|
-
),
|
|
787
|
-
],
|
|
788
|
-
),
|
|
789
|
-
),
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
795
|
-
// User footer — real data via Riverpod, KasyAvatar initials
|
|
796
|
-
// Logout icon only visible on hover to avoid visual clutter.
|
|
797
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
798
|
-
|
|
799
|
-
class _UserFooter extends ConsumerStatefulWidget {
|
|
800
|
-
const _UserFooter();
|
|
801
|
-
|
|
802
|
-
@override
|
|
803
|
-
ConsumerState<_UserFooter> createState() => _UserFooterState();
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
class _UserFooterState extends ConsumerState<_UserFooter> {
|
|
807
|
-
bool _hovered = false;
|
|
808
|
-
|
|
809
|
-
@override
|
|
810
|
-
Widget build(BuildContext context) {
|
|
811
|
-
final user = ref.watch(userStateNotifierProvider).user;
|
|
812
|
-
|
|
813
|
-
final String displayName;
|
|
814
|
-
final String subtitle;
|
|
815
|
-
|
|
816
|
-
if (user is AuthenticatedUserData) {
|
|
817
|
-
displayName = user.name ?? user.email.split('@').first;
|
|
818
|
-
subtitle = user.email;
|
|
819
|
-
} else {
|
|
820
|
-
displayName = 'Guest';
|
|
821
|
-
subtitle = 'Not signed in';
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
final String initials =
|
|
825
|
-
displayName.isNotEmpty ? displayName[0].toUpperCase() : 'G';
|
|
826
|
-
|
|
827
|
-
return MouseRegion(
|
|
828
|
-
onEnter: (_) => setState(() => _hovered = true),
|
|
829
|
-
onExit: (_) => setState(() => _hovered = false),
|
|
830
|
-
child: KasyHover(
|
|
831
|
-
onTap: () {},
|
|
832
|
-
hapticEnabled: false,
|
|
833
|
-
child: Padding(
|
|
834
|
-
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
|
835
|
-
child: Row(
|
|
836
|
-
children: [
|
|
837
|
-
// Avatar
|
|
838
|
-
KasyAvatar(
|
|
839
|
-
size: KasyAvatarSize.small,
|
|
840
|
-
diameter: 32,
|
|
841
|
-
initials: initials,
|
|
842
|
-
),
|
|
843
|
-
const SizedBox(width: 10),
|
|
844
|
-
// Name + email
|
|
845
|
-
Expanded(
|
|
846
|
-
child: Column(
|
|
847
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
848
|
-
mainAxisSize: MainAxisSize.min,
|
|
849
|
-
children: [
|
|
850
|
-
Text(
|
|
851
|
-
displayName,
|
|
852
|
-
style: context.textTheme.labelMedium?.copyWith(
|
|
853
|
-
fontWeight: FontWeight.w600,
|
|
854
|
-
),
|
|
855
|
-
overflow: TextOverflow.ellipsis,
|
|
856
|
-
maxLines: 1,
|
|
857
|
-
),
|
|
858
|
-
Text(
|
|
859
|
-
subtitle,
|
|
860
|
-
style: context.textTheme.labelSmall?.copyWith(
|
|
861
|
-
color: context.colors.muted,
|
|
862
|
-
),
|
|
863
|
-
overflow: TextOverflow.ellipsis,
|
|
864
|
-
maxLines: 1,
|
|
865
|
-
),
|
|
866
|
-
],
|
|
867
|
-
),
|
|
868
|
-
),
|
|
869
|
-
// Logout icon — only visible on hover
|
|
870
|
-
AnimatedOpacity(
|
|
871
|
-
opacity: _hovered ? 1.0 : 0.0,
|
|
872
|
-
duration: const Duration(milliseconds: 150),
|
|
873
|
-
child: Padding(
|
|
874
|
-
padding: const EdgeInsets.only(left: 8),
|
|
875
|
-
child: Icon(
|
|
876
|
-
KasyIcons.logout,
|
|
877
|
-
size: 16,
|
|
878
|
-
color: context.colors.muted,
|
|
879
|
-
),
|
|
880
|
-
),
|
|
881
|
-
),
|
|
882
|
-
],
|
|
883
|
-
),
|
|
884
|
-
),
|
|
885
|
-
),
|
|
886
|
-
);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
891
|
-
// Search modal — centered command palette with KasyTextField + live filter
|
|
892
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
893
|
-
|
|
894
|
-
class _SearchModal extends StatefulWidget {
|
|
895
|
-
final List<BartMenuRoute> routes;
|
|
896
|
-
final void Function(int index) onNavigate;
|
|
897
|
-
|
|
898
|
-
const _SearchModal({required this.routes, required this.onNavigate});
|
|
899
|
-
|
|
900
|
-
@override
|
|
901
|
-
State<_SearchModal> createState() => _SearchModalState();
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
class _SearchModalState extends State<_SearchModal> {
|
|
905
|
-
final _controller = TextEditingController();
|
|
906
|
-
late final FocusNode _focusNode;
|
|
907
|
-
final ScrollController _scrollController = ScrollController();
|
|
908
|
-
String _query = '';
|
|
909
|
-
int _selectedIndex = 0;
|
|
910
|
-
|
|
911
|
-
// Approximate height of each result tile (34px icon + 12px vertical padding)
|
|
912
|
-
static const double _kTileHeight = 47.0;
|
|
913
|
-
|
|
914
|
-
static const _placeholders = [
|
|
915
|
-
(icon: KasyIcons.flash, label: 'Activity'),
|
|
916
|
-
(icon: KasyIcons.note, label: 'Tasks'),
|
|
917
|
-
(icon: KasyIcons.book, label: 'Reports'),
|
|
918
|
-
(icon: KasyIcons.person, label: 'Team'),
|
|
919
|
-
];
|
|
920
|
-
|
|
921
|
-
@override
|
|
922
|
-
void initState() {
|
|
923
|
-
super.initState();
|
|
924
|
-
_focusNode = FocusNode(onKeyEvent: _onFocusKeyEvent);
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
@override
|
|
928
|
-
void dispose() {
|
|
929
|
-
_controller.dispose();
|
|
930
|
-
_focusNode.dispose();
|
|
931
|
-
_scrollController.dispose();
|
|
932
|
-
super.dispose();
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
KeyEventResult _onFocusKeyEvent(FocusNode node, KeyEvent event) {
|
|
936
|
-
if (event is! KeyDownEvent && event is! KeyRepeatEvent) {
|
|
937
|
-
return KeyEventResult.ignored;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
final results = _results;
|
|
941
|
-
|
|
942
|
-
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
|
943
|
-
if (results.isNotEmpty) {
|
|
944
|
-
setState(() {
|
|
945
|
-
_selectedIndex = (_selectedIndex + 1).clamp(0, results.length - 1);
|
|
946
|
-
});
|
|
947
|
-
_scrollToSelected();
|
|
948
|
-
}
|
|
949
|
-
return KeyEventResult.handled;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
|
953
|
-
if (results.isNotEmpty) {
|
|
954
|
-
setState(() {
|
|
955
|
-
_selectedIndex = (_selectedIndex - 1).clamp(0, results.length - 1);
|
|
956
|
-
});
|
|
957
|
-
_scrollToSelected();
|
|
958
|
-
}
|
|
959
|
-
return KeyEventResult.handled;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
|
963
|
-
if (_selectedIndex < results.length) {
|
|
964
|
-
final item = results[_selectedIndex];
|
|
965
|
-
if (item.navIndex != null) {
|
|
966
|
-
widget.onNavigate(item.navIndex!);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
return KeyEventResult.handled;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
|
973
|
-
Navigator.of(context, rootNavigator: true).pop();
|
|
974
|
-
return KeyEventResult.handled;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
return KeyEventResult.ignored;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
void _scrollToSelected() {
|
|
981
|
-
if (!_scrollController.hasClients) return;
|
|
982
|
-
final offset = (_selectedIndex * _kTileHeight)
|
|
983
|
-
.clamp(0.0, _scrollController.position.maxScrollExtent);
|
|
984
|
-
_scrollController.animateTo(
|
|
985
|
-
offset,
|
|
986
|
-
duration: const Duration(milliseconds: 120),
|
|
987
|
-
curve: Curves.easeOut,
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
List<({IconData icon, String label, int? navIndex})> get _results {
|
|
992
|
-
final q = _query.trim().toLowerCase();
|
|
993
|
-
final out = <({IconData icon, String label, int? navIndex})>[];
|
|
994
|
-
|
|
995
|
-
// Real nav routes
|
|
996
|
-
for (var i = 0; i < widget.routes.length; i++) {
|
|
997
|
-
final r = widget.routes[i];
|
|
998
|
-
final label = r.label ?? '';
|
|
999
|
-
if (q.isEmpty || label.toLowerCase().contains(q)) {
|
|
1000
|
-
out.add((
|
|
1001
|
-
icon: r.icon ?? KasyIcons.notification,
|
|
1002
|
-
label: label,
|
|
1003
|
-
navIndex: i,
|
|
1004
|
-
));
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// Placeholder workspace items
|
|
1009
|
-
for (final p in _placeholders) {
|
|
1010
|
-
if (q.isEmpty || p.label.toLowerCase().contains(q)) {
|
|
1011
|
-
out.add((icon: p.icon, label: p.label, navIndex: null));
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
return out;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
@override
|
|
1019
|
-
Widget build(BuildContext context) {
|
|
1020
|
-
final colors = context.colors;
|
|
1021
|
-
final results = _results;
|
|
1022
|
-
final double maxW = min(560.0, MediaQuery.of(context).size.width - 48);
|
|
1023
|
-
|
|
1024
|
-
return Material(
|
|
1025
|
-
color: Colors.transparent,
|
|
1026
|
-
child: Container(
|
|
1027
|
-
width: maxW,
|
|
1028
|
-
constraints: BoxConstraints(
|
|
1029
|
-
maxHeight: MediaQuery.of(context).size.height * 0.62,
|
|
1030
|
-
),
|
|
1031
|
-
decoration: BoxDecoration(
|
|
1032
|
-
color: colors.surface,
|
|
1033
|
-
borderRadius: BorderRadius.circular(16),
|
|
1034
|
-
border: Border.all(color: colors.outline.withValues(alpha: 0.25)),
|
|
1035
|
-
boxShadow: [
|
|
1036
|
-
BoxShadow(
|
|
1037
|
-
color: Colors.black.withValues(alpha: 0.18),
|
|
1038
|
-
blurRadius: 40,
|
|
1039
|
-
offset: const Offset(0, 12),
|
|
1040
|
-
),
|
|
1041
|
-
BoxShadow(
|
|
1042
|
-
color: Colors.black.withValues(alpha: 0.06),
|
|
1043
|
-
blurRadius: 8,
|
|
1044
|
-
offset: const Offset(0, 2),
|
|
1045
|
-
),
|
|
1046
|
-
],
|
|
1047
|
-
),
|
|
1048
|
-
child: ClipRRect(
|
|
1049
|
-
borderRadius: BorderRadius.circular(16),
|
|
1050
|
-
child: Column(
|
|
1051
|
-
mainAxisSize: MainAxisSize.min,
|
|
1052
|
-
children: [
|
|
1053
|
-
// ── Search input ──────────────────────────────────────────────
|
|
1054
|
-
Padding(
|
|
1055
|
-
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
|
1056
|
-
child: KasyTextField(
|
|
1057
|
-
controller: _controller,
|
|
1058
|
-
focusNode: _focusNode,
|
|
1059
|
-
hint: 'Search pages, settings, projects...',
|
|
1060
|
-
autofocus: true,
|
|
1061
|
-
onChanged: (v) => setState(() {
|
|
1062
|
-
_query = v;
|
|
1063
|
-
_selectedIndex = 0;
|
|
1064
|
-
}),
|
|
1065
|
-
),
|
|
1066
|
-
),
|
|
1067
|
-
|
|
1068
|
-
// ── Results ───────────────────────────────────────────────────
|
|
1069
|
-
if (results.isEmpty && _query.isNotEmpty)
|
|
1070
|
-
Padding(
|
|
1071
|
-
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
|
1072
|
-
child: Row(
|
|
1073
|
-
children: [
|
|
1074
|
-
Icon(KasyIcons.search, size: 16, color: colors.muted),
|
|
1075
|
-
const SizedBox(width: 8),
|
|
1076
|
-
Text(
|
|
1077
|
-
'No results for "$_query"',
|
|
1078
|
-
style: context.textTheme.bodyMedium
|
|
1079
|
-
?.copyWith(color: colors.muted),
|
|
1080
|
-
),
|
|
1081
|
-
],
|
|
1082
|
-
),
|
|
1083
|
-
)
|
|
1084
|
-
else ...[
|
|
1085
|
-
Divider(
|
|
1086
|
-
height: 1,
|
|
1087
|
-
color: colors.outline.withValues(alpha: 0.4)),
|
|
1088
|
-
Flexible(
|
|
1089
|
-
child: ListView.separated(
|
|
1090
|
-
controller: _scrollController,
|
|
1091
|
-
shrinkWrap: true,
|
|
1092
|
-
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
1093
|
-
itemCount: results.length,
|
|
1094
|
-
separatorBuilder: (_, idx) => const SizedBox(height: 1),
|
|
1095
|
-
itemBuilder: (_, i) {
|
|
1096
|
-
final item = results[i];
|
|
1097
|
-
return _SearchResultTile(
|
|
1098
|
-
icon: item.icon,
|
|
1099
|
-
label: item.label,
|
|
1100
|
-
isNavigable: item.navIndex != null,
|
|
1101
|
-
isSelected: i == _selectedIndex,
|
|
1102
|
-
onTap: item.navIndex != null
|
|
1103
|
-
? () => widget.onNavigate(item.navIndex!)
|
|
1104
|
-
: null,
|
|
1105
|
-
onHover: () => setState(() => _selectedIndex = i),
|
|
1106
|
-
);
|
|
1107
|
-
},
|
|
1108
|
-
),
|
|
1109
|
-
),
|
|
1110
|
-
],
|
|
1111
|
-
|
|
1112
|
-
// ── Footer hint ───────────────────────────────────────────────
|
|
1113
|
-
Container(
|
|
1114
|
-
decoration: BoxDecoration(
|
|
1115
|
-
color: colors.surfaceNeutralSoft,
|
|
1116
|
-
border: Border(
|
|
1117
|
-
top: BorderSide(
|
|
1118
|
-
color: colors.outline.withValues(alpha: 0.3)),
|
|
1119
|
-
),
|
|
1120
|
-
),
|
|
1121
|
-
padding: const EdgeInsets.symmetric(
|
|
1122
|
-
horizontal: 16, vertical: 8),
|
|
1123
|
-
child: const Row(
|
|
1124
|
-
children: [
|
|
1125
|
-
_KbdHint(label: '↑↓', tooltip: 'Navigate'),
|
|
1126
|
-
SizedBox(width: 12),
|
|
1127
|
-
_KbdHint(label: '↵', tooltip: 'Select'),
|
|
1128
|
-
SizedBox(width: 12),
|
|
1129
|
-
_KbdHint(label: 'Esc', tooltip: 'Close'),
|
|
1130
|
-
],
|
|
1131
|
-
),
|
|
1132
|
-
),
|
|
1133
|
-
],
|
|
1134
|
-
),
|
|
1135
|
-
),
|
|
1136
|
-
),
|
|
1137
|
-
);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
/// Keyboard shortcut hint chip shown in the modal footer.
|
|
1142
|
-
class _KbdHint extends StatelessWidget {
|
|
1143
|
-
final String label;
|
|
1144
|
-
final String tooltip;
|
|
1145
|
-
|
|
1146
|
-
const _KbdHint({required this.label, required this.tooltip});
|
|
1147
|
-
|
|
1148
|
-
@override
|
|
1149
|
-
Widget build(BuildContext context) {
|
|
1150
|
-
final colors = context.colors;
|
|
1151
|
-
return Row(
|
|
1152
|
-
mainAxisSize: MainAxisSize.min,
|
|
1153
|
-
children: [
|
|
1154
|
-
Container(
|
|
1155
|
-
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
|
1156
|
-
decoration: BoxDecoration(
|
|
1157
|
-
color: colors.surface,
|
|
1158
|
-
borderRadius: BorderRadius.circular(4),
|
|
1159
|
-
border:
|
|
1160
|
-
Border.all(color: colors.outline.withValues(alpha: 0.5)),
|
|
1161
|
-
),
|
|
1162
|
-
child: Text(
|
|
1163
|
-
label,
|
|
1164
|
-
style: context.textTheme.labelSmall?.copyWith(
|
|
1165
|
-
fontSize: 10,
|
|
1166
|
-
color: colors.muted,
|
|
1167
|
-
fontWeight: FontWeight.w600,
|
|
1168
|
-
),
|
|
1169
|
-
),
|
|
1170
|
-
),
|
|
1171
|
-
const SizedBox(width: 4),
|
|
1172
|
-
Text(
|
|
1173
|
-
tooltip,
|
|
1174
|
-
style: context.textTheme.labelSmall?.copyWith(
|
|
1175
|
-
fontSize: 10,
|
|
1176
|
-
color: colors.muted,
|
|
1177
|
-
),
|
|
1178
|
-
),
|
|
1179
|
-
],
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1185
|
-
// Search result tile
|
|
1186
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1187
|
-
|
|
1188
|
-
class _SearchResultTile extends StatelessWidget {
|
|
1189
|
-
final IconData icon;
|
|
1190
|
-
final String label;
|
|
1191
|
-
final bool isNavigable;
|
|
1192
|
-
final bool isSelected;
|
|
1193
|
-
final VoidCallback? onTap;
|
|
1194
|
-
final VoidCallback? onHover;
|
|
1195
|
-
|
|
1196
|
-
const _SearchResultTile({
|
|
1197
|
-
required this.icon,
|
|
1198
|
-
required this.label,
|
|
1199
|
-
required this.isNavigable,
|
|
1200
|
-
required this.isSelected,
|
|
1201
|
-
this.onTap,
|
|
1202
|
-
this.onHover,
|
|
1203
|
-
});
|
|
1204
|
-
|
|
1205
|
-
@override
|
|
1206
|
-
Widget build(BuildContext context) {
|
|
1207
|
-
final colors = context.colors;
|
|
1208
|
-
|
|
1209
|
-
final Widget content = AnimatedContainer(
|
|
1210
|
-
duration: const Duration(milliseconds: 100),
|
|
1211
|
-
color: isSelected
|
|
1212
|
-
? colors.surfacePrimarySoft.withValues(alpha: 0.5)
|
|
1213
|
-
: Colors.transparent,
|
|
1214
|
-
child: Padding(
|
|
1215
|
-
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
1216
|
-
child: Row(
|
|
1217
|
-
children: [
|
|
1218
|
-
// Icon badge
|
|
1219
|
-
Container(
|
|
1220
|
-
width: 34,
|
|
1221
|
-
height: 34,
|
|
1222
|
-
decoration: BoxDecoration(
|
|
1223
|
-
color: isNavigable
|
|
1224
|
-
? colors.surfacePrimarySoft
|
|
1225
|
-
: colors.surfaceNeutralSoft,
|
|
1226
|
-
borderRadius: BorderRadius.circular(8),
|
|
1227
|
-
),
|
|
1228
|
-
child: Icon(
|
|
1229
|
-
icon,
|
|
1230
|
-
size: 16,
|
|
1231
|
-
color: isNavigable ? colors.primary : colors.muted,
|
|
1232
|
-
),
|
|
1233
|
-
),
|
|
1234
|
-
const SizedBox(width: 12),
|
|
1235
|
-
// Label
|
|
1236
|
-
Expanded(
|
|
1237
|
-
child: Text(
|
|
1238
|
-
label,
|
|
1239
|
-
style: context.textTheme.bodyMedium?.copyWith(
|
|
1240
|
-
color: isNavigable ? colors.onSurface : colors.muted,
|
|
1241
|
-
fontWeight:
|
|
1242
|
-
isNavigable ? FontWeight.w500 : FontWeight.w400,
|
|
1243
|
-
),
|
|
1244
|
-
),
|
|
1245
|
-
),
|
|
1246
|
-
// Right indicator
|
|
1247
|
-
if (!isNavigable)
|
|
1248
|
-
Container(
|
|
1249
|
-
padding:
|
|
1250
|
-
const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
|
1251
|
-
decoration: BoxDecoration(
|
|
1252
|
-
color: colors.surfaceNeutralSoft,
|
|
1253
|
-
borderRadius: BorderRadius.circular(6),
|
|
1254
|
-
border: Border.all(
|
|
1255
|
-
color: colors.outline.withValues(alpha: 0.4)),
|
|
1256
|
-
),
|
|
1257
|
-
child: Text(
|
|
1258
|
-
'Soon',
|
|
1259
|
-
style: context.textTheme.labelSmall?.copyWith(
|
|
1260
|
-
color: colors.muted,
|
|
1261
|
-
fontSize: 10,
|
|
1262
|
-
),
|
|
1263
|
-
),
|
|
1264
|
-
)
|
|
1265
|
-
else
|
|
1266
|
-
Icon(KasyIcons.arrowForwardIos,
|
|
1267
|
-
size: 13, color: colors.muted.withValues(alpha: 0.6)),
|
|
1268
|
-
],
|
|
1269
|
-
),
|
|
1270
|
-
),
|
|
1271
|
-
);
|
|
1272
|
-
|
|
1273
|
-
return MouseRegion(
|
|
1274
|
-
onEnter: (_) => onHover?.call(),
|
|
1275
|
-
child: onTap != null
|
|
1276
|
-
? KasyHover(onTap: onTap!, child: content)
|
|
1277
|
-
: content,
|
|
1278
|
-
);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1283
|
-
// Theme toggle — pill switcher Light / Dark in the sidebar footer area
|
|
1284
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1285
|
-
|
|
1286
|
-
class _ThemeToggle extends StatelessWidget {
|
|
1287
|
-
const _ThemeToggle();
|
|
1288
|
-
|
|
1289
|
-
@override
|
|
1290
|
-
Widget build(BuildContext context) {
|
|
1291
|
-
final theme = ThemeProvider.of(context);
|
|
1292
|
-
final isDark = theme.mode == ThemeMode.dark;
|
|
1293
|
-
final colors = context.colors;
|
|
1294
|
-
|
|
1295
|
-
return Padding(
|
|
1296
|
-
padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
|
|
1297
|
-
child: Container(
|
|
1298
|
-
height: 46,
|
|
1299
|
-
padding: const EdgeInsets.all(4),
|
|
1300
|
-
decoration: BoxDecoration(
|
|
1301
|
-
color: colors.surfaceNeutralSoft,
|
|
1302
|
-
borderRadius: BorderRadius.circular(12),
|
|
1303
|
-
border: Border.all(color: colors.outline.withValues(alpha: 0.35)),
|
|
1304
|
-
),
|
|
1305
|
-
child: Row(
|
|
1306
|
-
children: [
|
|
1307
|
-
Expanded(
|
|
1308
|
-
child: _ThemeOption(
|
|
1309
|
-
icon: KasyIcons.lightMode,
|
|
1310
|
-
label: 'Light',
|
|
1311
|
-
isActive: !isDark,
|
|
1312
|
-
isDarkMode: isDark,
|
|
1313
|
-
onTap: isDark ? theme.toggle : null,
|
|
1314
|
-
),
|
|
1315
|
-
),
|
|
1316
|
-
Expanded(
|
|
1317
|
-
child: _ThemeOption(
|
|
1318
|
-
icon: KasyIcons.darkMode,
|
|
1319
|
-
label: 'Dark',
|
|
1320
|
-
isActive: isDark,
|
|
1321
|
-
isDarkMode: isDark,
|
|
1322
|
-
onTap: isDark ? null : theme.toggle,
|
|
1323
|
-
),
|
|
1324
|
-
),
|
|
1325
|
-
],
|
|
1326
|
-
),
|
|
1327
|
-
),
|
|
1328
|
-
);
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
class _ThemeOption extends StatelessWidget {
|
|
1333
|
-
final IconData icon;
|
|
1334
|
-
final String label;
|
|
1335
|
-
final bool isActive;
|
|
1336
|
-
final bool isDarkMode;
|
|
1337
|
-
final VoidCallback? onTap;
|
|
1338
|
-
|
|
1339
|
-
const _ThemeOption({
|
|
1340
|
-
required this.icon,
|
|
1341
|
-
required this.label,
|
|
1342
|
-
required this.isActive,
|
|
1343
|
-
required this.isDarkMode,
|
|
1344
|
-
this.onTap,
|
|
1345
|
-
});
|
|
1346
|
-
|
|
1347
|
-
@override
|
|
1348
|
-
Widget build(BuildContext context) {
|
|
1349
|
-
final colors = context.colors;
|
|
1350
|
-
|
|
1351
|
-
// Active pill — white card on light, distinct dark surface on dark
|
|
1352
|
-
final Color activeBg =
|
|
1353
|
-
isDarkMode ? colors.onSurface.withValues(alpha: 0.18) : colors.surface;
|
|
1354
|
-
final Color activeContentColor =
|
|
1355
|
-
isDarkMode ? Colors.white : colors.onSurface;
|
|
1356
|
-
final Color inactiveColor = colors.muted;
|
|
1357
|
-
|
|
1358
|
-
return KasyHover(
|
|
1359
|
-
onTap: onTap ?? () {},
|
|
1360
|
-
hapticEnabled: false,
|
|
1361
|
-
borderRadius: BorderRadius.circular(8),
|
|
1362
|
-
child: AnimatedContainer(
|
|
1363
|
-
duration: const Duration(milliseconds: 200),
|
|
1364
|
-
curve: Curves.easeOut,
|
|
1365
|
-
decoration: BoxDecoration(
|
|
1366
|
-
color: isActive ? activeBg : Colors.transparent,
|
|
1367
|
-
borderRadius: BorderRadius.circular(8),
|
|
1368
|
-
boxShadow: isActive && !isDarkMode
|
|
1369
|
-
? [
|
|
1370
|
-
BoxShadow(
|
|
1371
|
-
color: Colors.black.withValues(alpha: 0.12),
|
|
1372
|
-
blurRadius: 8,
|
|
1373
|
-
offset: const Offset(0, 2),
|
|
1374
|
-
),
|
|
1375
|
-
]
|
|
1376
|
-
: null,
|
|
1377
|
-
),
|
|
1378
|
-
child: Row(
|
|
1379
|
-
mainAxisAlignment: MainAxisAlignment.center,
|
|
1380
|
-
children: [
|
|
1381
|
-
Icon(
|
|
1382
|
-
icon,
|
|
1383
|
-
size: 15,
|
|
1384
|
-
color: isActive ? activeContentColor : inactiveColor,
|
|
1385
|
-
),
|
|
1386
|
-
const SizedBox(width: 6),
|
|
1387
|
-
Text(
|
|
1388
|
-
label,
|
|
1389
|
-
style: context.textTheme.bodySmall?.copyWith(
|
|
1390
|
-
color: isActive ? activeContentColor : inactiveColor,
|
|
1391
|
-
fontWeight: isActive ? FontWeight.w700 : FontWeight.w400,
|
|
1392
|
-
),
|
|
1393
|
-
),
|
|
1394
|
-
],
|
|
1395
|
-
),
|
|
1396
|
-
),
|
|
1397
|
-
);
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1402
|
-
// Rail theme toggle — single icon button in the collapsed rail
|
|
1403
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1404
|
-
|
|
1405
|
-
class _RailThemeToggle extends StatelessWidget {
|
|
1406
|
-
const _RailThemeToggle();
|
|
1407
|
-
|
|
1408
|
-
@override
|
|
1409
|
-
Widget build(BuildContext context) {
|
|
1410
|
-
final theme = ThemeProvider.of(context);
|
|
1411
|
-
final isDark = theme.mode == ThemeMode.dark;
|
|
1412
|
-
return _RailIconSlot(
|
|
1413
|
-
child: _RailIconBtn(
|
|
1414
|
-
icon: isDark ? KasyIcons.lightMode : KasyIcons.darkMode,
|
|
1415
|
-
tooltip: isDark ? 'Light mode' : 'Dark mode',
|
|
1416
|
-
onTap: theme.toggle,
|
|
1417
|
-
hapticEnabled: false,
|
|
1418
|
-
),
|
|
1419
|
-
);
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
1424
|
-
// COLLAPSED SIDEBAR — icon-only rail for medium breakpoint (768–1024 px)
|
|
1425
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
1426
|
-
|
|
1427
|
-
const double _kCollapsedWidth = 64.0;
|
|
1428
|
-
const double _kRailIconSize = 40.0;
|
|
1429
|
-
|
|
1430
|
-
/// Entry point for the collapsed icon-only sidebar.
|
|
1431
|
-
Widget kasySidebarCollapsedBuilder(
|
|
1432
|
-
List<BartMenuRoute> routes,
|
|
1433
|
-
OnTapItem onTapItem,
|
|
1434
|
-
ValueNotifier<int> currentItem,
|
|
1435
|
-
) {
|
|
1436
|
-
return _KasySidebarCollapsedRoot(
|
|
1437
|
-
routes: routes,
|
|
1438
|
-
onTapItem: onTapItem,
|
|
1439
|
-
currentItem: currentItem,
|
|
1440
|
-
);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1444
|
-
// Root
|
|
1445
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1446
|
-
|
|
1447
|
-
class _KasySidebarCollapsedRoot extends StatelessWidget {
|
|
1448
|
-
final List<BartMenuRoute> routes;
|
|
1449
|
-
final OnTapItem onTapItem;
|
|
1450
|
-
final ValueNotifier<int> currentItem;
|
|
1451
|
-
|
|
1452
|
-
const _KasySidebarCollapsedRoot({
|
|
1453
|
-
required this.routes,
|
|
1454
|
-
required this.onTapItem,
|
|
1455
|
-
required this.currentItem,
|
|
1456
|
-
});
|
|
1457
|
-
|
|
1458
|
-
@override
|
|
1459
|
-
Widget build(BuildContext context) {
|
|
1460
|
-
return ValueListenableBuilder<int>(
|
|
1461
|
-
valueListenable: currentItem,
|
|
1462
|
-
builder: (_, currentIndex, child) => _KasySidebarCollapsed(
|
|
1463
|
-
routes: routes,
|
|
1464
|
-
currentIndex: currentIndex,
|
|
1465
|
-
onTapItem: onTapItem,
|
|
1466
|
-
),
|
|
1467
|
-
);
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1472
|
-
// Collapsed sidebar — stateful for ⌘K shortcut
|
|
1473
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1474
|
-
|
|
1475
|
-
class _KasySidebarCollapsed extends StatefulWidget {
|
|
1476
|
-
final List<BartMenuRoute> routes;
|
|
1477
|
-
final int currentIndex;
|
|
1478
|
-
final OnTapItem onTapItem;
|
|
1479
|
-
|
|
1480
|
-
const _KasySidebarCollapsed({
|
|
1481
|
-
required this.routes,
|
|
1482
|
-
required this.currentIndex,
|
|
1483
|
-
required this.onTapItem,
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
@override
|
|
1487
|
-
State<_KasySidebarCollapsed> createState() => _KasySidebarCollapsedState();
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
class _KasySidebarCollapsedState extends State<_KasySidebarCollapsed> {
|
|
1491
|
-
bool _searchOpen = false;
|
|
1492
|
-
|
|
1493
|
-
static const _projects = [
|
|
1494
|
-
(color: Color(0xFF34C759), name: 'Kasy Studio'),
|
|
1495
|
-
(color: Color(0xFF1E88E5), name: 'Design System'),
|
|
1496
|
-
(color: Color(0xFFF57F17), name: 'Components'),
|
|
1497
|
-
];
|
|
1498
|
-
|
|
1499
|
-
static const _placeholders = [
|
|
1500
|
-
(icon: KasyIcons.flash, label: 'Activity'),
|
|
1501
|
-
(icon: KasyIcons.note, label: 'Tasks'),
|
|
1502
|
-
(icon: KasyIcons.book, label: 'Reports'),
|
|
1503
|
-
(icon: KasyIcons.person, label: 'Team'),
|
|
1504
|
-
];
|
|
1505
|
-
|
|
1506
|
-
// ── Keyboard shortcut ────────────────────────────────────────────────────
|
|
1507
|
-
|
|
1508
|
-
@override
|
|
1509
|
-
void initState() {
|
|
1510
|
-
super.initState();
|
|
1511
|
-
HardwareKeyboard.instance.addHandler(_onKeyEvent);
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
@override
|
|
1515
|
-
void dispose() {
|
|
1516
|
-
HardwareKeyboard.instance.removeHandler(_onKeyEvent);
|
|
1517
|
-
super.dispose();
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
bool _onKeyEvent(KeyEvent event) {
|
|
1521
|
-
if (!mounted || event is! KeyDownEvent) return false;
|
|
1522
|
-
if (event.logicalKey != LogicalKeyboardKey.keyK) return false;
|
|
1523
|
-
final bool trigger = HardwareKeyboard.instance.isMetaPressed ||
|
|
1524
|
-
HardwareKeyboard.instance.isControlPressed;
|
|
1525
|
-
if (trigger) {
|
|
1526
|
-
_openSearch();
|
|
1527
|
-
return true;
|
|
1528
|
-
}
|
|
1529
|
-
return false;
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
void _openSearch() {
|
|
1533
|
-
if (_searchOpen || !mounted) return;
|
|
1534
|
-
setState(() => _searchOpen = true);
|
|
1535
|
-
showGeneralDialog<void>(
|
|
1536
|
-
context: context,
|
|
1537
|
-
barrierDismissible: true,
|
|
1538
|
-
barrierLabel: 'Dismiss',
|
|
1539
|
-
barrierColor: Colors.black.withValues(alpha: 0.32),
|
|
1540
|
-
transitionDuration: const Duration(milliseconds: 220),
|
|
1541
|
-
pageBuilder: (ctx, anim, secAnim) => Align(
|
|
1542
|
-
alignment: const Alignment(0, -0.18),
|
|
1543
|
-
child: _SearchModal(
|
|
1544
|
-
routes: widget.routes,
|
|
1545
|
-
onNavigate: (index) {
|
|
1546
|
-
Navigator.of(ctx, rootNavigator: true).pop();
|
|
1547
|
-
widget.onTapItem(index);
|
|
1548
|
-
},
|
|
1549
|
-
),
|
|
1550
|
-
),
|
|
1551
|
-
transitionBuilder: (ctx, anim, secAnim, child) {
|
|
1552
|
-
final curved =
|
|
1553
|
-
CurvedAnimation(parent: anim, curve: Curves.easeOutCubic);
|
|
1554
|
-
return FadeTransition(
|
|
1555
|
-
opacity: curved,
|
|
1556
|
-
child: ScaleTransition(
|
|
1557
|
-
scale: Tween<double>(begin: 0.95, end: 1.0).animate(curved),
|
|
1558
|
-
child: child,
|
|
1559
|
-
),
|
|
1560
|
-
);
|
|
1561
|
-
},
|
|
1562
|
-
).then((_) {
|
|
1563
|
-
if (mounted) setState(() => _searchOpen = false);
|
|
1564
|
-
});
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
// ── Build ─────────────────────────────────────────────────────────────────
|
|
1568
|
-
|
|
1569
|
-
@override
|
|
1570
|
-
Widget build(BuildContext context) {
|
|
1571
|
-
final colors = context.colors;
|
|
1572
|
-
|
|
1573
|
-
return Container(
|
|
1574
|
-
width: _kCollapsedWidth,
|
|
1575
|
-
decoration: BoxDecoration(
|
|
1576
|
-
color: colors.surface,
|
|
1577
|
-
border: Border(
|
|
1578
|
-
right: BorderSide(color: colors.outline.withValues(alpha: 0.4)),
|
|
1579
|
-
),
|
|
1580
|
-
),
|
|
1581
|
-
child: SafeArea(
|
|
1582
|
-
right: false,
|
|
1583
|
-
child: Column(
|
|
1584
|
-
children: [
|
|
1585
|
-
// ── Scrollable area ────────────────────────────────────────────
|
|
1586
|
-
Expanded(
|
|
1587
|
-
child: ScrollConfiguration(
|
|
1588
|
-
behavior: ScrollConfiguration.of(context)
|
|
1589
|
-
.copyWith(scrollbars: false),
|
|
1590
|
-
child: SingleChildScrollView(
|
|
1591
|
-
physics: const ClampingScrollPhysics(),
|
|
1592
|
-
child: Column(
|
|
1593
|
-
children: [
|
|
1594
|
-
const SizedBox(height: 16),
|
|
1595
|
-
|
|
1596
|
-
// Brand icon
|
|
1597
|
-
_RailIconSlot(
|
|
1598
|
-
child: Container(
|
|
1599
|
-
width: 34,
|
|
1600
|
-
height: 34,
|
|
1601
|
-
decoration: BoxDecoration(
|
|
1602
|
-
color: colors.primary,
|
|
1603
|
-
borderRadius: BorderRadius.circular(8),
|
|
1604
|
-
),
|
|
1605
|
-
child: const Icon(
|
|
1606
|
-
KasyIcons.assistant,
|
|
1607
|
-
size: 18,
|
|
1608
|
-
color: Colors.white,
|
|
1609
|
-
),
|
|
1610
|
-
),
|
|
1611
|
-
),
|
|
1612
|
-
const SizedBox(height: 12),
|
|
1613
|
-
|
|
1614
|
-
// Search
|
|
1615
|
-
_RailIconSlot(
|
|
1616
|
-
child: _RailIconBtn(
|
|
1617
|
-
icon: KasyIcons.search,
|
|
1618
|
-
tooltip: defaultTargetPlatform == TargetPlatform.macOS
|
|
1619
|
-
? 'Search ⌘K'
|
|
1620
|
-
: 'Search Ctrl K',
|
|
1621
|
-
onTap: _openSearch,
|
|
1622
|
-
),
|
|
1623
|
-
),
|
|
1624
|
-
const SizedBox(height: 8),
|
|
1625
|
-
const _SidebarDivider(),
|
|
1626
|
-
const SizedBox(height: 8),
|
|
1627
|
-
|
|
1628
|
-
// ── Nav items ──────────────────────────────────────
|
|
1629
|
-
...widget.routes.asMap().entries.map((e) {
|
|
1630
|
-
final hasBuilder = e.value.iconBuilder != null;
|
|
1631
|
-
final icon = e.value.icon ?? KasyIcons.notification;
|
|
1632
|
-
final label = e.value.label ?? '';
|
|
1633
|
-
final isActive = widget.currentIndex == e.key;
|
|
1634
|
-
|
|
1635
|
-
if (hasBuilder) {
|
|
1636
|
-
// Notification item — show dot badge
|
|
1637
|
-
return Padding(
|
|
1638
|
-
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
1639
|
-
child: _CollapsedNotificationBtn(
|
|
1640
|
-
isActive: isActive,
|
|
1641
|
-
label: label,
|
|
1642
|
-
onTap: () => widget.onTapItem(e.key),
|
|
1643
|
-
),
|
|
1644
|
-
);
|
|
1645
|
-
}
|
|
1646
|
-
return Padding(
|
|
1647
|
-
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
1648
|
-
child: _RailNavBtn(
|
|
1649
|
-
icon: icon,
|
|
1650
|
-
tooltip: label,
|
|
1651
|
-
isActive: isActive,
|
|
1652
|
-
onTap: () => widget.onTapItem(e.key),
|
|
1653
|
-
),
|
|
1654
|
-
);
|
|
1655
|
-
}),
|
|
1656
|
-
const SizedBox(height: 8),
|
|
1657
|
-
const _SidebarDivider(),
|
|
1658
|
-
const SizedBox(height: 8),
|
|
1659
|
-
|
|
1660
|
-
// ── Workspace icons ────────────────────────────────
|
|
1661
|
-
..._placeholders.map(
|
|
1662
|
-
(p) => _RailIconSlot(
|
|
1663
|
-
child: _RailIconBtn(
|
|
1664
|
-
icon: p.icon,
|
|
1665
|
-
tooltip: p.label,
|
|
1666
|
-
onTap: () {},
|
|
1667
|
-
hapticEnabled: false,
|
|
1668
|
-
),
|
|
1669
|
-
),
|
|
1670
|
-
),
|
|
1671
|
-
const SizedBox(height: 12),
|
|
1672
|
-
const _SidebarDivider(),
|
|
1673
|
-
const SizedBox(height: 10),
|
|
1674
|
-
|
|
1675
|
-
// ── Project dots ───────────────────────────────────
|
|
1676
|
-
..._projects.map(
|
|
1677
|
-
(p) => _RailProjectDot(
|
|
1678
|
-
color: p.color,
|
|
1679
|
-
name: p.name,
|
|
1680
|
-
),
|
|
1681
|
-
),
|
|
1682
|
-
const SizedBox(height: 12),
|
|
1683
|
-
],
|
|
1684
|
-
),
|
|
1685
|
-
),
|
|
1686
|
-
),
|
|
1687
|
-
),
|
|
1688
|
-
|
|
1689
|
-
// ── Theme toggle + user avatar footer ─────────────────────────
|
|
1690
|
-
const _RailThemeToggle(),
|
|
1691
|
-
const SizedBox(height: 4),
|
|
1692
|
-
const _SidebarDivider(),
|
|
1693
|
-
const _CollapsedUserFooter(),
|
|
1694
|
-
],
|
|
1695
|
-
),
|
|
1696
|
-
),
|
|
1697
|
-
);
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1702
|
-
// Rail helpers
|
|
1703
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1704
|
-
|
|
1705
|
-
/// Centers a child horizontally within the 64 px rail column.
|
|
1706
|
-
class _RailIconSlot extends StatelessWidget {
|
|
1707
|
-
final Widget child;
|
|
1708
|
-
const _RailIconSlot({required this.child});
|
|
1709
|
-
|
|
1710
|
-
@override
|
|
1711
|
-
Widget build(BuildContext context) {
|
|
1712
|
-
return Padding(
|
|
1713
|
-
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
1714
|
-
child: Center(child: child),
|
|
1715
|
-
);
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
/// Generic icon-only button: tooltip + hover.
|
|
1720
|
-
class _RailIconBtn extends StatelessWidget {
|
|
1721
|
-
final IconData icon;
|
|
1722
|
-
final String tooltip;
|
|
1723
|
-
final VoidCallback onTap;
|
|
1724
|
-
final bool hapticEnabled;
|
|
1725
|
-
|
|
1726
|
-
const _RailIconBtn({
|
|
1727
|
-
required this.icon,
|
|
1728
|
-
required this.tooltip,
|
|
1729
|
-
required this.onTap,
|
|
1730
|
-
this.hapticEnabled = true,
|
|
1731
|
-
});
|
|
1732
|
-
|
|
1733
|
-
@override
|
|
1734
|
-
Widget build(BuildContext context) {
|
|
1735
|
-
final colors = context.colors;
|
|
1736
|
-
return Tooltip(
|
|
1737
|
-
message: tooltip,
|
|
1738
|
-
preferBelow: false,
|
|
1739
|
-
waitDuration: const Duration(milliseconds: 400),
|
|
1740
|
-
child: KasyHover(
|
|
1741
|
-
onTap: onTap,
|
|
1742
|
-
hapticEnabled: hapticEnabled,
|
|
1743
|
-
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
1744
|
-
child: SizedBox(
|
|
1745
|
-
width: _kRailIconSize,
|
|
1746
|
-
height: _kRailIconSize,
|
|
1747
|
-
child: Icon(
|
|
1748
|
-
icon,
|
|
1749
|
-
size: 19,
|
|
1750
|
-
color: colors.onSurface.withValues(alpha: 0.6),
|
|
1751
|
-
),
|
|
1752
|
-
),
|
|
1753
|
-
),
|
|
1754
|
-
);
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
/// Nav button with a left accent bar that appears when active.
|
|
1759
|
-
class _RailNavBtn extends StatelessWidget {
|
|
1760
|
-
final IconData icon;
|
|
1761
|
-
final String tooltip;
|
|
1762
|
-
final bool isActive;
|
|
1763
|
-
final VoidCallback onTap;
|
|
1764
|
-
|
|
1765
|
-
const _RailNavBtn({
|
|
1766
|
-
required this.icon,
|
|
1767
|
-
required this.tooltip,
|
|
1768
|
-
required this.isActive,
|
|
1769
|
-
required this.onTap,
|
|
1770
|
-
});
|
|
1771
|
-
|
|
1772
|
-
@override
|
|
1773
|
-
Widget build(BuildContext context) {
|
|
1774
|
-
final colors = context.colors;
|
|
1775
|
-
const double slotHeight = _kRailIconSize + 4;
|
|
1776
|
-
|
|
1777
|
-
return Tooltip(
|
|
1778
|
-
message: tooltip,
|
|
1779
|
-
preferBelow: false,
|
|
1780
|
-
waitDuration: const Duration(milliseconds: 400),
|
|
1781
|
-
child: SizedBox(
|
|
1782
|
-
width: _kCollapsedWidth,
|
|
1783
|
-
height: slotHeight,
|
|
1784
|
-
child: Stack(
|
|
1785
|
-
alignment: Alignment.center,
|
|
1786
|
-
children: [
|
|
1787
|
-
// Left accent bar
|
|
1788
|
-
Positioned(
|
|
1789
|
-
left: 0,
|
|
1790
|
-
top: (slotHeight - 20) / 2,
|
|
1791
|
-
child: AnimatedContainer(
|
|
1792
|
-
duration: const Duration(milliseconds: 160),
|
|
1793
|
-
width: 3,
|
|
1794
|
-
height: 20,
|
|
1795
|
-
decoration: BoxDecoration(
|
|
1796
|
-
color: isActive ? colors.primary : Colors.transparent,
|
|
1797
|
-
borderRadius: const BorderRadius.only(
|
|
1798
|
-
topRight: Radius.circular(2),
|
|
1799
|
-
bottomRight: Radius.circular(2),
|
|
1800
|
-
),
|
|
1801
|
-
),
|
|
1802
|
-
),
|
|
1803
|
-
),
|
|
1804
|
-
// Icon with hover
|
|
1805
|
-
KasyHover(
|
|
1806
|
-
onTap: onTap,
|
|
1807
|
-
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
1808
|
-
child: AnimatedContainer(
|
|
1809
|
-
duration: const Duration(milliseconds: 160),
|
|
1810
|
-
curve: Curves.easeOut,
|
|
1811
|
-
width: _kRailIconSize,
|
|
1812
|
-
height: _kRailIconSize,
|
|
1813
|
-
decoration: BoxDecoration(
|
|
1814
|
-
color: isActive
|
|
1815
|
-
? colors.surfacePrimarySoft
|
|
1816
|
-
: Colors.transparent,
|
|
1817
|
-
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
1818
|
-
),
|
|
1819
|
-
child: Icon(
|
|
1820
|
-
icon,
|
|
1821
|
-
size: 19,
|
|
1822
|
-
color: isActive
|
|
1823
|
-
? colors.primary
|
|
1824
|
-
: colors.onSurface.withValues(alpha: 0.6),
|
|
1825
|
-
),
|
|
1826
|
-
),
|
|
1827
|
-
),
|
|
1828
|
-
],
|
|
1829
|
-
),
|
|
1830
|
-
),
|
|
1831
|
-
);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
/// Notification button with a live unread dot indicator.
|
|
1836
|
-
class _CollapsedNotificationBtn extends ConsumerWidget {
|
|
1837
|
-
final bool isActive;
|
|
1838
|
-
final String label;
|
|
1839
|
-
final VoidCallback onTap;
|
|
1840
|
-
|
|
1841
|
-
const _CollapsedNotificationBtn({
|
|
1842
|
-
required this.isActive,
|
|
1843
|
-
required this.label,
|
|
1844
|
-
required this.onTap,
|
|
1845
|
-
});
|
|
1846
|
-
|
|
1847
|
-
@override
|
|
1848
|
-
Widget build(BuildContext context, WidgetRef ref) {
|
|
1849
|
-
final colors = context.colors;
|
|
1850
|
-
final String? userId =
|
|
1851
|
-
ref.watch(userStateNotifierProvider).user.idOrNull;
|
|
1852
|
-
const double slotHeight = _kRailIconSize + 4;
|
|
1853
|
-
|
|
1854
|
-
return Tooltip(
|
|
1855
|
-
message: label,
|
|
1856
|
-
preferBelow: false,
|
|
1857
|
-
waitDuration: const Duration(milliseconds: 400),
|
|
1858
|
-
child: SizedBox(
|
|
1859
|
-
width: _kCollapsedWidth,
|
|
1860
|
-
height: slotHeight,
|
|
1861
|
-
child: Stack(
|
|
1862
|
-
alignment: Alignment.center,
|
|
1863
|
-
children: [
|
|
1864
|
-
// Left accent bar
|
|
1865
|
-
Positioned(
|
|
1866
|
-
left: 0,
|
|
1867
|
-
top: (slotHeight - 20) / 2,
|
|
1868
|
-
child: AnimatedContainer(
|
|
1869
|
-
duration: const Duration(milliseconds: 160),
|
|
1870
|
-
width: 3,
|
|
1871
|
-
height: 20,
|
|
1872
|
-
decoration: BoxDecoration(
|
|
1873
|
-
color: isActive ? colors.primary : Colors.transparent,
|
|
1874
|
-
borderRadius: const BorderRadius.only(
|
|
1875
|
-
topRight: Radius.circular(2),
|
|
1876
|
-
bottomRight: Radius.circular(2),
|
|
1877
|
-
),
|
|
1878
|
-
),
|
|
1879
|
-
),
|
|
1880
|
-
),
|
|
1881
|
-
// Icon + dot badge
|
|
1882
|
-
KasyHover(
|
|
1883
|
-
onTap: onTap,
|
|
1884
|
-
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
1885
|
-
child: AnimatedContainer(
|
|
1886
|
-
duration: const Duration(milliseconds: 160),
|
|
1887
|
-
curve: Curves.easeOut,
|
|
1888
|
-
width: _kRailIconSize,
|
|
1889
|
-
height: _kRailIconSize,
|
|
1890
|
-
decoration: BoxDecoration(
|
|
1891
|
-
color:
|
|
1892
|
-
isActive ? colors.surfacePrimarySoft : Colors.transparent,
|
|
1893
|
-
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
1894
|
-
),
|
|
1895
|
-
child: Stack(
|
|
1896
|
-
clipBehavior: Clip.none,
|
|
1897
|
-
alignment: Alignment.center,
|
|
1898
|
-
children: [
|
|
1899
|
-
Icon(
|
|
1900
|
-
KasyIcons.notification,
|
|
1901
|
-
size: 19,
|
|
1902
|
-
color: isActive
|
|
1903
|
-
? colors.primary
|
|
1904
|
-
: colors.onSurface.withValues(alpha: 0.6),
|
|
1905
|
-
),
|
|
1906
|
-
if (userId != null)
|
|
1907
|
-
StreamBuilder<int>(
|
|
1908
|
-
stream: ref
|
|
1909
|
-
.read(notificationRepositoryProvider)
|
|
1910
|
-
.listenToUnreadNotificationsCount(userId),
|
|
1911
|
-
builder: (_, snapshot) {
|
|
1912
|
-
final int count = snapshot.data ?? 0;
|
|
1913
|
-
if (count == 0) return const SizedBox.shrink();
|
|
1914
|
-
return Positioned(
|
|
1915
|
-
top: 7,
|
|
1916
|
-
right: 7,
|
|
1917
|
-
child: Container(
|
|
1918
|
-
width: 7,
|
|
1919
|
-
height: 7,
|
|
1920
|
-
decoration: BoxDecoration(
|
|
1921
|
-
color: colors.error,
|
|
1922
|
-
shape: BoxShape.circle,
|
|
1923
|
-
border: Border.all(
|
|
1924
|
-
color: colors.surface,
|
|
1925
|
-
width: 1.2,
|
|
1926
|
-
),
|
|
1927
|
-
),
|
|
1928
|
-
),
|
|
1929
|
-
);
|
|
1930
|
-
},
|
|
1931
|
-
),
|
|
1932
|
-
],
|
|
1933
|
-
),
|
|
1934
|
-
),
|
|
1935
|
-
),
|
|
1936
|
-
],
|
|
1937
|
-
),
|
|
1938
|
-
),
|
|
1939
|
-
);
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
/// Small colored dot for the projects section in collapsed mode.
|
|
1944
|
-
class _RailProjectDot extends StatelessWidget {
|
|
1945
|
-
final Color color;
|
|
1946
|
-
final String name;
|
|
1947
|
-
|
|
1948
|
-
const _RailProjectDot({required this.color, required this.name});
|
|
1949
|
-
|
|
1950
|
-
@override
|
|
1951
|
-
Widget build(BuildContext context) {
|
|
1952
|
-
return _RailIconSlot(
|
|
1953
|
-
child: Tooltip(
|
|
1954
|
-
message: name,
|
|
1955
|
-
preferBelow: false,
|
|
1956
|
-
waitDuration: const Duration(milliseconds: 400),
|
|
1957
|
-
child: KasyHover(
|
|
1958
|
-
onTap: () {},
|
|
1959
|
-
hapticEnabled: false,
|
|
1960
|
-
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
1961
|
-
child: SizedBox(
|
|
1962
|
-
width: _kRailIconSize,
|
|
1963
|
-
height: _kRailIconSize,
|
|
1964
|
-
child: Center(
|
|
1965
|
-
child: Container(
|
|
1966
|
-
width: 10,
|
|
1967
|
-
height: 10,
|
|
1968
|
-
decoration: BoxDecoration(
|
|
1969
|
-
color: color,
|
|
1970
|
-
shape: BoxShape.circle,
|
|
1971
|
-
boxShadow: [
|
|
1972
|
-
BoxShadow(
|
|
1973
|
-
color: color.withValues(alpha: 0.5),
|
|
1974
|
-
blurRadius: 5,
|
|
1975
|
-
offset: const Offset(0, 1),
|
|
1976
|
-
),
|
|
1977
|
-
],
|
|
1978
|
-
),
|
|
1979
|
-
),
|
|
1980
|
-
),
|
|
1981
|
-
),
|
|
1982
|
-
),
|
|
1983
|
-
),
|
|
1984
|
-
);
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
/// User avatar in the collapsed rail footer.
|
|
1989
|
-
class _CollapsedUserFooter extends ConsumerWidget {
|
|
1990
|
-
const _CollapsedUserFooter();
|
|
1991
|
-
|
|
1992
|
-
@override
|
|
1993
|
-
Widget build(BuildContext context, WidgetRef ref) {
|
|
1994
|
-
final user = ref.watch(userStateNotifierProvider).user;
|
|
1995
|
-
final String displayName = user is AuthenticatedUserData
|
|
1996
|
-
? (user.name ?? user.email.split('@').first)
|
|
1997
|
-
: 'Guest';
|
|
1998
|
-
final String initials =
|
|
1999
|
-
displayName.isNotEmpty ? displayName[0].toUpperCase() : 'G';
|
|
2000
|
-
|
|
2001
|
-
return KasyHover(
|
|
2002
|
-
onTap: () {},
|
|
2003
|
-
hapticEnabled: false,
|
|
2004
|
-
child: Padding(
|
|
2005
|
-
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
2006
|
-
child: Center(
|
|
2007
|
-
child: Tooltip(
|
|
2008
|
-
message: displayName,
|
|
2009
|
-
preferBelow: false,
|
|
2010
|
-
waitDuration: const Duration(milliseconds: 400),
|
|
2011
|
-
child: KasyAvatar(
|
|
2012
|
-
size: KasyAvatarSize.small,
|
|
2013
|
-
diameter: 32,
|
|
2014
|
-
initials: initials,
|
|
2015
|
-
),
|
|
2016
|
-
),
|
|
2017
|
-
),
|
|
2018
|
-
),
|
|
2019
|
-
);
|
|
2020
|
-
}
|
|
2021
|
-
}
|