kasy-cli 1.31.14 → 1.32.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 (96) hide show
  1. package/lib/commands/new.js +15 -1
  2. package/lib/scaffold/CHANGELOG.json +9 -0
  3. package/lib/scaffold/backends/api/patch/README.md +87 -2
  4. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +34 -0
  5. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  6. package/lib/scaffold/backends/firebase/setup-from-scratch.js +22 -0
  7. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
  8. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
  9. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
  10. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
  11. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +12 -0
  12. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  13. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +3 -2
  14. package/lib/scaffold/generate.js +1 -1
  15. package/lib/scaffold/shared/generator-utils.js +22 -3
  16. package/lib/utils/i18n/messages-en.js +2 -0
  17. package/lib/utils/i18n/messages-es.js +2 -0
  18. package/lib/utils/i18n/messages-pt.js +2 -0
  19. package/package.json +2 -2
  20. package/templates/firebase/docs/auth-setup.en.md +7 -1
  21. package/templates/firebase/docs/auth-setup.es.md +7 -1
  22. package/templates/firebase/docs/auth-setup.pt.md +7 -1
  23. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
  24. package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
  25. package/templates/firebase/lib/components/kasy_alert.dart +1 -1
  26. package/templates/firebase/lib/components/kasy_app_bar.dart +3 -3
  27. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
  28. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  29. package/templates/firebase/lib/components/kasy_chip.dart +1 -1
  30. package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
  31. package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
  32. package/templates/firebase/lib/components/kasy_sidebar.dart +2 -2
  33. package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
  34. package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
  35. package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
  36. package/templates/firebase/lib/components/kasy_toast.dart +1 -1
  37. package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
  38. package/templates/firebase/lib/core/config/features.dart +13 -0
  39. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
  40. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
  41. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +1 -1
  42. package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
  43. package/templates/firebase/lib/core/theme/shadows.dart +13 -0
  44. package/templates/firebase/lib/core/theme/texts.dart +32 -0
  45. package/templates/firebase/lib/core/theme/theme.dart +2 -0
  46. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
  47. package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
  48. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +1 -1
  49. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
  50. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
  51. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
  52. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
  53. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +36 -14
  54. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +27 -11
  55. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
  56. package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
  57. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
  58. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -1
  59. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +22 -3
  60. package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
  61. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +2 -2
  62. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
  63. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +35 -38
  64. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
  65. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
  66. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
  67. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +1 -1
  68. package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
  69. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
  70. package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
  71. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
  72. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
  73. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
  74. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  75. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
  76. package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
  77. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
  78. package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
  79. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
  80. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
  81. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
  82. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
  83. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
  84. package/templates/firebase/lib/i18n/en.i18n.json +8 -0
  85. package/templates/firebase/lib/i18n/es.i18n.json +8 -0
  86. package/templates/firebase/lib/i18n/pt.i18n.json +8 -0
  87. package/templates/firebase/pubspec.yaml +0 -1
  88. package/templates/firebase/web/stripe_success.html +64 -26
  89. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  90. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  91. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  92. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  93. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  94. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  95. package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
  96. package/templates/firebase/login-redesign-preview.png +0 -0
@@ -1,9 +1,11 @@
1
1
  import 'dart:ui';
2
2
 
3
+ import 'package:flutter/foundation.dart';
3
4
  import 'package:flutter/material.dart';
4
5
  import 'package:flutter_riverpod/flutter_riverpod.dart';
5
6
  import 'package:go_router/go_router.dart';
6
7
  import 'package:kasy_kit/components/components.dart';
8
+ import 'package:kasy_kit/core/config/features.dart';
7
9
  import 'package:kasy_kit/core/data/models/user.dart';
8
10
  import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
