kasy-cli 1.31.13 → 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 (101) 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/deploy.js +5 -0
  8. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
  9. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
  10. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
  11. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
  12. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +69 -17
  13. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  14. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +6 -0
  15. package/lib/scaffold/generate.js +1 -1
  16. package/lib/scaffold/shared/generator-utils.js +22 -3
  17. package/lib/utils/i18n/messages-en.js +2 -0
  18. package/lib/utils/i18n/messages-es.js +2 -0
  19. package/lib/utils/i18n/messages-pt.js +2 -0
  20. package/package.json +2 -2
  21. package/templates/firebase/docs/auth-setup.en.md +7 -1
  22. package/templates/firebase/docs/auth-setup.es.md +7 -1
  23. package/templates/firebase/docs/auth-setup.pt.md +7 -1
  24. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
  25. package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
  26. package/templates/firebase/lib/components/kasy_alert.dart +1 -1
  27. package/templates/firebase/lib/components/kasy_app_bar.dart +3 -3
  28. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
  29. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  30. package/templates/firebase/lib/components/kasy_chip.dart +1 -1
  31. package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
  32. package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
  33. package/templates/firebase/lib/components/kasy_sidebar.dart +62 -11
  34. package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
  35. package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
  36. package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
  37. package/templates/firebase/lib/components/kasy_toast.dart +1 -1
  38. package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
  39. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +6 -0
  40. package/templates/firebase/lib/core/bottom_menu/notification_bottom_item.dart +16 -37
  41. package/templates/firebase/lib/core/config/features.dart +13 -0
  42. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
  43. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
  44. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +1 -1
  45. package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
  46. package/templates/firebase/lib/core/theme/shadows.dart +13 -0
  47. package/templates/firebase/lib/core/theme/texts.dart +32 -0
  48. package/templates/firebase/lib/core/theme/theme.dart +2 -0
  49. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
  50. package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
  51. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +1 -1
  52. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
  53. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
  54. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
  55. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
  56. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +36 -14
  57. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +27 -11
  58. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
  59. package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
  60. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
  61. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -1
  62. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +22 -3
  63. package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
  64. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +2 -2
  65. package/templates/firebase/lib/features/notifications/providers/unread_notifications_count_provider.dart +17 -0
  66. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
  67. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +35 -38
  68. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
  69. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
  70. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
  71. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +1 -1
  72. package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
  73. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
  74. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +13 -6
  75. package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
  76. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
  77. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
  78. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
  79. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  80. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
  81. package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
  82. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
  83. package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
  84. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
  85. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
  86. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
  87. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
  88. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
  89. package/templates/firebase/lib/i18n/en.i18n.json +10 -1
  90. package/templates/firebase/lib/i18n/es.i18n.json +10 -1
  91. package/templates/firebase/lib/i18n/pt.i18n.json +10 -1
  92. package/templates/firebase/pubspec.yaml +0 -1
  93. package/templates/firebase/web/stripe_success.html +64 -26
  94. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  95. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  96. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  97. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  98. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  99. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  100. package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
  101. package/templates/firebase/login-redesign-preview.png +0 -0
@@ -552,11 +552,13 @@ class _KasyDatePickerState extends State<KasyDatePicker>
552
552
  // Key on the trigger field — used to measure its width when opening the popover.
553
553
  final GlobalKey _fieldKey = GlobalKey();
554
554
 
555
- // Focus node owned by the trigger field. We want focus so the
556
- // KasyTextField paints its focus border while the calendar is open — but
557
- // the underlying TextField runs in readOnly mode, so no soft keyboard
558
- // appears even when focused.
559
- final FocusNode _fieldFocusNode = FocusNode();
555
+ // Drives the trigger's "active" (focused-look) border while the calendar or
556
+ // overlay is open. We deliberately do NOT focus the field for this: real
557
+ // focus would (a) light the wrapping KasyFocusRing's keyboard ring on a mouse
558
+ // click a second outline — and (b) get stolen by the overlay after a
559
+ // moment, dropping the border. KasyTextField.forceFocusBorder paints it from
560
+ // this flag instead, so it stays put for as long as the calendar is open.
561
+ bool _triggerActive = false;
560
562
 
561
563
  // Controller backing the trigger KasyTextField — text mirrors the formatted
562
564
  // date (or stays empty so the hint renders).
@@ -639,7 +641,6 @@ class _KasyDatePickerState extends State<KasyDatePicker>
639
641
  @override
