kasy-cli 1.37.1 → 1.38.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 (49) hide show
  1. package/lib/scaffold/CHANGELOG.json +9 -0
  2. package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
  3. package/lib/scaffold/backends/patch-base-hashes.json +2 -2
  4. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
  5. package/package.json +1 -1
  6. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  7. package/templates/firebase/AGENTS.md +7 -1
  8. package/templates/firebase/DESIGN_SYSTEM.md +13 -0
  9. package/templates/firebase/lib/components/kasy_app_bar.dart +57 -16
  10. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
  11. package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
  12. package/templates/firebase/lib/components/kasy_sidebar.dart +394 -25
  13. package/templates/firebase/lib/components/kasy_web_header.dart +11 -3
  14. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +2 -213
  15. package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
  16. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -4
  17. package/templates/firebase/lib/core/chrome/web_header_scope.dart +20 -0
  18. package/templates/firebase/lib/core/data/api/user_api.dart +4 -0
  19. package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
  20. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
  21. package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
  22. package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
  23. package/templates/firebase/lib/core/states/user_state_notifier.dart +41 -0
  24. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
  25. package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
  26. package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
  27. package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
  28. package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
  29. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +498 -466
  30. package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
  31. package/templates/firebase/lib/features/home/home_components_page.dart +16 -6
  32. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +7 -0
  33. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +23 -13
  34. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -0
  35. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
  36. package/templates/firebase/lib/features/settings/settings_page.dart +51 -38
  37. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +549 -376
  38. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
  39. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +266 -141
  40. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +23 -30
  41. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +30 -49
  42. package/templates/firebase/lib/i18n/en.i18n.json +23 -9
  43. package/templates/firebase/lib/i18n/es.i18n.json +23 -9
  44. package/templates/firebase/lib/i18n/pt.i18n.json +23 -9
  45. package/templates/firebase/lib/router.dart +43 -25
  46. package/templates/firebase/pubspec.yaml +1 -1
  47. package/templates/firebase/test/admin_shell_chrome_test.dart +104 -0
  48. package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
  49. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
@@ -1,15 +1,63 @@
1
1
  import 'dart:ui';
2
2
 
3
3
  import 'package:flutter/material.dart';
4
+ import 'package:kasy_kit/components/kasy_dialog.dart';
4
5
  import 'package:kasy_kit/core/theme/theme.dart';
6
+ import 'package:kasy_kit/core/widgets/responsive_layout.dart';
5
7
 
6
8
  /// Tone for the icon bubble displayed at the top of [KasyBottomSheet].
7
9
  enum KasySheetIconTone { info, success, warning, danger, neutral }
8
10
 
11
+ /// Width (logical px) at/above which design-system sheets present as a centered
12
+ /// dialog instead of a bottom sheet. Phones and tablets keep the bottom sheet;
13
+ /// only desktop ([DeviceType.large] and wider) switches to the dialog form.
14
+ const double kKasySheetDialogBreakpoint = 1024; // == DeviceType.large.breakpoint
15
+
16
+ /// True when [context]'s width means design-system sheets should be presented
17
+ /// as a centered dialog (desktop) rather than a bottom sheet.
18
+ bool kasySheetUsesDialog(BuildContext context) =>
19
+ MediaQuery.sizeOf(context).width >= kKasySheetDialogBreakpoint;
20
+
21
+ /// Whether the enclosing design-system sheet is currently presented as a
22
+ /// centered dialog (desktop) rather than a bottom sheet. Custom sheet bodies can
23
+ /// read this to drop their drag handle, round all corners, constrain width, etc.
24
+ /// Bodies built on [KasyBottomSheet] or wrapped in [KasySheetSurface] adapt
25
+ /// automatically and do not need to call this.
26
+ bool kasySheetIsFloating(BuildContext context) =>
27
+ _KasySheetPresentation.isFloatingOf(context);
28
+
29
+ /// Max width of a sheet (at scale 1.0) when presented as a desktop dialog.
30
+ const double _kFloatingSheetMaxWidth = 420;
31
+
32
+ /// Injected by [showKasyBottomSheet] / [showKasyBlurBottomSheet] so the sheet
33
+ /// body knows whether it is being shown as a bottom sheet or a centered dialog.
34
+ class _KasySheetPresentation extends InheritedWidget {
35
+ final bool isFloating;
36
+
37
+ const _KasySheetPresentation({
38
+ required this.isFloating,
39
+ required super.child,
40
+ });
41
+
42
+ static bool isFloatingOf(BuildContext context) =>
43
+ context
44
+ .dependOnInheritedWidgetOfExactType<_KasySheetPresentation>()
45
+ ?.isFloating ??
46
+ false;
47
+
48
+ @override
49
+ bool updateShouldNotify(_KasySheetPresentation oldWidget) =>
50
+ oldWidget.isFloating != isFloating;
51
+ }
52
+
9
53
  /// A design-system bottom sheet panel.
