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.
- package/lib/commands/new.js +15 -1
- package/lib/scaffold/CHANGELOG.json +9 -0
- package/lib/scaffold/backends/api/patch/README.md +87 -2
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +34 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +22 -0
- package/lib/scaffold/backends/supabase/deploy.js +5 -0
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +69 -17
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +6 -0
- package/lib/scaffold/generate.js +1 -1
- package/lib/scaffold/shared/generator-utils.js +22 -3
- package/lib/utils/i18n/messages-en.js +2 -0
- package/lib/utils/i18n/messages-es.js +2 -0
- package/lib/utils/i18n/messages-pt.js +2 -0
- package/package.json +2 -2
- package/templates/firebase/docs/auth-setup.en.md +7 -1
- package/templates/firebase/docs/auth-setup.es.md +7 -1
- package/templates/firebase/docs/auth-setup.pt.md +7 -1
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
- package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
- package/templates/firebase/lib/components/kasy_alert.dart +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +3 -3
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
- package/templates/firebase/lib/components/kasy_button.dart +8 -8
- package/templates/firebase/lib/components/kasy_chip.dart +1 -1
- package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
- package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
- package/templates/firebase/lib/components/kasy_sidebar.dart +62 -11
- package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
- package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
- package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
- package/templates/firebase/lib/components/kasy_toast.dart +1 -1
- package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +6 -0
- package/templates/firebase/lib/core/bottom_menu/notification_bottom_item.dart +16 -37
- package/templates/firebase/lib/core/config/features.dart +13 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
- package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +1 -1
- package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
- package/templates/firebase/lib/core/theme/shadows.dart +13 -0
- package/templates/firebase/lib/core/theme/texts.dart +32 -0
- package/templates/firebase/lib/core/theme/theme.dart +2 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
- package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
- package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
- package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
- package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +36 -14
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +27 -11
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +22 -3
- package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +2 -2
- package/templates/firebase/lib/features/notifications/providers/unread_notifications_count_provider.dart +17 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +35 -38
- package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +1 -1
- package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
- package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +13 -6
- package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
- package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
- package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
- package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
- package/templates/firebase/lib/i18n/en.i18n.json +10 -1
- package/templates/firebase/lib/i18n/es.i18n.json +10 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +10 -1
- package/templates/firebase/pubspec.yaml +0 -1
- package/templates/firebase/web/stripe_success.html +64 -26
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
- package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
- 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
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
//
|
|
559
|
-
|
|
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
|
-
//
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
909
|
+
if (mounted) setState(() => _triggerActive = false);
|
|
908
910
|
});
|
|
909
911
|
}
|
|
910
912
|
|
|
911
913
|
void _openBottomSheet() {
|
|
912
|
-
|
|
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)
|
|
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
|
-
//
|
|
1061
|
-
//
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
61
|
-
/// vertical
|
|
62
|
-
///
|
|
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 =
|
|
340
|
-
|
|
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:
|
|
359
|
-
|
|
360
|
-
|
|
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:
|
|
552
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
),
|