640
642
  void dispose() {
641
643
  _displayController.dispose();
642
- _fieldFocusNode.dispose();
643
644
  _animCtrl.dispose();
644
645
  super.dispose();
645
646
  }
@@ -741,11 +742,12 @@ class _KasyDatePickerState extends State<KasyDatePicker>
741
742
 
742
743
  _portalController.show();
743
744
  _animCtrl.forward();
744
- // Focus so the trigger field paints its focus border while the calendar
745
- // is open (only when focusBorder is enabled — otherwise we skip to avoid
746
- // the field announcing focus to assistive tech for no visual reason).
747
- if (widget.focusBorder) _fieldFocusNode.requestFocus();
748
- setState(() => _isOpen = true);
745
+ // Paint the trigger's "active" border while the calendar is open — driven
746
+ // by state, not real focus (see _triggerActive).
747
+ setState(() {
748
+ _isOpen = true;
749
+ _triggerActive = true;
750
+ });
749
751
  }
750
752
 
751
753
  void _close() {
@@ -765,7 +767,7 @@ class _KasyDatePickerState extends State<KasyDatePicker>
765
767
  _pendingPopoverRange = null;
766
768
  widget.onRangeChanged?.call(pendingRange);
767
769
  }
768
- _fieldFocusNode.unfocus();
770
+ setState(() => _triggerActive = false);
769
771
  });
770
772
  setState(() => _isOpen = false);
771
773
  }
@@ -830,7 +832,7 @@ class _KasyDatePickerState extends State<KasyDatePicker>
830
832
  // endpoint right away).
831
833
  DateTime dialogViewMonth = _viewMonth;
832
834
  KasyDateRange? dialogRange = _effectiveRange;
833
- if (widget.focusBorder) _fieldFocusNode.requestFocus();
835
+ setState(() => _triggerActive = true);
834
836
 
835
837
  showKasyDialog<void>(
836
838
  context: context,
@@ -904,12 +906,12 @@ class _KasyDatePickerState extends State<KasyDatePicker>
904
906
  },
905
907
  ),
906
908
  ).whenComplete(() {
907
- if (mounted) _fieldFocusNode.unfocus();
909
+ if (mounted) setState(() => _triggerActive = false);
908
910
  });
909
911
  }
910
912
 
911
913
  void _openBottomSheet() {
912
- if (widget.focusBorder) _fieldFocusNode.requestFocus();
914
+ setState(() => _triggerActive = true);
913
915
  showKasyBottomSheet<void>(
914
916
  context: context,
915
917
  isScrollControlled: true,
@@ -925,7 +927,7 @@ class _KasyDatePickerState extends State<KasyDatePicker>
925
927
  monthsToShow: widget.monthsToShow,
926
928
  ),
927
929
  ).whenComplete(() {
928
- if (mounted) _fieldFocusNode.unfocus();
930
+ if (mounted) setState(() => _triggerActive = false);
929
931
  });
930
932
  }
931
933
 
@@ -1050,15 +1052,18 @@ class _KasyDatePickerState extends State<KasyDatePicker>
1050
1052
  child: KasyTextField(
1051
1053
  key: _fieldKey,
1052
1054
  controller: _displayController,
1053
- focusNode: _fieldFocusNode,
1054
1055
  readOnly: true,
1055
1056
  enabled: widget.enabled,
1056
1057
  hint: _resolvedPlaceholder,
1057
1058
  isInvalid: hasInvalidState,
1058
1059
  variant: widget.variant,
1059
1060
  focusBorder: widget.focusBorder,
1060
- // No caret, no selection handles, no "blue text" when the
1061
- // trigger is focused while the calendar is open keeps the
1061
+ // "Active" border while the calendar is open painted from
1062
+ // state, not real focus, so it never doubles the wrapping
1063
+ // KasyFocusRing's keyboard ring on click and never gets
1064
+ // dropped when the overlay steals focus.
1065
+ forceFocusBorder: widget.focusBorder && _triggerActive,
1066
+ // No caret, no selection handles, no "blue text" — keeps the
1062
1067
  // field reading as a button, not an editable input.
1063
1068
  enableInteractiveSelection: false,
1064
1069
  suffix: widget.showSuffix
@@ -1691,7 +1696,7 @@ class _CalendarNavRow extends StatelessWidget {
1691
1696
  viewMode == _CalendarViewMode.month
1692
1697
  ? KasyIcons.chevronRight
1693
1698
  : KasyIcons.chevronDown,
1694
- size: 18,
1699
+ size: KasyIconSize.md,
1695
1700
  weight: 700,
1696
1701
  color: c.primary,
1697
1702
  ),
@@ -1766,7 +1771,7 @@ class _NavArrowButton extends StatelessWidget {
1766
1771
  child: Center(
1767
1772
  child: Icon(
1768
1773
  icon,
1769
- size: 20,
1774
+ size: KasyIconSize.lg,
1770
1775
  weight: 700,
1771
1776
  color: disabled ? c.muted.withValues(alpha: 0.45) : c.primary,
1772
1777
  ),
@@ -144,7 +144,7 @@ class KasyDialog extends StatelessWidget {
144
144
  padding: const EdgeInsets.all(6),
145
145
  minimumSize: const Size(40, 40),
146
146
  ),
147
- icon: const Icon(KasyIcons.close, size: 19),
147
+ icon: const Icon(KasyIcons.close, size: KasyIconSize.md),
148
148
  ),
149
149
  );
150
150
  }
