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
|
@@ -21,6 +21,53 @@ class DevInspectorInfo {
|
|
|
21
21
|
final String? semanticWidget;
|
|
22
22
|
final List<String> searchHints;
|
|
23
23
|
|
|
24
|
+
/// The widget's main color as a usable hex code (e.g. `#FC0303`), or null if
|
|
25
|
+
/// none is set. Raw `Color(...)` descriptions aren't useful to a dev — a hex
|
|
26
|
+
/// is. Text content / labels are intentionally NOT surfaced: they're already
|
|
27
|
+
/// visible on screen, so repeating them just adds noise.
|
|
28
|
+
String? get colorHex {
|
|
29
|
+
for (final p in properties) {
|
|
30
|
+
final int i = p.indexOf(':');
|
|
31
|
+
if (i <= 0) continue;
|
|
32
|
+
if (!p.substring(0, i).toLowerCase().contains('color')) continue;
|
|
33
|
+
final hex = _parseColorHex(p.substring(i + 1));
|
|
34
|
+
if (hex != null) return hex;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static String? _parseColorHex(String raw) {
|
|
40
|
+
// Legacy form: Color(0xAARRGGBB)
|
|
41
|
+
final legacy = RegExp('0x([0-9a-fA-F]{8})').firstMatch(raw);
|
|
42
|
+
if (legacy != null) {
|
|
43
|
+
final v = legacy.group(1)!.toUpperCase();
|
|
44
|
+
return v.startsWith('FF') ? '#${v.substring(2)}' : '#$v';
|
|
45
|
+
}
|
|
46
|
+
// Modern form: Color(alpha: 1.0, red: 0.98, green: 0.0, blue: 0.0, …)
|
|
47
|
+
double? comp(String key) {
|
|
48
|
+
final m = RegExp('$key:\\s*([0-9.]+)').firstMatch(raw);
|
|
49
|
+
return m == null ? null : double.tryParse(m.group(1)!);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
final r = comp('red');
|
|
53
|
+
final g = comp('green');
|
|
54
|
+
final b = comp('blue');
|
|
55
|
+
if (r == null || g == null || b == null) return null;
|
|
56
|
+
final a = comp('alpha');
|
|
57
|
+
String h(double v) =>
|
|
58
|
+
(v * 255).round().clamp(0, 255).toRadixString(16).padLeft(2, '0').toUpperCase();
|
|
59
|
+
final rgb = '${h(r)}${h(g)}${h(b)}';
|
|
60
|
+
return (a == null || a >= 0.999) ? '#$rgb' : '#${h(a)}$rgb';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Compact size string: `w×h` (logical pixels), e.g. `44×44`. Position is
|
|
64
|
+
/// intentionally omitted — it isn't useful when identifying/editing a widget.
|
|
65
|
+
String get sizeLabel {
|
|
66
|
+
final w = boundingBox.width.toStringAsFixed(0);
|
|
67
|
+
final h = boundingBox.height.toStringAsFixed(0);
|
|
68
|
+
return '$w×$h';
|
|
69
|
+
}
|
|
70
|
+
|
|
24
71
|
String toAIClipboard({String? routeName}) {
|
|
25
72
|
final buf = StringBuffer();
|
|
26
73
|
buf.writeln('## Inspected widget');
|
|
@@ -6,7 +6,15 @@ import 'package:kasy_kit/core/dev_inspector/dev_inspector_info.dart';
|
|
|
6
6
|
class DevInspectorService {
|
|
7
7
|
DevInspectorService._();
|
|
8
8
|
|
|
9
|
-
static const _screenRegex = r'(Page|Screen|Route|View)$';
|
|
9
|
+
static const _screenRegex = r'(Page|Screen|Route|View|Tab|Shell)$';
|
|
10
|
+
|
|
11
|
+
/// Strips generic type arguments so `PopupMenuItem<bool>` matches against
|
|
12
|
+
/// `PopupMenuItem`, `Router<Object>` against `Router`, etc. Set / regex
|
|
13
|
+
/// lookups all compare against this base name.
|
|
14
|
+
static String _baseName(String typeName) {
|
|
15
|
+
final i = typeName.indexOf('<');
|
|
16
|
+
return i < 0 ? typeName : typeName.substring(0, i);
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
// Widgets we refuse to return — even after climbing.
|
|
12
20
|
static const _skipSet = <String>{
|
|
@@ -64,9 +72,32 @@ class DevInspectorService {
|
|
|
64
72
|
|
|
65
73
|
// Context propagation
|
|
66
74
|
'Theme', 'DefaultTextStyle', 'IconTheme', 'IconButtonTheme',
|
|
75
|
+
'CupertinoTheme', 'InheritedCupertinoTheme', 'CupertinoUserInterfaceLevel',
|
|
76
|
+
'ResponsiveTextTheme',
|
|
67
77
|
'Localizations', 'Directionality', 'Title',
|
|
68
78
|
'DefaultSelectionStyle', 'SelectionContainer', 'AnnotatedRegion',
|
|
69
79
|
|
|
80
|
+
// App-root / navigation / overlay scaffolding (framework + go_router +
|
|
81
|
+
// project shell). Selecting these is never useful — they're plumbing.
|
|
82
|
+
'Router', 'InheritedGoRouter', 'GoRouterStateRegistryScope',
|
|
83
|
+
'StatefulNavigationShell', 'Navigator', 'Overlay', 'OverlayPortal',
|
|
84
|
+
'CustomMultiChildLayout', 'CustomSingleChildLayout', 'LayoutId',
|
|
85
|
+
'ScaffoldMessenger', 'TapRegion', 'TapRegionSurface',
|
|
86
|
+
'ShortcutRegistrar', 'DefaultTextEditingShortcuts', 'ExcludeFocus',
|
|
87
|
+
'FocusVisibility', 'Initializer',
|
|
88
|
+
|
|
89
|
+
// Framework app-root + dev wrappers (the engine's View/RootWidget, the app
|
|
90
|
+
// widget, Riverpod/i18n providers, and the device_preview chrome used on
|
|
91
|
+
// web). None is ever a meaningful selection or a "screen".
|
|
92
|
+
'RootWidget', 'View', 'RawView', 'ViewAnchor',
|
|
93
|
+
'WidgetsApp', 'MaterialApp', 'CupertinoApp', 'MyApp',
|
|
94
|
+
'RootRestorationScope', 'SharedAppData',
|
|
95
|
+
'TranslationProvider', 'InheritedLocaleData',
|
|
96
|
+
'ProviderScope', 'UncontrolledProviderScope',
|
|
97
|
+
'ThemeProvider', 'ChangeNotifierProvider', 'MediaQueryObserver',
|
|
98
|
+
'WebDevicePreview', 'DevicePreview', 'DeviceFrame', 'VirtualKeyboard',
|
|
99
|
+
'RotatedBox',
|
|
100
|
+
|
|
70
101
|
// Routing / storage
|
|
71
102
|
'KeyedSubtree', 'AutomaticKeepAlive', 'KeepAlive', 'TickerMode',
|
|
72
103
|
'Offstage', 'PageStorage', 'RestorationScope',
|
|
@@ -93,12 +124,8 @@ class DevInspectorService {
|
|
|
93
124
|
'RichText', 'RawMaterialButton',
|
|
94
125
|
};
|
|
95
126
|
|
|
96
|
-
static bool _shouldClimbPast(String typeName)
|
|
97
|
-
|
|
98
|
-
return typeName.startsWith('NotificationListener<') ||
|
|
99
|
-
typeName.startsWith('Listener<') ||
|
|
100
|
-
typeName.startsWith('Builder<');
|
|
101
|
-
}
|
|
127
|
+
static bool _shouldClimbPast(String typeName) =>
|
|
128
|
+
_climbPastSet.contains(_baseName(typeName));
|
|
102
129
|
|
|
103
130
|
// Used by ancestor-list builders (kept as-is — affects displayed tree only).
|
|
104
131
|
static bool _isGenericUi(String typeName) => _shouldClimbPast(typeName);
|
|
@@ -125,11 +152,16 @@ class DevInspectorService {
|
|
|
125
152
|
'_PrimaryScrollControllerScope',
|
|
126
153
|
'_TapRegionRegistry', '_TapRegionSurface',
|
|
127
154
|
'_KeyedSubtree',
|
|
155
|
+
// Semantics / pointer / scroll internals that wrap every tappable widget.
|
|
156
|
+
// Stopping here yields a useless `_GestureSemantics`/`_ScrollableScope`
|
|
157
|
+
// instead of the real widget.
|
|
158
|
+
'_GestureSemantics', '_InkResponseStateWidget', '_ScrollableScope',
|
|
159
|
+
'_RenderScrollSemantics',
|
|
128
160
|
};
|
|
129
161
|
|
|
130
162
|
static bool _shouldSkip(String typeName) {
|
|
131
|
-
|
|
132
|
-
return _privateFrameworkSet.contains(
|
|
163
|
+
final base = _baseName(typeName);
|
|
164
|
+
return _skipSet.contains(base) || _privateFrameworkSet.contains(base);
|
|
133
165
|
}
|
|
134
166
|
|
|
135
167
|
static List<String> _extractProperties(Element element) {
|
|
@@ -182,18 +214,26 @@ class DevInspectorService {
|
|
|
182
214
|
return routeName;
|
|
183
215
|
}
|
|
184
216
|
|
|
185
|
-
|
|
217
|
+
// Scan innermost-first so the most specific screen wins (e.g. the active
|
|
218
|
+
// `AdminUsersTab` rather than the surrounding `AdminShell`).
|
|
219
|
+
final privateAwareAncestors = _extractAncestors(
|
|
220
|
+
element,
|
|
221
|
+
includePrivate: true,
|
|
222
|
+
);
|
|
186
223
|
final regex = RegExp('^_?[A-Za-z0-9]+$_screenRegex');
|
|
187
|
-
for (final type in privateAwareAncestors
|
|
224
|
+
for (final type in privateAwareAncestors) {
|
|
188
225
|
if (regex.hasMatch(type)) return _sanitizeType(type);
|
|
189
226
|
}
|
|
190
|
-
for (final type in ancestors
|
|
191
|
-
if (
|
|
227
|
+
for (final type in ancestors) {
|
|
228
|
+
if (regex.hasMatch(type)) return _sanitizeType(type);
|
|
192
229
|
}
|
|
193
230
|
return null;
|
|
194
231
|
}
|
|
195
232
|
|
|
196
|
-
static String? _extractSemanticWidget(
|
|
233
|
+
static String? _extractSemanticWidget(
|
|
234
|
+
String widgetType,
|
|
235
|
+
List<String> ancestors,
|
|
236
|
+
) {
|
|
197
237
|
final chain = <String>[widgetType, ...ancestors];
|
|
198
238
|
final featureRegex = RegExp(
|
|
199
239
|
r'(Page|Screen|Card|Tile|Item|Section|Panel|Dialog|Modal|Header|Footer|Widget|Component)$',
|
|
@@ -203,7 +243,7 @@ class DevInspectorService {
|
|
|
203
243
|
for (final type in chain) {
|
|
204
244
|
if (_shouldSkip(type)) continue;
|
|
205
245
|
if (_shouldClimbPast(type)) continue;
|
|
206
|
-
if (featureRegex.hasMatch(type)) return type;
|
|
246
|
+
if (featureRegex.hasMatch(_baseName(type))) return type;
|
|
207
247
|
}
|
|
208
248
|
|
|
209
249
|
// 2) Fallback to first non-generic non-skipped widget.
|
|
@@ -87,7 +87,11 @@ abstract final class KasyIcons {
|
|
|
87
87
|
static const IconData moreVert = LucideIcons.ellipsisVertical300;
|
|
88
88
|
static const IconData microphone = LucideIcons.mic300;
|
|
89
89
|
static const IconData northEast = LucideIcons.arrowUpRight300;
|
|
90
|
-
|
|
90
|
+
// NOTE: the `bell300` glyph in lucide_icons_flutter 3.1.14+2 is corrupted in the
|
|
91
|
+
// Lucide300 weight font — it renders FILLED instead of outline (the same font
|
|
92
|
+
// renders every other 300 icon correctly). Use the base `bell` (primary Lucide
|
|
93
|
+
// font), which is a proper outline bell, so it matches the rest of the linear set.
|
|
94
|
+
static const IconData notification = LucideIcons.bell;
|
|
91
95
|
static const IconData notificationActive = LucideIcons.bellRing300;
|
|
92
96
|
static const IconData notificationAdd = LucideIcons.bellPlus300;
|
|
93
97
|
static const IconData notificationOff = LucideIcons.bellOff300;
|
|
@@ -117,4 +121,15 @@ abstract final class KasyIcons {
|
|
|
117
121
|
static const IconData upload = LucideIcons.upload300;
|
|
118
122
|
static const IconData video = LucideIcons.video300;
|
|
119
123
|
static const IconData widgets = LucideIcons.component300;
|
|
124
|
+
|
|
125
|
+
/// Device frame visible / hidden (web device preview chrome).
|
|
126
|
+
static const IconData deviceFrame = LucideIcons.smartphone300;
|
|
127
|
+
static const IconData deviceFrameOff = LucideIcons.scan300;
|
|
128
|
+
|
|
129
|
+
/// Screen orientation (web device preview chrome).
|
|
130
|
+
static const IconData landscape = LucideIcons.rectangleHorizontal300;
|
|
131
|
+
static const IconData portrait = LucideIcons.rectangleVertical300;
|
|
132
|
+
|
|
133
|
+
/// Element inspector pick tool (web device preview chrome).
|
|
134
|
+
static const IconData inspector = LucideIcons.squareDashedMousePointer300;
|
|
120
135
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
2
2
|
import 'package:flutter/material.dart';
|
|
3
3
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
4
|
-
import 'package:go_router/go_router.dart';
|
|
5
4
|
import 'package:kasy_kit/components/components.dart';
|
|
6
5
|
import 'package:kasy_kit/core/data/api/analytics_api.dart';
|
|
7
6
|
import 'package:kasy_kit/core/rating/providers/rating_repository.dart';
|
|
@@ -13,9 +12,10 @@ import 'package:logger/logger.dart';
|
|
|
13
12
|
/// Shows the in-app review dialog. Prefer a stable [context] (not a sheet that
|
|
14
13
|
/// will be popped before the async work finishes).
|
|
15
14
|
///
|
|
16
|
-
/// A clean [KasyDialog]
|
|
17
|
-
///
|
|
18
|
-
///
|
|
15
|
+
/// A clean [KasyDialog] focused on a single goal: a positive store rating. A
|
|
16
|
+
/// gold star, a warm title/message and one action (write a review on the
|
|
17
|
+
/// Android/Play or iOS App Store). Dismissing via the dialog's own close button
|
|
18
|
+
/// just defers the next ask and keeps the user where they are.
|
|
19
19
|
Future<bool> showReviewDialog(
|
|
20
20
|
BuildContext context,
|
|
21
21
|
WidgetRef ref, {
|
|
@@ -46,15 +46,16 @@ Future<bool> showReviewDialog(
|
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
await showKasyDialog<void>(
|
|
50
50
|
context: context,
|
|
51
|
-
barrierDismissible: false,
|
|
52
51
|
builder: (dialogContext) {
|
|
53
52
|
ratingRepository.delay();
|
|
54
53
|
final translations = Translations.of(dialogContext).review_popup;
|
|
55
54
|
return KasyDialog(
|
|
56
55
|
leadingIcon: KasyIcons.star,
|
|
57
|
-
|
|
56
|
+
// Gold/amber star via KasyDialog's default `warning` tone — the design
|
|
57
|
+
// system's amber (#F5A524 light / #F7B750 dark), the natural colour for
|
|
58
|
+
// a rating star.
|
|
58
59
|
title: translations.title,
|
|
59
60
|
titleCentered: true,
|
|
60
61
|
message: translations.description,
|
|
@@ -63,37 +64,19 @@ Future<bool> showReviewDialog(
|
|
|
63
64
|
rating.delay();
|
|
64
65
|
Navigator.of(dialogContext).pop();
|
|
65
66
|
},
|
|
66
|
-
footer:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (!dialogContext.mounted) return;
|
|
77
|
-
Navigator.of(dialogContext).pop();
|
|
78
|
-
},
|
|
79
|
-
);
|
|
80
|
-
},
|
|
81
|
-
),
|
|
82
|
-
const SizedBox(height: KasySpacing.sm),
|
|
83
|
-
KasyButton(
|
|
84
|
-
label: translations.cancel_button,
|
|
85
|
-
variant: KasyButtonVariant.soft,
|
|
86
|
-
expand: true,
|
|
87
|
-
onPressed: () => Navigator.of(dialogContext).pop(true),
|
|
88
|
-
),
|
|
89
|
-
],
|
|
67
|
+
footer: KasyButton(
|
|
68
|
+
label: translations.rate_button,
|
|
69
|
+
expand: true,
|
|
70
|
+
onPressed: () {
|
|
71
|
+
analytics.logEvent('rating_popup_show', {});
|
|
72
|
+
ratingRepository.rate().then((_) => rating.review()).then((_) {
|
|
73
|
+
if (!dialogContext.mounted) return;
|
|
74
|
+
Navigator.of(dialogContext).pop();
|
|
75
|
+
});
|
|
76
|
+
},
|
|
90
77
|
),
|
|
91
78
|
);
|
|
92
79
|
},
|
|
93
80
|
);
|
|
94
|
-
|
|
95
|
-
if (openFeedback == true && context.mounted) {
|
|
96
|
-
await context.push('/feedback');
|
|
97
|
-
}
|
|
98
81
|
return true;
|
|
99
82
|
}
|
|
@@ -105,4 +105,15 @@ class SharedPreferencesBuilder implements OnStartService {
|
|
|
105
105
|
Future<void> setAttSoftLastAskedAt(DateTime when) async {
|
|
106
106
|
await prefs.setInt('att_soft_last_asked_at', when.millisecondsSinceEpoch);
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
/// Whether we've already fired the one-time native push-permission prompt the
|
|
110
|
+
/// first time the user opened the notifications screen. iOS only ever shows
|
|
111
|
+
/// the native prompt once, so we must never auto-fire it more than once.
|
|
112
|
+
bool getPushAutoRequested() {
|
|
113
|
+
return prefs.getBool('push_auto_requested') ?? false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Future<void> setPushAutoRequested(bool value) async {
|
|
117
|
+
await prefs.setBool('push_auto_requested', value);
|
|
118
|
+
}
|
|
108
119
|
}
|
|
@@ -4,6 +4,7 @@ import 'package:kasy_kit/components/components.dart';
|
|
|
4
4
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
5
5
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
6
6
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
7
|
+
import 'package:kasy_kit/router.dart';
|
|
7
8
|
|
|
8
9
|
/// Standardized logout flow — the single source of truth for signing out.
|
|
9
10
|
///
|
|
@@ -25,5 +26,14 @@ Future<void> confirmLogout(BuildContext context, WidgetRef ref) {
|
|
|
25
26
|
// forget: that closed the dialog instantly and left the old screen frozen.)
|
|
26
27
|
onConfirmAsync: () =>
|
|
27
28
|
ref.read(userStateNotifierProvider.notifier).onLogout(),
|
|
28
|
-
)
|
|
29
|
+
).whenComplete(() {
|
|
30
|
+
// The confirm dialog is a pageless route on the ROOT navigator, so the
|
|
31
|
+
// redirect that fires when onLogout flips the state to anonymous runs while
|
|
32
|
+
// the dialog still sits on top — go_router keeps the current page (Settings)
|
|
33
|
+
// mounted underneath and the move to /signin never lands. Now that the
|
|
34
|
+
// dialog has popped, re-run the redirect so the (now unauthenticated) user
|
|
35
|
+
// is sent to sign-in. Harmless on cancel: the redirect keeps an
|
|
36
|
+
// authenticated user exactly where they are.
|
|
37
|
+
if (context.mounted) ref.read(goRouterProvider).refresh();
|
|
38
|
+
});
|
|
29
39
|
}
|
|
@@ -60,8 +60,11 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
60
60
|
return ref.read(environmentProvider).authenticationMode;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
StreamSubscription<String?>? _roleSubscription;
|
|
64
|
+
|
|
63
65
|
@override
|
|
64
66
|
UserState build() {
|
|
67
|
+
ref.onDispose(() => _roleSubscription?.cancel());
|
|
65
68
|
return const UserState(user: User.loading());
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -79,6 +82,7 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
79
82
|
rethrow;
|
|
80
83
|
}
|
|
81
84
|
assert(state.user is! LoadingUserData, 'UserStateNotifier is not ready');
|
|
85
|
+
_syncRoleListener();
|
|
82
86
|
await _initDeviceRegistration();
|
|
83
87
|
_deviceRepository.onTokenUpdate(_onUpdateToken);
|
|
84
88
|
}
|
|
@@ -89,6 +93,7 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
89
93
|
Future<void> onSignin() async {
|
|
90
94
|
state = const UserState(user: User.loading());
|
|
91
95
|
await _loadState();
|
|
96
|
+
_syncRoleListener();
|
|
92
97
|
await _initDeviceRegistration();
|
|
93
98
|
// A successful sign-in puts the user past first-run onboarding. Remember it
|
|
94
99
|
// so that after a later logout they land on the sign-in screen instead of
|
|
@@ -166,6 +171,7 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
166
171
|
/// It will unregister the device from the notification service
|
|
167
172
|
/// and logout the user
|
|
168
173
|
Future<void> onLogout() async {
|
|
174
|
+
_stopRoleListener();
|
|
169
175
|
final userId = state.user.idOrThrow;
|
|
170
176
|
_deviceRepository.removeTokenUpdateListener();
|
|
171
177
|
// Best-effort: if the network call fails we still proceed with logout so
|
|
@@ -181,6 +187,7 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
181
187
|
// Biometric lock is a per-account preference, not a device-wide one.
|
|
182
188
|
// The next user signing in on this install should start without it set.
|
|
183
189
|
await ref.read(sharedPreferencesProvider).setBiometricEnabled(false);
|
|
190
|
+
await _clearWebGuestPass();
|
|
184
191
|
// Forget the last bottom-bar tab so the next login lands on the default tab
|
|
185
192
|
// (Home) instead of wherever the previous account left off.
|
|
186
193
|
forgetActiveTab();
|
|
@@ -278,7 +285,19 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
278
285
|
/// Here is the function to do it.
|
|
279
286
|
/// It will delete the user account and logout the user
|
|
280
287
|
Future<void> deleteAccount() async {
|
|
281
|
-
|
|
288
|
+
_stopRoleListener();
|
|
289
|
+
final userId = state.user.idOrNull;
|
|
290
|
+
// No backend identity (a guest with no account): there is nothing to delete
|
|
291
|
+
// server-side. Just clear the local session so we never throw a generic
|
|
292
|
+
// error that strands the user on Settings — the UI hides the delete button
|
|
293
|
+
// in this case, this is only a safety net.
|
|
294
|
+
if (userId == null) {
|
|
295
|
+
await _authenticationRepository.logout();
|
|
296
|
+
await _clearWebGuestPass();
|
|
297
|
+
forgetActiveTab();
|
|
298
|
+
state = const UserState(user: User.anonymous());
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
282
301
|
_deviceRepository.removeTokenUpdateListener();
|
|
283
302
|
try {
|
|
284
303
|
await _deviceRepository.unregister(userId);
|
|
@@ -287,6 +306,7 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
287
306
|
}
|
|
288
307
|
await _userRepository.delete();
|
|
289
308
|
await _authenticationRepository.logout();
|
|
309
|
+
await _clearWebGuestPass();
|
|
290
310
|
// Same as onLogout: forget the last bottom-bar tab so the next account that
|
|
291
311
|
// signs in lands on Home, not wherever the deleted account left off (the
|
|
292
312
|
// user deletes from Settings, so without this the next login reopens it).
|
|
@@ -297,6 +317,54 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
297
317
|
// continue as a guest.
|
|
298
318
|
}
|
|
299
319
|
|
|
320
|
+
/// On WEB the auth redirect treats "onboarding done" as "this guest is allowed
|
|
321
|
+
/// to stay" (`ready = hasIdentity || isOnboarded || onboardingDone`). After a
|
|
322
|
+
/// logout / account deletion the flag is still true, so the redirect would
|
|
323
|
+
/// keep the just-signed-out user on the current screen instead of sending them
|
|
324
|
+
/// to /signin — and whether it happened to work depended on leftover browser
|
|
325
|
+
/// state. Clearing it on web makes the session boundary land on /signin every
|
|
326
|
+
/// time. No-op on native, where this flag only skips the (native-only)
|
|
327
|
+
/// onboarding screen and logout already routes correctly via hasIdentity, so
|
|
328
|
+
/// clearing it would wrongly show onboarding again.
|
|
329
|
+
Future<void> _clearWebGuestPass() async {
|
|
330
|
+
if (!kIsWeb) return;
|
|
331
|
+
await ref.read(sharedPreferencesProvider).setOnboardingCompleted(false);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// -------------------------------
|
|
335
|
+
// ROLE LISTENER
|
|
336
|
+
// -------------------------------
|
|
337
|
+
|
|
338
|
+
void _syncRoleListener() {
|
|
339
|
+
final id = state.user.idOrNull;
|
|
340
|
+
if (id != null && state.user is AuthenticatedUserData) {
|
|
341
|
+
_startRoleListener(id);
|
|
342
|
+
} else {
|
|
343
|
+
_stopRoleListener();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
void _startRoleListener(String userId) {
|
|
348
|
+
_roleSubscription?.cancel();
|
|
349
|
+
_roleSubscription = _userRepository
|
|
350
|
+
.watchRole(userId)
|
|
351
|
+
.skip(1) // skip initial snapshot — state was just loaded
|
|
352
|
+
.listen((newRole) {
|
|
353
|
+
final currentRole = switch (state.user) {
|
|
354
|
+
AuthenticatedUserData(:final role) => role,
|
|
355
|
+
_ => null,
|
|
356
|
+
};
|
|
357
|
+
if (newRole != currentRole) {
|
|
358
|
+
refresh();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
void _stopRoleListener() {
|
|
364
|
+
_roleSubscription?.cancel();
|
|
365
|
+
_roleSubscription = null;
|
|
366
|
+
}
|
|
367
|
+
|
|
300
368
|
// -------------------------------
|
|
301
369
|
// PRIVATES
|
|
302
370
|
// -------------------------------
|
|
@@ -147,19 +147,34 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
|
|
|
147
147
|
/// Top-level page title (auth, paywall, full-screen flows). 24 / w700.
|
|
148
148
|
TextStyle get pageTitle => headlineMedium;
|
|
149
149
|
|
|
150
|
-
/// Section or master-detail pane title (Settings detail, grouped areas).
|
|
150
|
+
/// Section or master-detail pane title (Settings detail, grouped areas).
|
|
151
|
+
/// 18 / w600 — a visible step above the content rows it heads.
|
|
151
152
|
TextStyle get sectionTitle => titleMedium;
|
|
152
153
|
|
|
153
154
|
/// Small header above a grouped list (e.g. "PREFERENCES"). 12 / w600, lightly tracked.
|
|
154
155
|
TextStyle get sectionLabel =>
|
|
155
156
|
bodySmall.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5);
|
|
156
157
|
|
|
157
|
-
/// Primary text of a
|
|
158
|
+
/// Primary text of a DENSE navigation / list row — sidebar, tabs, admin
|
|
159
|
+
/// tables. 14 / w500. For full-width CONTENT rows (Settings, a messaging /
|
|
160
|
+
/// conversation inbox) use [listRowTitle] (16) instead.
|
|
158
161
|
TextStyle get rowTitle => titleSmall;
|
|
159
162
|
|
|
160
|
-
/// Secondary / value text in a row (apply a muted colour at the call
|
|
163
|
+
/// Secondary / value text in a dense row (apply a muted colour at the call
|
|
164
|
+
/// site). 14 / w400.
|
|
161
165
|
TextStyle get rowValue => bodyMedium;
|
|
162
166
|
|
|
167
|
+
/// EMPHASIS text of a CONTENT list row — the row's label/title (Settings, and
|
|
168
|
+
/// other iOS-style lists where each row is primary content). 16 / w500. Pairs
|
|
169
|
+
/// with [listRowValue] (14) so the label clearly outranks its value.
|
|
170
|
+
TextStyle get listRowTitle =>
|
|
171
|
+
bodyLarge.copyWith(fontWeight: FontWeight.w500);
|
|
172
|
+
|
|
173
|
+
/// INFO text of a content list row — the value beside the label (apply a muted
|
|
174
|
+
/// colour at the call site). 14 / w400, deliberately one step below
|
|
175
|
+
/// [listRowTitle] so label vs value reads as a hierarchy, not a flat pair.
|
|
176
|
+
TextStyle get listRowValue => bodyMedium;
|
|
177
|
+
|
|
163
178
|
/// Card title. 14 / w500.
|
|
164
179
|
TextStyle get cardTitle => titleSmall;
|
|
165
180
|
|
|
@@ -188,17 +203,17 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
|
|
|
188
203
|
headlineMedium: _fromRamp(FontWeight.w700, KasyTypeScale.heading2, device),
|
|
189
204
|
headlineSmall: _fromRamp(FontWeight.w600, KasyTypeScale.heading3, device),
|
|
190
205
|
|
|
191
|
-
// Title → Heading 3 / Heading 4 / Body sm
|
|
206
|
+
// Title → Heading 3 / Heading 4 / Body sm (dense title 14, w500).
|
|
192
207
|
titleLarge: _fromRamp(FontWeight.w600, KasyTypeScale.heading3, device),
|
|
193
208
|
titleMedium: _fromRamp(FontWeight.w600, KasyTypeScale.heading4, device),
|
|
194
209
|
titleSmall: _fromRamp(FontWeight.w500, KasyTypeScale.bodySm, device),
|
|
195
210
|
|
|
196
|
-
// Body → Body base / Body sm / Body xs.
|
|
211
|
+
// Body → Body base (16) / Body sm (14) / Body xs (12).
|
|
197
212
|
bodyLarge: _fromRamp(FontWeight.w400, KasyTypeScale.bodyBase, device),
|
|
198
213
|
bodyMedium: _fromRamp(FontWeight.w400, KasyTypeScale.bodySm, device),
|
|
199
214
|
bodySmall: _fromRamp(FontWeight.w400, KasyTypeScale.bodyXs, device),
|
|
200
215
|
|
|
201
|
-
// Label →
|
|
216
|
+
// Label → Body sm (14, w500) / Body xs (12, w500).
|
|
202
217
|
labelLarge: _fromRamp(FontWeight.w500, KasyTypeScale.bodySm, device),
|
|
203
218
|
labelMedium: _fromRamp(FontWeight.w500, KasyTypeScale.bodyXs, device),
|
|
204
219
|
labelSmall: _fromRamp(FontWeight.w500, KasyTypeScale.bodyXs, device),
|
|
@@ -2,15 +2,25 @@ import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
|
2
2
|
|
|
3
3
|
/// Responsive type scale — explicit font size per role, per breakpoint.
|
|
4
4
|
///
|
|
5
|
-
/// Single source of truth for typography sizes
|
|
6
|
-
///
|
|
7
|
-
///
|
|
5
|
+
/// Single source of truth for typography sizes, and the ONLY place breakpoints
|
|
6
|
+
/// touch type. There is no global multiplier and no per-component scaling: every
|
|
7
|
+
/// role declares its own size at each breakpoint here, exactly like design-tool
|
|
8
|
+
/// modes (Figma variables) or platform text styles. Components only read the
|
|
9
|
+
/// resolved theme role (`context.textTheme.*`); they never scale text themselves
|
|
10
|
+
/// by width. (The `WebViewportScale` zoom is a separate, web-only UI alignment
|
|
11
|
+
/// fix — it scales the whole canvas, not these type sizes.)
|
|
8
12
|
///
|
|
9
|
-
/// The
|
|
10
|
-
///
|
|
11
|
-
///
|
|
12
|
-
///
|
|
13
|
-
///
|
|
13
|
+
/// The ladder is harmonious and SEMANTIC — each role one clear step from the
|
|
14
|
+
/// next, and each maps to a PURPOSE, not a guessed size:
|
|
15
|
+
/// - **Headings & display SCALE with the viewport** (this is where real
|
|
16
|
+
/// responsiveness lives): big on desktop, stepping down on mobile so a hero or
|
|
17
|
+
/// page title never wraps badly on a phone.
|
|
18
|
+
/// - **Body & labels are STABLE across breakpoints** — one comfortable reading
|
|
19
|
+
/// size per role, the Material/Apple model. The body ladder uses MARKED 2px
|
|
20
|
+
/// steps so hierarchy is actually visible: section/card title 18, content-row
|
|
21
|
+
/// title / body 16, secondary / value / nav 14, caption 12. No 1px in-between
|
|
22
|
+
/// levels — a 15-vs-16 difference reads as identical, so it isn't a hierarchy.
|
|
23
|
+
/// A row's LABEL (16) and its VALUE (14) are deliberately different sizes.
|
|
14
24
|
///
|
|
15
25
|
/// To re-tune the whole app's typography, edit the numbers here — nothing else.
|
|
16
26
|
/// The live ramp is visible under Design System -> Typography (tabs per
|
|
@@ -51,27 +61,36 @@ class RampSize {
|
|
|
51
61
|
class KasyTypeScale {
|
|
52
62
|
const KasyTypeScale._();
|
|
53
63
|
|
|
54
|
-
// Hero / display — large, compress hard
|
|
64
|
+
// Hero / display — scale with space: large on desktop, compress hard toward
|
|
65
|
+
// mobile so a hero headline never wraps awkwardly on a phone.
|
|
55
66
|
static const displayLarge =
|
|
56
|
-
RampSize(mobile:
|
|
67
|
+
RampSize(mobile: 36, tablet: 46, desktop: 57, lineHeightRatio: 64 / 57);
|
|
57
68
|
static const displayMedium =
|
|
58
|
-
RampSize(mobile:
|
|
69
|
+
RampSize(mobile: 32, tablet: 38, desktop: 45, lineHeightRatio: 52 / 45);
|
|
59
70
|
|
|
60
|
-
// Headings —
|
|
71
|
+
// Headings — scale with space. Mobile anchors on the iOS title ladder
|
|
72
|
+
// (Title1 28 / Title2 22 / Title3 20); desktop grows for the wider canvas.
|
|
61
73
|
static const heading1 =
|
|
62
74
|
RampSize(mobile: 28, tablet: 32, desktop: 36, lineHeightRatio: 40 / 36);
|
|
63
75
|
static const heading2 =
|
|
64
76
|
RampSize(mobile: 22, tablet: 23, desktop: 24, lineHeightRatio: 32 / 24);
|
|
65
77
|
static const heading3 =
|
|
66
|
-
RampSize(mobile:
|
|
78
|
+
RampSize(mobile: 20, tablet: 20, desktop: 20, lineHeightRatio: 28 / 20);
|
|
79
|
+
// Section / card title — 18 (Tailwind text-lg): a visible step above body.
|
|
67
80
|
static const heading4 =
|
|
68
|
-
RampSize(mobile:
|
|
81
|
+
RampSize(mobile: 18, tablet: 18, desktop: 18, lineHeightRatio: 26 / 18);
|
|
69
82
|
|
|
70
|
-
// Body & labels —
|
|
83
|
+
// Body & labels — STABLE, with MARKED 2px steps so hierarchy is visible.
|
|
84
|
+
// Mirrors Material's body/label tiers (16 / 14 / 12); no 1px in-between levels
|
|
85
|
+
// (a 15-vs-16 difference reads as identical, so it isn't a hierarchy). Weight
|
|
86
|
+
// and purpose differentiate roles that share a size (Material does the same).
|
|
87
|
+
// Content-row title, body paragraphs, chat message:
|
|
71
88
|
static const bodyBase =
|
|
72
89
|
RampSize(mobile: 16, tablet: 16, desktop: 16, lineHeightRatio: 24 / 16);
|
|
90
|
+
// Secondary / value, dense nav row, card title, labels:
|
|
73
91
|
static const bodySm =
|
|
74
92
|
RampSize(mobile: 14, tablet: 14, desktop: 14, lineHeightRatio: 20 / 14);
|
|
93
|
+
// Caption, timestamp, fine print:
|
|
75
94
|
static const bodyXs =
|
|
76
95
|
RampSize(mobile: 12, tablet: 12, desktop: 12, lineHeightRatio: 16 / 12);
|
|
77
96
|
}
|
|
@@ -78,6 +78,15 @@ class UniversalThemeFactory extends KasyThemeDataFactory {
|
|
|
78
78
|
bottomSheetTheme: const BottomSheetThemeData(
|
|
79
79
|
constraints: BoxConstraints(),
|
|
80
80
|
),
|
|
81
|
+
// Drawers are flat panels (like the sidebar rail), not Material 3's
|
|
82
|
+
// rounded, shadowed sheet: square edge, surface fill, no drop shadow.
|
|
83
|
+
// Set once here so every Drawer in the app is consistent — no per-use
|
|
84
|
+
// shape/elevation overrides needed.
|
|
85
|
+
drawerTheme: DrawerThemeData(
|
|
86
|
+
backgroundColor: colors.surface,
|
|
87
|
+
shape: const RoundedRectangleBorder(),
|
|
88
|
+
elevation: 0,
|
|
89
|
+
),
|
|
81
90
|
),
|
|
82
91
|
);
|
|
83
92
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/// Exports a device-preview screenshot PNG to the clipboard (web), with a
|
|
2
|
+
/// download fallback.
|
|
3
|
+
///
|
|
4
|
+
/// The real implementation uses web-only APIs (`dart:js_interop` + `package:web`)
|
|
5
|
+
/// which do NOT compile on the Dart VM / native. Those are isolated in
|
|
6
|
+
/// `png_clipboard_web.dart` and pulled in ONLY on web via a conditional import;
|
|
7
|
+
/// everywhere else the no-op stub (`png_clipboard_io.dart`) is used. This keeps
|
|
8
|
+
/// `flutter test` (VM compile) and native builds green — the web-only code never
|
|
9
|
+
/// reaches them.
|
|
10
|
+
library;
|
|
11
|
+
|
|
12
|
+
export 'png_clipboard_io.dart'
|
|
13
|
+
if (dart.library.js_interop) 'png_clipboard_web.dart';
|
|
14
|
+
export 'png_export_result.dart';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import 'dart:typed_data';
|
|
2
|
+
|
|
3
|
+
import 'package:kasy_kit/core/web_device_preview/png_export_result.dart';
|
|
4
|
+
|
|
5
|
+
/// Non-web stub. The device preview is web-only, so this never runs at runtime —
|
|
6
|
+
/// it exists only so the VM / native build (and `flutter test`) compile without
|
|
7
|
+
/// the web interop.
|
|
8
|
+
Future<PngExportResult> copyOrDownloadPng(Uint8List bytes) async =>
|
|
9
|
+
PngExportResult.unavailable;
|