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.
Files changed (102) hide show
  1. package/lib/scaffold/CHANGELOG.json +14 -0
  2. package/lib/scaffold/backends/api/patch/README.md +15 -0
  3. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
  4. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
  5. package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
  6. package/lib/scaffold/backends/patch-base-hashes.json +6 -6
  7. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  8. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
  9. package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
  10. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
  11. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  12. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
  13. package/lib/scaffold/shared/generator-utils.js +12 -6
  14. package/package.json +1 -1
  15. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  16. package/templates/firebase/DESIGN_SYSTEM.md +22 -8
  17. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  18. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  19. package/templates/firebase/assets/icons/facebook.svg +49 -0
  20. package/templates/firebase/assets/icons/google.svg +1 -0
  21. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  22. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  23. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  24. package/templates/firebase/lib/components/components.dart +1 -1
  25. package/templates/firebase/lib/components/kasy_app_bar.dart +314 -14
  26. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  27. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  28. package/templates/firebase/lib/components/kasy_sidebar.dart +18 -6
  29. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  30. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  31. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +27 -18
  32. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -10
  33. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  34. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  35. package/templates/firebase/lib/core/data/api/user_api.dart +11 -0
  36. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  37. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +18 -35
  38. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  39. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  40. package/templates/firebase/lib/core/states/user_state_notifier.dart +28 -1
  41. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  42. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  43. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +51 -19
  44. package/templates/firebase/lib/core/web_viewport_scale.dart +64 -35
  45. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  46. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  47. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  48. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  49. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  50. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  51. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  52. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  53. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  54. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  55. package/templates/firebase/lib/features/home/home_components_page.dart +253 -125
  56. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +231 -57
  57. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  58. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  59. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  60. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +111 -57
  61. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  62. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -4
  63. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  64. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  65. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  66. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +2 -2
  67. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  68. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  69. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  70. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  71. package/templates/firebase/lib/features/settings/settings_page.dart +53 -32
  72. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +895 -111
  73. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  74. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  75. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +171 -41
  76. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +1 -1
  77. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  78. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +48 -47
  79. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  80. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  81. package/templates/firebase/lib/i18n/en.i18n.json +749 -712
  82. package/templates/firebase/lib/i18n/es.i18n.json +749 -712
  83. package/templates/firebase/lib/i18n/pt.i18n.json +749 -712
  84. package/templates/firebase/lib/main.dart +20 -7
  85. package/templates/firebase/lib/router.dart +32 -26
  86. package/templates/firebase/pubspec.yaml +2 -1
  87. package/templates/firebase/test/admin_shell_chrome_test.dart +11 -5
  88. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  89. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  90. package/templates/firebase/tool/design_check.dart +9 -0
  91. package/templates/firebase/assets/icons/apple.png +0 -0
  92. package/templates/firebase/assets/icons/facebook.png +0 -0
  93. package/templates/firebase/assets/icons/google.png +0 -0
  94. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  95. package/templates/firebase/lib/components/kasy_web_header.dart +0 -218
  96. package/templates/firebase/lib/core/chrome/web_header_scope.dart +0 -20
  97. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  98. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  99. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  100. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  101. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -179
  102. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
@@ -205,6 +205,12 @@ class _EmptyState extends StatelessWidget {
205
205
  }
206
206
  }
207
207
 
