kasy-cli 1.37.1 → 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 +23 -0
- package/lib/scaffold/backends/api/patch/README.md +15 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -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/core/data/api/user_api.dart +8 -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/AGENTS.md +7 -1
- package/templates/firebase/DESIGN_SYSTEM.md +35 -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 +361 -20
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
- package/templates/firebase/lib/components/kasy_card.dart +4 -0
- package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
- package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +412 -31
- 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 +29 -231
- package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +19 -9
- 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 +15 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
- package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
- 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 +69 -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/theme/universal_theme.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
- package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +547 -483
- 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/core/widgets/responsive_layout.dart +8 -0
- 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 +264 -126
- 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 +118 -57
- package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +19 -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 +262 -0
- 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 +99 -65
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1379 -422
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
- 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 +404 -149
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +24 -31
- 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 +77 -95
- 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 -698
- package/templates/firebase/lib/i18n/es.i18n.json +749 -698
- package/templates/firebase/lib/i18n/pt.i18n.json +749 -698
- package/templates/firebase/lib/main.dart +20 -7
- package/templates/firebase/lib/router.dart +70 -46
- package/templates/firebase/pubspec.yaml +2 -1
- package/templates/firebase/test/admin_shell_chrome_test.dart +110 -0
- 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/test/features/authentication/data/api/user_api_fake.dart +3 -0
- 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 -210
- 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 -169
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
|
@@ -30,7 +30,25 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
30
30
|
return OnboardingState();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/// Enter/leave preview mode. Called once when [OnboardingPage] mounts: the
|
|
34
|
+
/// admin Debug "Test onboarding" entry sets `true`, the real flow sets
|
|
35
|
+
/// `false`, so the flag is always correct on every entry.
|
|
36
|
+
void setPreview(bool value) {
|
|
37
|
+
if (state.preview == value) return;
|
|
38
|
+
state = state.copyWith(preview: value);
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
Future<void> onAnsweredQuestion(UserInfoDetail value) async {
|
|
42
|
+
// Preview: never touch the admin's real profile. Buffer the answer so the
|
|
43
|
+
// question screens still behave (selection persists across steps), but it's
|
|
44
|
+
// discarded with the provider state once the preview ends.
|
|
45
|
+
if (state.preview) {
|
|
46
|
+
final others = state.pendingUserInfo
|
|
47
|
+
.where((info) => info.runtimeType != value.runtimeType)
|
|
48
|
+
.toList();
|
|
49
|
+
state = state.copyWith(pendingUserInfo: [...others, value]);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
34
52
|
final userId = ref.read(userStateNotifierProvider).user.idOrNull;
|
|
35
53
|
if (userId != null) {
|
|
36
54
|
// Account already exists (e.g. a returning guest re-onboarding): save now.
|
|
@@ -47,6 +65,10 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
Future<void> setupNotifications() async {
|
|
68
|
+
// Preview: don't fire the real OS permission dialog or log analytics — the
|
|
69
|
+
// permission screen is shown for review only.
|
|
70
|
+
if (state.preview) return;
|
|
71
|
+
|
|
50
72
|
final userStateNotifier = ref.read(userStateNotifierProvider.notifier);
|
|
51
73
|
final notificationsRepository = ref.read(notificationRepositoryProvider);
|
|
52
74
|
|
|
@@ -75,6 +97,10 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
75
97
|
}
|
|
76
98
|
|
|
77
99
|
Future<void> onOnboardingCompleted() async {
|
|
100
|
+
// Preview: this is the step that would create the anonymous guest account
|
|
101
|
+
// and flush profile writes. Do nothing — the preview just returns to Debug.
|
|
102
|
+
if (state.preview) return;
|
|
103
|
+
|
|
78
104
|
final userStateNotifier = ref.read(userStateNotifierProvider.notifier);
|
|
79
105
|
|
|
80
106
|
// This is the "preparing everything for you" moment (the loader screen):
|
|
@@ -106,6 +132,8 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
106
132
|
/// Skip onboarding: mark as onboarded instantly (optimistic) and navigate
|
|
107
133
|
/// to home. Permissions (push + ATT) are requested on the home screen.
|
|
108
134
|
void skipOnboarding() {
|
|
135
|
+
// Preview: skipping must not mark the real user as onboarded.
|
|
136
|
+
if (state.preview) return;
|
|
109
137
|
ref.read(userStateNotifierProvider.notifier).onSkippedOnboarding();
|
|
110
138
|
}
|
|
111
139
|
}
|
|
@@ -9,22 +9,55 @@ import 'package:kasy_kit/features/onboarding/ui/components/onboarding_features.d
|
|
|
9
9
|
import 'package:kasy_kit/features/onboarding/ui/components/onboarding_loader.dart';
|
|
10
10
|
import 'package:kasy_kit/features/onboarding/ui/components/onboarding_notifications_setup.dart';
|
|
11
11
|
import 'package:kasy_kit/features/onboarding/ui/components/onboarding_questions.dart';
|
|
12
|
+
import 'package:kasy_kit/features/settings/ui/components/admin/admin_routes.dart';
|
|
12
13
|
import 'package:kasy_kit/features/subscriptions/ui/premium_page.dart';
|
|
13
14
|
import 'package:kasy_kit/router.dart';
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
class OnboardingPage extends
|
|
17
|
-
|
|
17
|
+
class OnboardingPage extends ConsumerStatefulWidget {
|
|
18
|
+
/// Preview mode: the flow is opened from the admin Debug screen just to walk
|
|
19
|
+
/// the screens. Real side effects are suppressed (see [OnboardingNotifier])
|
|
20
|
+
/// and the flow returns to Debug instead of Home/paywall.
|
|
21
|
+
final bool preview;
|
|
22
|
+
|
|
23
|
+
const OnboardingPage({super.key, this.preview = false});
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
ConsumerState<OnboardingPage> createState() => _OnboardingPageState();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class _OnboardingPageState extends ConsumerState<OnboardingPage> {
|
|
30
|
+
@override
|
|
31
|
+
void initState() {
|
|
32
|
+
super.initState();
|
|
33
|
+
// Set the flag on every entry (true for the admin preview, false for the
|
|
34
|
+
// real flow) so the notifier's side-effect guards are always correct. The
|
|
35
|
+
// first screen has no side effects, so a post-frame write is safe.
|
|
36
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
37
|
+
if (!mounted) return;
|
|
38
|
+
ref.read(onboardingProvider.notifier).setPreview(widget.preview);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Where the flow lands when finished: back to the admin Debug screen in
|
|
43
|
+
/// preview, Home otherwise.
|
|
44
|
+
void _exit() => ref
|
|
45
|
+
.read(goRouterProvider)
|
|
46
|
+
.go(widget.preview ? adminRouteDebug : '/');
|
|
18
47
|
|
|
19
48
|
@override
|
|
20
|
-
Widget build(BuildContext context
|
|
49
|
+
Widget build(BuildContext context) {
|
|
50
|
+
final bool preview = widget.preview;
|
|
21
51
|
return Navigator(
|
|
22
52
|
initialRoute: 'feature_1',
|
|
53
|
+
// No analytics in preview — walking the flow from Debug must not pollute
|
|
54
|
+
// real onboarding funnels.
|
|
23
55
|
observers: [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
56
|
+
if (!preview)
|
|
57
|
+
AnalyticsObserver(
|
|
58
|
+
prefix: 'userOnboarding/',
|
|
59
|
+
analyticsApi: MixpanelAnalyticsApi.instance(),
|
|
60
|
+
),
|
|
28
61
|
],
|
|
29
62
|
onGenerateRoute: (settings) => switch (settings.name) {
|
|
30
63
|
'feature_1' => OnboardingRouteTransition(
|
|
@@ -33,7 +66,11 @@ class OnboardingPage extends ConsumerWidget {
|
|
|
33
66
|
onSkip: () => Navigator.of(context).pushReplacementNamed(
|
|
34
67
|
'skip_loader',
|
|
35
68
|
),
|
|
36
|
-
|
|
69
|
+
// In preview, leaving to the real sign-in screen would break the
|
|
70
|
+
// "everything just returns to Debug" contract, so go back instead.
|
|
71
|
+
onLogin: () => preview
|
|
72
|
+
? _exit()
|
|
73
|
+
: ref.read(goRouterProvider).go('/signin'),
|
|
37
74
|
),
|
|
38
75
|
settings: settings,
|
|
39
76
|
),
|
|
@@ -79,9 +116,11 @@ class OnboardingPage extends ConsumerWidget {
|
|
|
79
116
|
),
|
|
80
117
|
'loader' => OnboardingRouteTransition(
|
|
81
118
|
builder: (context) => OnboardingLoader(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
119
|
+
// Preview stops here (the paywall has its own admin preview) and
|
|
120
|
+
// returns to Debug; the real flow continues to the paywall.
|
|
121
|
+
onCompleted: () => preview
|
|
122
|
+
? _exit()
|
|
123
|
+
: Navigator.of(context).pushReplacementNamed('paywall'),
|
|
85
124
|
),
|
|
86
125
|
settings: settings,
|
|
87
126
|
),
|
|
@@ -89,7 +128,7 @@ class OnboardingPage extends ConsumerWidget {
|
|
|
89
128
|
builder: (context) => OnboardingLoader(
|
|
90
129
|
onCompleted: () {
|
|
91
130
|
ref.onboardingNotifier.skipOnboarding();
|
|
92
|
-
|
|
131
|
+
_exit();
|
|
93
132
|
},
|
|
94
133
|
),
|
|
95
134
|
settings: settings,
|
|
@@ -73,22 +73,10 @@ class _OnboardingSelectableRowGroupState
|
|
|
73
73
|
return Column(
|
|
74
74
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
75
75
|
children: [
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
_SelectableTileInk(
|
|
77
|
+
onTap: selectRow,
|
|
78
78
|
borderRadius: KasyRadius.lgBorderRadius,
|
|
79
|
-
child:
|
|
80
|
-
color: Colors.transparent,
|
|
81
|
-
borderRadius: KasyRadius.lgBorderRadius,
|
|
82
|
-
child: InkWell(
|
|
83
|
-
canRequestFocus: false,
|
|
84
|
-
borderRadius: KasyRadius.lgBorderRadius,
|
|
85
|
-
onTap: selectRow,
|
|
86
|
-
child: switch (hasOnSelectInfo && _selectedIndex == index) {
|
|
87
|
-
false => widget.options[index],
|
|
88
|
-
true => widget.options[index],
|
|
89
|
-
},
|
|
90
|
-
),
|
|
91
|
-
),
|
|
79
|
+
child: widget.options[index],
|
|
92
80
|
),
|
|
93
81
|
if (hasOnSelectInfo && _selectedIndex == index)
|
|
94
82
|
Padding(
|
|
@@ -150,23 +138,16 @@ class _OnboardingMultiSelectableRowGroupState
|
|
|
150
138
|
setState(() {});
|
|
151
139
|
}
|
|
152
140
|
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
child: SelectableRowTile(
|
|
164
|
-
title: option.title,
|
|
165
|
-
subtitle: option.subtitle,
|
|
166
|
-
selected: _selectedIndex.contains(index),
|
|
167
|
-
emoj: option.emoj,
|
|
168
|
-
),
|
|
169
|
-
),
|
|
141
|
+
return _SelectableTileInk(
|
|
142
|
+
onTap: toggleSelection,
|
|
143
|
+
// Match the tile's own lgBorderRadius so the hover veil and focus ring
|
|
144
|
+
// hug its shape (the old InkWell clipped at md, a mismatch).
|
|
145
|
+
borderRadius: KasyRadius.lgBorderRadius,
|
|
146
|
+
child: SelectableRowTile(
|
|
147
|
+
title: option.title,
|
|
148
|
+
subtitle: option.subtitle,
|
|
149
|
+
selected: _selectedIndex.contains(index),
|
|
150
|
+
emoj: option.emoj,
|
|
170
151
|
),
|
|
171
152
|
);
|
|
172
153
|
}).toList(),
|
|
@@ -338,6 +319,83 @@ class _SelectableRowTileState extends State<SelectableRowTile>
|
|
|
338
319
|
}
|
|
339
320
|
}
|
|
340
321
|
|
|
322
|
+
/// Adds a web hover highlight, a keyboard focus ring and a click cursor on top
|
|
323
|
+
/// of an opaque selectable tile — no Material ripple.
|
|
324
|
+
///
|
|
325
|
+
/// Because the tile paints its own background, the hover veil is layered ABOVE
|
|
326
|
+
/// the child (clipped to [borderRadius]), the same approach the tappable
|
|
327
|
+
/// [KasyCard] uses; a [KasyHover]-style overlay would sit behind the opaque
|
|
328
|
+
/// surface and never show. The child keeps its own semantics (the option title),
|
|
329
|
+
/// so screen readers still announce the option, not a generic label.
|
|
330
|
+
class _SelectableTileInk extends StatefulWidget {
|
|
331
|
+
const _SelectableTileInk({
|
|
332
|
+
required this.onTap,
|
|
333
|
+
required this.borderRadius,
|
|
334
|
+
required this.child,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
final VoidCallback onTap;
|
|
338
|
+
final BorderRadius borderRadius;
|
|
339
|
+
final Widget child;
|
|
340
|
+
|
|
341
|
+
@override
|
|
342
|
+
State<_SelectableTileInk> createState() => _SelectableTileInkState();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
class _SelectableTileInkState extends State<_SelectableTileInk> {
|
|
346
|
+
bool _hovered = false;
|
|
347
|
+
bool _pressed = false;
|
|
348
|
+
|
|
349
|
+
@override
|
|
350
|
+
Widget build(BuildContext context) {
|
|
351
|
+
final bool dark = context.isDark;
|
|
352
|
+
// Subtle tint that fades in on hover (web/desktop) and deepens briefly on
|
|
353
|
+
// press; invisible at rest. On touch _hovered never becomes true.
|
|
354
|
+
final double alpha = _pressed
|
|
355
|
+
? (dark ? 0.10 : 0.06)
|
|
356
|
+
: (_hovered ? (dark ? 0.06 : 0.04) : 0.0);
|
|
357
|
+
|
|
358
|
+
final Widget veiled = Stack(
|
|
359
|
+
children: <Widget>[
|
|
360
|
+
widget.child,
|
|
361
|
+
Positioned.fill(
|
|
362
|
+
child: IgnorePointer(
|
|
363
|
+
child: AnimatedContainer(
|
|
364
|
+
duration: const Duration(milliseconds: 120),
|
|
365
|
+
curve: Curves.easeOut,
|
|
366
|
+
decoration: BoxDecoration(
|
|
367
|
+
color: context.colors.onSurface.withValues(alpha: alpha),
|
|
368
|
+
borderRadius: widget.borderRadius,
|
|
369
|
+
),
|
|
370
|
+
),
|
|
371
|
+
),
|
|
372
|
+
),
|
|
373
|
+
],
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
return KasyFocusRing(
|
|
377
|
+
onActivate: widget.onTap,
|
|
378
|
+
borderRadius: widget.borderRadius,
|
|
379
|
+
child: MouseRegion(
|
|
380
|
+
cursor: SystemMouseCursors.click,
|
|
381
|
+
onEnter: (_) => setState(() => _hovered = true),
|
|
382
|
+
onExit: (_) => setState(() {
|
|
383
|
+
_hovered = false;
|
|
384
|
+
_pressed = false;
|
|
385
|
+
}),
|
|
386
|
+
child: GestureDetector(
|
|
387
|
+
behavior: HitTestBehavior.opaque,
|
|
388
|
+
onTap: widget.onTap,
|
|
389
|
+
onTapDown: (_) => setState(() => _pressed = true),
|
|
390
|
+
onTapUp: (_) => setState(() => _pressed = false),
|
|
391
|
+
onTapCancel: () => setState(() => _pressed = false),
|
|
392
|
+
child: veiled,
|
|
393
|
+
),
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
341
399
|
class RoundRadioBox extends StatelessWidget {
|
|
342
400
|
final Color bgColor;
|
|
343
401
|
final Color borderColor;
|
|
@@ -152,6 +152,7 @@ class SettingsPage extends ConsumerWidget {
|
|
|
152
152
|
context,
|
|
153
153
|
ref,
|
|
154
154
|
isAuthenticated: isAuthenticated,
|
|
155
|
+
hasAccount: userId != null,
|
|
155
156
|
isAdmin: user.isAdmin,
|
|
156
157
|
isPhone: isPhone,
|
|
157
158
|
),
|
|
@@ -181,6 +182,7 @@ class SettingsPage extends ConsumerWidget {
|
|
|
181
182
|
BuildContext context,
|
|
182
183
|
WidgetRef ref, {
|
|
183
184
|
required bool isAuthenticated,
|
|
185
|
+
required bool hasAccount,
|
|
184
186
|
required bool isAdmin,
|
|
185
187
|
required bool isPhone,
|
|
186
188
|
}) {
|
|
@@ -214,7 +216,7 @@ class SettingsPage extends ConsumerWidget {
|
|
|
214
216
|
padding: const EdgeInsets.only(left: KasySpacing.xs),
|
|
215
217
|
child: Text(
|
|
216
218
|
t.admin_console.settings_entry.caption,
|
|
217
|
-
style: context.textTheme.
|
|
219
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
218
220
|
color: context.colors.muted,
|
|
219
221
|
),
|
|
220
222
|
),
|
|
@@ -225,8 +227,13 @@ class SettingsPage extends ConsumerWidget {
|
|
|
225
227
|
_settingsGroup([_LogoutRow(onTap: () => confirmLogout(context, ref))]),
|
|
226
228
|
const SizedBox(height: KasySpacing.xl),
|
|
227
229
|
],
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
// Only offer account deletion when there's a real backend account behind
|
|
231
|
+
// it. A guest with no identity has nothing to delete, so the button would
|
|
232
|
+
// just dead-end in an error and trap them on this screen.
|
|
233
|
+
if (hasAccount) ...[
|
|
234
|
+
const DeleteUserButton(),
|
|
235
|
+
const SizedBox(height: KasySpacing.xl),
|
|
236
|
+
],
|
|
230
237
|
const _VersionLabel(),
|
|
231
238
|
];
|
|
232
239
|
}
|
|
@@ -434,10 +441,9 @@ class SettingsContainer extends StatelessWidget {
|
|
|
434
441
|
Widget build(BuildContext context) {
|
|
435
442
|
return KasyCard(
|
|
436
443
|
borderRadius: BorderRadius.circular(KasyRadius.lg),
|
|
437
|
-
padding:
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
),
|
|
444
|
+
// No card padding: each row carries its own inset so the press/hover
|
|
445
|
+
// highlight spans the full card width (clipped to the rounded corners),
|
|
446
|
+
// instead of a pill floating inside a white margin.
|
|
441
447
|
child: child,
|
|
442
448
|
);
|
|
443
449
|
}
|
|
@@ -459,7 +465,7 @@ class _FieldRow extends StatelessWidget {
|
|
|
459
465
|
children: [
|
|
460
466
|
Text(
|
|
461
467
|
label,
|
|
462
|
-
style: context.
|
|
468
|
+
style: context.kasyTextTheme.listRowTitle.copyWith(
|
|
463
469
|
color: context.colors.onSurface,
|
|
464
470
|
),
|
|
465
471
|
),
|
|
@@ -470,7 +476,7 @@ class _FieldRow extends StatelessWidget {
|
|
|
470
476
|
textAlign: TextAlign.right,
|
|
471
477
|
maxLines: 1,
|
|
472
478
|
overflow: TextOverflow.ellipsis,
|
|
473
|
-
style: context.
|
|
479
|
+
style: context.kasyTextTheme.listRowValue.copyWith(
|
|
474
480
|
color: context.colors.muted,
|
|
475
481
|
),
|
|
476
482
|
),
|
|
@@ -483,18 +489,22 @@ class _FieldRow extends StatelessWidget {
|
|
|
483
489
|
);
|
|
484
490
|
if (onTap == null) {
|
|
485
491
|
return Padding(
|
|
486
|
-
padding: const EdgeInsets.symmetric(
|
|
492
|
+
padding: const EdgeInsets.symmetric(
|
|
493
|
+
horizontal: KasySpacing.md,
|
|
494
|
+
vertical: KasySpacing.smd,
|
|
495
|
+
),
|
|
487
496
|
child: row,
|
|
488
497
|
);
|
|
489
498
|
}
|
|
490
499
|
return KasyHover(
|
|
491
500
|
onTap: onTap!,
|
|
492
|
-
hoverEnabled: false,
|
|
493
|
-
pressEnabled: false,
|
|
494
501
|
focusable: true,
|
|
495
|
-
|
|
502
|
+
// Rectangular highlight (default): the card clips the rounded ends.
|
|
496
503
|
semanticLabel: label,
|
|
497
|
-
padding: const EdgeInsets.symmetric(
|
|
504
|
+
padding: const EdgeInsets.symmetric(
|
|
505
|
+
horizontal: KasySpacing.md,
|
|
506
|
+
vertical: KasySpacing.smd,
|
|
507
|
+
),
|
|
498
508
|
child: row,
|
|
499
509
|
);
|
|
500
510
|
}
|
|
@@ -511,7 +521,7 @@ class _AccountAvatarHeader extends StatelessWidget {
|
|
|
511
521
|
return const Center(
|
|
512
522
|
child: Padding(
|
|
513
523
|
padding: EdgeInsets.only(top: KasySpacing.sm),
|
|
514
|
-
child: EditableUserAvatar(diameter:
|
|
524
|
+
child: EditableUserAvatar(diameter: 80),
|
|
515
525
|
),
|
|
516
526
|
);
|
|
517
527
|
}
|
|
@@ -580,6 +590,8 @@ class _SettingsDesktopView extends ConsumerStatefulWidget {
|
|
|
580
590
|
class _SettingsDesktopViewState extends ConsumerState<_SettingsDesktopView> {
|
|
581
591
|
_DesktopSection _selected = _DesktopSection.account;
|
|
582
592
|
|
|
593
|
+
void _select(_DesktopSection s) => setState(() => _selected = s);
|
|
594
|
+
|
|
583
595
|
List<_DesktopSection> get _sections => <_DesktopSection>[
|
|
584
596
|
_DesktopSection.account,
|
|
585
597
|
_DesktopSection.preferences,
|
|
@@ -593,47 +605,58 @@ class _SettingsDesktopViewState extends ConsumerState<_SettingsDesktopView> {
|
|
|
593
605
|
final List<_DesktopSection> sections = _sections;
|
|
594
606
|
if (!sections.contains(_selected)) _selected = sections.first;
|
|
595
607
|
|
|
608
|
+
final Widget pane = _DesktopDetail(
|
|
609
|
+
section: _selected,
|
|
610
|
+
userId: widget.userId,
|
|
611
|
+
name: widget.name,
|
|
612
|
+
editableName: widget.editableName,
|
|
613
|
+
email: widget.email,
|
|
614
|
+
isAuthenticated: widget.isAuthenticated,
|
|
615
|
+
isPhone: widget.isPhone,
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
const double navWidth = 220;
|
|
619
|
+
const double gap = KasySpacing.xl;
|
|
620
|
+
const double detailWidth = 560;
|
|
621
|
+
const double groupWidth = navWidth + gap + detailWidth;
|
|
622
|
+
|
|
596
623
|
return Center(
|
|
597
|
-
child:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
child: _DesktopDetail(
|
|
624
|
-
section: _selected,
|
|
625
|
-
userId: widget.userId,
|
|
626
|
-
name: widget.name,
|
|
627
|
-
editableName: widget.editableName,
|
|
628
|
-
email: widget.email,
|
|
629
|
-
isAuthenticated: widget.isAuthenticated,
|
|
630
|
-
isPhone: widget.isPhone,
|
|
631
|
-
),
|
|
624
|
+
child: Padding(
|
|
625
|
+
padding: const EdgeInsets.only(
|
|
626
|
+
top: KasySpacing.lg,
|
|
627
|
+
bottom: KasySpacing.xxl,
|
|
628
|
+
),
|
|
629
|
+
// Center the nav + detail as ONE unit so the whitespace is equal on
|
|
630
|
+
// both sides (Linear/Notion settings), instead of pinning the nav left
|
|
631
|
+
// and leaving the right empty. Settings are forms, so the detail keeps
|
|
632
|
+
// a comfortable reading width rather than stretching wide. On tight
|
|
633
|
+
// desktop widths the detail fills the remaining space so it never
|
|
634
|
+
// overflows.
|
|
635
|
+
child: LayoutBuilder(
|
|
636
|
+
builder: (context, constraints) {
|
|
637
|
+
final bool fits = constraints.maxWidth >= groupWidth;
|
|
638
|
+
return Row(
|
|
639
|
+
mainAxisSize: fits ? MainAxisSize.min : MainAxisSize.max,
|
|
640
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
641
|
+
children: [
|
|
642
|
+
SizedBox(
|
|
643
|
+
width: navWidth,
|
|
644
|
+
child: _DesktopNav(
|
|
645
|
+
sections: sections,
|
|
646
|
+
selected: _selected,
|
|
647
|
+
name: widget.name,
|
|
648
|
+
email: widget.email,
|
|
649
|
+
onSelect: _select,
|
|
632
650
|
),
|
|
633
651
|
),
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
652
|
+
const SizedBox(width: gap),
|
|
653
|
+
if (fits)
|
|
654
|
+
SizedBox(width: detailWidth, child: pane)
|
|
655
|
+
else
|
|
656
|
+
Expanded(child: pane),
|
|
657
|
+
],
|
|
658
|
+
);
|
|
659
|
+
},
|
|
637
660
|
),
|
|
638
661
|
),
|
|
639
662
|
);
|
|
@@ -669,7 +692,7 @@ class _DesktopNav extends StatelessWidget {
|
|
|
669
692
|
),
|
|
670
693
|
child: Row(
|
|
671
694
|
children: [
|
|
672
|
-
const EditableUserAvatar(diameter:
|
|
695
|
+
const EditableUserAvatar(diameter: 64),
|
|
673
696
|
const SizedBox(width: KasySpacing.sm),
|
|
674
697
|
Expanded(
|
|
675
698
|
child: Column(
|
|
@@ -689,7 +712,7 @@ class _DesktopNav extends StatelessWidget {
|
|
|
689
712
|
email,
|
|
690
713
|
maxLines: 1,
|
|
691
714
|
overflow: TextOverflow.ellipsis,
|
|
692
|
-
style: context.textTheme.
|
|
715
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
693
716
|
color: context.colors.muted,
|
|
694
717
|
),
|
|
695
718
|
),
|
|
@@ -742,7 +765,7 @@ class _NavTile extends StatelessWidget {
|
|
|
742
765
|
child: Container(
|
|
743
766
|
padding: const EdgeInsets.symmetric(
|
|
744
767
|
horizontal: KasySpacing.smd,
|
|
745
|
-
vertical: KasySpacing.
|
|
768
|
+
vertical: KasySpacing.sm,
|
|
746
769
|
),
|
|
747
770
|
decoration: BoxDecoration(
|
|
748
771
|
color: selected ? c.surfaceNeutralSoft : Colors.transparent,
|
|
@@ -864,8 +887,11 @@ class _DesktopDetail extends ConsumerWidget {
|
|
|
864
887
|
const SizedBox(height: KasySpacing.xl),
|
|
865
888
|
_settingsGroup([_LogoutRow(onTap: () => confirmLogout(context, ref))]),
|
|
866
889
|
],
|
|
867
|
-
|
|
868
|
-
|
|
890
|
+
// Only when there's a real backend account to delete (see mobile layout).
|
|
891
|
+
if (userId != null) ...[
|
|
892
|
+
const SizedBox(height: KasySpacing.xl),
|
|
893
|
+
const DeleteUserButton(),
|
|
894
|
+
],
|
|
869
895
|
const SizedBox(height: KasySpacing.xl),
|
|
870
896
|
const _VersionLabel(),
|
|
871
897
|
];
|
|
@@ -881,13 +907,18 @@ class _LogoutRow extends StatelessWidget {
|
|
|
881
907
|
@override
|
|
882
908
|
Widget build(BuildContext context) {
|
|
883
909
|
return KasyHover(
|
|
884
|
-
|
|
910
|
+
// Match the card radius so the full-bleed press fill hugs the rounded
|
|
911
|
+
// corners exactly (this row is the sole child of its card).
|
|
912
|
+
borderRadius: KasyRadius.lgBorderRadius,
|
|
885
913
|
pressColor: context.colors.error,
|
|
886
914
|
focusable: true,
|
|
887
915
|
focusGapColor: context.colors.surface,
|
|
888
916
|
onTap: onTap,
|
|
889
917
|
child: Padding(
|
|
890
|
-
padding: const EdgeInsets.symmetric(
|
|
918
|
+
padding: const EdgeInsets.symmetric(
|
|
919
|
+
horizontal: KasySpacing.md,
|
|
920
|
+
vertical: KasySpacing.smd,
|
|
921
|
+
),
|
|
891
922
|
child: Row(
|
|
892
923
|
children: [
|
|
893
924
|
Icon(
|
|
@@ -898,7 +929,7 @@ class _LogoutRow extends StatelessWidget {
|
|
|
898
929
|
const SizedBox(width: KasySpacing.sm),
|
|
899
930
|
Text(
|
|
900
931
|
context.t.settings.logout,
|
|
901
|
-
style: context.
|
|
932
|
+
style: context.kasyTextTheme.listRowTitle.copyWith(
|
|
902
933
|
color: context.colors.error,
|
|
903
934
|
),
|
|
904
935
|
),
|
|
@@ -967,12 +998,12 @@ class BiometricSwitcher extends ConsumerWidget {
|
|
|
967
998
|
),
|
|
968
999
|
Padding(
|
|
969
1000
|
padding: const EdgeInsets.only(
|
|
970
|
-
left: KasyIconSize.rowLeading + KasySpacing.sm,
|
|
1001
|
+
left: KasySpacing.md + KasyIconSize.rowLeading + KasySpacing.sm,
|
|
971
1002
|
bottom: KasySpacing.xs,
|
|
972
1003
|
),
|
|
973
1004
|
child: Text(
|
|
974
1005
|
subtitle,
|
|
975
|
-
style: context.textTheme.
|
|
1006
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
976
1007
|
color: context.colors.muted,
|
|
977
1008
|
),
|
|
978
1009
|
),
|
|
@@ -1101,7 +1132,10 @@ class ThemeSwitcher extends StatelessWidget {
|
|
|
1101
1132
|
),
|
|
1102
1133
|
];
|
|
1103
1134
|
return Padding(
|
|
1104
|
-
padding: const EdgeInsets.symmetric(
|
|
1135
|
+
padding: const EdgeInsets.symmetric(
|
|
1136
|
+
horizontal: KasySpacing.md,
|
|
1137
|
+
vertical: KasySpacing.smd,
|
|
1138
|
+
),
|
|
1105
1139
|
child: Row(
|
|
1106
1140
|
children: [
|
|
1107
1141
|
Icon(
|
|
@@ -1113,7 +1147,7 @@ class ThemeSwitcher extends StatelessWidget {
|
|
|
1113
1147
|
Expanded(
|
|
1114
1148
|
child: Text(
|
|
1115
1149
|
tr.theme_title,
|
|
1116
|
-
style: context.
|
|
1150
|
+
style: context.kasyTextTheme.listRowTitle.copyWith(
|
|
1117
1151
|
color: context.colors.onSurface,
|
|
1118
1152
|
),
|
|
1119
1153
|
),
|