kasy-cli 1.31.14 → 1.34.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 (127) hide show
  1. package/bin/kasy.js +42 -0
  2. package/lib/commands/apple-web.js +222 -0
  3. package/lib/commands/configure.js +3 -91
  4. package/lib/commands/doctor.js +20 -0
  5. package/lib/commands/facebook.js +189 -0
  6. package/lib/commands/new.js +65 -3
  7. package/lib/scaffold/CHANGELOG.json +27 -0
  8. package/lib/scaffold/backends/api/patch/README.md +87 -2
  9. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +34 -0
  10. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  11. package/lib/scaffold/backends/firebase/setup-from-scratch.js +186 -0
  12. package/lib/scaffold/backends/supabase/deploy.js +92 -0
  13. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
  14. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
  15. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
  16. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
  17. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +22 -0
  18. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  19. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +3 -2
  20. package/lib/scaffold/generate.js +1 -1
  21. package/lib/scaffold/shared/generator-utils.js +34 -3
  22. package/lib/utils/apple-web.js +147 -0
  23. package/lib/utils/facebook.js +162 -0
  24. package/lib/utils/i18n/messages-en.js +64 -0
  25. package/lib/utils/i18n/messages-es.js +64 -0
  26. package/lib/utils/i18n/messages-pt.js +64 -0
  27. package/package.json +2 -2
  28. package/templates/firebase/AGENTS.md +87 -0
  29. package/templates/firebase/CLAUDE.md +16 -0
  30. package/templates/firebase/DESIGN_SYSTEM.md +234 -0
  31. package/templates/firebase/docs/auth-setup.en.md +7 -1
  32. package/templates/firebase/docs/auth-setup.es.md +7 -1
  33. package/templates/firebase/docs/auth-setup.pt.md +7 -1
  34. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
  35. package/templates/firebase/lib/components/components.dart +1 -0
  36. package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
  37. package/templates/firebase/lib/components/kasy_alert.dart +1 -1
  38. package/templates/firebase/lib/components/kasy_app_bar.dart +7 -4
  39. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
  40. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  41. package/templates/firebase/lib/components/kasy_chip.dart +1 -1
  42. package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
  43. package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
  44. package/templates/firebase/lib/components/kasy_screen.dart +114 -0
  45. package/templates/firebase/lib/components/kasy_sidebar.dart +2 -2
  46. package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
  47. package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
  48. package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
  49. package/templates/firebase/lib/components/kasy_toast.dart +39 -70
  50. package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
  51. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
  52. package/templates/firebase/lib/core/config/features.dart +18 -0
  53. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
  54. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
  55. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +46 -124
  56. package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
  57. package/templates/firebase/lib/core/theme/shadows.dart +13 -0
  58. package/templates/firebase/lib/core/theme/texts.dart +32 -0
  59. package/templates/firebase/lib/core/theme/theme.dart +2 -0
  60. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
  61. package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
  62. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
  63. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -7
  64. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
  65. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  66. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
  67. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +61 -0
  68. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
  69. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
  70. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
  71. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +57 -29
  72. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +47 -25
  73. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
  74. package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
  75. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
  76. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +2 -3
  77. package/templates/firebase/lib/features/home/home_components_page.dart +7 -1
  78. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +54 -3
  79. package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
  80. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +165 -209
  81. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
  82. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
  83. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
  84. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -6
  85. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
  86. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +104 -156
  87. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
  88. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
  89. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
  90. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
  91. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
  92. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +3 -2
  93. package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
  94. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +17 -8
  95. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +4 -4
  96. package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
  97. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
  98. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
  99. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
  100. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  101. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
  102. package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
  103. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
  104. package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
  105. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
  106. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
  107. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
  108. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
  109. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
  110. package/templates/firebase/lib/i18n/en.i18n.json +13 -4
  111. package/templates/firebase/lib/i18n/es.i18n.json +13 -4
  112. package/templates/firebase/lib/i18n/pt.i18n.json +13 -4
  113. package/templates/firebase/lib/router.dart +2 -0
  114. package/templates/firebase/pubspec.yaml +1 -2
  115. package/templates/firebase/tool/design_check.dart +152 -0
  116. package/templates/firebase/web/stripe_success.html +64 -26
  117. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  118. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  119. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  120. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  121. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  122. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  123. package/templates/firebase/assets/images/review.png +0 -0
  124. package/templates/firebase/assets/images/update.png +0 -0
  125. package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
  126. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
  127. 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
 
