kasy-cli 1.38.0 → 1.39.1

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 (105) hide show
  1. package/lib/scaffold/CHANGELOG.json +23 -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/AGENTS.md +2 -2
  17. package/templates/firebase/DESIGN_SYSTEM.md +23 -8
  18. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  19. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  20. package/templates/firebase/assets/icons/facebook.svg +49 -0
  21. package/templates/firebase/assets/icons/google.svg +1 -0
  22. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  23. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  24. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  25. package/templates/firebase/lib/components/components.dart +5 -2
  26. package/templates/firebase/lib/components/kasy_app_bar.dart +325 -15
  27. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  28. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  29. package/templates/firebase/lib/components/kasy_sidebar.dart +18 -6
  30. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  31. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  32. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +27 -18
  33. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +34 -16
  34. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  35. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  36. package/templates/firebase/lib/core/data/api/user_api.dart +11 -0
  37. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  38. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +95 -30
  39. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  40. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  41. package/templates/firebase/lib/core/states/user_state_notifier.dart +28 -1
  42. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  43. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  44. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +51 -19
  45. package/templates/firebase/lib/core/web_viewport_scale.dart +66 -36
  46. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  47. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  48. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  49. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  50. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  51. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  52. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  53. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  54. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  55. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  56. package/templates/firebase/lib/features/home/home_components_page.dart +253 -125
  57. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +263 -59
  58. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  59. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  60. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  61. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +111 -57
  62. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  63. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -4
  64. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  65. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  66. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  67. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +2 -2
  68. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  69. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  70. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  71. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  72. package/templates/firebase/lib/features/settings/settings_page.dart +53 -32
  73. package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart +4 -0
  74. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +895 -111
  75. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  76. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  77. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +171 -41
  78. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +1 -1
  79. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  80. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +48 -47
  81. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  82. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  83. package/templates/firebase/lib/i18n/en.i18n.json +753 -712
  84. package/templates/firebase/lib/i18n/es.i18n.json +753 -712
  85. package/templates/firebase/lib/i18n/pt.i18n.json +753 -712
  86. package/templates/firebase/lib/main.dart +20 -7
  87. package/templates/firebase/lib/router.dart +32 -26
  88. package/templates/firebase/pubspec.yaml +2 -1
  89. package/templates/firebase/test/admin_shell_chrome_test.dart +11 -5
  90. package/templates/firebase/test/app_bar_config_test.dart +70 -0
  91. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  92. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  93. package/templates/firebase/tool/design_check.dart +9 -0
  94. package/templates/firebase/assets/icons/apple.png +0 -0
  95. package/templates/firebase/assets/icons/facebook.png +0 -0
  96. package/templates/firebase/assets/icons/google.png +0 -0
  97. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  98. package/templates/firebase/lib/components/kasy_web_header.dart +0 -218
  99. package/templates/firebase/lib/core/chrome/web_header_scope.dart +0 -20
  100. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  101. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  102. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  103. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  104. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -179
  105. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
@@ -2,27 +2,50 @@ import 'package:flutter/foundation.dart' show kIsWeb;
2
2
  import 'package:flutter/widgets.dart';
3
3
  import 'package:kasy_kit/core/web_screen_width.dart';
4
4
 
5
- /// Maximum render scale applied to the app on web (desktop breakpoint only).
5
+ /// Render scale applied to the app on web, on EVERY breakpoint (phone, tablet,
6
+ /// desktop).
6
7
  ///
7
- /// Flutter web tends to render ~10% larger than equivalent HTML apps at the
8
- /// browser's 100% zoom, so the whole UI feels oversized on desktop. `0.95`
9
- /// brings it to the proportion the design targets (i.e. what 95% zoom looked
10
- /// like) without the user having to touch the browser zoom. It acts as the cap:
11
- /// a desktop whose logical width is below the design target (high OS scale)
12
- /// reduces it further to pin the layout (see [kWebViewportScaleTargetWidth]).
8
+ /// Flutter web renders ~10% larger than an equivalent native/HTML app at the
9
+ /// browser's 100% zoom, at any width — so the whole web UI feels oversized.
10
+ /// `0.93` walks back ~7% so the web app reads close to the native baseline
11
+ /// without the user touching browser zoom a gentle correction (a bit stronger
12
+ /// than the midpoint between full size and a total undo, but still short of
13
+ /// fully undoing the 10%) that takes the "oversized" edge off Flutter web
14
+ /// without making things feel small (0.90 fully undoes the 10% but reads small
15
+ /// on a monitor — one number to nudge). On desktop it also acts
16
+ /// as a cap: a
17
+ /// screen below the design target (high OS scale) reduces it further to pin the
18
+ /// layout (see [kWebViewportScaleTargetWidth]); phone/tablet take the flat cap
19
+ /// because those layouts reflow.
13
20
  ///
