kasy-cli 1.38.0 → 1.39.1

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 (105) 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/features/notifications/api/device_api.dart +11 -6
  4. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
  5. package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
  6. package/lib/scaffold/backends/patch-base-hashes.json +6 -6
  7. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  8. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
  9. package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
  10. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
  11. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  12. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
  13. package/lib/scaffold/shared/generator-utils.js +12 -6
  14. package/package.json +1 -1
  15. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  16. package/templates/firebase/AGENTS.md +2 -2
  17. package/templates/firebase/DESIGN_SYSTEM.md +23 -8
  18. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  19. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  20. package/templates/firebase/assets/icons/facebook.svg +49 -0
  21. package/templates/firebase/assets/icons/google.svg +1 -0
  22. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  23. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  24. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  25. package/templates/firebase/lib/components/components.dart +5 -2
  26. package/templates/firebase/lib/components/kasy_app_bar.dart +325 -15
  27. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  28. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  29. package/templates/firebase/lib/components/kasy_sidebar.dart +18 -6
  30. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  31. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  32. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +27 -18
  33. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +34 -16
  34. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  35. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  36. package/templates/firebase/lib/core/data/api/user_api.dart +11 -0
  37. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  38. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +95 -30
  39. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  40. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  41. package/templates/firebase/lib/core/states/user_state_notifier.dart +28 -1
  42. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  43. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  44. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +51 -19
  45. package/templates/firebase/lib/core/web_viewport_scale.dart +66 -36
  46. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  47. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  48. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  49. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  50. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  51. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  52. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  53. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  54. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  55. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  56. package/templates/firebase/lib/features/home/home_components_page.dart +253 -125
  57. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +263 -59
  58. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  59. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  60. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  61. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +111 -57
  62. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  63. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -4
  64. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  65. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  66. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  67. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +2 -2
  68. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  69. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  70. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  71. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  72. package/templates/firebase/lib/features/settings/settings_page.dart +53 -32
  73. package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart +4 -0
  74. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +895 -111
  75. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  76. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  77. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +171 -41
  78. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +1 -1
  79. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  80. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +48 -47
  81. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  82. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  83. package/templates/firebase/lib/i18n/en.i18n.json +753 -712
  84. package/templates/firebase/lib/i18n/es.i18n.json +753 -712
  85. package/templates/firebase/lib/i18n/pt.i18n.json +753 -712
  86. package/templates/firebase/lib/main.dart +20 -7
  87. package/templates/firebase/lib/router.dart +32 -26
  88. package/templates/firebase/pubspec.yaml +2 -1
  89. package/templates/firebase/test/admin_shell_chrome_test.dart +11 -5
  90. package/templates/firebase/test/app_bar_config_test.dart +70 -0
  91. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  92. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  93. package/templates/firebase/tool/design_check.dart +9 -0
  94. package/templates/firebase/assets/icons/apple.png +0 -0
  95. package/templates/firebase/assets/icons/facebook.png +0 -0
  96. package/templates/firebase/assets/icons/google.png +0 -0
  97. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  98. package/templates/firebase/lib/components/kasy_web_header.dart +0 -218
  99. package/templates/firebase/lib/core/chrome/web_header_scope.dart +0 -20
  100. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  101. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  102. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  103. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  104. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -179
  105. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
@@ -1,9 +1,11 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:flutter_riverpod/flutter_riverpod.dart';
3
3
  import 'package:kasy_kit/components/kasy_button.dart';
4
+ import 'package:kasy_kit/components/kasy_drop_down.dart';
4
5
  import 'package:kasy_kit/components/kasy_tabs.dart';
5
6
  import 'package:kasy_kit/components/kasy_text_area.dart';
6
7
  import 'package:kasy_kit/components/kasy_text_field.dart';
8
+ import 'package:kasy_kit/core/config/features.dart';
7
9
  import 'package:kasy_kit/core/theme/theme.dart';
8
10
  import 'package:kasy_kit/core/toast/toast_service.dart';
9
11
  import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
