kasy-cli 1.31.14 → 1.34.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.
Files changed (127) hide show
  1. package/bin/kasy.js +42 -0
  2. package/lib/commands/apple-web.js +222 -0
  3. package/lib/commands/configure.js +3 -91
  4. package/lib/commands/doctor.js +20 -0
  5. package/lib/commands/facebook.js +189 -0
  6. package/lib/commands/new.js +65 -3
  7. package/lib/scaffold/CHANGELOG.json +27 -0
  8. package/lib/scaffold/backends/api/patch/README.md +87 -2
  9. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +34 -0
  10. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  11. package/lib/scaffold/backends/firebase/setup-from-scratch.js +186 -0
  12. package/lib/scaffold/backends/supabase/deploy.js +92 -0
  13. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
  14. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
  15. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
  16. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
  17. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +22 -0
  18. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  19. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +3 -2
  20. package/lib/scaffold/generate.js +1 -1
  21. package/lib/scaffold/shared/generator-utils.js +34 -3
  22. package/lib/utils/apple-web.js +147 -0
  23. package/lib/utils/facebook.js +162 -0
  24. package/lib/utils/i18n/messages-en.js +64 -0
  25. package/lib/utils/i18n/messages-es.js +64 -0
  26. package/lib/utils/i18n/messages-pt.js +64 -0
  27. package/package.json +2 -2
  28. package/templates/firebase/AGENTS.md +87 -0
  29. package/templates/firebase/CLAUDE.md +16 -0
  30. package/templates/firebase/DESIGN_SYSTEM.md +234 -0
  31. package/templates/firebase/docs/auth-setup.en.md +7 -1
  32. package/templates/firebase/docs/auth-setup.es.md +7 -1
  33. package/templates/firebase/docs/auth-setup.pt.md +7 -1
  34. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
  35. package/templates/firebase/lib/components/components.dart +1 -0
  36. package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
  37. package/templates/firebase/lib/components/kasy_alert.dart +1 -1
  38. package/templates/firebase/lib/components/kasy_app_bar.dart +7 -4
  39. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
  40. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  41. package/templates/firebase/lib/components/kasy_chip.dart +1 -1
  42. package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
  43. package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
  44. package/templates/firebase/lib/components/kasy_screen.dart +114 -0
  45. package/templates/firebase/lib/components/kasy_sidebar.dart +2 -2
  46. package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
  47. package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
  48. package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
  49. package/templates/firebase/lib/components/kasy_toast.dart +39 -70
  50. package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
  51. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
  52. package/templates/firebase/lib/core/config/features.dart +18 -0
  53. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
  54. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
  55. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +46 -124
  56. package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
  57. package/templates/firebase/lib/core/theme/shadows.dart +13 -0
  58. package/templates/firebase/lib/core/theme/texts.dart +32 -0
  59. package/templates/firebase/lib/core/theme/theme.dart +2 -0
  60. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
  61. package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
  62. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
  63. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -7
  64. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
  65. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  66. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
  67. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +61 -0
  68. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
  69. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
  70. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
  71. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +57 -29
  72. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +47 -25
  73. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
  74. package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
  75. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
  76. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +2 -3
  77. package/templates/firebase/lib/features/home/home_components_page.dart +7 -1
  78. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +54 -3
  79. package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
  80. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +165 -209
  81. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
  82. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
  83. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
  84. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -6
  85. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
  86. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +104 -156
  87. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
  88. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
  89. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
  90. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
  91. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
  92. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +3 -2
  93. package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
  94. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +17 -8
  95. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +4 -4
  96. package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
  97. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
  98. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
  99. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
  100. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  101. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
  102. package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
  103. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
  104. package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
  105. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
  106. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
  107. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
  108. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
  109. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
  110. package/templates/firebase/lib/i18n/en.i18n.json +13 -4
  111. package/templates/firebase/lib/i18n/es.i18n.json +13 -4
  112. package/templates/firebase/lib/i18n/pt.i18n.json +13 -4
  113. package/templates/firebase/lib/router.dart +2 -0
  114. package/templates/firebase/pubspec.yaml +1 -2
  115. package/templates/firebase/tool/design_check.dart +152 -0
  116. package/templates/firebase/web/stripe_success.html +64 -26
  117. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  118. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  119. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  120. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  121. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  122. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  123. package/templates/firebase/assets/images/review.png +0 -0
  124. package/templates/firebase/assets/images/update.png +0 -0
  125. package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
  126. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
  127. package/templates/firebase/login-redesign-preview.png +0 -0