208
+ /// How much of the available row width a single bubble may occupy. Caps long
209
+ /// messages so they don't stretch edge-to-edge on wide layouts (desktop),
210
+ /// keeping the left/right chat rhythm like WhatsApp. Short messages still hug
211
+ /// their content via [Flexible].
212
+ const double _kMaxBubbleWidthFraction = 0.75;
213
+
208
214
  class _ChatBubble extends StatelessWidget {
209
215
  const _ChatBubble({required this.message, required this.isUser});
210
216
 
@@ -223,50 +229,61 @@ class _ChatBubble extends StatelessWidget {
223
229
  ? context.colors.primary
224
230
  : context.colors.onBackground.withValues(alpha: 0.06),
225
231
  borderRadius: BorderRadius.only(
226
- topLeft: const Radius.circular(16),
227
- topRight: const Radius.circular(16),
232
+ topLeft: const Radius.circular(KasyRadius.lg),
233
+ topRight: const Radius.circular(KasyRadius.lg),
228
234
  bottomLeft: isUser
229
- ? const Radius.circular(16)
230
- : const Radius.circular(4),
235
+ ? const Radius.circular(KasyRadius.lg)
236
+ : const Radius.circular(KasyRadius.xs),
231
237
  bottomRight: isUser
232
- ? const Radius.circular(4)
233
- : const Radius.circular(16),
238
+ ? const Radius.circular(KasyRadius.xs)
239
+ : const Radius.circular(KasyRadius.lg),
234
240
  ),
235
241
  ),
236
242
  child: Text(
237
243
  message.content,
238
- style: context.textTheme.bodyMedium?.copyWith(
244
+ style: context.textTheme.bodyLarge?.copyWith(
239
245
  color: isUser ? context.colors.onPrimary : context.colors.onBackground,
240
246
  height: 1.4,
241
247
  ),
242
248
  ),
243
249
  );
244
250
 
245
- if (isUser) {
246
- return Padding(
247
- padding: const EdgeInsets.only(bottom: KasySpacing.sm),
248
- child: Row(
249
- mainAxisAlignment: MainAxisAlignment.end,
250
- crossAxisAlignment: CrossAxisAlignment.end,
251
- children: [
252
- Flexible(child: bubble),
253
- const SizedBox(width: KasySpacing.xs),
254
- const AiChatUserAvatar(),
255
- ],
256
- ),
257
- );
258
- }
251
+ return LayoutBuilder(
252
+ builder: (context, constraints) {
253
+ final Widget cappedBubble = ConstrainedBox(
254
+ constraints: BoxConstraints(
255
+ maxWidth: constraints.maxWidth * _kMaxBubbleWidthFraction,
256
+ ),
257
+ child: bubble,
258
+ );
259
259
 
260
- return Padding(
261
- padding: const EdgeInsets.only(bottom: KasySpacing.sm),
262
- child: Row(
263
- crossAxisAlignment: CrossAxisAlignment.start,
264
- children: [
265
- const AiChatAssistantAvatar(),
266
- const SizedBox(width: KasySpacing.xs),
267
- Flexible(child: bubble),
268
- ],
269
- ),
260
+ if (isUser) {
261
+ return Padding(
262
+ padding: const EdgeInsets.only(bottom: KasySpacing.sm),
263
+ child: Row(
264
+ mainAxisAlignment: MainAxisAlignment.end,
265
+ crossAxisAlignment: CrossAxisAlignment.end,
266
+ children: [
267
+ Flexible(child: cappedBubble),
268
+ const SizedBox(width: KasySpacing.xs),
269
+ const AiChatUserAvatar(),
270
+ ],
271
+ ),
272
+ );
273
+ }
274
+
275
+ return Padding(
276
+ padding: const EdgeInsets.only(bottom: KasySpacing.sm),
277
+ child: Row(
278
+ crossAxisAlignment: CrossAxisAlignment.start,
279
+ children: [
280
+ const AiChatAssistantAvatar(),
281
+ const SizedBox(width: KasySpacing.xs),
282
+ Flexible(child: cappedBubble),
283
+ ],
284
+ ),
285
+ );
286
+ },
270
287
  );
271
288
  }
272
289
  }
@@ -327,10 +344,10 @@ class _TypingIndicatorState extends State<_TypingIndicator>
327
344
  decoration: BoxDecoration(
328
345
  color: context.colors.onBackground.withValues(alpha: 0.06),
329
346
  borderRadius: const BorderRadius.only(
330
- topLeft: Radius.circular(16),
331
- topRight: Radius.circular(16),
332
- bottomLeft: Radius.circular(4),
333
- bottomRight: Radius.circular(16),
347
+ topLeft: Radius.circular(KasyRadius.lg),
348
+ topRight: Radius.circular(KasyRadius.lg),
349
+ bottomLeft: Radius.circular(KasyRadius.xs),
350
+ bottomRight: Radius.circular(KasyRadius.lg),
334
351
  ),
335
352
  ),