@@ -12,6 +14,18 @@ import 'package:kasy_kit/features/settings/ui/components/admin/send_push_notifie
12
14
  import 'package:kasy_kit/i18n/translations.g.dart';
13
15
  import 'package:package_info_plus/package_info_plus.dart';
14
16
 
17
+ /// Same canonical pattern the auth `Email` value object validates against — an
18
+ /// address must be well-formed before it can join the recipient list.
19
+ final RegExp _emailPattern = RegExp(
20
+ r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$',
21
+ );
22
+
23
+ bool _isValidEmail(String value) => _emailPattern.hasMatch(value.trim());
24
+
25
+ /// Default push destination: the in-app notifications list. Mirrors the
26
+ /// fallback in `Notification.onTap` (no route sent → `/notifications`).
27
+ const String _defaultRoute = '/notifications';
28
+
15
29
  class SendPushNotificationPage extends ConsumerStatefulWidget {
16
30
  const SendPushNotificationPage({super.key});
17
31
 
@@ -25,7 +39,6 @@ class _SendPushNotificationPageState
25
39
  final _titleCtrl = TextEditingController();
26
40
  final _bodyCtrl = TextEditingController();
27
41
  final _imageCtrl = TextEditingController();
28
- final _routeCtrl = TextEditingController();
29
42
  final _emailCtrl = TextEditingController();
30
43
 
31
44
  // Explicit FocusNodes for all fields keeps the keyboard open when switching.
@@ -33,12 +46,16 @@ class _SendPushNotificationPageState
33
46
  final _titleFocus = FocusNode();
34
47
  final _bodyFocus = FocusNode();
35
48
  final _imageFocus = FocusNode();
36
- final _routeFocus = FocusNode();
37
49
 
38
50
  bool _sendToAll = false;
39
51
  final List<String> _emails = [];
40
52
  String _appName = '';
41
53
 
54
+ /// Page the app opens when the user taps the notification. Defaults to the
55
+ /// notifications list (the same fallback `Notification.onTap` uses when no
56
+ /// route is sent), and the admin can repoint it to any available page.
57
+ String _route = _defaultRoute;
58
+
42
59
  @override
43
60
  void initState() {
44
61
  super.initState();
@@ -52,21 +69,26 @@ class _SendPushNotificationPageState
52
69
  _titleCtrl.dispose();
53
70
  _bodyCtrl.dispose();
54
71
  _imageCtrl.dispose();
55
- _routeCtrl.dispose();
56
72
  _emailCtrl.dispose();
57
73
  _emailFocus.dispose();
58
74
  _titleFocus.dispose();
59
75
  _bodyFocus.dispose();
60
76
  _imageFocus.dispose();
61
- _routeFocus.dispose();
62
77
  super.dispose();
63
78
  }
64
79
 
65
- void _addEmail() {
80
+ /// The "add" button stays disabled until the field holds a well-formed,
81
+ /// not-yet-listed e-mail — so it never adds blanks, malformed strings, or
82
+ /// duplicates (the Enter key shares this guard via [_addEmail]).
83
+ bool get _canAddEmail {
66
84
  final email = _emailCtrl.text.trim();
67
- if (email.isEmpty || _emails.contains(email)) return;
85
+ return _isValidEmail(email) && !_emails.contains(email);
86
+ }
87
+
88
+ void _addEmail() {
89
+ if (!_canAddEmail) return;
68
90
  setState(() {
69
- _emails.add(email);
91
+ _emails.add(_emailCtrl.text.trim());
70
92
  _emailCtrl.clear();
71
93
  });
72
94
  _emailFocus.requestFocus();
@@ -83,11 +105,11 @@ class _SendPushNotificationPageState
83
105
  _titleCtrl.clear();
84
106
  _bodyCtrl.clear();
85
107
  _imageCtrl.clear();
86
- _routeCtrl.clear();
87
108
  _emailCtrl.clear();
88
109
  setState(() {
89
110
  _emails.clear();
90
111
  _sendToAll = false;
112
+ _route = _defaultRoute;
91
113
  });
92
114
  }
93
115
 
@@ -118,9 +140,7 @@ class _SendPushNotificationPageState
118
140
  : _imageCtrl.text.trim(),
119
141
  emails: List.from(_emails),
120
142
  sendToAll: _sendToAll,
121
- route: _routeCtrl.text.trim().isEmpty
122
- ? null
123
- : _routeCtrl.text.trim(),
143
+ route: _route,
124
144
  );
125
145
  }
