kasy-cli 1.19.3 → 1.20.1
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/README.md +11 -3
- package/bin/kasy.js +1 -0
- package/lib/commands/new.js +87 -37
- package/lib/commands/run.js +14 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/meta_ads_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +4 -5
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_vote_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/providers/llm_chat_notifier.dart +317 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +40 -1
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/notifications_entity.dart +2 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -2
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -0
- package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +11 -8
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +91 -2
- package/lib/scaffold/backends/supabase/deploy.js +56 -3
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/storage_api.dart +5 -11
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +2 -2
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +31 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -0
- package/lib/scaffold/catalog.js +2 -2
- package/lib/scaffold/engine.js +5 -0
- package/lib/scaffold/generate.js +23 -3
- package/lib/scaffold/shared/generator-utils.js +303 -56
- package/lib/scaffold/shared/post-build.js +11 -0
- package/lib/utils/i18n/messages-en.js +6 -1
- package/lib/utils/i18n/messages-es.js +6 -1
- package/lib/utils/i18n/messages-pt.js +6 -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 +1150 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +156 -43
- package/templates/firebase/lib/components/kasy_text_field.dart +37 -34
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +13 -82
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +6 -102
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +8 -1
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +433 -243
- 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/states/user_state_notifier.dart +8 -10
- 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 +11 -14
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +121 -66
- package/templates/firebase/lib/features/home/home_page.dart +7 -8
- 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/lib/router.dart +60 -0
- package/templates/firebase/pubspec.yaml +6 -4
- package/templates/firebase/test/core/bottom_menu/detail_route_menu_test.dart +57 -0
- package/templates/firebase/web/index.html +7 -17
- package/lib/scaffold/backends/api/patch/lib/core/rating/widgets/review_popup.dart +0 -211
- package/lib/scaffold/backends/api/patch/lib/features/notifications/providers/models/notification.dart +0 -185
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
- package/lib/scaffold/backends/api/patch/lib/main.dart +0 -275
- package/lib/scaffold/backends/api/patch/lib/router.dart +0 -133
- package/lib/scaffold/backends/supabase/patch/lib/core/rating/widgets/review_popup.dart +0 -211
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/providers/models/notification.dart +0 -174
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
- package/lib/scaffold/backends/supabase/patch/lib/main.dart +0 -307
- package/lib/scaffold/backends/supabase/patch/lib/router.dart +0 -133
- package/templates/firebase/lib/firebase_options.dart +0 -75
|
@@ -7,15 +7,24 @@ import 'package:kasy_kit/core/theme/theme.dart';
|
|
|
7
7
|
|
|
8
8
|
/// Data model for a single tab item.
|
|
9
9
|
class KasyTabItem {
|
|
10
|
+
/// Visible text. Leave empty for an icon-only tab (requires [icon]).
|
|
10
11
|
final String label;
|
|
11
12
|
final IconData? icon;
|
|
12
13
|
final bool enabled;
|
|
13
14
|
|
|
15
|
+
/// Accessibility label read by screen readers. Falls back to [label] when
|
|
16
|
+
/// not provided; required (in spirit) for icon-only tabs, which have no text.
|
|
17
|
+
final String? semanticLabel;
|
|
18
|
+
|
|
14
19
|
const KasyTabItem({
|
|
15
|
-
|
|
20
|
+
this.label = '',
|
|
16
21
|
this.icon,
|
|
17
22
|
this.enabled = true,
|
|
18
|
-
|
|
23
|
+
this.semanticLabel,
|
|
24
|
+
}) : assert(
|
|
25
|
+
label != '' || icon != null,
|
|
26
|
+
'KasyTabItem needs a label, an icon, or both.',
|
|
27
|
+
);
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -190,27 +199,41 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
190
199
|
}
|
|
191
200
|
}
|
|
192
201
|
|
|
193
|
-
/// Scrolls the [SingleChildScrollView] so the selected tab is
|
|
202
|
+
/// Scrolls the inner [SingleChildScrollView] so the selected tab is visible.
|
|
194
203
|
///
|
|
195
|
-
///
|
|
196
|
-
///
|
|
204
|
+
/// Only the component's OWN horizontal controller is moved — never an
|
|
205
|
+
/// ancestor scrollable. (Using [Scrollable.ensureVisible] here would bubble
|
|
206
|
+
/// up and scroll the enclosing page vertically, e.g. nudging the Settings
|
|
207
|
+
/// list down when picking a middle tab.) First/last tab snap to the scroll
|
|
208
|
+
/// extremes so the container padding and the 4px pill overflow aren't clipped.
|
|
197
209
|
void _ensureSelectedVisible(int index) {
|
|
210
|
+
if (!_scrollController.hasClients) return;
|
|
211
|
+
|
|
212
|
+
final ScrollPosition position = _scrollController.position;
|
|
213
|
+
final double min = position.minScrollExtent;
|
|
214
|
+
final double max = position.maxScrollExtent;
|
|
215
|
+
// No horizontal overflow: leave every scroll position untouched.
|
|
216
|
+
if (max <= min) return;
|
|
217
|
+
|
|
198
218
|
const Duration duration = Duration(milliseconds: 250);
|
|
199
219
|
const Curve curve = Curves.easeInOut;
|
|
200
220
|
final int lastIndex = widget.items.length - 1;
|
|
201
221
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
222
|
+
final double target;
|
|
223
|
+
if (index == 0) {
|
|
224
|
+
target = min;
|
|
225
|
+
} else if (index == lastIndex) {
|
|
226
|
+
target = max;
|
|
227
|
+
} else {
|
|
228
|
+
// Centre the selected tab in the viewport. Measured geometry is relative
|
|
229
|
+
// to the inner Stack, which sits 8px in from the scrollable content edge
|
|
230
|
+
// (the tabsContent horizontal padding).
|
|
231
|
+
const double horizontalPadding = 8;
|
|
232
|
+
final double tabCentre =
|
|
233
|
+
horizontalPadding + _indicatorLeft + _indicatorWidth / 2;
|
|
234
|
+
target = (tabCentre - position.viewportDimension / 2).clamp(min, max);
|
|
209
235
|
}
|
|
210
|
-
|
|
211
|
-
final BuildContext? ctx = _keys[index].currentContext;
|
|
212
|
-
if (ctx == null) return;
|
|
213
|
-
Scrollable.ensureVisible(ctx, duration: duration, curve: curve);
|
|
236
|
+
_scrollController.animateTo(target, duration: duration, curve: curve);
|
|
214
237
|
}
|
|
215
238
|
|
|
216
239
|
@override
|
|
@@ -397,7 +420,31 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
397
420
|
// Internal tab widgets
|
|
398
421
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
399
422
|
|
|
400
|
-
|
|
423
|
+
/// Adds a pointer (click) cursor and reports hover changes on web/desktop.
|
|
424
|
+
/// Disabled tabs keep the default cursor and never report hover.
|
|
425
|
+
class _TabHoverRegion extends StatelessWidget {
|
|
426
|
+
const _TabHoverRegion({
|
|
427
|
+
required this.enabled,
|
|
428
|
+
required this.onHover,
|
|
429
|
+
required this.child,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
final bool enabled;
|
|
433
|
+
final ValueChanged<bool> onHover;
|
|
434
|
+
final Widget child;
|
|
435
|
+
|
|
436
|
+
@override
|
|
437
|
+
Widget build(BuildContext context) {
|
|
438
|
+
return MouseRegion(
|
|
439
|
+
cursor: enabled ? SystemMouseCursors.click : MouseCursor.defer,
|
|
440
|
+
onEnter: enabled ? (_) => onHover(true) : null,
|
|
441
|
+
onExit: enabled ? (_) => onHover(false) : null,
|
|
442
|
+
child: child,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
class _PrimaryTab extends StatefulWidget {
|
|
401
448
|
const _PrimaryTab({
|
|
402
449
|
super.key,
|
|
403
450
|
required this.item,
|
|
@@ -411,12 +458,27 @@ class _PrimaryTab extends StatelessWidget {
|
|
|
411
458
|
final bool expand;
|
|
412
459
|
final VoidCallback? onTap;
|
|
413
460
|
|
|
461
|
+
@override
|
|
462
|
+
State<_PrimaryTab> createState() => _PrimaryTabState();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
class _PrimaryTabState extends State<_PrimaryTab> {
|
|
466
|
+
bool _hovered = false;
|
|
467
|
+
|
|
414
468
|
Widget _tabContent(BuildContext context) {
|
|
415
469
|
final KasyColors c = context.colors;
|
|
470
|
+
final KasyTabItem item = widget.item;
|
|
471
|
+
final bool selected = widget.selected;
|
|
416
472
|
final bool disabled = !item.enabled;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
473
|
+
final bool hasLabel = item.label.isNotEmpty;
|
|
474
|
+
final bool iconOnly = item.icon != null && !hasLabel;
|
|
475
|
+
// Fill mode with a label + icon uses a vertical (Column) layout per Figma
|
|
476
|
+
// spec: icon stacked above label, 12px all-sides padding, 12px font size.
|
|
477
|
+
final bool verticalLayout = widget.expand && item.icon != null && hasLabel;
|
|
478
|
+
|
|
479
|
+
// On web/desktop, hovering an inactive tab lifts its foreground toward the
|
|
480
|
+
// selected color so it reads as interactive (mobile keeps the flat look).
|
|
481
|
+
final Color fg = (selected || _hovered) ? c.onSurface : c.muted;
|
|
420
482
|
|
|
421
483
|
final Widget iconWidget = item.icon != null
|
|
422
484
|
? Opacity(
|
|
@@ -424,7 +486,7 @@ class _PrimaryTab extends StatelessWidget {
|
|
|
424
486
|
child: Icon(
|
|
425
487
|
item.icon,
|
|
426
488
|
size: 16,
|
|
427
|
-
color:
|
|
489
|
+
color: fg,
|
|
428
490
|
),
|
|
429
491
|
)
|
|
430
492
|
: const SizedBox.shrink();
|
|
@@ -437,15 +499,15 @@ class _PrimaryTab extends StatelessWidget {
|
|
|
437
499
|
// Use labelLarge as defined in the Kasy theme (14px/w600).
|
|
438
500
|
// No fontWeight override — the theme token is the source of truth.
|
|
439
501
|
style: context.textTheme.labelLarge?.copyWith(
|
|
440
|
-
color:
|
|
502
|
+
color: fg,
|
|
441
503
|
// Fill+icon layout uses 12px (text-xs) — per Figma spec.
|
|
442
504
|
fontSize: verticalLayout ? 12 : null,
|
|
443
505
|
),
|
|
444
506
|
),
|
|
445
507
|
);
|
|
446
508
|
|
|
447
|
-
|
|
448
|
-
onTap: onTap,
|
|
509
|
+
final Widget gesture = GestureDetector(
|
|
510
|
+
onTap: widget.onTap,
|
|
449
511
|
behavior: HitTestBehavior.opaque,
|
|
450
512
|
child: Padding(
|
|
451
513
|
padding: verticalLayout
|
|
@@ -467,25 +529,42 @@ class _PrimaryTab extends StatelessWidget {
|
|
|
467
529
|
mainAxisSize: MainAxisSize.min,
|
|
468
530
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
469
531
|
children: [
|
|
470
|
-
if (item.icon != null)
|
|
471
|
-
|
|
532
|
+
if (item.icon != null) iconWidget,
|
|
533
|
+
if (item.icon != null && hasLabel)
|
|
472
534
|
const SizedBox(width: 6),
|
|
473
|
-
|
|
474
|
-
labelWidget,
|
|
535
|
+
if (hasLabel) labelWidget,
|
|
475
536
|
],
|
|
476
537
|
),
|
|
477
538
|
),
|
|
478
539
|
);
|
|
540
|
+
|
|
541
|
+
final Widget hoverable = _TabHoverRegion(
|
|
542
|
+
enabled: !disabled,
|
|
543
|
+
onHover: (value) => setState(() => _hovered = value),
|
|
544
|
+
child: gesture,
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
// Icon-only tabs carry no text, so expose the selection state and a label
|
|
548
|
+
// to screen readers explicitly (labelled tabs are described by their Text).
|
|
549
|
+
if (iconOnly) {
|
|
550
|
+
return Semantics(
|
|
551
|
+
button: true,
|
|
552
|
+
selected: selected,
|
|
553
|
+
label: item.semanticLabel,
|
|
554
|
+
child: hoverable,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
return hoverable;
|
|
479
558
|
}
|
|
480
559
|
|
|
481
560
|
@override
|
|
482
561
|
Widget build(BuildContext context) {
|
|
483
562
|
final Widget inner = _tabContent(context);
|
|
484
|
-
return expand ? Expanded(child: inner) : inner;
|
|
563
|
+
return widget.expand ? Expanded(child: inner) : inner;
|
|
485
564
|
}
|
|
486
565
|
}
|
|
487
566
|
|
|
488
|
-
class _SecondaryTab extends
|
|
567
|
+
class _SecondaryTab extends StatefulWidget {
|
|
489
568
|
const _SecondaryTab({
|
|
490
569
|
super.key,
|
|
491
570
|
required this.item,
|
|
@@ -499,12 +578,27 @@ class _SecondaryTab extends StatelessWidget {
|
|
|
499
578
|
final bool expand;
|
|
500
579
|
final VoidCallback? onTap;
|
|
501
580
|
|
|
581
|
+
@override
|
|
582
|
+
State<_SecondaryTab> createState() => _SecondaryTabState();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
class _SecondaryTabState extends State<_SecondaryTab> {
|
|
586
|
+
bool _hovered = false;
|
|
587
|
+
|
|
502
588
|
Widget _tabContent(BuildContext context) {
|
|
503
589
|
final KasyColors c = context.colors;
|
|
590
|
+
final KasyTabItem item = widget.item;
|
|
591
|
+
final bool selected = widget.selected;
|
|
504
592
|
final bool disabled = !item.enabled;
|
|
593
|
+
final bool hasLabel = item.label.isNotEmpty;
|
|
594
|
+
final bool iconOnly = item.icon != null && !hasLabel;
|
|
595
|
+
|
|
596
|
+
// On web/desktop, hovering an inactive tab lifts its foreground toward the
|
|
597
|
+
// selected color so it reads as interactive (mobile keeps the flat look).
|
|
598
|
+
final Color fg = (selected || _hovered) ? c.onSurface : c.muted;
|
|
505
599
|
|
|
506
|
-
|
|
507
|
-
onTap: onTap,
|
|
600
|
+
final Widget gesture = GestureDetector(
|
|
601
|
+
onTap: widget.onTap,
|
|
508
602
|
behavior: HitTestBehavior.opaque,
|
|
509
603
|
child: Padding(
|
|
510
604
|
// top:4 bottom:6 horizontal:12 — per Figma spec.
|
|
@@ -525,30 +619,49 @@ class _SecondaryTab extends StatelessWidget {
|
|
|
525
619
|
child: Icon(
|
|
526
620
|
item.icon,
|
|
527
621
|
size: 16,
|
|
528
|
-
color:
|
|
622
|
+
color: fg,
|
|
529
623
|
),
|
|
530
624
|
),
|
|
531
|
-
const SizedBox(width: 6),
|
|
625
|
+
if (hasLabel) const SizedBox(width: 6),
|
|
532
626
|
],
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
627
|
+
if (hasLabel)
|
|
628
|
+
Opacity(
|
|
629
|
+
opacity: disabled ? 0.4 : 1.0,
|
|
630
|
+
child: Text(
|
|
631
|
+
item.label,
|
|
632
|
+
// Use labelLarge as defined in the Kasy theme (14px/w600).
|
|
633
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
634
|
+
color: fg,
|
|
635
|
+
),
|
|
540
636
|
),
|
|
541
637
|
),
|
|
542
|
-
),
|
|
543
638
|
],
|
|
544
639
|
),
|
|
545
640
|
),
|
|
546
641
|
);
|
|
642
|
+
|
|
643
|
+
final Widget hoverable = _TabHoverRegion(
|
|
644
|
+
enabled: !disabled,
|
|
645
|
+
onHover: (value) => setState(() => _hovered = value),
|
|
646
|
+
child: gesture,
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
// Icon-only tabs carry no text, so expose the selection state and a label
|
|
650
|
+
// to screen readers explicitly (labelled tabs are described by their Text).
|
|
651
|
+
if (iconOnly) {
|
|
652
|
+
return Semantics(
|
|
653
|
+
button: true,
|
|
654
|
+
selected: selected,
|
|
655
|
+
label: item.semanticLabel,
|
|
656
|
+
child: hoverable,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
return hoverable;
|
|
547
660
|
}
|
|
548
661
|
|
|
549
662
|
@override
|
|
550
663
|
Widget build(BuildContext context) {
|
|
551
664
|
final Widget inner = _tabContent(context);
|
|
552
|
-
return expand ? Expanded(child: inner) : inner;
|
|
665
|
+
return widget.expand ? Expanded(child: inner) : inner;
|
|
553
666
|
}
|
|
554
667
|
}
|
|
@@ -241,9 +241,20 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
241
241
|
// (No more web-specific padding — the field now uses the same vertical
|
|
242
242
|
// padding on every platform so primary/web TextFields render at the same
|
|
243
243
|
// height as mobile and as the KasyDatePicker trigger.)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
|
|
245
|
+
// ── Disabled state: opaque "softened" colors, NOT transparency. ────────
|
|
246
|
+
// Kit-wide rule (see KasyButton): keep the original hue but render it
|
|
247
|
+
// weaker by alpha-blending toward the surface — never use raw opacity,
|
|
248
|
+
// which would leak whatever sits behind the widget. [dimDisabled] takes
|
|
249
|
+
// any base color and returns an opaque, softer version of it (or the
|
|
250
|
+
// color itself when the field is enabled). The [alpha] parameter
|
|
251
|
+
// controls how much of the original color is kept: higher = stronger
|
|
252
|
+
// (closer to the original), lower = softer (closer to the surface).
|
|
253
|
+
final Color blendSurface = context.colors.surface;
|
|
254
|
+
Color dimDisabled(Color base, {double alpha = 0.46}) {
|
|
255
|
+
if (!isDisabled) return base;
|
|
256
|
+
return Color.alphaBlend(base.withValues(alpha: alpha), blendSurface);
|
|
257
|
+
}
|
|
247
258
|
final bool isPassword =
|
|
248
259
|
widget.contentType == KasyTextFieldContentType.password;
|
|
249
260
|
final bool isEmail = widget.contentType == KasyTextFieldContentType.email;
|
|
@@ -289,16 +300,18 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
289
300
|
final TextStyle labelStyle = labelBaseStyle.copyWith(
|
|
290
301
|
color: hasInvalidState
|
|
291
302
|
? context.colors.error
|
|
292
|
-
: (
|
|
293
|
-
|
|
303
|
+
: dimDisabled(
|
|
304
|
+
labelBaseStyle.color ?? context.colors.fieldLabel,
|
|
305
|
+
alpha: 0.55,
|
|
294
306
|
),
|
|
295
307
|
);
|
|
296
308
|
final TextStyle descriptionStyle =
|
|
297
309
|
(hasInvalidState ? errorBaseStyle : helperBaseStyle).copyWith(
|
|
298
310
|
color: hasInvalidState
|
|
299
311
|
? context.colors.error
|
|
300
|
-
: (
|
|
301
|
-
|
|
312
|
+
: dimDisabled(
|
|
313
|
+
helperBaseStyle.color ?? context.colors.muted,
|
|
314
|
+
alpha: 0.45,
|
|
302
315
|
),
|
|
303
316
|
);
|
|
304
317
|
final BorderRadius fieldRadius = BorderRadius.circular(KasyRadius.md);
|
|
@@ -372,19 +385,17 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
372
385
|
? surfaceColor.withValues(alpha: context.isDark ? 0.9 : 0.94)
|
|
373
386
|
: surfaceColor;
|
|
374
387
|
// Affix icon tint matches the kit's helper-icon tone (0.62) when the
|
|
375
|
-
// field is enabled
|
|
376
|
-
// the
|
|
377
|
-
|
|
378
|
-
final
|
|
379
|
-
? 0.62 * disabledTextOpacity
|
|
380
|
-
: 0.62;
|
|
388
|
+
// field is enabled. When disabled, dimDisabled blends that base color
|
|
389
|
+
// toward the surface — the icon stays opaque, just less saturated.
|
|
390
|
+
final Color affixBase = context.colors.onSurface.withValues(alpha: 0.62);
|
|
391
|
+
final Color affixColor = dimDisabled(affixBase);
|
|
381
392
|
final Widget? resolvedPrefix = widget.prefix == null
|
|
382
393
|
? null
|
|
383
394
|
: Center(
|
|
384
395
|
child: IconTheme.merge(
|
|
385
396
|
data: IconThemeData(
|
|
386
397
|
size: KasyTextField.iconGlyphSize,
|
|
387
|
-
color:
|
|
398
|
+
color: affixColor,
|
|
388
399
|
),
|
|
389
400
|
child: widget.prefix!,
|
|
390
401
|
),
|
|
@@ -395,7 +406,7 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
395
406
|
child: IconTheme.merge(
|
|
396
407
|
data: IconThemeData(
|
|
397
408
|
size: KasyTextField.iconGlyphSize,
|
|
398
|
-
color:
|
|
409
|
+
color: affixColor,
|
|
399
410
|
),
|
|
400
411
|
child: widget.suffix!,
|
|
401
412
|
),
|
|
@@ -419,24 +430,15 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
419
430
|
child: Icon(
|
|
420
431
|
_passwordVisible ? KasyIcons.eyeOff : KasyIcons.eye,
|
|
421
432
|
size: KasyTextField.iconGlyphSize,
|
|
422
|
-
color:
|
|
423
|
-
alpha: widget.enabled ? 0.62 : disabledTextOpacity,
|
|
424
|
-
),
|
|
433
|
+
color: affixColor,
|
|
425
434
|
),
|
|
426
435
|
),
|
|
427
436
|
),
|
|
428
437
|
),
|
|
429
438
|
)
|
|
430
439
|
: null);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
// Material TextField's own disabled handling) so the disabled state is
|
|
434
|
-
// visually identical across native, web and DatePicker contexts. Without
|
|
435
|
-
// this explicit color, Material would render the value at full opacity
|
|
436
|
-
// because [style] takes precedence over the framework's disabled fade.
|
|
437
|
-
final Color fieldTextColor = isDisabled
|
|
438
|
-
? context.colors.onSurface.withValues(alpha: disabledTextOpacity)
|
|
439
|
-
: context.colors.onSurface;
|
|
440
|
+
|
|
441
|
+
final Color fieldTextColor = dimDisabled(context.colors.onSurface);
|
|
440
442
|
final TextStyle fieldTextStyle =
|
|
441
443
|
context.textTheme.bodyLarge?.copyWith(
|
|
442
444
|
color: fieldTextColor,
|
|
@@ -502,11 +504,11 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
502
504
|
),
|
|
503
505
|
),
|
|
504
506
|
hintStyle: hintBaseStyle.copyWith(
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
alpha:
|
|
507
|
+
// Placeholder dims along with the field via the kit-wide softened
|
|
508
|
+
// color rule (opaque blend toward the surface, not raw opacity).
|
|
509
|
+
color: dimDisabled(
|
|
510
|
+
hintBaseStyle.color ?? context.colors.muted,
|
|
511
|
+
alpha: 0.55,
|
|
510
512
|
),
|
|
511
513
|
),
|
|
512
514
|
);
|
|
@@ -618,8 +620,9 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
618
620
|
Text(
|
|
619
621
|
'${_effectiveController.text.length}/${widget.maxLength}',
|
|
620
622
|
style: context.textTheme.bodySmall?.copyWith(
|
|
621
|
-
color:
|
|
622
|
-
alpha:
|
|
623
|
+
color: dimDisabled(
|
|
624
|
+
context.colors.onSurface.withValues(alpha: 0.54),
|
|
625
|
+
alpha: 0.55,
|
|
623
626
|
),
|
|
624
627
|
fontWeight: FontWeight.w500,
|
|
625
628
|
),
|
|
@@ -1,96 +1,30 @@
|
|
|
1
1
|
import 'package:bart/bart.dart' as bart;
|
|
2
|
-
import 'package:bart/bart/bart_bottombar_actions.dart';
|
|
3
2
|
import 'package:flutter/foundation.dart';
|
|
4
3
|
import 'package:flutter/material.dart';
|
|
5
4
|
import 'package:flutter/services.dart';
|
|
5
|
+
import 'package:kasy_kit/components/kasy_sidebar_pro.dart';
|
|
6
6
|
import 'package:kasy_kit/core/bottom_menu/bottom_router.dart';
|
|
7
7
|
import 'package:kasy_kit/core/bottom_menu/kasy_bottom_bar_factory.dart';
|
|
8
|
-
import 'package:kasy_kit/core/sidebar/kasy_sidebar.dart';
|
|
9
8
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
10
9
|
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
11
10
|
|
|
12
11
|
/// Bottom navigation host powered by Bart (https://pub.dev/packages/bart).
|
|
13
12
|
///
|
|
14
|
-
///
|
|
15
|
-
///
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
|
|
19
|
-
/// from a native overlay (FaceID, photo picker, permission dialog), Bart's
|
|
20
|
-
/// internal state ends up out of sync with the visible route.
|
|
21
|
-
///
|
|
22
|
-
/// This widget owns visibility end-to-end so feature pages never need to
|
|
23
|
-
/// touch it. The rules are simple:
|
|
24
|
-
/// * Bottom-bar tab (1 path segment) → bar visible.
|
|
25
|
-
/// * Inner route (multi-segment, or [bart.BartMenuRoute.showBottomBar] =
|
|
26
|
-
/// false) → bar hidden.
|
|
27
|
-
///
|
|
28
|
-
/// Sync points: [onRouteChanged], post-frame after every [build] (catches
|
|
29
|
-
/// scaffold rebuilds), and [didChangeAppLifecycleState] on resume.
|
|
30
|
-
class BottomMenu extends StatefulWidget {
|
|
13
|
+
/// [ResponsiveLayout] swaps between three [bart.BartScaffold]s (small / medium /
|
|
14
|
+
/// large) based on width. The bottom bar shows on every tab; detail screens
|
|
15
|
+
/// (Features, Components, …) are pushed on the root navigator from the pages
|
|
16
|
+
/// themselves, so they cover this menu and there's nothing to toggle here.
|
|
17
|
+
class BottomMenu extends StatelessWidget {
|
|
31
18
|
final String? initialRoute;
|
|
32
19
|
|
|
33
20
|
const BottomMenu({super.key, this.initialRoute});
|
|
34
21
|
|
|
35
|
-
@override
|
|
36
|
-
State<BottomMenu> createState() => _BottomMenuState();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
class _BottomMenuState extends State<BottomMenu>
|
|
40
|
-
with WidgetsBindingObserver, BartNotifier {
|
|
41
|
-
String? _currentRoutePath;
|
|
42
|
-
|
|
43
|
-
@override
|
|
44
|
-
void initState() {
|
|
45
|
-
super.initState();
|
|
46
|
-
WidgetsBinding.instance.addObserver(this);
|
|
47
|
-
_currentRoutePath = _resolveInitialRoute(widget.initialRoute);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
@override
|
|
51
|
-
void dispose() {
|
|
52
|
-
WidgetsBinding.instance.removeObserver(this);
|
|
53
|
-
super.dispose();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@override
|
|
57
|
-
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
58
|
-
super.didChangeAppLifecycleState(state);
|
|
59
|
-
if (state == AppLifecycleState.resumed) {
|
|
60
|
-
_scheduleSync();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
void _onRouteChanged(bart.BartMenuRoute route) {
|
|
65
|
-
_currentRoutePath = route.path;
|
|
66
|
-
_scheduleSync();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
void _scheduleSync() {
|
|
70
|
-
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
71
|
-
if (!mounted) return;
|
|
72
|
-
_applyVisibility();
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
void _applyVisibility() {
|
|
77
|
-
if (_shouldShowBottomBar(_currentRoutePath)) {
|
|
78
|
-
showBottomBar(context);
|
|
79
|
-
} else {
|
|
80
|
-
hideBottomBar(context);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
22
|
@override
|
|
85
23
|
Widget build(BuildContext context) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
final String? resolvedInitialRoute = _resolveInitialRoute(
|
|
91
|
-
widget.initialRoute,
|
|
24
|
+
final String? resolvedInitialRoute = _resolveInitialRoute(initialRoute);
|
|
25
|
+
final bool showBottomBarOnStart = _shouldShowBottomBarOnStart(
|
|
26
|
+
resolvedInitialRoute,
|
|
92
27
|
);
|
|
93
|
-
final bool showBottomBarOnStart = _shouldShowBottomBar(resolvedInitialRoute);
|
|
94
28
|
final scaffoldOptions = bart.ScaffoldOptions(
|
|
95
29
|
backgroundColor: context.colors.background,
|
|
96
30
|
);
|
|
@@ -107,9 +41,8 @@ class _BottomMenuState extends State<BottomMenu>
|
|
|
107
41
|
initialRoute: resolvedInitialRoute,
|
|
108
42
|
showBottomBarOnStart: showBottomBarOnStart,
|
|
109
43
|
scaffoldOptions: scaffoldOptions,
|
|
110
|
-
onRouteChanged: _onRouteChanged,
|
|
111
44
|
),
|
|
112
|
-
// medium (768–1024 px):
|
|
45
|
+
// medium (768–1024 px): KasySidebarPro auto-collapses at this width
|
|
113
46
|
medium: bart.BartScaffold(
|
|
114
47
|
routesBuilder: subRoutes,
|
|
115
48
|
bottomBar: kasyPaddedSurfaceBottomBar(),
|
|
@@ -117,9 +50,8 @@ class _BottomMenuState extends State<BottomMenu>
|
|
|
117
50
|
showBottomBarOnStart: showBottomBarOnStart,
|
|
118
51
|
scaffoldOptions: scaffoldOptions,
|
|
119
52
|
sideBarOptions: bart.CustomSideBarOptions(
|
|
120
|
-
sideBarBuilder:
|
|
53
|
+
sideBarBuilder: (routes, onTap, current) => const KasySidebarPro(),
|
|
121
54
|
),
|
|
122
|
-
onRouteChanged: _onRouteChanged,
|
|
123
55
|
),
|
|
124
56
|
// large (1024 px+): full expanded sidebar
|
|
125
57
|
large: bart.BartScaffold(
|
|
@@ -129,9 +61,8 @@ class _BottomMenuState extends State<BottomMenu>
|
|
|
129
61
|
showBottomBarOnStart: showBottomBarOnStart,
|
|
130
62
|
scaffoldOptions: scaffoldOptions,
|
|
131
63
|
sideBarOptions: bart.CustomSideBarOptions(
|
|
132
|
-
sideBarBuilder:
|
|
64
|
+
sideBarBuilder: (routes, onTap, current) => const KasySidebarPro(),
|
|
133
65
|
),
|
|
134
|
-
onRouteChanged: _onRouteChanged,
|
|
135
66
|
),
|
|
136
67
|
),
|
|
137
68
|
);
|
|
@@ -170,7 +101,7 @@ class _BottomMenuState extends State<BottomMenu>
|
|
|
170
101
|
return path;
|
|
171
102
|
}
|
|
172
103
|
|
|
173
|
-
bool
|
|
104
|
+
bool _shouldShowBottomBarOnStart(String? route) {
|
|
174
105
|
if (route == null) {
|
|
175
106
|
return true;
|
|
176
107
|
}
|