kasy-cli 1.37.0 → 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.
- package/lib/scaffold/CHANGELOG.json +9 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
- package/lib/scaffold/backends/patch-base-hashes.json +4 -4
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
- package/templates/firebase/AGENTS.md +20 -10
- package/templates/firebase/DESIGN_SYSTEM.md +13 -0
- package/templates/firebase/README.en.md +1 -1
- package/templates/firebase/README.es.md +1 -1
- package/templates/firebase/README.md +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +57 -16
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
- package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
- package/templates/firebase/lib/components/kasy_sidebar.dart +397 -28
- package/templates/firebase/lib/components/kasy_web_header.dart +11 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +2 -213
- package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -4
- package/templates/firebase/lib/core/chrome/web_header_scope.dart +20 -0
- package/templates/firebase/lib/core/data/api/user_api.dart +4 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
- package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +41 -0
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
- package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +498 -466
- package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -7
- package/templates/firebase/lib/features/home/home_components_page.dart +16 -6
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +7 -0
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +23 -13
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
- package/templates/firebase/lib/features/settings/settings_page.dart +51 -38
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +549 -376
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +266 -141
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +23 -30
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +30 -49
- package/templates/firebase/lib/i18n/en.i18n.json +23 -9
- package/templates/firebase/lib/i18n/es.i18n.json +23 -9
- package/templates/firebase/lib/i18n/pt.i18n.json +23 -9
- package/templates/firebase/lib/router.dart +43 -25
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/admin_shell_chrome_test.dart +104 -0
- package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
- 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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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]
|
|
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:
|
|
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.
|
|
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
|
-
///
|
|
218
|
-
/// [KasyDatePickerLocale.es].
|
|
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
|
|
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].
|
|
504
|
-
///
|
|
505
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
//
|
|
871
|
-
//
|
|
872
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
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
|
-
|
|
2163
|
+
dayNumber,
|
|
2149
2164
|
],
|
|
2150
2165
|
);
|
|
2151
2166
|
|