126
146
 
@@ -186,6 +206,9 @@ class _SendPushNotificationPageState
186
206
  focusNode: _emailFocus,
187
207
  onAdd: _addEmail,
188
208
  onRemove: _removeEmail,
209
+ // Rebuild as the admin types so the add button enables/disables live.
210
+ onChanged: () => setState(() {}),
211
+ canAdd: _canAddEmail,
189
212
  label: tr.send_push_email_label,
190
213
  hint: tr.send_push_email_hint,
191
214
  ),
@@ -202,6 +225,9 @@ class _SendPushNotificationPageState
202
225
  focusNode: _titleFocus,
203
226
  maxLength: 64,
204
227
  showRequiredIndicator: true,
228
+ // Secondary fill contrasts against the surface-colored section card —
229
+ // the primary (surface) fill would blend white-on-white.
230
+ variant: KasyTextFieldVariant.secondary,
205
231
  textInputAction: TextInputAction.next,
206
232
  onChanged: (_) => setState(() {}),
207
233
  onEditingComplete: () => _bodyFocus.requestFocus(),
@@ -214,6 +240,7 @@ class _SendPushNotificationPageState
214
240
  focusNode: _bodyFocus,
215
241
  maxLength: 250,
216
242
  showRequiredIndicator: true,
243
+ variant: KasyTextFieldVariant.secondary,
217
244
  minLines: 3,
218
245
  maxLines: 5,
219
246
  onChanged: (_) => setState(() {}),
@@ -224,20 +251,50 @@ class _SendPushNotificationPageState
224
251
  hint: tr.send_push_image_hint,
225
252
  controller: _imageCtrl,
226
253
  focusNode: _imageFocus,
254
+ variant: KasyTextFieldVariant.secondary,
227
255
  onChanged: (_) => setState(() {}),
228
- textInputAction: TextInputAction.next,
229
- onEditingComplete: () => _routeFocus.requestFocus(),
256
+ // Last text input on the form — close the keyboard on submit.
257
+ textInputAction: TextInputAction.done,
258
+ onEditingComplete: () => _imageFocus.unfocus(),
230
259
  ),
231
260
  ],
232
261
  );
233
262
 
234
- final Widget advancedFields = KasyTextField(
263
+ // Pick the landing page from the routes this build actually ships (the
264
+ // optional ones are feature-flagged, exactly like the router), instead of
265
+ // typing a raw path. Leaving the default sends the user to /notifications.
266
+ final Widget advancedFields = KasyDropDown<String>(
235
267
  label: tr.send_push_route_label,
236
- hint: tr.send_push_route_hint,
237
- controller: _routeCtrl,
238
- focusNode: _routeFocus,
239
- textInputAction: TextInputAction.done,
240
- onEditingComplete: () => _routeFocus.unfocus(),
268
+ description: tr.send_push_route_description,
269
+ value: _route,
270
+ variant: KasyTextFieldVariant.secondary,
271
+ onChanged: (route) => setState(() => _route = route),
272
+ items: <KasyDropDownItem<String>>[
273
+ KasyDropDownItem(
274
+ value: '/notifications',
275
+ label: tr.send_push_route_notifications,
276
+ ),
277
+ KasyDropDownItem(value: '/', label: tr.send_push_route_home),
278
+ KasyDropDownItem(
279
+ value: '/settings',
280
+ label: tr.send_push_route_settings,
281
+ ),
282
+ if (withRevenuecat)
283
+ KasyDropDownItem(
284
+ value: '/premium',
285
+ label: tr.send_push_route_premium,
286
+ ),
287
+ if (withLocalReminders)
288
+ KasyDropDownItem(
289
+ value: '/reminder',
290
+ label: tr.send_push_route_reminder,
291
+ ),
292
+ if (withFeedback)
293
+ KasyDropDownItem(
294
+ value: '/feedback',
295
+ label: tr.send_push_route_feedback,
296
+ ),
297
+ ],
241
298
  );
242
299
 
243
300
  final Widget formColumn = Column(
@@ -337,25 +394,84 @@ class _SendPushNotificationPageState
337
394
  ),
338
395
  ),