10
54
  ///
11
55
  /// Use [showKasyBottomSheet] to present it. The sheet handles drag-handle,
12
56
  /// optional icon bubble, title, message, custom body, and action buttons.
57
+ ///
58
+ /// On desktop the presenter shows the same panel as a centered dialog; this
59
+ /// widget adapts automatically (no drag handle, rounded on all corners,
60
+ /// width-constrained) so callers never have to branch on screen size.
13
61
  class KasyBottomSheet extends StatelessWidget {
14
62
  final String? title;
15
63
  final String? message;
@@ -49,13 +97,19 @@ class KasyBottomSheet extends StatelessWidget {
49
97
 
50
98
  @override
51
99
  Widget build(BuildContext context) {
52
- final double bottomSafeArea =
53
- addBottomSafeArea ? MediaQuery.paddingOf(context).bottom : 0;
54
- final double keyboardInset =
55
- addKeyboardInset ? MediaQuery.viewInsetsOf(context).bottom : 0;
100
+ final bool floating = _KasySheetPresentation.isFloatingOf(context);
101
+ // In dialog mode the centered presenter owns the safe area and lifts the
102
+ // card above the keyboard, so the panel itself adds neither.
103
+ final double bottomSafeArea = (addBottomSafeArea && !floating)
104
+ ? MediaQuery.paddingOf(context).bottom
105
+ : 0;
106
+ final double keyboardInset = (addKeyboardInset && !floating)
107
+ ? MediaQuery.viewInsetsOf(context).bottom
108
+ : 0;
56
109
  final double bottomInset = bottomSafeArea + keyboardInset;
57
110
  final bool hasIcon = icon != null;
58
111
  final bool centered = hasIcon;
112
+ final bool effectiveDragHandle = showDragHandle && !floating;
59
113
 
60
114
  // Elevate the input fill color for any KasyTextField.primary inside the
61
115
  // sheet. primary variant reads inputDecorationTheme.fillColor first, so
@@ -65,10 +119,18 @@ class KasyBottomSheet extends StatelessWidget {
65
119
  ? const Color(0xFF272729)
66
120
  : const Color(0xFFF0F0F2);
67
121
 
68
- return Material(
122
+ final BorderRadius radius = floating
123
+ ? BorderRadius.circular(KasyRadius.xl)
124
+ : const BorderRadius.vertical(top: Radius.circular(KasyRadius.xl));
125
+
126
+ Widget panel = Material(
69
127
  color: context.colors.surface,
70
- borderRadius: const BorderRadius.vertical(
71
- top: Radius.circular(KasyRadius.xl),
128
+ clipBehavior: Clip.antiAlias,
129
+ shape: RoundedRectangleBorder(
130
+ borderRadius: radius,
131
+ side: floating
132
+ ? BorderSide(color: context.colors.outline.withValues(alpha: 0.22))
133
+ : BorderSide.none,
72
134
  ),
73
135
  child: Theme(
74
136
  data: Theme.of(context).copyWith(
@@ -77,72 +139,181 @@ class KasyBottomSheet extends StatelessWidget {
77
139
  ),
78
140
  ),
79
141
  child: Column(
80
- mainAxisSize: MainAxisSize.min,
81
- crossAxisAlignment:
82
- centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
83
- children: [
84
- if (showDragHandle) _DragHandle(),
85
- AnimatedPadding(
86
- duration: const Duration(milliseconds: 180),
87
- curve: Curves.easeOutCubic,
88
- padding: EdgeInsets.fromLTRB(
89
- KasySpacing.lg,
90
- hasIcon ? KasySpacing.lg : KasySpacing.md,
91
- KasySpacing.lg,
92
- bottomInset + KasySpacing.lg,
93
- ),
94
- child: Column(
95
- mainAxisSize: MainAxisSize.min,
96
- crossAxisAlignment:
97
- centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
98
- children: [
99
- if (hasIcon) ...[
100
- _IconBubble(icon: icon!, tone: iconTone),
101
- const SizedBox(height: KasySpacing.md),
102
- ],
103
- if (title != null)
104
- Text(
105
- title!,
106
- textAlign: centered ? TextAlign.center : TextAlign.start,
107
- style: context.textTheme.titleLarge?.copyWith(
108
- color: context.colors.onSurface,
142
+ mainAxisSize: MainAxisSize.min,
143
+ crossAxisAlignment:
144
+ centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
145
+ children: [
146
+ if (effectiveDragHandle) _DragHandle(),
147
+ AnimatedPadding(
148
+ duration: const Duration(milliseconds: 180),
149
+ curve: Curves.easeOutCubic,
150
+ padding: EdgeInsets.fromLTRB(
151
+ KasySpacing.lg,
152
+ hasIcon ? KasySpacing.lg : KasySpacing.md,
153
+ KasySpacing.lg,
154
+ bottomInset + KasySpacing.lg,
155
+ ),
156
+ child: Column(
157
+ mainAxisSize: MainAxisSize.min,
158
+ crossAxisAlignment: centered
159
+ ? CrossAxisAlignment.center
160
+ : CrossAxisAlignment.start,
161
+ children: [
162
+ if (hasIcon) ...[
163
+ _IconBubble(icon: icon!, tone: iconTone),
164
+ const SizedBox(height: KasySpacing.md),
165
+ ],
166
+ if (title != null)
167
+ Text(
168
+ title!,
169
+ textAlign: centered ? TextAlign.center : TextAlign.start,
170
+ style: context.textTheme.titleLarge?.copyWith(
171
+ color: context.colors.onSurface,
172
+ ),
109
173
  ),
110
- ),
111
- if (message != null) ...[
112
- const SizedBox(height: KasySpacing.sm),
113
- Text(
114
- message!,
115
- textAlign: centered ? TextAlign.center : TextAlign.start,
116
- style: context.textTheme.bodyMedium?.copyWith(
117
- color: context.colors.onSurface.withValues(alpha: 0.6),
118
- height: 1.5,
174
+ if (message != null) ...[
175
+ const SizedBox(height: KasySpacing.sm),
176
+ Text(
177
+ message!,
178
+ textAlign: centered ? TextAlign.center : TextAlign.start,
179
+ style: context.textTheme.bodyMedium?.copyWith(
180
+ color: context.colors.onSurface.withValues(alpha: 0.6),
181
+ height: 1.5,
182
+ ),
119
183
  ),
120
- ),
121
- ],
122
- if (body != null) ...[
123
- const SizedBox(height: KasySpacing.md),
124
- body!,
125
- ],
126
- if (actions.isNotEmpty) ...[
127
- const SizedBox(height: KasySpacing.lg),
128
- ...actions.expand(
129
- (w) => [
130
- w,
131
- if (w != actions.last) const SizedBox(height: KasySpacing.sm),
132
- ],
133
- ),
184
+ ],
185
+ if (body != null) ...[
186
+ const SizedBox(height: KasySpacing.md),
187
+ body!,
188
+ ],
189
+ if (actions.isNotEmpty) ...[
190
+ const SizedBox(height: KasySpacing.lg),
191
+ ...actions.expand(
192
+ (w) => [
193
+ w,
194
+ if (w != actions.last)
195
+ const SizedBox(height: KasySpacing.sm),
196
+ ],
197
+ ),
198
+ ],
134
199
  ],
135
- ],
200
+ ),
136
201
  ),
137
- ),
138
- ],
202
+ ],
203
+ ),
139
204
  ),
205
+ );
206
+
207
+ if (floating) {
208
+ panel = ConstrainedBox(
209
+ constraints: const BoxConstraints(maxWidth: _kFloatingSheetMaxWidth),
210
+ child: DecoratedBox(
211
+ decoration: BoxDecoration(
212
+ borderRadius: radius,
213
+ boxShadow: [
214
+ KasyShadows.component(
215
+ context,
216
+ blurRadius: 20,
217
+ spreadRadius: -2,
218
+ offset: const Offset(0, 8),
219
+ ),
220
+ ],
221
+ ),
222
+ child: panel,
223
+ ),
224
+ );
225
+ }
226
+
227
+ return panel;
228
+ }
229
+ }
230
+
231
+ /// Surface chrome for custom sheet bodies presented via [showKasyBottomSheet].
232
+ ///
233
+ /// Paints the design-system surface and adapts to the presentation: top-rounded
234
+ /// with a drag handle as a bottom sheet (mobile / tablet), fully rounded,
235
+ /// bordered and width-constrained as a centered dialog (desktop). Wrap bespoke
236
+ /// content (option lists, pickers) with this so it looks right in both forms
237
+ /// without the caller branching on screen size.
238
+ class KasySheetSurface extends StatelessWidget {
239
+ final Widget child;
240
+
241
+ /// Shows the drag handle in bottom-sheet form. Ignored when floating.
242
+ final bool showDragHandle;
243
+
244
+ /// Surface color; defaults to [KasyColors.surface].
245
+ final Color? color;
246
+
247
+ const KasySheetSurface({
248
+ super.key,
249
+ required this.child,
250
+ this.showDragHandle = true,
251
+ this.color,
252
+ });
253
+
254
+ @override
255
+ Widget build(BuildContext context) {
256
+ final bool floating = _KasySheetPresentation.isFloatingOf(context);
257
+ final BorderRadius radius = floating
258
+ ? BorderRadius.circular(KasyRadius.xl)
259
+ : const BorderRadius.vertical(top: Radius.circular(KasyRadius.xl));
260
+
261
+ Widget content = Column(
262
+ mainAxisSize: MainAxisSize.min,
263
+ children: [
264
+ if (showDragHandle && !floating) _DragHandle(),
265
+ // Flexible lets the body shrink within a bottom sheet's bounded height.
266
+ // In dialog form the presenter wraps everything in a scroll view (with
267
+ // unbounded height), where Flexible would throw, so pass the child raw.
268
+ if (floating) child else Flexible(child: child),
269
+ ],
270
+ );
271
+
272
+ if (!floating) {
273
+ content = SafeArea(top: false, child: content);
274
+ }
275
+
276
+ Widget panel = Material(
277
+ color: color ?? context.colors.surface,
278
+ clipBehavior: Clip.antiAlias,
279
+ shape: RoundedRectangleBorder(
280
+ borderRadius: radius,
281
+ side: floating
282
+ ? BorderSide(color: context.colors.outline.withValues(alpha: 0.22))
283
+ : BorderSide.none,
140
284
  ),
285
+ child: content,
141
286
  );
287
+
288
+ if (floating) {
289
+ panel = ConstrainedBox(
290
+ constraints: const BoxConstraints(maxWidth: _kFloatingSheetMaxWidth),
291
+ child: DecoratedBox(
292
+ decoration: BoxDecoration(
293
+ borderRadius: radius,
294
+ boxShadow: [
295
+ KasyShadows.component(
296
+ context,
297
+ blurRadius: 20,
298
+ spreadRadius: -2,
299
+ offset: const Offset(0, 8),
300
+ ),
301
+ ],
302
+ ),
303
+ child: panel,
304
+ ),
305
+ );
306
+ }
307
+
308
+ return panel;
142
309
  }
143
310
  }
144
311
 
145
- /// Shows a [KasyBottomSheet] using Material's modal bottom sheet.
312
+ /// Shows a [KasyBottomSheet] as a modal bottom sheet on phones and tablets, and
313
+ /// as a centered dialog on desktop ([kKasySheetDialogBreakpoint] and wider).
314
+ ///
315
+ /// The same [builder] is used for both forms; bodies built on [KasyBottomSheet]
316
+ /// (or wrapped in [KasySheetSurface]) adapt automatically.
146
317
  ///
147
318
  /// Example:
148
319
  /// ```dart
@@ -168,6 +339,17 @@ Future<T?> showKasyBottomSheet<T>({
168
339
  bool isScrollControlled = false,
169
340
  Color? barrierColor,
170
341
  }) {
342
+ if (kasySheetUsesDialog(context)) {
343
+ return showKasyDialog<T>(
344
+ context: context,
345
+ barrierDismissible: isDismissible,
346
+ barrierColor: barrierColor,
347
+ builder: (ctx) => _KasySheetPresentation(
348
+ isFloating: true,
349
+ child: _FloatingSheetScrollHost(child: builder(ctx)),
350
+ ),
351
+ );
352
+ }
171
353
  return showModalBottomSheet<T>(
172
354
  context: context,
173
355
  useRootNavigator: true,
@@ -179,18 +361,31 @@ Future<T?> showKasyBottomSheet<T>({
179
361
  Colors.black.withValues(
180
362
  alpha: Theme.of(context).brightness == Brightness.dark ? 0.6 : 0.45,
181
363
  ),
182
- builder: builder,
364
+ builder: (ctx) =>
365
+ _KasySheetPresentation(isFloating: false, child: builder(ctx)),
183
366
  );
184
367
  }
185
368
 
186
369
  /// Shows a [KasyBottomSheet] with a frosted-glass blur overlay instead of
187
- /// the standard dim barrier. Uses [showGeneralDialog] for full-screen control.
370
+ /// the standard dim barrier. On desktop it presents as a centered dialog over
371
+ /// the same blur ([showKasyBlurDialog]).
188
372
  Future<T?> showKasyBlurBottomSheet<T>({
189
373
  required BuildContext context,
190
374
  required WidgetBuilder builder,
191
375
  bool isDismissible = true,
192
376
  double blurSigma = 8,
193
377
  }) {
378
+ if (kasySheetUsesDialog(context)) {
379
+ return showKasyBlurDialog<T>(
380
+ context: context,
381
+ barrierDismissible: isDismissible,
382
+ blurSigma: blurSigma,
383
+ builder: (ctx) => _KasySheetPresentation(
384
+ isFloating: true,
385
+ child: _FloatingSheetScrollHost(child: builder(ctx)),
386
+ ),
387
+ );
388
+ }
194
389
  return showGeneralDialog<T>(
195
390
  context: context,
196
391
  barrierDismissible: isDismissible,
@@ -201,12 +396,34 @@ Future<T?> showKasyBlurBottomSheet<T>({
201
396
  animation: animation,
202
397
  blurSigma: blurSigma,
203
398
  isDismissible: isDismissible,
204
- child: builder(ctx),
399
+ child: _KasySheetPresentation(isFloating: false, child: builder(ctx)),
205
400
  ),
206
401
  transitionBuilder: (_, _, _, child) => child,
207
402
  );
208
403
  }
209
404
 
405
+ /// Bounds a floating (dialog-form) sheet to the viewport and makes it scroll
406
+ /// when its content is taller than the screen — while still shrink-wrapping and
407
+ /// centering when the content fits. Used only on the desktop dialog path.
408
+ class _FloatingSheetScrollHost extends StatelessWidget {
409
+ final Widget child;
410
+
411
+ const _FloatingSheetScrollHost({required this.child});
412
+
413
+ @override
414
+ Widget build(BuildContext context) {
415
+ return ConstrainedBox(
416
+ constraints: BoxConstraints(
417
+ maxHeight: MediaQuery.sizeOf(context).height * 0.9,
418
+ ),
419
+ child: SingleChildScrollView(
420
+ // Shrink-wraps to the child until it exceeds maxHeight, then scrolls.
421
+ child: child,
422
+ ),
423
+ );
424
+ }
425
+ }
426
+
210
427
  class _BlurSheetScaffold extends StatelessWidget {
211
428
  final Animation<double> animation;
212
429
  final double blurSigma;
@@ -214,8 +214,10 @@ const List<BoxShadow> _kCalendarShadows = [
214
214
  /// Controls month names, weekday labels, week start, and month/year separator
215
215
  /// inside the [KasyDatePicker] calendar overlay.
216
216
  ///
217
- /// Two built-in presets are provided: [KasyDatePickerLocale.en] and
218
- /// [KasyDatePickerLocale.es]. For other locales, construct a custom instance.
217
+ /// Three built-in presets are provided: [KasyDatePickerLocale.en],
218
+ /// [KasyDatePickerLocale.pt] and [KasyDatePickerLocale.es]. By default the
219
+ /// picker auto-selects one from the app's active language; pass an explicit
220
+ /// instance to override (or construct a custom one for another locale).
219
221
  class KasyDatePickerLocale {
220
222
  const KasyDatePickerLocale({
221
223
  required this.monthNames,
@@ -299,6 +301,10 @@ enum KasyDatePickerNavStyle {
299
301
  // KasyDatePickerPresentation — controls how the calendar is shown
300
302
  // ─────────────────────────────────────────────────────────────────────────────
301
303
 
304
+ /// Comfortable fixed width for the calendar when shown as a centered dialog, so
305
+ /// it does not stretch to the dialog inset (nearly full screen) on desktop.
306
+ const double _kDialogCalendarWidth = 360;
307
+
302
308
  /// Controls how the calendar is presented when the date field is tapped.
303
309
  enum KasyDatePickerPresentation {
304
310
  /// Floating overlay anchored below the trigger field (default).
@@ -429,7 +435,7 @@ class KasyDatePicker extends StatefulWidget {
429
435
  this.description,
430
436
  this.dateFormat,
431
437
  this.formatDate,
432
- this.locale = KasyDatePickerLocale.en,
438
+ this.locale,
433
439
  this.variant = KasyTextFieldVariant.primary,
434
440
  this.focusBorder = true,
435
441
  this.showSuffix = true,
@@ -500,9 +506,10 @@ class KasyDatePicker extends StatefulWidget {
500
506
  final String Function(DateTime)? formatDate;
501
507
 
502
508
  /// Calendar display locale — controls month names, weekday labels, week
503
- /// start day, and the regional [defaultDateFormat]. Defaults to
504
- /// [KasyDatePickerLocale.en] (English, Sunday-first, US date format).
505
- final KasyDatePickerLocale locale;
509
+ /// start day, and the regional [defaultDateFormat]. When null (default), it
510
+ /// follows the app's active language (pt / es / en) via
511
+ /// [Localizations.localeOf]; pass an explicit preset to override.
512
+ final KasyDatePickerLocale? locale;
506
513
 
507
514
  /// Visual variant of the trigger field. Defaults to
508
515
  /// [KasyTextFieldVariant.primary] (white surface + soft shadow on mobile).
@@ -603,7 +610,9 @@ class _KasyDatePickerState extends State<KasyDatePicker>
603
610
  void initState() {
604
611
  super.initState();
605
612
  _viewMonth = _monthOf(_initialAnchorDate() ?? DateTime.now());
606
- _displayController = TextEditingController(text: _displayText());
613
+ // Empty here: the display text depends on the locale (Localizations), which
614
+ // can't be read in initState. It's set in didChangeDependencies below.
615
+ _displayController = TextEditingController();
607
616
  _animCtrl = AnimationController(
608
617
  vsync: this,
609
618
  duration: const Duration(milliseconds: 180),
@@ -618,6 +627,14 @@ class _KasyDatePickerState extends State<KasyDatePicker>
618
627
  );
619
628
  }
620
629
 
630
+ @override
631
+ void didChangeDependencies() {
632
+ super.didChangeDependencies();
633
+ // Reads the locale (legal here, not in initState) to format the field, and
634
+ // re-runs if the app language changes so the displayed date follows it.
635
+ _displayController.text = _displayText();
636
+ }
637
+
621
638
  @override
622
639
  void didUpdateWidget(KasyDatePicker old) {
623
640
  super.didUpdateWidget(old);
@@ -647,10 +664,25 @@ class _KasyDatePickerState extends State<KasyDatePicker>
647
664
 
648
665
  DateTime _monthOf(DateTime d) => DateTime(d.year, d.month);
649
666
 
667
+ /// Effective calendar locale: the explicit [KasyDatePicker.locale] when set,
668
+ /// otherwise resolved from the app's active language (pt / es / en) so the
669
+ /// calendar always renders in the language the user is using the app in.
670
+ KasyDatePickerLocale get _effectiveLocale {
671
+ if (widget.locale != null) return widget.locale!;
672
+ switch (Localizations.localeOf(context).languageCode) {
673
+ case 'pt':
674
+ return KasyDatePickerLocale.pt;
675
+ case 'es':
676
+ return KasyDatePickerLocale.es;
677
+ default:
678
+ return KasyDatePickerLocale.en;
679
+ }
680
+ }
681
+
650
682
  /// Resolved formatting preset: respects an explicit override on the widget
651
683
  /// and falls back to the locale's regional default.
652
684
  KasyDateFormat get _resolvedFormat =>
653
- widget.dateFormat ?? widget.locale.defaultDateFormat;
685
+ widget.dateFormat ?? _effectiveLocale.defaultDateFormat;
654
686
 
655
687
  /// Unified selection. In single mode the picked [value] is wrapped in a
656
688
  /// range with `start == end == value` so the calendar tree only deals
@@ -867,9 +899,9 @@ class _KasyDatePickerState extends State<KasyDatePicker>
867
899
  }
868
900
 
869
901
  return _CalendarPanel(
870
- // Omit calendarWidth so the panel stretches to the basic dialog's
871
- // natural inset (showKasyDialog uses KasySpacing.lg horizontal
872
- // insetPadding). Matches the "basic dialog" proportion.
902
+ // Cap the width so the calendar doesn't stretch to the dialog inset
903
+ // (nearly full screen on desktop). A fixed comfortable card width.
904
+ calendarWidth: _kDialogCalendarWidth,
873
905
  viewMonth: dialogViewMonth,
874
906
  range: dialogRange,
875
907
  monthsToShow: widget.monthsToShow,
@@ -901,7 +933,7 @@ class _KasyDatePickerState extends State<KasyDatePicker>
901
933
  },
902
934
  minDate: widget.minDate,
903
935
  maxDate: widget.maxDate,
904
- locale: widget.locale,
936
+ locale: _effectiveLocale,
905
937
  );
906
938
  },
907
939
  ),
@@ -923,7 +955,7 @@ class _KasyDatePickerState extends State<KasyDatePicker>
923
955
  onRangeChanged: widget.onRangeChanged,
924
956
  minDate: widget.minDate,
925
957
  maxDate: widget.maxDate,
926
- locale: widget.locale,
958
+ locale: _effectiveLocale,
927
959
  monthsToShow: widget.monthsToShow,
928
960
  ),
929
961
  ).whenComplete(() {
@@ -1157,7 +1189,7 @@ class _KasyDatePickerState extends State<KasyDatePicker>
1157
1189
  }),
1158
1190
  minDate: widget.minDate,
1159
1191
  maxDate: widget.maxDate,
1160
- locale: widget.locale,
1192
+ locale: _effectiveLocale,
1161
1193
  ),
1162
1194
  ),
1163
1195
  ),
@@ -1674,10 +1706,13 @@ class _CalendarNavRow extends StatelessWidget {
1674
1706
  '${locale.monthNames[viewMonth.month - 1]}${locale.monthYearSeparator}${viewMonth.year}';
1675
1707
 
1676
1708
  // Month/year text + chevron used to toggle the year picker — shared by
1677
- // both nav styles, only its position in the row changes.
1678
- final Widget monthLabel = GestureDetector(
1709
+ // both nav styles, only its position in the row changes. Wrapped in
1710
+ // KasyHover so it gets the pointer cursor + hover feedback on the web,
1711
+ // matching the day cells and nav arrows.
1712
+ final Widget monthLabel = KasyHover(
1679
1713
  onTap: onToggleMode,
1680
- behavior: HitTestBehavior.opaque,
1714
+ borderRadius: BorderRadius.circular(KasyRadius.sm),
1715
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
1681
1716
  child: Row(
1682
1717
  mainAxisSize: MainAxisSize.min,
1683
1718
  children: [
@@ -2028,10 +2063,6 @@ class _DayCell extends StatelessWidget {
2028
2063
  textColor = c.onSurface;
2029
2064
  }
2030
2065
 
2031
- // ── Today dot ──────────────────────────────────────────────────────────
2032
- final bool showDot = data.isToday;
2033
- final Color dotColor = _isEndpoint ? const Color(0xFFFCFCFC) : c.muted;
2034
-
2035
2066
  // ── Out-of-month / disabled fade ───────────────────────────────────────
2036
2067
  final double opacity =
2037
2068
  (!data.isCurrentMonth || data.isDisabled) ? 0.5 : 1.0;
@@ -2113,30 +2144,14 @@ class _DayCell extends StatelessWidget {
2113
2144
  );
2114
2145
  }
2115
2146
 
2116
- final Widget textAndDot = Stack(
2117
- alignment: Alignment.center,
2118
- children: [
2119
- Text(
2120
- data.date.day.toString(),
2121
- style: context.textTheme.labelLarge?.copyWith(
2122
- color: textColor,
2123
- fontWeight: FontWeight.w600,
2124
- height: 1.43,
2125
- ),
2126
- ),
2127
- if (showDot)
2128
- Positioned(
2129
- bottom: 4,
2130
- child: Container(
2131
- width: 3,
2132
- height: 3,
2133
- decoration: BoxDecoration(
2134
- color: dotColor,
2135
- borderRadius: BorderRadius.circular(12),
2136
- ),
2137
- ),
2138
- ),
2139
- ],
2147
+ // Today is marked by the accent text color alone (no extra dot).
2148
+ final Widget dayNumber = Text(
2149
+ data.date.day.toString(),
2150
+ style: context.textTheme.labelLarge?.copyWith(
2151
+ color: textColor,
2152
+ fontWeight: FontWeight.w600,
2153
+ height: 1.43,
2154
+ ),
2140
2155
  );
2141
2156
 
2142
2157
  // Inner cell content: endpoint circle (if any) + text. Stays 36×36 so it
@@ -2145,7 +2160,7 @@ class _DayCell extends StatelessWidget {
2145
2160
  alignment: Alignment.center,
2146
2161
  children: [
2147
2162
  if (endpoint != null) endpoint,
2148
- textAndDot,
2163
+ dayNumber,
2149
2164
  ],
2150
2165
  );
2151
2166