kasy-cli 1.37.1 → 1.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/lib/scaffold/CHANGELOG.json +23 -0
  2. package/lib/scaffold/backends/api/patch/README.md +15 -0
  3. package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
  4. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
  5. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
  6. package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
  7. package/lib/scaffold/backends/patch-base-hashes.json +6 -6
  8. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  9. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
  10. package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
  11. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
  12. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
  13. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  14. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
  15. package/lib/scaffold/shared/generator-utils.js +12 -6
  16. package/package.json +1 -1
  17. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  18. package/templates/firebase/AGENTS.md +7 -1
  19. package/templates/firebase/DESIGN_SYSTEM.md +35 -8
  20. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  21. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  22. package/templates/firebase/assets/icons/facebook.svg +49 -0
  23. package/templates/firebase/assets/icons/google.svg +1 -0
  24. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  25. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  26. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  27. package/templates/firebase/lib/components/components.dart +1 -1
  28. package/templates/firebase/lib/components/kasy_app_bar.dart +361 -20
  29. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
  30. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  31. package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
  32. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  33. package/templates/firebase/lib/components/kasy_sidebar.dart +412 -31
  34. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  35. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  36. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +29 -231
  37. package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
  38. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +19 -9
  39. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  40. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  41. package/templates/firebase/lib/core/data/api/user_api.dart +15 -0
  42. package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
  43. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
  44. package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
  45. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  46. package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
  47. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +18 -35
  48. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  49. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  50. package/templates/firebase/lib/core/states/user_state_notifier.dart +69 -1
  51. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  52. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  53. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
  54. package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
  55. package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
  56. package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
  57. package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
  58. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +547 -483
  59. package/templates/firebase/lib/core/web_viewport_scale.dart +64 -35
  60. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  61. package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
  62. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  63. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  64. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  65. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  66. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  67. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  68. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  69. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  70. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  71. package/templates/firebase/lib/features/home/home_components_page.dart +264 -126
  72. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +231 -57
  73. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  74. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  75. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  76. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +118 -57
  77. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  78. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +19 -4
  79. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  80. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  81. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  82. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
  83. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  84. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  85. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  86. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  87. package/templates/firebase/lib/features/settings/settings_page.dart +99 -65
  88. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1379 -422
  89. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
  90. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  91. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  92. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +404 -149
  93. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +24 -31
  94. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  95. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +77 -95
  96. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  97. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  98. package/templates/firebase/lib/i18n/en.i18n.json +749 -698
  99. package/templates/firebase/lib/i18n/es.i18n.json +749 -698
  100. package/templates/firebase/lib/i18n/pt.i18n.json +749 -698
  101. package/templates/firebase/lib/main.dart +20 -7
  102. package/templates/firebase/lib/router.dart +70 -46
  103. package/templates/firebase/pubspec.yaml +2 -1
  104. package/templates/firebase/test/admin_shell_chrome_test.dart +110 -0
  105. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  106. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  107. package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
  108. package/templates/firebase/tool/design_check.dart +9 -0
  109. package/templates/firebase/assets/icons/apple.png +0 -0
  110. package/templates/firebase/assets/icons/facebook.png +0 -0
  111. package/templates/firebase/assets/icons/google.png +0 -0
  112. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  113. package/templates/firebase/lib/components/kasy_web_header.dart +0 -210
  114. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  115. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  116. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  117. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  118. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -169
  119. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
  120. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
