kasy-cli 1.37.0 → 1.38.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 +9 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
- package/lib/scaffold/backends/patch-base-hashes.json +4 -4
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
- package/templates/firebase/AGENTS.md +20 -10
- package/templates/firebase/DESIGN_SYSTEM.md +13 -0
- package/templates/firebase/README.en.md +1 -1
- package/templates/firebase/README.es.md +1 -1
- package/templates/firebase/README.md +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +57 -16
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
- package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
- package/templates/firebase/lib/components/kasy_sidebar.dart +397 -28
- package/templates/firebase/lib/components/kasy_web_header.dart +11 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +2 -213
- package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -4
- package/templates/firebase/lib/core/chrome/web_header_scope.dart +20 -0
- package/templates/firebase/lib/core/data/api/user_api.dart +4 -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/icons/kasy_icons.dart +16 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +41 -0
- 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 +498 -466
- package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -7
- package/templates/firebase/lib/features/home/home_components_page.dart +16 -6
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +7 -0
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +23 -13
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
- package/templates/firebase/lib/features/settings/settings_page.dart +51 -38
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +549 -376
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +266 -141
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +23 -30
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +30 -49
- package/templates/firebase/lib/i18n/en.i18n.json +23 -9
- package/templates/firebase/lib/i18n/es.i18n.json +23 -9
- package/templates/firebase/lib/i18n/pt.i18n.json +23 -9
- package/templates/firebase/lib/router.dart +43 -25
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/admin_shell_chrome_test.dart +104 -0
- package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
- 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,34 @@ 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
|
+
// Kit primary/accent (HeroUI blue). The chrome lives above the MaterialApp, so
|
|
36
|
+
// it can't read context.colors — mirror the design-system value here. Used to
|
|
37
|
+
// highlight active toggles so it's obvious at a glance what's turned on.
|
|
38
|
+
const Color _kAccent = Color(0xFF0485F7);
|
|
39
|
+
|
|
40
|
+
// ROADMAP: secondary controls (locale, landscape orientation, text-scale) are
|
|
41
|
+
// hidden for now — they'll move to a dedicated vertical toolbar later, while the
|
|
42
|
+
// main horizontal pill stays for quick access. Return true to bring them back
|
|
43
|
+
// inline. (A function, not a const, so the guarded widgets aren't dead code.)
|
|
44
|
+
bool _showSecondaryTools() => false;
|
|
45
|
+
|
|
46
|
+
// Inter — the kit's typeface. Centralizes the chrome's text styling so every
|
|
47
|
+
// label shares the design-system font.
|
|
48
|
+
TextStyle _chromeText({
|
|
49
|
+
required double size,
|
|
50
|
+
required FontWeight weight,
|
|
51
|
+
required Color color,
|
|
52
|
+
double spacing = 0,
|
|
53
|
+
}) => GoogleFonts.inter(
|
|
54
|
+
fontSize: size,
|
|
55
|
+
fontWeight: weight,
|
|
56
|
+
color: color,
|
|
57
|
+
letterSpacing: spacing,
|
|
58
|
+
);
|
|
30
59
|
|
|
31
60
|
/// Keyboard shortcut for toggling the web device preview chrome, formatted
|
|
32
61
|
/// for the current platform. Key names stay in English regardless of the
|
|
@@ -61,20 +90,6 @@ final List<DeviceInfo> _iPadDevices = [
|
|
|
61
90
|
Devices.ios.iPadPro13InchesM4,
|
|
62
91
|
];
|
|
63
92
|
|
|
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
93
|
class WebDevicePreview extends StatefulWidget {
|
|
79
94
|
const WebDevicePreview({super.key, required this.child});
|
|
80
95
|
|
|
@@ -92,14 +107,18 @@ class WebDevicePreview extends StatefulWidget {
|
|
|
92
107
|
class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
93
108
|
with WidgetsBindingObserver {
|
|
94
109
|
int _platform = 0; // 0 = iOS, 1 = Android, 2 = iPad
|
|
95
|
-
int _iosIndex = 1;
|
|
110
|
+
int _iosIndex = 1; // default: iPhone 16
|
|
96
111
|
int _androidIndex = 1; // default: Google Pixel 9
|
|
97
112
|
int _iPadIndex = 0;
|
|
98
113
|
bool _controlsVisible = false;
|
|
99
114
|
Timer? _controlsTimer;
|
|
115
|
+
String? _toast;
|
|
116
|
+
bool _toastIsError = false;
|
|
117
|
+
Timer? _toastTimer;
|
|
100
118
|
|
|
101
|
-
final ValueNotifier<DeviceInfo> _deviceNotifier =
|
|
102
|
-
|
|
119
|
+
final ValueNotifier<DeviceInfo> _deviceNotifier = ValueNotifier<DeviceInfo>(
|
|
120
|
+
_iosDevices[1],
|
|
121
|
+
); // iPhone 16
|
|
103
122
|
final ValueNotifier<bool> _frameVisibleNotifier = ValueNotifier<bool>(true);
|
|
104
123
|
final ValueNotifier<bool> _bgDarkNotifier = ValueNotifier<bool>(false);
|
|
105
124
|
final ValueNotifier<bool> _landscapeNotifier = ValueNotifier<bool>(false);
|
|
@@ -110,16 +129,16 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
110
129
|
static const _textScaleSteps = [1.0, 1.3, 1.5];
|
|
111
130
|
|
|
112
131
|
List<DeviceInfo> get _devices => switch (_platform) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
1 => _androidDevices,
|
|
133
|
+
2 => _iPadDevices,
|
|
134
|
+
_ => _iosDevices,
|
|
135
|
+
};
|
|
117
136
|
|
|
118
137
|
int get _currentIndex => switch (_platform) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
1 => _androidIndex,
|
|
139
|
+
2 => _iPadIndex,
|
|
140
|
+
_ => _iosIndex,
|
|
141
|
+
};
|
|
123
142
|
|
|
124
143
|
DeviceInfo get _currentDevice => _devices[_currentIndex];
|
|
125
144
|
|
|
@@ -132,6 +151,7 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
132
151
|
);
|
|
133
152
|
webDevicePreviewEnabledNotifier.addListener(_onEnabledChanged);
|
|
134
153
|
_bgDarkNotifier.addListener(_onBgChanged);
|
|
154
|
+
devInspectorToastNotifier.addListener(_onInspectorToast);
|
|
135
155
|
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
|
136
156
|
unawaited(_bootstrap());
|
|
137
157
|
}
|
|
@@ -174,6 +194,7 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
174
194
|
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
|
175
195
|
webDevicePreviewEnabledNotifier.removeListener(_onEnabledChanged);
|
|
176
196
|
_bgDarkNotifier.removeListener(_onBgChanged);
|
|
197
|
+
devInspectorToastNotifier.removeListener(_onInspectorToast);
|
|
177
198
|
_deviceNotifier.dispose();
|
|
178
199
|
_frameVisibleNotifier.dispose();
|
|
179
200
|
_bgDarkNotifier.dispose();
|
|
@@ -181,9 +202,31 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
181
202
|
_textScaleNotifier.dispose();
|
|
182
203
|
_localeNotifier.dispose();
|
|
183
204
|
_controlsTimer?.cancel();
|
|
205
|
+
_toastTimer?.cancel();
|
|
184
206
|
super.dispose();
|
|
185
207
|
}
|
|
186
208
|
|
|
209
|
+
/// Transient confirmation pill shown in the chrome (e.g. "Image copied").
|
|
210
|
+
void _showToast(String message, {bool isError = false}) {
|
|
211
|
+
_toastTimer?.cancel();
|
|
212
|
+
setState(() {
|
|
213
|
+
_toast = message;
|
|
214
|
+
_toastIsError = isError;
|
|
215
|
+
});
|
|
216
|
+
_toastTimer = Timer(const Duration(milliseconds: 2200), () {
|
|
217
|
+
if (mounted) setState(() => _toast = null);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// The DevInspector hands its "copied" message here so it renders in the same
|
|
222
|
+
/// spot and size as the chrome's own toasts (instead of inside the device).
|
|
223
|
+
void _onInspectorToast() {
|
|
224
|
+
final msg = devInspectorToastNotifier.value;
|
|
225
|
+
if (msg == null) return;
|
|
226
|
+
devInspectorToastNotifier.value = null;
|
|
227
|
+
if (mounted) _showToast(msg.message, isError: msg.isError);
|
|
228
|
+
}
|
|
229
|
+
|
|
187
230
|
void _onEnabledChanged() {
|
|
188
231
|
// Our chrome already surfaces inspector state via the pill, so suppress
|
|
189
232
|
// the DevInspector's in-app status pill while the preview is on.
|
|
@@ -283,9 +326,16 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
283
326
|
}
|
|
284
327
|
|
|
285
328
|
void _setPlatform(int p) {
|
|
286
|
-
setState(()
|
|
329
|
+
setState(() {
|
|
330
|
+
_platform = p;
|
|
331
|
+
// Choosing a platform always lands on its first device:
|
|
332
|
+
// iPad → plain iPad, Android → Samsung Galaxy A50.
|
|
333
|
+
if (p == 1) _androidIndex = 0;
|
|
334
|
+
if (p == 2) _iPadIndex = 0;
|
|
335
|
+
});
|
|
287
336
|
_deviceNotifier.value = _currentDevice;
|
|
288
337
|
unawaited(_savePlatform());
|
|
338
|
+
if (p == 1 || p == 2) unawaited(_saveDeviceIndices());
|
|
289
339
|
}
|
|
290
340
|
|
|
291
341
|
void _toggleFrame() {
|
|
@@ -301,10 +351,6 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
301
351
|
devInspectorActiveNotifier.value = !devInspectorActiveNotifier.value;
|
|
302
352
|
}
|
|
303
353
|
|
|
304
|
-
void _copyInspection() {
|
|
305
|
-
devInspectorCopyTriggerNotifier.value = true;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
354
|
void _toggleLandscape() {
|
|
309
355
|
_landscapeNotifier.value = !_landscapeNotifier.value;
|
|
310
356
|
unawaited(_saveLandscape());
|
|
@@ -330,44 +376,34 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
330
376
|
}
|
|
331
377
|
|
|
332
378
|
Future<void> _takeScreenshot() async {
|
|
333
|
-
final boundary =
|
|
334
|
-
|
|
379
|
+
final boundary =
|
|
380
|
+
_screenshotKey.currentContext?.findRenderObject()
|
|
381
|
+
as RenderRepaintBoundary?;
|
|
335
382
|
if (boundary == null) return;
|
|
336
383
|
final dpr =
|
|
337
|
-
WidgetsBinding
|
|
338
|
-
|
|
384
|
+
WidgetsBinding
|
|
385
|
+
.instance
|
|
386
|
+
.platformDispatcher
|
|
387
|
+
.implicitView
|
|
388
|
+
?.devicePixelRatio ??
|
|
389
|
+
2.0;
|
|
339
390
|
final image = await boundary.toImage(pixelRatio: dpr);
|
|
340
391
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
341
392
|
if (byteData == null) return;
|
|
342
393
|
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());
|
|
394
|
+
// Copy the PNG to the clipboard (with a download fallback). The web-only
|
|
395
|
+
// implementation lives behind a conditional import (png_clipboard.dart) so
|
|
396
|
+
// this file stays VM/native-compilable and never breaks `flutter test`.
|
|
397
|
+
final result = await copyOrDownloadPng(bytes);
|
|
398
|
+
if (!mounted) return;
|
|
399
|
+
switch (result) {
|
|
400
|
+
case PngExportResult.copied:
|
|
401
|
+
_showToast(t.webDevicePreview.imageCopied);
|
|
402
|
+
case PngExportResult.downloaded:
|
|
403
|
+
_showToast(t.webDevicePreview.imageDownloaded);
|
|
404
|
+
case PngExportResult.unavailable:
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
371
407
|
}
|
|
372
408
|
|
|
373
409
|
void _next() {
|
|
@@ -389,15 +425,27 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
389
425
|
Widget build(BuildContext context) {
|
|
390
426
|
final enabled = webDevicePreviewEnabledNotifier.value;
|
|
391
427
|
|
|
428
|
+
// App theme (light/dark) lives in ThemeProvider, which sits ABOVE this
|
|
429
|
+
// widget — reading it here rebuilds the toolbar when the theme flips.
|
|
430
|
+
final AppTheme appTheme = ThemeProvider.of(context);
|
|
431
|
+
final bool appDark = appTheme.effectiveMode == ThemeMode.dark;
|
|
432
|
+
|
|
392
433
|
// ThemeData.light() in Flutter 3.x M3 uses a lavender-tinted surface.
|
|
393
434
|
// Passing backgroundColor directly avoids that and gives us an exact color.
|
|
394
|
-
final canvasColor =
|
|
395
|
-
|
|
435
|
+
final canvasColor = _bgDarkNotifier.value
|
|
436
|
+
? const Color(0xFF1C1C1E)
|
|
437
|
+
: Colors.white;
|
|
396
438
|
|
|
397
439
|
final preview = DevicePreview(
|
|
398
440
|
enabled: enabled,
|
|
399
441
|
isToolbarVisible: false,
|
|
400
442
|
backgroundColor: canvasColor,
|
|
443
|
+
// Reserve room at the top so the floating toolbar never sits on the device.
|
|
444
|
+
// DevicePreview fills the whole viewport, so this is the single source of
|
|
445
|
+
// background — there is no second surface that could create a seam.
|
|
446
|
+
padding: enabled
|
|
447
|
+
? const EdgeInsets.only(top: 56, left: 20, right: 20, bottom: 20)
|
|
448
|
+
: null,
|
|
401
449
|
storage: DevicePreviewStorage.none(),
|
|
402
450
|
defaultDevice: _currentDevice,
|
|
403
451
|
devices: [..._iosDevices, ..._androidDevices, ..._iPadDevices],
|
|
@@ -409,8 +457,8 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
409
457
|
// DevicePreview hard-codes platformBrightness to light inside the
|
|
410
458
|
// simulated frame, which breaks MaterialApp's ThemeMode.system.
|
|
411
459
|
// Forward the real host brightness so "system" tracks the OS.
|
|
412
|
-
platformBrightness:
|
|
413
|
-
.instance.platformDispatcher.platformBrightness,
|
|
460
|
+
platformBrightness:
|
|
461
|
+
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
|
414
462
|
),
|
|
415
463
|
child: _DeviceSwitchBridge(
|
|
416
464
|
deviceNotifier: _deviceNotifier,
|
|
@@ -426,65 +474,108 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
426
474
|
|
|
427
475
|
return TapRegionSurface(
|
|
428
476
|
child: Directionality(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
child: _DevInspectorExternalHighlight(),
|
|
477
|
+
textDirection: TextDirection.ltr,
|
|
478
|
+
child: Stack(
|
|
479
|
+
children: [
|
|
480
|
+
// DevicePreview paints the entire viewport (single background, no
|
|
481
|
+
// seam). The device frame is pushed below the toolbar by the top
|
|
482
|
+
// padding above, so the toolbar never overlaps it.
|
|
483
|
+
RepaintBoundary(key: _screenshotKey, child: preview),
|
|
484
|
+
// Inspector highlight drawn ABOVE the device frame so widgets that
|
|
485
|
+
// hug the viewport edges (AppBar, bottom nav, …) keep a full border.
|
|
486
|
+
const Positioned.fill(
|
|
487
|
+
child: IgnorePointer(child: _DevInspectorExternalHighlight()),
|
|
440
488
|
),
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
489
|
+
// Floating toolbar — overlaid at the top, centered. Opening its
|
|
490
|
+
// dropdowns never pushes or resizes the device (they overlay).
|
|
491
|
+
Positioned(
|
|
492
|
+
top: 12,
|
|
493
|
+
left: 0,
|
|
494
|
+
right: 0,
|
|
495
|
+
child: AnimatedOpacity(
|
|
496
|
+
opacity: _controlsVisible ? 1.0 : 0.0,
|
|
497
|
+
duration: const Duration(milliseconds: 400),
|
|
498
|
+
child: IgnorePointer(
|
|
499
|
+
ignoring: !_controlsVisible,
|
|
500
|
+
child: Center(
|
|
501
|
+
child: ListenableBuilder(
|
|
502
|
+
listenable: Listenable.merge([
|
|
503
|
+
_frameVisibleNotifier,
|
|
504
|
+
_bgDarkNotifier,
|
|
505
|
+
_landscapeNotifier,
|
|
506
|
+
_textScaleNotifier,
|
|
507
|
+
_localeNotifier,
|
|
508
|
+
devInspectorActiveNotifier,
|
|
509
|
+
]),
|
|
510
|
+
builder: (context, child) => _PreviewControls(
|
|
511
|
+
platform: _platform,
|
|
512
|
+
deviceName: _currentDevice.name,
|
|
513
|
+
dark: _bgDarkNotifier.value,
|
|
514
|
+
appDark: appDark,
|
|
515
|
+
onToggleAppTheme: appTheme.toggle,
|
|
516
|
+
frameVisible: _frameVisibleNotifier.value,
|
|
517
|
+
isLandscape: _landscapeNotifier.value,
|
|
518
|
+
textScale: _textScaleNotifier.value,
|
|
519
|
+
currentLocale: _localeNotifier.value,
|
|
520
|
+
inspectorEnabled: devInspectorActiveNotifier.value,
|
|
521
|
+
onPlatformChanged: _setPlatform,
|
|
522
|
+
onNext: _next,
|
|
523
|
+
onToggleFrame: _toggleFrame,
|
|
524
|
+
onToggleLandscape: _toggleLandscape,
|
|
525
|
+
onCycleTextScale: _cycleTextScale,
|
|
526
|
+
onCycleLocale: _cycleLocale,
|
|
527
|
+
onToggleInspector: _toggleInspector,
|
|
528
|
+
onScreenshot: () => unawaited(_takeScreenshot()),
|
|
529
|
+
onClose: () => unawaited(_close()),
|
|
530
|
+
),
|
|
531
|
+
),
|
|
532
|
+
),
|
|
533
|
+
),
|
|
534
|
+
),
|
|
535
|
+
),
|
|
536
|
+
// Theme toggle — standalone sun/moon button pinned to the
|
|
537
|
+
// bottom-right corner, independent of the toolbar.
|
|
538
|
+
Positioned(
|
|
539
|
+
bottom: 10,
|
|
540
|
+
right: 10,
|
|
541
|
+
child: AnimatedOpacity(
|
|
542
|
+
opacity: _controlsVisible ? 1.0 : 0.0,
|
|
543
|
+
duration: const Duration(milliseconds: 400),
|
|
544
|
+
child: IgnorePointer(
|
|
545
|
+
ignoring: !_controlsVisible,
|
|
546
|
+
child: ListenableBuilder(
|
|
547
|
+
listenable: _bgDarkNotifier,
|
|
548
|
+
builder: (context, child) => _ThemeCornerButton(
|
|
549
|
+
dark: _bgDarkNotifier.value,
|
|
550
|
+
onTap: _toggleBg,
|
|
551
|
+
),
|
|
552
|
+
),
|
|
553
|
+
),
|
|
554
|
+
),
|
|
555
|
+
),
|
|
556
|
+
// Transient confirmation toast (e.g. screenshot copied).
|
|
557
|
+
Positioned(
|
|
558
|
+
left: 0,
|
|
559
|
+
right: 0,
|
|
560
|
+
bottom: 24,
|
|
448
561
|
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()),
|
|
562
|
+
child: Center(
|
|
563
|
+
child: AnimatedSwitcher(
|
|
564
|
+
duration: const Duration(milliseconds: 200),
|
|
565
|
+
child: _toast == null
|
|
566
|
+
? const SizedBox.shrink()
|
|
567
|
+
: _Toast(
|
|
568
|
+
key: ValueKey(_toast),
|
|
569
|
+
message: _toast!,
|
|
570
|
+
isError: _toastIsError,
|
|
571
|
+
),
|
|
480
572
|
),
|
|
481
573
|
),
|
|
482
574
|
),
|
|
483
575
|
),
|
|
484
|
-
|
|
485
|
-
|
|
576
|
+
],
|
|
577
|
+
),
|
|
486
578
|
),
|
|
487
|
-
),
|
|
488
579
|
);
|
|
489
580
|
}
|
|
490
581
|
}
|
|
@@ -607,44 +698,45 @@ class _PreviewControls extends StatefulWidget {
|
|
|
607
698
|
const _PreviewControls({
|
|
608
699
|
required this.platform,
|
|
609
700
|
required this.deviceName,
|
|
701
|
+
required this.dark,
|
|
702
|
+
required this.appDark,
|
|
703
|
+
required this.onToggleAppTheme,
|
|
610
704
|
required this.frameVisible,
|
|
611
|
-
required this.bgDark,
|
|
612
705
|
required this.isLandscape,
|
|
613
706
|
required this.textScale,
|
|
614
707
|
required this.currentLocale,
|
|
615
708
|
required this.inspectorEnabled,
|
|
616
709
|
required this.onPlatformChanged,
|
|
617
|
-
required this.onPrev,
|
|
618
710
|
required this.onNext,
|
|
619
711
|
required this.onToggleFrame,
|
|
620
|
-
required this.onToggleBg,
|
|
621
712
|
required this.onToggleLandscape,
|
|
622
713
|
required this.onCycleTextScale,
|
|
623
714
|
required this.onCycleLocale,
|
|
624
715
|
required this.onToggleInspector,
|
|
625
|
-
required this.onCopyInspection,
|
|
626
716
|
required this.onScreenshot,
|
|
627
717
|
required this.onClose,
|
|
628
718
|
});
|
|
629
719
|
|
|
630
720
|
final int platform;
|
|
631
721
|
final String deviceName;
|
|
722
|
+
final bool dark;
|
|
723
|
+
|
|
724
|
+
/// App theme (light/dark) state + toggle — flips the real app ThemeMode.
|
|
725
|
+
final bool appDark;
|
|
726
|
+
final VoidCallback onToggleAppTheme;
|
|
727
|
+
|
|
632
728
|
final bool frameVisible;
|
|
633
|
-
final bool bgDark;
|
|
634
729
|
final bool isLandscape;
|
|
635
730
|
final double textScale;
|
|
636
731
|
final AppLocale currentLocale;
|
|
637
732
|
final bool inspectorEnabled;
|
|
638
733
|
final ValueChanged<int> onPlatformChanged;
|
|
639
|
-
final VoidCallback onPrev;
|
|
640
734
|
final VoidCallback onNext;
|
|
641
735
|
final VoidCallback onToggleFrame;
|
|
642
|
-
final VoidCallback onToggleBg;
|
|
643
736
|
final VoidCallback onToggleLandscape;
|
|
644
737
|
final VoidCallback onCycleTextScale;
|
|
645
738
|
final VoidCallback onCycleLocale;
|
|
646
739
|
final VoidCallback onToggleInspector;
|
|
647
|
-
final VoidCallback onCopyInspection;
|
|
648
740
|
final VoidCallback onScreenshot;
|
|
649
741
|
final VoidCallback onClose;
|
|
650
742
|
|
|
@@ -654,137 +746,60 @@ class _PreviewControls extends StatefulWidget {
|
|
|
654
746
|
|
|
655
747
|
class _PreviewControlsState extends State<_PreviewControls> {
|
|
656
748
|
bool _menuOpen = false;
|
|
657
|
-
bool _toolsMenuOpen = false;
|
|
658
|
-
bool _minimized = true;
|
|
659
749
|
bool _platformBtnHovered = false;
|
|
660
|
-
bool
|
|
661
|
-
bool _shadowVisible = true;
|
|
662
|
-
Timer? _shadowTimer;
|
|
750
|
+
bool _deviceBtnHovered = false;
|
|
663
751
|
|
|
664
752
|
static const _platformLabels = ['iOS', 'Android', 'iPad'];
|
|
665
753
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
754
|
+
// Color scheme adapts to the preview theme: dark pill (white content) in dark
|
|
755
|
+
// mode, white pill (dark content) in light mode.
|
|
756
|
+
Color get _pillColor => widget.dark ? const Color(0xFF2C2C2E) : Colors.white;
|
|
757
|
+
Color get _menuColor => widget.dark ? const Color(0xFF3A3A3C) : Colors.white;
|
|
758
|
+
// Foreground (text/icons) base — alphas are derived from this.
|
|
759
|
+
Color get _fg => widget.dark ? Colors.white : const Color(0xFF1C1C1E);
|
|
760
|
+
|
|
761
|
+
BoxDecoration get _pillDecoration => BoxDecoration(
|
|
762
|
+
color: _pillColor,
|
|
763
|
+
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
|
669
764
|
);
|
|
670
765
|
|
|
766
|
+
// Tight, low-spread shadow: enough to lift the pill off the canvas without
|
|
767
|
+
// casting a wide smudge below it that reads as a horizontal "division" line.
|
|
671
768
|
static const _pillShadow = [
|
|
672
|
-
BoxShadow(
|
|
673
|
-
color: Color(0x73000000),
|
|
674
|
-
blurRadius: 20,
|
|
675
|
-
offset: Offset(0, 6),
|
|
676
|
-
),
|
|
769
|
+
BoxShadow(color: Color(0x33000000), blurRadius: 8, offset: Offset(0, 2)),
|
|
677
770
|
];
|
|
678
771
|
|
|
679
772
|
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
|
-
});
|
|
773
|
+
1.0 => '1×',
|
|
774
|
+
1.3 => '1.3×',
|
|
775
|
+
_ => '1.5×',
|
|
776
|
+
};
|
|
689
777
|
|
|
690
|
-
void
|
|
691
|
-
_toolsMenuOpen = !_toolsMenuOpen;
|
|
692
|
-
if (_toolsMenuOpen) _menuOpen = false;
|
|
693
|
-
});
|
|
778
|
+
void _toggleMenu() => setState(() => _menuOpen = !_menuOpen);
|
|
694
779
|
|
|
695
780
|
void _selectPlatform(int i) {
|
|
696
781
|
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();
|
|
782
|
+
setState(() => _menuOpen = false);
|
|
729
783
|
}
|
|
730
784
|
|
|
731
785
|
@override
|
|
732
786
|
Widget build(BuildContext context) {
|
|
733
787
|
return TapRegion(
|
|
734
788
|
onTapOutside: (_) {
|
|
735
|
-
if (_menuOpen
|
|
736
|
-
setState(() {
|
|
737
|
-
_menuOpen = false;
|
|
738
|
-
_toolsMenuOpen = false;
|
|
739
|
-
});
|
|
740
|
-
}
|
|
789
|
+
if (_menuOpen) setState(() => _menuOpen = false);
|
|
741
790
|
},
|
|
742
791
|
child: Column(
|
|
743
792
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
744
793
|
mainAxisSize: MainAxisSize.min,
|
|
745
794
|
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
|
-
),
|
|
795
|
+
DecoratedBox(
|
|
796
|
+
decoration: const BoxDecoration(
|
|
797
|
+
borderRadius: BorderRadius.all(Radius.circular(24)),
|
|
798
|
+
boxShadow: _pillShadow,
|
|
760
799
|
),
|
|
800
|
+
child: _buildPill(),
|
|
761
801
|
),
|
|
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),
|
|
802
|
+
if (_menuOpen) _buildMenu(),
|
|
788
803
|
],
|
|
789
804
|
),
|
|
790
805
|
);
|
|
@@ -792,7 +807,7 @@ class _PreviewControlsState extends State<_PreviewControls> {
|
|
|
792
807
|
|
|
793
808
|
Widget _buildPill() {
|
|
794
809
|
return Container(
|
|
795
|
-
height:
|
|
810
|
+
height: 34,
|
|
796
811
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
797
812
|
decoration: _pillDecoration,
|
|
798
813
|
child: Row(
|
|
@@ -804,131 +819,144 @@ class _PreviewControlsState extends State<_PreviewControls> {
|
|
|
804
819
|
onExit: (_) => setState(() => _platformBtnHovered = false),
|
|
805
820
|
cursor: SystemMouseCursors.click,
|
|
806
821
|
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,
|
|
822
|
+
onTap: _toggleMenu,
|
|
873
823
|
child: AnimatedContainer(
|
|
874
824
|
duration: const Duration(milliseconds: 160),
|
|
875
825
|
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
|
876
|
-
padding: const EdgeInsets.symmetric(
|
|
826
|
+
padding: const EdgeInsets.symmetric(
|
|
827
|
+
horizontal: 10,
|
|
828
|
+
vertical: 5,
|
|
829
|
+
),
|
|
877
830
|
decoration: BoxDecoration(
|
|
878
|
-
|
|
879
|
-
|
|
831
|
+
// No resting fill — it just alternates platforms. Subtle fill
|
|
832
|
+
// only while open or hovered.
|
|
833
|
+
color: _fg.withValues(
|
|
834
|
+
alpha: _menuOpen
|
|
835
|
+
? 0.16
|
|
836
|
+
: (_platformBtnHovered ? 0.12 : 0.0),
|
|
880
837
|
),
|
|
881
838
|
borderRadius: BorderRadius.circular(20),
|
|
882
839
|
),
|
|
883
840
|
child: Row(
|
|
884
841
|
mainAxisSize: MainAxisSize.min,
|
|
885
842
|
children: [
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
style:
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
843
|
+
Text(
|
|
844
|
+
_platformLabels[widget.platform],
|
|
845
|
+
style: _chromeText(
|
|
846
|
+
size: 12,
|
|
847
|
+
weight: FontWeight.w600,
|
|
848
|
+
color: _fg,
|
|
892
849
|
),
|
|
893
850
|
),
|
|
894
851
|
const SizedBox(width: 2),
|
|
895
852
|
AnimatedRotation(
|
|
896
|
-
turns:
|
|
853
|
+
turns: _menuOpen ? 0.5 : 0,
|
|
897
854
|
duration: const Duration(milliseconds: 160),
|
|
898
|
-
child:
|
|
899
|
-
Icons.keyboard_arrow_down_rounded,
|
|
900
|
-
color: Colors.white70,
|
|
901
|
-
size: 13,
|
|
902
|
-
),
|
|
855
|
+
child: Icon(KasyIcons.chevronDown, color: _fg, size: 13),
|
|
903
856
|
),
|
|
904
857
|
],
|
|
905
858
|
),
|
|
906
859
|
),
|
|
907
860
|
),
|
|
908
861
|
),
|
|
909
|
-
|
|
910
|
-
|
|
862
|
+
_VerticalDivider(base: _fg),
|
|
863
|
+
// Tapping the name cycles to the next device — no side chevrons, saves
|
|
864
|
+
// space (same pattern as the locale cycler).
|
|
865
|
+
MouseRegion(
|
|
866
|
+
onEnter: (_) => setState(() => _deviceBtnHovered = true),
|
|
867
|
+
onExit: (_) => setState(() => _deviceBtnHovered = false),
|
|
868
|
+
cursor: SystemMouseCursors.click,
|
|
869
|
+
child: GestureDetector(
|
|
870
|
+
onTap: widget.onNext,
|
|
871
|
+
child: AnimatedContainer(
|
|
872
|
+
duration: const Duration(milliseconds: 160),
|
|
873
|
+
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
|
874
|
+
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
|
875
|
+
decoration: BoxDecoration(
|
|
876
|
+
color: _fg.withValues(alpha: _deviceBtnHovered ? 0.12 : 0.0),
|
|
877
|
+
borderRadius: BorderRadius.circular(20),
|
|
878
|
+
),
|
|
879
|
+
child: Text(
|
|
880
|
+
widget.deviceName,
|
|
881
|
+
style: _chromeText(
|
|
882
|
+
size: 12,
|
|
883
|
+
weight: FontWeight.w500,
|
|
884
|
+
color: _fg,
|
|
885
|
+
spacing: -0.2,
|
|
886
|
+
),
|
|
887
|
+
),
|
|
888
|
+
),
|
|
889
|
+
),
|
|
890
|
+
),
|
|
891
|
+
// Locale chip — hidden for now (see ROADMAP: moves to vertical bar).
|
|
892
|
+
if (_showSecondaryTools()) ...[
|
|
893
|
+
_VerticalDivider(base: _fg),
|
|
894
|
+
_PillChip(
|
|
895
|
+
label: widget.currentLocale.languageCode.toUpperCase(),
|
|
896
|
+
onTap: widget.onCycleLocale,
|
|
897
|
+
base: _fg,
|
|
898
|
+
),
|
|
899
|
+
],
|
|
900
|
+
// Tools, all inline (no dropdown).
|
|
901
|
+
_VerticalDivider(base: _fg),
|
|
902
|
+
// Frame is a mode switch (with / without device frame), not an on/off
|
|
903
|
+
// toggle — the icon itself flips, so no accent highlight.
|
|
911
904
|
_IconBtn(
|
|
912
|
-
icon:
|
|
913
|
-
|
|
914
|
-
|
|
905
|
+
icon: widget.frameVisible
|
|
906
|
+
? KasyIcons.deviceFrame
|
|
907
|
+
: KasyIcons.deviceFrameOff,
|
|
908
|
+
onTap: widget.onToggleFrame,
|
|
909
|
+
base: _fg,
|
|
910
|
+
color: _fg,
|
|
915
911
|
),
|
|
916
|
-
|
|
912
|
+
// App theme switch (light/dark of the app inside the device). Mode
|
|
913
|
+
// switch like frame — the sun/moon icon flips, so no accent fill.
|
|
914
|
+
_IconBtn(
|
|
915
|
+
icon: widget.appDark ? KasyIcons.darkMode : KasyIcons.lightMode,
|
|
916
|
+
onTap: widget.onToggleAppTheme,
|
|
917
|
+
base: _fg,
|
|
918
|
+
color: _fg,
|
|
919
|
+
),
|
|
920
|
+
// Orientation + text-scale hidden for now (see ROADMAP flag).
|
|
921
|
+
if (_showSecondaryTools()) ...[
|
|
917
922
|
_IconBtn(
|
|
918
|
-
icon:
|
|
919
|
-
|
|
923
|
+
icon: widget.isLandscape
|
|
924
|
+
? KasyIcons.landscape
|
|
925
|
+
: KasyIcons.portrait,
|
|
926
|
+
onTap: widget.onToggleLandscape,
|
|
927
|
+
base: _fg,
|
|
928
|
+
active: widget.isLandscape,
|
|
929
|
+
color: _fg,
|
|
930
|
+
),
|
|
931
|
+
_PillChip(
|
|
932
|
+
label: _textScaleLabel(widget.textScale),
|
|
933
|
+
onTap: widget.onCycleTextScale,
|
|
934
|
+
base: _fg,
|
|
935
|
+
active: widget.textScale != 1.0,
|
|
920
936
|
),
|
|
921
|
-
|
|
922
|
-
|
|
937
|
+
],
|
|
938
|
+
_IconBtn(
|
|
939
|
+
icon: KasyIcons.cameraAlt,
|
|
940
|
+
onTap: widget.onScreenshot,
|
|
941
|
+
base: _fg,
|
|
942
|
+
color: _fg,
|
|
943
|
+
),
|
|
944
|
+
// Inspector group
|
|
945
|
+
_VerticalDivider(base: _fg),
|
|
923
946
|
_IconBtn(
|
|
924
|
-
icon:
|
|
925
|
-
onTap:
|
|
926
|
-
|
|
947
|
+
icon: KasyIcons.inspector,
|
|
948
|
+
onTap: widget.onToggleInspector,
|
|
949
|
+
base: _fg,
|
|
950
|
+
active: widget.inspectorEnabled,
|
|
951
|
+
color: _fg,
|
|
927
952
|
),
|
|
953
|
+
// Close
|
|
954
|
+
_VerticalDivider(base: _fg),
|
|
928
955
|
_IconBtn(
|
|
929
|
-
icon:
|
|
956
|
+
icon: KasyIcons.close,
|
|
930
957
|
onTap: widget.onClose,
|
|
931
|
-
|
|
958
|
+
base: _fg,
|
|
959
|
+
color: _fg,
|
|
932
960
|
),
|
|
933
961
|
const SizedBox(width: 2),
|
|
934
962
|
],
|
|
@@ -941,11 +969,11 @@ class _PreviewControlsState extends State<_PreviewControls> {
|
|
|
941
969
|
margin: const EdgeInsets.only(top: 6),
|
|
942
970
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
943
971
|
decoration: BoxDecoration(
|
|
944
|
-
color:
|
|
972
|
+
color: _menuColor,
|
|
945
973
|
borderRadius: BorderRadius.circular(12),
|
|
946
974
|
boxShadow: [
|
|
947
975
|
BoxShadow(
|
|
948
|
-
color: Colors.black.withValues(alpha: 0.4),
|
|
976
|
+
color: Colors.black.withValues(alpha: widget.dark ? 0.4 : 0.18),
|
|
949
977
|
blurRadius: 16,
|
|
950
978
|
offset: const Offset(0, 4),
|
|
951
979
|
),
|
|
@@ -960,6 +988,7 @@ class _PreviewControlsState extends State<_PreviewControls> {
|
|
|
960
988
|
return _DropdownItem(
|
|
961
989
|
label: _platformLabels[i],
|
|
962
990
|
selected: selected,
|
|
991
|
+
base: _fg,
|
|
963
992
|
onTap: () => _selectPlatform(i),
|
|
964
993
|
);
|
|
965
994
|
}),
|
|
@@ -967,72 +996,6 @@ class _PreviewControlsState extends State<_PreviewControls> {
|
|
|
967
996
|
),
|
|
968
997
|
);
|
|
969
998
|
}
|
|
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
999
|
}
|
|
1037
1000
|
|
|
1038
1001
|
class _DropdownItem extends StatefulWidget {
|
|
@@ -1040,11 +1003,13 @@ class _DropdownItem extends StatefulWidget {
|
|
|
1040
1003
|
required this.label,
|
|
1041
1004
|
required this.selected,
|
|
1042
1005
|
required this.onTap,
|
|
1006
|
+
this.base = Colors.white,
|
|
1043
1007
|
});
|
|
1044
1008
|
|
|
1045
1009
|
final String label;
|
|
1046
1010
|
final bool selected;
|
|
1047
1011
|
final VoidCallback onTap;
|
|
1012
|
+
final Color base;
|
|
1048
1013
|
|
|
1049
1014
|
@override
|
|
1050
1015
|
State<_DropdownItem> createState() => _DropdownItemState();
|
|
@@ -1065,15 +1030,16 @@ class _DropdownItemState extends State<_DropdownItem> {
|
|
|
1065
1030
|
duration: const Duration(milliseconds: 120),
|
|
1066
1031
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
1067
1032
|
color: _hovered
|
|
1068
|
-
?
|
|
1033
|
+
? widget.base.withValues(alpha: 0.10)
|
|
1069
1034
|
: Colors.transparent,
|
|
1070
1035
|
child: Text(
|
|
1071
1036
|
widget.label,
|
|
1072
|
-
style:
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1037
|
+
style: _chromeText(
|
|
1038
|
+
size: 13,
|
|
1039
|
+
weight: widget.selected ? FontWeight.w600 : FontWeight.w400,
|
|
1040
|
+
color: widget.selected
|
|
1041
|
+
? widget.base
|
|
1042
|
+
: widget.base.withValues(alpha: 0.5),
|
|
1077
1043
|
),
|
|
1078
1044
|
),
|
|
1079
1045
|
),
|
|
@@ -1086,10 +1052,16 @@ class _PillChip extends StatefulWidget {
|
|
|
1086
1052
|
const _PillChip({
|
|
1087
1053
|
required this.label,
|
|
1088
1054
|
required this.onTap,
|
|
1055
|
+
this.base = Colors.white,
|
|
1056
|
+
this.active = false,
|
|
1089
1057
|
});
|
|
1090
1058
|
|
|
1091
1059
|
final String label;
|
|
1092
1060
|
final VoidCallback onTap;
|
|
1061
|
+
final Color base;
|
|
1062
|
+
|
|
1063
|
+
/// When true the chip reads as "non-default" (accent tint + accent text).
|
|
1064
|
+
final bool active;
|
|
1093
1065
|
|
|
1094
1066
|
@override
|
|
1095
1067
|
State<_PillChip> createState() => _PillChipState();
|
|
@@ -1100,6 +1072,14 @@ class _PillChipState extends State<_PillChip> {
|
|
|
1100
1072
|
|
|
1101
1073
|
@override
|
|
1102
1074
|
Widget build(BuildContext context) {
|
|
1075
|
+
final bool on = widget.active;
|
|
1076
|
+
final Color textColor = on ? Colors.white : widget.base;
|
|
1077
|
+
// No resting fill — it just cycles a value. Solid accent when active,
|
|
1078
|
+
// subtle fill only on hover otherwise.
|
|
1079
|
+
final Color bg = on
|
|
1080
|
+
? (_hovered ? _kAccent.withValues(alpha: 0.85) : _kAccent)
|
|
1081
|
+
: (_hovered ? widget.base.withValues(alpha: 0.14) : Colors.transparent);
|
|
1082
|
+
|
|
1103
1083
|
return MouseRegion(
|
|
1104
1084
|
onEnter: (_) => setState(() => _hovered = true),
|
|
1105
1085
|
onExit: (_) => setState(() => _hovered = false),
|
|
@@ -1111,16 +1091,16 @@ class _PillChipState extends State<_PillChip> {
|
|
|
1111
1091
|
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
|
1112
1092
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
1113
1093
|
decoration: BoxDecoration(
|
|
1114
|
-
color:
|
|
1094
|
+
color: bg,
|
|
1115
1095
|
borderRadius: BorderRadius.circular(20),
|
|
1116
1096
|
),
|
|
1117
1097
|
child: Text(
|
|
1118
1098
|
widget.label,
|
|
1119
|
-
style:
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1099
|
+
style: _chromeText(
|
|
1100
|
+
size: 11,
|
|
1101
|
+
weight: FontWeight.w600,
|
|
1102
|
+
color: textColor,
|
|
1103
|
+
spacing: 0.3,
|
|
1124
1104
|
),
|
|
1125
1105
|
),
|
|
1126
1106
|
),
|
|
@@ -1129,75 +1109,111 @@ class _PillChipState extends State<_PillChip> {
|
|
|
1129
1109
|
}
|
|
1130
1110
|
}
|
|
1131
1111
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
required this.onTap,
|
|
1137
|
-
this.checked,
|
|
1138
|
-
this.trailingLabel,
|
|
1139
|
-
});
|
|
1112
|
+
/// Small confirmation pill shown briefly in the chrome (e.g. after a screenshot
|
|
1113
|
+
/// is copied). Dark, frosted, centered near the bottom.
|
|
1114
|
+
class _Toast extends StatelessWidget {
|
|
1115
|
+
const _Toast({super.key, required this.message, this.isError = false});
|
|
1140
1116
|
|
|
1141
|
-
final
|
|
1142
|
-
final
|
|
1117
|
+
final String message;
|
|
1118
|
+
final bool isError;
|
|
1119
|
+
|
|
1120
|
+
@override
|
|
1121
|
+
Widget build(BuildContext context) {
|
|
1122
|
+
return DecoratedBox(
|
|
1123
|
+
decoration: BoxDecoration(
|
|
1124
|
+
color: const Color(0xF02C2C2E),
|
|
1125
|
+
borderRadius: BorderRadius.circular(20),
|
|
1126
|
+
boxShadow: const [
|
|
1127
|
+
BoxShadow(color: Color(0x40000000), blurRadius: 12, offset: Offset(0, 3)),
|
|
1128
|
+
],
|
|
1129
|
+
),
|
|
1130
|
+
child: Padding(
|
|
1131
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
|
|
1132
|
+
child: Row(
|
|
1133
|
+
mainAxisSize: MainAxisSize.min,
|
|
1134
|
+
children: [
|
|
1135
|
+
Icon(
|
|
1136
|
+
isError ? KasyIcons.error : KasyIcons.checkCircle,
|
|
1137
|
+
color: isError
|
|
1138
|
+
? const Color(0xFFFF453A)
|
|
1139
|
+
: const Color(0xFF34C759),
|
|
1140
|
+
size: 15,
|
|
1141
|
+
),
|
|
1142
|
+
const SizedBox(width: 8),
|
|
1143
|
+
Text(
|
|
1144
|
+
message,
|
|
1145
|
+
style: _chromeText(
|
|
1146
|
+
size: 12,
|
|
1147
|
+
weight: FontWeight.w500,
|
|
1148
|
+
color: Colors.white,
|
|
1149
|
+
),
|
|
1150
|
+
),
|
|
1151
|
+
],
|
|
1152
|
+
),
|
|
1153
|
+
),
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/// Standalone sun/moon theme toggle pinned to the top-right corner. Switches
|
|
1159
|
+
/// the preview canvas between light (sun) and dark (moon).
|
|
1160
|
+
class _ThemeCornerButton extends StatefulWidget {
|
|
1161
|
+
const _ThemeCornerButton({required this.dark, required this.onTap});
|
|
1162
|
+
|
|
1163
|
+
final bool dark;
|
|
1143
1164
|
final VoidCallback onTap;
|
|
1144
|
-
final bool? checked;
|
|
1145
|
-
final String? trailingLabel;
|
|
1146
1165
|
|
|
1147
1166
|
@override
|
|
1148
|
-
State<
|
|
1167
|
+
State<_ThemeCornerButton> createState() => _ThemeCornerButtonState();
|
|
1149
1168
|
}
|
|
1150
1169
|
|
|
1151
|
-
class
|
|
1170
|
+
class _ThemeCornerButtonState extends State<_ThemeCornerButton> {
|
|
1152
1171
|
bool _hovered = false;
|
|
1153
1172
|
|
|
1154
1173
|
@override
|
|
1155
1174
|
Widget build(BuildContext context) {
|
|
1175
|
+
// Light mode (sun): white fill, dark icon. Dark mode (moon): dark fill,
|
|
1176
|
+
// white icon. Both keep a subtle drop shadow to lift the button.
|
|
1177
|
+
final Color fill = widget.dark ? const Color(0xFF2C2C2E) : Colors.white;
|
|
1178
|
+
final Color iconColor = widget.dark
|
|
1179
|
+
? Colors.white
|
|
1180
|
+
: const Color(0xFF1C1C1E);
|
|
1181
|
+
final Color hoverOverlay = widget.dark
|
|
1182
|
+
? Colors.white.withValues(alpha: 0.12)
|
|
1183
|
+
: Colors.black.withValues(alpha: 0.08);
|
|
1184
|
+
|
|
1156
1185
|
return MouseRegion(
|
|
1157
1186
|
onEnter: (_) => setState(() => _hovered = true),
|
|
1158
1187
|
onExit: (_) => setState(() => _hovered = false),
|
|
1159
1188
|
cursor: SystemMouseCursors.click,
|
|
1160
1189
|
child: GestureDetector(
|
|
1161
1190
|
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
|
-
),
|
|
1191
|
+
child: DecoratedBox(
|
|
1192
|
+
decoration: BoxDecoration(
|
|
1193
|
+
shape: BoxShape.circle,
|
|
1194
|
+
color: fill,
|
|
1195
|
+
boxShadow: const [
|
|
1196
|
+
BoxShadow(
|
|
1197
|
+
color: Color(0x40000000),
|
|
1198
|
+
blurRadius: 10,
|
|
1199
|
+
offset: Offset(0, 3),
|
|
1180
1200
|
),
|
|
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
1201
|
],
|
|
1200
1202
|
),
|
|
1203
|
+
child: AnimatedContainer(
|
|
1204
|
+
duration: const Duration(milliseconds: 120),
|
|
1205
|
+
width: 28,
|
|
1206
|
+
height: 28,
|
|
1207
|
+
decoration: BoxDecoration(
|
|
1208
|
+
shape: BoxShape.circle,
|
|
1209
|
+
color: _hovered ? hoverOverlay : Colors.transparent,
|
|
1210
|
+
),
|
|
1211
|
+
child: Icon(
|
|
1212
|
+
widget.dark ? KasyIcons.darkMode : KasyIcons.lightMode,
|
|
1213
|
+
color: iconColor,
|
|
1214
|
+
size: 15,
|
|
1215
|
+
),
|
|
1216
|
+
),
|
|
1201
1217
|
),
|
|
1202
1218
|
),
|
|
1203
1219
|
);
|
|
@@ -1205,13 +1221,17 @@ class _ToolsItemState extends State<_ToolsItem> {
|
|
|
1205
1221
|
}
|
|
1206
1222
|
|
|
1207
1223
|
class _VerticalDivider extends StatelessWidget {
|
|
1224
|
+
const _VerticalDivider({this.base = Colors.white});
|
|
1225
|
+
|
|
1226
|
+
final Color base;
|
|
1227
|
+
|
|
1208
1228
|
@override
|
|
1209
1229
|
Widget build(BuildContext context) {
|
|
1210
1230
|
return Container(
|
|
1211
1231
|
width: 1,
|
|
1212
1232
|
height: 18,
|
|
1213
1233
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
|
1214
|
-
color:
|
|
1234
|
+
color: base.withValues(alpha: 0.12),
|
|
1215
1235
|
);
|
|
1216
1236
|
}
|
|
1217
1237
|
}
|
|
@@ -1221,11 +1241,17 @@ class _IconBtn extends StatefulWidget {
|
|
|
1221
1241
|
required this.icon,
|
|
1222
1242
|
required this.onTap,
|
|
1223
1243
|
this.color = Colors.white60,
|
|
1244
|
+
this.base = Colors.white,
|
|
1245
|
+
this.active = false,
|
|
1224
1246
|
});
|
|
1225
1247
|
|
|
1226
1248
|
final IconData icon;
|
|
1227
1249
|
final VoidCallback onTap;
|
|
1228
1250
|
final Color color;
|
|
1251
|
+
final Color base;
|
|
1252
|
+
|
|
1253
|
+
/// When true the button reads as "on": accent-tinted fill + accent icon.
|
|
1254
|
+
final bool active;
|
|
1229
1255
|
|
|
1230
1256
|
@override
|
|
1231
1257
|
State<_IconBtn> createState() => _IconBtnState();
|
|
@@ -1236,6 +1262,14 @@ class _IconBtnState extends State<_IconBtn> {
|
|
|
1236
1262
|
|
|
1237
1263
|
@override
|
|
1238
1264
|
Widget build(BuildContext context) {
|
|
1265
|
+
final bool on = widget.active;
|
|
1266
|
+
// Active: solid accent fill + white icon (filled-button contrast). Inactive:
|
|
1267
|
+
// solid foreground icon (no muted grays), background only on hover.
|
|
1268
|
+
final Color iconColor = on ? Colors.white : widget.color;
|
|
1269
|
+
final Color bg = on
|
|
1270
|
+
? (_hovered ? _kAccent.withValues(alpha: 0.85) : _kAccent)
|
|
1271
|
+
: (_hovered ? widget.base.withValues(alpha: 0.12) : Colors.transparent);
|
|
1272
|
+
|
|
1239
1273
|
return MouseRegion(
|
|
1240
1274
|
onEnter: (_) => setState(() => _hovered = true),
|
|
1241
1275
|
onExit: (_) => setState(() => _hovered = false),
|
|
@@ -1244,14 +1278,12 @@ class _IconBtnState extends State<_IconBtn> {
|
|
|
1244
1278
|
onTap: widget.onTap,
|
|
1245
1279
|
child: AnimatedContainer(
|
|
1246
1280
|
duration: const Duration(milliseconds: 120),
|
|
1247
|
-
padding: const EdgeInsets.symmetric(horizontal:
|
|
1281
|
+
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
|
1248
1282
|
decoration: BoxDecoration(
|
|
1249
|
-
color:
|
|
1250
|
-
? Colors.white.withValues(alpha: 0.12)
|
|
1251
|
-
: Colors.transparent,
|
|
1283
|
+
color: bg,
|
|
1252
1284
|
borderRadius: BorderRadius.circular(8),
|
|
1253
1285
|
),
|
|
1254
|
-
child: Icon(widget.icon, color:
|
|
1286
|
+
child: Icon(widget.icon, color: iconColor, size: 15),
|
|
1255
1287
|
),
|
|
1256
1288
|
),
|
|
1257
1289
|
);
|