336
353
  child: Row(
@@ -126,7 +126,7 @@ class AiConversationList extends ConsumerWidget {
126
126
  padding: const EdgeInsets.symmetric(horizontal: KasySpacing.lg),
127
127
  decoration: BoxDecoration(
128
128
  color: context.colors.surfaceErrorSoft,
129
- borderRadius: BorderRadius.circular(16),
129
+ borderRadius: BorderRadius.circular(KasyRadius.lg),
130
130
  ),
131
131
  child: Icon(KasyIcons.trash, color: context.colors.error),
132
132
  );
@@ -57,7 +57,7 @@ class _AiConversationTileState extends ConsumerState<AiConversationTile> {
57
57
  color: widget.selected
58
58
  ? context.colors.accentSoft
59
59
  : (_hovered ? context.colors.surfaceNeutralSoft : null),
60
- borderRadius: BorderRadius.circular(16),
60
+ borderRadius: BorderRadius.circular(KasyRadius.lg),
61
61
  ),
62
62
  child: Row(
63
63
  crossAxisAlignment: CrossAxisAlignment.start,
@@ -75,7 +75,7 @@ class _AiConversationTileState extends ConsumerState<AiConversationTile> {
75
75
  title,
76
76
  maxLines: 1,
77
77
  overflow: TextOverflow.ellipsis,
78
- style: context.kasyTextTheme.rowTitle.copyWith(
78
+ style: context.kasyTextTheme.listRowTitle.copyWith(
79
79
  color: titleColor,
80
80
  ),
81
81
  ),
@@ -89,7 +89,7 @@ class _AiConversationTileState extends ConsumerState<AiConversationTile> {
89
89
  preview,
90
90
  maxLines: 1,
91
91
  overflow: TextOverflow.ellipsis,
92
- style: context.textTheme.bodySmall?.copyWith(
92
+ style: context.textTheme.bodyMedium?.copyWith(
93
93
  color: subtitleColor,
94
94
  ),
95
95
  ),
@@ -106,7 +106,7 @@ class _AiConversationTileState extends ConsumerState<AiConversationTile> {
106
106
  label: title,
107
107
  child: KasyFocusRing(
108
108
  onActivate: widget.onTap,
109
- borderRadius: BorderRadius.circular(16),
109
+ borderRadius: BorderRadius.circular(KasyRadius.lg),
110
110
  child: GestureDetector(
111
111
  behavior: HitTestBehavior.opaque,
112
112
  onTap: widget.onTap,
@@ -129,10 +129,20 @@ class _AiConversationTileState extends ConsumerState<AiConversationTile> {
129
129
  if (kIsWeb && _hovered) {
130
130
  return SizedBox(
131
131
  height: 16,
132
- child: InkResponse(
133
- onTap: widget.onDelete,
134
- radius: 18,
135
- child: Icon(KasyIcons.trash, size: KasyIconSize.sm, color: context.colors.error),
132
+ // Plain pointer-cursor tap (no Material ripple), consistent with the
133
+ // rest of the kit's web controls. The icon only appears while the row
134
+ // is hovered, so it's already a pointer-only affordance.
135
+ child: MouseRegion(
136
+ cursor: SystemMouseCursors.click,
137
+ child: GestureDetector(
138
+ behavior: HitTestBehavior.opaque,
139
+ onTap: widget.onDelete,
140
+ child: Icon(
141
+ KasyIcons.trash,
142
+ size: KasyIconSize.sm,
143
+ color: context.colors.error,
144
+ ),
145
+ ),
136
146
  ),
137
147
  );
138
148
  }
@@ -8,10 +8,8 @@ import 'package:kasy_kit/components/components.dart';
8
8
  import 'package:kasy_kit/core/bottom_menu/web_url.dart';
9
9
  import 'package:kasy_kit/core/config/features.dart';
10
10
  import 'package:kasy_kit/core/data/models/user.dart';
11
- import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
12
11
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
13
12
  import 'package:kasy_kit/core/theme/theme.dart';
14
- import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
15
13
  import 'package:kasy_kit/core/widgets/kasy_pressable_depth.dart';
16
14
  import 'package:kasy_kit/environments.dart';
17
15
  import 'package:kasy_kit/features/authentication/providers/models/email.dart';
@@ -20,6 +18,7 @@ import 'package:kasy_kit/features/authentication/providers/models/signin_state.d
20
18
  import 'package:kasy_kit/features/authentication/providers/signin_state_provider.dart';
21
19
  import 'package:kasy_kit/features/authentication/ui/widgets/auth_card_scaffold.dart';
22
20
  import 'package:kasy_kit/features/authentication/ui/widgets/auth_page_back_button.dart';
21
+ import 'package:kasy_kit/features/authentication/ui/widgets/social_auth_tile.dart';
23
22
  import 'package:kasy_kit/features/authentication/ui/widgets/social_separator.dart';
24
23
  import 'package:kasy_kit/i18n/translations.g.dart';
25
24
 
@@ -323,9 +322,9 @@ class _SocialSigninRow extends ConsumerWidget {
323
322
  return Row(
324
323
  children: [
325
324
  Expanded(
326
- child: _SocialSigninTile(
325
+ child: SocialAuthButton(
327
326
  label: t.auth.signin.google,
328
- icon: Image.asset('assets/icons/google.png', width: 20, height: 20),
327
+ iconAsset: 'assets/icons/google.svg',
329
328
  onPressed: isSending
330
329
  ? null
331
330
  : () =>
@@ -335,9 +334,13 @@ class _SocialSigninRow extends ConsumerWidget {
335
334
  if (showApple) ...[
336
335
  const SizedBox(width: KasySpacing.sm),
337
336
  Expanded(
338
- child: _SocialSigninTile(
337
+ child: SocialAuthButton(
339
338
  label: t.auth.signin.apple,
340
- icon: Image.asset('assets/icons/apple.png', width: 20, height: 20),
339
+ // Apple's mark is officially black-on-light / white-on-dark, so
340
+ // pick the variant that stays visible on the current theme.
341
+ iconAsset: context.isDark
342
+ ? 'assets/icons/apple_white.svg'
343
+ : 'assets/icons/apple_black.svg',
341
344
  onPressed: isSending
342
345
  ? null
343
346
  : () => ref
@@ -349,13 +352,9 @@ class _SocialSigninRow extends ConsumerWidget {
349
352
  if (showFacebook) ...[
350
353
  const SizedBox(width: KasySpacing.sm),
351
354
  Expanded(
352
- child: _SocialSigninTile(
355
+ child: SocialAuthButton(
353
356
  label: t.auth.signin.facebook,
354
- icon: Image.asset(
355
- 'assets/icons/facebook.png',
356
- width: 20,
357
- height: 20,
358
- ),
357
+ iconAsset: 'assets/icons/facebook.svg',
359
358
  onPressed: isSending
360
359
  ? null
361
360
  : () => ref
@@ -369,52 +368,3 @@ class _SocialSigninRow extends ConsumerWidget {
369
368
  }
370
369
  }
371
370
 
372
- class _SocialSigninTile extends StatelessWidget {
373
- const _SocialSigninTile({
374
- required this.label,
375
- required this.icon,
376
- required this.onPressed,
377
- });
378
-
379
- final String label;
380
- final Widget icon;
381
- final VoidCallback? onPressed;
382
-
383
- @override
384
- Widget build(BuildContext context) {
385
- final bool enabled = onPressed != null;
386
- void handleTap() {
387
- KasyHaptics.medium(context);
388
- onPressed?.call();
389
- }
390
- return KasyFocusRing(
391
- enabled: enabled,
392
- onActivate: handleTap,
393
- borderRadius: BorderRadius.circular(KasyRadius.sm),
394
- child: Material(
395
- color: Colors.transparent,
396
- child: InkWell(
397
- // Focus + keyboard activation live in KasyFocusRing; the InkWell keeps
398
- // its tap ripple but doesn't take focus, so the ring is the only Tab
399
- // stop and matches every other button's focus outline.
400
- canRequestFocus: false,
401
- onTap: enabled ? handleTap : null,
402
- borderRadius: BorderRadius.circular(KasyRadius.md),
403
- child: Ink(
404
- height: 44,
405
- decoration: BoxDecoration(
406
- color: context.colors.surface,
407
- borderRadius: BorderRadius.circular(KasyRadius.sm),
408
- border: Border.all(
409
- color: context.colors.outline.withValues(alpha: 0.38),
410
- ),
411
- ),
412
- child: Center(
413
- child: Semantics(button: true, label: label, child: icon),
414
- ),
415
- ),
416
- ),
417
- ),
418
- );
419
- }
420
- }
@@ -7,10 +7,8 @@ import 'package:go_router/go_router.dart';
7
7
  import 'package:kasy_kit/components/components.dart';
8
8
  import 'package:kasy_kit/core/config/features.dart';
9
9
  import 'package:kasy_kit/core/data/models/user.dart';
10
- import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
11
10
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
12
11
  import 'package:kasy_kit/core/theme/theme.dart';
13
- import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
14
12
  import 'package:kasy_kit/core/widgets/kasy_pressable_depth.dart';
15
13
  import 'package:kasy_kit/features/authentication/providers/models/email.dart';
16
14
  import 'package:kasy_kit/features/authentication/providers/models/password.dart';
@@ -20,6 +18,7 @@ import 'package:kasy_kit/features/authentication/providers/signin_state_provider
20
18
  import 'package:kasy_kit/features/authentication/providers/signup_state_provider.dart';
21
19
  import 'package:kasy_kit/features/authentication/ui/widgets/auth_card_scaffold.dart';
22
20
  import 'package:kasy_kit/features/authentication/ui/widgets/auth_page_back_button.dart';
21
+ import 'package:kasy_kit/features/authentication/ui/widgets/social_auth_tile.dart';
23
22
  import 'package:kasy_kit/features/authentication/ui/widgets/social_separator.dart';
24
23
  import 'package:kasy_kit/i18n/translations.g.dart';
25
24
 
@@ -223,9 +222,9 @@ class _SocialSignupRow extends ConsumerWidget {
223
222
  return Row(
224
223
  children: [
225
224
  Expanded(
226
- child: _SocialSignupTile(
225
+ child: SocialAuthButton(
227
226
  label: t.auth.signin.google,
228
- icon: Image.asset('assets/icons/google.png', width: 20, height: 20),
227
+ iconAsset: 'assets/icons/google.svg',
229
228
  onPressed: isSending
230
229
  ? null
231
230
  : () =>
@@ -235,9 +234,13 @@ class _SocialSignupRow extends ConsumerWidget {
235
234
  if (showApple) ...[
236
235
  const SizedBox(width: KasySpacing.sm),
237
236
  Expanded(
238
- child: _SocialSignupTile(
237
+ child: SocialAuthButton(
239
238
  label: t.auth.signin.apple,
240
- icon: Image.asset('assets/icons/apple.png', width: 20, height: 20),
239
+ // Apple's mark is officially black-on-light / white-on-dark, so
240
+ // pick the variant that stays visible on the current theme.
241
+ iconAsset: context.isDark
242
+ ? 'assets/icons/apple_white.svg'
243
+ : 'assets/icons/apple_black.svg',
241
244
  onPressed: isSending
242
245
  ? null
243
246
  : () => ref
@@ -249,13 +252,9 @@ class _SocialSignupRow extends ConsumerWidget {
249
252
  if (showFacebook) ...[
250
253
  const SizedBox(width: KasySpacing.sm),
251
254
  Expanded(
252
- child: _SocialSignupTile(
255
+ child: SocialAuthButton(
253
256
  label: t.auth.signin.facebook,
254
- icon: Image.asset(
255
- 'assets/icons/facebook.png',
256
- width: 20,
257
- height: 20,
258
- ),
257
+ iconAsset: 'assets/icons/facebook.svg',
259
258
  onPressed: isSending
260
259
  ? null
261
260
  : () => ref
@@ -269,52 +268,3 @@ class _SocialSignupRow extends ConsumerWidget {
269
268
  }
270
269
  }
271
270
 
272
- class _SocialSignupTile extends StatelessWidget {
273
- const _SocialSignupTile({
274
- required this.label,
275
- required this.icon,
276
- required this.onPressed,
277
- });
278
-
279
- final String label;
280
- final Widget icon;
281
- final VoidCallback? onPressed;
282
-
283
- @override
284
- Widget build(BuildContext context) {
285
- final bool enabled = onPressed != null;
286
- void handleTap() {
287
- KasyHaptics.medium(context);
288
- onPressed?.call();
289
- }
290
- return KasyFocusRing(
291
- enabled: enabled,
292
- onActivate: handleTap,
293
- borderRadius: BorderRadius.circular(KasyRadius.sm),
294
- child: Material(
295
- color: Colors.transparent,
296
- child: InkWell(
297
- // Focus + keyboard activation live in KasyFocusRing; the InkWell keeps
298
- // its tap ripple but doesn't take focus, so the ring is the only Tab
299
- // stop and matches every other button's focus outline.
300
- canRequestFocus: false,
301
- onTap: enabled ? handleTap : null,
302
- borderRadius: BorderRadius.circular(KasyRadius.md),
303
- child: Ink(
304
- height: 44,
305
- decoration: BoxDecoration(
306
- color: context.colors.surface,
307
- borderRadius: BorderRadius.circular(KasyRadius.sm),
308
- border: Border.all(
309
- color: context.colors.outline.withValues(alpha: 0.38),
310
- ),
311
- ),
312
- child: Center(
313
- child: Semantics(button: true, label: label, child: icon),
314
- ),
315
- ),
316
- ),
317
- ),
318
- );
319
- }
320
- }
@@ -25,7 +25,7 @@ class AuthCardScaffold extends StatelessWidget {
25
25
  required this.subtitle,
26
26
  required this.children,
27
27
  this.showLogo = true,
28
- this.logoHeight = 96,
28
+ this.logoHeight = 132,
29
29
  this.maxContentWidth = 420,
30
30
  });
31
31
 
@@ -74,7 +74,7 @@ class AuthCardScaffold extends StatelessWidget {
74
74
  curve: Curves.easeOut,
75
75
  ),
76
76
  ),
77
- const SizedBox(height: KasySpacing.lg),
77
+ const SizedBox(height: KasySpacing.sm),
78
78
  ],
79
79
  Text(
80
80
  title,
@@ -111,9 +111,11 @@ class AuthCardScaffold extends StatelessWidget {
111
111
  child: isMobile
112
112
  ? content
113
113
  : KasyCard(
114
- padding: const EdgeInsets.symmetric(
115
- horizontal: KasySpacing.lg,
116
- vertical: KasySpacing.xl,
114
+ padding: const EdgeInsets.fromLTRB(
115
+ KasySpacing.lg,
116
+ KasySpacing.md,
117
+ KasySpacing.lg,
118
+ KasySpacing.xl,
117
119
  ),
118
120
  child: content,
119
121
  ),
@@ -0,0 +1,83 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter_svg/flutter_svg.dart';
3
+ import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
4
+ import 'package:kasy_kit/core/theme/theme.dart';
5
+ import 'package:kasy_kit/core/widgets/kasy_hover.dart';
6
+
7
+ /// Outlined social sign-in button (Google / Apple / Facebook), shared by the
8
+ /// sign-in and sign-up screens.
9
+ ///
10
+ /// Each tile fills its slot in the social row (the parent wraps it in an
11
+ /// [Expanded]) and centers the provider glyph. It uses [KasyHover] so it has a
12
+ /// real hover highlight on web/desktop and a press fill on every platform — no
13
+ /// Material ripple, matching the rest of the kit's interactive controls. While a
14
+ /// request is in flight ([onPressed] null) it dims and stops responding to
15
+ /// pointer and keyboard.
16
+ class SocialAuthButton extends StatelessWidget {
17
+ const SocialAuthButton({
18
+ super.key,
19
+ required this.label,
20
+ required this.iconAsset,
21
+ required this.onPressed,
22
+ });
23
+
24
+ /// Accessibility label (the provider name) announced to screen readers.
25
+ final String label;
26
+
27
+ /// Path to the provider's official brand SVG bundled under `assets/icons/`
28
+ /// (e.g. `assets/icons/google.svg`). Rendered at full brand colour and at a
29
+ /// shared [KasyIconSize.lg] box so every provider stays the same size. For
30
+ /// Apple, the caller picks the black/white variant for the current theme.
31
+ final String iconAsset;
32
+
33
+ /// Tap handler. When null the button is disabled (dimmed, inert).
34
+ final VoidCallback? onPressed;
35
+
36
+ @override
37
+ Widget build(BuildContext context) {
38
+ final bool enabled = onPressed != null;
39
+ final BorderRadius radius = BorderRadius.circular(KasyRadius.sm);
40
+
41
+ // No background fill: the KasyHover overlay sits behind the transparent
42
+ // interior to provide the hover/press tint, and on the surface auth card a
43
+ // fill would be invisible anyway. The hairline border gives the outlined
44
+ // shape, consistent with KasyButton's outlined variant.
45
+ final Widget surface = Container(
46
+ height: 44,
47
+ decoration: BoxDecoration(
48
+ borderRadius: radius,
49
+ border: Border.all(
50
+ color: context.colors.outline.withValues(alpha: 0.38),
51
+ ),
52
+ ),
53
+ child: Center(
54
+ // Brand logos have different aspect ratios (Apple is tall, Google is
55
+ // square); SvgPicture defaults to BoxFit.contain, which keeps each one
56
+ // inside the shared box without distortion so the row stays balanced.
57
+ child: SvgPicture.asset(
58
+ iconAsset,
59
+ width: KasyIconSize.lg,
60
+ height: KasyIconSize.lg,
61
+ ),
62
+ ),
63
+ );
64
+
65
+ if (!enabled) {
66
+ return Opacity(opacity: 0.5, child: surface);
67
+ }
68
+
69
+ return KasyHover(
70
+ onTap: () {
71
+ KasyHaptics.medium(context);
72
+ onPressed!();
73
+ },
74
+ // KasyHover fires a light selection haptic by default; a social sign-in is
75
+ // a significant action, so opt out and fire the heavier "medium" above.
76
+ hapticEnabled: false,
77
+ focusable: true,
78
+ borderRadius: radius,
79
+ semanticLabel: label,
80
+ child: surface,
81
+ );
82
+ }
83
+ }
@@ -59,15 +59,15 @@ class _FeatureCardState extends State<FeatureCard> {
59
59
  children: [
60
60
  Text(
61
61
  widget.title,
62
- style: context.textTheme.titleMedium?.copyWith(
63
- fontWeight: FontWeight.w700,
62
+ style: context.kasyTextTheme.listRowTitle.copyWith(
63
+ color: context.colors.onSurface,
64
+ fontWeight: FontWeight.w600,
64
65
  ),
65
66
  ),
66
67
  const SizedBox(height: 2),
67
68
  Text(
68
69
  widget.description,
69
- style: context.textTheme.bodySmall?.copyWith(
70
- fontWeight: FontWeight.w400,
70
+ style: context.textTheme.bodyMedium?.copyWith(
71
71
  color: context.colors.muted,
72
72
  ),
73
73
  ),