kasy-cli 1.31.14 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/lib/commands/new.js +15 -1
  2. package/lib/scaffold/CHANGELOG.json +9 -0
  3. package/lib/scaffold/backends/api/patch/README.md +87 -2
  4. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +34 -0
  5. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  6. package/lib/scaffold/backends/firebase/setup-from-scratch.js +22 -0
  7. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
  8. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
  9. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
  10. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
  11. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +12 -0
  12. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  13. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +3 -2
  14. package/lib/scaffold/generate.js +1 -1
  15. package/lib/scaffold/shared/generator-utils.js +22 -3
  16. package/lib/utils/i18n/messages-en.js +2 -0
  17. package/lib/utils/i18n/messages-es.js +2 -0
  18. package/lib/utils/i18n/messages-pt.js +2 -0
  19. package/package.json +2 -2
  20. package/templates/firebase/docs/auth-setup.en.md +7 -1
  21. package/templates/firebase/docs/auth-setup.es.md +7 -1
  22. package/templates/firebase/docs/auth-setup.pt.md +7 -1
  23. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
  24. package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
  25. package/templates/firebase/lib/components/kasy_alert.dart +1 -1
  26. package/templates/firebase/lib/components/kasy_app_bar.dart +3 -3
  27. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
  28. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  29. package/templates/firebase/lib/components/kasy_chip.dart +1 -1
  30. package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
  31. package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
  32. package/templates/firebase/lib/components/kasy_sidebar.dart +2 -2
  33. package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
  34. package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
  35. package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
  36. package/templates/firebase/lib/components/kasy_toast.dart +1 -1
  37. package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
  38. package/templates/firebase/lib/core/config/features.dart +13 -0
  39. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
  40. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
  41. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +1 -1
  42. package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
  43. package/templates/firebase/lib/core/theme/shadows.dart +13 -0
  44. package/templates/firebase/lib/core/theme/texts.dart +32 -0
  45. package/templates/firebase/lib/core/theme/theme.dart +2 -0
  46. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
  47. package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
  48. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +1 -1
  49. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
  50. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
  51. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
  52. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
  53. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +36 -14
  54. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +27 -11
  55. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
  56. package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
  57. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
  58. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -1
  59. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +22 -3
  60. package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
  61. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +2 -2
  62. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
  63. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +35 -38
  64. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
  65. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
  66. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
  67. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +1 -1
  68. package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
  69. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
  70. package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
  71. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
  72. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
  73. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
  74. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  75. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
  76. package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
  77. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
  78. package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
  79. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
  80. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
  81. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
  82. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
  83. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
  84. package/templates/firebase/lib/i18n/en.i18n.json +8 -0
  85. package/templates/firebase/lib/i18n/es.i18n.json +8 -0
  86. package/templates/firebase/lib/i18n/pt.i18n.json +8 -0
  87. package/templates/firebase/pubspec.yaml +0 -1
  88. package/templates/firebase/web/stripe_success.html +64 -26
  89. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  90. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  91. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  92. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  93. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  94. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  95. package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
  96. package/templates/firebase/login-redesign-preview.png +0 -0
@@ -1,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
  );
@@ -6,6 +6,12 @@ const bool withAiChat = true;
6
6
  const bool withFeedback = true;
7
7
  const bool withRevenuecat = true;
8
8
  const bool withLocalReminders = true;
9
+ /// When true, the Apple sign-in button is shown on WEB. Apple-on-web needs a paid
10
+ /// Apple Service ID + manual setup (see docs/auth-setup): Firebase can do it
11
+ /// (signInWithPopup), so this ships `true`; Supabase/API can't (no web secret), so
12
+ /// the CLI generates them as `false`. Apple shows on iOS/macOS regardless; it is
13
+ /// hidden on Android (also needs a paid Service ID, and the native flow throws).
14
+ const bool withAppleWebSignin = true;
9
15
  /// When true, the app includes web support:
10
16
  /// - anonymous sign-up is disabled on web (user is redirected to /signin)
11
17
  /// - onboarding is skipped on web
@@ -14,3 +20,10 @@ const bool withWeb = true;
14
20
  /// When true, the Stripe web-subscription module is included (web checkout +
15
21
  /// customer portal). Independent from RevenueCat (which stays mobile-only).
16
22
  const bool withStripe = true;