@@ -449,7 +449,7 @@ class _DialogIconBubble extends StatelessWidget {
449
449
  width: _size,
450
450
  height: _size,
451
451
  decoration: BoxDecoration(color: p.background, shape: BoxShape.circle),
452
- child: Icon(icon, size: 20, color: p.foreground),
452
+ child: Icon(icon, size: KasyIconSize.lg, color: p.foreground),
453
453
  );
454
454
  }
455
455
 
@@ -213,8 +213,14 @@ class KasySidebar extends StatefulWidget {
213
213
  this.profileAvatar,
214
214
  this.profileGradient = KasyAvatarGradients.indigo,
215
215
  this.onProfileTap,
216
+ this.notificationsUnread = 0,
216
217
  });
217
218
 
219
+ /// Unread notification count. When greater than zero, the Notifications nav
220
+ /// item shows an unread dot (mirrors the bottom-bar badge). Purely an unread
221
+ /// indicator — not tied to push (which is native-only).
222
+ final int notificationsUnread;
223
+
218
224
  final VoidCallback? onSettingsTap;
219
225
 
220
226
  /// Whether the profile block is shown at the bottom of the rail. Set false to
@@ -496,6 +502,9 @@ class _KasySidebarState extends State<KasySidebar> {
496
502
  : (widget.routes![i].label ?? ''),
497
503
  isActive: _activeItemId.isEmpty && currentIndex == i,
498
504
  onTap: () => _navigateTo(i),
505
+ showBadge: i < meta.length &&
506
+ meta[i].icon == KasyIcons.notification &&
507
+ widget.notificationsUnread > 0,
499
508
  ),
500
509
  // Static showcase extras (incl. the Income submenu).
501
510
  for (final item in _kMainItems.skip(1))
