kasy-cli 1.19.1 → 1.20.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/bin/kasy.js +1 -0
- package/lib/commands/new.js +9 -0
- package/lib/commands/run.js +22 -6
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +91 -2
- package/lib/scaffold/engine.js +5 -0
- package/lib/scaffold/generate.js +4 -0
- package/lib/scaffold/shared/generator-utils.js +38 -1
- package/lib/utils/flutter-run.js +16 -4
- package/lib/utils/i18n/messages-en.js +2 -1
- package/lib/utils/i18n/messages-es.js +2 -1
- package/lib/utils/i18n/messages-pt.js +2 -1
- package/package.json +1 -1
- package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
- package/templates/firebase/lib/components/kasy_date_picker.dart +14 -8
- package/templates/firebase/lib/components/kasy_sidebar_pro.dart +1148 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +156 -43
- package/templates/firebase/lib/components/kasy_text_field.dart +39 -29
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +90 -69
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +30 -7
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +8 -1
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +439 -232
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +198 -83
- package/templates/firebase/lib/core/icons/kasy_icons.dart +1 -0
- package/templates/firebase/lib/core/theme/colors.dart +6 -2
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +119 -19
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +68 -27
- package/templates/firebase/lib/features/home/home_components_page.dart +3 -0
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +202 -79
- package/templates/firebase/lib/features/settings/settings_page.dart +27 -146
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +16 -3
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +22 -5
- package/templates/firebase/lib/i18n/en.i18n.json +3 -1
- package/templates/firebase/lib/i18n/es.i18n.json +3 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +3 -1
- package/templates/firebase/pubspec.yaml +6 -4
- package/templates/firebase/web/index.html +7 -17
- package/templates/firebase/lib/firebase_options.dart +0 -75
|
@@ -7,18 +7,33 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
7
7
|
import 'package:kasy_kit/core/haptics/haptic_feedback_notifier.dart';
|
|
8
8
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
9
9
|
|
|
10
|
-
/// Universal hover/press wrapper — works on any widget shape.
|
|
10
|
+
/// Universal hover/press wrapper — works on any widget shape, any platform.
|
|
11
11
|
///
|
|
12
|
-
///
|
|
13
|
-
/// The pressed state
|
|
14
|
-
/// even on quick taps.
|
|
12
|
+
/// **Press feedback:** animated background fill + optional haptic. No Material
|
|
13
|
+
/// ripple. The pressed state persists for at least [_kMinPressDuration] so
|
|
14
|
+
/// it is always visible, even on quick taps.
|
|
15
15
|
///
|
|
16
|
-
///
|
|
16
|
+
/// **Hover feedback:** subtle overlay on pointer-capable devices (web,
|
|
17
|
+
/// desktop, trackpad). Has no visible effect on touch-only devices.
|
|
18
|
+
///
|
|
19
|
+
/// **Cursor:** [SystemMouseCursors.click] is set automatically on all
|
|
20
|
+
/// platforms that support pointer input.
|
|
21
|
+
///
|
|
22
|
+
/// ### Customising colours
|
|
23
|
+
///
|
|
24
|
+
/// - [pressColor] — tint base for the press overlay (defaults to
|
|
25
|
+
/// `onSurface`). Pass the surface accent colour (e.g. `primary`) to avoid
|
|
26
|
+
/// a mismatched gray cast on tinted backgrounds.
|
|
27
|
+
///
|
|
28
|
+
/// - [hoverColor] — exact solid background colour shown while the pointer is
|
|
29
|
+
/// over the widget. When set, **replaces** the default semi-transparent
|
|
30
|
+
/// overlay with a solid fill. Use this on navigation items where hover and
|
|
31
|
+
/// selected states must look identical (same bg, bolder text/icon in the
|
|
32
|
+
/// child widget signals selection). When null, falls back to a subtle
|
|
33
|
+
/// [pressColor]-tinted overlay.
|
|
17
34
|
///
|
|
18
35
|
/// Contrast with [KasyPressableDepth]: this component uses a background fill
|
|
19
|
-
/// while [KasyPressableDepth] uses a scale animation.
|
|
20
|
-
/// surfaces that already have their own shape (list rows, cards, menu items,
|
|
21
|
-
/// custom buttons); use [KasyPressableDepth] when you want a "push-in" feel.
|
|
36
|
+
/// while [KasyPressableDepth] uses a scale/depth animation.
|
|
22
37
|
class KasyHover extends ConsumerStatefulWidget {
|
|
23
38
|
const KasyHover({
|
|
24
39
|
super.key,
|
|
@@ -29,6 +44,7 @@ class KasyHover extends ConsumerStatefulWidget {
|
|
|
29
44
|
this.margin,
|
|
30
45
|
this.semanticLabel,
|
|
31
46
|
this.hapticEnabled = true,
|
|
47
|
+
this.hoverColor,
|
|
32
48
|
this.pressColor,
|
|
33
49
|
});
|
|
34
50
|
|
|
@@ -51,15 +67,24 @@ class KasyHover extends ConsumerStatefulWidget {
|
|
|
51
67
|
/// Haptic on tap (automatically ignored on web).
|
|
52
68
|
final bool hapticEnabled;
|
|
53
69
|
|
|
54
|
-
///
|
|
70
|
+
/// Exact background colour used while the pointer hovers over the widget.
|
|
71
|
+
///
|
|
72
|
+
/// When non-null, this solid colour replaces the default semi-transparent
|
|
73
|
+
/// overlay during hover. Ideal for navigation items: pass the same colour
|
|
74
|
+
/// used for the selected state so hover and active look identical — the
|
|
75
|
+
/// child widget is responsible for rendering the selected indicator (e.g.
|
|
76
|
+
/// bolder text, accent icon colour).
|
|
77
|
+
///
|
|
78
|
+
/// When null, a subtle [pressColor]-tinted overlay is used instead.
|
|
79
|
+
final Color? hoverColor;
|
|
80
|
+
|
|
81
|
+
/// Tint base for the press overlay.
|
|
55
82
|
///
|
|
56
83
|
/// When null, falls back to [ColorScheme.onSurface] — a neutral dark/light
|
|
57
84
|
/// overlay suited for plain surfaces.
|
|
58
85
|
///
|
|
59
|
-
/// Pass the surface's accent
|
|
60
|
-
/// background is already tinted so the feedback stays on-palette
|
|
61
|
-
/// a mismatched gray cast (e.g. a primary-tinted button should deepen with
|
|
62
|
-
/// primary, not darken with neutral gray).
|
|
86
|
+
/// Pass the surface's accent colour (e.g. [KasyColors.primary]) when the
|
|
87
|
+
/// background is already tinted so the feedback stays on-palette.
|
|
63
88
|
final Color? pressColor;
|
|
64
89
|
|
|
65
90
|
@override
|
|
@@ -85,7 +110,21 @@ class _KasyHoverState extends ConsumerState<KasyHover> {
|
|
|
85
110
|
final bool isDark = Theme.of(context).brightness == Brightness.dark;
|
|
86
111
|
final Color base = widget.pressColor ?? context.colors.onSurface;
|
|
87
112
|
if (_pressed) return base.withValues(alpha: isDark ? 0.04 : 0.10);
|
|
88
|
-
if (_hovered)
|
|
113
|
+
if (_hovered) {
|
|
114
|
+
// Solid hover colour — used for nav items where hover must exactly
|
|
115
|
+
// match the selected background.
|
|
116
|
+
if (widget.hoverColor != null) return widget.hoverColor!;
|
|
117
|
+
return base.withValues(alpha: isDark ? 0.02 : 0.06);
|
|
118
|
+
}
|
|
119
|
+
// IMPORTANT: when hoverColor is set, the resting state must be the same
|
|
120
|
+
// colour at alpha=0 (not Colors.transparent = 0x00000000 black).
|
|
121
|
+
// AnimatedContainer lerps between the two values — lerping from black-
|
|
122
|
+
// transparent to a light solid colour passes through dark intermediates,
|
|
123
|
+
// causing a visible flash. Lerping from the same hue at alpha=0 to
|
|
124
|
+
// alpha=1 only changes opacity → no dark artefact.
|
|
125
|
+
if (widget.hoverColor != null) {
|
|
126
|
+
return widget.hoverColor!.withValues(alpha: 0);
|
|
127
|
+
}
|
|
89
128
|
return Colors.transparent;
|
|
90
129
|
}
|
|
91
130
|
|
|
@@ -136,19 +175,21 @@ class _KasyHoverState extends ConsumerState<KasyHover> {
|
|
|
136
175
|
child: content,
|
|
137
176
|
);
|
|
138
177
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
178
|
+
// MouseRegion is active on all platforms:
|
|
179
|
+
// - Web / desktop / trackpad: fires onEnter/onExit for real hover effects.
|
|
180
|
+
// - Touch-only (iOS/Android): pointer events never fire, zero overhead.
|
|
181
|
+
// The click cursor is also set universally so desktop feels native.
|
|
182
|
+
interactive = MouseRegion(
|
|
183
|
+
cursor: SystemMouseCursors.click,
|
|
184
|
+
onEnter: (_) => setState(() => _hovered = true),
|
|
185
|
+
onExit: (_) {
|
|
186
|
+
setState(() {
|
|
187
|
+
_hovered = false;
|
|
188
|
+
_pressed = false;
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
child: interactive,
|
|
192
|
+
);
|
|
152
193
|
|
|
153
194
|
if (widget.semanticLabel != null) {
|
|
154
195
|
interactive = Semantics(
|
|
@@ -189,6 +189,7 @@ const Set<String> _kReadyComponents = <String>{
|
|
|
189
189
|
'TextArea',
|
|
190
190
|
'TextField',
|
|
191
191
|
'Sidebar',
|
|
192
|
+
'Sidebar Pro',
|
|
192
193
|
'Skeleton',
|
|
193
194
|
'SwipeAction',
|
|
194
195
|
'TextFieldOTP',
|
|
@@ -211,6 +212,7 @@ const Set<String> _kWebReadyComponents = <String>{
|
|
|
211
212
|
'Dialog',
|
|
212
213
|
'Hover',
|
|
213
214
|
'Sidebar',
|
|
215
|
+
'Sidebar Pro',
|
|
214
216
|
'Skeleton',
|
|
215
217
|
'SwipeAction',
|
|
216
218
|
'TextArea',
|
|
@@ -241,6 +243,7 @@ const List<_CatalogRow> _kCatalog = [
|
|
|
241
243
|
_CatalogRow('Dialog'),
|
|
242
244
|
_CatalogRow('Hover'),
|
|
243
245
|
_CatalogRow('Sidebar'),
|
|
246
|
+
_CatalogRow('Sidebar Pro'),
|
|
244
247
|
_CatalogRow('Skeleton'),
|
|
245
248
|
_CatalogRow('SwipeAction'),
|
|
246
249
|
_CatalogRow('TextArea'),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import 'package:bart/bart/bart_model.dart';
|
|
2
|
+
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
2
3
|
import 'package:flutter/material.dart';
|
|
3
4
|
import 'package:flutter/services.dart';
|
|
4
5
|
import 'package:kasy_kit/components/components.dart';
|
|
6
|
+
import 'package:kasy_kit/components/kasy_sidebar_pro.dart';
|
|
5
7
|
import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
|
|
6
8
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
7
9
|
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
@@ -243,10 +245,6 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
243
245
|
label: 'Range selection',
|
|
244
246
|
builder: _buildDatePickerRange,
|
|
245
247
|
),
|
|
246
|
-
ComponentPreviewVariant(
|
|
247
|
-
label: 'Multi-month',
|
|
248
|
-
builder: _buildDatePickerMultiMonth,
|
|
249
|
-
),
|
|
250
248
|
ComponentPreviewVariant(
|
|
251
249
|
label: 'Date formats',
|
|
252
250
|
builder: _buildDatePickerFormats,
|
|
@@ -259,6 +257,13 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
259
257
|
label: 'Field states',
|
|
260
258
|
builder: _buildDatePickerFieldStates,
|
|
261
259
|
),
|
|
260
|
+
// Multi-month last — it's a desktop-only opt-in layout and the
|
|
261
|
+
// preview itself falls back to an explainer card on mobile, so
|
|
262
|
+
// keeping it at the end avoids burying the main use cases.
|
|
263
|
+
ComponentPreviewVariant(
|
|
264
|
+
label: 'Multi-month',
|
|
265
|
+
builder: _buildDatePickerMultiMonth,
|
|
266
|
+
),
|
|
262
267
|
],
|
|
263
268
|
);
|
|
264
269
|
case 'Dialog':
|
|
@@ -512,6 +517,20 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
512
517
|
),
|
|
513
518
|
],
|
|
514
519
|
);
|
|
520
|
+
case 'Sidebar Pro':
|
|
521
|
+
return const ComponentPreviewDefinition(
|
|
522
|
+
title: 'Sidebar Pro',
|
|
523
|
+
variants: [
|
|
524
|
+
ComponentPreviewVariant(
|
|
525
|
+
label: 'Expanded',
|
|
526
|
+
builder: _buildSidebarProExpanded,
|
|
527
|
+
),
|
|
528
|
+
ComponentPreviewVariant(
|
|
529
|
+
label: 'Collapsed',
|
|
530
|
+
builder: _buildSidebarProCollapsed,
|
|
531
|
+
),
|
|
532
|
+
],
|
|
533
|
+
);
|
|
515
534
|
case 'Skeleton':
|
|
516
535
|
return const ComponentPreviewDefinition(
|
|
517
536
|
title: 'Skeleton',
|
|
@@ -684,6 +703,91 @@ class _SidebarPreviewState extends State<_SidebarPreview> {
|
|
|
684
703
|
}
|
|
685
704
|
|
|
686
705
|
|
|
706
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
707
|
+
// Sidebar Pro — interactive preview
|
|
708
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
709
|
+
|
|
710
|
+
Widget _buildSidebarProExpanded(BuildContext context) =>
|
|
711
|
+
const _SidebarProPreview(initiallyCollapsed: false);
|
|
712
|
+
|
|
713
|
+
Widget _buildSidebarProCollapsed(BuildContext context) =>
|
|
714
|
+
const _SidebarProPreview(initiallyCollapsed: true);
|
|
715
|
+
|
|
716
|
+
class _SidebarProPreview extends StatelessWidget {
|
|
717
|
+
const _SidebarProPreview({required this.initiallyCollapsed});
|
|
718
|
+
|
|
719
|
+
final bool initiallyCollapsed;
|
|
720
|
+
|
|
721
|
+
void _open(BuildContext context, {required bool fromEnd}) {
|
|
722
|
+
showGeneralDialog<void>(
|
|
723
|
+
context: context,
|
|
724
|
+
barrierDismissible: true,
|
|
725
|
+
barrierLabel: 'Dismiss',
|
|
726
|
+
barrierColor: Colors.black.withValues(alpha: 0.42),
|
|
727
|
+
transitionDuration: const Duration(milliseconds: 290),
|
|
728
|
+
pageBuilder: (ctx, anim, secAnim) => Align(
|
|
729
|
+
alignment: fromEnd ? Alignment.centerRight : Alignment.centerLeft,
|
|
730
|
+
child: SizedBox(
|
|
731
|
+
height: double.infinity,
|
|
732
|
+
child: KasySidebarPro(
|
|
733
|
+
initiallyCollapsed: initiallyCollapsed,
|
|
734
|
+
side: fromEnd
|
|
735
|
+
? KasySidebarProSide.right
|
|
736
|
+
: KasySidebarProSide.left,
|
|
737
|
+
),
|
|
738
|
+
),
|
|
739
|
+
),
|
|
740
|
+
transitionBuilder: (ctx, anim, secAnim, child) {
|
|
741
|
+
final curved = CurvedAnimation(
|
|
742
|
+
parent: anim,
|
|
743
|
+
curve: Curves.easeOutCubic,
|
|
744
|
+
);
|
|
745
|
+
return SlideTransition(
|
|
746
|
+
position: Tween<Offset>(
|
|
747
|
+
begin: Offset(fromEnd ? 1.0 : -1.0, 0),
|
|
748
|
+
end: Offset.zero,
|
|
749
|
+
).animate(curved),
|
|
750
|
+
child: FadeTransition(
|
|
751
|
+
opacity: Tween<double>(begin: 0.85, end: 1.0).animate(curved),
|
|
752
|
+
child: child,
|
|
753
|
+
),
|
|
754
|
+
);
|
|
755
|
+
},
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
@override
|
|
760
|
+
Widget build(BuildContext context) {
|
|
761
|
+
return Column(
|
|
762
|
+
mainAxisSize: MainAxisSize.min,
|
|
763
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
764
|
+
children: [
|
|
765
|
+
Padding(
|
|
766
|
+
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
|
|
767
|
+
child: _previewIntrinsicLaunch(
|
|
768
|
+
KasyButton(
|
|
769
|
+
label: 'Open Left',
|
|
770
|
+
variant: KasyButtonVariant.soft,
|
|
771
|
+
onPressed: () => _open(context, fromEnd: false),
|
|
772
|
+
),
|
|
773
|
+
),
|
|
774
|
+
),
|
|
775
|
+
const SizedBox(height: KasySpacing.sm),
|
|
776
|
+
Padding(
|
|
777
|
+
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
|
|
778
|
+
child: _previewIntrinsicLaunch(
|
|
779
|
+
KasyButton(
|
|
780
|
+
label: 'Open Right',
|
|
781
|
+
variant: KasyButtonVariant.soft,
|
|
782
|
+
onPressed: () => _open(context, fromEnd: true),
|
|
783
|
+
),
|
|
784
|
+
),
|
|
785
|
+
),
|
|
786
|
+
],
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
687
791
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
688
792
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
689
793
|
// Tabs — interactive demos
|
|
@@ -2626,29 +2730,18 @@ Widget _buildTextFieldCharacterLimit(BuildContext context) {
|
|
|
2626
2730
|
}
|
|
2627
2731
|
|
|
2628
2732
|
Widget _buildTextFieldVariantsVariant(BuildContext context) {
|
|
2629
|
-
|
|
2630
|
-
context.textTheme.labelSmall?.copyWith(
|
|
2631
|
-
color: context.colors.muted,
|
|
2632
|
-
letterSpacing: 1.2,
|
|
2633
|
-
fontWeight: FontWeight.w700,
|
|
2634
|
-
) ??
|
|
2635
|
-
const TextStyle();
|
|
2636
|
-
return Column(
|
|
2733
|
+
return const Column(
|
|
2637
2734
|
mainAxisSize: MainAxisSize.min,
|
|
2638
2735
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
2639
2736
|
children: [
|
|
2640
|
-
|
|
2641
|
-
const SizedBox(height: KasySpacing.sm),
|
|
2642
|
-
const KasyTextField(
|
|
2737
|
+
KasyTextField(
|
|
2643
2738
|
label: 'Primary Variant',
|
|
2644
2739
|
hint: 'Primary style input',
|
|
2645
2740
|
description: 'Default variant with primary styling',
|
|
2646
2741
|
contentType: KasyTextFieldContentType.email,
|
|
2647
2742
|
),
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
const SizedBox(height: KasySpacing.sm),
|
|
2651
|
-
const KasyTextField(
|
|
2743
|
+
SizedBox(height: KasySpacing.lg),
|
|
2744
|
+
KasyTextField(
|
|
2652
2745
|
label: 'Secondary Variant',
|
|
2653
2746
|
hint: 'Secondary style input',
|
|
2654
2747
|
description: 'Secondary variant for surfaces',
|
|
@@ -2660,39 +2753,26 @@ Widget _buildTextFieldVariantsVariant(BuildContext context) {
|
|
|
2660
2753
|
}
|
|
2661
2754
|
|
|
2662
2755
|
Widget _buildTextFieldStates(BuildContext context) {
|
|
2663
|
-
|
|
2664
|
-
context.textTheme.labelSmall?.copyWith(
|
|
2665
|
-
color: context.colors.muted,
|
|
2666
|
-
letterSpacing: 1.2,
|
|
2667
|
-
fontWeight: FontWeight.w700,
|
|
2668
|
-
) ??
|
|
2669
|
-
const TextStyle();
|
|
2670
|
-
return Column(
|
|
2756
|
+
return const Column(
|
|
2671
2757
|
mainAxisSize: MainAxisSize.min,
|
|
2672
2758
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
2673
2759
|
children: [
|
|
2674
|
-
|
|
2675
|
-
const SizedBox(height: KasySpacing.sm),
|
|
2676
|
-
const KasyTextField(
|
|
2760
|
+
KasyTextField(
|
|
2677
2761
|
label: 'Default State',
|
|
2678
2762
|
hint: 'Enter your email',
|
|
2679
2763
|
description: 'Normal input state',
|
|
2680
2764
|
contentType: KasyTextFieldContentType.email,
|
|
2681
2765
|
),
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
const SizedBox(height: KasySpacing.sm),
|
|
2685
|
-
const KasyTextField(
|
|
2766
|
+
SizedBox(height: KasySpacing.lg),
|
|
2767
|
+
KasyTextField(
|
|
2686
2768
|
label: 'Disabled State',
|
|
2687
2769
|
hint: 'Read only value',
|
|
2688
2770
|
description: 'TextField is disabled and cannot be edited',
|
|
2689
2771
|
enabled: false,
|
|
2690
2772
|
contentType: KasyTextFieldContentType.email,
|
|
2691
2773
|
),
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
const SizedBox(height: KasySpacing.sm),
|
|
2695
|
-
const KasyTextField(
|
|
2774
|
+
SizedBox(height: KasySpacing.lg),
|
|
2775
|
+
KasyTextField(
|
|
2696
2776
|
label: 'Invalid State',
|
|
2697
2777
|
hint: 'Enter your email',
|
|
2698
2778
|
errorText: 'Please enter a valid email address',
|
|
@@ -2720,29 +2800,18 @@ Widget _buildTextAreaWithLabelDescription(BuildContext context) {
|
|
|
2720
2800
|
}
|
|
2721
2801
|
|
|
2722
2802
|
Widget _buildTextAreaStates(BuildContext context) {
|
|
2723
|
-
|
|
2724
|
-
context.textTheme.labelSmall?.copyWith(
|
|
2725
|
-
color: context.colors.muted,
|
|
2726
|
-
letterSpacing: 1.2,
|
|
2727
|
-
fontWeight: FontWeight.w700,
|
|
2728
|
-
) ??
|
|
2729
|
-
const TextStyle();
|
|
2730
|
-
return Column(
|
|
2803
|
+
return const Column(
|
|
2731
2804
|
mainAxisSize: MainAxisSize.min,
|
|
2732
2805
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
2733
2806
|
children: [
|
|
2734
|
-
|
|
2735
|
-
const SizedBox(height: KasySpacing.sm),
|
|
2736
|
-
const KasyTextArea(
|
|
2807
|
+
KasyTextArea(
|
|
2737
2808
|
label: 'Disabled State',
|
|
2738
2809
|
hint: 'Read only value',
|
|
2739
2810
|
description: 'Text area is disabled and cannot be edited',
|
|
2740
2811
|
enabled: false,
|
|
2741
2812
|
),
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
const SizedBox(height: KasySpacing.sm),
|
|
2745
|
-
const KasyTextArea(
|
|
2813
|
+
SizedBox(height: KasySpacing.lg),
|
|
2814
|
+
KasyTextArea(
|
|
2746
2815
|
label: 'Invalid State',
|
|
2747
2816
|
hint: 'Enter your message',
|
|
2748
2817
|
isInvalid: true,
|
|
@@ -4859,19 +4928,10 @@ class _TextFieldWithIconContentState extends State<_TextFieldWithIconContent> {
|
|
|
4859
4928
|
|
|
4860
4929
|
@override
|
|
4861
4930
|
Widget build(BuildContext context) {
|
|
4862
|
-
final TextStyle labelStyle =
|
|
4863
|
-
context.textTheme.labelSmall?.copyWith(
|
|
4864
|
-
color: context.colors.muted,
|
|
4865
|
-
letterSpacing: 1.2,
|
|
4866
|
-
fontWeight: FontWeight.w700,
|
|
4867
|
-
) ??
|
|
4868
|
-
const TextStyle();
|
|
4869
4931
|
return Column(
|
|
4870
4932
|
mainAxisSize: MainAxisSize.min,
|
|
4871
4933
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
4872
4934
|
children: [
|
|
4873
|
-
Text('PHONE', style: labelStyle),
|
|
4874
|
-
const SizedBox(height: KasySpacing.sm),
|
|
4875
4935
|
const KasyTextField(
|
|
4876
4936
|
label: 'Phone',
|
|
4877
4937
|
hint: '5550000000',
|
|
@@ -4879,16 +4939,12 @@ class _TextFieldWithIconContentState extends State<_TextFieldWithIconContent> {
|
|
|
4879
4939
|
contentType: KasyTextFieldContentType.phone,
|
|
4880
4940
|
),
|
|
4881
4941
|
const SizedBox(height: KasySpacing.lg),
|
|
4882
|
-
Text('PASSWORD', style: labelStyle),
|
|
4883
|
-
const SizedBox(height: KasySpacing.sm),
|
|
4884
4942
|
const KasyTextField(
|
|
4885
4943
|
label: 'Password',
|
|
4886
4944
|
hint: 'Enter your password',
|
|
4887
4945
|
contentType: KasyTextFieldContentType.password,
|
|
4888
4946
|
),
|
|
4889
4947
|
const SizedBox(height: KasySpacing.lg),
|
|
4890
|
-
Text('MESSAGE', style: labelStyle),
|
|
4891
|
-
const SizedBox(height: KasySpacing.sm),
|
|
4892
4948
|
KasyTextField(
|
|
4893
4949
|
controller: _messageController,
|
|
4894
4950
|
label: 'Message',
|
|
@@ -8693,6 +8749,13 @@ class _DatePickerMultiMonthPreview extends StatefulWidget {
|
|
|
8693
8749
|
_DatePickerMultiMonthPreviewState();
|
|
8694
8750
|
}
|
|
8695
8751
|
|
|
8752
|
+
// Breakpoints for the multi-month preview. Below the 2-month threshold the
|
|
8753
|
+
// preview shows an explanation card instead of the calendars — picking dates
|
|
8754
|
+
// across 2 or 3 grids only makes sense on canvases wide enough to lay them
|
|
8755
|
+
// out side by side.
|
|
8756
|
+
const double _kMultiMonthMinWidth2 = 620;
|
|
8757
|
+
const double _kMultiMonthMinWidth3 = 920;
|
|
8758
|
+
|
|
8696
8759
|
class _DatePickerMultiMonthPreviewState
|
|
8697
8760
|
extends State<_DatePickerMultiMonthPreview> {
|
|
8698
8761
|
KasyDateRange? _stay;
|
|
@@ -8700,24 +8763,38 @@ class _DatePickerMultiMonthPreviewState
|
|
|
8700
8763
|
|
|
8701
8764
|
@override
|
|
8702
8765
|
Widget build(BuildContext context) {
|
|
8766
|
+
// Multi-month is a desktop/wide-tablet pattern. On native iOS/Android and
|
|
8767
|
+
// narrow web viewports we don't have horizontal room to lay 2–3 month
|
|
8768
|
+
// grids side by side, so the preview shows a notice instead.
|
|
8769
|
+
final double width = MediaQuery.sizeOf(context).width;
|
|
8770
|
+
final bool fitsTwoMonths = kIsWeb && width >= _kMultiMonthMinWidth2;
|
|
8771
|
+
final bool fitsThreeMonths = kIsWeb && width >= _kMultiMonthMinWidth3;
|
|
8772
|
+
|
|
8773
|
+
if (!fitsTwoMonths) {
|
|
8774
|
+
return _MultiMonthUnavailableNotice();
|
|
8775
|
+
}
|
|
8776
|
+
|
|
8703
8777
|
final KasyDatePickerLocale locale = _datePickerLocaleForApp(context);
|
|
8704
8778
|
return Column(
|
|
8705
8779
|
mainAxisSize: MainAxisSize.min,
|
|
8706
8780
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
8707
8781
|
children: [
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8782
|
+
if (fitsThreeMonths) ...[
|
|
8783
|
+
// 3-month layout combined with range selection — the classic
|
|
8784
|
+
// Airbnb / Booking desktop pattern for picking a stay.
|
|
8785
|
+
KasyDatePicker(
|
|
8786
|
+
label: '3 months + range',
|
|
8787
|
+
placeholder: 'Check-in -> Check-out',
|
|
8788
|
+
description:
|
|
8789
|
+
'Pass monthsToShow: 3 to opt in. Best on wide canvases.',
|
|
8790
|
+
selectionMode: KasyDateSelectionMode.range,
|
|
8791
|
+
range: _stay,
|
|
8792
|
+
onRangeChanged: (r) => setState(() => _stay = r),
|
|
8793
|
+
monthsToShow: 3,
|
|
8794
|
+
locale: locale,
|
|
8795
|
+
),
|
|
8796
|
+
const SizedBox(height: KasySpacing.lg),
|
|
8797
|
+
],
|
|
8721
8798
|
// 2-month layout in single-date mode — useful when there's enough
|
|
8722
8799
|
// horizontal room but you still want one tap to commit.
|
|
8723
8800
|
KasyDatePicker(
|
|
@@ -8733,6 +8810,52 @@ class _DatePickerMultiMonthPreviewState
|
|
|
8733
8810
|
}
|
|
8734
8811
|
}
|
|
8735
8812
|
|
|
8813
|
+
/// Friendly placeholder shown when the multi-month preview is opened on a
|
|
8814
|
+
/// viewport too narrow for the layout (native mobile, narrow web). Surfacing
|
|
8815
|
+
/// this instead of hiding the variant keeps the design-system docs honest:
|
|
8816
|
+
/// the feature exists, it just isn't appropriate on small canvases.
|
|
8817
|
+
class _MultiMonthUnavailableNotice extends StatelessWidget {
|
|
8818
|
+
@override
|
|
8819
|
+
Widget build(BuildContext context) {
|
|
8820
|
+
final KasyColors c = context.colors;
|
|
8821
|
+
return Container(
|
|
8822
|
+
padding: const EdgeInsets.all(KasySpacing.lg),
|
|
8823
|
+
decoration: BoxDecoration(
|
|
8824
|
+
color: c.avatarFallbackFill,
|
|
8825
|
+
borderRadius: BorderRadius.circular(KasyRadius.md),
|
|
8826
|
+
),
|
|
8827
|
+
child: Column(
|
|
8828
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
8829
|
+
mainAxisSize: MainAxisSize.min,
|
|
8830
|
+
children: [
|
|
8831
|
+
Row(
|
|
8832
|
+
children: [
|
|
8833
|
+
Icon(KasyIcons.info, size: 18, color: c.primary),
|
|
8834
|
+
const SizedBox(width: KasySpacing.sm),
|
|
8835
|
+
Text(
|
|
8836
|
+
'Multi-month works best on wide screens',
|
|
8837
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
8838
|
+
fontWeight: FontWeight.w700,
|
|
8839
|
+
color: c.onSurface,
|
|
8840
|
+
),
|
|
8841
|
+
),
|
|
8842
|
+
],
|
|
8843
|
+
),
|
|
8844
|
+
const SizedBox(height: KasySpacing.sm),
|
|
8845
|
+
Text(
|
|
8846
|
+
'The 2 and 3-month layouts need room to render side by side, so '
|
|
8847
|
+
'they are previewed only on web at tablet width or larger. On '
|
|
8848
|
+
'mobile breakpoints and native iOS/Android the picker stays at '
|
|
8849
|
+
'a single month — opt in by passing monthsToShow: 2 or 3 from '
|
|
8850
|
+
'your own desktop screens.',
|
|
8851
|
+
style: context.textTheme.bodyMedium?.copyWith(color: c.muted),
|
|
8852
|
+
),
|
|
8853
|
+
],
|
|
8854
|
+
),
|
|
8855
|
+
);
|
|
8856
|
+
}
|
|
8857
|
+
}
|
|
8858
|
+
|
|
8736
8859
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
8737
8860
|
// DatePicker — Variants (filled, no backdrop, no suffix, no focus border)
|
|
8738
8861
|
// ─────────────────────────────────────────────────────────────────────────────
|