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
|
@@ -6,12 +6,15 @@ import 'package:flutter/foundation.dart';
|
|
|
6
6
|
import 'package:flutter/material.dart';
|
|
7
7
|
import 'package:flutter/rendering.dart';
|
|
8
8
|
import 'package:flutter/services.dart';
|
|
9
|
+
import 'package:google_fonts/google_fonts.dart';
|
|
9
10
|
import 'package:jiffy/jiffy.dart';
|
|
10
11
|
import 'package:kasy_kit/core/dev_inspector/dev_inspector.dart';
|
|
12
|
+
import 'package:kasy_kit/core/icons/kasy_icons.dart';
|
|
13
|
+
import 'package:kasy_kit/core/theme/providers/theme_provider.dart';
|
|
14
|
+
import 'package:kasy_kit/core/web_device_preview/png_clipboard.dart';
|
|
11
15
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
12
16
|
import 'package:provider/provider.dart';
|
|
13
17
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
14
|
-
import 'package:universal_html/html.dart' as html;
|
|
15
18
|
|
|
16
19
|
// Suffixed `_v2` because the default flipped from OFF to ON. Values saved under
|
|
17
20
|
// the old key were written while the default was OFF, so we ignore them and
|
|
@@ -25,8 +28,43 @@ const String _bgDarkPrefKey = 'web_device_preview_bg_dark';
|
|
|
25
28
|
const String _landscapePrefKey = 'web_device_preview_landscape';
|
|
26
29
|
const String _textScalePrefKey = 'web_device_preview_text_scale';
|
|
27
30
|
|
|
28
|
-
final ValueNotifier<bool> webDevicePreviewEnabledNotifier =
|
|
29
|
-
|
|
31
|
+
final ValueNotifier<bool> webDevicePreviewEnabledNotifier = ValueNotifier<bool>(
|
|
32
|
+
false,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/// True only once the device frame is actually ON SCREEN (not the instant the
|
|
36
|
+
/// toggle flips — the frame takes a moment to build). The web viewport scale
|
|
37
|
+
/// reads THIS, not [webDevicePreviewEnabledNotifier], so the scale drops to
|
|
38
|
+
/// native 1.0 only when the frame is up — otherwise the toggle would flash a
|
|
39
|
+
/// big, unframed, unscaled app while the frame builds.
|
|
40
|
+
final ValueNotifier<bool> webDevicePreviewActiveNotifier = ValueNotifier<bool>(
|
|
41
|
+
false,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Kit primary/accent (HeroUI blue). The chrome lives above the MaterialApp, so
|
|
45
|
+
// it can't read context.colors — mirror the design-system value here. Used to
|
|
46
|
+
// highlight active toggles so it's obvious at a glance what's turned on.
|
|
47
|
+
const Color _kAccent = Color(0xFF0485F7);
|
|
48
|
+
|
|
49
|
+
// ROADMAP: secondary controls (locale, landscape orientation, text-scale) are
|
|
50
|
+
// hidden for now — they'll move to a dedicated vertical toolbar later, while the
|
|
51
|
+
// main horizontal pill stays for quick access. Return true to bring them back
|
|
52
|
+
// inline. (A function, not a const, so the guarded widgets aren't dead code.)
|
|
53
|
+
bool _showSecondaryTools() => false;
|
|
54
|
+
|
|
55
|
+
// Inter — the kit's typeface. Centralizes the chrome's text styling so every
|
|
56
|
+
// label shares the design-system font.
|
|
57
|
+
TextStyle _chromeText({
|
|
58
|
+
required double size,
|
|
59
|
+
required FontWeight weight,
|
|
60
|
+
required Color color,
|
|
61
|
+
double spacing = 0,
|
|
62
|
+
}) => GoogleFonts.inter(
|
|
63
|
+
fontSize: size,
|
|
64
|
+
fontWeight: weight,
|
|
65
|
+
color: color,
|
|
66
|
+
letterSpacing: spacing,
|
|
67
|
+
);
|
|
30
68
|
|
|
31
69
|
/// Keyboard shortcut for toggling the web device preview chrome, formatted
|
|
32
70
|
/// for the current platform. Key names stay in English regardless of the
|
|
@@ -61,20 +99,6 @@ final List<DeviceInfo> _iPadDevices = [
|
|
|
61
99
|
Devices.ios.iPadPro13InchesM4,
|
|
62
100
|
];
|
|
63
101
|
|
|
64
|
-
// Compact labels so the pill stays narrow regardless of how many devices we add.
|
|
65
|
-
String _shortName(String name) => switch (name) {
|
|
66
|
-
'iPhone SE' => 'SE',
|
|
67
|
-
'iPhone 16' => '16',
|
|
68
|
-
'iPhone 16 Pro' => '16 Pro',
|
|
69
|
-
'iPad' => 'iPad',
|
|
70
|
-
'iPad Pro 11" (M4)' => '11"',
|
|
71
|
-
'iPad Pro 13" (M4)' => '13"',
|
|
72
|
-
'Samsung Galaxy A50' => 'A50',
|
|
73
|
-
'Google Pixel 9' => 'Pixel 9',
|
|
74
|
-
'Samsung Galaxy S25' => 'S25',
|
|
75
|
-
_ => name,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
102
|
class WebDevicePreview extends StatefulWidget {
|
|
79
103
|
const WebDevicePreview({super.key, required this.child});
|
|
80
104
|
|
|
@@ -92,14 +116,18 @@ class WebDevicePreview extends StatefulWidget {
|
|
|
92
116
|
class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
93
117
|
with WidgetsBindingObserver {
|
|
94
118
|
int _platform = 0; // 0 = iOS, 1 = Android, 2 = iPad
|
|
95
|
-
int _iosIndex = 1;
|
|
119
|
+
int _iosIndex = 1; // default: iPhone 16
|
|
96
120
|
int _androidIndex = 1; // default: Google Pixel 9
|
|
97
121
|
int _iPadIndex = 0;
|
|
98
122
|
bool _controlsVisible = false;
|
|
99
123
|
Timer? _controlsTimer;
|
|
124
|
+
String? _toast;
|
|
125
|
+
bool _toastIsError = false;
|
|
126
|
+
Timer? _toastTimer;
|
|
100
127
|
|
|
101
|
-
final ValueNotifier<DeviceInfo> _deviceNotifier =
|
|
102
|
-
|
|
128
|
+
final ValueNotifier<DeviceInfo> _deviceNotifier = ValueNotifier<DeviceInfo>(
|
|
129
|
+
_iosDevices[1],
|
|
130
|
+
); // iPhone 16
|
|
103
131
|
final ValueNotifier<bool> _frameVisibleNotifier = ValueNotifier<bool>(true);
|
|
104
132
|
final ValueNotifier<bool> _bgDarkNotifier = ValueNotifier<bool>(false);
|
|
105
133
|
final ValueNotifier<bool> _landscapeNotifier = ValueNotifier<bool>(false);
|
|
@@ -110,16 +138,16 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
110
138
|
static const _textScaleSteps = [1.0, 1.3, 1.5];
|
|
111
139
|
|
|
112
140
|
List<DeviceInfo> get _devices => switch (_platform) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
141
|
+
1 => _androidDevices,
|
|
142
|
+
2 => _iPadDevices,
|
|
143
|
+
_ => _iosDevices,
|
|
144
|
+
};
|
|
117
145
|
|
|
118
146
|
int get _currentIndex => switch (_platform) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
1 => _androidIndex,
|
|
148
|
+
2 => _iPadIndex,
|
|
149
|
+
_ => _iosIndex,
|
|
150
|
+
};
|
|
123
151
|
|
|
124
152
|
DeviceInfo get _currentDevice => _devices[_currentIndex];
|
|
125
153
|
|
|
@@ -132,6 +160,7 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
132
160
|
);
|
|
133
161
|
webDevicePreviewEnabledNotifier.addListener(_onEnabledChanged);
|
|
134
162
|
_bgDarkNotifier.addListener(_onBgChanged);
|
|
163
|
+
devInspectorToastNotifier.addListener(_onInspectorToast);
|
|
135
164
|
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
|
136
165
|
unawaited(_bootstrap());
|
|
137
166
|
}
|
|
@@ -174,6 +203,7 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
174
203
|
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
|
175
204
|
webDevicePreviewEnabledNotifier.removeListener(_onEnabledChanged);
|
|
176
205
|
_bgDarkNotifier.removeListener(_onBgChanged);
|
|
206
|
+
devInspectorToastNotifier.removeListener(_onInspectorToast);
|
|
177
207
|
_deviceNotifier.dispose();
|
|
178
208
|
_frameVisibleNotifier.dispose();
|
|
179
209
|
_bgDarkNotifier.dispose();
|
|
@@ -181,9 +211,31 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
181
211
|
_textScaleNotifier.dispose();
|
|
182
212
|
_localeNotifier.dispose();
|
|
183
213
|
_controlsTimer?.cancel();
|
|
214
|
+
_toastTimer?.cancel();
|
|
184
215
|
super.dispose();
|
|
185
216
|
}
|
|
186
217
|
|
|
218
|
+
/// Transient confirmation pill shown in the chrome (e.g. "Image copied").
|
|
219
|
+
void _showToast(String message, {bool isError = false}) {
|
|
220
|
+
_toastTimer?.cancel();
|
|
221
|
+
setState(() {
|
|
222
|
+
_toast = message;
|
|
223
|
+
_toastIsError = isError;
|
|
224
|
+
});
|
|
225
|
+
_toastTimer = Timer(const Duration(milliseconds: 2200), () {
|
|
226
|
+
if (mounted) setState(() => _toast = null);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// The DevInspector hands its "copied" message here so it renders in the same
|
|
231
|
+
/// spot and size as the chrome's own toasts (instead of inside the device).
|
|
232
|
+
void _onInspectorToast() {
|
|
233
|
+
final msg = devInspectorToastNotifier.value;
|
|
234
|
+
if (msg == null) return;
|
|
235
|
+
devInspectorToastNotifier.value = null;
|
|
236
|
+
if (mounted) _showToast(msg.message, isError: msg.isError);
|
|
237
|
+
}
|
|
238
|
+
|
|
187
239
|
void _onEnabledChanged() {
|
|
188
240
|
// Our chrome already surfaces inspector state via the pill, so suppress
|
|
189
241
|
// the DevInspector's in-app status pill while the preview is on.
|
|
@@ -198,14 +250,55 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
198
250
|
if (mounted) setState(() => _controlsVisible = true);
|
|
199
251
|
});
|
|
200
252
|
setState(() {}); // rebuild DevicePreview with enabled: true
|
|
253
|
+
// [webDevicePreviewActiveNotifier] flips to true only once the frame is
|
|
254
|
+
// actually built — see [_buildFramedContent] — so the web scale stays put
|
|
255
|
+
// until then (no big-unframed-app flash during the build).
|
|
201
256
|
} else {
|
|
202
257
|
_controlsTimer?.cancel();
|
|
258
|
+
// Leaving the frame: restore the web scale immediately.
|
|
259
|
+
webDevicePreviewActiveNotifier.value = false;
|
|
203
260
|
setState(() => _controlsVisible = false);
|
|
204
261
|
}
|
|
205
262
|
}
|
|
206
263
|
|
|
207
264
|
void _onBgChanged() => setState(() {});
|
|
208
265
|
|
|
266
|
+
/// Builds the app content placed INSIDE the device frame. DevicePreview calls
|
|
267
|
+
/// this as it brings the framed environment up, so it doubles as the "frame is
|
|
268
|
+
/// on screen now" signal: after this paints we flip
|
|
269
|
+
/// [webDevicePreviewActiveNotifier], which is what lets the web viewport scale
|
|
270
|
+
/// drop to native 1.0 — and only then, so the toggle never shows a flash of
|
|
271
|
+
/// big, unframed, unscaled app while the frame is still building.
|
|
272
|
+
Widget _buildFramedContent(BuildContext context) {
|
|
273
|
+
if (webDevicePreviewEnabledNotifier.value &&
|
|
274
|
+
!webDevicePreviewActiveNotifier.value) {
|
|
275
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
276
|
+
if (mounted && webDevicePreviewEnabledNotifier.value) {
|
|
277
|
+
webDevicePreviewActiveNotifier.value = true;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return ListenableBuilder(
|
|
282
|
+
listenable: _textScaleNotifier,
|
|
283
|
+
builder: (ctx, _) => MediaQuery(
|
|
284
|
+
data: MediaQuery.of(ctx).copyWith(
|
|
285
|
+
textScaler: TextScaler.linear(_textScaleNotifier.value),
|
|
286
|
+
// DevicePreview hard-codes platformBrightness to light inside the
|
|
287
|
+
// simulated frame, which breaks MaterialApp's ThemeMode.system.
|
|
288
|
+
// Forward the real host brightness so "system" tracks the OS.
|
|
289
|
+
platformBrightness:
|
|
290
|
+
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
|
291
|
+
),
|
|
292
|
+
child: _DeviceSwitchBridge(
|
|
293
|
+
deviceNotifier: _deviceNotifier,
|
|
294
|
+
frameVisibleNotifier: _frameVisibleNotifier,
|
|
295
|
+
landscapeNotifier: _landscapeNotifier,
|
|
296
|
+
child: widget.child,
|
|
297
|
+
),
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
209
302
|
Future<void> _bootstrap() async {
|
|
210
303
|
final prefs = await SharedPreferences.getInstance();
|
|
211
304
|
|
|
@@ -283,9 +376,16 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
283
376
|
}
|
|
284
377
|
|
|
285
378
|
void _setPlatform(int p) {
|
|
286
|
-
setState(()
|
|
379
|
+
setState(() {
|
|
380
|
+
_platform = p;
|
|
381
|
+
// Choosing a platform always lands on its first device:
|
|
382
|
+
// iPad → plain iPad, Android → Samsung Galaxy A50.
|
|
383
|
+
if (p == 1) _androidIndex = 0;
|
|
384
|
+
if (p == 2) _iPadIndex = 0;
|
|
385
|
+
});
|
|
287
386
|
_deviceNotifier.value = _currentDevice;
|
|
288
387
|
unawaited(_savePlatform());
|
|
388
|
+
if (p == 1 || p == 2) unawaited(_saveDeviceIndices());
|
|
289
389
|
}
|
|
290
390
|
|
|
291
391
|
void _toggleFrame() {
|
|
@@ -301,10 +401,6 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
301
401
|
devInspectorActiveNotifier.value = !devInspectorActiveNotifier.value;
|
|
302
402
|
}
|
|
303
403
|
|
|
304
|
-
void _copyInspection() {
|
|
305
|
-
devInspectorCopyTriggerNotifier.value = true;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
404
|
void _toggleLandscape() {
|
|
309
405
|
_landscapeNotifier.value = !_landscapeNotifier.value;
|
|
310
406
|
unawaited(_saveLandscape());
|
|
@@ -330,44 +426,34 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
330
426
|
}
|
|
331
427
|
|
|
332
428
|
Future<void> _takeScreenshot() async {
|
|
333
|
-
final boundary =
|
|
334
|
-
|
|
429
|
+
final boundary =
|
|
430
|
+
_screenshotKey.currentContext?.findRenderObject()
|
|
431
|
+
as RenderRepaintBoundary?;
|
|
335
432
|
if (boundary == null) return;
|
|
336
433
|
final dpr =
|
|
337
|
-
WidgetsBinding
|
|
338
|
-
|
|
434
|
+
WidgetsBinding
|
|
435
|
+
.instance
|
|
436
|
+
.platformDispatcher
|
|
437
|
+
.implicitView
|
|
438
|
+
?.devicePixelRatio ??
|
|
439
|
+
2.0;
|
|
339
440
|
final image = await boundary.toImage(pixelRatio: dpr);
|
|
340
441
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
341
442
|
if (byteData == null) return;
|
|
342
443
|
final bytes = byteData.buffer.asUint8List();
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
switch (_platform) {
|
|
357
|
-
case 1:
|
|
358
|
-
_androidIndex =
|
|
359
|
-
(_androidIndex - 1 + _androidDevices.length) %
|
|
360
|
-
_androidDevices.length;
|
|
361
|
-
case 2:
|
|
362
|
-
_iPadIndex =
|
|
363
|
-
(_iPadIndex - 1 + _iPadDevices.length) % _iPadDevices.length;
|
|
364
|
-
default:
|
|
365
|
-
_iosIndex =
|
|
366
|
-
(_iosIndex - 1 + _iosDevices.length) % _iosDevices.length;
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
_deviceNotifier.value = _currentDevice;
|
|
370
|
-
unawaited(_saveDeviceIndices());
|
|
444
|
+
// Copy the PNG to the clipboard (with a download fallback). The web-only
|
|
445
|
+
// implementation lives behind a conditional import (png_clipboard.dart) so
|
|
446
|
+
// this file stays VM/native-compilable and never breaks `flutter test`.
|
|
447
|
+
final result = await copyOrDownloadPng(bytes);
|
|
448
|
+
if (!mounted) return;
|
|
449
|
+
switch (result) {
|
|
450
|
+
case PngExportResult.copied:
|
|
451
|
+
_showToast(t.webDevicePreview.imageCopied);
|
|
452
|
+
case PngExportResult.downloaded:
|
|
453
|
+
_showToast(t.webDevicePreview.imageDownloaded);
|
|
454
|
+
case PngExportResult.unavailable:
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
371
457
|
}
|
|
372
458
|
|
|
373
459
|
void _next() {
|
|
@@ -389,102 +475,139 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
389
475
|
Widget build(BuildContext context) {
|
|
390
476
|
final enabled = webDevicePreviewEnabledNotifier.value;
|
|
391
477
|
|
|
478
|
+
// App theme (light/dark) lives in ThemeProvider, which sits ABOVE this
|
|
479
|
+
// widget — reading it here rebuilds the toolbar when the theme flips.
|
|
480
|
+
final AppTheme appTheme = ThemeProvider.of(context);
|
|
481
|
+
final bool appDark = appTheme.effectiveMode == ThemeMode.dark;
|
|
482
|
+
|
|
392
483
|
// ThemeData.light() in Flutter 3.x M3 uses a lavender-tinted surface.
|
|
393
484
|
// Passing backgroundColor directly avoids that and gives us an exact color.
|
|
394
|
-
final canvasColor =
|
|
395
|
-
|
|
485
|
+
final canvasColor = _bgDarkNotifier.value
|
|
486
|
+
? const Color(0xFF1C1C1E)
|
|
487
|
+
: Colors.white;
|
|
396
488
|
|
|
397
489
|
final preview = DevicePreview(
|
|
398
490
|
enabled: enabled,
|
|
399
491
|
isToolbarVisible: false,
|
|
400
492
|
backgroundColor: canvasColor,
|
|
493
|
+
// Reserve room at the top so the floating toolbar never sits on the device.
|
|
494
|
+
// DevicePreview fills the whole viewport, so this is the single source of
|
|
495
|
+
// background — there is no second surface that could create a seam.
|
|
496
|
+
padding: enabled
|
|
497
|
+
? const EdgeInsets.only(top: 56, left: 20, right: 20, bottom: 20)
|
|
498
|
+
: null,
|
|
401
499
|
storage: DevicePreviewStorage.none(),
|
|
402
500
|
defaultDevice: _currentDevice,
|
|
403
501
|
devices: [..._iosDevices, ..._androidDevices, ..._iPadDevices],
|
|
404
|
-
builder:
|
|
405
|
-
listenable: _textScaleNotifier,
|
|
406
|
-
builder: (ctx, _) => MediaQuery(
|
|
407
|
-
data: MediaQuery.of(ctx).copyWith(
|
|
408
|
-
textScaler: TextScaler.linear(_textScaleNotifier.value),
|
|
409
|
-
// DevicePreview hard-codes platformBrightness to light inside the
|
|
410
|
-
// simulated frame, which breaks MaterialApp's ThemeMode.system.
|
|
411
|
-
// Forward the real host brightness so "system" tracks the OS.
|
|
412
|
-
platformBrightness: WidgetsBinding
|
|
413
|
-
.instance.platformDispatcher.platformBrightness,
|
|
414
|
-
),
|
|
415
|
-
child: _DeviceSwitchBridge(
|
|
416
|
-
deviceNotifier: _deviceNotifier,
|
|
417
|
-
frameVisibleNotifier: _frameVisibleNotifier,
|
|
418
|
-
landscapeNotifier: _landscapeNotifier,
|
|
419
|
-
child: widget.child,
|
|
420
|
-
),
|
|
421
|
-
),
|
|
422
|
-
),
|
|
502
|
+
builder: _buildFramedContent,
|
|
423
503
|
);
|
|
424
504
|
|
|
425
505
|
if (!enabled) return preview;
|
|
426
506
|
|
|
427
507
|
return TapRegionSurface(
|
|
428
508
|
child: Directionality(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
child: _DevInspectorExternalHighlight(),
|
|
509
|
+
textDirection: TextDirection.ltr,
|
|
510
|
+
child: Stack(
|
|
511
|
+
children: [
|
|
512
|
+
// DevicePreview paints the entire viewport (single background, no
|
|
513
|
+
// seam). The device frame is pushed below the toolbar by the top
|
|
514
|
+
// padding above, so the toolbar never overlaps it.
|
|
515
|
+
RepaintBoundary(key: _screenshotKey, child: preview),
|
|
516
|
+
// Inspector highlight drawn ABOVE the device frame so widgets that
|
|
517
|
+
// hug the viewport edges (AppBar, bottom nav, …) keep a full border.
|
|
518
|
+
const Positioned.fill(
|
|
519
|
+
child: IgnorePointer(child: _DevInspectorExternalHighlight()),
|
|
440
520
|
),
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
521
|
+
// Floating toolbar — overlaid at the top, centered. Opening its
|
|
522
|
+
// dropdowns never pushes or resizes the device (they overlay).
|
|
523
|
+
Positioned(
|
|
524
|
+
top: 12,
|
|
525
|
+
left: 0,
|
|
526
|
+
right: 0,
|
|
527
|
+
child: AnimatedOpacity(
|
|
528
|
+
opacity: _controlsVisible ? 1.0 : 0.0,
|
|
529
|
+
duration: const Duration(milliseconds: 400),
|
|
530
|
+
child: IgnorePointer(
|
|
531
|
+
ignoring: !_controlsVisible,
|
|
532
|
+
child: Center(
|
|
533
|
+
child: ListenableBuilder(
|
|
534
|
+
listenable: Listenable.merge([
|
|
535
|
+
_frameVisibleNotifier,
|
|
536
|
+
_bgDarkNotifier,
|
|
537
|
+
_landscapeNotifier,
|
|
538
|
+
_textScaleNotifier,
|
|
539
|
+
_localeNotifier,
|
|
540
|
+
devInspectorActiveNotifier,
|
|
541
|
+
]),
|
|
542
|
+
builder: (context, child) => _PreviewControls(
|
|
543
|
+
platform: _platform,
|
|
544
|
+
deviceName: _currentDevice.name,
|
|
545
|
+
dark: _bgDarkNotifier.value,
|
|
546
|
+
appDark: appDark,
|
|
547
|
+
onToggleAppTheme: appTheme.toggle,
|
|
548
|
+
frameVisible: _frameVisibleNotifier.value,
|
|
549
|
+
isLandscape: _landscapeNotifier.value,
|
|
550
|
+
textScale: _textScaleNotifier.value,
|
|
551
|
+
currentLocale: _localeNotifier.value,
|
|
552
|
+
inspectorEnabled: devInspectorActiveNotifier.value,
|
|
553
|
+
onPlatformChanged: _setPlatform,
|
|
554
|
+
onNext: _next,
|
|
555
|
+
onToggleFrame: _toggleFrame,
|
|
556
|
+
onToggleLandscape: _toggleLandscape,
|
|
557
|
+
onCycleTextScale: _cycleTextScale,
|
|
558
|
+
onCycleLocale: _cycleLocale,
|
|
559
|
+
onToggleInspector: _toggleInspector,
|
|
560
|
+
onScreenshot: () => unawaited(_takeScreenshot()),
|
|
561
|
+
onClose: () => unawaited(_close()),
|
|
562
|
+
),
|
|
563
|
+
),
|
|
564
|
+
),
|
|
565
|
+
),
|
|
566
|
+
),
|
|
567
|
+
),
|
|
568
|
+
// Theme toggle — standalone sun/moon button pinned to the
|
|
569
|
+
// bottom-right corner, independent of the toolbar.
|
|
570
|
+
Positioned(
|
|
571
|
+
bottom: 10,
|
|
572
|
+
right: 10,
|
|
573
|
+
child: AnimatedOpacity(
|
|
574
|
+
opacity: _controlsVisible ? 1.0 : 0.0,
|
|
575
|
+
duration: const Duration(milliseconds: 400),
|
|
576
|
+
child: IgnorePointer(
|
|
577
|
+
ignoring: !_controlsVisible,
|
|
578
|
+
child: ListenableBuilder(
|
|
579
|
+
listenable: _bgDarkNotifier,
|
|
580
|
+
builder: (context, child) => _ThemeCornerButton(
|
|
581
|
+
dark: _bgDarkNotifier.value,
|
|
582
|
+
onTap: _toggleBg,
|
|
583
|
+
),
|
|
584
|
+
),
|
|
585
|
+
),
|
|
586
|
+
),
|
|
587
|
+
),
|
|
588
|
+
// Transient confirmation toast (e.g. screenshot copied).
|
|
589
|
+
Positioned(
|
|
590
|
+
left: 0,
|
|
591
|
+
right: 0,
|
|
592
|
+
bottom: 24,
|
|
448
593
|
child: IgnorePointer(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
builder: (context, child) => _PreviewControls(
|
|
460
|
-
platform: _platform,
|
|
461
|
-
deviceName: _shortName(_currentDevice.name),
|
|
462
|
-
frameVisible: _frameVisibleNotifier.value,
|
|
463
|
-
bgDark: _bgDarkNotifier.value,
|
|
464
|
-
isLandscape: _landscapeNotifier.value,
|
|
465
|
-
textScale: _textScaleNotifier.value,
|
|
466
|
-
currentLocale: _localeNotifier.value,
|
|
467
|
-
inspectorEnabled: devInspectorActiveNotifier.value,
|
|
468
|
-
onPlatformChanged: _setPlatform,
|
|
469
|
-
onPrev: _prev,
|
|
470
|
-
onNext: _next,
|
|
471
|
-
onToggleFrame: _toggleFrame,
|
|
472
|
-
onToggleBg: _toggleBg,
|
|
473
|
-
onToggleLandscape: _toggleLandscape,
|
|
474
|
-
onCycleTextScale: _cycleTextScale,
|
|
475
|
-
onCycleLocale: _cycleLocale,
|
|
476
|
-
onToggleInspector: _toggleInspector,
|
|
477
|
-
onCopyInspection: _copyInspection,
|
|
478
|
-
onScreenshot: () => unawaited(_takeScreenshot()),
|
|
479
|
-
onClose: () => unawaited(_close()),
|
|
594
|
+
child: Center(
|
|
595
|
+
child: AnimatedSwitcher(
|
|
596
|
+
duration: const Duration(milliseconds: 200),
|
|
597
|
+
child: _toast == null
|
|
598
|
+
? const SizedBox.shrink()
|
|
599
|
+
: _Toast(
|
|
600
|
+
key: ValueKey(_toast),
|
|
601
|
+
message: _toast!,
|
|
602
|
+
isError: _toastIsError,
|
|
603
|
+
),
|
|
480
604
|
),
|
|
481
605
|
),
|
|
482
606
|
),
|
|
483
607
|
),
|
|
484
|
-
|
|
485
|
-
|
|
608
|
+
],
|
|
609
|
+
),
|
|
486
610
|
),
|
|
487
|
-
),
|
|
488
611
|
);
|
|
489
612
|
}
|
|
490
613
|
}
|
|
@@ -607,44 +730,45 @@ class _PreviewControls extends StatefulWidget {
|
|
|
607
730
|
const _PreviewControls({
|
|
608
731
|
required this.platform,
|
|
609
732
|
required this.deviceName,
|
|
733
|
+
required this.dark,
|
|
734
|
+
required this.appDark,
|
|
735
|
+
required this.onToggleAppTheme,
|
|
610
736
|
required this.frameVisible,
|
|
611
|
-
required this.bgDark,
|
|
612
737
|
required this.isLandscape,
|
|
613
738
|
required this.textScale,
|
|
614
739
|
required this.currentLocale,
|
|
615
740
|
required this.inspectorEnabled,
|
|
616
741
|
required this.onPlatformChanged,
|
|
617
|
-
required this.onPrev,
|
|
618
742
|
required this.onNext,
|
|
619
743
|
required this.onToggleFrame,
|
|
620
|
-
required this.onToggleBg,
|
|
621
744
|
required this.onToggleLandscape,
|
|
622
745
|
required this.onCycleTextScale,
|
|
623
746
|
required this.onCycleLocale,
|
|
624
747
|
required this.onToggleInspector,
|
|
625
|
-
required this.onCopyInspection,
|
|
626
748
|
required this.onScreenshot,
|
|
627
749
|
required this.onClose,
|
|
628
750
|
});
|
|
629
751
|
|
|
630
752
|
final int platform;
|
|
631
753
|
final String deviceName;
|
|
754
|
+
final bool dark;
|
|
755
|
+
|
|
756
|
+
/// App theme (light/dark) state + toggle — flips the real app ThemeMode.
|
|
757
|
+
final bool appDark;
|
|
758
|
+
final VoidCallback onToggleAppTheme;
|
|
759
|
+
|
|
632
760
|
final bool frameVisible;
|
|
633
|
-
final bool bgDark;
|
|
634
761
|
final bool isLandscape;
|
|
635
762
|
final double textScale;
|
|
636
763
|
final AppLocale currentLocale;
|
|
637
764
|
final bool inspectorEnabled;
|
|
638
765
|
final ValueChanged<int> onPlatformChanged;
|
|
639
|
-
final VoidCallback onPrev;
|
|
640
766
|
final VoidCallback onNext;
|
|
641
767
|
final VoidCallback onToggleFrame;
|
|
642
|
-
final VoidCallback onToggleBg;
|
|
643
768
|
final VoidCallback onToggleLandscape;
|
|
644
769
|
final VoidCallback onCycleTextScale;
|
|
645
770
|
final VoidCallback onCycleLocale;
|
|
646
771
|
final VoidCallback onToggleInspector;
|
|
647
|
-
final VoidCallback onCopyInspection;
|
|
648
772
|
final VoidCallback onScreenshot;
|
|
649
773
|
final VoidCallback onClose;
|
|
650
774
|
|
|
@@ -654,137 +778,60 @@ class _PreviewControls extends StatefulWidget {
|
|
|
654
778
|
|
|
655
779
|
class _PreviewControlsState extends State<_PreviewControls> {
|
|
656
780
|
bool _menuOpen = false;
|
|
657
|
-
bool _toolsMenuOpen = false;
|
|
658
|
-
bool _minimized = true;
|
|
659
781
|
bool _platformBtnHovered = false;
|
|
660
|
-
bool
|
|
661
|
-
bool _shadowVisible = true;
|
|
662
|
-
Timer? _shadowTimer;
|
|
782
|
+
bool _deviceBtnHovered = false;
|
|
663
783
|
|
|
664
784
|
static const _platformLabels = ['iOS', 'Android', 'iPad'];
|
|
665
785
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
786
|
+
// Color scheme adapts to the preview theme: dark pill (white content) in dark
|
|
787
|
+
// mode, white pill (dark content) in light mode.
|
|
788
|
+
Color get _pillColor => widget.dark ? const Color(0xFF2C2C2E) : Colors.white;
|
|
789
|
+
Color get _menuColor => widget.dark ? const Color(0xFF3A3A3C) : Colors.white;
|
|
790
|
+
// Foreground (text/icons) base — alphas are derived from this.
|
|
791
|
+
Color get _fg => widget.dark ? Colors.white : const Color(0xFF1C1C1E);
|
|
792
|
+
|
|
793
|
+
BoxDecoration get _pillDecoration => BoxDecoration(
|
|
794
|
+
color: _pillColor,
|
|
795
|
+
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
|
669
796
|
);
|
|
670
797
|
|
|
798
|
+
// Tight, low-spread shadow: enough to lift the pill off the canvas without
|
|
799
|
+
// casting a wide smudge below it that reads as a horizontal "division" line.
|
|
671
800
|
static const _pillShadow = [
|
|
672
|
-
BoxShadow(
|
|
673
|
-
color: Color(0x73000000),
|
|
674
|
-
blurRadius: 20,
|
|
675
|
-
offset: Offset(0, 6),
|
|
676
|
-
),
|
|
801
|
+
BoxShadow(color: Color(0x33000000), blurRadius: 8, offset: Offset(0, 2)),
|
|
677
802
|
];
|
|
678
803
|
|
|
679
804
|
static String _textScaleLabel(double scale) => switch (scale) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
void _toggleMenu() => setState(() {
|
|
686
|
-
_menuOpen = !_menuOpen;
|
|
687
|
-
if (_menuOpen) _toolsMenuOpen = false;
|
|
688
|
-
});
|
|
805
|
+
1.0 => '1×',
|
|
806
|
+
1.3 => '1.3×',
|
|
807
|
+
_ => '1.5×',
|
|
808
|
+
};
|
|
689
809
|
|
|
690
|
-
void
|
|
691
|
-
_toolsMenuOpen = !_toolsMenuOpen;
|
|
692
|
-
if (_toolsMenuOpen) _menuOpen = false;
|
|
693
|
-
});
|
|
810
|
+
void _toggleMenu() => setState(() => _menuOpen = !_menuOpen);
|
|
694
811
|
|
|
695
812
|
void _selectPlatform(int i) {
|
|
696
813
|
widget.onPlatformChanged(i);
|
|
697
|
-
setState(()
|
|
698
|
-
_menuOpen = false;
|
|
699
|
-
_toolsMenuOpen = false;
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
@override
|
|
704
|
-
void dispose() {
|
|
705
|
-
_shadowTimer?.cancel();
|
|
706
|
-
super.dispose();
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
void _hideShadowDuringAnimation() {
|
|
710
|
-
_shadowTimer?.cancel();
|
|
711
|
-
setState(() => _shadowVisible = false);
|
|
712
|
-
_shadowTimer = Timer(const Duration(milliseconds: 140), () {
|
|
713
|
-
if (mounted) setState(() => _shadowVisible = true);
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
void _minimize() {
|
|
718
|
-
setState(() {
|
|
719
|
-
_minimized = true;
|
|
720
|
-
_menuOpen = false;
|
|
721
|
-
_toolsMenuOpen = false;
|
|
722
|
-
});
|
|
723
|
-
_hideShadowDuringAnimation();
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
void _maximize() {
|
|
727
|
-
setState(() => _minimized = false);
|
|
728
|
-
_hideShadowDuringAnimation();
|
|
814
|
+
setState(() => _menuOpen = false);
|
|
729
815
|
}
|
|
730
816
|
|
|
731
817
|
@override
|
|
732
818
|
Widget build(BuildContext context) {
|
|
733
819
|
return TapRegion(
|
|
734
820
|
onTapOutside: (_) {
|
|
735
|
-
if (_menuOpen
|
|
736
|
-
setState(() {
|
|
737
|
-
_menuOpen = false;
|
|
738
|
-
_toolsMenuOpen = false;
|
|
739
|
-
});
|
|
740
|
-
}
|
|
821
|
+
if (_menuOpen) setState(() => _menuOpen = false);
|
|
741
822
|
},
|
|
742
823
|
child: Column(
|
|
743
824
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
744
825
|
mainAxisSize: MainAxisSize.min,
|
|
745
826
|
children: [
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
decoration: const BoxDecoration(
|
|
751
|
-
borderRadius: BorderRadius.all(Radius.circular(24)),
|
|
752
|
-
boxShadow: _pillShadow,
|
|
753
|
-
),
|
|
754
|
-
child: AnimatedSize(
|
|
755
|
-
duration: const Duration(milliseconds: 140),
|
|
756
|
-
curve: Curves.easeInOutCubic,
|
|
757
|
-
alignment: Alignment.centerRight,
|
|
758
|
-
child: _minimized ? _buildMinimizedPill() : _buildPill(),
|
|
759
|
-
),
|
|
827
|
+
DecoratedBox(
|
|
828
|
+
decoration: const BoxDecoration(
|
|
829
|
+
borderRadius: BorderRadius.all(Radius.circular(24)),
|
|
830
|
+
boxShadow: _pillShadow,
|
|
760
831
|
),
|
|
832
|
+
child: _buildPill(),
|
|
761
833
|
),
|
|
762
|
-
if (_menuOpen
|
|
763
|
-
if (_toolsMenuOpen && !_minimized) _buildToolsMenu(),
|
|
764
|
-
],
|
|
765
|
-
),
|
|
766
|
-
);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
Widget _buildMinimizedPill() {
|
|
770
|
-
return Container(
|
|
771
|
-
height: 40,
|
|
772
|
-
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
773
|
-
decoration: _pillDecoration,
|
|
774
|
-
child: Row(
|
|
775
|
-
mainAxisSize: MainAxisSize.min,
|
|
776
|
-
children: [
|
|
777
|
-
_IconBtn(
|
|
778
|
-
icon: Icons.unfold_more_rounded,
|
|
779
|
-
onTap: _maximize,
|
|
780
|
-
),
|
|
781
|
-
_VerticalDivider(),
|
|
782
|
-
_IconBtn(
|
|
783
|
-
icon: Icons.close_rounded,
|
|
784
|
-
onTap: widget.onClose,
|
|
785
|
-
color: Colors.white54,
|
|
786
|
-
),
|
|
787
|
-
const SizedBox(width: 2),
|
|
834
|
+
if (_menuOpen) _buildMenu(),
|
|
788
835
|
],
|
|
789
836
|
),
|
|
790
837
|
);
|
|
@@ -792,7 +839,7 @@ class _PreviewControlsState extends State<_PreviewControls> {
|
|
|
792
839
|
|
|
793
840
|
Widget _buildPill() {
|
|
794
841
|
return Container(
|
|
795
|
-
height:
|
|
842
|
+
height: 34,
|
|
796
843
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
797
844
|
decoration: _pillDecoration,
|
|
798
845
|
child: Row(
|
|
@@ -804,131 +851,144 @@ class _PreviewControlsState extends State<_PreviewControls> {
|
|
|
804
851
|
onExit: (_) => setState(() => _platformBtnHovered = false),
|
|
805
852
|
cursor: SystemMouseCursors.click,
|
|
806
853
|
child: GestureDetector(
|
|
807
|
-
|
|
808
|
-
child: AnimatedContainer(
|
|
809
|
-
duration: const Duration(milliseconds: 160),
|
|
810
|
-
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
|
811
|
-
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
|
812
|
-
decoration: BoxDecoration(
|
|
813
|
-
color: Colors.white.withValues(
|
|
814
|
-
alpha: _menuOpen ? 0.22 : (_platformBtnHovered ? 0.20 : 0.14),
|
|
815
|
-
),
|
|
816
|
-
borderRadius: BorderRadius.circular(20),
|
|
817
|
-
),
|
|
818
|
-
child: Row(
|
|
819
|
-
mainAxisSize: MainAxisSize.min,
|
|
820
|
-
children: [
|
|
821
|
-
Text(
|
|
822
|
-
_platformLabels[widget.platform],
|
|
823
|
-
style: const TextStyle(
|
|
824
|
-
color: Colors.white,
|
|
825
|
-
fontSize: 12,
|
|
826
|
-
fontWeight: FontWeight.w600,
|
|
827
|
-
),
|
|
828
|
-
),
|
|
829
|
-
const SizedBox(width: 2),
|
|
830
|
-
AnimatedRotation(
|
|
831
|
-
turns: _menuOpen ? 0.5 : 0,
|
|
832
|
-
duration: const Duration(milliseconds: 160),
|
|
833
|
-
child: const Icon(
|
|
834
|
-
Icons.keyboard_arrow_down_rounded,
|
|
835
|
-
color: Colors.white70,
|
|
836
|
-
size: 13,
|
|
837
|
-
),
|
|
838
|
-
),
|
|
839
|
-
],
|
|
840
|
-
),
|
|
841
|
-
),
|
|
842
|
-
),
|
|
843
|
-
),
|
|
844
|
-
_VerticalDivider(),
|
|
845
|
-
_IconBtn(icon: Icons.chevron_left_rounded, onTap: widget.onPrev),
|
|
846
|
-
Padding(
|
|
847
|
-
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
848
|
-
child: Text(
|
|
849
|
-
widget.deviceName,
|
|
850
|
-
style: const TextStyle(
|
|
851
|
-
color: Colors.white,
|
|
852
|
-
fontSize: 12,
|
|
853
|
-
fontWeight: FontWeight.w500,
|
|
854
|
-
letterSpacing: -0.2,
|
|
855
|
-
),
|
|
856
|
-
),
|
|
857
|
-
),
|
|
858
|
-
_IconBtn(icon: Icons.chevron_right_rounded, onTap: widget.onNext),
|
|
859
|
-
// Locale chip
|
|
860
|
-
_VerticalDivider(),
|
|
861
|
-
_PillChip(
|
|
862
|
-
label: widget.currentLocale.languageCode.toUpperCase(),
|
|
863
|
-
onTap: widget.onCycleLocale,
|
|
864
|
-
),
|
|
865
|
-
// Tools dropdown
|
|
866
|
-
_VerticalDivider(),
|
|
867
|
-
MouseRegion(
|
|
868
|
-
onEnter: (_) => setState(() => _toolsBtnHovered = true),
|
|
869
|
-
onExit: (_) => setState(() => _toolsBtnHovered = false),
|
|
870
|
-
cursor: SystemMouseCursors.click,
|
|
871
|
-
child: GestureDetector(
|
|
872
|
-
onTap: _toggleToolsMenu,
|
|
854
|
+
onTap: _toggleMenu,
|
|
873
855
|
child: AnimatedContainer(
|
|
874
856
|
duration: const Duration(milliseconds: 160),
|
|
875
857
|
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
|
876
|
-
padding: const EdgeInsets.symmetric(
|
|
858
|
+
padding: const EdgeInsets.symmetric(
|
|
859
|
+
horizontal: 10,
|
|
860
|
+
vertical: 5,
|
|
861
|
+
),
|
|
877
862
|
decoration: BoxDecoration(
|
|
878
|
-
|
|
879
|
-
|
|
863
|
+
// No resting fill — it just alternates platforms. Subtle fill
|
|
864
|
+
// only while open or hovered.
|
|
865
|
+
color: _fg.withValues(
|
|
866
|
+
alpha: _menuOpen
|
|
867
|
+
? 0.16
|
|
868
|
+
: (_platformBtnHovered ? 0.12 : 0.0),
|
|
880
869
|
),
|
|
881
870
|
borderRadius: BorderRadius.circular(20),
|
|
882
871
|
),
|
|
883
872
|
child: Row(
|
|
884
873
|
mainAxisSize: MainAxisSize.min,
|
|
885
874
|
children: [
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
style:
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
875
|
+
Text(
|
|
876
|
+
_platformLabels[widget.platform],
|
|
877
|
+
style: _chromeText(
|
|
878
|
+
size: 12,
|
|
879
|
+
weight: FontWeight.w600,
|
|
880
|
+
color: _fg,
|
|
892
881
|
),
|
|
893
882
|
),
|
|
894
883
|
const SizedBox(width: 2),
|
|
895
884
|
AnimatedRotation(
|
|
896
|
-
turns:
|
|
885
|
+
turns: _menuOpen ? 0.5 : 0,
|
|
897
886
|
duration: const Duration(milliseconds: 160),
|
|
898
|
-
child:
|
|
899
|
-
Icons.keyboard_arrow_down_rounded,
|
|
900
|
-
color: Colors.white70,
|
|
901
|
-
size: 13,
|
|
902
|
-
),
|
|
887
|
+
child: Icon(KasyIcons.chevronDown, color: _fg, size: 13),
|
|
903
888
|
),
|
|
904
889
|
],
|
|
905
890
|
),
|
|
906
891
|
),
|
|
907
892
|
),
|
|
908
893
|
),
|
|
909
|
-
|
|
910
|
-
|
|
894
|
+
_VerticalDivider(base: _fg),
|
|
895
|
+
// Tapping the name cycles to the next device — no side chevrons, saves
|
|
896
|
+
// space (same pattern as the locale cycler).
|
|
897
|
+
MouseRegion(
|
|
898
|
+
onEnter: (_) => setState(() => _deviceBtnHovered = true),
|
|
899
|
+
onExit: (_) => setState(() => _deviceBtnHovered = false),
|
|
900
|
+
cursor: SystemMouseCursors.click,
|
|
901
|
+
child: GestureDetector(
|
|
902
|
+
onTap: widget.onNext,
|
|
903
|
+
child: AnimatedContainer(
|
|
904
|
+
duration: const Duration(milliseconds: 160),
|
|
905
|
+
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
|
906
|
+
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
|
907
|
+
decoration: BoxDecoration(
|
|
908
|
+
color: _fg.withValues(alpha: _deviceBtnHovered ? 0.12 : 0.0),
|
|
909
|
+
borderRadius: BorderRadius.circular(20),
|
|
910
|
+
),
|
|
911
|
+
child: Text(
|
|
912
|
+
widget.deviceName,
|
|
913
|
+
style: _chromeText(
|
|
914
|
+
size: 12,
|
|
915
|
+
weight: FontWeight.w500,
|
|
916
|
+
color: _fg,
|
|
917
|
+
spacing: -0.2,
|
|
918
|
+
),
|
|
919
|
+
),
|
|
920
|
+
),
|
|
921
|
+
),
|
|
922
|
+
),
|
|
923
|
+
// Locale chip — hidden for now (see ROADMAP: moves to vertical bar).
|
|
924
|
+
if (_showSecondaryTools()) ...[
|
|
925
|
+
_VerticalDivider(base: _fg),
|
|
926
|
+
_PillChip(
|
|
927
|
+
label: widget.currentLocale.languageCode.toUpperCase(),
|
|
928
|
+
onTap: widget.onCycleLocale,
|
|
929
|
+
base: _fg,
|
|
930
|
+
),
|
|
931
|
+
],
|
|
932
|
+
// Tools, all inline (no dropdown).
|
|
933
|
+
_VerticalDivider(base: _fg),
|
|
934
|
+
// Frame is a mode switch (with / without device frame), not an on/off
|
|
935
|
+
// toggle — the icon itself flips, so no accent highlight.
|
|
911
936
|
_IconBtn(
|
|
912
|
-
icon:
|
|
913
|
-
|
|
914
|
-
|
|
937
|
+
icon: widget.frameVisible
|
|
938
|
+
? KasyIcons.deviceFrame
|
|
939
|
+
: KasyIcons.deviceFrameOff,
|
|
940
|
+
onTap: widget.onToggleFrame,
|
|
941
|
+
base: _fg,
|
|
942
|
+
color: _fg,
|
|
915
943
|
),
|
|
916
|
-
|
|
944
|
+
// App theme switch (light/dark of the app inside the device). Mode
|
|
945
|
+
// switch like frame — the sun/moon icon flips, so no accent fill.
|
|
946
|
+
_IconBtn(
|
|
947
|
+
icon: widget.appDark ? KasyIcons.darkMode : KasyIcons.lightMode,
|
|
948
|
+
onTap: widget.onToggleAppTheme,
|
|
949
|
+
base: _fg,
|
|
950
|
+
color: _fg,
|
|
951
|
+
),
|
|
952
|
+
// Orientation + text-scale hidden for now (see ROADMAP flag).
|
|
953
|
+
if (_showSecondaryTools()) ...[
|
|
917
954
|
_IconBtn(
|
|
918
|
-
icon:
|
|
919
|
-
|
|
955
|
+
icon: widget.isLandscape
|
|
956
|
+
? KasyIcons.landscape
|
|
957
|
+
: KasyIcons.portrait,
|
|
958
|
+
onTap: widget.onToggleLandscape,
|
|
959
|
+
base: _fg,
|
|
960
|
+
active: widget.isLandscape,
|
|
961
|
+
color: _fg,
|
|
920
962
|
),
|
|
921
|
-
|
|
922
|
-
|
|
963
|
+
_PillChip(
|
|
964
|
+
label: _textScaleLabel(widget.textScale),
|
|
965
|
+
onTap: widget.onCycleTextScale,
|
|
966
|
+
base: _fg,
|
|
967
|
+
active: widget.textScale != 1.0,
|
|
968
|
+
),
|
|
969
|
+
],
|
|
923
970
|
_IconBtn(
|
|
924
|
-
icon:
|
|
925
|
-
onTap:
|
|
926
|
-
|
|
971
|
+
icon: KasyIcons.cameraAlt,
|
|
972
|
+
onTap: widget.onScreenshot,
|
|
973
|
+
base: _fg,
|
|
974
|
+
color: _fg,
|
|
975
|
+
),
|
|
976
|
+
// Inspector group
|
|
977
|
+
_VerticalDivider(base: _fg),
|
|
978
|
+
_IconBtn(
|
|
979
|
+
icon: KasyIcons.inspector,
|
|
980
|
+
onTap: widget.onToggleInspector,
|
|
981
|
+
base: _fg,
|
|
982
|
+
active: widget.inspectorEnabled,
|
|
983
|
+
color: _fg,
|
|
927
984
|
),
|
|
985
|
+
// Close
|
|
986
|
+
_VerticalDivider(base: _fg),
|
|
928
987
|
_IconBtn(
|
|
929
|
-
icon:
|
|
988
|
+
icon: KasyIcons.close,
|
|
930
989
|
onTap: widget.onClose,
|
|
931
|
-
|
|
990
|
+
base: _fg,
|
|
991
|
+
color: _fg,
|
|
932
992
|
),
|
|
933
993
|
const SizedBox(width: 2),
|
|
934
994
|
],
|
|
@@ -941,11 +1001,11 @@ class _PreviewControlsState extends State<_PreviewControls> {
|
|
|
941
1001
|
margin: const EdgeInsets.only(top: 6),
|
|
942
1002
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
943
1003
|
decoration: BoxDecoration(
|
|
944
|
-
color:
|
|
1004
|
+
color: _menuColor,
|
|
945
1005
|
borderRadius: BorderRadius.circular(12),
|
|
946
1006
|
boxShadow: [
|
|
947
1007
|
BoxShadow(
|
|
948
|
-
color: Colors.black.withValues(alpha: 0.4),
|
|
1008
|
+
color: Colors.black.withValues(alpha: widget.dark ? 0.4 : 0.18),
|
|
949
1009
|
blurRadius: 16,
|
|
950
1010
|
offset: const Offset(0, 4),
|
|
951
1011
|
),
|
|
@@ -960,6 +1020,7 @@ class _PreviewControlsState extends State<_PreviewControls> {
|
|
|
960
1020
|
return _DropdownItem(
|
|
961
1021
|
label: _platformLabels[i],
|
|
962
1022
|
selected: selected,
|
|
1023
|
+
base: _fg,
|
|
963
1024
|
onTap: () => _selectPlatform(i),
|
|
964
1025
|
);
|
|
965
1026
|
}),
|
|
@@ -967,72 +1028,6 @@ class _PreviewControlsState extends State<_PreviewControls> {
|
|
|
967
1028
|
),
|
|
968
1029
|
);
|
|
969
1030
|
}
|
|
970
|
-
|
|
971
|
-
Widget _buildToolsMenu() {
|
|
972
|
-
return Container(
|
|
973
|
-
margin: const EdgeInsets.only(top: 6),
|
|
974
|
-
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
975
|
-
decoration: BoxDecoration(
|
|
976
|
-
color: const Color(0xFF3A3A3C),
|
|
977
|
-
borderRadius: BorderRadius.circular(12),
|
|
978
|
-
boxShadow: [
|
|
979
|
-
BoxShadow(
|
|
980
|
-
color: Colors.black.withValues(alpha: 0.4),
|
|
981
|
-
blurRadius: 16,
|
|
982
|
-
offset: const Offset(0, 4),
|
|
983
|
-
),
|
|
984
|
-
],
|
|
985
|
-
),
|
|
986
|
-
child: IntrinsicWidth(
|
|
987
|
-
child: Column(
|
|
988
|
-
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
989
|
-
mainAxisSize: MainAxisSize.min,
|
|
990
|
-
children: [
|
|
991
|
-
_ToolsItem(
|
|
992
|
-
icon: widget.frameVisible
|
|
993
|
-
? Icons.smartphone_rounded
|
|
994
|
-
: Icons.crop_free_rounded,
|
|
995
|
-
label: t.webDevicePreview.frame,
|
|
996
|
-
checked: widget.frameVisible,
|
|
997
|
-
onTap: widget.onToggleFrame,
|
|
998
|
-
),
|
|
999
|
-
_ToolsItem(
|
|
1000
|
-
icon: Icons.contrast_rounded,
|
|
1001
|
-
label: t.webDevicePreview.darkBackground,
|
|
1002
|
-
checked: widget.bgDark,
|
|
1003
|
-
onTap: widget.onToggleBg,
|
|
1004
|
-
),
|
|
1005
|
-
_ToolsItem(
|
|
1006
|
-
icon: widget.isLandscape
|
|
1007
|
-
? Icons.stay_current_landscape_rounded
|
|
1008
|
-
: Icons.stay_current_portrait_rounded,
|
|
1009
|
-
label: t.webDevicePreview.landscape,
|
|
1010
|
-
checked: widget.isLandscape,
|
|
1011
|
-
onTap: widget.onToggleLandscape,
|
|
1012
|
-
),
|
|
1013
|
-
_ToolsItem(
|
|
1014
|
-
icon: Icons.text_fields_rounded,
|
|
1015
|
-
label: t.webDevicePreview.textScale,
|
|
1016
|
-
trailingLabel: _textScaleLabel(widget.textScale),
|
|
1017
|
-
onTap: widget.onCycleTextScale,
|
|
1018
|
-
),
|
|
1019
|
-
Divider(
|
|
1020
|
-
height: 1,
|
|
1021
|
-
color: Colors.white.withValues(alpha: 0.10),
|
|
1022
|
-
),
|
|
1023
|
-
_ToolsItem(
|
|
1024
|
-
icon: Icons.photo_camera_rounded,
|
|
1025
|
-
label: t.webDevicePreview.screenshot,
|
|
1026
|
-
onTap: () {
|
|
1027
|
-
setState(() => _toolsMenuOpen = false);
|
|
1028
|
-
widget.onScreenshot();
|
|
1029
|
-
},
|
|
1030
|
-
),
|
|
1031
|
-
],
|
|
1032
|
-
),
|
|
1033
|
-
),
|
|
1034
|
-
);
|
|
1035
|
-
}
|
|
1036
1031
|
}
|
|
1037
1032
|
|
|
1038
1033
|
class _DropdownItem extends StatefulWidget {
|
|
@@ -1040,11 +1035,13 @@ class _DropdownItem extends StatefulWidget {
|
|
|
1040
1035
|
required this.label,
|
|
1041
1036
|
required this.selected,
|
|
1042
1037
|
required this.onTap,
|
|
1038
|
+
this.base = Colors.white,
|
|
1043
1039
|
});
|
|
1044
1040
|
|
|
1045
1041
|
final String label;
|
|
1046
1042
|
final bool selected;
|
|
1047
1043
|
final VoidCallback onTap;
|
|
1044
|
+
final Color base;
|
|
1048
1045
|
|
|
1049
1046
|
@override
|
|
1050
1047
|
State<_DropdownItem> createState() => _DropdownItemState();
|
|
@@ -1065,15 +1062,16 @@ class _DropdownItemState extends State<_DropdownItem> {
|
|
|
1065
1062
|
duration: const Duration(milliseconds: 120),
|
|
1066
1063
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
1067
1064
|
color: _hovered
|
|
1068
|
-
?
|
|
1065
|
+
? widget.base.withValues(alpha: 0.10)
|
|
1069
1066
|
: Colors.transparent,
|
|
1070
1067
|
child: Text(
|
|
1071
1068
|
widget.label,
|
|
1072
|
-
style:
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1069
|
+
style: _chromeText(
|
|
1070
|
+
size: 13,
|
|
1071
|
+
weight: widget.selected ? FontWeight.w600 : FontWeight.w400,
|
|
1072
|
+
color: widget.selected
|
|
1073
|
+
? widget.base
|
|
1074
|
+
: widget.base.withValues(alpha: 0.5),
|
|
1077
1075
|
),
|
|
1078
1076
|
),
|
|
1079
1077
|
),
|
|
@@ -1086,10 +1084,16 @@ class _PillChip extends StatefulWidget {
|
|
|
1086
1084
|
const _PillChip({
|
|
1087
1085
|
required this.label,
|
|
1088
1086
|
required this.onTap,
|
|
1087
|
+
this.base = Colors.white,
|
|
1088
|
+
this.active = false,
|
|
1089
1089
|
});
|
|
1090
1090
|
|
|
1091
1091
|
final String label;
|
|
1092
1092
|
final VoidCallback onTap;
|
|
1093
|
+
final Color base;
|
|
1094
|
+
|
|
1095
|
+
/// When true the chip reads as "non-default" (accent tint + accent text).
|
|
1096
|
+
final bool active;
|
|
1093
1097
|
|
|
1094
1098
|
@override
|
|
1095
1099
|
State<_PillChip> createState() => _PillChipState();
|
|
@@ -1100,6 +1104,14 @@ class _PillChipState extends State<_PillChip> {
|
|
|
1100
1104
|
|
|
1101
1105
|
@override
|
|
1102
1106
|
Widget build(BuildContext context) {
|
|
1107
|
+
final bool on = widget.active;
|
|
1108
|
+
final Color textColor = on ? Colors.white : widget.base;
|
|
1109
|
+
// No resting fill — it just cycles a value. Solid accent when active,
|
|
1110
|
+
// subtle fill only on hover otherwise.
|
|
1111
|
+
final Color bg = on
|
|
1112
|
+
? (_hovered ? _kAccent.withValues(alpha: 0.85) : _kAccent)
|
|
1113
|
+
: (_hovered ? widget.base.withValues(alpha: 0.14) : Colors.transparent);
|
|
1114
|
+
|
|
1103
1115
|
return MouseRegion(
|
|
1104
1116
|
onEnter: (_) => setState(() => _hovered = true),
|
|
1105
1117
|
onExit: (_) => setState(() => _hovered = false),
|
|
@@ -1111,16 +1123,16 @@ class _PillChipState extends State<_PillChip> {
|
|
|
1111
1123
|
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
|
1112
1124
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
1113
1125
|
decoration: BoxDecoration(
|
|
1114
|
-
color:
|
|
1126
|
+
color: bg,
|
|
1115
1127
|
borderRadius: BorderRadius.circular(20),
|
|
1116
1128
|
),
|
|
1117
1129
|
child: Text(
|
|
1118
1130
|
widget.label,
|
|
1119
|
-
style:
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1131
|
+
style: _chromeText(
|
|
1132
|
+
size: 11,
|
|
1133
|
+
weight: FontWeight.w600,
|
|
1134
|
+
color: textColor,
|
|
1135
|
+
spacing: 0.3,
|
|
1124
1136
|
),
|
|
1125
1137
|
),
|
|
1126
1138
|
),
|
|
@@ -1129,75 +1141,111 @@ class _PillChipState extends State<_PillChip> {
|
|
|
1129
1141
|
}
|
|
1130
1142
|
}
|
|
1131
1143
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
required this.onTap,
|
|
1137
|
-
this.checked,
|
|
1138
|
-
this.trailingLabel,
|
|
1139
|
-
});
|
|
1144
|
+
/// Small confirmation pill shown briefly in the chrome (e.g. after a screenshot
|
|
1145
|
+
/// is copied). Dark, frosted, centered near the bottom.
|
|
1146
|
+
class _Toast extends StatelessWidget {
|
|
1147
|
+
const _Toast({super.key, required this.message, this.isError = false});
|
|
1140
1148
|
|
|
1141
|
-
final
|
|
1142
|
-
final
|
|
1149
|
+
final String message;
|
|
1150
|
+
final bool isError;
|
|
1151
|
+
|
|
1152
|
+
@override
|
|
1153
|
+
Widget build(BuildContext context) {
|
|
1154
|
+
return DecoratedBox(
|
|
1155
|
+
decoration: BoxDecoration(
|
|
1156
|
+
color: const Color(0xF02C2C2E),
|
|
1157
|
+
borderRadius: BorderRadius.circular(20),
|
|
1158
|
+
boxShadow: const [
|
|
1159
|
+
BoxShadow(color: Color(0x40000000), blurRadius: 12, offset: Offset(0, 3)),
|
|
1160
|
+
],
|
|
1161
|
+
),
|
|
1162
|
+
child: Padding(
|
|
1163
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
|
|
1164
|
+
child: Row(
|
|
1165
|
+
mainAxisSize: MainAxisSize.min,
|
|
1166
|
+
children: [
|
|
1167
|
+
Icon(
|
|
1168
|
+
isError ? KasyIcons.error : KasyIcons.checkCircle,
|
|
1169
|
+
color: isError
|
|
1170
|
+
? const Color(0xFFFF453A)
|
|
1171
|
+
: const Color(0xFF34C759),
|
|
1172
|
+
size: 15,
|
|
1173
|
+
),
|
|
1174
|
+
const SizedBox(width: 8),
|
|
1175
|
+
Text(
|
|
1176
|
+
message,
|
|
1177
|
+
style: _chromeText(
|
|
1178
|
+
size: 12,
|
|
1179
|
+
weight: FontWeight.w500,
|
|
1180
|
+
color: Colors.white,
|
|
1181
|
+
),
|
|
1182
|
+
),
|
|
1183
|
+
],
|
|
1184
|
+
),
|
|
1185
|
+
),
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/// Standalone sun/moon theme toggle pinned to the top-right corner. Switches
|
|
1191
|
+
/// the preview canvas between light (sun) and dark (moon).
|
|
1192
|
+
class _ThemeCornerButton extends StatefulWidget {
|
|
1193
|
+
const _ThemeCornerButton({required this.dark, required this.onTap});
|
|
1194
|
+
|
|
1195
|
+
final bool dark;
|
|
1143
1196
|
final VoidCallback onTap;
|
|
1144
|
-
final bool? checked;
|
|
1145
|
-
final String? trailingLabel;
|
|
1146
1197
|
|
|
1147
1198
|
@override
|
|
1148
|
-
State<
|
|
1199
|
+
State<_ThemeCornerButton> createState() => _ThemeCornerButtonState();
|
|
1149
1200
|
}
|
|
1150
1201
|
|
|
1151
|
-
class
|
|
1202
|
+
class _ThemeCornerButtonState extends State<_ThemeCornerButton> {
|
|
1152
1203
|
bool _hovered = false;
|
|
1153
1204
|
|
|
1154
1205
|
@override
|
|
1155
1206
|
Widget build(BuildContext context) {
|
|
1207
|
+
// Light mode (sun): white fill, dark icon. Dark mode (moon): dark fill,
|
|
1208
|
+
// white icon. Both keep a subtle drop shadow to lift the button.
|
|
1209
|
+
final Color fill = widget.dark ? const Color(0xFF2C2C2E) : Colors.white;
|
|
1210
|
+
final Color iconColor = widget.dark
|
|
1211
|
+
? Colors.white
|
|
1212
|
+
: const Color(0xFF1C1C1E);
|
|
1213
|
+
final Color hoverOverlay = widget.dark
|
|
1214
|
+
? Colors.white.withValues(alpha: 0.12)
|
|
1215
|
+
: Colors.black.withValues(alpha: 0.08);
|
|
1216
|
+
|
|
1156
1217
|
return MouseRegion(
|
|
1157
1218
|
onEnter: (_) => setState(() => _hovered = true),
|
|
1158
1219
|
onExit: (_) => setState(() => _hovered = false),
|
|
1159
1220
|
cursor: SystemMouseCursors.click,
|
|
1160
1221
|
child: GestureDetector(
|
|
1161
1222
|
onTap: widget.onTap,
|
|
1162
|
-
child:
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
Icon(widget.icon, color: Colors.white60, size: 14),
|
|
1172
|
-
const SizedBox(width: 10),
|
|
1173
|
-
Text(
|
|
1174
|
-
widget.label,
|
|
1175
|
-
style: const TextStyle(
|
|
1176
|
-
color: Colors.white,
|
|
1177
|
-
fontSize: 13,
|
|
1178
|
-
fontWeight: FontWeight.w400,
|
|
1179
|
-
),
|
|
1223
|
+
child: DecoratedBox(
|
|
1224
|
+
decoration: BoxDecoration(
|
|
1225
|
+
shape: BoxShape.circle,
|
|
1226
|
+
color: fill,
|
|
1227
|
+
boxShadow: const [
|
|
1228
|
+
BoxShadow(
|
|
1229
|
+
color: Color(0x40000000),
|
|
1230
|
+
blurRadius: 10,
|
|
1231
|
+
offset: Offset(0, 3),
|
|
1180
1232
|
),
|
|
1181
|
-
const SizedBox(width: 16),
|
|
1182
|
-
if (widget.trailingLabel != null)
|
|
1183
|
-
Text(
|
|
1184
|
-
widget.trailingLabel!,
|
|
1185
|
-
style: const TextStyle(
|
|
1186
|
-
color: Colors.white38,
|
|
1187
|
-
fontSize: 12,
|
|
1188
|
-
fontWeight: FontWeight.w500,
|
|
1189
|
-
),
|
|
1190
|
-
)
|
|
1191
|
-
else
|
|
1192
|
-
Icon(
|
|
1193
|
-
Icons.check_rounded,
|
|
1194
|
-
size: 14,
|
|
1195
|
-
color: (widget.checked ?? false)
|
|
1196
|
-
? Colors.white
|
|
1197
|
-
: Colors.transparent,
|
|
1198
|
-
),
|
|
1199
1233
|
],
|
|
1200
1234
|
),
|
|
1235
|
+
child: AnimatedContainer(
|
|
1236
|
+
duration: const Duration(milliseconds: 120),
|
|
1237
|
+
width: 28,
|
|
1238
|
+
height: 28,
|
|
1239
|
+
decoration: BoxDecoration(
|
|
1240
|
+
shape: BoxShape.circle,
|
|
1241
|
+
color: _hovered ? hoverOverlay : Colors.transparent,
|
|
1242
|
+
),
|
|
1243
|
+
child: Icon(
|
|
1244
|
+
widget.dark ? KasyIcons.darkMode : KasyIcons.lightMode,
|
|
1245
|
+
color: iconColor,
|
|
1246
|
+
size: 15,
|
|
1247
|
+
),
|
|
1248
|
+
),
|
|
1201
1249
|
),
|
|
1202
1250
|
),
|
|
1203
1251
|
);
|
|
@@ -1205,13 +1253,17 @@ class _ToolsItemState extends State<_ToolsItem> {
|
|
|
1205
1253
|
}
|
|
1206
1254
|
|
|
1207
1255
|
class _VerticalDivider extends StatelessWidget {
|
|
1256
|
+
const _VerticalDivider({this.base = Colors.white});
|
|
1257
|
+
|
|
1258
|
+
final Color base;
|
|
1259
|
+
|
|
1208
1260
|
@override
|
|
1209
1261
|
Widget build(BuildContext context) {
|
|
1210
1262
|
return Container(
|
|
1211
1263
|
width: 1,
|
|
1212
1264
|
height: 18,
|
|
1213
1265
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
|
1214
|
-
color:
|
|
1266
|
+
color: base.withValues(alpha: 0.12),
|
|
1215
1267
|
);
|
|
1216
1268
|
}
|
|
1217
1269
|
}
|
|
@@ -1221,11 +1273,17 @@ class _IconBtn extends StatefulWidget {
|
|
|
1221
1273
|
required this.icon,
|
|
1222
1274
|
required this.onTap,
|
|
1223
1275
|
this.color = Colors.white60,
|
|
1276
|
+
this.base = Colors.white,
|
|
1277
|
+
this.active = false,
|
|
1224
1278
|
});
|
|
1225
1279
|
|
|
1226
1280
|
final IconData icon;
|
|
1227
1281
|
final VoidCallback onTap;
|
|
1228
1282
|
final Color color;
|
|
1283
|
+
final Color base;
|
|
1284
|
+
|
|
1285
|
+
/// When true the button reads as "on": accent-tinted fill + accent icon.
|
|
1286
|
+
final bool active;
|
|
1229
1287
|
|
|
1230
1288
|
@override
|
|
1231
1289
|
State<_IconBtn> createState() => _IconBtnState();
|
|
@@ -1236,6 +1294,14 @@ class _IconBtnState extends State<_IconBtn> {
|
|
|
1236
1294
|
|
|
1237
1295
|
@override
|
|
1238
1296
|
Widget build(BuildContext context) {
|
|
1297
|
+
final bool on = widget.active;
|
|
1298
|
+
// Active: solid accent fill + white icon (filled-button contrast). Inactive:
|
|
1299
|
+
// solid foreground icon (no muted grays), background only on hover.
|
|
1300
|
+
final Color iconColor = on ? Colors.white : widget.color;
|
|
1301
|
+
final Color bg = on
|
|
1302
|
+
? (_hovered ? _kAccent.withValues(alpha: 0.85) : _kAccent)
|
|
1303
|
+
: (_hovered ? widget.base.withValues(alpha: 0.12) : Colors.transparent);
|
|
1304
|
+
|
|
1239
1305
|
return MouseRegion(
|
|
1240
1306
|
onEnter: (_) => setState(() => _hovered = true),
|
|
1241
1307
|
onExit: (_) => setState(() => _hovered = false),
|
|
@@ -1244,14 +1310,12 @@ class _IconBtnState extends State<_IconBtn> {
|
|
|
1244
1310
|
onTap: widget.onTap,
|
|
1245
1311
|
child: AnimatedContainer(
|
|
1246
1312
|
duration: const Duration(milliseconds: 120),
|
|
1247
|
-
padding: const EdgeInsets.symmetric(horizontal:
|
|
1313
|
+
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
|
1248
1314
|
decoration: BoxDecoration(
|
|
1249
|
-
color:
|
|
1250
|
-
? Colors.white.withValues(alpha: 0.12)
|
|
1251
|
-
: Colors.transparent,
|
|
1315
|
+
color: bg,
|
|
1252
1316
|
borderRadius: BorderRadius.circular(8),
|
|
1253
1317
|
),
|
|
1254
|
-
child: Icon(widget.icon, color:
|
|
1318
|
+
child: Icon(widget.icon, color: iconColor, size: 15),
|
|
1255
1319
|
),
|
|
1256
1320
|
),
|
|
1257
1321
|
);
|