kasy-cli 1.17.0 → 1.19.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 +16 -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 +483 -324
- package/lib/commands/run.js +17 -4
- 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 +123 -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 +62 -5
- package/lib/utils/i18n/messages-es.js +62 -5
- package/lib/utils/i18n/messages-pt.js +63 -6
- package/lib/utils/ui.js +79 -4
- package/package.json +1 -2
- 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 +2173 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +214 -91
- package/templates/firebase/lib/components/kasy_text_area.dart +9 -4
- package/templates/firebase/lib/components/kasy_text_field.dart +96 -36
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -2
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +88 -35
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +7 -43
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +118 -16
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +14 -20
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -18
- package/templates/firebase/lib/core/security/secured_storage.dart +56 -15
- package/templates/firebase/lib/core/theme/providers/theme_provider.dart +3 -0
- package/templates/firebase/lib/core/theme/web_background_sync.dart +3 -0
- package/templates/firebase/lib/core/theme/web_background_sync_web.dart +18 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -6
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +6 -0
- package/templates/firebase/lib/features/home/home_components_page.dart +3 -2
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +949 -77
- package/templates/firebase/lib/features/home/home_page.dart +17 -40
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +1 -16
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +0 -4
- 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/lib/main.dart +34 -34
- package/templates/firebase/pubspec.yaml +2 -1
- package/templates/firebase/storage.cors.json +8 -0
- package/templates/firebase/web/index.html +24 -2
- 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
- package/templates/firebase/lib/core/bottom_menu/kasy_bart_navigation.dart +0 -22
|
@@ -33,7 +33,7 @@ enum KasyTabsVariant {
|
|
|
33
33
|
|
|
34
34
|
/// Sizing mode for [KasyTabs].
|
|
35
35
|
enum KasyTabsMode {
|
|
36
|
-
/// Wraps content (intrinsic width tabs).
|
|
36
|
+
/// Wraps content (intrinsic width tabs). Scrolls horizontally if needed.
|
|
37
37
|
hug,
|
|
38
38
|
|
|
39
39
|
/// Each tab stretches to fill the available width equally.
|
|
@@ -99,6 +99,15 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
99
99
|
// One GlobalKey per tab to measure position/size after layout.
|
|
100
100
|
late List<GlobalKey> _keys;
|
|
101
101
|
|
|
102
|
+
// Key for the inner indicator Stack — used as the coordinate reference
|
|
103
|
+
// for measurements, ensuring correctness even when wrapped in a ScrollView.
|
|
104
|
+
final GlobalKey _stackKey = GlobalKey();
|
|
105
|
+
|
|
106
|
+
// Controls horizontal scrolling in hug mode so we can snap to the
|
|
107
|
+
// beginning/end when the first/last tab is selected (clears container
|
|
108
|
+
// padding and the pill overflow that extends 4px past tab edges).
|
|
109
|
+
final ScrollController _scrollController = ScrollController();
|
|
110
|
+
|
|
102
111
|
// Indicator geometry (left offset, width) resolved from measured keys.
|
|
103
112
|
double _indicatorLeft = 0;
|
|
104
113
|
double _indicatorWidth = 0;
|
|
@@ -116,6 +125,12 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
116
125
|
WidgetsBinding.instance.addPostFrameCallback((_) => _measure());
|
|
117
126
|
}
|
|
118
127
|
|
|
128
|
+
@override
|
|
129
|
+
void dispose() {
|
|
130
|
+
_scrollController.dispose();
|
|
131
|
+
super.dispose();
|
|
132
|
+
}
|
|
133
|
+
|
|
119
134
|
@override
|
|
120
135
|
void didUpdateWidget(KasyTabs old) {
|
|
121
136
|
super.didUpdateWidget(old);
|
|
@@ -140,8 +155,10 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
140
155
|
widget.items.length - 1,
|
|
141
156
|
);
|
|
142
157
|
|
|
158
|
+
// Measure relative to the inner Stack, not the outermost widget.
|
|
159
|
+
// This stays correct even when the component is inside a ScrollView.
|
|
143
160
|
final RenderBox? containerBox =
|
|
144
|
-
|
|
161
|
+
_stackKey.currentContext?.findRenderObject() as RenderBox?;
|
|
145
162
|
if (containerBox == null) return;
|
|
146
163
|
|
|
147
164
|
final GlobalKey key = _keys[clampedIndex];
|
|
@@ -166,6 +183,34 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
166
183
|
_measured = true;
|
|
167
184
|
});
|
|
168
185
|
}
|
|
186
|
+
|
|
187
|
+
// In hug mode, scroll the selected tab into view (handles overflow).
|
|
188
|
+
if (widget.mode == KasyTabsMode.hug) {
|
|
189
|
+
_ensureSelectedVisible(clampedIndex);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Scrolls the [SingleChildScrollView] so the selected tab is fully visible.
|
|
194
|
+
///
|
|
195
|
+
/// First/last tab snap to the scroll extremes so the container's outer
|
|
196
|
+
/// padding (and the pill that extends 4px beyond tab edges) is never clipped.
|
|
197
|
+
void _ensureSelectedVisible(int index) {
|
|
198
|
+
const Duration duration = Duration(milliseconds: 250);
|
|
199
|
+
const Curve curve = Curves.easeInOut;
|
|
200
|
+
final int lastIndex = widget.items.length - 1;
|
|
201
|
+
|
|
202
|
+
if (_scrollController.hasClients &&
|
|
203
|
+
(index == 0 || index == lastIndex)) {
|
|
204
|
+
final double target = index == 0
|
|
205
|
+
? _scrollController.position.minScrollExtent
|
|
206
|
+
: _scrollController.position.maxScrollExtent;
|
|
207
|
+
_scrollController.animateTo(target, duration: duration, curve: curve);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
final BuildContext? ctx = _keys[index].currentContext;
|
|
212
|
+
if (ctx == null) return;
|
|
213
|
+
Scrollable.ensureVisible(ctx, duration: duration, curve: curve);
|
|
169
214
|
}
|
|
170
215
|
|
|
171
216
|
@override
|
|
@@ -182,67 +227,99 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
182
227
|
Widget _buildPrimary(BuildContext context) {
|
|
183
228
|
final KasyColors c = context.colors;
|
|
184
229
|
final bool isDark = context.isDark;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
),
|
|
191
|
-
child:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
offset: const Offset(0, 2),
|
|
215
|
-
),
|
|
216
|
-
BoxShadow(
|
|
217
|
-
color: Colors.black.withValues(
|
|
218
|
-
alpha: isDark ? 0.10 : 0.04,
|
|
219
|
-
),
|
|
220
|
-
spreadRadius: 1,
|
|
230
|
+
final bool isFill = widget.mode == KasyTabsMode.fill;
|
|
231
|
+
final BorderRadius pillRadius = BorderRadius.circular(KasyRadius.full);
|
|
232
|
+
|
|
233
|
+
final Widget tabsContent = Padding(
|
|
234
|
+
// 8px horizontal, 4px vertical — per Figma spec.
|
|
235
|
+
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
236
|
+
child: Stack(
|
|
237
|
+
key: _stackKey,
|
|
238
|
+
// Allow pill to extend 4px beyond each tab edge (Figma: inset 0 -4px).
|
|
239
|
+
clipBehavior: Clip.none,
|
|
240
|
+
children: [
|
|
241
|
+
// Animated pill background.
|
|
242
|
+
if (_measured)
|
|
243
|
+
AnimatedPositioned(
|
|
244
|
+
duration: const Duration(milliseconds: 250),
|
|
245
|
+
curve: Curves.easeInOut,
|
|
246
|
+
// Extends 4px on each side beyond the measured tab.
|
|
247
|
+
left: _indicatorLeft - 4,
|
|
248
|
+
top: 0,
|
|
249
|
+
bottom: 0,
|
|
250
|
+
width: _indicatorWidth + 8,
|
|
251
|
+
child: DecoratedBox(
|
|
252
|
+
decoration: BoxDecoration(
|
|
253
|
+
color: c.surface,
|
|
254
|
+
borderRadius: BorderRadius.circular(KasyRadius.full),
|
|
255
|
+
boxShadow: [
|
|
256
|
+
BoxShadow(
|
|
257
|
+
color: Colors.black.withValues(
|
|
258
|
+
alpha: isDark ? 0.14 : 0.06,
|
|
221
259
|
),
|
|
222
|
-
|
|
223
|
-
|
|
260
|
+
blurRadius: 8,
|
|
261
|
+
offset: const Offset(0, 2),
|
|
262
|
+
),
|
|
263
|
+
],
|
|
224
264
|
),
|
|
225
265
|
),
|
|
226
|
-
// Tab labels (on top of the pill).
|
|
227
|
-
Row(
|
|
228
|
-
mainAxisSize: widget.mode == KasyTabsMode.fill
|
|
229
|
-
? MainAxisSize.max
|
|
230
|
-
: MainAxisSize.min,
|
|
231
|
-
children: List.generate(widget.items.length, (i) {
|
|
232
|
-
final KasyTabItem item = widget.items[i];
|
|
233
|
-
final bool selected = i == widget.selectedIndex;
|
|
234
|
-
return _PrimaryTab(
|
|
235
|
-
key: _keys[i],
|
|
236
|
-
item: item,
|
|
237
|
-
selected: selected,
|
|
238
|
-
expand: widget.mode == KasyTabsMode.fill,
|
|
239
|
-
onTap: item.enabled ? () => widget.onTabSelected(i) : null,
|
|
240
|
-
);
|
|
241
|
-
}),
|
|
242
266
|
),
|
|
243
|
-
|
|
267
|
+
// Tab labels (on top of the pill).
|
|
268
|
+
Row(
|
|
269
|
+
mainAxisSize: isFill ? MainAxisSize.max : MainAxisSize.min,
|
|
270
|
+
children: [
|
|
271
|
+
for (int i = 0; i < widget.items.length; i++) ...[
|
|
272
|
+
// 2px gap between adjacent tabs — per Figma spec.
|
|
273
|
+
if (i > 0) const SizedBox(width: 2),
|
|
274
|
+
_PrimaryTab(
|
|
275
|
+
key: _keys[i],
|
|
276
|
+
item: widget.items[i],
|
|
277
|
+
selected: i == widget.selectedIndex,
|
|
278
|
+
expand: isFill,
|
|
279
|
+
onTap: widget.items[i].enabled
|
|
280
|
+
? () => widget.onTabSelected(i)
|
|
281
|
+
: null,
|
|
282
|
+
),
|
|
283
|
+
],
|
|
284
|
+
],
|
|
285
|
+
),
|
|
286
|
+
],
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (!isFill) {
|
|
291
|
+
// Hug mode + horizontal overflow: keep the rounded container fixed at
|
|
292
|
+
// the parent's width and scroll only the tab strip INSIDE it. Putting
|
|
293
|
+
// the DecoratedBox inside the SingleChildScrollView (the previous
|
|
294
|
+
// structure) made the entire pill background scroll along with the
|
|
295
|
+
// tabs, so once the user scrolled the rounded corners were pushed off-
|
|
296
|
+
// screen and the visible portion looked like a flat-sided rectangle.
|
|
297
|
+
// Now the corners stay visible at the viewport edges no matter where
|
|
298
|
+
// the strip is scrolled, and ClipRRect masks any tab content that
|
|
299
|
+
// would otherwise leak past the rounded edges.
|
|
300
|
+
return DecoratedBox(
|
|
301
|
+
decoration: BoxDecoration(
|
|
302
|
+
color: c.avatarFallbackFill,
|
|
303
|
+
borderRadius: pillRadius,
|
|
244
304
|
),
|
|
305
|
+
child: ClipRRect(
|
|
306
|
+
borderRadius: pillRadius,
|
|
307
|
+
child: SingleChildScrollView(
|
|
308
|
+
controller: _scrollController,
|
|
309
|
+
scrollDirection: Axis.horizontal,
|
|
310
|
+
physics: const ClampingScrollPhysics(),
|
|
311
|
+
child: tabsContent,
|
|
312
|
+
),
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return DecoratedBox(
|
|
318
|
+
decoration: BoxDecoration(
|
|
319
|
+
color: c.avatarFallbackFill,
|
|
320
|
+
borderRadius: pillRadius,
|
|
245
321
|
),
|
|
322
|
+
child: tabsContent,
|
|
246
323
|
);
|
|
247
324
|
}
|
|
248
325
|
|
|
@@ -250,8 +327,10 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
250
327
|
|
|
251
328
|
Widget _buildSecondary(BuildContext context) {
|
|
252
329
|
final KasyColors c = context.colors;
|
|
330
|
+
final bool isFill = widget.mode == KasyTabsMode.fill;
|
|
253
331
|
|
|
254
|
-
|
|
332
|
+
final Widget inner = Stack(
|
|
333
|
+
key: _stackKey,
|
|
255
334
|
clipBehavior: Clip.none,
|
|
256
335
|
children: [
|
|
257
336
|
// Full-width bottom divider.
|
|
@@ -284,9 +363,7 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
284
363
|
),
|
|
285
364
|
// Tab labels.
|
|
286
365
|
Row(
|
|
287
|
-
mainAxisSize:
|
|
288
|
-
? MainAxisSize.max
|
|
289
|
-
: MainAxisSize.min,
|
|
366
|
+
mainAxisSize: isFill ? MainAxisSize.max : MainAxisSize.min,
|
|
290
367
|
children: List.generate(widget.items.length, (i) {
|
|
291
368
|
final KasyTabItem item = widget.items[i];
|
|
292
369
|
final bool selected = i == widget.selectedIndex;
|
|
@@ -294,13 +371,25 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
294
371
|
key: _keys[i],
|
|
295
372
|
item: item,
|
|
296
373
|
selected: selected,
|
|
297
|
-
expand:
|
|
374
|
+
expand: isFill,
|
|
298
375
|
onTap: item.enabled ? () => widget.onTabSelected(i) : null,
|
|
299
376
|
);
|
|
300
377
|
}),
|
|
301
378
|
),
|
|
302
379
|
],
|
|
303
380
|
);
|
|
381
|
+
|
|
382
|
+
// Same as primary: hug mode sizes to content via ScrollView.
|
|
383
|
+
if (!isFill) {
|
|
384
|
+
return SingleChildScrollView(
|
|
385
|
+
controller: _scrollController,
|
|
386
|
+
scrollDirection: Axis.horizontal,
|
|
387
|
+
physics: const ClampingScrollPhysics(),
|
|
388
|
+
child: inner,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return inner;
|
|
304
393
|
}
|
|
305
394
|
}
|
|
306
395
|
|
|
@@ -325,39 +414,66 @@ class _PrimaryTab extends StatelessWidget {
|
|
|
325
414
|
Widget _tabContent(BuildContext context) {
|
|
326
415
|
final KasyColors c = context.colors;
|
|
327
416
|
final bool disabled = !item.enabled;
|
|
417
|
+
// Fill mode with icons uses a vertical (Column) layout per Figma spec:
|
|
418
|
+
// icon stacked above label, 12px all-sides padding, 12px font size.
|
|
419
|
+
final bool verticalLayout = expand && item.icon != null;
|
|
420
|
+
|
|
421
|
+
final Widget iconWidget = item.icon != null
|
|
422
|
+
? Opacity(
|
|
423
|
+
opacity: disabled ? 0.4 : 1.0,
|
|
424
|
+
child: Icon(
|
|
425
|
+
item.icon,
|
|
426
|
+
size: 16,
|
|
427
|
+
color: selected ? c.onSurface : c.muted,
|
|
428
|
+
),
|
|
429
|
+
)
|
|
430
|
+
: const SizedBox.shrink();
|
|
431
|
+
|
|
432
|
+
final Widget labelWidget = Opacity(
|
|
433
|
+
opacity: disabled ? 0.4 : 1.0,
|
|
434
|
+
child: Text(
|
|
435
|
+
item.label,
|
|
436
|
+
textAlign: TextAlign.center,
|
|
437
|
+
// Use labelLarge as defined in the Kasy theme (14px/w600).
|
|
438
|
+
// No fontWeight override — the theme token is the source of truth.
|
|
439
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
440
|
+
color: selected ? c.onSurface : c.muted,
|
|
441
|
+
// Fill+icon layout uses 12px (text-xs) — per Figma spec.
|
|
442
|
+
fontSize: verticalLayout ? 12 : null,
|
|
443
|
+
),
|
|
444
|
+
),
|
|
445
|
+
);
|
|
328
446
|
|
|
329
447
|
return GestureDetector(
|
|
330
448
|
onTap: onTap,
|
|
331
449
|
behavior: HitTestBehavior.opaque,
|
|
332
450
|
child: Padding(
|
|
333
|
-
padding:
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
451
|
+
padding: verticalLayout
|
|
452
|
+
// Fill+icon: 12px all sides — per Figma spec.
|
|
453
|
+
? const EdgeInsets.all(12)
|
|
454
|
+
// Default: 6px vertical, 12px horizontal — per Figma spec.
|
|
455
|
+
: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
|
456
|
+
child: verticalLayout
|
|
457
|
+
? Column(
|
|
458
|
+
mainAxisSize: MainAxisSize.min,
|
|
459
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
460
|
+
children: [
|
|
461
|
+
iconWidget,
|
|
462
|
+
const SizedBox(height: 6),
|
|
463
|
+
labelWidget,
|
|
464
|
+
],
|
|
465
|
+
)
|
|
466
|
+
: Row(
|
|
467
|
+
mainAxisSize: MainAxisSize.min,
|
|
468
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
469
|
+
children: [
|
|
470
|
+
if (item.icon != null) ...[
|
|
471
|
+
iconWidget,
|
|
472
|
+
const SizedBox(width: 6),
|
|
473
|
+
],
|
|
474
|
+
labelWidget,
|
|
475
|
+
],
|
|
346
476
|
),
|
|
347
|
-
const SizedBox(width: 6),
|
|
348
|
-
],
|
|
349
|
-
Opacity(
|
|
350
|
-
opacity: disabled ? 0.4 : 1.0,
|
|
351
|
-
child: Text(
|
|
352
|
-
item.label,
|
|
353
|
-
style: context.textTheme.labelLarge?.copyWith(
|
|
354
|
-
color: selected ? c.onSurface : c.muted,
|
|
355
|
-
fontWeight: selected ? FontWeight.w500 : FontWeight.w400,
|
|
356
|
-
),
|
|
357
|
-
),
|
|
358
|
-
),
|
|
359
|
-
],
|
|
360
|
-
),
|
|
361
477
|
),
|
|
362
478
|
);
|
|
363
479
|
}
|
|
@@ -391,7 +507,14 @@ class _SecondaryTab extends StatelessWidget {
|
|
|
391
507
|
onTap: onTap,
|
|
392
508
|
behavior: HitTestBehavior.opaque,
|
|
393
509
|
child: Padding(
|
|
394
|
-
|
|
510
|
+
// top:4 bottom:6 horizontal:12 — per Figma spec.
|
|
511
|
+
// Extra bottom padding visually balances the 2px underline indicator.
|
|
512
|
+
padding: const EdgeInsets.only(
|
|
513
|
+
top: 4,
|
|
514
|
+
bottom: 6,
|
|
515
|
+
left: 12,
|
|
516
|
+
right: 12,
|
|
517
|
+
),
|
|
395
518
|
child: Row(
|
|
396
519
|
mainAxisSize: MainAxisSize.min,
|
|
397
520
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
@@ -411,9 +534,9 @@ class _SecondaryTab extends StatelessWidget {
|
|
|
411
534
|
opacity: disabled ? 0.4 : 1.0,
|
|
412
535
|
child: Text(
|
|
413
536
|
item.label,
|
|
537
|
+
// Use labelLarge as defined in the Kasy theme (14px/w600).
|
|
414
538
|
style: context.textTheme.labelLarge?.copyWith(
|
|
415
539
|
color: selected ? c.onSurface : c.muted,
|
|
416
|
-
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
|
|
417
540
|
),
|
|
418
541
|
),
|
|
419
542
|
),
|
|
@@ -135,9 +135,14 @@ class _KasyTextAreaState extends State<KasyTextArea> {
|
|
|
135
135
|
const double disabledLabelOpacity = 0.46;
|
|
136
136
|
const double disabledTextOpacity = 0.56;
|
|
137
137
|
const double disabledDescriptionOpacity = 0.34;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
// Contained shadow — no Y offset, negative spread so the shadow hugs the
|
|
139
|
+
// border instead of leaking a halo below the field. Matches KasyTextField.
|
|
140
|
+
final BoxShadow fieldShadow = BoxShadow(
|
|
141
|
+
color: const Color(0xFF000000).withValues(
|
|
142
|
+
alpha: context.isDark ? 0.28 : 0.11,
|
|
143
|
+
),
|
|
144
|
+
blurRadius: 3,
|
|
145
|
+
spreadRadius: -1,
|
|
141
146
|
);
|
|
142
147
|
|
|
143
148
|
const BorderRadius fieldRadius = BorderRadius.all(
|
|
@@ -345,7 +350,7 @@ class _KasyTextAreaState extends State<KasyTextArea> {
|
|
|
345
350
|
DecoratedBox(
|
|
346
351
|
decoration: BoxDecoration(
|
|
347
352
|
borderRadius: fieldRadius,
|
|
348
|
-
boxShadow:
|
|
353
|
+
boxShadow: [fieldShadow],
|
|
349
354
|
),
|
|
350
355
|
child: innerField,
|
|
351
356
|
),
|
|
@@ -56,6 +56,30 @@ class KasyTextField extends StatefulWidget {
|
|
|
56
56
|
/// Optional widget rendered to the right of [label] (e.g. a "Forgot password?" link).
|
|
57
57
|
final Widget? labelTrailing;
|
|
58
58
|
|
|
59
|
+
/// Override for the field's vertical/horizontal padding. When null, the
|
|
60
|
+
/// design-system default is used (`KasySpacing.md` horizontal, `10`
|
|
61
|
+
/// vertical on mobile / `webSingleLineVerticalPadding` on web). Pass a
|
|
62
|
+
/// smaller value to make the field render shorter.
|
|
63
|
+
final EdgeInsetsGeometry? contentPadding;
|
|
64
|
+
|
|
65
|
+
/// Override for the field's drop shadow. When null, the design-system
|
|
66
|
+
/// default is used (single soft shadow on primary mobile variant, nothing
|
|
67
|
+
/// on secondary/embedded/web). Pass an empty list to render no shadow.
|
|
68
|
+
final List<BoxShadow>? boxShadow;
|
|
69
|
+
|
|
70
|
+
/// When true (default), the field shows the design-system blue focus border
|
|
71
|
+
/// while focused. Set to false to keep the resting border in every state —
|
|
72
|
+
/// useful for read-only triggers or compact contexts where the focus
|
|
73
|
+
/// affordance would feel noisy.
|
|
74
|
+
final bool focusBorder;
|
|
75
|
+
|
|
76
|
+
/// Forwards to [TextField.enableInteractiveSelection]. When false, the
|
|
77
|
+
/// field renders no caret, suppresses text-selection gestures, and stops
|
|
78
|
+
/// showing the I-beam cursor on web/desktop — handy for read-only triggers
|
|
79
|
+
/// (like [KasyDatePicker]) that should look like an input but feel like a
|
|
80
|
+
/// button.
|
|
81
|
+
final bool enableInteractiveSelection;
|
|
82
|
+
|
|
59
83
|
const KasyTextField({
|
|
60
84
|
super.key,
|
|
61
85
|
this.controller,
|
|
@@ -89,6 +113,10 @@ class KasyTextField extends StatefulWidget {
|
|
|
89
113
|
this.prefix,
|
|
90
114
|
this.suffix,
|
|
91
115
|
this.labelTrailing,
|
|
116
|
+
this.contentPadding,
|
|
117
|
+
this.boxShadow,
|
|
118
|
+
this.focusBorder = true,
|
|
119
|
+
this.enableInteractiveSelection = true,
|
|
92
120
|
});
|
|
93
121
|
|
|
94
122
|
@override
|
|
@@ -210,12 +238,9 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
210
238
|
widget.variant == KasyTextFieldVariant.secondary;
|
|
211
239
|
final bool isEmbeddedVariant =
|
|
212
240
|
widget.variant == KasyTextFieldVariant.embedded;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
kIsWeb &&
|
|
217
|
-
widget.minLines == null &&
|
|
218
|
-
widget.maxLines == 1;
|
|
241
|
+
// (No more web-specific padding — the field now uses the same vertical
|
|
242
|
+
// padding on every platform so primary/web TextFields render at the same
|
|
243
|
+
// height as mobile and as the KasyDatePicker trigger.)
|
|
219
244
|
const double disabledTextOpacity = 0.56;
|
|
220
245
|
const double disabledLabelOpacity = 0.46;
|
|
221
246
|
const double disabledDescriptionOpacity = 0.34;
|
|
@@ -277,11 +302,17 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
277
302
|
),
|
|
278
303
|
);
|
|
279
304
|
final BorderRadius fieldRadius = BorderRadius.circular(KasyRadius.md);
|
|
305
|
+
// Contained shadow (no Y offset, negative spread). Renders on every
|
|
306
|
+
// platform — removed the previous `!kIsWeb` guard so web matches mobile
|
|
307
|
+
// and the KasyDatePicker trigger.
|
|
280
308
|
final bool shouldShowShadow =
|
|
281
|
-
!isSecondaryVariant && !isEmbeddedVariant
|
|
282
|
-
final BoxShadow resolvedShadow =
|
|
283
|
-
|
|
284
|
-
|
|
309
|
+
!isSecondaryVariant && !isEmbeddedVariant;
|
|
310
|
+
final BoxShadow resolvedShadow = BoxShadow(
|
|
311
|
+
color: const Color(0xFF000000).withValues(
|
|
312
|
+
alpha: context.isDark ? 0.28 : 0.11,
|
|
313
|
+
),
|
|
314
|
+
blurRadius: 3,
|
|
315
|
+
spreadRadius: -1,
|
|
285
316
|
);
|
|
286
317
|
final TargetPlatform platform = Theme.of(context).platform;
|
|
287
318
|
final bool isApplePlatform =
|
|
@@ -317,15 +348,21 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
317
348
|
width: restingBorderWidth,
|
|
318
349
|
),
|
|
319
350
|
);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
351
|
+
// When focusBorder is disabled, the focused state collapses to the
|
|
352
|
+
// resting border so the field never grows that bright outline. Error
|
|
353
|
+
// states still take priority via resolvedEnabledBorder picking the error
|
|
354
|
+
// color, so invalid fields keep the red highlight.
|
|
355
|
+
final InputBorder resolvedFocusedBorder = !widget.focusBorder
|
|
356
|
+
? resolvedEnabledBorder
|
|
357
|
+
: isEmbeddedVariant
|
|
358
|
+
? embeddedBorder
|
|
359
|
+
: OutlineInputBorder(
|
|
360
|
+
borderRadius: fieldRadius,
|
|
361
|
+
borderSide: BorderSide(
|
|
362
|
+
color: resolvedFocusedBorderColor,
|
|
363
|
+
width: focusedBorderWidth,
|
|
364
|
+
),
|
|
365
|
+
);
|
|
329
366
|
final Color surfaceColor = isEmbeddedVariant
|
|
330
367
|
? Colors.transparent
|
|
331
368
|
: isSecondaryVariant
|
|
@@ -334,13 +371,20 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
334
371
|
final Color fieldFillColor = isDisabled
|
|
335
372
|
? surfaceColor.withValues(alpha: context.isDark ? 0.9 : 0.94)
|
|
336
373
|
: surfaceColor;
|
|
374
|
+
// Affix icon tint matches the kit's helper-icon tone (0.62) when the
|
|
375
|
+
// field is enabled, and fades down with the same disabled curve used by
|
|
376
|
+
// the rest of the field so prefix/suffix don't stay "alive" while
|
|
377
|
+
// everything else dims.
|
|
378
|
+
final double affixAlpha = isDisabled
|
|
379
|
+
? 0.62 * disabledTextOpacity
|
|
380
|
+
: 0.62;
|
|
337
381
|
final Widget? resolvedPrefix = widget.prefix == null
|
|
338
382
|
? null
|
|
339
383
|
: Center(
|
|
340
384
|
child: IconTheme.merge(
|
|
341
385
|
data: IconThemeData(
|
|
342
386
|
size: KasyTextField.iconGlyphSize,
|
|
343
|
-
color: context.colors.onSurface.withValues(alpha:
|
|
387
|
+
color: context.colors.onSurface.withValues(alpha: affixAlpha),
|
|
344
388
|
),
|
|
345
389
|
child: widget.prefix!,
|
|
346
390
|
),
|
|
@@ -351,7 +395,7 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
351
395
|
child: IconTheme.merge(
|
|
352
396
|
data: IconThemeData(
|
|
353
397
|
size: KasyTextField.iconGlyphSize,
|
|
354
|
-
color: context.colors.onSurface.withValues(alpha:
|
|
398
|
+
color: context.colors.onSurface.withValues(alpha: affixAlpha),
|
|
355
399
|
),
|
|
356
400
|
child: widget.suffix!,
|
|
357
401
|
),
|
|
@@ -410,12 +454,11 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
410
454
|
width: KasyTextField.iconSlotExtent,
|
|
411
455
|
height: KasyTextField.iconSlotExtent,
|
|
412
456
|
),
|
|
413
|
-
contentPadding:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
),
|
|
457
|
+
contentPadding: widget.contentPadding ??
|
|
458
|
+
EdgeInsets.symmetric(
|
|
459
|
+
horizontal: isEmbeddedVariant ? 0 : KasySpacing.md,
|
|
460
|
+
vertical: 13,
|
|
461
|
+
),
|
|
419
462
|
fillColor: fieldFillColor,
|
|
420
463
|
filled: !isEmbeddedVariant,
|
|
421
464
|
// On web: keep fill unchanged on hover (hoverColor replaces fillColor — don't let
|
|
@@ -434,16 +477,29 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
434
477
|
borderRadius: fieldRadius,
|
|
435
478
|
borderSide: BorderSide(color: context.colors.error, width: 1.3),
|
|
436
479
|
),
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
480
|
+
// Mirrors the focusBorder opt-out: when disabled, the focused-error
|
|
481
|
+
// state reuses the resting error border (no thicker outline on focus).
|
|
482
|
+
focusedErrorBorder: !widget.focusBorder
|
|
483
|
+
? OutlineInputBorder(
|
|
484
|
+
borderRadius: fieldRadius,
|
|
485
|
+
borderSide: BorderSide(
|
|
486
|
+
color: context.colors.error,
|
|
487
|
+
width: 1.3,
|
|
488
|
+
),
|
|
489
|
+
)
|
|
490
|
+
: OutlineInputBorder(
|
|
491
|
+
borderRadius: fieldRadius,
|
|
492
|
+
borderSide: BorderSide(
|
|
493
|
+
color: context.colors.error,
|
|
494
|
+
width: focusedBorderWidth,
|
|
495
|
+
),
|
|
496
|
+
),
|
|
444
497
|
hintStyle: hintBaseStyle.copyWith(
|
|
498
|
+
// Match the disabled description fade so the placeholder dims along
|
|
499
|
+
// with the rest of the field (the previous 0.42 read as "kind of
|
|
500
|
+
// greyed out" rather than disabled).
|
|
445
501
|
color: (hintBaseStyle.color ?? context.colors.muted).withValues(
|
|
446
|
-
alpha: isDisabled ?
|
|
502
|
+
alpha: isDisabled ? disabledDescriptionOpacity : 1,
|
|
447
503
|
),
|
|
448
504
|
),
|
|
449
505
|
);
|
|
@@ -471,11 +527,15 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
471
527
|
buildCounter: hasCounter ? _hideInputCounter : null,
|
|
472
528
|
style: fieldTextStyle,
|
|
473
529
|
decoration: decoration,
|
|
530
|
+
enableInteractiveSelection: widget.enableInteractiveSelection,
|
|
474
531
|
);
|
|
532
|
+
// TESTE: sombra desabilitada para investigar diferença visual com DatePicker
|
|
533
|
+
final List<BoxShadow>? effectiveBoxShadow = widget.boxShadow ??
|
|
534
|
+
(shouldShowShadow ? <BoxShadow>[resolvedShadow] : null);
|
|
475
535
|
final Widget decoratedField = DecoratedBox(
|
|
476
536
|
decoration: BoxDecoration(
|
|
477
537
|
borderRadius: fieldRadius,
|
|
478
|
-
boxShadow:
|
|
538
|
+
boxShadow: effectiveBoxShadow,
|
|
479
539
|
),
|
|
480
540
|
child: Semantics(
|
|
481
541
|
textField: true,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
2
1
|
import 'package:flutter/material.dart';
|
|
3
2
|
import 'package:flutter/services.dart';
|
|
4
3
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
@@ -422,7 +421,7 @@ class _OTPCell extends StatelessWidget {
|
|
|
422
421
|
? _neutralFlatOtpFill(context)
|
|
423
422
|
: (cellFillColor ?? context.colors.surface);
|
|
424
423
|
final bool suppressShadow =
|
|
425
|
-
enabled && (neutralFlatCells || suppressCellShadow
|
|
424
|
+
enabled && (neutralFlatCells || suppressCellShadow);
|
|
426
425
|
final List<BoxShadow> boxShadow = suppressShadow
|
|
427
426
|
? <BoxShadow>[]
|
|
428
427
|
: <BoxShadow>[adjustedShadow];
|