kasy-cli 1.37.1 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/scaffold/CHANGELOG.json +23 -0
- package/lib/scaffold/backends/api/patch/README.md +15 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
- package/lib/scaffold/backends/patch-base-hashes.json +6 -6
- package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
- package/lib/scaffold/shared/generator-utils.js +12 -6
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
- package/templates/firebase/AGENTS.md +7 -1
- package/templates/firebase/DESIGN_SYSTEM.md +35 -8
- package/templates/firebase/assets/icons/apple_black.svg +3 -0
- package/templates/firebase/assets/icons/apple_white.svg +4 -0
- package/templates/firebase/assets/icons/facebook.svg +49 -0
- package/templates/firebase/assets/icons/google.svg +1 -0
- package/templates/firebase/functions/src/admin/functions.ts +2 -0
- package/templates/firebase/functions/src/authentication/functions.ts +13 -7
- package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
- package/templates/firebase/lib/components/components.dart +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +361 -20
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
- package/templates/firebase/lib/components/kasy_card.dart +4 -0
- package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
- package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +412 -31
- package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
- package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +29 -231
- package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +19 -9
- package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
- package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
- package/templates/firebase/lib/core/data/api/user_api.dart +15 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
- package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +18 -35
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
- package/templates/firebase/lib/core/states/logout_action.dart +11 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +69 -1
- package/templates/firebase/lib/core/theme/texts.dart +21 -6
- package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
- package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +547 -483
- package/templates/firebase/lib/core/web_viewport_scale.dart +64 -35
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
- package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
- package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
- package/templates/firebase/lib/features/home/home_components_page.dart +264 -126
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +231 -57
- package/templates/firebase/lib/features/home/home_feed.dart +2 -2
- package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
- package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +118 -57
- package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +19 -4
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
- package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
- package/templates/firebase/lib/features/settings/settings_page.dart +99 -65
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1379 -422
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +404 -149
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +24 -31
- package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +77 -95
- package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
- package/templates/firebase/lib/i18n/en.i18n.json +749 -698
- package/templates/firebase/lib/i18n/es.i18n.json +749 -698
- package/templates/firebase/lib/i18n/pt.i18n.json +749 -698
- package/templates/firebase/lib/main.dart +20 -7
- package/templates/firebase/lib/router.dart +70 -46
- package/templates/firebase/pubspec.yaml +2 -1
- package/templates/firebase/test/admin_shell_chrome_test.dart +110 -0
- package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
- package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
- package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
- package/templates/firebase/tool/design_check.dart +9 -0
- package/templates/firebase/assets/icons/apple.png +0 -0
- package/templates/firebase/assets/icons/facebook.png +0 -0
- package/templates/firebase/assets/icons/google.png +0 -0
- package/templates/firebase/assets/icons/google_play_games.png +0 -0
- package/templates/firebase/lib/components/kasy_web_header.dart +0 -210
- package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
- package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
- package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -169
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
|
@@ -2,27 +2,49 @@ 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
|
-
///
|
|
5
|
+
/// Render scale applied to the app on web, on EVERY breakpoint (phone, tablet,
|
|
6
|
+
/// desktop).
|
|
6
7
|
///
|
|
7
|
-
/// Flutter web
|
|
8
|
-
/// browser's 100% zoom, so the whole UI feels oversized
|
|
9
|
-
///
|
|
10
|
-
///
|
|
11
|
-
///
|
|
12
|
-
///
|
|
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.95` walks back ~5% so the web app reads close to the native baseline
|
|
11
|
+
/// without the user touching browser zoom — a gentle correction (the exact
|
|
12
|
+
/// midpoint between full size and a total undo) that takes the "oversized" edge
|
|
13
|
+
/// off Flutter web without making things feel small (0.90 fully undoes the 10%
|
|
14
|
+
/// but reads small on a monitor — one number to nudge). On desktop it also acts
|
|
15
|
+
/// as a cap: a
|
|
16
|
+
/// screen below the design target (high OS scale) reduces it further to pin the
|
|
17
|
+
/// layout (see [kWebViewportScaleTargetWidth]); phone/tablet take the flat cap
|
|
18
|
+
/// because those layouts reflow.
|
|
13
19
|
///
|
|
14
|
-
///
|
|
15
|
-
///
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
///
|
|
20
|
+
/// Two things are deliberately NOT scaled, both rendering at native 1.0:
|
|
21
|
+
/// - NATIVE itself — the mechanism is gated on [kIsWeb], so iOS/Android/macOS/
|
|
22
|
+
/// Windows render at 1.0 and keep respecting the user's system text-size
|
|
23
|
+
/// (accessibility).
|
|
24
|
+
/// - The in-app DEVICE PREVIEW — it simulates a native device, so it must show
|
|
25
|
+
/// the native 1.0 truth; the scale is skipped once the preview frame is up
|
|
26
|
+
/// (see main.dart, gated on `webDevicePreviewActiveNotifier`).
|
|
19
27
|
const double kWebViewportScale = 0.95;
|
|
20
28
|
|
|
29
|
+
/// Master on/off for the web render scale — the single knob.
|
|
30
|
+
///
|
|
31
|
+
/// `true` (default): the web app is rendered ~5% smaller than native via
|
|
32
|
+
/// [kWebViewportScale], correcting Flutter web's oversized feel on desktop while
|
|
33
|
+
/// native stays at true 1.0. This is a deliberate, web-only density correction
|
|
34
|
+
/// (the same technique `responsive_framework`'s autoScale productizes), applied
|
|
35
|
+
/// ON TOP OF an already-adaptive layout — not a substitute for it. We verified
|
|
36
|
+
/// nothing overflows/crops without it, so it is a density choice, not a crutch.
|
|
37
|
+
///
|
|
38
|
+
/// `false`: [WebViewportScale.wrap] becomes a true no-op everywhere and the web
|
|
39
|
+
/// app renders at native 1.0. There is no hidden coupling, so this one flag is
|
|
40
|
+
/// the entire on/off — flip it (or set [kWebViewportScale] to `1.0`) to opt out.
|
|
41
|
+
const bool kWebViewportScaleEnabled = true;
|
|
42
|
+
|
|
21
43
|
/// Design target width (logical px) the desktop shell is laid out against.
|
|
22
44
|
///
|
|
23
45
|
/// A display with high OS scaling (Windows at 125/150/175%, or a Mac in a scaled
|
|
24
46
|
/// "more space"/"larger text" mode) reports a smaller logical SCREEN width, so the
|
|
25
|
-
///
|
|
47
|
+
/// flat scale alone left the shell cramped/cropped (the user had to
|
|
26
48
|
/// Ctrl-minus). When the screen is below this target the scale drops further
|
|
27
49
|
/// (`screenWidth / kWebViewportScaleTargetWidth`) so the full design still fits.
|
|
28
50
|
/// Compared against the SCREEN width, not the window width — see
|
|
@@ -39,34 +61,37 @@ const double kWebViewportScaleDesktopBreakpoint = 1024; // DeviceType.large.brea
|
|
|
39
61
|
/// Effective web render scale (pure math, unit-testable — see
|
|
40
62
|
/// web_viewport_scale_test.dart).
|
|
41
63
|
///
|
|
42
|
-
/// [windowWidth] is the browser window width
|
|
43
|
-
///
|
|
44
|
-
/// logical px (null = unknown/native).
|
|
64
|
+
/// [windowWidth] is the browser window width. [screenWidth] is the physical
|
|
65
|
+
/// screen width in logical px (null = unknown/native).
|
|
45
66
|
///
|
|
46
|
-
///
|
|
47
|
-
///
|
|
48
|
-
///
|
|
67
|
+
/// On tablet/phone web ([windowWidth] below [kWebViewportScaleDesktopBreakpoint])
|
|
68
|
+
/// it returns the flat [maxScale]: those layouts reflow, so they just take the
|
|
69
|
+
/// cap that undoes the ~10% web oversize.
|
|
49
70
|
///
|
|
50
|
-
/// On desktop it returns the flat [maxScale] cap
|
|
51
|
-
///
|
|
52
|
-
///
|
|
53
|
-
///
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
/// [screenWidth] null (native, or web before the screen is known) it stays at the
|
|
57
|
-
/// flat cap.
|
|
71
|
+
/// On desktop it returns the flat [maxScale] cap and only drops BELOW it when the
|
|
72
|
+
/// SCREEN is small (high OS scale), via `screenWidth / kWebViewportScaleTargetWidth`.
|
|
73
|
+
/// Keying the compensation off the screen — not the window — is the whole point:
|
|
74
|
+
/// merely resizing the browser window narrower must not shrink the UI further (the
|
|
75
|
+
/// layout just reflows); the extra shrink happens only when the screen itself is
|
|
76
|
+
/// cramped. With [screenWidth] null it stays at the flat cap.
|
|
58
77
|
///
|
|
59
78
|
/// This is web-only semantics: native never reaches the scaling path (the
|
|
60
|
-
/// [WebViewportScale] widget short-circuits when not on web, via [kIsWeb]),
|
|
61
|
-
///
|
|
79
|
+
/// [WebViewportScale] widget short-circuits when not on web, via [kIsWeb]), and
|
|
80
|
+
/// the device preview is skipped too — both render at native 1.0.
|
|
62
81
|
double webViewportEffectiveScale(
|
|
63
82
|
double windowWidth, {
|
|
64
83
|
double? screenWidth,
|
|
65
84
|
double maxScale = kWebViewportScale,
|
|
66
85
|
}) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
// Desktop: keep the high-OS-scale compensation (drop below the cap only when
|
|
87
|
+
// the SCREEN is small, so the full wide desktop design still fits).
|
|
88
|
+
if (windowWidth >= kWebViewportScaleDesktopBreakpoint) {
|
|
89
|
+
final double basis = screenWidth ?? double.infinity;
|
|
90
|
+
return (basis / kWebViewportScaleTargetWidth).clamp(0.5, maxScale);
|
|
91
|
+
}
|
|
92
|
+
// Tablet & phone web: the same ~10% oversize applies, but these layouts reflow,
|
|
93
|
+
// so they take the flat cap (no wide design to fit, no compensation needed).
|
|
94
|
+
return maxScale;
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
/// Renders [child] uniformly scaled by [scale] on web (no-op elsewhere).
|
|
@@ -85,9 +110,13 @@ class WebViewportScale extends StatelessWidget {
|
|
|
85
110
|
this.scale = kWebViewportScale,
|
|
86
111
|
});
|
|
87
112
|
|
|
88
|
-
/// Wraps [child] on web; returns it untouched
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
/// Wraps [child] on web when the scale is enabled; returns it untouched
|
|
114
|
+
/// otherwise (every non-web platform, and whenever [kWebViewportScaleEnabled]
|
|
115
|
+
/// is `false`). With the scale off this is a real pass-through — no FittedBox,
|
|
116
|
+
/// no MediaQuery rewrite — so the web app is byte-for-byte native size.
|
|
117
|
+
static Widget wrap(Widget child) => (kIsWeb && kWebViewportScaleEnabled)
|
|
118
|
+
? WebViewportScale(child: child)
|
|
119
|
+
: child;
|
|
91
120
|
|
|
92
121
|
@override
|
|
93
122
|
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:
|
|
200
|
-
|
|
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
|
}
|
|
@@ -179,6 +179,14 @@ class _DeviceSizeBuilderState extends State<DeviceSizeBuilder>
|
|
|
179
179
|
/// The maximum width for large devices.
|
|
180
180
|
const kMaxLargeLayoutWidth = 650.0;
|
|
181
181
|
|
|
182
|
+
/// Canonical max width for a single-column internal screen's content (notifications,
|
|
183
|
+
/// settings detail, reminder, ...). On desktop the content is centered within this
|
|
184
|
+
/// width instead of stretching edge-to-edge, so lists/cards never read too wide.
|
|
185
|
+
///
|
|
186
|
+
/// This is the one knob for "contained internal content" — prefer it over bespoke
|
|
187
|
+
/// per-screen `maxWidth` literals so every internal page lines up to the same ruler.
|
|
188
|
+
const double kKasyContentMaxWidth = 600.0;
|
|
189
|
+
|
|
182
190
|
/// A widget that constrains its child to a maximum width on large devices.
|
|
183
191
|
/// It uses the [LayoutBuilder] widget to get the current device width
|
|
184
192
|
/// and constrains the child to the maximum width on large devices.
|
|
@@ -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(
|
|
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(
|
|
227
|
-
topRight: const Radius.circular(
|
|
232
|
+
topLeft: const Radius.circular(KasyRadius.lg),
|
|
233
|
+
topRight: const Radius.circular(KasyRadius.lg),
|
|
228
234
|
bottomLeft: isUser
|
|
229
|
-
? const Radius.circular(
|
|
230
|
-
: const Radius.circular(
|
|
235
|
+
? const Radius.circular(KasyRadius.lg)
|
|
236
|
+
: const Radius.circular(KasyRadius.xs),
|
|
231
237
|
bottomRight: isUser
|
|
232
|
-
? const Radius.circular(
|
|
233
|
-
: const Radius.circular(
|
|
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.
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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(
|
|
331
|
-
topRight: Radius.circular(
|
|
332
|
-
bottomLeft: Radius.circular(
|
|
333
|
-
bottomRight: Radius.circular(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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:
|
|
325
|
+
child: SocialAuthButton(
|
|
327
326
|
label: t.auth.signin.google,
|
|
328
|
-
|
|
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:
|
|
337
|
+
child: SocialAuthButton(
|
|
339
338
|
label: t.auth.signin.apple,
|
|
340
|
-
|
|
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:
|
|
355
|
+
child: SocialAuthButton(
|
|
353
356
|
label: t.auth.signin.facebook,
|
|
354
|
-
|
|
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:
|
|
225
|
+
child: SocialAuthButton(
|
|
227
226
|
label: t.auth.signin.google,
|
|
228
|
-
|
|
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:
|
|
237
|
+
child: SocialAuthButton(
|
|
239
238
|
label: t.auth.signin.apple,
|
|
240
|
-
|
|
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:
|
|
255
|
+
child: SocialAuthButton(
|
|
253
256
|
label: t.auth.signin.facebook,
|
|
254
|
-
|
|
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
|
-
}
|