339
396
  )
340
- : scroll(
341
- padding: EdgeInsets.fromLTRB(
342
- gutter,
343
- KasySpacing.belowChromeContentGap,
344
- gutter,
345
- bottomInset,
346
- ),
347
- child: Column(
348
- crossAxisAlignment: CrossAxisAlignment.stretch,
349
- children: [
350
- previewPanel,
351
- const SizedBox(height: KasySpacing.lg),
352
- formColumn,
353
- ],
354
- ),
397
+ : Stack(
398
+ children: [
399
+ // The form scrolls; the live preview stays PINNED at the top so
400
+ // the admin watches the notification update while filling the
401
+ // fields. An invisible copy of the preview at the head of the
402
+ // scroll reserves exactly its height, so the first section
403
+ // starts just below the banner and slides BEHIND it on scroll.
404
+ Positioned.fill(
405
+ child: scroll(
406
+ padding: EdgeInsets.fromLTRB(
407
+ gutter,
408
+ KasySpacing.belowChromeContentGap,
409
+ gutter,
410
+ bottomInset,
411
+ ),
412
+ child: Column(
413
+ crossAxisAlignment: CrossAxisAlignment.stretch,
414
+ children: [
415
+ Visibility(
416
+ visible: false,
417
+ maintainSize: true,
418
+ maintainAnimation: true,
419
+ maintainState: true,
420
+ child: previewPanel,
421
+ ),
422
+ const SizedBox(height: KasySpacing.lg),
423
+ formColumn,
424
+ ],
425
+ ),
426
+ ),
427
+ ),
428
+ // Pinned banner: an opaque page-colored band (so the scrolling
429
+ // form disappears behind it) with a short fade at its bottom
430
+ // edge. IgnorePointer lets drags over the preview still scroll
431
+ // the form — the preview itself has no interaction.
432
+ Positioned(
433
+ top: 0,
434
+ left: 0,
435
+ right: 0,
436
+ child: IgnorePointer(
437
+ child: Column(
438
+ mainAxisSize: MainAxisSize.min,
439
+ children: [
440
+ Container(
441
+ color: context.colors.background,
442
+ padding: const EdgeInsets.fromLTRB(
443
+ gutter,
444
+ KasySpacing.belowChromeContentGap,
445
+ gutter,
446
+ 0,
447
+ ),
448
+ child: previewPanel,
449
+ ),
450
+ Container(
451
+ height: KasySpacing.md,
452
+ decoration: BoxDecoration(
453
+ gradient: LinearGradient(
454
+ begin: Alignment.topCenter,
455
+ end: Alignment.bottomCenter,
456
+ colors: [
457
+ context.colors.background,
458
+ context.colors.background.withValues(alpha: 0),
459
+ ],
460
+ ),
461
+ ),
462
+ ),
463
+ ],
464
+ ),
465
+ ),
466
+ ),
467
+ ],
355
468
  );
356
469
 
357
470
  return ScrollConfiguration(
358
- behavior: const KasyKitScrollBehavior(),
471
+ // Hide the scrollbar: on the two-column layout it lands between the
472
+ // form and the live preview and reads as a stray mark next to the
473
+ // cards. The form is short enough not to need it.
474
+ behavior: const KasyKitScrollBehavior().copyWith(scrollbars: false),
359
475
  child: bodyContent,
360
476
  );
361
477
  },
@@ -416,6 +532,8 @@ class _EmailsSection extends StatelessWidget {
416
532
  final FocusNode focusNode;
417
533
  final VoidCallback onAdd;
418
534
  final void Function(String) onRemove;
535
+ final VoidCallback onChanged;
536
+ final bool canAdd;
419
537
  final String label;
420
538
  final String hint;
421
539
 
@@ -425,6 +543,8 @@ class _EmailsSection extends StatelessWidget {
425
543
  required this.focusNode,
426
544
  required this.onAdd,
427
545
  required this.onRemove,
546
+ required this.onChanged,
547
+ required this.canAdd,
428
548
  required this.label,
429
549
  required this.hint,
430
550
  });
