kasy-cli 1.17.0 → 1.18.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 +15 -2
- package/lib/commands/add.js +7 -7
- package/lib/commands/configure.js +548 -0
- package/lib/commands/deploy.js +4 -4
- package/lib/commands/doctor.js +17 -0
- package/lib/commands/favicon.js +4 -4
- package/lib/commands/icon.js +5 -5
- package/lib/commands/new.js +403 -238
- package/lib/commands/run.js +1 -1
- package/lib/commands/splash.js +5 -5
- package/lib/commands/update.js +9 -9
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +108 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +44 -5
- package/lib/scaffold/generate.js +24 -8
- package/lib/scaffold/shared/post-build.js +8 -0
- package/lib/utils/brand.js +16 -12
- package/lib/utils/flutter-run.js +139 -11
- package/lib/utils/i18n/messages-en.js +58 -5
- package/lib/utils/i18n/messages-es.js +58 -5
- package/lib/utils/i18n/messages-pt.js +59 -6
- package/lib/utils/ui.js +79 -4
- package/package.json +1 -1
- 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/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +0 -15
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +65 -26
- package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +7 -2
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +6 -6
- package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +1 -1
- package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +1 -1
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +3 -3
- package/templates/firebase/android/app/src/main/res/values/colors.xml +32 -0
- package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
- package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
- package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +75 -29
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
- package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_avatar.dart +65 -17
- package/templates/firebase/lib/components/kasy_avatar_presets.dart +121 -97
- package/templates/firebase/lib/components/kasy_button.dart +8 -8
- package/templates/firebase/lib/components/kasy_date_picker.dart +834 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +145 -61
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -18
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +565 -77
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +10 -5
- package/templates/firebase/lib/i18n/en.i18n.json +2 -1
- package/templates/firebase/lib/i18n/es.i18n.json +2 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +2 -1
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/web/index.html +9 -0
- package/templates/firebase/web/splash/img/dark-1x.png +0 -0
- package/templates/firebase/web/splash/img/dark-2x.png +0 -0
- package/templates/firebase/web/splash/img/dark-3x.png +0 -0
- package/templates/firebase/web/splash/img/dark-4x.png +0 -0
- package/templates/firebase/web/splash/img/light-1x.png +0 -0
- package/templates/firebase/web/splash/img/light-2x.png +0 -0
- package/templates/firebase/web/splash/img/light-3x.png +0 -0
- package/templates/firebase/web/splash/img/light-4x.png +0 -0
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:kasy_kit/core/theme/theme.dart';
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Data helpers
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const List<String> _kMonthNames = [
|
|
9
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
10
|
+
'July', 'August', 'September', 'October', 'November', 'December',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const List<String> _kWeekdayAbbreviations = [
|
|
14
|
+
'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/// Returns the number of days in [month] of [year].
|
|
18
|
+
int _daysInMonth(int year, int month) => DateTime(year, month + 1, 0).day;
|
|
19
|
+
|
|
20
|
+
/// Formats [date] as "mm / dd / yyyy".
|
|
21
|
+
String _formatDate(DateTime date) =>
|
|
22
|
+
'${date.month.toString().padLeft(2, '0')} / '
|
|
23
|
+
'${date.day.toString().padLeft(2, '0')} / '
|
|
24
|
+
'${date.year}';
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// _DayData — internal model for a single calendar cell
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
class _DayData {
|
|
31
|
+
const _DayData({
|
|
32
|
+
required this.date,
|
|
33
|
+
required this.isCurrentMonth,
|
|
34
|
+
this.isToday = false,
|
|
35
|
+
this.isDisabled = false,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
final DateTime date;
|
|
39
|
+
final bool isCurrentMonth;
|
|
40
|
+
final bool isToday;
|
|
41
|
+
final bool isDisabled;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Builds the 42-cell (6 × 7) grid for [viewMonth].
|
|
45
|
+
///
|
|
46
|
+
/// Cells before day 1 are filled from the previous month;
|
|
47
|
+
/// remaining cells after the last day are from the next month.
|
|
48
|
+
List<_DayData> _buildDayGrid(
|
|
49
|
+
DateTime viewMonth, {
|
|
50
|
+
DateTime? minDate,
|
|
51
|
+
DateTime? maxDate,
|
|
52
|
+
}) {
|
|
53
|
+
final DateTime today = DateTime.now();
|
|
54
|
+
final int daysInCurrent = _daysInMonth(viewMonth.year, viewMonth.month);
|
|
55
|
+
|
|
56
|
+
// Grid starts on Sunday (index 0).
|
|
57
|
+
// Dart weekday: 1=Mon … 7=Sun → sunday offset = weekday % 7.
|
|
58
|
+
final DateTime firstOfMonth = DateTime(viewMonth.year, viewMonth.month);
|
|
59
|
+
final int offset = firstOfMonth.weekday % 7;
|
|
60
|
+
|
|
61
|
+
// Previous-month fill.
|
|
62
|
+
final int prevYear =
|
|
63
|
+
viewMonth.month == 1 ? viewMonth.year - 1 : viewMonth.year;
|
|
64
|
+
final int prevMonth = viewMonth.month == 1 ? 12 : viewMonth.month - 1;
|
|
65
|
+
final int daysInPrev = _daysInMonth(prevYear, prevMonth);
|
|
66
|
+
|
|
67
|
+
final List<_DayData> grid = [];
|
|
68
|
+
|
|
69
|
+
// Prev-month trailing days.
|
|
70
|
+
for (int i = 0; i < offset; i++) {
|
|
71
|
+
final int d = daysInPrev - offset + 1 + i;
|
|
72
|
+
final DateTime date = DateTime(prevYear, prevMonth, d);
|
|
73
|
+
grid.add(_DayData(
|
|
74
|
+
date: date,
|
|
75
|
+
isCurrentMonth: false,
|
|
76
|
+
isDisabled: _isDayDisabled(date, minDate, maxDate),
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Current-month days.
|
|
81
|
+
for (int d = 1; d <= daysInCurrent; d++) {
|
|
82
|
+
final DateTime date = DateTime(viewMonth.year, viewMonth.month, d);
|
|
83
|
+
final bool isToday = date.year == today.year &&
|
|
84
|
+
date.month == today.month &&
|
|
85
|
+
date.day == today.day;
|
|
86
|
+
grid.add(_DayData(
|
|
87
|
+
date: date,
|
|
88
|
+
isCurrentMonth: true,
|
|
89
|
+
isToday: isToday,
|
|
90
|
+
isDisabled: _isDayDisabled(date, minDate, maxDate),
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Next-month leading days.
|
|
95
|
+
final int nextYear =
|
|
96
|
+
viewMonth.month == 12 ? viewMonth.year + 1 : viewMonth.year;
|
|
97
|
+
final int nextMonth = viewMonth.month == 12 ? 1 : viewMonth.month + 1;
|
|
98
|
+
int nextDay = 1;
|
|
99
|
+
while (grid.length < 42) {
|
|
100
|
+
final DateTime date = DateTime(nextYear, nextMonth, nextDay++);
|
|
101
|
+
grid.add(_DayData(
|
|
102
|
+
date: date,
|
|
103
|
+
isCurrentMonth: false,
|
|
104
|
+
isDisabled: _isDayDisabled(date, minDate, maxDate),
|
|
105
|
+
));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return grid;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
bool _isDayDisabled(DateTime date, DateTime? min, DateTime? max) {
|
|
112
|
+
final DateTime d = DateTime(date.year, date.month, date.day);
|
|
113
|
+
if (min != null) {
|
|
114
|
+
final DateTime mn = DateTime(min.year, min.month, min.day);
|
|
115
|
+
if (d.isBefore(mn)) return true;
|
|
116
|
+
}
|
|
117
|
+
if (max != null) {
|
|
118
|
+
final DateTime mx = DateTime(max.year, max.month, max.day);
|
|
119
|
+
if (d.isAfter(mx)) return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
bool _isSameDay(DateTime a, DateTime b) =>
|
|
125
|
+
a.year == b.year && a.month == b.month && a.day == b.day;
|
|
126
|
+
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
// Design constants (from Figma variable defs)
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
// DateField: 3 drop-shadow layers — offset(0,2)blur:4, offset(0,1)blur:2, offset(0,0)blur:1
|
|
132
|
+
const List<BoxShadow> _kFieldShadows = [
|
|
133
|
+
BoxShadow(
|
|
134
|
+
color: Color(0x0A000000), // field/shadow rgba(0,0,0,0.04)
|
|
135
|
+
blurRadius: 4,
|
|
136
|
+
offset: Offset(0, 2),
|
|
137
|
+
),
|
|
138
|
+
BoxShadow(
|
|
139
|
+
color: Color(0x0F000000), // field/shadow-2 rgba(0,0,0,0.06)
|
|
140
|
+
blurRadius: 2,
|
|
141
|
+
offset: Offset(0, 1),
|
|
142
|
+
),
|
|
143
|
+
BoxShadow(
|
|
144
|
+
color: Color(0x0F000000), // field/shadow-2 rgba(0,0,0,0.06)
|
|
145
|
+
blurRadius: 1,
|
|
146
|
+
),
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
// Calendar overlay: shadow-overlay from Figma variable defs.
|
|
150
|
+
const List<BoxShadow> _kCalendarShadows = [
|
|
151
|
+
BoxShadow(
|
|
152
|
+
color: Color(0x14000000), // rgba(0,0,0,0.08)
|
|
153
|
+
blurRadius: 28,
|
|
154
|
+
offset: Offset(0, 14),
|
|
155
|
+
),
|
|
156
|
+
BoxShadow(
|
|
157
|
+
color: Color(0x08000000), // rgba(0,0,0,0.03)
|
|
158
|
+
blurRadius: 12,
|
|
159
|
+
offset: Offset(0, -6),
|
|
160
|
+
),
|
|
161
|
+
BoxShadow(
|
|
162
|
+
color: Color(0x0F000000), // rgba(0,0,0,0.06)
|
|
163
|
+
blurRadius: 8,
|
|
164
|
+
offset: Offset(0, 2),
|
|
165
|
+
),
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// KasyDatePicker
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/// Date picker combining a text-field trigger with a floating calendar overlay.
|
|
173
|
+
///
|
|
174
|
+
/// Tapping the field opens a calendar below the trigger. Selecting a day
|
|
175
|
+
/// calls [onChanged] and closes the calendar.
|
|
176
|
+
///
|
|
177
|
+
/// Usage:
|
|
178
|
+
/// ```dart
|
|
179
|
+
/// KasyDatePicker(
|
|
180
|
+
/// label: 'Date',
|
|
181
|
+
/// value: _date,
|
|
182
|
+
/// onChanged: (d) => setState(() => _date = d),
|
|
183
|
+
/// )
|
|
184
|
+
/// ```
|
|
185
|
+
class KasyDatePicker extends StatefulWidget {
|
|
186
|
+
const KasyDatePicker({
|
|
187
|
+
super.key,
|
|
188
|
+
this.label,
|
|
189
|
+
this.value,
|
|
190
|
+
this.onChanged,
|
|
191
|
+
this.minDate,
|
|
192
|
+
this.maxDate,
|
|
193
|
+
this.enabled = true,
|
|
194
|
+
this.placeholder = 'mm / dd / yyyy',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/// Label displayed above the field. Omit to hide.
|
|
198
|
+
final String? label;
|
|
199
|
+
|
|
200
|
+
/// Currently selected date. Pass null for no selection.
|
|
201
|
+
final DateTime? value;
|
|
202
|
+
|
|
203
|
+
/// Called when the user picks a date.
|
|
204
|
+
final ValueChanged<DateTime>? onChanged;
|
|
205
|
+
|
|
206
|
+
/// Earliest selectable date (inclusive).
|
|
207
|
+
final DateTime? minDate;
|
|
208
|
+
|
|
209
|
+
/// Latest selectable date (inclusive).
|
|
210
|
+
final DateTime? maxDate;
|
|
211
|
+
|
|
212
|
+
final bool enabled;
|
|
213
|
+
|
|
214
|
+
/// Placeholder text shown when no date is selected.
|
|
215
|
+
final String placeholder;
|
|
216
|
+
|
|
217
|
+
@override
|
|
218
|
+
State<KasyDatePicker> createState() => _KasyDatePickerState();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
class _KasyDatePickerState extends State<KasyDatePicker>
|
|
222
|
+
with SingleTickerProviderStateMixin {
|
|
223
|
+
final LayerLink _layerLink = LayerLink();
|
|
224
|
+
final OverlayPortalController _portalController = OverlayPortalController();
|
|
225
|
+
|
|
226
|
+
// Unique ID so TapRegion knows "field + calendar" form one group.
|
|
227
|
+
final Object _tapGroupId = Object();
|
|
228
|
+
|
|
229
|
+
// Month currently displayed in the calendar (always first of the month).
|
|
230
|
+
late DateTime _viewMonth;
|
|
231
|
+
|
|
232
|
+
bool _isOpen = false;
|
|
233
|
+
|
|
234
|
+
late final AnimationController _animCtrl;
|
|
235
|
+
late final Animation<double> _fadeAnim;
|
|
236
|
+
late final Animation<double> _scaleAnim;
|
|
237
|
+
|
|
238
|
+
@override
|
|
239
|
+
void initState() {
|
|
240
|
+
super.initState();
|
|
241
|
+
_viewMonth = _monthOf(widget.value ?? DateTime.now());
|
|
242
|
+
_animCtrl = AnimationController(
|
|
243
|
+
vsync: this,
|
|
244
|
+
duration: const Duration(milliseconds: 180),
|
|
245
|
+
);
|
|
246
|
+
_fadeAnim = CurvedAnimation(
|
|
247
|
+
parent: _animCtrl,
|
|
248
|
+
curve: Curves.easeOut,
|
|
249
|
+
reverseCurve: Curves.easeIn,
|
|
250
|
+
);
|
|
251
|
+
_scaleAnim = Tween<double>(begin: 0.95, end: 1.0).animate(
|
|
252
|
+
CurvedAnimation(parent: _animCtrl, curve: Curves.easeOut),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@override
|
|
257
|
+
void didUpdateWidget(KasyDatePicker old) {
|
|
258
|
+
super.didUpdateWidget(old);
|
|
259
|
+
if (old.value != widget.value && widget.value != null) {
|
|
260
|
+
_viewMonth = _monthOf(widget.value!);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@override
|
|
265
|
+
void dispose() {
|
|
266
|
+
_animCtrl.dispose();
|
|
267
|
+
super.dispose();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
DateTime _monthOf(DateTime d) => DateTime(d.year, d.month);
|
|
271
|
+
|
|
272
|
+
// ── Calendar toggle ────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
void _toggleCalendar() {
|
|
275
|
+
if (!widget.enabled) return;
|
|
276
|
+
if (_isOpen) {
|
|
277
|
+
_close();
|
|
278
|
+
} else {
|
|
279
|
+
_open();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
void _open() {
|
|
284
|
+
_portalController.show();
|
|
285
|
+
_animCtrl.forward();
|
|
286
|
+
setState(() => _isOpen = true);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
void _close() {
|
|
290
|
+
_animCtrl.reverse().then((_) {
|
|
291
|
+
if (mounted) _portalController.hide();
|
|
292
|
+
});
|
|
293
|
+
setState(() => _isOpen = false);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
void _selectDate(DateTime date) {
|
|
297
|
+
widget.onChanged?.call(date);
|
|
298
|
+
_close();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
void _previousMonth() {
|
|
302
|
+
setState(() {
|
|
303
|
+
_viewMonth = DateTime(
|
|
304
|
+
_viewMonth.month == 1 ? _viewMonth.year - 1 : _viewMonth.year,
|
|
305
|
+
_viewMonth.month == 1 ? 12 : _viewMonth.month - 1,
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
void _nextMonth() {
|
|
311
|
+
setState(() {
|
|
312
|
+
_viewMonth = DateTime(
|
|
313
|
+
_viewMonth.month == 12 ? _viewMonth.year + 1 : _viewMonth.year,
|
|
314
|
+
_viewMonth.month == 12 ? 1 : _viewMonth.month + 1,
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Build ──────────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
@override
|
|
322
|
+
Widget build(BuildContext context) {
|
|
323
|
+
return TapRegion(
|
|
324
|
+
groupId: _tapGroupId,
|
|
325
|
+
child: OverlayPortal(
|
|
326
|
+
controller: _portalController,
|
|
327
|
+
overlayChildBuilder: _buildOverlay,
|
|
328
|
+
child: CompositedTransformTarget(
|
|
329
|
+
link: _layerLink,
|
|
330
|
+
child: GestureDetector(
|
|
331
|
+
onTap: _toggleCalendar,
|
|
332
|
+
child: _buildField(context),
|
|
333
|
+
),
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── DateField trigger ──────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
Widget _buildField(BuildContext context) {
|
|
342
|
+
final KasyColors c = context.colors;
|
|
343
|
+
final DateTime? date = widget.value;
|
|
344
|
+
final bool hasDate = date != null;
|
|
345
|
+
|
|
346
|
+
return Column(
|
|
347
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
348
|
+
mainAxisSize: MainAxisSize.min,
|
|
349
|
+
children: [
|
|
350
|
+
if (widget.label != null) ...[
|
|
351
|
+
Text(
|
|
352
|
+
widget.label!,
|
|
353
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
354
|
+
color: c.onSurface,
|
|
355
|
+
fontWeight: FontWeight.w500,
|
|
356
|
+
fontSize: 14,
|
|
357
|
+
),
|
|
358
|
+
),
|
|
359
|
+
// 4px gap between label and field — per Figma spec.
|
|
360
|
+
const SizedBox(height: 4),
|
|
361
|
+
],
|
|
362
|
+
AnimatedContainer(
|
|
363
|
+
duration: const Duration(milliseconds: 150),
|
|
364
|
+
// Field height: 36px — per Figma spec (dimensions/spacing/9).
|
|
365
|
+
height: 36,
|
|
366
|
+
decoration: BoxDecoration(
|
|
367
|
+
color: c.surface,
|
|
368
|
+
// 12px radius — per Figma spec (field/radius: 12).
|
|
369
|
+
borderRadius: BorderRadius.circular(KasyRadius.md),
|
|
370
|
+
// Border is always 1px transparent — the focus ring uses
|
|
371
|
+
// box-shadow spread (not border-width) so layout never shifts.
|
|
372
|
+
border: Border.all(color: Colors.transparent),
|
|
373
|
+
// Open: solid 2px primary ring only (no field shadows while
|
|
374
|
+
// focused — matches Figma focus-ring-field token).
|
|
375
|
+
// Closed: three-layer depth shadow only (no ring).
|
|
376
|
+
boxShadow: _isOpen
|
|
377
|
+
? [
|
|
378
|
+
BoxShadow(
|
|
379
|
+
color: c.primary,
|
|
380
|
+
spreadRadius: 2,
|
|
381
|
+
),
|
|
382
|
+
]
|
|
383
|
+
: _kFieldShadows,
|
|
384
|
+
),
|
|
385
|
+
child: Row(
|
|
386
|
+
children: [
|
|
387
|
+
// 12px left padding — per Figma spec (dimensions/spacing/3).
|
|
388
|
+
const SizedBox(width: 12),
|
|
389
|
+
Expanded(
|
|
390
|
+
child: Text(
|
|
391
|
+
hasDate ? _formatDate(date) : widget.placeholder,
|
|
392
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
393
|
+
color: hasDate ? c.onSurface : c.muted,
|
|
394
|
+
fontWeight:
|
|
395
|
+
hasDate ? FontWeight.w500 : FontWeight.w400,
|
|
396
|
+
fontSize: 14,
|
|
397
|
+
),
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
// Calendar icon suffix — 12px padding per Figma spec.
|
|
401
|
+
SizedBox(
|
|
402
|
+
width: 40,
|
|
403
|
+
height: 36,
|
|
404
|
+
child: Center(
|
|
405
|
+
child: Opacity(
|
|
406
|
+
opacity: widget.enabled ? 1.0 : 0.4,
|
|
407
|
+
child: Icon(
|
|
408
|
+
KasyIcons.calendar,
|
|
409
|
+
size: 16,
|
|
410
|
+
color: c.muted,
|
|
411
|
+
),
|
|
412
|
+
),
|
|
413
|
+
),
|
|
414
|
+
),
|
|
415
|
+
],
|
|
416
|
+
),
|
|
417
|
+
),
|
|
418
|
+
],
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Calendar overlay ───────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
Widget _buildOverlay(BuildContext context) {
|
|
425
|
+
return TapRegion(
|
|
426
|
+
groupId: _tapGroupId,
|
|
427
|
+
onTapOutside: (_) => _close(),
|
|
428
|
+
child: CompositedTransformFollower(
|
|
429
|
+
link: _layerLink,
|
|
430
|
+
targetAnchor: Alignment.bottomLeft,
|
|
431
|
+
// 8px gap between field bottom and calendar top — per Figma spec.
|
|
432
|
+
offset: const Offset(0, 8),
|
|
433
|
+
child: Align(
|
|
434
|
+
alignment: Alignment.topLeft,
|
|
435
|
+
child: FadeTransition(
|
|
436
|
+
opacity: _fadeAnim,
|
|
437
|
+
child: ScaleTransition(
|
|
438
|
+
scale: _scaleAnim,
|
|
439
|
+
alignment: Alignment.topCenter,
|
|
440
|
+
child: _CalendarPanel(
|
|
441
|
+
viewMonth: _viewMonth,
|
|
442
|
+
selectedDate: widget.value,
|
|
443
|
+
onDateSelected: _selectDate,
|
|
444
|
+
onPreviousMonth: _previousMonth,
|
|
445
|
+
onNextMonth: _nextMonth,
|
|
446
|
+
minDate: widget.minDate,
|
|
447
|
+
maxDate: widget.maxDate,
|
|
448
|
+
),
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
),
|
|
452
|
+
),
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
458
|
+
// _CalendarPanel
|
|
459
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
class _CalendarPanel extends StatelessWidget {
|
|
462
|
+
const _CalendarPanel({
|
|
463
|
+
required this.viewMonth,
|
|
464
|
+
required this.selectedDate,
|
|
465
|
+
required this.onDateSelected,
|
|
466
|
+
required this.onPreviousMonth,
|
|
467
|
+
required this.onNextMonth,
|
|
468
|
+
this.minDate,
|
|
469
|
+
this.maxDate,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
final DateTime viewMonth;
|
|
473
|
+
final DateTime? selectedDate;
|
|
474
|
+
final ValueChanged<DateTime> onDateSelected;
|
|
475
|
+
final VoidCallback onPreviousMonth;
|
|
476
|
+
final VoidCallback onNextMonth;
|
|
477
|
+
final DateTime? minDate;
|
|
478
|
+
final DateTime? maxDate;
|
|
479
|
+
|
|
480
|
+
@override
|
|
481
|
+
Widget build(BuildContext context) {
|
|
482
|
+
final KasyColors c = context.colors;
|
|
483
|
+
final List<_DayData> days = _buildDayGrid(
|
|
484
|
+
viewMonth,
|
|
485
|
+
minDate: minDate,
|
|
486
|
+
maxDate: maxDate,
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
return Material(
|
|
490
|
+
color: Colors.transparent,
|
|
491
|
+
child: Container(
|
|
492
|
+
// Width matches the DateField — 288px per Figma spec.
|
|
493
|
+
width: 288,
|
|
494
|
+
decoration: BoxDecoration(
|
|
495
|
+
color: c.surface,
|
|
496
|
+
// 24px radius — per Figma spec (dimensions/radius/rounded-3xl: 24).
|
|
497
|
+
borderRadius: BorderRadius.circular(KasyRadius.xl),
|
|
498
|
+
boxShadow: _kCalendarShadows,
|
|
499
|
+
),
|
|
500
|
+
child: Padding(
|
|
501
|
+
// 16px all sides — per Figma spec (dimensions/spacing/4: 16).
|
|
502
|
+
padding: const EdgeInsets.all(KasySpacing.md),
|
|
503
|
+
child: Column(
|
|
504
|
+
mainAxisSize: MainAxisSize.min,
|
|
505
|
+
children: [
|
|
506
|
+
// Navigation row (month/year + prev/next arrows).
|
|
507
|
+
_CalendarNavRow(
|
|
508
|
+
viewMonth: viewMonth,
|
|
509
|
+
onPrevious: onPreviousMonth,
|
|
510
|
+
onNext: onNextMonth,
|
|
511
|
+
),
|
|
512
|
+
// Weekday abbreviation headers.
|
|
513
|
+
const _WeekdayHeaderRow(),
|
|
514
|
+
// Day grid: 6 rows × 7 columns, 2px gap between rows.
|
|
515
|
+
for (int row = 0; row < 6; row++) ...[
|
|
516
|
+
if (row > 0) const SizedBox(height: 2),
|
|
517
|
+
_DayRow(
|
|
518
|
+
days: days.sublist(row * 7, row * 7 + 7),
|
|
519
|
+
selectedDate: selectedDate,
|
|
520
|
+
onDateSelected: onDateSelected,
|
|
521
|
+
),
|
|
522
|
+
],
|
|
523
|
+
],
|
|
524
|
+
),
|
|
525
|
+
),
|
|
526
|
+
),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
532
|
+
// _CalendarNavRow — Month/year header with prev/next navigation
|
|
533
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
class _CalendarNavRow extends StatelessWidget {
|
|
536
|
+
const _CalendarNavRow({
|
|
537
|
+
required this.viewMonth,
|
|
538
|
+
required this.onPrevious,
|
|
539
|
+
required this.onNext,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
final DateTime viewMonth;
|
|
543
|
+
final VoidCallback onPrevious;
|
|
544
|
+
final VoidCallback onNext;
|
|
545
|
+
|
|
546
|
+
@override
|
|
547
|
+
Widget build(BuildContext context) {
|
|
548
|
+
final KasyColors c = context.colors;
|
|
549
|
+
|
|
550
|
+
return SizedBox(
|
|
551
|
+
// Navigation row height: 56px (16px padding top+bottom) — Figma spec.
|
|
552
|
+
height: 56,
|
|
553
|
+
child: Row(
|
|
554
|
+
children: [
|
|
555
|
+
// Month + Year text.
|
|
556
|
+
Expanded(
|
|
557
|
+
child: Row(
|
|
558
|
+
children: [
|
|
559
|
+
Text(
|
|
560
|
+
_kMonthNames[viewMonth.month - 1],
|
|
561
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
562
|
+
color: c.onSurface,
|
|
563
|
+
fontWeight: FontWeight.w500,
|
|
564
|
+
fontSize: 14,
|
|
565
|
+
),
|
|
566
|
+
),
|
|
567
|
+
const SizedBox(width: 6),
|
|
568
|
+
Text(
|
|
569
|
+
viewMonth.year.toString(),
|
|
570
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
571
|
+
color: c.onSurface,
|
|
572
|
+
fontWeight: FontWeight.w500,
|
|
573
|
+
fontSize: 14,
|
|
574
|
+
),
|
|
575
|
+
),
|
|
576
|
+
const SizedBox(width: 4),
|
|
577
|
+
// Expand chevron (right arrow, 12px) — indicates year picker.
|
|
578
|
+
Icon(
|
|
579
|
+
KasyIcons.chevronRight,
|
|
580
|
+
size: 12,
|
|
581
|
+
color: c.muted,
|
|
582
|
+
),
|
|
583
|
+
],
|
|
584
|
+
),
|
|
585
|
+
),
|
|
586
|
+
// Previous-month arrow button.
|
|
587
|
+
_NavArrowButton(
|
|
588
|
+
icon: KasyIcons.arrowBackIos,
|
|
589
|
+
onTap: onPrevious,
|
|
590
|
+
),
|
|
591
|
+
const SizedBox(width: 4),
|
|
592
|
+
// Next-month arrow button.
|
|
593
|
+
_NavArrowButton(
|
|
594
|
+
icon: KasyIcons.arrowForwardIos,
|
|
595
|
+
onTap: onNext,
|
|
596
|
+
),
|
|
597
|
+
],
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
604
|
+
// _NavArrowButton — 24×24 rounded navigation arrow
|
|
605
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
606
|
+
|
|
607
|
+
class _NavArrowButton extends StatefulWidget {
|
|
608
|
+
const _NavArrowButton({required this.icon, required this.onTap});
|
|
609
|
+
|
|
610
|
+
final IconData icon;
|
|
611
|
+
final VoidCallback onTap;
|
|
612
|
+
|
|
613
|
+
@override
|
|
614
|
+
State<_NavArrowButton> createState() => _NavArrowButtonState();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
class _NavArrowButtonState extends State<_NavArrowButton> {
|
|
618
|
+
bool _hovered = false;
|
|
619
|
+
|
|
620
|
+
@override
|
|
621
|
+
Widget build(BuildContext context) {
|
|
622
|
+
final KasyColors c = context.colors;
|
|
623
|
+
|
|
624
|
+
return GestureDetector(
|
|
625
|
+
onTap: widget.onTap,
|
|
626
|
+
onTapDown: (_) => setState(() => _hovered = true),
|
|
627
|
+
onTapUp: (_) => setState(() => _hovered = false),
|
|
628
|
+
onTapCancel: () => setState(() => _hovered = false),
|
|
629
|
+
child: AnimatedContainer(
|
|
630
|
+
duration: const Duration(milliseconds: 100),
|
|
631
|
+
// Button size: 24×24 — per Figma spec.
|
|
632
|
+
width: 24,
|
|
633
|
+
height: 24,
|
|
634
|
+
// 4px padding → 6px radius — per Figma (rounded-md: 6).
|
|
635
|
+
decoration: BoxDecoration(
|
|
636
|
+
color: _hovered ? c.avatarFallbackFill : Colors.transparent,
|
|
637
|
+
borderRadius: BorderRadius.circular(6),
|
|
638
|
+
),
|
|
639
|
+
child: Center(
|
|
640
|
+
child: Icon(
|
|
641
|
+
widget.icon,
|
|
642
|
+
size: 16,
|
|
643
|
+
color: c.onSurface,
|
|
644
|
+
),
|
|
645
|
+
),
|
|
646
|
+
),
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
652
|
+
// _WeekdayHeaderRow — Sun Mon Tue Wed Thu Fri Sat
|
|
653
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
654
|
+
|
|
655
|
+
class _WeekdayHeaderRow extends StatelessWidget {
|
|
656
|
+
const _WeekdayHeaderRow();
|
|
657
|
+
|
|
658
|
+
@override
|
|
659
|
+
Widget build(BuildContext context) {
|
|
660
|
+
final KasyColors c = context.colors;
|
|
661
|
+
|
|
662
|
+
return Row(
|
|
663
|
+
children: _kWeekdayAbbreviations.map((abbr) {
|
|
664
|
+
return Expanded(
|
|
665
|
+
child: SizedBox(
|
|
666
|
+
height: 36,
|
|
667
|
+
child: Center(
|
|
668
|
+
child: Text(
|
|
669
|
+
abbr,
|
|
670
|
+
style: context.textTheme.labelMedium?.copyWith(
|
|
671
|
+
// 12px/w500 — per Figma spec (Body xs medium: text-xs).
|
|
672
|
+
color: c.muted,
|
|
673
|
+
fontSize: 12,
|
|
674
|
+
fontWeight: FontWeight.w500,
|
|
675
|
+
),
|
|
676
|
+
),
|
|
677
|
+
),
|
|
678
|
+
),
|
|
679
|
+
);
|
|
680
|
+
}).toList(),
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
686
|
+
// _DayRow — one week row (7 day cells)
|
|
687
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
688
|
+
|
|
689
|
+
class _DayRow extends StatelessWidget {
|
|
690
|
+
const _DayRow({
|
|
691
|
+
required this.days,
|
|
692
|
+
required this.selectedDate,
|
|
693
|
+
required this.onDateSelected,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
final List<_DayData> days;
|
|
697
|
+
final DateTime? selectedDate;
|
|
698
|
+
final ValueChanged<DateTime> onDateSelected;
|
|
699
|
+
|
|
700
|
+
@override
|
|
701
|
+
Widget build(BuildContext context) {
|
|
702
|
+
return Row(
|
|
703
|
+
children: days.map((day) {
|
|
704
|
+
final bool isSelected =
|
|
705
|
+
selectedDate != null && _isSameDay(day.date, selectedDate!);
|
|
706
|
+
return Expanded(
|
|
707
|
+
child: _DayCell(
|
|
708
|
+
data: day,
|
|
709
|
+
isSelected: isSelected,
|
|
710
|
+
onTap: (day.isDisabled || !day.isCurrentMonth && day.isDisabled)
|
|
711
|
+
? null
|
|
712
|
+
: () => onDateSelected(day.date),
|
|
713
|
+
),
|
|
714
|
+
);
|
|
715
|
+
}).toList(),
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
721
|
+
// _DayCell — individual calendar day
|
|
722
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
723
|
+
|
|
724
|
+
class _DayCell extends StatefulWidget {
|
|
725
|
+
const _DayCell({
|
|
726
|
+
required this.data,
|
|
727
|
+
required this.isSelected,
|
|
728
|
+
required this.onTap,
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
final _DayData data;
|
|
732
|
+
final bool isSelected;
|
|
733
|
+
final VoidCallback? onTap;
|
|
734
|
+
|
|
735
|
+
@override
|
|
736
|
+
State<_DayCell> createState() => _DayCellState();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
class _DayCellState extends State<_DayCell> {
|
|
740
|
+
bool _pressed = false;
|
|
741
|
+
|
|
742
|
+
@override
|
|
743
|
+
Widget build(BuildContext context) {
|
|
744
|
+
final KasyColors c = context.colors;
|
|
745
|
+
final _DayData day = widget.data;
|
|
746
|
+
|
|
747
|
+
// ── Color determination ────────────────────────────────────────────────
|
|
748
|
+
|
|
749
|
+
final Color textColor;
|
|
750
|
+
final Color? bgColor;
|
|
751
|
+
|
|
752
|
+
if (widget.isSelected) {
|
|
753
|
+
textColor = const Color(0xFFFCFCFC); // accent/accent-foreground
|
|
754
|
+
bgColor = c.primary;
|
|
755
|
+
} else if (day.isToday) {
|
|
756
|
+
textColor = c.primary; // accent/accent-soft-foreground
|
|
757
|
+
bgColor = _pressed ? c.avatarFallbackFill : null;
|
|
758
|
+
} else if (!day.isCurrentMonth) {
|
|
759
|
+
textColor = c.muted; // out-of-month
|
|
760
|
+
bgColor = _pressed ? c.avatarFallbackFill : null;
|
|
761
|
+
} else if (day.isDisabled) {
|
|
762
|
+
textColor = c.muted;
|
|
763
|
+
bgColor = null;
|
|
764
|
+
} else {
|
|
765
|
+
textColor = c.onSurface; // default
|
|
766
|
+
bgColor = _pressed ? c.avatarFallbackFill : null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ── Today indicator dot ────────────────────────────────────────────────
|
|
770
|
+
// 3px dot, positioned 4px from bottom — Figma spec.
|
|
771
|
+
// Hidden when the day is selected (blue circle already communicates
|
|
772
|
+
// selection clearly — dot would be redundant).
|
|
773
|
+
final bool showDot = day.isToday && !widget.isSelected;
|
|
774
|
+
final Color dotColor = widget.isSelected
|
|
775
|
+
? const Color(0xFFFCFCFC)
|
|
776
|
+
: c.muted;
|
|
777
|
+
|
|
778
|
+
// ── Opacity ────────────────────────────────────────────────────────────
|
|
779
|
+
// Out-of-month and disabled cells use 50% opacity — Figma spec.
|
|
780
|
+
final double opacity =
|
|
781
|
+
(!day.isCurrentMonth || day.isDisabled) ? 0.5 : 1.0;
|
|
782
|
+
|
|
783
|
+
final Widget cell = AnimatedContainer(
|
|
784
|
+
duration: const Duration(milliseconds: 100),
|
|
785
|
+
// Cell size: 36×36 — Figma spec (dimensions/spacing/9: 36).
|
|
786
|
+
width: 36,
|
|
787
|
+
height: 36,
|
|
788
|
+
decoration: BoxDecoration(
|
|
789
|
+
color: bgColor,
|
|
790
|
+
// 24px radius → pill/circle — Figma spec (rounded-3xl: 24).
|
|
791
|
+
borderRadius: BorderRadius.circular(KasyRadius.xl),
|
|
792
|
+
),
|
|
793
|
+
child: Stack(
|
|
794
|
+
alignment: Alignment.center,
|
|
795
|
+
children: [
|
|
796
|
+
// Day number.
|
|
797
|
+
Text(
|
|
798
|
+
day.date.day.toString(),
|
|
799
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
800
|
+
color: textColor,
|
|
801
|
+
fontWeight: FontWeight.w500,
|
|
802
|
+
fontSize: 14,
|
|
803
|
+
),
|
|
804
|
+
),
|
|
805
|
+
// Today indicator dot — 3px circle at bottom center.
|
|
806
|
+
if (showDot)
|
|
807
|
+
Positioned(
|
|
808
|
+
bottom: 4,
|
|
809
|
+
child: Container(
|
|
810
|
+
width: 3,
|
|
811
|
+
height: 3,
|
|
812
|
+
decoration: BoxDecoration(
|
|
813
|
+
color: dotColor,
|
|
814
|
+
borderRadius: BorderRadius.circular(12),
|
|
815
|
+
),
|
|
816
|
+
),
|
|
817
|
+
),
|
|
818
|
+
],
|
|
819
|
+
),
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
if (widget.onTap == null) {
|
|
823
|
+
return Opacity(opacity: opacity, child: Center(child: cell));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return GestureDetector(
|
|
827
|
+
onTap: widget.onTap,
|
|
828
|
+
onTapDown: (_) => setState(() => _pressed = true),
|
|
829
|
+
onTapUp: (_) => setState(() => _pressed = false),
|
|
830
|
+
onTapCancel: () => setState(() => _pressed = false),
|
|
831
|
+
child: Opacity(opacity: opacity, child: Center(child: cell)),
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
}
|