14
- /// Mobile and tablet web are deliberately left at 1.0 same as native and as the
15
- /// device preview, so a developer previewing a phone/tablet sees exactly what the
16
- /// native build renders. NATIVE is never scaled: the mechanism is gated on
17
- /// [kIsWeb], so iOS/Android/macOS/Windows apps render at 1.0 and keep respecting
18
- /// the user's system text-size (accessibility).
19
- const double kWebViewportScale = 0.95;
21
+ /// Two things are deliberately NOT scaled, both rendering at native 1.0:
22
+ /// - NATIVE itself the mechanism is gated on [kIsWeb], so iOS/Android/macOS/
23
+ /// Windows render at 1.0 and keep respecting the user's system text-size
24
+ /// (accessibility).
25
+ /// - The in-app DEVICE PREVIEW — it simulates a native device, so it must show
26
+ /// the native 1.0 truth; the scale is skipped once the preview frame is up
27
+ /// (see main.dart, gated on `webDevicePreviewActiveNotifier`).
28
+ const double kWebViewportScale = 0.93;
29
+
30
+ /// Master on/off for the web render scale — the single knob.
31
+ ///
32
+ /// `true` (default): the web app is rendered ~7% smaller than native via
33
+ /// [kWebViewportScale], correcting Flutter web's oversized feel on desktop while
34
+ /// native stays at true 1.0. This is a deliberate, web-only density correction
35
+ /// (the same technique `responsive_framework`'s autoScale productizes), applied
36
+ /// ON TOP OF an already-adaptive layout — not a substitute for it. We verified
37
+ /// nothing overflows/crops without it, so it is a density choice, not a crutch.
38
+ ///
39
+ /// `false`: [WebViewportScale.wrap] becomes a true no-op everywhere and the web
40
+ /// app renders at native 1.0. There is no hidden coupling, so this one flag is
41
+ /// the entire on/off — flip it (or set [kWebViewportScale] to `1.0`) to opt out.
42
+ const bool kWebViewportScaleEnabled = true;
20
43
 
21
44
  /// Design target width (logical px) the desktop shell is laid out against.
22
45
  ///
23
46
  /// A display with high OS scaling (Windows at 125/150/175%, or a Mac in a scaled
24
47
  /// "more space"/"larger text" mode) reports a smaller logical SCREEN width, so the
25
- /// `0.95` baseline alone left the shell cramped/cropped (the user had to
48
+ /// flat scale alone left the shell cramped/cropped (the user had to
26
49
  /// Ctrl-minus). When the screen is below this target the scale drops further
27
50
  /// (`screenWidth / kWebViewportScaleTargetWidth`) so the full design still fits.
28
51
  /// Compared against the SCREEN width, not the window width — see
@@ -39,34 +62,37 @@ const double kWebViewportScaleDesktopBreakpoint = 1024; // DeviceType.large.brea
39
62
  /// Effective web render scale (pure math, unit-testable — see
40
63
  /// web_viewport_scale_test.dart).
41
64
  ///
42
- /// [windowWidth] is the browser window width (drives the desktop breakpoint, since
43
- /// the layout follows the window). [screenWidth] is the physical screen width in
44
- /// logical px (null = unknown/native).
65
+ /// [windowWidth] is the browser window width. [screenWidth] is the physical
66
+ /// screen width in logical px (null = unknown/native).
45
67
  ///
46
- /// Returns 1.0 below the desktop breakpoint ([kWebViewportScaleDesktopBreakpoint]):
47
- /// tablet/phone web render at natural size same as native and the device preview,
48
- /// so previewing a phone/tablet shows what the native build does.
68
+ /// On tablet/phone web ([windowWidth] below [kWebViewportScaleDesktopBreakpoint])
69
+ /// it returns the flat [maxScale]: those layouts reflow, so they just take the
70
+ /// cap that undoes the ~10% web oversize.
49
71
  ///
50
- /// On desktop it returns the flat [maxScale] cap (0.95) and only drops BELOW it
51
- /// when the SCREEN is small (high OS scale), via
52
- /// `screenWidth / kWebViewportScaleTargetWidth`. Keying off the screen — not the
53
- /// window is the whole point: merely resizing the browser window narrower does
54
- /// NOT shrink the UI (the layout just reflows); the extra shrink happens only when
55
- /// the screen itself is cramped, which is what the compensation is for. With
56
- /// [screenWidth] null (native, or web before the screen is known) it stays at the
57
- /// flat cap.
72
+ /// On desktop it returns the flat [maxScale] cap and only drops BELOW it when the
73
+ /// SCREEN is small (high OS scale), via `screenWidth / kWebViewportScaleTargetWidth`.
74
+ /// Keying the compensation off the screen — not the window — is the whole point:
75
+ /// merely resizing the browser window narrower must not shrink the UI further (the
76
+ /// layout just reflows); the extra shrink happens only when the screen itself is
77
+ /// cramped. With [screenWidth] null it stays at the flat cap.
58
78
  ///
59
79
  /// This is web-only semantics: native never reaches the scaling path (the
