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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
3
|
import 'package:kasy_kit/components/kasy_button.dart';
|
|
4
|
+
import 'package:kasy_kit/components/kasy_drop_down.dart';
|
|
4
5
|
import 'package:kasy_kit/components/kasy_tabs.dart';
|
|
5
6
|
import 'package:kasy_kit/components/kasy_text_area.dart';
|
|
6
7
|
import 'package:kasy_kit/components/kasy_text_field.dart';
|
|
8
|
+
import 'package:kasy_kit/core/config/features.dart';
|
|
7
9
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
8
10
|
import 'package:kasy_kit/core/toast/toast_service.dart';
|
|
9
11
|
import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
|
|
@@ -12,6 +14,18 @@ import 'package:kasy_kit/features/settings/ui/components/admin/send_push_notifie
|
|
|
12
14
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
13
15
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
14
16
|
|
|
17
|
+
/// Same canonical pattern the auth `Email` value object validates against — an
|
|
18
|
+
/// address must be well-formed before it can join the recipient list.
|
|
19
|
+
final RegExp _emailPattern = RegExp(
|
|
20
|
+
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$',
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
bool _isValidEmail(String value) => _emailPattern.hasMatch(value.trim());
|
|
24
|
+
|
|
25
|
+
/// Default push destination: the in-app notifications list. Mirrors the
|
|
26
|
+
/// fallback in `Notification.onTap` (no route sent → `/notifications`).
|
|
27
|
+
const String _defaultRoute = '/notifications';
|
|
28
|
+
|
|
15
29
|
class SendPushNotificationPage extends ConsumerStatefulWidget {
|
|
16
30
|
const SendPushNotificationPage({super.key});
|
|
17
31
|
|
|
@@ -25,7 +39,6 @@ class _SendPushNotificationPageState
|
|
|
25
39
|
final _titleCtrl = TextEditingController();
|
|
26
40
|
final _bodyCtrl = TextEditingController();
|
|
27
41
|
final _imageCtrl = TextEditingController();
|
|
28
|
-
final _routeCtrl = TextEditingController();
|
|
29
42
|
final _emailCtrl = TextEditingController();
|
|
30
43
|
|
|
31
44
|
// Explicit FocusNodes for all fields keeps the keyboard open when switching.
|
|
@@ -33,12 +46,16 @@ class _SendPushNotificationPageState
|
|
|
33
46
|
final _titleFocus = FocusNode();
|
|
34
47
|
final _bodyFocus = FocusNode();
|
|
35
48
|
final _imageFocus = FocusNode();
|
|
36
|
-
final _routeFocus = FocusNode();
|
|
37
49
|
|
|
38
50
|
bool _sendToAll = false;
|
|
39
51
|
final List<String> _emails = [];
|
|
40
52
|
String _appName = '';
|
|
41
53
|
|
|
54
|
+
/// Page the app opens when the user taps the notification. Defaults to the
|
|
55
|
+
/// notifications list (the same fallback `Notification.onTap` uses when no
|
|
56
|
+
/// route is sent), and the admin can repoint it to any available page.
|
|
57
|
+
String _route = _defaultRoute;
|
|
58
|
+
|
|
42
59
|
@override
|
|
43
60
|
void initState() {
|
|
44
61
|
super.initState();
|
|
@@ -52,21 +69,26 @@ class _SendPushNotificationPageState
|
|
|
52
69
|
_titleCtrl.dispose();
|
|
53
70
|
_bodyCtrl.dispose();
|
|
54
71
|
_imageCtrl.dispose();
|
|
55
|
-
_routeCtrl.dispose();
|
|
56
72
|
_emailCtrl.dispose();
|
|
57
73
|
_emailFocus.dispose();
|
|
58
74
|
_titleFocus.dispose();
|
|
59
75
|
_bodyFocus.dispose();
|
|
60
76
|
_imageFocus.dispose();
|
|
61
|
-
_routeFocus.dispose();
|
|
62
77
|
super.dispose();
|
|
63
78
|
}
|
|
64
79
|
|
|
65
|
-
|
|
80
|
+
/// The "add" button stays disabled until the field holds a well-formed,
|
|
81
|
+
/// not-yet-listed e-mail — so it never adds blanks, malformed strings, or
|
|
82
|
+
/// duplicates (the Enter key shares this guard via [_addEmail]).
|
|
83
|
+
bool get _canAddEmail {
|
|
66
84
|
final email = _emailCtrl.text.trim();
|
|
67
|
-
|
|
85
|
+
return _isValidEmail(email) && !_emails.contains(email);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
void _addEmail() {
|
|
89
|
+
if (!_canAddEmail) return;
|
|
68
90
|
setState(() {
|
|
69
|
-
_emails.add(
|
|
91
|
+
_emails.add(_emailCtrl.text.trim());
|
|
70
92
|
_emailCtrl.clear();
|
|
71
93
|
});
|
|
72
94
|
_emailFocus.requestFocus();
|
|
@@ -83,11 +105,11 @@ class _SendPushNotificationPageState
|
|
|
83
105
|
_titleCtrl.clear();
|
|
84
106
|
_bodyCtrl.clear();
|
|
85
107
|
_imageCtrl.clear();
|
|
86
|
-
_routeCtrl.clear();
|
|
87
108
|
_emailCtrl.clear();
|
|
88
109
|
setState(() {
|
|
89
110
|
_emails.clear();
|
|
90
111
|
_sendToAll = false;
|
|
112
|
+
_route = _defaultRoute;
|
|
91
113
|
});
|
|
92
114
|
}
|
|
93
115
|
|
|
@@ -118,9 +140,7 @@ class _SendPushNotificationPageState
|
|
|
118
140
|
: _imageCtrl.text.trim(),
|
|
119
141
|
emails: List.from(_emails),
|
|
120
142
|
sendToAll: _sendToAll,
|
|
121
|
-
route:
|
|
122
|
-
? null
|
|
123
|
-
: _routeCtrl.text.trim(),
|
|
143
|
+
route: _route,
|
|
124
144
|
);
|
|
125
145
|
}
|
|
126
146
|
|
|
@@ -186,6 +206,9 @@ class _SendPushNotificationPageState
|
|
|
186
206
|
focusNode: _emailFocus,
|
|
187
207
|
onAdd: _addEmail,
|
|
188
208
|
onRemove: _removeEmail,
|
|
209
|
+
// Rebuild as the admin types so the add button enables/disables live.
|
|
210
|
+
onChanged: () => setState(() {}),
|
|
211
|
+
canAdd: _canAddEmail,
|
|
189
212
|
label: tr.send_push_email_label,
|
|
190
213
|
hint: tr.send_push_email_hint,
|
|
191
214
|
),
|
|
@@ -202,6 +225,9 @@ class _SendPushNotificationPageState
|
|
|
202
225
|
focusNode: _titleFocus,
|
|
203
226
|
maxLength: 64,
|
|
204
227
|
showRequiredIndicator: true,
|
|
228
|
+
// Secondary fill contrasts against the surface-colored section card —
|
|
229
|
+
// the primary (surface) fill would blend white-on-white.
|
|
230
|
+
variant: KasyTextFieldVariant.secondary,
|
|
205
231
|
textInputAction: TextInputAction.next,
|
|
206
232
|
onChanged: (_) => setState(() {}),
|
|
207
233
|
onEditingComplete: () => _bodyFocus.requestFocus(),
|
|
@@ -214,6 +240,7 @@ class _SendPushNotificationPageState
|
|
|
214
240
|
focusNode: _bodyFocus,
|
|
215
241
|
maxLength: 250,
|
|
216
242
|
showRequiredIndicator: true,
|
|
243
|
+
variant: KasyTextFieldVariant.secondary,
|
|
217
244
|
minLines: 3,
|
|
218
245
|
maxLines: 5,
|
|
219
246
|
onChanged: (_) => setState(() {}),
|
|
@@ -224,20 +251,50 @@ class _SendPushNotificationPageState
|
|
|
224
251
|
hint: tr.send_push_image_hint,
|
|
225
252
|
controller: _imageCtrl,
|
|
226
253
|
focusNode: _imageFocus,
|
|
254
|
+
variant: KasyTextFieldVariant.secondary,
|
|
227
255
|
onChanged: (_) => setState(() {}),
|
|
228
|
-
|
|
229
|
-
|
|
256
|
+
// Last text input on the form — close the keyboard on submit.
|
|
257
|
+
textInputAction: TextInputAction.done,
|
|
258
|
+
onEditingComplete: () => _imageFocus.unfocus(),
|
|
230
259
|
),
|
|
231
260
|
],
|
|
232
261
|
);
|
|
233
262
|
|
|
234
|
-
|
|
263
|
+
// Pick the landing page from the routes this build actually ships (the
|
|
264
|
+
// optional ones are feature-flagged, exactly like the router), instead of
|
|
265
|
+
// typing a raw path. Leaving the default sends the user to /notifications.
|
|
266
|
+
final Widget advancedFields = KasyDropDown<String>(
|
|
235
267
|
label: tr.send_push_route_label,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
268
|
+
description: tr.send_push_route_description,
|
|
269
|
+
value: _route,
|
|
270
|
+
variant: KasyTextFieldVariant.secondary,
|
|
271
|
+
onChanged: (route) => setState(() => _route = route),
|
|
272
|
+
items: <KasyDropDownItem<String>>[
|
|
273
|
+
KasyDropDownItem(
|
|
274
|
+
value: '/notifications',
|
|
275
|
+
label: tr.send_push_route_notifications,
|
|
276
|
+
),
|
|
277
|
+
KasyDropDownItem(value: '/', label: tr.send_push_route_home),
|
|
278
|
+
KasyDropDownItem(
|
|
279
|
+
value: '/settings',
|
|
280
|
+
label: tr.send_push_route_settings,
|
|
281
|
+
),
|
|
282
|
+
if (withRevenuecat)
|
|
283
|
+
KasyDropDownItem(
|
|
284
|
+
value: '/premium',
|
|
285
|
+
label: tr.send_push_route_premium,
|
|
286
|
+
),
|
|
287
|
+
if (withLocalReminders)
|
|
288
|
+
KasyDropDownItem(
|
|
289
|
+
value: '/reminder',
|
|
290
|
+
label: tr.send_push_route_reminder,
|
|
291
|
+
),
|
|
292
|
+
if (withFeedback)
|
|
293
|
+
KasyDropDownItem(
|
|
294
|
+
value: '/feedback',
|
|
295
|
+
label: tr.send_push_route_feedback,
|
|
296
|
+
),
|
|
297
|
+
],
|
|
241
298
|
);
|
|
242
299
|
|
|
243
300
|
final Widget formColumn = Column(
|
|
@@ -337,25 +394,84 @@ class _SendPushNotificationPageState
|
|
|
337
394
|
),
|
|
338
395
|
),
|
|
339
396
|
)
|
|
340
|
-
:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
397
|
+
: Stack(
|
|
398
|
+
children: [
|
|
399
|
+
// The form scrolls; the live preview stays PINNED at the top so
|
|
400
|
+
// the admin watches the notification update while filling the
|
|
401
|
+
// fields. An invisible copy of the preview at the head of the
|
|
402
|
+
// scroll reserves exactly its height, so the first section
|
|
403
|
+
// starts just below the banner and slides BEHIND it on scroll.
|
|
404
|
+
Positioned.fill(
|
|
405
|
+
child: scroll(
|
|
406
|
+
padding: EdgeInsets.fromLTRB(
|
|
407
|
+
gutter,
|
|
408
|
+
KasySpacing.belowChromeContentGap,
|
|
409
|
+
gutter,
|
|
410
|
+
bottomInset,
|
|
411
|
+
),
|
|
412
|
+
child: Column(
|
|
413
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
414
|
+
children: [
|
|
415
|
+
Visibility(
|
|
416
|
+
visible: false,
|
|
417
|
+
maintainSize: true,
|
|
418
|
+
maintainAnimation: true,
|
|
419
|
+
maintainState: true,
|
|
420
|
+
child: previewPanel,
|
|
421
|
+
),
|
|
422
|
+
const SizedBox(height: KasySpacing.lg),
|
|
423
|
+
formColumn,
|
|
424
|
+
],
|
|
425
|
+
),
|
|
426
|
+
),
|
|
427
|
+
),
|
|
428
|
+
// Pinned banner: an opaque page-colored band (so the scrolling
|
|
429
|
+
// form disappears behind it) with a short fade at its bottom
|
|
430
|
+
// edge. IgnorePointer lets drags over the preview still scroll
|
|
431
|
+
// the form — the preview itself has no interaction.
|
|
432
|
+
Positioned(
|
|
433
|
+
top: 0,
|
|
434
|
+
left: 0,
|
|
435
|
+
right: 0,
|
|
436
|
+
child: IgnorePointer(
|
|
437
|
+
child: Column(
|
|
438
|
+
mainAxisSize: MainAxisSize.min,
|
|
439
|
+
children: [
|
|
440
|
+
Container(
|
|
441
|
+
color: context.colors.background,
|
|
442
|
+
padding: const EdgeInsets.fromLTRB(
|
|
443
|
+
gutter,
|
|
444
|
+
KasySpacing.belowChromeContentGap,
|
|
445
|
+
gutter,
|
|
446
|
+
0,
|
|
447
|
+
),
|
|
448
|
+
child: previewPanel,
|
|
449
|
+
),
|
|
450
|
+
Container(
|
|
451
|
+
height: KasySpacing.md,
|
|
452
|
+
decoration: BoxDecoration(
|
|
453
|
+
gradient: LinearGradient(
|
|
454
|
+
begin: Alignment.topCenter,
|
|
455
|
+
end: Alignment.bottomCenter,
|
|
456
|
+
colors: [
|
|
457
|
+
context.colors.background,
|
|
458
|
+
context.colors.background.withValues(alpha: 0),
|
|
459
|
+
],
|
|
460
|
+
),
|
|
461
|
+
),
|
|
462
|
+
),
|
|
463
|
+
],
|
|
464
|
+
),
|
|
465
|
+
),
|
|
466
|
+
),
|
|
467
|
+
],
|
|
355
468
|
);
|
|
356
469
|
|
|
357
470
|
return ScrollConfiguration(
|
|
358
|
-
|
|
471
|
+
// Hide the scrollbar: on the two-column layout it lands between the
|
|
472
|
+
// form and the live preview and reads as a stray mark next to the
|
|
473
|
+
// cards. The form is short enough not to need it.
|
|
474
|
+
behavior: const KasyKitScrollBehavior().copyWith(scrollbars: false),
|
|
359
475
|
child: bodyContent,
|
|
360
476
|
);
|
|
361
477
|
},
|
|
@@ -416,6 +532,8 @@ class _EmailsSection extends StatelessWidget {
|
|
|
416
532
|
final FocusNode focusNode;
|
|
417
533
|
final VoidCallback onAdd;
|
|
418
534
|
final void Function(String) onRemove;
|
|
535
|
+
final VoidCallback onChanged;
|
|
536
|
+
final bool canAdd;
|
|
419
537
|
final String label;
|
|
420
538
|
final String hint;
|
|
421
539
|
|
|
@@ -425,6 +543,8 @@ class _EmailsSection extends StatelessWidget {
|
|
|
425
543
|
required this.focusNode,
|
|
426
544
|
required this.onAdd,
|
|
427
545
|
required this.onRemove,
|
|
546
|
+
required this.onChanged,
|
|
547
|
+
required this.canAdd,
|
|
428
548
|
required this.label,
|
|
429
549
|
required this.hint,
|
|
430
550
|
});
|
|
@@ -444,6 +564,9 @@ class _EmailsSection extends StatelessWidget {
|
|
|
444
564
|
controller: controller,
|
|
445
565
|
focusNode: focusNode,
|
|
446
566
|
showRequiredIndicator: true,
|
|
567
|
+
variant: KasyTextFieldVariant.secondary,
|
|
568
|
+
contentType: KasyTextFieldContentType.email,
|
|
569
|
+
onChanged: (_) => onChanged(),
|
|
447
570
|
// "next" keeps keyboard open when pressing the action key.
|
|
448
571
|
textInputAction: TextInputAction.next,
|
|
449
572
|
onEditingComplete: onAdd,
|
|
@@ -453,10 +576,12 @@ class _EmailsSection extends StatelessWidget {
|
|
|
453
576
|
Padding(
|
|
454
577
|
padding: const EdgeInsets.only(bottom: 2),
|
|
455
578
|
child: KasyButton.iconOnly(
|
|
456
|
-
icon: Icons.
|
|
457
|
-
|
|
579
|
+
icon: Icons.add_rounded,
|
|
580
|
+
// Disabled (greyed, non-tappable) until the field holds a valid,
|
|
581
|
+
// non-duplicate e-mail.
|
|
582
|
+
onPressed: canAdd ? onAdd : null,
|
|
458
583
|
size: KasyButtonSize.small,
|
|
459
|
-
|
|
584
|
+
iconGlyphSize: 20,
|
|
460
585
|
),
|
|
461
586
|
),
|
|
462
587
|
],
|
|
@@ -489,11 +614,16 @@ class _EmailChip extends StatelessWidget {
|
|
|
489
614
|
|
|
490
615
|
@override
|
|
491
616
|
Widget build(BuildContext context) {
|
|
617
|
+
// An e-mail is plain data the admin listed, not an action or a status — so
|
|
618
|
+
// it reads as a neutral pill (clean, color used in moderation), not an
|
|
619
|
+
// accent-blue tag. surfaceSecondary contrasts against the section card's
|
|
620
|
+
// surface in both modes; the text uses the normal foreground tone.
|
|
492
621
|
return Container(
|
|
493
622
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
|
494
623
|
decoration: BoxDecoration(
|
|
495
|
-
color: context.colors.
|
|
624
|
+
color: context.colors.surfaceSecondary,
|
|
496
625
|
borderRadius: BorderRadius.circular(20),
|
|
626
|
+
border: Border.all(color: context.colors.border),
|
|
497
627
|
),
|
|
498
628
|
child: Row(
|
|
499
629
|
mainAxisSize: MainAxisSize.min,
|
|
@@ -501,7 +631,7 @@ class _EmailChip extends StatelessWidget {
|
|
|
501
631
|
Text(
|
|
502
632
|
email,
|
|
503
633
|
style: context.textTheme.bodySmall?.copyWith(
|
|
504
|
-
color: context.colors.
|
|
634
|
+
color: context.colors.onSurface,
|
|
505
635
|
fontWeight: FontWeight.w500,
|
|
506
636
|
),
|
|
507
637
|
),
|
|
@@ -511,7 +641,7 @@ class _EmailChip extends StatelessWidget {
|
|
|
511
641
|
child: Icon(
|
|
512
642
|
Icons.close,
|
|
513
643
|
size: 13,
|
|
514
|
-
color: context.colors.
|
|
644
|
+
color: context.colors.muted,
|
|
515
645
|
),
|
|
516
646
|
),
|
|
517
647
|
],
|
|
@@ -67,7 +67,7 @@ class _EditableUserAvatarState extends ConsumerState<EditableUserAvatar> {
|
|
|
67
67
|
return KasyFocusRing(
|
|
68
68
|
enabled: onTapAvatar != null,
|
|
69
69
|
onActivate: onTapAvatar,
|
|
70
|
-
borderRadius: BorderRadius.circular(
|
|
70
|
+
borderRadius: BorderRadius.circular(KasyRadius.full),
|
|
71
71
|
child: GestureDetector(
|
|
72
72
|
behavior: HitTestBehavior.opaque,
|
|
73
73
|
onTap: onTapAvatar,
|
|
@@ -4,6 +4,7 @@ import 'package:kasy_kit/components/components.dart';
|
|
|
4
4
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
5
5
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
6
6
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
7
|
+
import 'package:kasy_kit/router.dart';
|
|
7
8
|
|
|
8
9
|
class DeleteUserButton extends ConsumerWidget {
|
|
9
10
|
const DeleteUserButton({super.key});
|
|
@@ -40,7 +41,14 @@ class DeleteUserButton extends ConsumerWidget {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
},
|
|
43
|
-
)
|
|
44
|
+
).whenComplete(() {
|
|
45
|
+
// Same pageless-dialog issue as logout: on success deleteAccount flips
|
|
46
|
+
// the state to anonymous while this confirm dialog still sits on the
|
|
47
|
+
// root navigator, so the redirect to /signin can't land. Re-run it
|
|
48
|
+
// once the dialog has popped. Harmless when the user cancels or the
|
|
49
|
+
// deletion failed (still authenticated → redirect keeps them here).
|
|
50
|
+
if (context.mounted) ref.read(goRouterProvider).refresh();
|
|
51
|
+
});
|
|
44
52
|
},
|
|
45
53
|
);
|
|
46
54
|
}
|
|
@@ -6,7 +6,6 @@ import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
|
|
|
6
6
|
import 'package:kasy_kit/core/home_widgets/home_widget_mywidget_service.dart';
|
|
7
7
|
import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
|
|
8
8
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
9
|
-
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
10
9
|
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
11
10
|
import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
|
|
12
11
|
import 'package:kasy_kit/i18n/app_locale_display.dart';
|
|
@@ -20,41 +19,41 @@ class LanguageSwitcher extends ConsumerWidget {
|
|
|
20
19
|
@override
|
|
21
20
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
22
21
|
final AppLocale current = TranslationProvider.of(context).locale;
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
return KasyHover(
|
|
23
|
+
onTap: () => _showLanguagePicker(context, ref, current),
|
|
24
|
+
focusable: true,
|
|
25
|
+
// Rectangular highlight (KasyHover default): this row lives inside a card,
|
|
26
|
+
// whose rounded corners clip the ends — a rounded fill here would float.
|
|
27
|
+
semanticLabel: context.t.settings.language_title,
|
|
28
|
+
padding: const EdgeInsets.symmetric(
|
|
29
|
+
horizontal: KasySpacing.md,
|
|
30
|
+
vertical: KasySpacing.smd,
|
|
31
|
+
),
|
|
32
|
+
child: Row(
|
|
33
|
+
children: <Widget>[
|
|
34
|
+
Icon(
|
|
35
|
+
KasyIcons.language,
|
|
36
|
+
size: KasyIconSize.rowLeading,
|
|
37
|
+
color: context.colors.onSurface,
|
|
38
|
+
),
|
|
39
|
+
const SizedBox(width: KasySpacing.sm),
|
|
40
|
+
Expanded(
|
|
41
|
+
child: Text(
|
|
42
|
+
context.t.settings.language_title,
|
|
43
|
+
style: context.kasyTextTheme.listRowTitle.copyWith(
|
|
36
44
|
color: context.colors.onSurface,
|
|
37
45
|
),
|
|
38
|
-
|
|
39
|
-
Expanded(
|
|
40
|
-
child: Text(
|
|
41
|
-
context.t.settings.language_title,
|
|
42
|
-
style: context.textTheme.titleSmall?.copyWith(
|
|
43
|
-
color: context.colors.onSurface,
|
|
44
|
-
),
|
|
45
|
-
),
|
|
46
|
-
),
|
|
47
|
-
Text(
|
|
48
|
-
current.nativeName,
|
|
49
|
-
style: context.textTheme.bodyMedium?.copyWith(
|
|
50
|
-
color: context.colors.muted,
|
|
51
|
-
),
|
|
52
|
-
),
|
|
53
|
-
const SizedBox(width: KasySpacing.xs),
|
|
54
|
-
const SettingsListChevron(),
|
|
55
|
-
],
|
|
46
|
+
),
|
|
56
47
|
),
|
|
57
|
-
|
|
48
|
+
Text(
|
|
49
|
+
current.nativeName,
|
|
50
|
+
style: context.kasyTextTheme.listRowValue.copyWith(
|
|
51
|
+
color: context.colors.muted,
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
const SizedBox(width: KasySpacing.xs),
|
|
55
|
+
const SettingsListChevron(),
|
|
56
|
+
],
|
|
58
57
|
),
|
|
59
58
|
);
|
|
60
59
|
}
|
|
@@ -69,21 +68,22 @@ class LanguageSwitcher extends ConsumerWidget {
|
|
|
69
68
|
context: context,
|
|
70
69
|
builder: (sheetContext) => KasySheetSurface(
|
|
71
70
|
child: Padding(
|
|
72
|
-
padding:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
KasySpacing.sm,
|
|
77
|
-
),
|
|
71
|
+
// No horizontal padding: option rows go full-bleed so the highlight
|
|
72
|
+
// spans the whole sheet (no inset pill). Title and rows carry their
|
|
73
|
+
// own horizontal inset instead.
|
|
74
|
+
padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
|
|
78
75
|
child: Column(
|
|
79
76
|
mainAxisSize: MainAxisSize.min,
|
|
80
77
|
children: <Widget>[
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
child:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
Padding(
|
|
79
|
+
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
|
|
80
|
+
child: Align(
|
|
81
|
+
alignment: Alignment.centerLeft,
|
|
82
|
+
child: Text(
|
|
83
|
+
sheetTitle,
|
|
84
|
+
style: sheetContext.textTheme.titleMedium?.copyWith(
|
|
85
|
+
color: sheetContext.colors.onSurface,
|
|
86
|
+
),
|
|
87
87
|
),
|
|
88
88
|
),
|
|
89
89
|
),
|
|
@@ -147,9 +147,10 @@ class _LocaleOptionTile extends StatelessWidget {
|
|
|
147
147
|
return KasyHover(
|
|
148
148
|
onTap: onTap,
|
|
149
149
|
focusable: true,
|
|
150
|
-
|
|
150
|
+
// Full-bleed rectangular highlight (default radius): the sheet rounds its
|
|
151
|
+
// own corners, so options span edge-to-edge like a native menu/list.
|
|
151
152
|
padding: const EdgeInsets.symmetric(
|
|
152
|
-
horizontal: KasySpacing.
|
|
153
|
+
horizontal: KasySpacing.md,
|
|
153
154
|
vertical: KasySpacing.smd,
|
|
154
155
|
),
|
|
155
156
|
child: Row(
|
package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
3
|
+
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
3
4
|
|
|
4
5
|
/// Row used in settings modal bottom sheets (avatar source, language, etc.).
|
|
5
6
|
class SettingsBottomSheetOptionTile extends StatelessWidget {
|
|
@@ -26,26 +27,28 @@ class SettingsBottomSheetOptionTile extends StatelessWidget {
|
|
|
26
27
|
final TextStyle? labelStyle =
|
|
27
28
|
context.textTheme.titleMedium?.copyWith(color: fg);
|
|
28
29
|
final Text labelWidget = Text(label, style: labelStyle);
|
|
29
|
-
return
|
|
30
|
+
return KasyHover(
|
|
30
31
|
onTap: onTap,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
focusable: true,
|
|
33
|
+
semanticLabel: label,
|
|
34
|
+
// Full-bleed rectangular highlight (default radius): these rows live in a
|
|
35
|
+
// bottom sheet whose rounded corners clip the ends, like a native menu.
|
|
36
|
+
padding: const EdgeInsets.symmetric(
|
|
37
|
+
horizontal: KasySpacing.md,
|
|
38
|
+
vertical: KasySpacing.smd,
|
|
39
|
+
),
|
|
40
|
+
child: Row(
|
|
41
|
+
children: <Widget>[
|
|
42
|
+
if (leading != null) ...<Widget>[
|
|
43
|
+
leading!,
|
|
44
|
+
const SizedBox(width: KasySpacing.sm),
|
|
45
|
+
] else if (icon != null) ...<Widget>[
|
|
46
|
+
Icon(icon, size: KasyIconSize.lg, color: fg),
|
|
47
|
+
const SizedBox(width: KasySpacing.sm),
|
|
47
48
|
],
|
|
48
|
-
|
|
49
|
+
Expanded(child: labelWidget),
|
|
50
|
+
if (trailing != null) trailing!,
|
|
51
|
+
],
|
|
49
52
|
),
|
|
50
53
|
);
|
|
51
54
|
}
|
|
@@ -26,7 +26,7 @@ class SettingsIconBadge extends StatelessWidget {
|
|
|
26
26
|
height: size,
|
|
27
27
|
decoration: BoxDecoration(
|
|
28
28
|
color: color.withValues(alpha: 0.12),
|
|
29
|
-
borderRadius: BorderRadius.circular(
|
|
29
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
30
30
|
),
|
|
31
31
|
child: Icon(
|
|
32
32
|
icon,
|
|
@@ -65,7 +65,17 @@ class SettingsDivider extends StatelessWidget {
|
|
|
65
65
|
|
|
66
66
|
@override
|
|
67
67
|
Widget build(BuildContext context) {
|
|
68
|
-
|
|
68
|
+
// Inset to align with the rows' content (which now carry the horizontal
|
|
69
|
+
// padding the card used to have), so the hairline doesn't touch the edges.
|
|
70
|
+
return Divider(
|
|
71
|
+
// Hairline only — no extra height — so rows sit close together (iOS-style
|
|
72
|
+
// contiguous list) instead of floating apart with a 16px gap.
|
|
73
|
+
height: 1,
|
|
74
|
+
thickness: 1,
|
|
75
|
+
color: context.colors.onBackground.withValues(alpha: .06),
|
|
76
|
+
indent: KasySpacing.md,
|
|
77
|
+
endIndent: KasySpacing.md,
|
|
78
|
+
);
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
81
|
|
|
@@ -91,7 +101,10 @@ class SettingsSwitchTile extends StatelessWidget {
|
|
|
91
101
|
@override
|
|
92
102
|
Widget build(BuildContext context) {
|
|
93
103
|
return Padding(
|
|
94
|
-
padding: const EdgeInsets.symmetric(
|
|
104
|
+
padding: const EdgeInsets.symmetric(
|
|
105
|
+
horizontal: KasySpacing.md,
|
|
106
|
+
vertical: KasySpacing.smd,
|
|
107
|
+
),
|
|
95
108
|
child: Row(
|
|
96
109
|
children: <Widget>[
|
|
97
110
|
if (iconBackgroundColor != null)
|
|
@@ -110,7 +123,7 @@ class SettingsSwitchTile extends StatelessWidget {
|
|
|
110
123
|
children: <Widget>[
|
|
111
124
|
Text(
|
|
112
125
|
title,
|
|
113
|
-
style: context.
|
|
126
|
+
style: context.kasyTextTheme.listRowTitle.copyWith(
|
|
114
127
|
color: context.colors.onSurface,
|
|
115
128
|
),
|
|
116
129
|
),
|
|
@@ -165,12 +178,14 @@ class SettingsTile extends StatelessWidget {
|
|
|
165
178
|
Widget build(BuildContext context) {
|
|
166
179
|
return KasyHover(
|
|
167
180
|
onTap: onTap,
|
|
168
|
-
hoverEnabled: false,
|
|
169
|
-
pressEnabled: false,
|
|
170
181
|
focusable: true,
|
|
171
|
-
|
|
182
|
+
// Rectangular highlight (default): the card clips the rounded ends, so
|
|
183
|
+
// middle rows stay square instead of showing a floating rounded pill.
|
|
172
184
|
semanticLabel: title,
|
|
173
|
-
padding: const EdgeInsets.symmetric(
|
|
185
|
+
padding: const EdgeInsets.symmetric(
|
|
186
|
+
horizontal: KasySpacing.md,
|
|
187
|
+
vertical: KasySpacing.smd,
|
|
188
|
+
),
|
|
174
189
|
child: Row(
|
|
175
190
|
children: [
|
|
176
191
|
if (iconBackgroundColor != null)
|
|
@@ -185,7 +200,7 @@ class SettingsTile extends StatelessWidget {
|
|
|
185
200
|
Expanded(
|
|
186
201
|
child: Text(
|
|
187
202
|
title,
|
|
188
|
-
style: context.
|
|
203
|
+
style: context.kasyTextTheme.listRowTitle.copyWith(
|
|
189
204
|
color: context.colors.onSurface,
|
|
190
205
|
),
|
|
191
206
|
),
|
|
@@ -193,7 +208,7 @@ class SettingsTile extends StatelessWidget {
|
|
|
193
208
|
if (trailingLabel != null) ...[
|
|
194
209
|
Text(
|
|
195
210
|
trailingLabel!,
|
|
196
|
-
style: context.
|
|
211
|
+
style: context.kasyTextTheme.listRowValue.copyWith(
|
|
197
212
|
color: context.colors.muted,
|
|
198
213
|
),
|
|
199
214
|
),
|