9
11
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
@@ -64,6 +66,7 @@ class SigninPage extends ConsumerWidget {
64
66
  subtitle: t.auth.signin.subtitle,
65
67
  children: [
66
68
  KasyTextField(
69
+ variant: KasyTextFieldVariant.flat,
67
70
  key: const Key('email_input'),
68
71
  onChanged: (value) => ref
69
72
  .read(signinStateProvider.notifier)
@@ -83,6 +86,7 @@ class SigninPage extends ConsumerWidget {
83
86
  ),
84
87
  const SizedBox(height: _authFieldSpacing),
85
88
  KasyTextField(
89
+ variant: KasyTextFieldVariant.flat,
86
90
  key: const Key('password_input'),
87
91
  onChanged: (newValue) => ref
88
92
  .read(signinStateProvider.notifier)
@@ -98,10 +102,15 @@ class SigninPage extends ConsumerWidget {
98
102
  FocusScope.of(context).unfocus();
99
103
  ref.read(signinStateProvider.notifier).signin();
100
104
  },
101
- labelTrailing: KasyFocusRing(
102
- onActivate: () => context.push('/recover_password'),
103
- borderRadius: BorderRadius.circular(KasyRadius.sm),
105
+ // Secondary link: intentionally NOT a Tab stop, so
106
+ // Tab/next flows email → password → submit → social
107
+ // buttons. Still tappable by mouse/touch and announced
108
+ // to screen readers as a button.
109
+ labelTrailing: Semantics(
110
+ button: true,
111
+ label: t.auth.signin.forgot_password,
104
112
  child: GestureDetector(
113
+ behavior: HitTestBehavior.opaque,
105
114
  onTap: () => context.push('/recover_password'),
106
115
  child: Text(
107
116
  t.auth.signin.forgot_password,
@@ -195,7 +204,9 @@ class _SignupPrompt extends StatelessWidget {
195
204
  KasyPressableDepth(
196
205
  semanticLabel: t.auth.signin.signup_link,
197
206
  onPressed: () => context.pushReplacement('/signup'),
198
- focusable: true,
207
+ // Secondary link: kept out of Tab traversal (focusable defaults to
208
+ // false) so keyboard/next jumps straight to the social sign-in
209
+ // buttons (the most-used path).
199
210
  child: Text(
200
211
  t.auth.signin.signup_link,
201
212
  style: context.textTheme.bodyMedium?.copyWith(
@@ -218,6 +229,14 @@ class _SocialSigninRow extends ConsumerWidget {
218
229
  Widget build(BuildContext context, WidgetRef ref) {
219
230
  final state = ref.watch(signinStateProvider);
220
231
  final isSending = state is SigninStateSending;
232
+ // Apple sign-in is reliable only on Apple-native platforms (iOS/macOS). On web
233
+ // it needs a paid Apple Service ID + manual console setup (see auth docs), gated
234
+ // by withAppleWebSignin (off by default). On Android it needs the same Service ID
235
+ // and the Supabase/API native flow throws, so Apple is hidden there.
236
+ final bool showApple = kIsWeb
237
+ ? withAppleWebSignin
238
+ : (defaultTargetPlatform == TargetPlatform.iOS ||
239
+ defaultTargetPlatform == TargetPlatform.macOS);
221
240
  return Row(
222
241
  children: [
223
242
  Expanded(
@@ -230,17 +249,20 @@ class _SocialSigninRow extends ConsumerWidget {
230
249
  ref.read(signinStateProvider.notifier).signinWithGoogle(),
231
250
  ),
232
251
  ),
233
- const SizedBox(width: KasySpacing.sm),
234
- Expanded(
235
- child: _SocialSigninTile(
236
- label: t.auth.signin.apple,
237
- icon: Image.asset('assets/icons/apple.png', width: 20, height: 20),
238
- onPressed: isSending
239
- ? null
240
- : () =>
241
- ref.read(signinStateProvider.notifier).signinWithApple(),
252
+ if (showApple) ...[
253
+ const SizedBox(width: KasySpacing.sm),
254
+ Expanded(
255
+ child: _SocialSigninTile(
256
+ label: t.auth.signin.apple,
257
+ icon: Image.asset('assets/icons/apple.png', width: 20, height: 20),
258
+ onPressed: isSending
259
+ ? null
260
+ : () => ref
261
+ .read(signinStateProvider.notifier)
262
+ .signinWithApple(),
263
+ ),
242
264
  ),
243
- ),
265
+ ],
244
266
  const SizedBox(width: KasySpacing.sm),
245
267
  Expanded(
246
268
  child: _SocialSigninTile(
@@ -1,9 +1,11 @@
1
1
  import 'dart:ui';
2
2
 
3
+ import 'package:flutter/foundation.dart';
3
4
  import 'package:flutter/material.dart';
4
5
  import 'package:flutter_riverpod/flutter_riverpod.dart';
5
6
  import 'package:go_router/go_router.dart';
6
7
  import 'package:kasy_kit/components/components.dart';
8
+ import 'package:kasy_kit/core/config/features.dart';
7
9
  import 'package:kasy_kit/core/data/models/user.dart';
8
10
  import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
9
11
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
@@ -60,6 +62,7 @@ class SignupPage extends ConsumerWidget {
60
62
  subtitle: t.auth.signup.subtitle,
61
63
  children: [
62
64
  KasyTextField(
65
+ variant: KasyTextFieldVariant.flat,
63
66
  key: const Key('email_input'),
64
67
  onChanged: (value) => ref
65
68
  .read(signupStateProvider.notifier)
@@ -79,6 +82,7 @@ class SignupPage extends ConsumerWidget {
79
82
  ),
80
83
  const SizedBox(height: _authFieldSpacing),
81
84
  KasyTextField(
85
+ variant: KasyTextFieldVariant.flat,
82
86
  key: const Key('password_input'),
83
87
  onChanged: (newValue) => ref
84
88
  .read(signupStateProvider.notifier)
@@ -180,7 +184,8 @@ class _SigninPrompt extends StatelessWidget {
180
184
  KasyPressableDepth(
181
185
  semanticLabel: t.auth.signup.signin_link,
182
186
  onPressed: () => context.pushReplacement('/signin'),
183
- focusable: true,
187
+ // Secondary link: kept out of Tab traversal (focusable defaults to
188
+ // false) so keyboard/next jumps straight to the social buttons.
184
189
  child: Text(
185
190
  t.auth.signup.signin_link,
186
191
  style: context.textTheme.bodyMedium?.copyWith(
@@ -203,6 +208,14 @@ class _SocialSignupRow extends ConsumerWidget {
203
208
  Widget build(BuildContext context, WidgetRef ref) {
204
209
  final state = ref.watch(signinStateProvider);
205
210
  final isSending = state is SigninStateSending;
211
+ // Apple sign-in is reliable only on Apple-native platforms (iOS/macOS). On web
212
+ // it needs a paid Apple Service ID + manual console setup (see auth docs), gated
213
+ // by withAppleWebSignin (off by default). On Android it needs the same Service ID
214
+ // and the Supabase/API native flow throws, so Apple is hidden there.
215
+ final bool showApple = kIsWeb
216
+ ? withAppleWebSignin
217
+ : (defaultTargetPlatform == TargetPlatform.iOS ||
218
+ defaultTargetPlatform == TargetPlatform.macOS);
206
219
  return Row(
207
220
  children: [
208
221
  Expanded(
@@ -215,17 +228,20 @@ class _SocialSignupRow extends ConsumerWidget {
215
228
  ref.read(signinStateProvider.notifier).signinWithGoogle(),
216
229
  ),
217
230
  ),
218
- const SizedBox(width: KasySpacing.sm),
219
- Expanded(
220
- child: _SocialSignupTile(
221
- label: t.auth.signin.apple,
222
- icon: Image.asset('assets/icons/apple.png', width: 20, height: 20),
223
- onPressed: isSending
224
- ? null
225
- : () =>
226
- ref.read(signinStateProvider.notifier).signinWithApple(),
231
+ if (showApple) ...[
232
+ const SizedBox(width: KasySpacing.sm),
233
+ Expanded(
234
+ child: _SocialSignupTile(
235
+ label: t.auth.signin.apple,
236
+ icon: Image.asset('assets/icons/apple.png', width: 20, height: 20),
237
+ onPressed: isSending
238
+ ? null
239
+ : () => ref
240
+ .read(signinStateProvider.notifier)
241
+ .signinWithApple(),
242
+ ),
227
243
  ),
228
- ),
244
+ ],
229
245
  const SizedBox(width: KasySpacing.sm),
230
246
  Expanded(
231
247
  child: _SocialSignupTile(
@@ -28,7 +28,7 @@ class RecoverPasswordSent extends StatelessWidget {
28
28
  KasyBrandBadge(
29
29
  icon: KasyIcons.checkCircle,
30
30
  size: 72,
31
- glyphSize: 36,
31
+ glyphSize: KasyIconSize.display,
32
32
  gradient: LinearGradient(
33
33
  begin: Alignment.topLeft,
34
34
  end: Alignment.bottomRight,
@@ -167,7 +167,7 @@ class _AddChipButton extends StatelessWidget {
167
167
  child: Row(
168
168
  mainAxisSize: MainAxisSize.min,
169
169
  children: [
170
- Icon(KasyIcons.add, size: 14, color: context.colors.primary),
170
+ Icon(KasyIcons.add, size: KasyIconSize.xs, color: context.colors.primary),
171
171
  const SizedBox(width: KasySpacing.xs),
172
172
  Text(
173
173
  label,
@@ -56,7 +56,7 @@ class AddFeatureButton extends StatelessWidget {
56
56
  const SizedBox(width: KasySpacing.smd),
57
57
  Icon(
58
58
  KasyIcons.northEast,
59
- size: 16,
59
+ size: KasyIconSize.rowTrailing,
60
60
  color: context.colors.onPrimary.withValues(alpha: 0.55),
61
61
  ),
62
62
  ],
@@ -220,7 +220,7 @@ class _VoteCardState extends State<VoteCard>
220
220
  children: [
221
221
  Icon(
222
222
  KasyIcons.voteUp,
223
- size: 22,
223
+ size: KasyIconSize.lg,
224
224
  color: widget.textColor,
225
225
  ),
226
226
  ClipRect(
@@ -2714,18 +2714,26 @@ Widget _buildTextFieldVariantsVariant(BuildContext context) {
2714
2714
  children: [
2715
2715
  KasyTextField(
2716
2716
  label: 'Primary Variant',
2717
- hint: 'Primary style input',
2717
+ hint: 'Primary style text field',
2718
2718
  description: 'Default variant with primary styling',
2719
2719
  contentType: KasyTextFieldContentType.email,
2720
2720
  ),
2721
2721
  SizedBox(height: KasySpacing.lg),
2722
2722
  KasyTextField(
2723
2723
  label: 'Secondary Variant',
2724
- hint: 'Secondary style input',
2724
+ hint: 'Secondary style text field',
2725
2725
  description: 'Secondary variant for surfaces',
2726
2726
  variant: KasyTextFieldVariant.secondary,
2727
2727
  contentType: KasyTextFieldContentType.email,
2728
2728
  ),
2729
+ SizedBox(height: KasySpacing.lg),
2730
+ KasyTextField(
2731
+ label: 'Flat Variant',
2732
+ hint: 'Flat style text field',
2733
+ description: 'Primary fill, hairline border, no shadow',
2734
+ variant: KasyTextFieldVariant.flat,
2735
+ contentType: KasyTextFieldContentType.email,
2736
+ ),
2729
2737
  ],
2730
2738
  );
2731
2739
  }
@@ -2738,7 +2746,7 @@ Widget _buildTextFieldStates(BuildContext context) {
2738
2746
  KasyTextField(
2739
2747
  label: 'Default State',
2740
2748
  hint: 'Enter your email',
2741
- description: 'Normal input state',
2749
+ description: 'Normal text field state',
2742
2750
  contentType: KasyTextFieldContentType.email,
2743
2751
  ),
2744
2752
  SizedBox(height: KasySpacing.lg),
@@ -9013,6 +9021,7 @@ class _DatePickerVariantsPreview extends StatefulWidget {
9013
9021
  class _DatePickerVariantsPreviewState
9014
9022
  extends State<_DatePickerVariantsPreview> {
9015
9023
  DateTime? _filledDate;
9024
+ DateTime? _flatDate;
9016
9025
  DateTime? _noBarrierDate;
9017
9026
  DateTime? _noSuffixDate;
9018
9027
  DateTime? _noFocusDate;
@@ -9035,6 +9044,16 @@ class _DatePickerVariantsPreviewState
9035
9044
  locale: locale,
9036
9045
  ),
9037
9046
  const SizedBox(height: KasySpacing.lg),
9047
+ // Flat — surface fill + hairline border, no shadow (KasyTextField.flat).
9048
+ KasyDatePicker(
9049
+ label: 'Flat (no shadow)',
9050
+ placeholder: 'Choose a date',
9051
+ value: _flatDate,
9052
+ onChanged: (d) => setState(() => _flatDate = d),
9053
+ variant: KasyTextFieldVariant.flat,
9054
+ locale: locale,
9055
+ ),
9056
+ const SizedBox(height: KasySpacing.lg),
9038
9057
  // Popover without the dimmed backdrop — feels lighter / tooltip-like.
9039
9058
  KasyDatePicker(
9040
9059
  label: 'No backdrop',
@@ -396,7 +396,7 @@ class _LikeButtonState extends State<_LikeButton>
396
396
  scale: _scale,
397
397
  child: Icon(
398
398
  widget.liked ? KasyIcons.favoriteFilled : KasyIcons.favorite,
399
- size: 18,
399
+ size: KasyIconSize.md,
400
400
  color: heart,
401
401
  ),
402
402
  ),
@@ -194,7 +194,7 @@ class _TimeTile extends StatelessWidget {
194
194
  const Spacer(),
195
195
  Icon(
196
196
  KasyIcons.arrowForwardIos,
197
- size: 14,
197
+ size: KasyIconSize.xs,
198
198
  color: context.colors.muted,
199
199
  ),
200
200
  ],
@@ -299,7 +299,7 @@ class _DateTile extends StatelessWidget {
299
299
  const Spacer(),
300
300
  Icon(
301
301
  KasyIcons.arrowForwardIos,
302
- size: 14,
302
+ size: KasyIconSize.xs,
303
303
  color: context.colors.muted,
304
304
  ),
305
305
  ],
@@ -1,3 +1,4 @@
1
+ import 'package:flutter/foundation.dart' show kIsWeb;
1
2
  import 'package:flutter/material.dart';
2
3
  import 'package:flutter_riverpod/flutter_riverpod.dart';
3
4
  import 'package:kasy_kit/components/components.dart';
@@ -52,7 +53,7 @@ class _EmptyNotificationsState extends ConsumerState<EmptyNotifications> {
52
53
  ),
53
54
  child: Icon(
54
55
  KasyIcons.notificationOff,
55
- size: 36,
56
+ size: KasyIconSize.display,
56
57
  color: context.colors.primary.withValues(alpha: 0.6),
57
58
  ),
58
59
  ),
@@ -79,6 +80,10 @@ class _EmptyNotificationsState extends ConsumerState<EmptyNotifications> {
79
80
  builder: (context, snapshot) {
80
81
  final permission = snapshot.data;
81
82
  if (permission == null) return const SizedBox.shrink();
83
+ // Web has no usable notification-permission prompt (askPermission is a
84
+ // no-op on web and permission_handler has no web support), so the CTA
85
+ // would be a dead button. Show the empty state without it.
86
+ if (kIsWeb) return const SizedBox.shrink();
82
87
  if (permission is NotificationPermissionGranted) {
83
88
  return const SizedBox.shrink();
84
89
  }
@@ -1,6 +1,6 @@
1
- import 'package:better_skeleton/better_skeleton.dart';
2
1
  import 'package:flutter/material.dart';
3
2
  import 'package:jiffy/jiffy.dart';
3
+ import 'package:kasy_kit/components/kasy_skeleton.dart';
4
4
  import 'package:kasy_kit/core/theme/theme.dart';
5
5
  import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
6
6
  import 'package:kasy_kit/features/notifications/providers/models/notification.dart'
@@ -307,54 +307,51 @@ class TileNotificationIcon extends StatelessWidget {
307
307
  ),
308
308
  child: Icon(
309
309
  active ? KasyIcons.notificationActive : KasyIcons.notification,
310
- size: 20,
310
+ size: KasyIconSize.lg,
311
311
  color: color,
312
312
  ),
313
313
  );
314
314
  }
315
315
  }
316
316
 
317
- /// A skeleton for the [NotificationTile] widget.
318
- class NotificationSkeletonTile extends StatefulWidget {
317
+ /// Skeleton placeholder for [NotificationTile], built from the design-system
318
+ /// [KasySkeleton] bones so the loading shimmer matches the kit everywhere and
319
+ /// mirrors the real tile's layout (leading badge + title / body / date lines).
320
+ class NotificationSkeletonTile extends StatelessWidget {
319
321
  const NotificationSkeletonTile({super.key});
320
322
 
321
- @override
322
- State<NotificationSkeletonTile> createState() =>
323
- _NotificationSkeletonTileState();
324
- }
325
-
326
- class _NotificationSkeletonTileState extends State<NotificationSkeletonTile>
327
- with SingleTickerProviderStateMixin {
328
- late final AnimationController animationController;
329
-
330
- @override
331
- void initState() {
332
- super.initState();
333
- animationController = AnimationController(
334
- vsync: this,
335
- duration: const Duration(milliseconds: 1000),
336
- )..repeat();
337
- }
338
-
339
- @override
340
- void deactivate() {
341
- animationController.dispose();
342
- super.deactivate();
343
- }
344
-
345
323
  @override
346
324
  Widget build(BuildContext context) {
347
- return Padding(
348
- padding: const EdgeInsets.only(top: KasySpacing.sm),
349
- child: AnimatedSkeleton(
350
- listenable: animationController,
351
- child: Container(
352
- height: 80,
353
- decoration: BoxDecoration(
354
- color: context.colors.primary.withValues(alpha: .1),
355
- borderRadius: KasyRadius.smBorderRadius,
325
+ return const Padding(
326
+ padding: EdgeInsets.only(top: KasySpacing.sm),
327
+ child: KasySkeletonGroup(
328
+ child: Padding(
329
+ padding: EdgeInsets.all(KasySpacing.md),
330
+ child: Row(
331
+ crossAxisAlignment: CrossAxisAlignment.start,
332
+ children: [
333
+ KasySkeleton(
334
+ width: 40,
335
+ height: 40,
336
+ borderRadius: BorderRadius.all(Radius.circular(10)),
337
+ ),
338
+ SizedBox(width: KasySpacing.smd),
339
+ Expanded(
340
+ child: Column(
341
+ crossAxisAlignment: CrossAxisAlignment.start,
342
+ children: [
343
+ KasySkeleton(width: 150, height: 13),
344
+ SizedBox(height: KasySpacing.sm),
345
+ KasySkeleton(width: double.infinity, height: 11),
346
+ SizedBox(height: KasySpacing.xs),
347
+ KasySkeleton(width: 210, height: 11),
348
+ SizedBox(height: KasySpacing.smd),
349
+ KasySkeleton(width: 70, height: 10),
350
+ ],
351
+ ),
352
+ ),
353
+ ],
356
354
  ),
357
- padding: const EdgeInsets.all(KasySpacing.md),
358
355
  ),
359
356
  ),
360
357
  );
@@ -76,7 +76,7 @@ class PermissionRequestView extends StatelessWidget {
76
76
  child: Center(
77
77
  child:
78
78
  illustration ??
79
- KasyBrandBadge(icon: icon, size: 76, glyphSize: 36),
79
+ KasyBrandBadge(icon: icon, size: 76, glyphSize: KasyIconSize.display),
80
80
  ),
81
81
  ),
82
82
  const SizedBox(height: KasySpacing.xl),
@@ -102,7 +102,7 @@ class _PulsingOrb extends StatelessWidget {
102
102
  ),
103
103
  ],
104
104
  ),
105
- child: Icon(KasyIcons.flash, color: colors.onPrimary, size: 34),
105
+ child: Icon(KasyIcons.flash, color: colors.onPrimary, size: KasyIconSize.display),
106
106
  )
107
107
  .animate(onPlay: (c) => c.repeat(reverse: true))
108
108
  .scaleXY(
@@ -283,7 +283,7 @@ class _PlanRow extends StatelessWidget {
283
283
  ),
284
284
  ),
285
285
  child: selected
286
- ? Icon(KasyIcons.check, size: 12, color: colors.onPrimary)
286
+ ? Icon(KasyIcons.check, size: KasyIconSize.xxs, color: colors.onPrimary)
287
287
  : null,
288
288
  ),
289
289
  const SizedBox(width: KasySpacing.smd),
@@ -421,7 +421,7 @@ class _FakeField extends StatelessWidget {
421
421
  ),
422
422
  child: Row(
423
423
  children: [
424
- Icon(icon, size: 16, color: colors.muted),
424
+ Icon(icon, size: KasyIconSize.sm, color: colors.muted),
425
425
  const SizedBox(width: KasySpacing.sm),
426
426
  Text(
427
427
  hint,
@@ -452,7 +452,7 @@ class _SocialChip extends StatelessWidget {
452
452
  borderRadius: KasyRadius.mdBorderRadius,
453
453
  border: Border.all(color: colors.outline),
454
454
  ),
455
- child: Icon(icon, size: 18, color: colors.onSurface),
455
+ child: Icon(icon, size: KasyIconSize.md, color: colors.onSurface),
456
456
  );
457
457
  }
458
458
  }
@@ -391,7 +391,7 @@ class RoundRadioBox extends StatelessWidget {
391
391
  opacity: iconOpacity,
392
392
  child: Transform.scale(
393
393
  scale: iconSize,
394
- child: Icon(icon, color: context.colors.onPrimary, size: 15),
394
+ child: Icon(icon, color: context.colors.onPrimary, size: KasyIconSize.sm),
395
395
  ),
396
396
  )
397
397
  : const SizedBox.shrink(),