23
+ /// When true, Stripe Checkout shows a promo-code / coupon field.
24
+ /// Set to false if you have no promotions strategy yet.
25
+ const bool withStripePromoCodes = true;
26
+ /// When true, the Stripe Customer Portal lets subscribers switch plans
27
+ /// (upgrade / downgrade). Requires at least two recurring prices on the same
28
+ /// product. The portal configuration is created automatically on first use.
29
+ const bool withStripePlanSwitching = true;
@@ -36,6 +36,13 @@ final ValueNotifier<bool> devInspectorEnabledNotifier =
36
36
  final ValueNotifier<bool> devInspectorCopyTriggerNotifier =
37
37
  ValueNotifier<bool>(false);
38
38
 
39
+ /// Set to true to clear the current selection WITHOUT deactivating the
40
+ /// inspector. The Web Device Preview toggle fires this when entering/leaving
41
+ /// the device frame so a stale highlight doesn't linger across the transition.
42
+ /// [DevInspector] clears the selection and resets this to false.
43
+ final ValueNotifier<bool> devInspectorClearSelectionTriggerNotifier =
44
+ ValueNotifier<bool>(false);
45
+
39
46
  /// Runtime active state of the inspector. Mirrors [devInspectorEnabledNotifier]
40
47
  /// — the Web Device Preview pill, the admin toggle and the Esc shortcut all
41
48
  /// flip the persisted notifier, and this one follows.
@@ -127,6 +134,8 @@ class _DevInspectorState extends State<DevInspector>
127
134
  devInspectorActiveNotifier.addListener(_handleActiveChanged);
128
135
  devInspectorEnabledNotifier.addListener(_handleEnabledChanged);
129
136
  devInspectorCopyTriggerNotifier.addListener(_onCopyTriggered);
137
+ devInspectorClearSelectionTriggerNotifier
138
+ .addListener(_onClearSelectionTriggered);
130
139
  HardwareKeyboard.instance.addHandler(_handleKeyEvent);
131
140
  unawaited(_bootstrapEnabledPreference());
132
141
  }
@@ -139,6 +148,8 @@ class _DevInspectorState extends State<DevInspector>
139
148
  HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
140
149
  devInspectorEnabledNotifier.removeListener(_handleEnabledChanged);
141
150
  devInspectorCopyTriggerNotifier.removeListener(_onCopyTriggered);
151
+ devInspectorClearSelectionTriggerNotifier
152
+ .removeListener(_onClearSelectionTriggered);
142
153
  devInspectorActiveNotifier.removeListener(_handleActiveChanged);
143
154
  if (devInspectorActiveNotifier.value) {
144
155
  devInspectorActiveNotifier.value = false;
@@ -312,6 +323,16 @@ class _DevInspectorState extends State<DevInspector>
312
323
  unawaited(_copySelection());
313
324
  }
314
325
 
326
+ /// Drops the current selection while keeping the inspector active, so a stale
327
+ /// highlight doesn't carry over when the Web Device Preview is toggled.
328
+ void _onClearSelectionTriggered() {
329
+ if (!devInspectorClearSelectionTriggerNotifier.value) return;
330
+ devInspectorClearSelectionTriggerNotifier.value = false;
331
+ if (!mounted) return;
332
+ if (_selectedInfo == null && _selectedRender == null) return;
333
+ setState(_clearSelection);
334
+ }
335
+
315
336
  void _handleActiveChanged() {
316
337
  final bool active = devInspectorActiveNotifier.value;
317
338
  if (_active == active) return;
@@ -127,7 +127,7 @@ class RateBannerWidget extends StatelessWidget {
127
127
  color: cs.primary.withValues(alpha: 0.12),
128
128
  shape: BoxShape.circle,
129
129
  ),
130
- child: Icon(KasyIcons.star, color: cs.primary, size: 28),
130
+ child: Icon(KasyIcons.star, color: cs.primary, size: KasyIconSize.xxl),
131
131
  ),
132
132
  const SizedBox(height: KasySpacing.md),
133
133
  Text(
@@ -165,7 +165,7 @@ class CloseIcon extends StatelessWidget {
165
165
  child: Icon(
166
166
  KasyIcons.close,
167
167
  color: context.colors.onBackground,
168
- size: 21,
168
+ size: KasyIconSize.lg,
169
169
  ),
170
170
  ),
171
171
  ),