@@ -13,10 +13,10 @@ import 'package:kasy_kit/core/security/biometric_ui_bundle.dart';
13
13
  import 'package:kasy_kit/core/states/logout_action.dart';
14
14
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
15
15
  import 'package:kasy_kit/core/theme/theme.dart';
16
- import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
17
16
  import 'package:kasy_kit/core/widgets/kasy_hover.dart';
18
17
  import 'package:kasy_kit/features/settings/ui/components/avatar_component.dart';
19
18
  import 'package:kasy_kit/features/settings/ui/components/delete_user_component.dart';
19
+ import 'package:kasy_kit/features/settings/ui/components/edit_name_sheet.dart';
20
20
  import 'package:kasy_kit/features/settings/ui/components/language_switcher.dart';
21
21
  import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
22
22
  import 'package:kasy_kit/i18n/translations.g.dart';
@@ -31,6 +31,7 @@ class SettingsPage extends ConsumerWidget {
31
31
  final tr = context.t.settings;
32
32
  final user = ref.watch(userStateNotifierProvider).user;
33
33
  final isAuthenticated = user is AuthenticatedUserData;
34
+ final String? userId = user.idOrNull;
34
35
  final (displayName, displayEmail) = switch (user) {
35
36
  final AuthenticatedUserData u => (
36
37
  (u.name?.isNotEmpty ?? false) ? u.name! : u.email.split('@').first,
@@ -38,41 +39,43 @@ class SettingsPage extends ConsumerWidget {
38
39
  ),
39
40
  _ => (tr.my_account, ''),
40
41
  };
42
+ // The real stored name (may be empty) — what the editor seeds with, as
43
+ // opposed to the email-prefix fallback shown for display.
44
+ final String editableName = switch (user) {
45
+ final AuthenticatedUserData u => u.name ?? '',
46
+ _ => '',
47
+ };
48
+
49
+ final double width = MediaQuery.sizeOf(context).width;
50
+ // Two-pane master/detail (Vercel/Stripe/Claude) spans the whole desktop
51
+ // range and only collapses to a single column on tablet/phone.
52
+ final bool wide = width >= 1024;
53
+ // The "hide bars on scroll" toggle only makes sense on phones — tablet and
54
+ // desktop use the sidebar, which never hides.
55
+ final bool isPhone = width < 768;
41
56
 
42
57
  return KasyOverlayScaffold(
43
58
  title: tr.title,
44
59
  appBarStyle: KasyAppBarStyle.rootTab,
45
60
  hideAppBarOnScroll: true,
46
- trailing: Builder(
47
- builder: (ctx) => KasyChromeOrbIconButton(
48
- icon: KasyIcons.logout,
49
- iconSize: 20,
50
- foregroundColor: ctx.colors.error,
51
- onPressed: () => confirmLogout(ctx, ref),
52
- ),
53
- ),
54
61
  slivers: [
55
62
  SliverToBoxAdapter(
56
63
  child: Builder(
57
64
  builder: (context) {
58
- // Breakpoint by viewport width — the industry standard and the
59
- // project's own `large` cutoff (1024px ≈ Tailwind lg). Two-pane
60
- // spans the whole desktop range and only collapses to a single
61
- // column when entering tablet territory (the layout phones and
62
- // tablets already use), instead of switching while still wide.
63
- final bool wide = MediaQuery.sizeOf(context).width >= 1024;
64
-
65
65
  if (!wide) {
66
66
  return Column(
67
67
  crossAxisAlignment: CrossAxisAlignment.stretch,
68
68
  children: [
69
- ProfileTile(
70
- title: displayName,
71
- subtitle: displayEmail,
69
+ const _AccountAvatarHeader(),
70
+ const SizedBox(height: KasySpacing.md),
71
+ ..._accountFields(
72
+ context,
73
+ userId: userId,
74
+ name: displayName,
75
+ editableName: editableName,
76
+ email: displayEmail,
72
77
  isAuthenticated: isAuthenticated,
73
- onTap: isAuthenticated
74
- ? null
75
- : () => context.push('/signup'),
78
+ onRegister: () => context.push('/signup'),
76
79
  ),
77
80
  const SizedBox(height: KasySpacing.xl),
78
81
  ..._sections(
@@ -80,19 +83,21 @@ class SettingsPage extends ConsumerWidget {
80
83
  ref,
81
84
  isAuthenticated: isAuthenticated,
82
85
  isAdmin: user.isAdmin,
86
+ isPhone: isPhone,
83
87
  ),
84
88
  const SizedBox(height: KasySpacing.xl),
85
89
  ],
86
90
  );
87
91
  }
88
92
 
89
- // Desktop: a SaaS-style master/detail (Vercel/Stripe/Claude) —
90
- // a section nav on the left, the selected section on the right.
91
93
  return _SettingsDesktopView(
94
+ userId: userId,
92
95
  name: displayName,
96
+ editableName: editableName,
93
97
  email: displayEmail,
94
98
  isAuthenticated: isAuthenticated,
95
99
  isAdmin: user.isAdmin,
100
+ isPhone: isPhone,
96
101
  );
97
102
  },
98
103
  ),
@@ -101,95 +106,40 @@ class SettingsPage extends ConsumerWidget {
101
106
  );
102
107
  }
103
108
 
104
- /// The settings sections (everything except the profile header), shared
105
- /// between the single-column (mobile/tablet) and two-pane (desktop) layouts.
109
+ /// The settings sections below the account block shared by the single
110
+ /// column (mobile/tablet) layout. Ends with sign-out, delete and version.
106
111
  List<Widget> _sections(
107
112
  BuildContext context,
108
113
  WidgetRef ref, {
109
114
  required bool isAuthenticated,
110
115
  required bool isAdmin,
116
+ required bool isPhone,
111
117
  }) {
112
118
  final tr = context.t.settings;
113
119
  return [
114
120
  _SectionLabel(tr.section_preferences_label),
115
121
  const SizedBox(height: KasySpacing.xs),
116
- SettingsContainer(
117
- child: Wrap(
118
- children: [
119
- const ThemeSwitcher(),
120
- const SettingsDivider(),
121
- const HapticFeedbackSwitcher(),
122
- const SettingsDivider(),
123
- if (kShowHideChromeOnScrollSetting) ...[
124
- const HideChromeOnScrollSwitcher(),
125
- const SettingsDivider(),
126
- ],
127
- const LanguageSwitcher(),
128
- if (withLocalReminders) ...[
129
- const SettingsDivider(),
130
- SettingsTile(
131
- icon: KasyIcons.notification,
132
- title: tr.reminders,
133
- onTap: () => context.push('/reminder'),
134
- ),
135
- ],
136
- if (withFeedback) ...[
137
- const SettingsDivider(),
138
- SettingsTile(
139
- icon: KasyIcons.message,
140
- title: tr.feedback,
141
- onTap: () => context.push('/feedback'),
142
- ),
143
- ],
144
- if (withRevenuecat) ...[
145
- const SettingsDivider(),
146
- SettingsTile(
147
- icon: KasyIcons.payment,
148
- title: tr.premium,
149
- onTap: () => context.push('/premium'),
150
- ),
151
- ],
152
- ],
153
- ),
154
- ),
122
+ _settingsGroup(_preferenceRows(context, isPhone: isPhone)),
155
123
  if (isAuthenticated && !kIsWeb) ...[
156
124
  const SizedBox(height: KasySpacing.xl),
157
125
  _SectionLabel(tr.section_security_label),
158
126
  const SizedBox(height: KasySpacing.xs),
159
- const SettingsContainer(
160
- child: BiometricSwitcher(),
161
- ),
127
+ const SettingsContainer(child: BiometricSwitcher()),
162
128
  ],
163
129
  const SizedBox(height: KasySpacing.xl),
164
130
  _SectionLabel(tr.section_support_label),
165
131
  const SizedBox(height: KasySpacing.xs),
166
- SettingsContainer(
167
- child: Wrap(
168
- children: [
169
- SettingsTile(
170
- icon: KasyIcons.privacy,
171
- title: tr.privacy,
172
- onTap: () => launchUrl(Uri.parse('https://kasy.dev/privacy/')),
173
- ),
174
- const SettingsDivider(),
175
- SettingsTile(
176
- icon: KasyIcons.help,
177
- title: tr.support,
178
- onTap: () => launchUrl(Uri.parse('https://kasy.dev/')),
179
- ),
180
- ],
181
- ),
182
- ),
132
+ _settingsGroup(_supportRows(context)),
183
133
  // Admin entry — only for administrators or in development mode.
184
134
  if (isAdmin || kDebugMode) ...[
185
135
  const SizedBox(height: KasySpacing.xl),
186
- SettingsContainer(
187
- child: SettingsTile(
136
+ _settingsGroup([
137
+ SettingsTile(
188
138
  icon: Icons.admin_panel_settings_outlined,
189
139
  title: t.admin_console.settings_entry.title,
190
140
  onTap: () => context.push('/admin'),
191
141
  ),
192
- ),
142
+ ]),
193
143
  const SizedBox(height: KasySpacing.sm),
194
144
  Padding(
195
145
  padding: const EdgeInsets.only(left: KasySpacing.xs),
@@ -202,6 +152,10 @@ class SettingsPage extends ConsumerWidget {
202
152
  ),
203
153
  ],
204
154
  const SizedBox(height: KasySpacing.xxl),
155
+ if (isAuthenticated) ...[
156
+ _settingsGroup([_LogoutRow(onTap: () => confirmLogout(context, ref))]),
157
+ const SizedBox(height: KasySpacing.xl),
158
+ ],
205
159
  const DeleteUserButton(),
206
160
  const SizedBox(height: KasySpacing.xl),
207
161
  const _VersionLabel(),
@@ -209,24 +163,132 @@ class SettingsPage extends ConsumerWidget {
209
163
  }
210
164
  }
211
165
 
166
+ // ─── Shared building blocks ────────────────────────────────────────────────
167
+
168
+ /// Wraps a list of settings rows in a refined card, inserting hairline
169
+ /// dividers between them. The card matches the design-system elevated surface
170
+ /// (soft shadow + hairline border) instead of the old flat block.
171
+ Widget _settingsGroup(List<Widget> rows) {
172
+ return SettingsContainer(
173
+ child: Column(
174
+ crossAxisAlignment: CrossAxisAlignment.stretch,
175
+ children: [
176
+ for (int i = 0; i < rows.length; i++) ...[
177
+ if (i > 0) const SettingsDivider(),
178
+ rows[i],
179
+ ],
180
+ ],
181
+ ),
182
+ );
183
+ }
184
+
185
+ /// The Preferences rows, shared between the mobile and desktop layouts.
186
+ /// Platform-aware: haptics only on native, hide-on-scroll only on phones.
187
+ List<Widget> _preferenceRows(BuildContext context, {required bool isPhone}) {
188
+ final tr = context.t.settings;
189
+ return [
190
+ const ThemeSwitcher(),
191
+ if (!kIsWeb) const HapticFeedbackSwitcher(),
192
+ if (kShowHideChromeOnScrollSetting && isPhone)
193
+ const HideChromeOnScrollSwitcher(),
194
+ const LanguageSwitcher(),
195
+ if (withLocalReminders)
196
+ SettingsTile(
197
+ icon: KasyIcons.notification,
198
+ title: tr.reminders,
199
+ onTap: () => context.push('/reminder'),
200
+ ),
201
+ if (withFeedback)
202
+ SettingsTile(
203
+ icon: KasyIcons.message,
204
+ title: tr.feedback,
205
+ onTap: () => context.push('/feedback'),
206
+ ),
207
+ if (withRevenuecat)
208
+ SettingsTile(
209
+ icon: KasyIcons.payment,
210
+ title: tr.premium,
211
+ onTap: () => context.push('/premium'),
212
+ ),
213
+ ];
214
+ }
215
+
216
+ /// The Support rows, shared between the mobile and desktop layouts.
217
+ List<Widget> _supportRows(BuildContext context) {
218
+ final tr = context.t.settings;
219
+ return [
220
+ SettingsTile(
221
+ icon: KasyIcons.privacy,
222
+ title: tr.privacy,
223
+ onTap: () => launchUrl(Uri.parse('https://kasy.dev/privacy/')),
224
+ ),
225
+ SettingsTile(
226
+ icon: KasyIcons.help,
227
+ title: tr.support,
228
+ onTap: () => launchUrl(Uri.parse('https://kasy.dev/')),
229
+ ),
230
+ ];
231
+ }
232
+
233
+ /// The account identity fields, shared between the mobile and desktop layouts.
234
+ /// Signed-in users get an editable Name row and a read-only Email row; guests
235
+ /// get a single Register call to action.
236
+ List<Widget> _accountFields(
237
+ BuildContext context, {
238
+ required String? userId,
239
+ required String name,
240
+ required String editableName,
241
+ required String email,
242
+ required bool isAuthenticated,
243
+ required VoidCallback onRegister,
244
+ }) {
245
+ final tr = context.t.settings;
246
+ if (!isAuthenticated) {
247
+ return [
248
+ KasyButton(label: tr.register, expand: true, onPressed: onRegister),
249
+ ];
250
+ }
251
+ return [
252
+ _settingsGroup([
253
+ _FieldRow(
254
+ label: tr.name_label,
255
+ value: name,
256
+ onTap: userId == null
257
+ ? null
258
+ : () => showEditNameSheet(
259
+ context,
260
+ userId: userId,
261
+ email: email,
262
+ currentName: editableName,
263
+ ),
264
+ ),
265
+ _FieldRow(label: tr.email_label, value: email),
266
+ ]),
267
+ ];
268
+ }
269
+
212
270
  class _SectionLabel extends StatelessWidget {
213
271
  final String label;
214
272
  const _SectionLabel(this.label);
215
273
 
216
274
  @override
217
275
  Widget build(BuildContext context) {
218
- return Text(
219
- label,
220
- style: context.textTheme.labelMedium?.copyWith(
221
- fontSize: 13,
222
- color: context.colors.muted,
223
- letterSpacing: 1.2,
224
- fontWeight: FontWeight.w600,
276
+ // Quieter than before and aligned with the sidebar's section labels: small,
277
+ // gently tracked, muted — so it never out-shouts the content it heads.
278
+ return Padding(
279
+ padding: const EdgeInsets.only(left: KasySpacing.xs),
280
+ child: Text(
281
+ label,
282
+ style: KasyTextTheme.sectionLabel.copyWith(
283
+ color: context.colors.muted,
284
+ ),
225
285
  ),
226
286
  );
227
287
  }
228
288
  }
229
289
 
290
+ /// Grouped surface for settings rows — the design-system elevated card with a
291
+ /// settings-density radius.
230
292
  class SettingsContainer extends StatelessWidget {
231
293
  final Widget child;
232
294
 
@@ -234,121 +296,86 @@ class SettingsContainer extends StatelessWidget {
234
296
 
235
297
  @override
236
298
  Widget build(BuildContext context) {
237
- return Container(
299
+ return KasyCard(
300
+ borderRadius: BorderRadius.circular(KasyRadius.lg),
238
301
  padding: const EdgeInsets.symmetric(
239
- vertical: KasySpacing.smd,
240
302
  horizontal: KasySpacing.md,
241
- ),
242
- decoration: BoxDecoration(
243
- borderRadius: KasyRadius.smBorderRadius,
244
- color: context.colors.surface,
303
+ vertical: KasySpacing.xs,
245
304
  ),
246
305
  child: child,
247
306
  );
248
307
  }
249
308
  }
250
309
 
251
- class ProfileTile extends StatelessWidget {
252
- final String title;
253
- final String subtitle;
254
- final bool isAuthenticated;
310
+ /// A label + value row used in the Account section. With [onTap] it is an
311
+ /// editable field (trailing chevron, keyboard-focusable); without it the value
312
+ /// is read-only (e.g. the email).
313
+ class _FieldRow extends StatelessWidget {
314
+ final String label;
315
+ final String value;
255
316
  final VoidCallback? onTap;
256
317
 
257
- const ProfileTile({
258
- super.key,
259
- required this.title,
260
- required this.subtitle,
261
- required this.isAuthenticated,
262
- this.onTap,
263
- });
318
+ const _FieldRow({required this.label, required this.value, this.onTap});
264
319
 
265
320
  @override
266
321
  Widget build(BuildContext context) {
267
- final Widget content = Padding(
268
- padding: const EdgeInsets.symmetric(
269
- vertical: KasySpacing.smd,
270
- horizontal: KasySpacing.md,
271
- ),
272
- child: Row(
273
- children: [
274
- const EditableUserAvatar(
275
- diameter: 70,
322
+ final Widget row = Row(
323
+ children: [
324
+ Text(
325
+ label,
326
+ style: context.textTheme.titleSmall?.copyWith(
327
+ color: context.colors.onSurface,
276
328
  ),
277
- const SizedBox(width: KasySpacing.sm),
278
- Expanded(
279
- child: Column(
280
- crossAxisAlignment: CrossAxisAlignment.start,
281
- mainAxisAlignment: MainAxisAlignment.center,
282
- mainAxisSize: MainAxisSize.min,
283
- children: [
284
- Text(
285
- title,
286
- textHeightBehavior: const TextHeightBehavior(
287
- applyHeightToFirstAscent: false,
288
- applyHeightToLastDescent: false,
289
- ),
290
- style: context.textTheme.titleMedium!.copyWith(
291
- color: context.colors.onSurface,
292
- fontWeight: FontWeight.w600,
293
- height: 1.12,
294
- ),
295
- ),
296
- if (subtitle.isNotEmpty) ...[
297
- Text(
298
- subtitle,
299
- textHeightBehavior: const TextHeightBehavior(
300
- applyHeightToFirstAscent: false,
301
- applyHeightToLastDescent: false,
302
- ),
303
- style: context.textTheme.bodySmall!.copyWith(
304
- color: context.colors.muted,
305
- height: 1.15,
306
- ),
307
- ),
308
- ],
309
- if (!isAuthenticated) ...[
310
- Text(
311
- context.t.settings.register,
312
- textHeightBehavior: const TextHeightBehavior(
313
- applyHeightToFirstAscent: false,
314
- applyHeightToLastDescent: false,
315
- ),
316
- style: context.textTheme.bodyLarge!.copyWith(
317
- color: context.colors.muted,
318
- fontWeight: FontWeight.w500,
319
- height: 1.15,
320
- ),
321
- ),
322
- ],
323
- ],
329
+ ),
330
+ const SizedBox(width: KasySpacing.md),
331
+ Expanded(
332
+ child: Text(
333
+ value,
334
+ textAlign: TextAlign.right,
335
+ maxLines: 1,
336
+ overflow: TextOverflow.ellipsis,
337
+ style: context.textTheme.bodyMedium?.copyWith(
338
+ color: context.colors.muted,
324
339
  ),
325
340
  ),
326
- if (onTap != null) const SettingsListChevron(),
341
+ ),
342
+ if (onTap != null) ...[
343
+ const SizedBox(width: KasySpacing.xs),
344
+ const SettingsListChevron(),
327
345
  ],
328
- ),
346
+ ],
347
+ );
348
+ if (onTap == null) {
349
+ return Padding(
350
+ padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
351
+ child: row,
352
+ );
353
+ }
354
+ return KasyHover(
355
+ onTap: onTap!,
356
+ hoverEnabled: false,
357
+ pressEnabled: false,
358
+ focusable: true,
359
+ borderRadius: KasyRadius.smBorderRadius,
360
+ semanticLabel: label,
361
+ padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
362
+ child: row,
329
363
  );
364
+ }
365
+ }
330
366
 
331
- return Container(
332
- decoration: BoxDecoration(
333
- borderRadius: KasyRadius.smBorderRadius,
334
- color: context.colors.surface,
335
- ),
336
- child: ClipRRect(
337
- borderRadius: KasyRadius.smBorderRadius,
338
- child: onTap != null
339
- ? KasyFocusRing(
340
- onActivate: onTap,
341
- borderRadius: KasyRadius.smBorderRadius,
342
- gapColor: context.colors.surface,
343
- // The InkWell keeps its tap ripple but yields focus to the ring,
344
- // so the keyboard outline matches every other Kasy control.
345
- child: InkWell(
346
- canRequestFocus: false,
347
- onTap: onTap,
348
- child: content,
349
- ),
350
- )
351
- : content,
367
+ /// The avatar header shown at the top of the Account block on the single-column
368
+ /// (mobile/tablet) layout. Tapping the avatar changes the photo; identity text
369
+ /// lives in the editable fields below, so nothing is shown twice.
370
+ class _AccountAvatarHeader extends StatelessWidget {
371
+ const _AccountAvatarHeader();
372
+
373
+ @override
374
+ Widget build(BuildContext context) {
375
+ return const Center(
376
+ child: Padding(
377
+ padding: EdgeInsets.only(top: KasySpacing.sm),
378
+ child: EditableUserAvatar(diameter: 64),
352
379
  ),
353
380
  );
354
381
  }
@@ -391,16 +418,22 @@ String _titleCase(String s) =>
391
418
  }
392
419
 
393
420
  class _SettingsDesktopView extends ConsumerStatefulWidget {
421
+ final String? userId;
394
422
  final String name;
423
+ final String editableName;
395
424
  final String email;
396
425
  final bool isAuthenticated;
397
426
  final bool isAdmin;
427
+ final bool isPhone;
398
428
 
399
429
  const _SettingsDesktopView({
430
+ required this.userId,
400
431
  required this.name,
432
+ required this.editableName,
401
433
  required this.email,
402
434
  required this.isAuthenticated,
403
435
  required this.isAdmin,
436
+ required this.isPhone,
404
437
  });
405
438
 
406
439
  @override
@@ -450,12 +483,15 @@ class _SettingsDesktopViewState extends ConsumerState<_SettingsDesktopView> {
450
483
  child: Align(
451
484
  alignment: Alignment.topLeft,
452
485
  child: ConstrainedBox(
453
- constraints: const BoxConstraints(maxWidth: 720),
486
+ constraints: const BoxConstraints(maxWidth: 600),
454
487
  child: _DesktopDetail(
455
488
  section: _selected,
489
+ userId: widget.userId,
456
490
  name: widget.name,
491
+ editableName: widget.editableName,
457
492
  email: widget.email,
458
493
  isAuthenticated: widget.isAuthenticated,
494
+ isPhone: widget.isPhone,
459
495
  ),
460
496
  ),
461
497
  ),
@@ -579,7 +615,7 @@ class _NavTile extends StatelessWidget {
579
615
  ),
580
616
  child: Row(
581
617
  children: [
582
- Icon(icon, size: 20, color: fg),
618
+ Icon(icon, size: KasyIconSize.rowLeading, color: fg),
583
619
  const SizedBox(width: KasySpacing.sm),
584
620
  Text(
585
621
  label,
@@ -597,15 +633,21 @@ class _NavTile extends StatelessWidget {
597
633
 
598
634
  class _DesktopDetail extends ConsumerWidget {
599
635
  final _DesktopSection section;
636
+ final String? userId;
600
637
  final String name;
638
+ final String editableName;
601
639
  final String email;
602
640
  final bool isAuthenticated;
641
+ final bool isPhone;
603
642
 
604
643
  const _DesktopDetail({
605
644
  required this.section,
645
+ required this.userId,
606
646
  required this.name,
647
+ required this.editableName,
607
648
  required this.email,
608
649
  required this.isAuthenticated,
650
+ required this.isPhone,
609
651
  });
610
652
 
611
653
  @override
@@ -622,9 +664,8 @@ class _DesktopDetail extends ConsumerWidget {
622
664
  ),
623
665
  child: Text(
624
666
  title,
625
- style: context.textTheme.headlineSmall?.copyWith(
667
+ style: KasyTextTheme.sectionTitle.copyWith(
626
668
  color: context.colors.onSurface,
627
- fontWeight: FontWeight.w700,
628
669
  ),
629
670
  ),
630
671
  ),
@@ -634,86 +675,26 @@ class _DesktopDetail extends ConsumerWidget {
634
675
  }
635
676
 
636
677
  List<Widget> _content(BuildContext context, WidgetRef ref) {
637
- final tr = context.t.settings;
638
678
  switch (section) {
639
679
  case _DesktopSection.account:
640
680
  return _accountContent(context, ref);
641
681
  case _DesktopSection.preferences:
642
- return [
643
- SettingsContainer(
644
- child: Wrap(
645
- children: [
646
- const ThemeSwitcher(),
647
- const SettingsDivider(),
648
- const HapticFeedbackSwitcher(),
649
- const SettingsDivider(),
650
- if (kShowHideChromeOnScrollSetting) ...[
651
- const HideChromeOnScrollSwitcher(),
652
- const SettingsDivider(),
653
- ],
654
- const LanguageSwitcher(),
655
- if (withLocalReminders) ...[
656
- const SettingsDivider(),
657
- SettingsTile(
658
- icon: KasyIcons.notification,
659
- title: tr.reminders,
660
- onTap: () => context.push('/reminder'),
661
- ),
662
- ],
663
- if (withFeedback) ...[
664
- const SettingsDivider(),
665
- SettingsTile(
666
- icon: KasyIcons.message,
667
- title: tr.feedback,
668
- onTap: () => context.push('/feedback'),
669
- ),
670
- ],
671
- if (withRevenuecat) ...[
672
- const SettingsDivider(),
673
- SettingsTile(
674
- icon: KasyIcons.payment,
675
- title: tr.premium,
676
- onTap: () => context.push('/premium'),
677
- ),
678
- ],
679
- ],
680
- ),
681
- ),
682
- ];
682
+ return [_settingsGroup(_preferenceRows(context, isPhone: isPhone))];
683
683
  case _DesktopSection.security:
684
684
  return const [
685
685
  SettingsContainer(child: BiometricSwitcher()),
686
686
  ];
687
687
  case _DesktopSection.support:
688
- return [
689
- SettingsContainer(
690
- child: Wrap(
691
- children: [
692
- SettingsTile(
693
- icon: KasyIcons.privacy,
694
- title: tr.privacy,
695
- onTap: () =>
696
- launchUrl(Uri.parse('https://kasy.dev/privacy/')),
697
- ),
698
- const SettingsDivider(),
699
- SettingsTile(
700
- icon: KasyIcons.help,
701
- title: tr.support,
702
- onTap: () => launchUrl(Uri.parse('https://kasy.dev/')),
703
- ),
704
- ],
705
- ),
706
- ),
707
- ];
688
+ return [_settingsGroup(_supportRows(context))];
708
689
  case _DesktopSection.admin:
709
690
  return [
710
- SettingsContainer(
711
- child: SettingsTile(
691
+ _settingsGroup([
692
+ SettingsTile(
712
693
  icon: Icons.admin_panel_settings_outlined,
713
694
  title: t.admin_console.settings_entry.title,
714
695
  onTap: () => context.push('/admin'),
715
696
  ),
716
- ),
697
+ ]),
717
698
  const SizedBox(height: KasySpacing.sm),
718
699
  Padding(
719
700
  padding: const EdgeInsets.only(left: KasySpacing.xs),
@@ -729,51 +710,19 @@ class _DesktopDetail extends ConsumerWidget {
729
710
  }
730
711
 
731
712
  List<Widget> _accountContent(BuildContext context, WidgetRef ref) {
732
- final tr = context.t.settings;
733
713
  return [
734
- SettingsContainer(
735
- child: Padding(
736
- padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
737
- child: Row(
738
- children: [
739
- const EditableUserAvatar(diameter: 56),
740
- const SizedBox(width: KasySpacing.md),
741
- Expanded(
742
- child: Column(
743
- crossAxisAlignment: CrossAxisAlignment.start,
744
- mainAxisSize: MainAxisSize.min,
745
- children: [
746
- Text(
747
- name,
748
- style: context.textTheme.titleMedium?.copyWith(
749
- color: context.colors.onSurface,
750
- fontWeight: FontWeight.w600,
751
- ),
752
- ),
753
- if (email.isNotEmpty)
754
- Text(
755
- email,
756
- style: context.textTheme.bodySmall?.copyWith(
757
- color: context.colors.muted,
758
- ),
759
- ),
760
- ],
761
- ),
762
- ),
763
- if (!isAuthenticated)
764
- KasyButton(
765
- label: tr.register,
766
- onPressed: () => context.push('/signup'),
767
- ),
768
- ],
769
- ),
770
- ),
714
+ ..._accountFields(
715
+ context,
716
+ userId: userId,
717
+ name: name,
718
+ editableName: editableName,
719
+ email: email,
720
+ isAuthenticated: isAuthenticated,
721
+ onRegister: () => context.push('/signup'),
771
722
  ),
772
723
  if (isAuthenticated) ...[
773
724
  const SizedBox(height: KasySpacing.xl),
774
- SettingsContainer(
775
- child: _LogoutRow(onTap: () => confirmLogout(context, ref)),
776
- ),
725
+ _settingsGroup([_LogoutRow(onTap: () => confirmLogout(context, ref))]),
777
726
  ],
778
727
  const SizedBox(height: KasySpacing.xl),
779
728
  const DeleteUserButton(),
@@ -783,7 +732,7 @@ class _DesktopDetail extends ConsumerWidget {
783
732
  }
784
733
  }
785
734
 
786
- /// A danger-tinted "Sign out" row used in the desktop Account section.
735
+ /// A danger-tinted "Sign out" row used in the Account section.
787
736
  class _LogoutRow extends StatelessWidget {
788
737
  final VoidCallback onTap;
789
738
 
@@ -801,11 +750,15 @@ class _LogoutRow extends StatelessWidget {
801
750
  padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
802
751
  child: Row(
803
752
  children: [
804
- Icon(KasyIcons.logout, size: 21, color: context.colors.error),
753
+ Icon(
754
+ KasyIcons.logout,
755
+ size: KasyIconSize.rowLeading,
756
+ color: context.colors.error,
757
+ ),
805
758
  const SizedBox(width: KasySpacing.sm),
806
759
  Text(
807
760
  context.t.settings.logout,
808
- style: context.textTheme.titleMedium?.copyWith(
761
+ style: context.textTheme.titleSmall?.copyWith(
809
762
  color: context.colors.error,
810
763
  ),
811
764
  ),
@@ -874,7 +827,7 @@ class BiometricSwitcher extends ConsumerWidget {
874
827
  ),
875
828
  Padding(
876
829
  padding: const EdgeInsets.only(
877
- left: 21 + KasySpacing.sm,
830
+ left: KasyIconSize.rowLeading + KasySpacing.sm,
878
831
  bottom: KasySpacing.xs,
879
832
  ),
880
833
  child: Text(
@@ -1011,12 +964,16 @@ class ThemeSwitcher extends StatelessWidget {
1011
964
  padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
1012
965
  child: Row(
1013
966
  children: [
1014
- Icon(KasyIcons.palette, size: 21, color: context.colors.onSurface),
967
+ Icon(
968
+ KasyIcons.palette,
969
+ size: KasyIconSize.rowLeading,
970
+ color: context.colors.onSurface,
971
+ ),
1015
972
  const SizedBox(width: KasySpacing.sm),
1016
973
  Expanded(
1017
974
  child: Text(
1018
975
  tr.theme_title,
1019
- style: context.textTheme.titleMedium?.copyWith(
976
+ style: context.textTheme.titleSmall?.copyWith(
1020
977
  color: context.colors.onSurface,
1021
978
  ),
1022
979
  ),