@@ -1,210 +0,0 @@
1
- /// Application top header for the **web desktop breakpoint** (viewport ≥ 1024px).
2
- ///
3
- /// This is the *application* chrome (global search, quick-create, notifications,
4
- /// profile) — distinct from [KasyAppBar], which is *page* chrome (title / back /
5
- /// theme) used on phone and tablet. On desktop the sidebar handles navigation,
6
- /// so this header carries global actions instead of a title or back button.
7
- ///
8
- /// Composed entirely from existing kit widgets: a [KasyTextField] search box
9
- /// (fixed 220px), a ghost [KasyButton.iconOnly] for notifications, a neutral
10
- /// pill [KasyButton] for create, and a gradient [KasyAvatar].
11
- ///
12
- /// Barrel: [components.dart].
13
- library;
14
-
15
- import 'package:flutter/material.dart';
16
- import 'package:kasy_kit/components/kasy_avatar.dart';
17
- import 'package:kasy_kit/components/kasy_avatar_presets.dart';
18
- import 'package:kasy_kit/components/kasy_button.dart';
19
- import 'package:kasy_kit/components/kasy_text_field.dart';
20
- import 'package:kasy_kit/core/theme/theme.dart';
21
-
22
- /// Total header height (matches the design: 36px content band + 16px top/bottom).
23
- const double kasyWebHeaderHeight = 68;
24
-
25
- /// Fixed width of the leading search field (matches the design).
26
- const double kasyWebHeaderSearchWidth = 220;
27
-
28
- /// Desktop application header. Place it at the top of the content area (to the
29
- /// right of the sidebar) on viewports ≥ 1024px.
30
- class KasyWebHeader extends StatelessWidget {
31
- /// Controller for the search field. Optional — omit for a display-only header.
32
- final TextEditingController? searchController;
33
-
34
- /// Placeholder shown in the search field.
35
- final String searchHint;
36
-
37
- /// Called as the user types in the search field.
38
- final ValueChanged<String>? onSearchChanged;
39
-
40
- /// Called when the search field is submitted (Enter).
41
- final ValueChanged<String>? onSearchSubmitted;
42
-
43
- /// Notifications (bell) action. When null the bell is disabled.
44
- final VoidCallback? onNotifications;
45
-
46
- /// Shows the unread dot on the notifications bell.
47
- final bool showNotificationBadge;
48
-
49
- /// Primary quick-create action. When null the button is disabled.
50
- final VoidCallback? onCreate;
51
-
52
- /// Label for the create button.
53
- final String createLabel;
54
-
55
- /// Gradient used for the profile avatar fallback (when [avatar] is null).
56
- final KasyAvatarGradientData avatarGradient;
57
-
58
- /// Custom avatar widget — pass the signed-in user's avatar (e.g.
59
- /// `KasyUserAvatar`) to show their real photo. When null (and [showAvatar] is
60
- /// true), a gradient-fill avatar is shown instead.
61
- final Widget? avatar;
62
-
63
- /// Whether the profile avatar is shown at all. Set false for a header that
64
- /// carries no account chip (e.g. when the sidebar already owns the profile).
65
- final bool showAvatar;
66
-
67
- /// Profile avatar tap (open menu / profile). When null the avatar is inert.
68
- final VoidCallback? onAvatarTap;
69
-
70
- /// Theme toggle. When set, a sun/moon ghost button is shown before the bell
71
- /// (on desktop the web header replaces the app bar's theme toggle).
72
- final VoidCallback? onToggleTheme;
73
-
74
- const KasyWebHeader({
75
- super.key,
76
- this.searchController,
77
- this.searchHint = 'Search...',
78
- this.onSearchChanged,
79
- this.onSearchSubmitted,
80
- this.onNotifications,
81
- this.showNotificationBadge = false,
82
- this.onCreate,
83
- this.createLabel = 'Create',
84
- this.avatarGradient = KasyAvatarGradients.orange,
85
- this.avatar,
86
- this.showAvatar = true,
87
- this.onAvatarTap,
88
- this.onToggleTheme,
89
- });
90
-
91
- @override
92
- Widget build(BuildContext context) {
93
- final KasyColors c = context.colors;
94
- return DecoratedBox(
95
- decoration: BoxDecoration(
96
- // Matches KasySidebar exactly: surface fill + the same `border` hairline
97
- // used by the sidebar's vertical edge line, at the same 0.5px width — so
98
- // the header's bottom border and the sidebar's top divider read as one
99
- // continuous line across the whole chrome in light and dark.
100
- color: c.surface,
101
- border: Border(
102
- bottom: BorderSide(color: c.border, width: 0.5),
103
- ),
104
- ),
105
- // minHeight (not a fixed height) keeps the band at 68 while letting the
106
- // search field size naturally, so it never overflows the toolbar row.
107
- child: ConstrainedBox(
108
- constraints: const BoxConstraints(minHeight: kasyWebHeaderHeight),
109
- child: Padding(
110
- // Design gutters: 20px horizontal; vertical breathes around the field.
111
- padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
112
- child: Row(
113
- children: [
114
- SizedBox(
115
- width: kasyWebHeaderSearchWidth,
116
- child: _buildSearch(context),
117
- ),
118
- const Spacer(),
119
- if (onToggleTheme != null) ...[
120
- _buildThemeToggle(context),
121
- const SizedBox(width: KasySpacing.md),
122
- ],
123
- _buildNotifications(context),
124
- const SizedBox(width: KasySpacing.md),
125
- KasyButton(
126
- label: createLabel,
127
- variant: KasyButtonVariant.neutral,
128
- size: KasyButtonSize.small,
129
- onPressed: onCreate,
130
- ),
131
- if (showAvatar) ...[
132
- const SizedBox(width: KasySpacing.md),
133
- avatar ??
134
- KasyAvatar.gradientFill(
135
- size: KasyAvatarSize.small,
136
- diameter: 36,
137
- gradient: avatarGradient,
138
- showShadow: false,
139
- onTap: onAvatarTap,
140
- ),
141
- ],
142
- ],
143
- ),
144
- ),
145
- ),
146
- );
147
- }
148
-
149
- Widget _buildSearch(BuildContext context) {
150
- return KasyTextField(
151
- variant: KasyTextFieldVariant.flat,
152
- controller: searchController,
153
- hint: searchHint,
154
- onChanged: onSearchChanged,
155
- onSubmitted: onSearchSubmitted,
156
- prefix: Icon(
157
- KasyIcons.search,
158
- size: KasyIconSize.md,
159
- color: context.colors.muted,
160
- ),
161
- contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
162
- );
163
- }
164
-
165
- Widget _buildThemeToggle(BuildContext context) {
166
- final bool isDark = Theme.of(context).brightness == Brightness.dark;
167
- return KasyButton.iconOnly(
168
- icon: isDark ? KasyIcons.lightMode : KasyIcons.darkMode,
169
- variant: KasyButtonVariant.ghost,
170
- size: KasyButtonSize.small,
171
- iconOnlyLayoutExtent: 36,
172
- iconGlyphSize: KasyIconSize.md,
173
- onPressed: onToggleTheme,
174
- semanticLabel: isDark ? 'Light mode' : 'Dark mode',
175
- );
176
- }
177
-
178
- Widget _buildNotifications(BuildContext context) {
179
- final KasyColors c = context.colors;
180
- final Widget bell = KasyButton.iconOnly(
181
- icon: KasyIcons.notification,
182
- variant: KasyButtonVariant.ghost,
183
- size: KasyButtonSize.small,
184
- iconOnlyLayoutExtent: 36,
185
- iconGlyphSize: KasyIconSize.md,
186
- onPressed: onNotifications,
187
- semanticLabel: 'Notifications',
188
- );
189
- if (!showNotificationBadge) return bell;
190
- return Stack(
191
- clipBehavior: Clip.none,
192
- children: [
193
- bell,
194
- Positioned(
195
- top: 8,
196
- right: 8,
197
- child: Container(
198
- width: 8,
199
- height: 8,
200
- decoration: BoxDecoration(
201
- color: c.error,
202
- shape: BoxShape.circle,
203
- border: Border.all(color: c.background, width: 1.5),
204
- ),
205
- ),
206
- ),
207
- ],
208
- );
209
- }
210
- }
@@ -1,19 +0,0 @@
1
- import 'package:flutter/material.dart';
2
- import 'package:flutter_riverpod/flutter_riverpod.dart';
3
- import 'package:kasy_kit/features/authentication/providers/models/signin_state.dart';
4
- import 'package:kasy_kit/features/authentication/providers/signin_state_provider.dart';
5
- import 'package:kasy_kit/features/authentication/ui/widgets/round_signin.dart';
6
-
7
- class AppleSigninComponent extends ConsumerWidget {
8
- const AppleSigninComponent({super.key});
9
-
10
- @override
11
- Widget build(BuildContext context, WidgetRef ref) {
12
- // watch keeps the provider alive during async sign-in
13
- final state = ref.watch(signinStateProvider);
14
- final isSending = state is SigninStateSending;
15
- return SocialSigninButton.apple(
16
- () { if (!isSending) ref.read(signinStateProvider.notifier).signinWithApple(); },
17
- );
18
- }
19
- }
@@ -1,32 +0,0 @@
1
- import 'package:flutter/material.dart';
2
- import 'package:flutter_riverpod/flutter_riverpod.dart';
3
- import 'package:kasy_kit/features/authentication/providers/models/signin_state.dart';
4
- import 'package:kasy_kit/features/authentication/providers/signin_state_provider.dart';
5
- import 'package:kasy_kit/features/authentication/ui/widgets/round_signin.dart';
6
-
7
- class GoogleSignInComponent extends ConsumerWidget {
8
- const GoogleSignInComponent({super.key});
9
-
10
- @override
11
- Widget build(BuildContext context, WidgetRef ref) {
12
- // watch keeps the provider alive during async sign-in
13
- final state = ref.watch(signinStateProvider);
14
- final isSending = state is SigninStateSending;
15
- return SocialSigninButton.google(
16
- () { if (!isSending) ref.read(signinStateProvider.notifier).signinWithGoogle(); },
17
- );
18
- }
19
- }
20
-
21
- class GooglePlayGamesSignInComponent extends ConsumerWidget {
22
- const GooglePlayGamesSignInComponent({super.key});
23
-
24
- @override
25
- Widget build(BuildContext context, WidgetRef ref) {
26
- final state = ref.watch(signinStateProvider);
27
- final isSending = state is SigninStateSending;
28
- return SocialSigninButton.googlePlayGames(
29
- () { if (!isSending) ref.read(signinStateProvider.notifier).signinWithGooglePlayGames(); },
30
- );
31
- }
32
- }
@@ -1,73 +0,0 @@
1
- import 'package:flutter/material.dart';
2
- import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
3
- import 'package:kasy_kit/core/theme/theme.dart';
4
-
5
- typedef SocialSigninCallback = void Function();
6
-
7
- class SocialSigninButton extends StatelessWidget {
8
- final Image iconImage;
9
- final SocialSigninCallback? onPressed;
10
-
11
- const SocialSigninButton({
12
- super.key,
13
- required this.iconImage,
14
- required this.onPressed,
15
- });
16
-
17
- factory SocialSigninButton.google(SocialSigninCallback onPressed) {
18
- return SocialSigninButton(
19
- iconImage: Image.asset("assets/icons/google.png", width: 24),
20
- onPressed: onPressed,
21
- );
22
- }
23
-
24
- factory SocialSigninButton.googlePlayGames(SocialSigninCallback onPressed) {
25
- return SocialSigninButton(
26
- iconImage: Image.asset("assets/icons/google_play_games.png", width: 24),
27
- onPressed: onPressed,
28
- );
29
- }
30
-
31
- factory SocialSigninButton.facebook(SocialSigninCallback onPressed) {
32
- return SocialSigninButton(
33
- iconImage: Image.asset("assets/icons/facebook.png", width: 24),
34
- onPressed: onPressed,
35
- );
36
- }
37
-
38
- factory SocialSigninButton.apple(SocialSigninCallback onPressed) {
39
- return SocialSigninButton(
40
- iconImage: Image.asset("assets/icons/apple.png", width: 24),
41
- onPressed: onPressed,
42
- );
43
- }
44
-
45
- @override
46
- Widget build(BuildContext context) {
47
- return Container(
48
- width: 56,
49
- decoration: BoxDecoration(
50
- border: Border.all(color: context.colors.outline),
51
- shape: BoxShape.circle,
52
- ),
53
- child: RawMaterialButton(
54
- elevation: 0,
55
- focusElevation: 0,
56
- hoverElevation: 0,
57
- highlightElevation: 0,
58
- splashColor: Colors.transparent,
59
- highlightColor: Colors.transparent,
60
- hoverColor: Colors.transparent,
61
- focusColor: Colors.transparent,
62
- onPressed: () {
63
- KasyHaptics.medium(context);
64
- onPressed?.call();
65
- },
66
- shape: const CircleBorder(),
67
- fillColor: Colors.transparent,
68
- padding: const EdgeInsets.all(KasySpacing.md),
69
- child: iconImage,
70
- ),
71
- );
72
- }
73
- }
@@ -1,66 +0,0 @@
1
- import 'package:flutter/material.dart';
2
- import 'package:kasy_kit/core/theme/theme.dart';
3
- import 'package:kasy_kit/core/widgets/kasy_hover.dart';
4
-
5
- class AddFeatureButton extends StatelessWidget {
6
- final String title;
7
- final String description;
8
- final VoidCallback onPressed;
9
-
10
- const AddFeatureButton({
11
- super.key,
12
- required this.title,
13
- required this.description,
14
- required this.onPressed,
15
- });
16
-
17
- @override
18
- Widget build(BuildContext context) {
19
- return KasyHover(
20
- onTap: onPressed,
21
- borderRadius: KasyRadius.mdBorderRadius,
22
- pressColor: context.colors.onPrimary,
23
- focusable: true,
24
- child: Container(
25
- padding: const EdgeInsets.symmetric(
26
- horizontal: KasySpacing.lg,
27
- vertical: KasySpacing.smd,
28
- ),
29
- decoration: BoxDecoration(
30
- color: context.colors.primary,
31
- borderRadius: KasyRadius.mdBorderRadius,
32
- ),
33
- child: Row(
34
- children: [
35
- Expanded(
36
- child: Column(
37
- crossAxisAlignment: CrossAxisAlignment.start,
38
- spacing: KasySpacing.xs,
39
- children: [
40
- Text(
41
- title,
42
- style: context.textTheme.titleMedium?.copyWith(
43
- color: context.colors.onPrimary,
44
- ),
45
- ),
46
- Text(
47
- description,
48
- style: context.textTheme.bodySmall?.copyWith(
49
- color: context.colors.onPrimary.withValues(alpha: 0.72),
50
- ),
51
- ),
52
- ],
53
- ),
54
- ),
55
- const SizedBox(width: KasySpacing.smd),
56
- Icon(
57
- KasyIcons.northEast,
58
- size: KasyIconSize.rowTrailing,
59
- color: context.colors.onPrimary.withValues(alpha: 0.55),
60
- ),
61
- ],
62
- ),
63
- ),
64
- );
65
- }
66
- }
@@ -1,169 +0,0 @@
1
- import 'package:flutter/material.dart';
2
- import 'package:flutter_riverpod/flutter_riverpod.dart';
3
- import 'package:kasy_kit/core/theme/theme.dart';
4
- import 'package:kasy_kit/features/notifications/providers/models/notification.dart';
5
- import 'package:kasy_kit/features/notifications/providers/notifications_provider.dart';
6
- import 'package:kasy_kit/features/notifications/repositories/notifications_repository.dart';
7
- import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
8
- import 'package:kasy_kit/i18n/translations.g.dart';
9
-
10
- void showNotificationSettingsSheet(BuildContext context) {
11
- showModalBottomSheet<void>(
12
- context: context,
13
- useRootNavigator: true,
14
- backgroundColor: Colors.transparent,
15
- isScrollControlled: true,
16
- builder: (_) => const _NotificationSettingsSheet(),
17
- );
18
- }
19
-
20
- class _NotificationSettingsSheet extends ConsumerStatefulWidget {
21
- const _NotificationSettingsSheet();
22
-
23
- @override
24
- ConsumerState<_NotificationSettingsSheet> createState() =>
25
- _NotificationSettingsSheetState();
26
- }
27
-
28
- class _NotificationSettingsSheetState
29
- extends ConsumerState<_NotificationSettingsSheet> {
30
- NotificationPermission? _permission;
31
- bool _toggling = false;
32
-
33
- @override
34
- void initState() {
35
- super.initState();
36
- _loadPermission();
37
- }
38
-
39
- Future<void> _loadPermission() async {
40
- final p =
41
- await ref.read(notificationRepositoryProvider).getPermissionStatus();
42
- if (mounted) setState(() => _permission = p);
43
- }
44
-
45
- bool get _isEnabled => _permission is NotificationPermissionGranted;
46
-
47
- String _subtitle(BuildContext context) {
48
- final tr = context.t.notifications;
49
- return switch (_permission) {
50
- NotificationPermissionGranted() => tr.push_subtitle_enabled,
51
- NotificationPermissionDenied() => tr.push_subtitle_disabled,
52
- _ => tr.push_subtitle_waiting,
53
- };
54
- }
55
-
56
- Future<void> _onToggle(bool value) async {
57
- if (_toggling || _permission == null) return;
58
- setState(() => _toggling = true);
59
- await _permission!.maybeAsk();
60
- await _loadPermission();
61
- if (mounted) setState(() => _toggling = false);
62
- }
63
-
64
- Future<void> _markAllRead() async {
65
- await ref.read(notificationsProvider.notifier).readAll();
66
- if (mounted) Navigator.of(context).pop();
67
- }
68
-
69
- @override
70
- Widget build(BuildContext context) {
71
- final tr = context.t.notifications;
72
- final notificationsState = ref.watch(notificationsProvider);
73
- final hasUnread =
74
- notificationsState.value?.data.any((n) => !n.seen) ?? false;
75
-
76
- return Container(
77
- margin: EdgeInsets.only(
78
- left: KasySpacing.sm,
79
- right: KasySpacing.sm,
80
- bottom: MediaQuery.of(context).viewInsets.bottom +
81
- MediaQuery.of(context).padding.bottom +
82
- KasySpacing.sm,
83
- ),
84
- decoration: BoxDecoration(
85
- color: context.colors.background,
86
- borderRadius: KasyRadius.lgBorderRadius,
87
- border: Border.all(
88
- color: context.colors.outline.withValues(alpha: 0.5),
89
- ),
90
- ),
91
- child: Column(
92
- mainAxisSize: MainAxisSize.min,
93
- children: [
94
- // drag handle
95
- Padding(
96
- padding: const EdgeInsets.only(top: KasySpacing.smd),
97
- child: Container(
98
- width: 36,
99
- height: 4,
100
- decoration: BoxDecoration(
101
- color: context.colors.onSurface.withValues(alpha: 0.14),
102
- borderRadius: KasyRadius.fullBorderRadius,
103
- ),
104
- ),
105
- ),
106
- Padding(
107
- padding: const EdgeInsets.fromLTRB(
108
- KasySpacing.md,
109
- KasySpacing.md,
110
- KasySpacing.md,
111
- KasySpacing.smd,
112
- ),
113
- child: Column(
114
- crossAxisAlignment: CrossAxisAlignment.start,
115
- children: [
116
- // Push toggle row
117
- SettingsSwitchTile(
118
- icon: _isEnabled
119
- ? KasyIcons.notificationActive
120
- : KasyIcons.notification,
121
- title: tr.push_title,
122
- value: _isEnabled,
123
- iconBackgroundColor: _isEnabled
124
- ? const Color(0xFFFF6B35) // design-check: ignore — category accent
125
- : const Color(0xFF78909C), // design-check: ignore — category accent
126
- onChanged: _toggling ? (_) {} : _onToggle,
127
- ),
128
- // subtitle below
129
- Padding(
130
- padding: const EdgeInsets.only(
131
- left: 32 + KasySpacing.sm,
132
- bottom: KasySpacing.xs,
133
- ),
134
- child: _permission == null
135
- ? SizedBox(
136
- height: 13,
137
- width: 140,
138
- child: LinearProgressIndicator(
139
- borderRadius: BorderRadius.circular(4),
140
- color: context.colors.primary.withValues(alpha: 0.25),
141
- backgroundColor: context.colors.onSurface
142
- .withValues(alpha: 0.06),
143
- ),
144
- )
145
- : Text(
146
- _subtitle(context),
147
- style: context.textTheme.bodySmall?.copyWith(
148
- color: context.colors.muted,
149
- ),
150
- ),
151
- ),
152
- if (hasUnread) ...[
153
- const SettingsDivider(),
154
- const SizedBox(height: KasySpacing.xs),
155
- SettingsTile(
156
- icon: KasyIcons.checkCircle,
157
- title: tr.mark_all_read,
158
- iconBackgroundColor: context.colors.primary,
159
- onTap: _markAllRead,
160
- ),
161
- ],
162
- ],
163
- ),
164
- ),
165
- ],
166
- ),
167
- );
168
- }
169
- }
@@ -1,106 +0,0 @@
1
- import 'package:flutter/material.dart';
2
- import 'package:flutter_riverpod/flutter_riverpod.dart';
3
- import 'package:kasy_kit/core/theme/theme.dart';
4
- import 'package:kasy_kit/features/notifications/providers/models/notification.dart';
5
- import 'package:kasy_kit/features/notifications/repositories/notifications_repository.dart';
6
- import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
7
- import 'package:kasy_kit/i18n/translations.g.dart';
8
-
9
- class PushNotificationSwitcher extends ConsumerStatefulWidget {
10
- const PushNotificationSwitcher({super.key});
11
-
12
- @override
13
- ConsumerState<PushNotificationSwitcher> createState() =>
14
- _PushNotificationSwitcherState();
15
- }
16
-
17
- class _PushNotificationSwitcherState
18
- extends ConsumerState<PushNotificationSwitcher> {
19
- NotificationPermission? _permission;
20
- bool _loading = false;
21
-
22
- @override
23
- void initState() {
24
- super.initState();
25
- _loadPermission();
26
- }
27
-
28
- Future<void> _loadPermission() async {
29
- final p =
30
- await ref.read(notificationRepositoryProvider).getPermissionStatus();
31
- if (mounted) setState(() => _permission = p);
32
- }
33
-
34
- bool get _isEnabled => _permission is NotificationPermissionGranted;
35
-
36
- String _subtitle(BuildContext context) {
37
- final tr = context.t.notifications;
38
- return switch (_permission) {
39
- NotificationPermissionGranted() => tr.push_subtitle_enabled,
40
- NotificationPermissionDenied() => tr.push_subtitle_disabled,
41
- _ => tr.push_subtitle_waiting,
42
- };
43
- }
44
-
45
- Future<void> _onChanged(bool value) async {
46
- if (_loading || _permission == null) return;
47
- setState(() => _loading = true);
48
- await _permission!.maybeAsk();
49
- await _loadPermission();
50
- if (mounted) setState(() => _loading = false);
51
- }
52
-
53
- @override
54
- Widget build(BuildContext context) {
55
- final tr = context.t.notifications;
56
- return Container(
57
- padding: const EdgeInsets.symmetric(
58
- vertical: KasySpacing.smd,
59
- horizontal: KasySpacing.md,
60
- ),
61
- decoration: BoxDecoration(
62
- borderRadius: KasyRadius.smBorderRadius,
63
- color: context.colors.surface,
64
- ),
65
- child: Column(
66
- crossAxisAlignment: CrossAxisAlignment.start,
67
- children: [
68
- SettingsSwitchTile(
69
- icon: _isEnabled
70
- ? KasyIcons.notificationActive
71
- : KasyIcons.notification,
72
- title: tr.push_title,
73
- value: _isEnabled,
74
- iconBackgroundColor: _isEnabled
75
- ? const Color(0xFFFF6B35) // design-check: ignore — category accent
76
- : const Color(0xFF78909C), // design-check: ignore — category accent
77
- onChanged: _loading ? (_) {} : _onChanged,
78
- ),
79
- Padding(
80
- padding: const EdgeInsets.only(
81
- left: 32 + KasySpacing.sm,
82
- bottom: KasySpacing.xs,
83
- ),
84
- child: _permission == null
85
- ? SizedBox(
86
- height: 14,
87
- width: 120,
88
- child: LinearProgressIndicator(
89
- borderRadius: BorderRadius.circular(4),
90
- color: context.colors.primary.withValues(alpha: 0.3),
91
- backgroundColor:
92
- context.colors.onSurface.withValues(alpha: 0.06),
93
- ),
94
- )
95
- : Text(
96
- _subtitle(context),
97
- style: context.textTheme.bodySmall?.copyWith(
98
- color: context.colors.muted,
99
- ),
100
- ),
101
- ),
102
- ],
103
- ),
104
- );
105
- }
106
- }