@@ -0,0 +1,47 @@
1
+ /// Kasy Design System — Icon Size Tokens
2
+ ///
3
+ /// Single source of truth for every icon dimension in the app. Screens and
4
+ /// components must read from here instead of hardcoding `size: 20` etc., so the
5
+ /// whole product can be re-scaled from one place.
6
+ ///
7
+ /// Usage: `Icon(KasyIcons.person, size: KasyIconSize.rowLeading)`
8
+ ///
9
+ /// Scale reference:
10
+ /// xxs → 12 micro glyphs inside small badges
11
+ /// xs → 14 tiny, dense inline glyphs
12
+ /// sm → 16 secondary / trailing (chevrons, value adornments)
13
+ /// md → 18 inline-with-text (chips, accordions, alerts)
14
+ /// lg → 20 default — list-row leading icons, chrome actions
15
+ /// xl → 24 prominent / large chrome
16
+ /// xxl → 28 feature highlights
17
+ /// display → 36 illustrative glyphs (empty states, brand badges)
18
+ /// hero → 72 hero glyph at the top of a focused screen (auth, onboarding)
19
+ class KasyIconSize {
20
+ KasyIconSize._();
21
+
22
+ static const double xxs = 12;
23
+ static const double xs = 14;
24
+ static const double sm = 16;
25
+ static const double md = 18;
26
+ static const double lg = 20;
27
+ static const double xl = 24;
28
+ static const double xxl = 28;
29
+ static const double display = 36;
30
+ static const double hero = 72;
31
+
32
+ // ── Semantic aliases ─────────────────────────────────────────────────────
33
+ // Prefer these in screens so intent is explicit and a single edit re-scales
34
+ // every row/chrome at once.
35
+
36
+ /// Leading icon in a list / settings row (the standard list glyph).
37
+ static const double rowLeading = lg; // 20
38
+
39
+ /// Trailing affordance in a row — chevron, small value adornment.
40
+ static const double rowTrailing = sm; // 16
41
+
42
+ /// Action icons in the top/bottom chrome (app bar orbs, nav).
43
+ static const double chrome = lg; // 20
44
+
45
+ /// Glyph sitting inline next to body text (chips, inline status).
46
+ static const double inline = md; // 18
47
+ }
@@ -38,6 +38,19 @@ class KasyShadows {
38
38
  );
39
39
  }
40
40
 
41
+ /// Resting border for the FLAT [KasyTextField] / [KasyTextArea] variant.
42
+ ///
43
+ /// Flat fields share their container's fill (no fill contrast) and cast no
44
+ /// shadow, so the border alone separates them from the surface behind. It is
45
+ /// derived from `onSurface` (not the fixed-hue `outline` token) so it reads
46
+ /// clearly in BOTH modes: a soft dark hairline on light, a soft light
47
+ /// hairline on dark — where the regular hairline would vanish.
48
+ static Color inputFieldFlatBorder(BuildContext context) {
49
+ return context.colors.onSurface.withValues(
50
+ alpha: context.isDark ? 0.16 : 0.12,
51
+ );
52
+ }
53
+
41
54
  /// Standard shadow for input surfaces ([KasyTextField] and [KasyTextArea]).
42
55
  ///
43
56
  /// Slightly tighter and softer than [component] — native only (callers skip on web).
@@ -54,6 +54,38 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
54
54
  static TextStyle get buttonBase => _inter(FontWeight.w500, 16, 24);
55
55
  static TextStyle get buttonSm => _inter(FontWeight.w500, 14, 20);
56
56
 
57
+ // -----------------------------------------------------------------------
58
+ // Semantic app roles — name the recurring UI text roles so screens stop
59
+ // hand-tuning slots with copyWith(fontSize/fontWeight/...). Each composes a
60
+ // scale role above; restyle a role app-wide by editing it here once.
61
+ // Apply color at the call site (.copyWith(color: ...)); the scale is colorless.
62
+ // -----------------------------------------------------------------------
63
+
64
+ /// Top-level page title (auth, paywall, full-screen flows). 24 / w700.
65
+ static TextStyle get pageTitle => heading2;
66
+
67
+ /// Section or master-detail pane title (Settings detail, grouped areas). 16 / w600.
68
+ static TextStyle get sectionTitle => heading4;
69
+
70
+ /// Small header above a grouped list (e.g. "PREFERENCES"). 12 / w600, lightly tracked.
71
+ static TextStyle get sectionLabel =>
72
+ _inter(FontWeight.w600, 12, 16).copyWith(letterSpacing: 0.5);
73
+
74
+ /// Primary text of a list / settings row. 14 / w500.
75
+ static TextStyle get rowTitle => bodySmMedium;
76
+
77
+ /// Secondary / value text in a row (apply a muted color at the call site). 14 / w400.
78
+ static TextStyle get rowValue => bodySm;
79
+
80
+ /// Card title. 14 / w500.
81
+ static TextStyle get cardTitle => bodySmMedium;
82
+
83
+ /// Card subtitle / supporting line. 12 / w400.
84
+ static TextStyle get cardSubtitle => bodyXs;
85
+
86
+ /// Caption, hint, version label, footnote. 12 / w400.
87
+ static TextStyle get caption => bodyXs;
88
+
57
89
  // -----------------------------------------------------------------------
