kasy-cli 1.38.0 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/api/patch/README.md +15 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
- package/lib/scaffold/backends/patch-base-hashes.json +6 -6
- package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
- package/lib/scaffold/shared/generator-utils.js +12 -6
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
- package/templates/firebase/DESIGN_SYSTEM.md +22 -8
- package/templates/firebase/assets/icons/apple_black.svg +3 -0
- package/templates/firebase/assets/icons/apple_white.svg +4 -0
- package/templates/firebase/assets/icons/facebook.svg +49 -0
- package/templates/firebase/assets/icons/google.svg +1 -0
- package/templates/firebase/functions/src/admin/functions.ts +2 -0
- package/templates/firebase/functions/src/authentication/functions.ts +13 -7
- package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
- package/templates/firebase/lib/components/components.dart +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +314 -14
- package/templates/firebase/lib/components/kasy_card.dart +4 -0
- package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +18 -6
- package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
- package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +27 -18
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -10
- package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
- package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
- package/templates/firebase/lib/core/data/api/user_api.dart +11 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +18 -35
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
- package/templates/firebase/lib/core/states/logout_action.dart +11 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +28 -1
- package/templates/firebase/lib/core/theme/texts.dart +21 -6
- package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +51 -19
- package/templates/firebase/lib/core/web_viewport_scale.dart +64 -35
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
- package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
- package/templates/firebase/lib/features/home/home_components_page.dart +253 -125
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +231 -57
- package/templates/firebase/lib/features/home/home_feed.dart +2 -2
- package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
- package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +111 -57
- package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -4
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
- package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +2 -2
- package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
- package/templates/firebase/lib/features/settings/settings_page.dart +53 -32
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +895 -111
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +171 -41
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +48 -47
- package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
- package/templates/firebase/lib/i18n/en.i18n.json +749 -712
- package/templates/firebase/lib/i18n/es.i18n.json +749 -712
- package/templates/firebase/lib/i18n/pt.i18n.json +749 -712
- package/templates/firebase/lib/main.dart +20 -7
- package/templates/firebase/lib/router.dart +32 -26
- package/templates/firebase/pubspec.yaml +2 -1
- package/templates/firebase/test/admin_shell_chrome_test.dart +11 -5
- package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
- package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
- package/templates/firebase/tool/design_check.dart +9 -0
- package/templates/firebase/assets/icons/apple.png +0 -0
- package/templates/firebase/assets/icons/facebook.png +0 -0
- package/templates/firebase/assets/icons/google.png +0 -0
- package/templates/firebase/assets/icons/google_play_games.png +0 -0
- package/templates/firebase/lib/components/kasy_web_header.dart +0 -218
- package/templates/firebase/lib/core/chrome/web_header_scope.dart +0 -20
- package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
- package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
- package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -179
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
|
@@ -71,39 +71,35 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
71
71
|
return const ComponentPreviewDefinition(
|
|
72
72
|
title: 'AppBar',
|
|
73
73
|
variants: [
|
|
74
|
+
// Phone / tablet — the page chrome (title + back + orbs).
|
|
74
75
|
ComponentPreviewVariant(
|
|
75
|
-
label: 'Subpage',
|
|
76
|
+
label: 'Subpage (mobile)',
|
|
76
77
|
builder: _buildAppBarSubpageVariant,
|
|
77
78
|
),
|
|
78
79
|
ComponentPreviewVariant(
|
|
79
|
-
label: 'Subpage Simple',
|
|
80
|
+
label: 'Subpage Simple (mobile)',
|
|
80
81
|
builder: _buildAppBarSubpageSimpleVariant,
|
|
81
82
|
),
|
|
82
83
|
ComponentPreviewVariant(
|
|
83
|
-
label: 'Subpage Actions',
|
|
84
|
+
label: 'Subpage Actions (mobile)',
|
|
84
85
|
builder: _buildAppBarSubpageActionsVariant,
|
|
85
86
|
),
|
|
86
87
|
ComponentPreviewVariant(
|
|
87
|
-
label: 'Menu opens sidebar',
|
|
88
|
+
label: 'Menu opens sidebar (mobile)',
|
|
88
89
|
builder: _buildAppBarMenuVariant,
|
|
89
90
|
),
|
|
90
|
-
|
|
91
|
-
);
|
|
92
|
-
case 'Web Header':
|
|
93
|
-
return const ComponentPreviewDefinition(
|
|
94
|
-
title: 'Web Header',
|
|
95
|
-
variants: [
|
|
91
|
+
// Desktop — the application chrome (search + actions + profile).
|
|
96
92
|
ComponentPreviewVariant(
|
|
97
|
-
label: '
|
|
98
|
-
builder:
|
|
93
|
+
label: 'Application (desktop)',
|
|
94
|
+
builder: _buildAppBarApplicationVariant,
|
|
99
95
|
),
|
|
100
96
|
ComponentPreviewVariant(
|
|
101
|
-
label: '
|
|
102
|
-
builder:
|
|
97
|
+
label: 'Application + notification (desktop)',
|
|
98
|
+
builder: _buildAppBarApplicationBadgeVariant,
|
|
103
99
|
),
|
|
104
100
|
ComponentPreviewVariant(
|
|
105
|
-
label: '
|
|
106
|
-
builder:
|
|
101
|
+
label: 'Application no avatar (desktop)',
|
|
102
|
+
builder: _buildAppBarApplicationNoAvatarVariant,
|
|
107
103
|
),
|
|
108
104
|
],
|
|
109
105
|
);
|
|
@@ -309,6 +305,24 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
309
305
|
),
|
|
310
306
|
],
|
|
311
307
|
);
|
|
308
|
+
case 'DropDown':
|
|
309
|
+
return const ComponentPreviewDefinition(
|
|
310
|
+
title: 'DropDown',
|
|
311
|
+
variants: [
|
|
312
|
+
ComponentPreviewVariant(
|
|
313
|
+
label: 'Basic',
|
|
314
|
+
builder: _buildDropDownBasic,
|
|
315
|
+
),
|
|
316
|
+
ComponentPreviewVariant(
|
|
317
|
+
label: 'With icons & subtitles',
|
|
318
|
+
builder: _buildDropDownRich,
|
|
319
|
+
),
|
|
320
|
+
ComponentPreviewVariant(
|
|
321
|
+
label: 'States',
|
|
322
|
+
builder: _buildDropDownStates,
|
|
323
|
+
),
|
|
324
|
+
],
|
|
325
|
+
);
|
|
312
326
|
case 'Badge':
|
|
313
327
|
return const ComponentPreviewDefinition(
|
|
314
328
|
title: 'Badge',
|
|
@@ -1928,16 +1942,16 @@ Widget _buildAppBarMenuVariant(BuildContext context) {
|
|
|
1928
1942
|
);
|
|
1929
1943
|
}
|
|
1930
1944
|
|
|
1931
|
-
Widget
|
|
1932
|
-
return const
|
|
1945
|
+
Widget _buildAppBarApplicationVariant(BuildContext context) {
|
|
1946
|
+
return const _ApplicationBarPreview();
|
|
1933
1947
|
}
|
|
1934
1948
|
|
|
1935
|
-
Widget
|
|
1936
|
-
return const
|
|
1949
|
+
Widget _buildAppBarApplicationBadgeVariant(BuildContext context) {
|
|
1950
|
+
return const _ApplicationBarPreview(showBadge: true);
|
|
1937
1951
|
}
|
|
1938
1952
|
|
|
1939
|
-
Widget
|
|
1940
|
-
return const
|
|
1953
|
+
Widget _buildAppBarApplicationNoAvatarVariant(BuildContext context) {
|
|
1954
|
+
return const _ApplicationBarPreview(showAvatar: false);
|
|
1941
1955
|
}
|
|
1942
1956
|
|
|
1943
1957
|
Widget _buildButtonSizesVariant(BuildContext context) {
|
|
@@ -3866,49 +3880,62 @@ class _AccordionPreviewState extends State<_AccordionPreview> {
|
|
|
3866
3880
|
}
|
|
3867
3881
|
}
|
|
3868
3882
|
|
|
3869
|
-
/// Presents [
|
|
3870
|
-
/// faux sidebar + content) so the preview reads as the
|
|
3871
|
-
|
|
3883
|
+
/// Presents [KasyAppBar.application] inside a desktop browser-window mock (title
|
|
3884
|
+
/// bar + faux sidebar + content) so the preview reads as the desktop chrome it
|
|
3885
|
+
/// is — the responsive desktop half of [KasyAppBar].
|
|
3886
|
+
class _ApplicationBarPreview extends StatelessWidget {
|
|
3872
3887
|
final bool showBadge;
|
|
3873
3888
|
final bool showAvatar;
|
|
3874
3889
|
|
|
3875
|
-
const
|
|
3890
|
+
const _ApplicationBarPreview({this.showBadge = false, this.showAvatar = true});
|
|
3891
|
+
|
|
3892
|
+
/// Width the mock window is laid out at. The application bar is desktop chrome
|
|
3893
|
+
/// (220px search + actions), so it needs a desktop-class width — we render at
|
|
3894
|
+
/// this width and scale the whole window down to fit the (narrow) preview card.
|
|
3895
|
+
/// Without this, the bar overflows on a phone-sized preview.
|
|
3896
|
+
static const double _mockWindowWidth = 760;
|
|
3876
3897
|
|
|
3877
3898
|
@override
|
|
3878
3899
|
Widget build(BuildContext context) {
|
|
3879
3900
|
final KasyColors c = context.colors;
|
|
3880
|
-
return
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3901
|
+
return FittedBox(
|
|
3902
|
+
fit: BoxFit.scaleDown,
|
|
3903
|
+
child: SizedBox(
|
|
3904
|
+
width: _mockWindowWidth,
|
|
3905
|
+
child: DecoratedBox(
|
|
3906
|
+
decoration: BoxDecoration(
|
|
3907
|
+
color: c.surface,
|
|
3908
|
+
borderRadius: BorderRadius.circular(KasyRadius.lg),
|
|
3909
|
+
border: Border.all(color: c.onSurface.withValues(alpha: 0.10)),
|
|
3910
|
+
boxShadow: [KasyShadows.component(context)],
|
|
3911
|
+
),
|
|
3912
|
+
child: ClipRRect(
|
|
3913
|
+
borderRadius: BorderRadius.circular(KasyRadius.lg),
|
|
3914
|
+
child: Column(
|
|
3915
|
+
mainAxisSize: MainAxisSize.min,
|
|
3916
|
+
children: [
|
|
3917
|
+
const _BrowserTopBar(),
|
|
3918
|
+
// The real bar, flush — no corner radius of its own.
|
|
3919
|
+
KasyAppBar.application(
|
|
3920
|
+
showNotificationBadge: showBadge,
|
|
3921
|
+
showAvatar: showAvatar,
|
|
3922
|
+
onNotifications: () {},
|
|
3923
|
+
onCreate: () {},
|
|
3924
|
+
onAvatarTap: () {},
|
|
3925
|
+
),
|
|
3926
|
+
const SizedBox(
|
|
3927
|
+
height: 150,
|
|
3928
|
+
child: Row(
|
|
3929
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
3930
|
+
children: [
|
|
3931
|
+
_DesktopMockSidebar(),
|
|
3932
|
+
Expanded(child: _DesktopMockContent()),
|
|
3933
|
+
],
|
|
3934
|
+
),
|
|
3935
|
+
),
|
|
3936
|
+
],
|
|
3910
3937
|
),
|
|
3911
|
-
|
|
3938
|
+
),
|
|
3912
3939
|
),
|
|
3913
3940
|
),
|
|
3914
3941
|
);
|
|
@@ -9447,3 +9474,150 @@ class _DatePickerFieldStatesPreviewState
|
|
|
9447
9474
|
);
|
|
9448
9475
|
}
|
|
9449
9476
|
}
|
|
9477
|
+
|
|
9478
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9479
|
+
// DropDown — single-select dropdown (HeroUI "Select")
|
|
9480
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9481
|
+
|
|
9482
|
+
const List<KasyDropDownItem<String>> _kDropDownStates = [
|
|
9483
|
+
KasyDropDownItem(value: 'fl', label: 'Florida'),
|
|
9484
|
+
KasyDropDownItem(value: 'de', label: 'Delaware'),
|
|
9485
|
+
KasyDropDownItem(value: 'tx', label: 'Texas'),
|
|
9486
|
+
KasyDropDownItem(value: 'ca', label: 'California'),
|
|
9487
|
+
KasyDropDownItem(value: 'ny', label: 'New York'),
|
|
9488
|
+
KasyDropDownItem(value: 'wy', label: 'Wyoming'),
|
|
9489
|
+
];
|
|
9490
|
+
|
|
9491
|
+
Widget _buildDropDownBasic(BuildContext context) => const _DropDownBasicPreview();
|
|
9492
|
+
|
|
9493
|
+
class _DropDownBasicPreview extends StatefulWidget {
|
|
9494
|
+
const _DropDownBasicPreview();
|
|
9495
|
+
|
|
9496
|
+
@override
|
|
9497
|
+
State<_DropDownBasicPreview> createState() => _DropDownBasicPreviewState();
|
|
9498
|
+
}
|
|
9499
|
+
|
|
9500
|
+
class _DropDownBasicPreviewState extends State<_DropDownBasicPreview> {
|
|
9501
|
+
String? _state;
|
|
9502
|
+
|
|
9503
|
+
@override
|
|
9504
|
+
Widget build(BuildContext context) {
|
|
9505
|
+
return Column(
|
|
9506
|
+
mainAxisSize: MainAxisSize.min,
|
|
9507
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
9508
|
+
children: [
|
|
9509
|
+
KasyDropDown<String>(
|
|
9510
|
+
label: 'State',
|
|
9511
|
+
hint: 'Select one',
|
|
9512
|
+
showRequiredIndicator: true,
|
|
9513
|
+
value: _state,
|
|
9514
|
+
items: _kDropDownStates,
|
|
9515
|
+
onChanged: (v) => setState(() => _state = v),
|
|
9516
|
+
),
|
|
9517
|
+
],
|
|
9518
|
+
);
|
|
9519
|
+
}
|
|
9520
|
+
}
|
|
9521
|
+
|
|
9522
|
+
Widget _buildDropDownRich(BuildContext context) => const _DropDownRichPreview();
|
|
9523
|
+
|
|
9524
|
+
class _DropDownRichPreview extends StatefulWidget {
|
|
9525
|
+
const _DropDownRichPreview();
|
|
9526
|
+
|
|
9527
|
+
@override
|
|
9528
|
+
State<_DropDownRichPreview> createState() => _DropDownRichPreviewState();
|
|
9529
|
+
}
|
|
9530
|
+
|
|
9531
|
+
class _DropDownRichPreviewState extends State<_DropDownRichPreview> {
|
|
9532
|
+
String? _action;
|
|
9533
|
+
|
|
9534
|
+
@override
|
|
9535
|
+
Widget build(BuildContext context) {
|
|
9536
|
+
return Column(
|
|
9537
|
+
mainAxisSize: MainAxisSize.min,
|
|
9538
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
9539
|
+
children: [
|
|
9540
|
+
KasyDropDown<String>(
|
|
9541
|
+
label: 'Quick action',
|
|
9542
|
+
hint: 'Pick an action',
|
|
9543
|
+
leadingIcon: KasyIcons.idea,
|
|
9544
|
+
value: _action,
|
|
9545
|
+
items: const [
|
|
9546
|
+
KasyDropDownItem(
|
|
9547
|
+
value: 'new',
|
|
9548
|
+
label: 'New file',
|
|
9549
|
+
icon: KasyIcons.add,
|
|
9550
|
+
subtitle: 'Create a new file',
|
|
9551
|
+
),
|
|
9552
|
+
KasyDropDownItem(
|
|
9553
|
+
value: 'copy',
|
|
9554
|
+
label: 'Copy link',
|
|
9555
|
+
icon: KasyIcons.copy,
|
|
9556
|
+
subtitle: 'Copy a shareable link',
|
|
9557
|
+
),
|
|
9558
|
+
KasyDropDownItem(
|
|
9559
|
+
value: 'settings',
|
|
9560
|
+
label: 'Settings',
|
|
9561
|
+
icon: KasyIcons.settings,
|
|
9562
|
+
subtitle: 'Manage file permissions',
|
|
9563
|
+
),
|
|
9564
|
+
],
|
|
9565
|
+
onChanged: (v) => setState(() => _action = v),
|
|
9566
|
+
),
|
|
9567
|
+
],
|
|
9568
|
+
);
|
|
9569
|
+
}
|
|
9570
|
+
}
|
|
9571
|
+
|
|
9572
|
+
Widget _buildDropDownStates(BuildContext context) =>
|
|
9573
|
+
const _DropDownStatesPreview();
|
|
9574
|
+
|
|
9575
|
+
class _DropDownStatesPreview extends StatefulWidget {
|
|
9576
|
+
const _DropDownStatesPreview();
|
|
9577
|
+
|
|
9578
|
+
@override
|
|
9579
|
+
State<_DropDownStatesPreview> createState() => _DropDownStatesPreviewState();
|
|
9580
|
+
}
|
|
9581
|
+
|
|
9582
|
+
class _DropDownStatesPreviewState extends State<_DropDownStatesPreview> {
|
|
9583
|
+
String? _invalid;
|
|
9584
|
+
|
|
9585
|
+
@override
|
|
9586
|
+
Widget build(BuildContext context) {
|
|
9587
|
+
return Column(
|
|
9588
|
+
mainAxisSize: MainAxisSize.min,
|
|
9589
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
9590
|
+
children: [
|
|
9591
|
+
// Pre-selected value.
|
|
9592
|
+
KasyDropDown<String>(
|
|
9593
|
+
label: 'Selected',
|
|
9594
|
+
hint: 'Select one',
|
|
9595
|
+
value: 'ca',
|
|
9596
|
+
items: _kDropDownStates,
|
|
9597
|
+
onChanged: (_) {},
|
|
9598
|
+
),
|
|
9599
|
+
const SizedBox(height: KasySpacing.lg),
|
|
9600
|
+
// Invalid + error text.
|
|
9601
|
+
KasyDropDown<String>(
|
|
9602
|
+
label: 'Required',
|
|
9603
|
+
hint: 'Select one',
|
|
9604
|
+
showRequiredIndicator: true,
|
|
9605
|
+
isInvalid: _invalid == null,
|
|
9606
|
+
errorText: _invalid == null ? 'Please choose a state.' : null,
|
|
9607
|
+
value: _invalid,
|
|
9608
|
+
items: _kDropDownStates,
|
|
9609
|
+
onChanged: (v) => setState(() => _invalid = v),
|
|
9610
|
+
),
|
|
9611
|
+
const SizedBox(height: KasySpacing.lg),
|
|
9612
|
+
// Disabled trigger.
|
|
9613
|
+
const KasyDropDown<String>(
|
|
9614
|
+
label: 'Disabled',
|
|
9615
|
+
hint: 'Locked',
|
|
9616
|
+
enabled: false,
|
|
9617
|
+
items: _kDropDownStates,
|
|
9618
|
+
onChanged: null,
|
|
9619
|
+
),
|
|
9620
|
+
],
|
|
9621
|
+
);
|
|
9622
|
+
}
|
|
9623
|
+
}
|
|
@@ -336,7 +336,7 @@ class _FilterCard extends StatelessWidget {
|
|
|
336
336
|
data.title,
|
|
337
337
|
maxLines: 1,
|
|
338
338
|
overflow: TextOverflow.ellipsis,
|
|
339
|
-
style: context.
|
|
339
|
+
style: context.kasyTextTheme.cardTitle.copyWith(
|
|
340
340
|
color: selected ? c.primary : c.onSurface,
|
|
341
341
|
),
|
|
342
342
|
),
|
|
@@ -345,7 +345,7 @@ class _FilterCard extends StatelessWidget {
|
|
|
345
345
|
data.subtitle,
|
|
346
346
|
maxLines: 1,
|
|
347
347
|
overflow: TextOverflow.ellipsis,
|
|
348
|
-
style: context.
|
|
348
|
+
style: context.kasyTextTheme.cardSubtitle.copyWith(
|
|
349
349
|
color: c.muted,
|
|
350
350
|
),
|
|
351
351
|
),
|
|
@@ -234,7 +234,7 @@ class _PhotoTileState extends State<_PhotoTile> {
|
|
|
234
234
|
photo.author,
|
|
235
235
|
maxLines: 1,
|
|
236
236
|
overflow: TextOverflow.ellipsis,
|
|
237
|
-
style: context.
|
|
237
|
+
style: context.kasyTextTheme.cardTitle.copyWith(
|
|
238
238
|
color: context.colors.onSurface,
|
|
239
239
|
),
|
|
240
240
|
),
|
|
@@ -242,7 +242,7 @@ class _PhotoTileState extends State<_PhotoTile> {
|
|
|
242
242
|
photo.ago,
|
|
243
243
|
maxLines: 1,
|
|
244
244
|
overflow: TextOverflow.ellipsis,
|
|
245
|
-
style: context.
|
|
245
|
+
style: context.kasyTextTheme.caption.copyWith(
|
|
246
246
|
color: context.colors.muted,
|
|
247
247
|
),
|
|
248
248
|
),
|
|
@@ -375,7 +375,7 @@ class _LikeButtonState extends State<_LikeButton>
|
|
|
375
375
|
|
|
376
376
|
return KasyFocusRing(
|
|
377
377
|
onActivate: widget.onTap,
|
|
378
|
-
borderRadius: BorderRadius.circular(
|
|
378
|
+
borderRadius: BorderRadius.circular(KasyRadius.full),
|
|
379
379
|
child: GestureDetector(
|
|
380
380
|
onTap: widget.onTap,
|
|
381
381
|
behavior: HitTestBehavior.opaque,
|
|
@@ -21,7 +21,14 @@ class ReminderNotifier extends _$ReminderNotifier {
|
|
|
21
21
|
|
|
22
22
|
Future<void> setType(ReminderType type) async {
|
|
23
23
|
final current = state.requireValue;
|
|
24
|
-
|
|
24
|
+
// Leaving "specific date" drops the stored one-off date so it doesn't
|
|
25
|
+
// linger (and silently come back) if the user returns to that mode later.
|
|
26
|
+
await _applyAndSave(
|
|
27
|
+
current.copyWith(
|
|
28
|
+
type: type,
|
|
29
|
+
clearDate: type != ReminderType.specificDate,
|
|
30
|
+
),
|
|
31
|
+
);
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
Future<void> setTime(int hour, int minute) async {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:intl/intl.dart';
|
|
3
4
|
import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
4
5
|
import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
|
|
5
6
|
import 'package:kasy_kit/components/kasy_card.dart';
|
|
@@ -7,6 +8,7 @@ import 'package:kasy_kit/components/kasy_chip.dart';
|
|
|
7
8
|
import 'package:kasy_kit/components/kasy_date_picker.dart';
|
|
8
9
|
import 'package:kasy_kit/components/kasy_tabs.dart';
|
|
9
10
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
11
|
+
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
10
12
|
import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
|
|
11
13
|
import 'package:kasy_kit/features/local_reminders/providers/reminder_notifier.dart';
|
|
12
14
|
import 'package:kasy_kit/features/local_reminders/repositories/reminder_preferences.dart';
|
|
@@ -77,14 +79,16 @@ class _ReminderForm extends ConsumerWidget {
|
|
|
77
79
|
child: Column(
|
|
78
80
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
79
81
|
children: [
|
|
82
|
+
// The master toggle reads like a Settings row: a colored icon
|
|
83
|
+
// badge, the label, and a live one-line summary of when the
|
|
84
|
+
// reminder fires (or a hint when it's off) as the subtitle.
|
|
80
85
|
KasyCard(
|
|
81
|
-
|
|
82
|
-
horizontal: KasySpacing.md,
|
|
83
|
-
vertical: KasySpacing.xs,
|
|
84
|
-
),
|
|
86
|
+
borderRadius: KasyRadius.lgBorderRadius,
|
|
85
87
|
child: SettingsSwitchTile(
|
|
86
88
|
icon: KasyIcons.notification,
|
|
89
|
+
iconBackgroundColor: context.colors.primary,
|
|
87
90
|
title: tr.toggleLabel,
|
|
91
|
+
subtitle: _scheduleSummary(context, state),
|
|
88
92
|
value: state.enabled,
|
|
89
93
|
onChanged: notifier.setEnabled,
|
|
90
94
|
),
|
|
@@ -93,26 +97,17 @@ class _ReminderForm extends ConsumerWidget {
|
|
|
93
97
|
const SizedBox(height: KasySpacing.xl),
|
|
94
98
|
_FieldLabel(tr.typeLabel),
|
|
95
99
|
const SizedBox(height: KasySpacing.sm),
|
|
96
|
-
//
|
|
97
|
-
//
|
|
100
|
+
// Fill mode so the three options split the width equally
|
|
101
|
+
// (a segmented control), instead of hug mode where each tab
|
|
102
|
+
// only takes its own text width and the spacing looks uneven.
|
|
98
103
|
KasyTabs(
|
|
99
104
|
tabs: [tr.daily, tr.weekly, tr.specificDate],
|
|
100
105
|
selectedIndex: _types.indexOf(state.type),
|
|
101
106
|
onTabSelected: (i) => notifier.setType(_types[i]),
|
|
107
|
+
mode: KasyTabsMode.fill,
|
|
102
108
|
),
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
if (state.type == ReminderType.daily ||
|
|
106
|
-
state.type == ReminderType.weekly) ...[
|
|
107
|
-
const SizedBox(height: KasySpacing.lg),
|
|
108
|
-
_FieldLabel(tr.timeLabel),
|
|
109
|
-
const SizedBox(height: KasySpacing.sm),
|
|
110
|
-
_TimeTile(
|
|
111
|
-
hour: state.hour,
|
|
112
|
-
minute: state.minute,
|
|
113
|
-
onChanged: (h, m) => notifier.setTime(h, m),
|
|
114
|
-
),
|
|
115
|
-
],
|
|
109
|
+
// Pick the day/date BEFORE the time so the schedule reads in a
|
|
110
|
+
// natural order (which day → at what time).
|
|
116
111
|
if (state.type == ReminderType.weekly) ...[
|
|
117
112
|
const SizedBox(height: KasySpacing.lg),
|
|
118
113
|
_FieldLabel(tr.dayLabel),
|
|
@@ -149,9 +144,14 @@ class _ReminderForm extends ConsumerWidget {
|
|
|
149
144
|
);
|
|
150
145
|
},
|
|
151
146
|
),
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
],
|
|
148
|
+
// Time is shared by every type. daily / weekly schedule by a
|
|
149
|
+
// wall-clock time (hour + minute); specificDate carries its own
|
|
150
|
+
// time inside the chosen date.
|
|
151
|
+
const SizedBox(height: KasySpacing.lg),
|
|
152
|
+
_FieldLabel(tr.timeLabel),
|
|
153
|
+
const SizedBox(height: KasySpacing.sm),
|
|
154
|
+
if (state.type == ReminderType.specificDate)
|
|
155
155
|
_TimeTile(
|
|
156
156
|
hour: state.date?.hour ?? 9,
|
|
157
157
|
minute: state.date?.minute ?? 0,
|
|
@@ -162,8 +162,13 @@ class _ReminderForm extends ConsumerWidget {
|
|
|
162
162
|
DateTime(base.year, base.month, base.day, h, m),
|
|
163
163
|
);
|
|
164
164
|
},
|
|
165
|
+
)
|
|
166
|
+
else
|
|
167
|
+
_TimeTile(
|
|
168
|
+
hour: state.hour,
|
|
169
|
+
minute: state.minute,
|
|
170
|
+
onChanged: (h, m) => notifier.setTime(h, m),
|
|
165
171
|
),
|
|
166
|
-
],
|
|
167
172
|
],
|
|
168
173
|
],
|
|
169
174
|
),
|
|
@@ -173,8 +178,43 @@ class _ReminderForm extends ConsumerWidget {
|
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
/// Human-readable, one-line description of the active schedule used as the
|
|
182
|
+
/// toggle subtitle. Reads "Every day at 09:00" / "Monday at 09:00" /
|
|
183
|
+
/// "On June 15 at 09:00", and falls back to a hint while the reminder is off
|
|
184
|
+
/// (or asks for a date when a specific-date reminder has none yet).
|
|
185
|
+
String _scheduleSummary(BuildContext context, ReminderState state) {
|
|
186
|
+
final tr = Translations.of(context).reminderPage;
|
|
187
|
+
if (!state.enabled) return tr.hint;
|
|
188
|
+
|
|
189
|
+
final String locale = Localizations.localeOf(context).toString();
|
|
190
|
+
String pad(int v) => v.toString().padLeft(2, '0');
|
|
191
|
+
|
|
192
|
+
switch (state.type) {
|
|
193
|
+
case ReminderType.daily:
|
|
194
|
+
return tr.summaryDaily(time: '${pad(state.hour)}:${pad(state.minute)}');
|
|
195
|
+
case ReminderType.weekly:
|
|
196
|
+
// January 2024 starts on a Monday, so day-of-month N maps to weekday N.
|
|
197
|
+
// DateFormat already cases the weekday per locale (e.g. "Tuesday" vs the
|
|
198
|
+
// lowercase "terça-feira"), so we use it as-is inside the summary.
|
|
199
|
+
final String day =
|
|
200
|
+
DateFormat.EEEE(locale).format(DateTime(2024, 1, state.dayOfWeek));
|
|
201
|
+
return tr.summaryWeekly(
|
|
202
|
+
day: day,
|
|
203
|
+
time: '${pad(state.hour)}:${pad(state.minute)}',
|
|
204
|
+
);
|
|
205
|
+
case ReminderType.specificDate:
|
|
206
|
+
final DateTime? date = state.date;
|
|
207
|
+
if (date == null) return tr.selectDate;
|
|
208
|
+
return tr.summaryDate(
|
|
209
|
+
date: DateFormat.MMMMd(locale).format(date),
|
|
210
|
+
time: '${pad(date.hour)}:${pad(date.minute)}',
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
176
215
|
/// Quiet group eyebrow above each field — the design-system section label
|
|
177
216
|
/// (small, gently tracked, muted), matching the Settings / sidebar pattern.
|
|
217
|
+
/// The small left inset aligns it with the Settings section labels.
|
|
178
218
|
class _FieldLabel extends StatelessWidget {
|
|
179
219
|
final String label;
|
|
180
220
|
|
|
@@ -182,10 +222,13 @@ class _FieldLabel extends StatelessWidget {
|
|
|
182
222
|
|
|
183
223
|
@override
|
|
184
224
|
Widget build(BuildContext context) {
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
225
|
+
return Padding(
|
|
226
|
+
padding: const EdgeInsets.only(left: KasySpacing.xs),
|
|
227
|
+
child: Text(
|
|
228
|
+
label,
|
|
229
|
+
style: context.kasyTextTheme.sectionLabel.copyWith(
|
|
230
|
+
color: context.colors.muted,
|
|
231
|
+
),
|
|
189
232
|
),
|
|
190
233
|
);
|
|
191
234
|
}
|
|
@@ -199,15 +242,21 @@ class _DaySelector extends StatelessWidget {
|
|
|
199
242
|
|
|
200
243
|
@override
|
|
201
244
|
Widget build(BuildContext context) {
|
|
202
|
-
// 1=Monday ... 7=Sunday (matches DateTime.weekday)
|
|
203
|
-
|
|
245
|
+
// 1=Monday ... 7=Sunday (matches DateTime.weekday). Short weekday names are
|
|
246
|
+
// localized via intl, so the chips read correctly in every app language
|
|
247
|
+
// instead of being hardcoded to one locale.
|
|
248
|
+
final DateFormat format = DateFormat.E(
|
|
249
|
+
Localizations.localeOf(context).toString(),
|
|
250
|
+
);
|
|
251
|
+
// January 2024 starts on a Monday, so day-of-month N maps to weekday N (1..7).
|
|
252
|
+
String shortName(int weekday) => format.format(DateTime(2024, 1, weekday));
|
|
204
253
|
return Wrap(
|
|
205
254
|
spacing: KasySpacing.xs,
|
|
206
255
|
runSpacing: KasySpacing.xs,
|
|
207
256
|
children: List.generate(7, (i) {
|
|
208
257
|
final dayIndex = i + 1;
|
|
209
258
|
return KasyChip(
|
|
210
|
-
label:
|
|
259
|
+
label: shortName(dayIndex),
|
|
211
260
|
selected: current == dayIndex,
|
|
212
261
|
onSelected: (_) => onChanged(dayIndex),
|
|
213
262
|
);
|
|
@@ -243,35 +292,40 @@ class _TimeTile extends StatelessWidget {
|
|
|
243
292
|
}
|
|
244
293
|
}
|
|
245
294
|
|
|
295
|
+
// Same surface + interaction model as a Settings row: an elevated card,
|
|
296
|
+
// a colored icon badge, the value, and a trailing chevron. KasyHover gives
|
|
297
|
+
// the press depth and keyboard focus ring, clipped to the card corners.
|
|
246
298
|
return KasyCard(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
style: context.textTheme.bodyLarge?.copyWith(
|
|
264
|
-
color: context.colors.onSurface,
|
|
265
|
-
fontWeight: FontWeight.w600,
|
|
299
|
+
borderRadius: KasyRadius.lgBorderRadius,
|
|
300
|
+
child: KasyHover(
|
|
301
|
+
onTap: pickTime,
|
|
302
|
+
focusable: true,
|
|
303
|
+
borderRadius: KasyRadius.lgBorderRadius,
|
|
304
|
+
focusGapColor: context.colors.surface,
|
|
305
|
+
semanticLabel: '${_pad(hour)}:${_pad(minute)}',
|
|
306
|
+
padding: const EdgeInsets.symmetric(
|
|
307
|
+
horizontal: KasySpacing.md,
|
|
308
|
+
vertical: KasySpacing.smd,
|
|
309
|
+
),
|
|
310
|
+
child: Row(
|
|
311
|
+
children: [
|
|
312
|
+
SettingsIconBadge(
|
|
313
|
+
icon: KasyIcons.time,
|
|
314
|
+
color: context.colors.primary,
|
|
266
315
|
),
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
316
|
+
const SizedBox(width: KasySpacing.sm),
|
|
317
|
+
Expanded(
|
|
318
|
+
child: Text(
|
|
319
|
+
'${_pad(hour)}:${_pad(minute)}',
|
|
320
|
+
style: context.kasyTextTheme.listRowTitle.copyWith(
|
|
321
|
+
color: context.colors.onSurface,
|
|
322
|
+
fontWeight: FontWeight.w600,
|
|
323
|
+
),
|
|
324
|
+
),
|
|
325
|
+
),
|
|
326
|
+
const SettingsListChevron(),
|
|
327
|
+
],
|
|
328
|
+
),
|
|
275
329
|
),
|
|
276
330
|
);
|
|
277
331
|
}
|