@@ -842,6 +851,37 @@ class _KasySidebarState extends State<KasySidebar> {
842
851
 
843
852
  // ── Generic row (expanded) / icon+tooltip (collapsed) ────────────────────────
844
853
 
854
+ /// Overlays a small unread dot on the top-right of [child] when [show] is true.
855
+ /// The dot carries a thin border in the sidebar background color so it reads
856
+ /// cleanly over the icon.
857
+ Widget _withBadgeDot({
858
+ required Widget child,
859
+ required bool show,
860
+ required Color dotColor,
861
+ required Color borderColor,
862
+ }) {
863
+ if (!show) return child;
864
+ return Stack(
865
+ clipBehavior: Clip.none,
866
+ children: [
867
+ child,
868
+ Positioned(
869
+ top: -2,
870
+ right: -2,
871
+ child: Container(
872
+ width: 8,
873
+ height: 8,
874
+ decoration: BoxDecoration(
875
+ color: dotColor,
876
+ shape: BoxShape.circle,
877
+ border: Border.all(color: borderColor, width: 1.5),
878
+ ),
879
+ ),
880
+ ),
881
+ ],
882
+ );
883
+ }
884
+
845
885
  Widget _buildItemRow(
846
886
  _SidebarColors c, {
847
887
  required IconData icon,
@@ -849,6 +889,7 @@ class _KasySidebarState extends State<KasySidebar> {
849
889
  required bool isActive,
850
890
  required VoidCallback onTap,
851
891
  bool isLogout = false,
892
+ bool showBadge = false,
852
893
  List<Widget> trailing = const [],
853
894
  double bottomGap = _kItemGap,
854
895
  }) {
@@ -861,14 +902,19 @@ class _KasySidebarState extends State<KasySidebar> {
861
902
  if (_collapsed) {
862
903
  return Padding(
863
904
  padding: EdgeInsets.only(bottom: bottomGap),
864
- child: _ProTooltipIcon(
865
- icon: icon,
866
- label: label,
867
- iconBg: fill,
868
- iconColor: iconColor,
869
- activeBg: c.activeBg,
870
- colors: c,
871
- onTap: onTap,
905
+ child: _withBadgeDot(
906
+ show: showBadge,
907
+ dotColor: c.logout,
908
+ borderColor: c.bg,
909
+ child: _ProTooltipIcon(
910
+ icon: icon,
911
+ label: label,
912
+ iconBg: fill,
913
+ iconColor: iconColor,
914
+ activeBg: c.activeBg,
915
+ colors: c,
916
+ onTap: onTap,
917
+ ),
872
918
  ),
873
919
  );
874
920
  }
@@ -894,7 +940,12 @@ class _KasySidebarState extends State<KasySidebar> {
894
940
  ),
895
941
  child: Row(
896
942
  children: [
897
- Icon(icon, size: _kIconSize, color: iconColor),
943
+ _withBadgeDot(
944
+ show: showBadge,
945
+ dotColor: c.logout,
946
+ borderColor: c.bg,
947
+ child: Icon(icon, size: _kIconSize, color: iconColor),
948
+ ),
898
949
  const SizedBox(width: _kIconGap),
899
950
  Expanded(
900
951
  child: Text(
@@ -1176,7 +1227,7 @@ class _ProHoverPopupIconState extends State<_ProHoverPopupIcon> {
1176
1227
  color: widget.iconBg,
1177
1228
  borderRadius: BorderRadius.circular(_kItemRadius),
1178
1229
  ),
1179
- child: Icon(widget.icon, size: 20, color: widget.iconColor),
1230
+ child: Icon(widget.icon, size: KasyIconSize.lg, color: widget.iconColor),
1180
1231
  ),
1181
1232
  ),
1182
1233
  ),
@@ -1314,7 +1365,7 @@ class _ProTooltipIconState extends State<_ProTooltipIcon> {
1314
1365
  color: widget.iconBg,
1315
1366
  borderRadius: BorderRadius.circular(_kItemRadius),
1316
1367
  ),
1317
- child: Icon(widget.icon, size: 20, color: widget.iconColor),
1368
+ child: Icon(widget.icon, size: KasyIconSize.lg, color: widget.iconColor),
1318
1369
  ),
1319
1370
  ),
1320
1371
  ),
@@ -486,7 +486,7 @@ class _PrimaryTabState extends State<_PrimaryTab> {
486
486
  opacity: disabled ? 0.4 : 1.0,
487
487
  child: Icon(
488
488
  item.icon,
489
- size: 16,
489
+ size: KasyIconSize.sm,
490
490
  color: fg,
491
491
  ),
492
492
  )
@@ -628,7 +628,7 @@ class _SecondaryTabState extends State<_SecondaryTab> {
628
628
  opacity: disabled ? 0.4 : 1.0,
629
629
  child: Icon(
630
630
  item.icon,
631
- size: 16,
631
+ size: KasyIconSize.sm,
632
632
  color: fg,
633
633
  ),
634
634
  ),
@@ -1,6 +1,8 @@
1
1
  import 'package:flutter/foundation.dart' show kIsWeb;
2
2
  import 'package:flutter/material.dart';
3
3
  import 'package:flutter/services.dart';
4
+ import 'package:kasy_kit/components/kasy_text_field.dart'
5
+ show KasyTextFieldVariant;
4
6
  import 'package:kasy_kit/core/theme/theme.dart';
5
7
 
6
8
  /// Kasy Design System — multi-line text area.
@@ -31,6 +33,11 @@ class KasyTextArea extends StatefulWidget {
31
33
  final int? maxLines;
32
34
  final int? maxLength;
33
35
 
36
+ /// Visual treatment — same set as [KasyTextField] (primary / secondary /
37
+ /// flat / embedded). [flat] is the surface fill with a hairline border and
38
+ /// NO shadow. Defaults to [primary].
39
+ final KasyTextFieldVariant variant;
40
+
34
41
  const KasyTextArea({
35
42
  super.key,
36
43
  this.controller,
@@ -53,6 +60,7 @@ class KasyTextArea extends StatefulWidget {
53
60
  this.minLines = 4,
54
61
  this.maxLines = 8,
55
62
  this.maxLength,
63
+ this.variant = KasyTextFieldVariant.primary,
56
64
  });
57
65
 
58
66
  @override
@@ -129,6 +137,10 @@ class _KasyTextAreaState extends State<KasyTextArea> {
129
137
  @override
130
138
  Widget build(BuildContext context) {
131
139
  final bool isDisabled = !widget.enabled;
140
+ final bool isSecondary = widget.variant == KasyTextFieldVariant.secondary;
141
+ final bool isEmbedded = widget.variant == KasyTextFieldVariant.embedded;
142
+ final bool isFlat = widget.variant == KasyTextFieldVariant.flat;
143
+ final bool showShadow = widget.variant == KasyTextFieldVariant.primary;
132
144
  final bool hasInvalidState = widget.isInvalid || widget.errorText != null;
133
145
  final bool useForm = widget.validator != null || widget.onSaved != null;
134
146
 
@@ -156,12 +168,24 @@ class _KasyTextAreaState extends State<KasyTextArea> {
156
168
  final Color resolvedFocusedBorderColor = hasInvalidState
157
169
  ? context.colors.error
158
170
  : focusedBorderColor;
159
- final Color enabledBorderColor = KasyShadows.inputFieldRestingBorder(context);
171
+ final Color enabledBorderColor = isFlat
172
+ ? KasyShadows.inputFieldFlatBorder(context)
173
+ : KasyShadows.inputFieldRestingBorder(context);
160
174
  final Color restingBorderColor = hasInvalidState
161
175
  ? context.colors.error
162
176
  : enabledBorderColor;
163
- final double restingBorderWidth = hasInvalidState ? 1.3 : 1;
164
- final Color surfaceColor = context.colors.surface;
177
+ final double restingBorderWidth = isEmbedded
178
+ ? 0
179
+ : hasInvalidState
180
+ ? 1.3
181
+ : 1;
182
+ final Color surfaceColor = isEmbedded
183
+ ? Colors.transparent
184
+ : isSecondary
185
+ ? (context.isDark
186
+ ? const Color(0xFF272729)
187
+ : const Color(0xFFF0F0F2))
188
+ : context.colors.surface;
165
189
  final Color fieldFillColor = isDisabled
166
190
  ? surfaceColor.withValues(alpha: context.isDark ? 0.9 : 0.94)
167
191
  : surfaceColor;
@@ -348,9 +372,17 @@ class _KasyTextAreaState extends State<KasyTextArea> {
348
372
  DecoratedBox(
349
373
  decoration: BoxDecoration(
350
374
  borderRadius: fieldRadius,
351
- boxShadow: [fieldShadow],
375
+ boxShadow: showShadow ? [fieldShadow] : null,
376
+ ),
377
+ // Force standard visual density so the area renders identically on
378
+ // web/desktop and mobile (Flutter's adaptive density is compact on
379
+ // web and would otherwise shrink line spacing). Mirrors KasyTextField.
380
+ child: Theme(
381
+ data: Theme.of(context).copyWith(
382
+ visualDensity: VisualDensity.standard,
383
+ ),
384
+ child: innerField,
352
385
  ),
353
- child: innerField,
354
386
  ),
355
387
  if (hasHelperText || hasCounter) ...[
356
388
  const SizedBox(height: KasySpacing.xs),
@@ -3,7 +3,12 @@ import 'package:flutter/material.dart';
3
3
  import 'package:flutter/services.dart';
4
4
  import 'package:kasy_kit/core/theme/theme.dart';
5
5
 
6
- enum KasyTextFieldVariant { primary, secondary, embedded }
6
+ /// Visual treatments for [KasyTextField]:
7
+ /// - [primary] filled (surface) + hairline border + soft shadow
8
+ /// - [secondary] elevated fill (contrasts on a surface/card) + border, no shadow
9
+ /// - [flat] primary's surface fill + border, but NO shadow (e.g. header search)
10
+ /// - [embedded] transparent, no border, no shadow
11
+ enum KasyTextFieldVariant { primary, secondary, embedded, flat }
7
12
 
8
13
  enum KasyTextFieldContentType { text, email, password, phone }
9
14
 
@@ -18,7 +23,13 @@ class KasyTextField extends StatefulWidget {
18
23
  static const double adjacentFieldSpacing = KasySpacing.md;
19
24
  static const double iconSlotExtent = 38;
20
25
  static const double iconGlyphSize = 17;
21
- static const double webSingleLineVerticalPadding = 16;
26
+
27
+ /// Canonical resting height for a single-line field. Drives the field's
28
+ /// vertical content padding (what the filled/bordered box actually wraps), so
29
+ /// changing this value grows or shrinks the visible box on every platform.
30
+ /// Matches the medium [KasyButton] height (45) so fields, the DatePicker
31
+ /// trigger and the primary action all share one control height.
32
+ static const double singleLineHeight = 41;
22
33
 
23
34
  final TextEditingController? controller;
24
35
  final FocusNode? focusNode;
@@ -57,9 +68,10 @@ class KasyTextField extends StatefulWidget {
57
68
  final Widget? labelTrailing;
58
69
 
59
70
  /// Override for the field's vertical/horizontal padding. When null, the
60
- /// design-system default is used (`KasySpacing.md` horizontal, `10`
61
- /// vertical on mobile / `webSingleLineVerticalPadding` on web). Pass a
62
- /// smaller value to make the field render shorter.
71
+ /// design-system default is used (`KasySpacing.md` horizontal; single-line
72
+ /// fields use 0 vertical and take their height from the [singleLineHeight]
73
+ /// SizedBox, multi-line uses `13`). Passing a custom value opts the field out
74
+ /// of the fixed single-line height (e.g. the compact header search).
63
75
  final EdgeInsetsGeometry? contentPadding;
64
76
 
65
77
  /// Override for the field's drop shadow. When null, the design-system
@@ -74,6 +86,14 @@ class KasyTextField extends StatefulWidget {
74
86
  /// affordance would feel noisy.
75
87
  final bool focusBorder;
76
88
 
89
+ /// When true, paints the focused (primary) border even though the field holds
90
+ /// no real focus. Composite controls like [KasyDatePicker] use this to show
91
+ /// the trigger as "active" while their overlay is open WITHOUT taking focus —
92
+ /// taking real focus would double up with the wrapper's keyboard focus ring
93
+ /// (a second outline on mouse click) and can be stolen by the overlay after a
94
+ /// moment. No effect on the embedded variant.
95
+ final bool forceFocusBorder;
96
+
77
97
  /// Forwards to [TextField.enableInteractiveSelection]. When false, the
78
98
  /// field renders no caret, suppresses text-selection gestures, and stops
79
99
  /// showing the I-beam cursor on web/desktop — handy for read-only triggers
@@ -117,6 +137,7 @@ class KasyTextField extends StatefulWidget {
117
137
  this.contentPadding,
118
138
  this.boxShadow,
119
139
  this.focusBorder = true,
140
+ this.forceFocusBorder = false,
120
141
  this.enableInteractiveSelection = true,
121
142
  });
122
143
 
@@ -239,6 +260,7 @@ class _KasyTextFieldState extends State<KasyTextField> {
239
260
  widget.variant == KasyTextFieldVariant.secondary;
240
261
  final bool isEmbeddedVariant =
241
262
  widget.variant == KasyTextFieldVariant.embedded;
263
+ final bool isFlatVariant = widget.variant == KasyTextFieldVariant.flat;
242
264
  // (No more web-specific padding — the field now uses the same vertical
243
265
  // padding on every platform so primary/web TextFields render at the same
244
266
  // height as mobile and as the KasyDatePicker trigger.)
@@ -265,6 +287,24 @@ class _KasyTextFieldState extends State<KasyTextField> {
265
287
  final bool resolvedObscureText = isPassword
266
288
  ? !_passwordVisible
267
289
  : widget.obscureText;
290
+ // Single-line fields with the default padding get a fixed canonical height
291
+ // so they match the medium KasyButton and render identically on every
292
+ // platform. Multi-line fields (minLines set, or maxLines > 1) keep growing
293
+ // with their content; callers that pass a custom [contentPadding] (e.g. the
294
+ // compact header search) are opting into their own height, so the lock is
295
+ // skipped for them.
296
+ final bool isSingleLineField =
297
+ widget.contentPadding == null &&
298
+ widget.minLines == null &&
299
+ (resolvedObscureText || (widget.maxLines ?? 1) == 1);
300
+ // Height of the filled/bordered box = single-line text height + 2× vertical
301
+ // padding. So to hit [singleLineHeight] we back out the padding from it
302
+ // (subtract one text line, halve). This is the only lever that actually
303
+ // stretches the visible box — constraints/SizedBox just pad around it.
304
+ const double singleLineTextHeight = 19;
305
+ final double singleLineVerticalPadding =
306
+ ((KasyTextField.singleLineHeight - singleLineTextHeight) / 2)
307
+ .clamp(0.0, 60.0);
268
308
  final Iterable<String>? resolvedAutofillHints =
269
309
  widget.autofillHints ?? _defaultAutofillHints(widget.contentType);
270
310
  final TextInputType? resolvedKeyboardType =
@@ -320,7 +360,7 @@ class _KasyTextFieldState extends State<KasyTextField> {
320
360
  // platform — removed the previous `!kIsWeb` guard so web matches mobile
321
361
  // and the KasyDatePicker trigger.
322
362
  final bool shouldShowShadow =
323
- !isSecondaryVariant && !isEmbeddedVariant;
363
+ !isSecondaryVariant && !isEmbeddedVariant && !isFlatVariant;
324
364
  final BoxShadow resolvedShadow = BoxShadow(
325
365
  color: const Color(0xFF000000).withValues(
326
366
  alpha: context.isDark ? 0.28 : 0.11,
@@ -336,9 +376,9 @@ class _KasyTextFieldState extends State<KasyTextField> {
336
376
  final Color resolvedFocusedBorderColor = hasInvalidState
337
377
  ? context.colors.error
338
378
  : focusedBorderColor;
339
- final Color enabledBorderColor = KasyShadows.inputFieldRestingBorder(
340
- context,
341
- );
379
+ final Color enabledBorderColor = isFlatVariant
380
+ ? KasyShadows.inputFieldFlatBorder(context)
381
+ : KasyShadows.inputFieldRestingBorder(context);
342
382
  final Color restingBorderColor = hasInvalidState
343
383
  ? context.colors.error
344
384
  : enabledBorderColor;
@@ -351,14 +391,22 @@ class _KasyTextFieldState extends State<KasyTextField> {
351
391
  borderRadius: fieldRadius,
352
392
  borderSide: BorderSide.none,
353
393
  );
394
+ // When forceFocusBorder is set, the always-visible (resting) border adopts
395
+ // the focused look — so a composite trigger reads as "active" without the
396
+ // field ever holding real focus.
354
397
  final InputBorder resolvedEnabledBorder = isEmbeddedVariant
355
398
  ? embeddedBorder
356
399
  : OutlineInputBorder(
357
400
  borderRadius: fieldRadius,
358
- borderSide: BorderSide(
359
- color: restingBorderColor,
360
- width: restingBorderWidth,
361
- ),
401
+ borderSide: widget.forceFocusBorder
402
+ ? BorderSide(
403
+ color: resolvedFocusedBorderColor,
404
+ width: focusedBorderWidth,
405
+ )
406
+ : BorderSide(
407
+ color: restingBorderColor,
408
+ width: restingBorderWidth,
409
+ ),
362
410
  );
363
411
  // When focusBorder is disabled, the focused state collapses to the
364
412
  // resting border so the field never grows that bright outline. Error
@@ -462,10 +510,12 @@ class _KasyTextFieldState extends State<KasyTextField> {
462
510
  width: KasyTextField.iconSlotExtent,
463
511
  height: KasyTextField.iconSlotExtent,
464
512
  ),
513
+ // Single-line height comes from this vertical padding (derived from
514
+ // singleLineHeight); multi-line keeps a fixed comfortable padding.
465
515
  contentPadding: widget.contentPadding ??
466
516
  EdgeInsets.symmetric(
467
517
  horizontal: isEmbeddedVariant ? 0 : KasySpacing.md,
468
- vertical: 13,
518
+ vertical: isSingleLineField ? singleLineVerticalPadding : 13,
469
519
  ),
470
520
  fillColor: fieldFillColor,
471
521
  filled: !isEmbeddedVariant,
@@ -548,8 +598,19 @@ class _KasyTextFieldState extends State<KasyTextField> {
548
598
  child: Semantics(
549
599
  textField: true,
550
600
  enabled: widget.enabled,
551
- label: widget.semanticLabel ?? widget.label ?? widget.hint ?? 'Input',
552
- child: innerField,
601
+ label:
602
+ widget.semanticLabel ?? widget.label ?? widget.hint ?? 'Text field',
603
+ // Force standard visual density so the field renders at the same height
604
+ // on web/desktop as on mobile (Flutter's adaptive density is compact on
605
+ // web and would otherwise shave ~4px). The height itself comes from the
606
+ // vertical contentPadding below — that's what the filled/bordered box
607
+ // wraps, so it grows/shrinks the box you actually see.
608
+ child: Theme(
609
+ data: Theme.of(context).copyWith(
610
+ visualDensity: VisualDensity.standard,
611
+ ),
612
+ child: innerField,
613
+ ),
553
614
  ),
554
615
  );
555
616
  final Widget? labelTrailing = widget.labelTrailing;
@@ -72,7 +72,7 @@ class KasyToast extends StatelessWidget {
72
72
  children: [
73
73
  Padding(
74
74
  padding: const EdgeInsets.only(top: 2),
75
- child: icon ?? Icon(pal.icon, size: 24, color: pal.accent),
75
+ child: icon ?? Icon(pal.icon, size: KasyIconSize.xl, color: pal.accent),
76
76
  ),
77
77
  const SizedBox(width: KasySpacing.smd),
78
78
  Expanded(
@@ -148,13 +148,14 @@ class KasyWebHeader extends StatelessWidget {
148
148
 
149
149
  Widget _buildSearch(BuildContext context) {
150
150
  return KasyTextField(
151
+ variant: KasyTextFieldVariant.flat,
151
152
  controller: searchController,
152
153
  hint: searchHint,
153
154
  onChanged: onSearchChanged,
154
155
  onSubmitted: onSearchSubmitted,
155
156
  prefix: Icon(
156
157
  KasyIcons.search,
157
- size: 17,
158
+ size: KasyIconSize.md,
158
159
  color: context.colors.muted,
159
160
  ),
160
161
  contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
@@ -168,7 +169,7 @@ class KasyWebHeader extends StatelessWidget {
168
169
  variant: KasyButtonVariant.ghost,
169
170
  size: KasyButtonSize.small,
170
171
  iconOnlyLayoutExtent: 36,
171
- iconGlyphSize: 18,
172
+ iconGlyphSize: KasyIconSize.md,
172
173
  onPressed: onToggleTheme,
173
174
  semanticLabel: isDark ? 'Light mode' : 'Dark mode',
174
175
  );
@@ -181,7 +182,7 @@ class KasyWebHeader extends StatelessWidget {
181
182
  variant: KasyButtonVariant.ghost,
182
183
  size: KasyButtonSize.small,
183
184
  iconOnlyLayoutExtent: 36,
184
- iconGlyphSize: 18,
185
+ iconGlyphSize: KasyIconSize.md,
185
186
  onPressed: onNotifications,
186
187
  semanticLabel: 'Notifications',
187
188
  );
@@ -14,6 +14,7 @@ import 'package:kasy_kit/core/states/logout_action.dart';
14
14
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
15
15
  import 'package:kasy_kit/core/theme/theme.dart';
16
16
  import 'package:kasy_kit/core/widgets/responsive_layout.dart';
17
+ import 'package:kasy_kit/features/notifications/providers/unread_notifications_count_provider.dart';
17
18
  import 'package:kasy_kit/features/settings/ui/widgets/kasy_user_avatar.dart';
18
19
  import 'package:kasy_kit/i18n/translations.g.dart';
19
20
 
@@ -64,6 +65,10 @@ class BottomMenu extends StatelessWidget {
64
65
  builder: (context, ref, _) {
65
66
  final User user =
66
67
  ref.watch(userStateNotifierProvider).user;
68
+ final int unread = ref
69
+ .watch(unreadNotificationsCountProvider)
70
+ .value ??
71
+ 0;
67
72
  final (String name, String email) = switch (user) {
68
73
  final AuthenticatedUserData u => (
69
74
  (u.name?.isNotEmpty ?? false)
@@ -81,6 +86,7 @@ class BottomMenu extends StatelessWidget {
81
86
  profileName: name,
82
87
  profileEmail: email,
83
88
  profileAvatar: const KasyUserAvatar(),
89
+ notificationsUnread: unread,
84
90
  );
85
91
  },
86
92
  ),