58
90
  // Display — large hero text (extends the HeroUI scale upward in Inter)
59
91
  // -----------------------------------------------------------------------
@@ -9,11 +9,13 @@
9
9
  /// KasyTextTheme → context.textTheme.bodyMedium / .titleLarge / etc.
10
10
  /// KasySpacing → KasySpacing.md (16) / .lg (24) / etc.
11
11
  /// KasyRadius → KasyRadius.smBorderRadius / .lgBorderRadius / etc.
12
+ /// KasyIconSize → KasyIconSize.rowLeading (20) / .rowTrailing (16) / etc.
12
13
  library;
13
14
 
14
15
  export '../icons/kasy_icons.dart';
15
16
  export 'colors.dart';
16
17
  export 'extensions/theme_extension.dart';
18
+ export 'icon_sizes.dart';
17
19
  export 'providers/theme_provider.dart';
18
20
  export 'radius.dart';
19
21
  export 'shadows.dart';
@@ -189,6 +189,9 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
189
189
  // the DevInspector's in-app status pill while the preview is on.
190
190
  devInspectorSuppressStatusPillNotifier.value =
191
191
  webDevicePreviewEnabledNotifier.value;
192
+ // Entering or leaving the device frame: drop any lingering inspector
193
+ // selection so an old highlight doesn't carry across the transition.
194
+ devInspectorClearSelectionTriggerNotifier.value = true;
192
195
  if (webDevicePreviewEnabledNotifier.value) {
193
196
  _controlsTimer?.cancel();
194
197
  _controlsTimer = Timer(const Duration(milliseconds: 800), () {
@@ -1,14 +1,25 @@
1
1
  import 'package:flutter/foundation.dart' show kIsWeb;
2
2
  import 'package:flutter/widgets.dart';
3
3
 
4
- /// Global render scale applied to the app on web.
4
+ /// Maximum render scale applied to the app on web (used on wide viewports).
5
5
  ///
6
6
  /// Flutter web tends to render ~10% larger than equivalent HTML apps at the
7
7
  /// browser's 100% zoom, so the whole UI feels oversized on desktop. `0.95`
8
8
  /// brings it to the proportion the design targets (i.e. what 95% zoom looked
9
- /// like) without the user having to touch the browser zoom.
9
+ /// like) without the user having to touch the browser zoom. On narrower
10
+ /// viewports the effective scale is reduced further (see [kWebViewportScaleTargetWidth]).
10
11
  const double kWebViewportScale = 0.95;
11
12
 
13
+ /// Design target width (logical px) the desktop shell is laid out against.
14
+ ///
15
+ /// A high-DPI display with OS scaling (Windows at 125/150/175%) reports a
16
+ /// smaller logical viewport width than a Mac at the same physical size, so a
17
+ /// fixed [kWebViewportScale] left the shell laid out narrower than the design
18
+ /// target and it looked cropped (the user had to Ctrl-minus). Scaling by
19
+ /// `width / kWebViewportScaleTargetWidth` instead pins the logical layout width
20
+ /// to this target on those displays, so Mac and Windows render the same.
21
+ const double kWebViewportScaleTargetWidth = 1280;
22
+
12
23
  /// Minimum real viewport width (logical px) at which the web scale kicks in.
13
24
  ///
14
25
  /// The "oversized" problem only shows up on tablet/desktop layouts. On mobile
@@ -44,9 +55,17 @@ class WebViewportScale extends StatelessWidget {
44
55
  // Mobile web (narrow browser) renders at its natural size, just like the
45
56
  // native build. The scale only applies from the tablet breakpoint up.
46
57
  if (mq.size.width < kWebViewportScaleMinWidth) return child;
58
+ // Width-aware scale: on a wide viewport keep [scale] (0.95); on a high-DPI
59
+ // display with OS scaling the browser reports a smaller logical width, so
60
+ // scale down just enough to lay the shell out at the design target width
61
+ // instead of cropping it. Mac (wide) stays at 0.95; Windows at 125/150/175%
62
+ // shrinks proportionally so both look identical.
63
+ final double effectiveScale =
64
+ (mq.size.width / kWebViewportScaleTargetWidth).clamp(0.5, scale);
65
+ if (effectiveScale == 1.0) return child;
47
66
  final Size logicalSize = Size(
48
- mq.size.width / scale,
49
- mq.size.height / scale,
67
+ mq.size.width / effectiveScale,
68
+ mq.size.height / effectiveScale,
50
69
  );
51
70
  return MediaQuery(
52
71
  data: mq.copyWith(size: logicalSize),
@@ -81,7 +81,7 @@ class _UpdateBottomSheet extends StatelessWidget {
81
81
  child: Icon(
82
82
  KasyIcons.star,
83
83
  color: context.colors.primary,
84
- size: 24,
84
+ size: KasyIconSize.xl,
85
85
  ),
86
86
  ),
87
87
  const SizedBox(width: KasySpacing.md),
@@ -133,7 +133,7 @@ class _AiConversationTileState extends ConsumerState<AiConversationTile> {
133
133
  child: InkResponse(
134
134
  onTap: widget.onDelete,
135
135
  radius: 18,
136
- child: Icon(KasyIcons.trash, size: 16, color: context.colors.error),
136
+ child: Icon(KasyIcons.trash, size: KasyIconSize.sm, color: context.colors.error),
137
137
  ),
138
138
  );
139
139
  }
@@ -32,7 +32,7 @@ class _OtpVerificationComponentState
32
32
  crossAxisAlignment: CrossAxisAlignment.stretch,
33
33
  children: [
34
34
  const SizedBox(height: KasySpacing.lg),
35
- Icon(KasyIcons.sms, size: 72, color: context.colors.primary),
35
+ Icon(KasyIcons.sms, size: KasyIconSize.hero, color: context.colors.primary),
36
36
  const SizedBox(height: KasySpacing.lg),
37
37
  Text(
38
38
  t.phone_auth.verification_code,
@@ -34,7 +34,7 @@ class _PhoneInputComponentState extends ConsumerState<PhoneInputComponent> {
34
34
  crossAxisAlignment: CrossAxisAlignment.stretch,
35
35
  children: [
36
36
  const SizedBox(height: KasySpacing.lg),
37
- Icon(KasyIcons.phoneAndroid, size: 72, color: context.colors.primary),
37
+ Icon(KasyIcons.phoneAndroid, size: KasyIconSize.hero, color: context.colors.primary),
38
38
  const SizedBox(height: KasySpacing.lg),
39
39
  Text(
40
40
  t.phone_auth.subtitle_input,
@@ -71,6 +71,7 @@ class _PhoneInputComponentState extends ConsumerState<PhoneInputComponent> {
71
71
  ),
72
72
  ),
73
73
  KasyTextField(
74
+ variant: KasyTextFieldVariant.flat,
74
75
  controller: _phoneController,
75
76
  keyboardType: TextInputType.phone,
76
77
  label: t.phone_auth.phone_label,
@@ -53,6 +53,7 @@ class RecoverPasswordPage extends ConsumerWidget {
53
53
  subtitle: t.auth.recover.subtitle,
54
54
  children: [
55
55
  KasyTextField(
56
+ variant: KasyTextFieldVariant.flat,
56
57
  key: const Key('email_input'),
57
58
  label: t.auth.recover.email_label,
58
59
  contentType: KasyTextFieldContentType.email,
@@ -124,7 +125,8 @@ class _BackToSigninPrompt extends StatelessWidget {
124
125
  context.go('/signin');
125
126
  }
126
127
  },
127
- focusable: true,
128
+ // Secondary link: kept out of Tab traversal (focusable defaults to
129
+ // false) so keyboard/next flows email → submit, not this link.
128
130
  child: Text(
129
131
  t.auth.recover.signin_link,
130
132
  style: context.textTheme.bodyMedium?.copyWith(