@@ -0,0 +1,114 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:kasy_kit/components/kasy_card.dart';
3
+ import 'package:kasy_kit/core/theme/theme.dart';
4
+ import 'package:kasy_kit/core/widgets/page_background.dart';
5
+
6
+ /// Canonical screen scaffold. A new page that uses it inherits the internal
7
+ /// screen contract for free: the page background, a centred content column
8
+ /// capped at [maxContentWidth], the page gutter, scroll handling, and
9
+ /// (optionally) a [KasyCard] wrapper — all from design-system tokens.
10
+ ///
11
+ /// Opt-in by design: it is a thin convenience over [Scaffold] (it forwards the
12
+ /// same `appBar`, `floatingActionButton`, `bottomNavigationBar` slots), not a
13
+ /// cage. Screens that need bespoke chrome can keep using [Scaffold] directly —
14
+ /// nothing here is forced, and any value is overridable.
15
+ ///
16
+ /// ```dart
17
+ /// KasyScreen(
18
+ /// appBar: KasyAppBar.root(title: 'Settings'),
19
+ /// card: true,
20
+ /// child: Column(children: [...]),
21
+ /// )
22
+ /// ```
23
+ class KasyScreen extends StatelessWidget {
24
+ const KasyScreen({
25
+ super.key,
26
+ required this.child,
27
+ this.appBar,
28
+ this.scrollable = true,
29
+ this.maxContentWidth = 600,
30
+ this.padding,
31
+ this.card = false,
32
+ this.backgroundColor,
33
+ this.resizeToAvoidBottomInset,
34
+ this.floatingActionButton,
35
+ this.bottomNavigationBar,
36
+ });
37
+
38
+ /// Page content.
39
+ final Widget child;
40
+
41
+ /// Optional top bar. Same slot as [Scaffold.appBar]; pass any
42
+ /// [PreferredSizeWidget].
43
+ final PreferredSizeWidget? appBar;
44
+
45
+ /// Wrap the content in a scroll view (default true). Set false for screens
46
+ /// that manage their own scrolling (e.g. a full-bleed list).
47
+ final bool scrollable;
48
+
49
+ /// Max width of the content column on wide screens. The internal-screen
50
+ /// contract is ~600; content is centred within the available width so it does
51
+ /// not stretch edge-to-edge on web/desktop.
52
+ final double maxContentWidth;
53
+
54
+ /// Padding around the content. Defaults to the horizontal page gutter plus
55
+ /// vertical breathing room.
56
+ final EdgeInsetsGeometry? padding;
57
+
58
+ /// Wrap the content in an elevated [KasyCard] (radius 16), matching the Home
59
+ /// reference look.
60
+ final bool card;
61
+
62
+ /// Page background. Defaults to the theme background token.
63
+ final Color? backgroundColor;
64
+
65
+ /// Forwarded to [Scaffold.resizeToAvoidBottomInset].
66
+ final bool? resizeToAvoidBottomInset;
67
+
68
+ /// Forwarded to [Scaffold.floatingActionButton].
69
+ final Widget? floatingActionButton;
70
+
71
+ /// Forwarded to [Scaffold.bottomNavigationBar].
72
+ final Widget? bottomNavigationBar;
73
+
74
+ @override
75
+ Widget build(BuildContext context) {
76
+ final EdgeInsetsGeometry resolvedPadding = padding ??
77
+ const EdgeInsets.symmetric(
78
+ horizontal: KasySpacing.pageHorizontalGutter,
79
+ vertical: KasySpacing.lg,
80
+ );
81
+
82
+ Widget content = ConstrainedBox(
83
+ constraints: BoxConstraints(maxWidth: maxContentWidth),
84
+ child: card ? KasyCard(child: child) : child,
85
+ );
86
+
87
+ content = Align(
88
+ alignment: Alignment.topCenter,
89
+ child: Padding(padding: resolvedPadding, child: content),
90
+ );
91
+
92
+ if (scrollable) {
93
+ content = SingleChildScrollView(
94
+ physics: const ClampingScrollPhysics(),
95
+ keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
96
+ child: content,
97
+ );
98
+ }
99
+
100
+ return Scaffold(
101
+ backgroundColor: backgroundColor ?? context.colors.background,
102
+ resizeToAvoidBottomInset: resizeToAvoidBottomInset,
103
+ appBar: appBar,
104
+ floatingActionButton: floatingActionButton,
105
+ bottomNavigationBar: bottomNavigationBar,
106
+ body: Background(
107
+ bgColor: backgroundColor,
108
+ // top:false when an app bar is present — Scaffold already insets the
109
+ // body below it, so SafeArea only needs to guard the bottom/sides.
110
+ child: SafeArea(top: appBar == null, child: content),
111
+ ),
112
+ );
113
+ }
114
+ }
@@ -1227,7 +1227,7 @@ class _ProHoverPopupIconState extends State<_ProHoverPopupIcon> {
1227
1227
  color: widget.iconBg,
1228
1228
  borderRadius: BorderRadius.circular(_kItemRadius),
1229
1229
  ),
1230
- child: Icon(widget.icon, size: 20, color: widget.iconColor),
1230
+ child: Icon(widget.icon, size: KasyIconSize.lg, color: widget.iconColor),
1231
1231
  ),
1232
1232
  ),
1233
1233
  ),
@@ -1365,7 +1365,7 @@ class _ProTooltipIconState extends State<_ProTooltipIcon> {
1365
1365
  color: widget.iconBg,
1366
1366
  borderRadius: BorderRadius.circular(_kItemRadius),
1367
1367
  ),
1368
- child: Icon(widget.icon, size: 20, color: widget.iconColor),
1368
+ child: Icon(widget.icon, size: KasyIconSize.lg, color: widget.iconColor),
1369
1369
  ),
1370
1370
  ),
1371
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;