60
- /// [WebViewportScale] widget short-circuits when not on web, via [kIsWeb]), so
61
- /// iOS/Android and macOS/Windows apps always render at 1.0.
80
+ /// [WebViewportScale] widget short-circuits when not on web, via [kIsWeb]), and
81
+ /// the device preview is skipped too — both render at native 1.0.
62
82
  double webViewportEffectiveScale(
63
83
  double windowWidth, {
64
84
  double? screenWidth,
65
85
  double maxScale = kWebViewportScale,
66
86
  }) {
67
- if (windowWidth < kWebViewportScaleDesktopBreakpoint) return 1.0;
68
- final double basis = screenWidth ?? double.infinity;
69
- return (basis / kWebViewportScaleTargetWidth).clamp(0.5, maxScale);
87
+ // Desktop: keep the high-OS-scale compensation (drop below the cap only when
88
+ // the SCREEN is small, so the full wide desktop design still fits).
89
+ if (windowWidth >= kWebViewportScaleDesktopBreakpoint) {
90
+ final double basis = screenWidth ?? double.infinity;
91
+ return (basis / kWebViewportScaleTargetWidth).clamp(0.5, maxScale);
92
+ }
93
+ // Tablet & phone web: the same ~10% oversize applies, but these layouts reflow,
94
+ // so they take the flat cap (no wide design to fit, no compensation needed).
95
+ return maxScale;
70
96
  }
71
97
 
72
98
  /// Renders [child] uniformly scaled by [scale] on web (no-op elsewhere).
@@ -85,9 +111,13 @@ class WebViewportScale extends StatelessWidget {
85
111
  this.scale = kWebViewportScale,
86
112
  });
87
113
 
88
- /// Wraps [child] on web; returns it untouched on every other platform.
89
- static Widget wrap(Widget child) =>
90
- kIsWeb ? WebViewportScale(child: child) : child;
114
+ /// Wraps [child] on web when the scale is enabled; returns it untouched
115
+ /// otherwise (every non-web platform, and whenever [kWebViewportScaleEnabled]
116
+ /// is `false`). With the scale off this is a real pass-through — no FittedBox,
117
+ /// no MediaQuery rewrite — so the web app is byte-for-byte native size.
118
+ static Widget wrap(Widget child) => (kIsWeb && kWebViewportScaleEnabled)
119
+ ? WebViewportScale(child: child)
120
+ : child;
91
121
 
92
122
  @override
93
123
  Widget build(BuildContext context) {
@@ -68,6 +68,7 @@ class _KasyPressableDepthState extends ConsumerState<KasyPressableDepth>
68
68
  late final AnimationController _depthController;
69
69
  Timer? _veilTimer;
70
70
  bool _veilVisible = false;
71
+ bool _hovered = false;
71
72
 
72
73
  bool get _useVeil =>
73
74
  widget.pressOverlayColor != null && widget.clipBorderRadius != null;
@@ -134,10 +135,13 @@ class _KasyPressableDepthState extends ConsumerState<KasyPressableDepth>
134
135
  return Transform.scale(scale: _depthScale, child: rawChild);
135
136
  }
136
137
 
138
+ // On web the overlay doubles as a hover highlight: a subtle persistent fill
139
+ // while the pointer is over the control, flashing to full on press. On touch
140
+ // it only flashes on press (_hovered never becomes true).
137
141
  final Widget pressVeil = AnimatedOpacity(
138
142
  duration: const Duration(milliseconds: 90),
139
143
  curve: Curves.easeOutCubic,
140
- opacity: _veilVisible ? 1.0 : 0.0,
144
+ opacity: _veilVisible ? 1.0 : (_hovered ? 0.6 : 0.0),
141
145
  child: IgnorePointer(child: ColoredBox(color: widget.pressOverlayColor!)),
142
146
  );
143
147
 
@@ -196,7 +200,14 @@ class _KasyPressableDepthState extends ConsumerState<KasyPressableDepth>
196
200
 
197
201
  if (!kIsWeb) return inner;
198
202
 
199
- // On web: show pointer cursor only no hover highlight.
200
- return MouseRegion(cursor: SystemMouseCursors.click, child: inner);
203
+ // On web: pointer cursor + a subtle hover highlight (drives the overlay
204
+ // opacity above), so buttons feel interactive on hover as expected on the
205
+ // web. Only meaningful when there's an overlay to show (_useVeil).
206
+ return MouseRegion(
207
+ cursor: SystemMouseCursors.click,
208
+ onEnter: _useVeil ? (_) => setState(() => _hovered = true) : null,
209
+ onExit: _useVeil ? (_) => setState(() => _hovered = false) : null,
210
+ child: inner,
211
+ );
201
212
  }
202
213
  }
@@ -83,7 +83,7 @@ class _AiChatComposerState extends State<AiChatComposer> {
83
83
  @override
84
84
  Widget build(BuildContext context) {
85
85
  final bool enabled = !widget.isReplying;
86
- final BorderRadius shellRadius = BorderRadius.circular(22);
86
+ final BorderRadius shellRadius = BorderRadius.circular(KasyRadius.xl);
87
87
  final Color borderColor = KasyShadows.inputFieldRestingBorder(context);
88
88
 
89
89
  return DecoratedBox(
@@ -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
  ),