@@ -444,6 +564,9 @@ class _EmailsSection extends StatelessWidget {
444
564
  controller: controller,
445
565
  focusNode: focusNode,
446
566
  showRequiredIndicator: true,
567
+ variant: KasyTextFieldVariant.secondary,
568
+ contentType: KasyTextFieldContentType.email,
569
+ onChanged: (_) => onChanged(),
447
570
  // "next" keeps keyboard open when pressing the action key.
448
571
  textInputAction: TextInputAction.next,
449
572
  onEditingComplete: onAdd,
@@ -453,10 +576,12 @@ class _EmailsSection extends StatelessWidget {
453
576
  Padding(
454
577
  padding: const EdgeInsets.only(bottom: 2),
455
578
  child: KasyButton.iconOnly(
456
- icon: Icons.add,
457
- onPressed: onAdd,
579
+ icon: Icons.add_rounded,
580
+ // Disabled (greyed, non-tappable) until the field holds a valid,
581
+ // non-duplicate e-mail.
582
+ onPressed: canAdd ? onAdd : null,
458
583
  size: KasyButtonSize.small,
459
- variant: KasyButtonVariant.soft,
584
+ iconGlyphSize: 20,
460
585
  ),
461
586
  ),
462
587
  ],
@@ -489,11 +614,16 @@ class _EmailChip extends StatelessWidget {
489
614
 
490
615
  @override
491
616
  Widget build(BuildContext context) {
617
+ // An e-mail is plain data the admin listed, not an action or a status — so
618
+ // it reads as a neutral pill (clean, color used in moderation), not an
619
+ // accent-blue tag. surfaceSecondary contrasts against the section card's
620
+ // surface in both modes; the text uses the normal foreground tone.
492
621
  return Container(
493
622
  padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
494
623
  decoration: BoxDecoration(
495
- color: context.colors.primary.withValues(alpha: 0.10),
624
+ color: context.colors.surfaceSecondary,
496
625
  borderRadius: BorderRadius.circular(20),
626
+ border: Border.all(color: context.colors.border),
497
627
  ),
498
628
  child: Row(
499
629
  mainAxisSize: MainAxisSize.min,
@@ -501,7 +631,7 @@ class _EmailChip extends StatelessWidget {
501
631
  Text(
502
632
  email,
503
633
  style: context.textTheme.bodySmall?.copyWith(
504
- color: context.colors.primary,
634
+ color: context.colors.onSurface,
505
635
  fontWeight: FontWeight.w500,
506
636
  ),
507
637
  ),
@@ -511,7 +641,7 @@ class _EmailChip extends StatelessWidget {
511
641
  child: Icon(
512
642
  Icons.close,
513
643
  size: 13,
514
- color: context.colors.primary.withValues(alpha: 0.7),
644
+ color: context.colors.muted,
515
645
  ),
516
646
  ),
517
647
  ],
@@ -67,7 +67,7 @@ class _EditableUserAvatarState extends ConsumerState<EditableUserAvatar> {
67
67
  return KasyFocusRing(
68
68
  enabled: onTapAvatar != null,
69
69
  onActivate: onTapAvatar,
70
- borderRadius: BorderRadius.circular(999),
70
+ borderRadius: BorderRadius.circular(KasyRadius.full),
71
71
  child: GestureDetector(
72
72
  behavior: HitTestBehavior.opaque,
73
73
  onTap: onTapAvatar,
@@ -4,6 +4,7 @@ import 'package:kasy_kit/components/components.dart';
4
4
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
5
5
  import 'package:kasy_kit/core/theme/theme.dart';
6
6
  import 'package:kasy_kit/i18n/translations.g.dart';
7
+ import 'package:kasy_kit/router.dart';
7
8
 
8
9
  class DeleteUserButton extends ConsumerWidget {
9
10
  const DeleteUserButton({super.key});
@@ -40,7 +41,14 @@ class DeleteUserButton extends ConsumerWidget {
40
41
  }
41
42
  }
42
43
  },
43
- );
44
+ ).whenComplete(() {
45
+ // Same pageless-dialog issue as logout: on success deleteAccount flips
46
+ // the state to anonymous while this confirm dialog still sits on the
47
+ // root navigator, so the redirect to /signin can't land. Re-run it
48
+ // once the dialog has popped. Harmless when the user cancels or the
49
+ // deletion failed (still authenticated → redirect keeps them here).
50
+ if (context.mounted) ref.read(goRouterProvider).refresh();
51
+ });
44
52
  },
45
53
  );
46
54
  }
@@ -6,7 +6,6 @@ import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
6
6
  import 'package:kasy_kit/core/home_widgets/home_widget_mywidget_service.dart';
7
7
  import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
8
8
  import 'package:kasy_kit/core/theme/theme.dart';
9
- import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
10
9
  import 'package:kasy_kit/core/widgets/kasy_hover.dart';
11
10
  import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
12
11
  import 'package:kasy_kit/i18n/app_locale_display.dart';
@@ -20,41 +19,41 @@ class LanguageSwitcher extends ConsumerWidget {
20
19
  @override
21
20
  Widget build(BuildContext context, WidgetRef ref) {
22
21
  final AppLocale current = TranslationProvider.of(context).locale;
23
- return KasyFocusRing(
24
- onActivate: () => _showLanguagePicker(context, ref, current),
25
- borderRadius: KasyRadius.smBorderRadius,
26
- child: InkWell(
27
- canRequestFocus: false,
28
- onTap: () => _showLanguagePicker(context, ref, current),
29
- child: Padding(
30
- padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
31
- child: Row(
32
- children: <Widget>[
33
- Icon(
34
- KasyIcons.language,
35
- size: KasyIconSize.rowLeading,
22
+ return KasyHover(
23
+ onTap: () => _showLanguagePicker(context, ref, current),
24
+ focusable: true,
25
+ // Rectangular highlight (KasyHover default): this row lives inside a card,
26
+ // whose rounded corners clip the ends — a rounded fill here would float.
27
+ semanticLabel: context.t.settings.language_title,
28
+ padding: const EdgeInsets.symmetric(
29
+ horizontal: KasySpacing.md,
30
+ vertical: KasySpacing.smd,
31
+ ),
32
+ child: Row(
33
+ children: <Widget>[
34
+ Icon(
35
+ KasyIcons.language,
36
+ size: KasyIconSize.rowLeading,
37
+ color: context.colors.onSurface,
38
+ ),
39
+ const SizedBox(width: KasySpacing.sm),
40
+ Expanded(
41
+ child: Text(
42
+ context.t.settings.language_title,
43
+ style: context.kasyTextTheme.listRowTitle.copyWith(
36
44
  color: context.colors.onSurface,
37
45
  ),
38
- const SizedBox(width: KasySpacing.sm),
39
- Expanded(
40
- child: Text(
41
- context.t.settings.language_title,
42
- style: context.textTheme.titleSmall?.copyWith(
43
- color: context.colors.onSurface,
44
- ),
45
- ),
46
- ),
47
- Text(
48
- current.nativeName,
49
- style: context.textTheme.bodyMedium?.copyWith(
50
- color: context.colors.muted,
51
- ),
52
- ),
53
- const SizedBox(width: KasySpacing.xs),
54
- const SettingsListChevron(),
55
- ],
46
+ ),
56
47
  ),
57
- ),
48
+ Text(
49
+ current.nativeName,
50
+ style: context.kasyTextTheme.listRowValue.copyWith(
51
+ color: context.colors.muted,
52
+ ),
53
+ ),
54
+ const SizedBox(width: KasySpacing.xs),
55
+ const SettingsListChevron(),
56
+ ],
58
57
  ),
59
58
  );
60
59
  }
@@ -69,21 +68,22 @@ class LanguageSwitcher extends ConsumerWidget {
69
68
  context: context,
70
69
  builder: (sheetContext) => KasySheetSurface(
71
70
  child: Padding(
72
- padding: const EdgeInsets.fromLTRB(
73
- KasySpacing.md,
74
- KasySpacing.sm,
75
- KasySpacing.md,
76
- KasySpacing.sm,
77
- ),
71
+ // No horizontal padding: option rows go full-bleed so the highlight
72
+ // spans the whole sheet (no inset pill). Title and rows carry their
73
+ // own horizontal inset instead.
74
+ padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
78
75
  child: Column(
79
76
  mainAxisSize: MainAxisSize.min,
80
77
  children: <Widget>[
81
- Align(
82
- alignment: Alignment.centerLeft,
83
- child: Text(
84
- sheetTitle,
85
- style: sheetContext.textTheme.titleMedium?.copyWith(
86
- color: sheetContext.colors.onSurface,
78
+ Padding(
79
+ padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
80
+ child: Align(
81
+ alignment: Alignment.centerLeft,
82
+ child: Text(
83
+ sheetTitle,
84
+ style: sheetContext.textTheme.titleMedium?.copyWith(
85
+ color: sheetContext.colors.onSurface,
86
+ ),
87
87
  ),
88
88
  ),
89
89
  ),
@@ -147,9 +147,10 @@ class _LocaleOptionTile extends StatelessWidget {
147
147
  return KasyHover(
148
148
  onTap: onTap,
149
149
  focusable: true,
150
- borderRadius: BorderRadius.circular(KasyRadius.sm),
150
+ // Full-bleed rectangular highlight (default radius): the sheet rounds its
151
+ // own corners, so options span edge-to-edge like a native menu/list.
151
152
  padding: const EdgeInsets.symmetric(
152
- horizontal: KasySpacing.xs,
153
+ horizontal: KasySpacing.md,
153
154
  vertical: KasySpacing.smd,
154
155
  ),
155
156
  child: Row(
@@ -1,5 +1,6 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:kasy_kit/core/theme/theme.dart';
3
+ import 'package:kasy_kit/core/widgets/kasy_hover.dart';
3
4
 
4
5
  /// Row used in settings modal bottom sheets (avatar source, language, etc.).
5
6
  class SettingsBottomSheetOptionTile extends StatelessWidget {
@@ -26,26 +27,28 @@ class SettingsBottomSheetOptionTile extends StatelessWidget {
26
27
  final TextStyle? labelStyle =
27
28
  context.textTheme.titleMedium?.copyWith(color: fg);
28
29
  final Text labelWidget = Text(label, style: labelStyle);
29
- return InkWell(
30
+ return KasyHover(
30
31
  onTap: onTap,
31
- child: Padding(
32
- padding: const EdgeInsets.symmetric(
33
- horizontal: KasySpacing.md,
34
- vertical: KasySpacing.smd,
35
- ),
36
- child: Row(
37
- children: <Widget>[
38
- if (leading != null) ...<Widget>[
39
- leading!,
40
- const SizedBox(width: KasySpacing.sm),
41
- ] else if (icon != null) ...<Widget>[
42
- Icon(icon, size: KasyIconSize.lg, color: fg),
43
- const SizedBox(width: KasySpacing.sm),
44
- ],
45
- Expanded(child: labelWidget),
46
- if (trailing != null) trailing!,
32
+ focusable: true,
33
+ semanticLabel: label,
34
+ // Full-bleed rectangular highlight (default radius): these rows live in a
35
+ // bottom sheet whose rounded corners clip the ends, like a native menu.
36
+ padding: const EdgeInsets.symmetric(
37
+ horizontal: KasySpacing.md,
38
+ vertical: KasySpacing.smd,
39
+ ),
40
+ child: Row(
41
+ children: <Widget>[
42
+ if (leading != null) ...<Widget>[
43
+ leading!,
44
+ const SizedBox(width: KasySpacing.sm),
45
+ ] else if (icon != null) ...<Widget>[
46
+ Icon(icon, size: KasyIconSize.lg, color: fg),
47
+ const SizedBox(width: KasySpacing.sm),
47
48
  ],
48
- ),
49
+ Expanded(child: labelWidget),
50
+ if (trailing != null) trailing!,
51
+ ],
49
52
  ),
50
53
  );
51
54
  }
@@ -26,7 +26,7 @@ class SettingsIconBadge extends StatelessWidget {
26
26
  height: size,
27
27
  decoration: BoxDecoration(
28
28
  color: color.withValues(alpha: 0.12),
29
- borderRadius: BorderRadius.circular(8),
29
+ borderRadius: BorderRadius.circular(KasyRadius.sm),
30
30
  ),
31
31
  child: Icon(
32
32
  icon,
@@ -65,7 +65,17 @@ class SettingsDivider extends StatelessWidget {
65
65
 
66
66
  @override
67
67
  Widget build(BuildContext context) {
68
- return Divider(color: context.colors.onBackground.withValues(alpha: .06));
68
+ // Inset to align with the rows' content (which now carry the horizontal
69
+ // padding the card used to have), so the hairline doesn't touch the edges.
70
+ return Divider(
71
+ // Hairline only — no extra height — so rows sit close together (iOS-style
72
+ // contiguous list) instead of floating apart with a 16px gap.
73
+ height: 1,
74
+ thickness: 1,
75
+ color: context.colors.onBackground.withValues(alpha: .06),
76
+ indent: KasySpacing.md,
77
+ endIndent: KasySpacing.md,
78
+ );
69
79
  }
70
80
  }
71
81
 
@@ -91,7 +101,10 @@ class SettingsSwitchTile extends StatelessWidget {
91
101
  @override
92
102
  Widget build(BuildContext context) {
93
103
  return Padding(
94
- padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
104
+ padding: const EdgeInsets.symmetric(
105
+ horizontal: KasySpacing.md,
106
+ vertical: KasySpacing.smd,
107
+ ),
95
108
  child: Row(
96
109
  children: <Widget>[
97
110
  if (iconBackgroundColor != null)
@@ -110,7 +123,7 @@ class SettingsSwitchTile extends StatelessWidget {
110
123
  children: <Widget>[
111
124
  Text(
112
125
  title,
113
- style: context.textTheme.titleSmall?.copyWith(
126
+ style: context.kasyTextTheme.listRowTitle.copyWith(
114
127
  color: context.colors.onSurface,
115
128
  ),
116
129
  ),
@@ -165,12 +178,14 @@ class SettingsTile extends StatelessWidget {
165
178
  Widget build(BuildContext context) {
166
179
  return KasyHover(
167
180
  onTap: onTap,
168
- hoverEnabled: false,
169
- pressEnabled: false,
170
181
  focusable: true,
171
- borderRadius: KasyRadius.smBorderRadius,
182
+ // Rectangular highlight (default): the card clips the rounded ends, so
183
+ // middle rows stay square instead of showing a floating rounded pill.
172
184
  semanticLabel: title,
173
- padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
185
+ padding: const EdgeInsets.symmetric(
186
+ horizontal: KasySpacing.md,
187
+ vertical: KasySpacing.smd,
188
+ ),
174
189
  child: Row(
175
190
  children: [
176
191
  if (iconBackgroundColor != null)
@@ -185,7 +200,7 @@ class SettingsTile extends StatelessWidget {
185
200
  Expanded(
186
201
  child: Text(
187
202
  title,
188
- style: context.textTheme.titleSmall?.copyWith(
203
+ style: context.kasyTextTheme.listRowTitle.copyWith(
189
204
  color: context.colors.onSurface,
190
205
  ),
191
206
  ),
@@ -193,7 +208,7 @@ class SettingsTile extends StatelessWidget {
193
208
  if (trailingLabel != null) ...[
194
209
  Text(
195
210
  trailingLabel!,
196
- style: context.textTheme.bodyMedium?.copyWith(
211
+ style: context.kasyTextTheme.listRowValue.copyWith(
197
212
  color: context.colors.muted,
198
213
  ),
199
214
  ),