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
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import 'dart:async';
|
|
2
|
+
import 'dart:ui' show ImageFilter;
|
|
2
3
|
|
|
3
4
|
import 'package:flutter/foundation.dart';
|
|
4
5
|
import 'package:flutter/material.dart';
|
|
5
6
|
import 'package:flutter/scheduler.dart';
|
|
6
7
|
import 'package:flutter/services.dart';
|
|
8
|
+
import 'package:google_fonts/google_fonts.dart';
|
|
7
9
|
import 'package:kasy_kit/core/dev_inspector/dev_inspector_info.dart';
|
|
8
10
|
import 'package:kasy_kit/core/dev_inspector/dev_inspector_service.dart';
|
|
11
|
+
import 'package:kasy_kit/core/icons/kasy_icons.dart';
|
|
9
12
|
import 'package:kasy_kit/core/theme/providers/theme_provider.dart';
|
|
10
13
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
11
14
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
@@ -28,13 +31,15 @@ final GlobalKey<ScaffoldMessengerState> devInspectorRootScaffoldMessengerKey =
|
|
|
28
31
|
/// users who already had the toggle on before the FAB was removed.
|
|
29
32
|
const String devInspectorEnabledPrefKey = 'dev_inspector_fab_enabled';
|
|
30
33
|
|
|
31
|
-
final ValueNotifier<bool> devInspectorEnabledNotifier =
|
|
32
|
-
|
|
34
|
+
final ValueNotifier<bool> devInspectorEnabledNotifier = ValueNotifier<bool>(
|
|
35
|
+
false,
|
|
36
|
+
);
|
|
33
37
|
|
|
34
38
|
/// Set to true to trigger a copy of the currently selected widget.
|
|
35
39
|
/// [DevInspector] handles the copy + feedback and resets this to false.
|
|
36
|
-
final ValueNotifier<bool> devInspectorCopyTriggerNotifier =
|
|
37
|
-
|
|
40
|
+
final ValueNotifier<bool> devInspectorCopyTriggerNotifier = ValueNotifier<bool>(
|
|
41
|
+
false,
|
|
42
|
+
);
|
|
38
43
|
|
|
39
44
|
/// Set to true to clear the current selection WITHOUT deactivating the
|
|
40
45
|
/// inspector. The Web Device Preview toggle fires this when entering/leaving
|
|
@@ -46,8 +51,9 @@ final ValueNotifier<bool> devInspectorClearSelectionTriggerNotifier =
|
|
|
46
51
|
/// Runtime active state of the inspector. Mirrors [devInspectorEnabledNotifier]
|
|
47
52
|
/// — the Web Device Preview pill, the admin toggle and the Esc shortcut all
|
|
48
53
|
/// flip the persisted notifier, and this one follows.
|
|
49
|
-
final ValueNotifier<bool> devInspectorActiveNotifier =
|
|
50
|
-
|
|
54
|
+
final ValueNotifier<bool> devInspectorActiveNotifier = ValueNotifier<bool>(
|
|
55
|
+
false,
|
|
56
|
+
);
|
|
51
57
|
|
|
52
58
|
/// Set to true to hide the in-app status pill that the [DevInspector] shows
|
|
53
59
|
/// while active (e.g. when the WebDevicePreview chrome is already displaying
|
|
@@ -55,6 +61,17 @@ final ValueNotifier<bool> devInspectorActiveNotifier =
|
|
|
55
61
|
final ValueNotifier<bool> devInspectorSuppressStatusPillNotifier =
|
|
56
62
|
ValueNotifier<bool>(false);
|
|
57
63
|
|
|
64
|
+
/// Confirmation message the [DevInspector] wants surfaced. When the
|
|
65
|
+
/// WebDevicePreview chrome is present it OWNS the toast surface (so the
|
|
66
|
+
/// inspector's "copied" pill lands in the exact same spot and size as the
|
|
67
|
+
/// chrome's own toasts, e.g. "Image copied"). The chrome listens here, shows
|
|
68
|
+
/// the toast, and resets this to null. When no chrome is present the inspector
|
|
69
|
+
/// renders its own in-app toast instead.
|
|
70
|
+
final ValueNotifier<({String message, bool isError})?>
|
|
71
|
+
devInspectorToastNotifier = ValueNotifier<({String message, bool isError})?>(
|
|
72
|
+
null,
|
|
73
|
+
);
|
|
74
|
+
|
|
58
75
|
/// Rect of the currently selected widget expressed in **root view coordinates**
|
|
59
76
|
/// (the browser window / native window). External surfaces — e.g. the Web
|
|
60
77
|
/// Device Preview chrome which renders OUTSIDE the device frame — listen to
|
|
@@ -94,11 +111,13 @@ class DevInspector extends StatefulWidget {
|
|
|
94
111
|
class _DevInspectorState extends State<DevInspector>
|
|
95
112
|
with TickerProviderStateMixin {
|
|
96
113
|
static const Duration _copyFeedbackVisible = Duration(milliseconds: 2200);
|
|
97
|
-
static const double _copyFeedbackRightInset = 16;
|
|
98
114
|
static const int _historyCapacity = 20;
|
|
99
115
|
|
|
100
116
|
bool _active = false;
|
|
101
117
|
bool _copyBusy = false;
|
|
118
|
+
// The X only hides the info card; the highlighted selection stays. Tapping a
|
|
119
|
+
// widget again (or selecting another) brings the card back.
|
|
120
|
+
bool _infoCardHidden = false;
|
|
102
121
|
|
|
103
122
|
Timer? _copyFeedbackTimer;
|
|
104
123
|
String? _copyFeedbackText;
|
|
@@ -107,6 +126,9 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
107
126
|
DevInspectorInfo? _selectedInfo;
|
|
108
127
|
RenderObject? _selectedRender;
|
|
109
128
|
Rect? _highlightRect;
|
|
129
|
+
// Live highlight of whatever the cursor is over (hover-to-inspect). Distinct
|
|
130
|
+
// from the locked selection [_highlightRect] — drawn softer, no history/copy.
|
|
131
|
+
Rect? _hoverRect;
|
|
110
132
|
Ticker? _highlightTicker;
|
|
111
133
|
late final AnimationController _transitionCtrl;
|
|
112
134
|
Rect? _transitionFromRect;
|
|
@@ -134,8 +156,9 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
134
156
|
devInspectorActiveNotifier.addListener(_handleActiveChanged);
|
|
135
157
|
devInspectorEnabledNotifier.addListener(_handleEnabledChanged);
|
|
136
158
|
devInspectorCopyTriggerNotifier.addListener(_onCopyTriggered);
|
|
137
|
-
devInspectorClearSelectionTriggerNotifier
|
|
138
|
-
|
|
159
|
+
devInspectorClearSelectionTriggerNotifier.addListener(
|
|
160
|
+
_onClearSelectionTriggered,
|
|
161
|
+
);
|
|
139
162
|
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
|
140
163
|
unawaited(_bootstrapEnabledPreference());
|
|
141
164
|
}
|
|
@@ -148,8 +171,9 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
148
171
|
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
|
149
172
|
devInspectorEnabledNotifier.removeListener(_handleEnabledChanged);
|
|
150
173
|
devInspectorCopyTriggerNotifier.removeListener(_onCopyTriggered);
|
|
151
|
-
devInspectorClearSelectionTriggerNotifier
|
|
152
|
-
|
|
174
|
+
devInspectorClearSelectionTriggerNotifier.removeListener(
|
|
175
|
+
_onClearSelectionTriggered,
|
|
176
|
+
);
|
|
153
177
|
devInspectorActiveNotifier.removeListener(_handleActiveChanged);
|
|
154
178
|
if (devInspectorActiveNotifier.value) {
|
|
155
179
|
devInspectorActiveNotifier.value = false;
|
|
@@ -195,7 +219,8 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
195
219
|
|
|
196
220
|
if (!_active) return false;
|
|
197
221
|
|
|
198
|
-
final bool hasModifier =
|
|
222
|
+
final bool hasModifier =
|
|
223
|
+
kb.isMetaPressed ||
|
|
199
224
|
kb.isControlPressed ||
|
|
200
225
|
kb.isAltPressed ||
|
|
201
226
|
kb.isShiftPressed;
|
|
@@ -347,6 +372,8 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
347
372
|
_selectedInfo = null;
|
|
348
373
|
_selectedRender = null;
|
|
349
374
|
_highlightRect = null;
|
|
375
|
+
_hoverRect = null;
|
|
376
|
+
_infoCardHidden = false;
|
|
350
377
|
_transitionFromRect = null;
|
|
351
378
|
_transitionToRect = null;
|
|
352
379
|
_transitionFromGlobalRect = null;
|
|
@@ -371,8 +398,27 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
371
398
|
_historyCursor = _history.length - 1;
|
|
372
399
|
}
|
|
373
400
|
|
|
401
|
+
/// Live hover preview: highlight whatever widget is under the cursor without
|
|
402
|
+
/// selecting it. Cheap rect-diff guard avoids rebuilding when nothing moved.
|
|
403
|
+
void _onInspectorHover(Offset globalPosition) {
|
|
404
|
+
if (!_active) return;
|
|
405
|
+
final RenderObject? content = _contentKey.currentContext
|
|
406
|
+
?.findRenderObject();
|
|
407
|
+
if (content is! RenderBox) return;
|
|
408
|
+
final picked = DevInspectorService.pickAtInBox(content, globalPosition);
|
|
409
|
+
final Rect? rect = picked == null
|
|
410
|
+
? null
|
|
411
|
+
: _rectInOverlaySpace(picked.renderObject);
|
|
412
|
+
if (rect != _hoverRect) setState(() => _hoverRect = rect);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
void _clearHover() {
|
|
416
|
+
if (_hoverRect != null) setState(() => _hoverRect = null);
|
|
417
|
+
}
|
|
418
|
+
|
|
374
419
|
void _onInspectorTap(Offset globalPosition) {
|
|
375
|
-
final RenderObject? content = _contentKey.currentContext
|
|
420
|
+
final RenderObject? content = _contentKey.currentContext
|
|
421
|
+
?.findRenderObject();
|
|
376
422
|
if (content is! RenderBox) {
|
|
377
423
|
HapticFeedback.heavyImpact();
|
|
378
424
|
return;
|
|
@@ -400,6 +446,8 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
400
446
|
setState(() {
|
|
401
447
|
_selectedInfo = picked!.info;
|
|
402
448
|
_selectedRender = picked.renderObject;
|
|
449
|
+
// A fresh tap always brings the card back (even on the same element).
|
|
450
|
+
_infoCardHidden = false;
|
|
403
451
|
});
|
|
404
452
|
|
|
405
453
|
if (!_navigatingHistory) {
|
|
@@ -457,8 +505,8 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
457
505
|
/// transforms applied above the overlay (e.g. WebDevicePreview's scale).
|
|
458
506
|
Rect? _rectInOverlaySpace(RenderObject target) {
|
|
459
507
|
if (!target.attached) return null;
|
|
460
|
-
final RenderObject? overlay =
|
|
461
|
-
|
|
508
|
+
final RenderObject? overlay = _overlayKey.currentContext
|
|
509
|
+
?.findRenderObject();
|
|
462
510
|
if (overlay == null || !overlay.attached) return null;
|
|
463
511
|
try {
|
|
464
512
|
final Matrix4 transform = target.getTransformTo(overlay);
|
|
@@ -516,6 +564,14 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
516
564
|
}
|
|
517
565
|
|
|
518
566
|
void _showCopyFeedback(String message, {required bool isError}) {
|
|
567
|
+
// When the WebDevicePreview chrome is up it owns the toast surface — hand
|
|
568
|
+
// the message off so the confirmation appears in the exact same place and
|
|
569
|
+
// size as the chrome's other toasts (e.g. "Image copied"). No local toast
|
|
570
|
+
// in that case, to avoid showing two.
|
|
571
|
+
if (devInspectorSuppressStatusPillNotifier.value) {
|
|
572
|
+
devInspectorToastNotifier.value = (message: message, isError: isError);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
519
575
|
_copyFeedbackTimer?.cancel();
|
|
520
576
|
setState(() {
|
|
521
577
|
_copyFeedbackText = message;
|
|
@@ -559,6 +615,43 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
559
615
|
}
|
|
560
616
|
}
|
|
561
617
|
|
|
618
|
+
/// Positions the glass info card next to the selected element, always clamped
|
|
619
|
+
/// inside the viewport — so it stays visible even for full-screen selections
|
|
620
|
+
/// (where there's no room above or below the element).
|
|
621
|
+
Widget _buildInfoCard(Size size, EdgeInsets padding, bool dark) {
|
|
622
|
+
final Rect rect = _highlightRect!;
|
|
623
|
+
final DevInspectorInfo info = _selectedInfo!;
|
|
624
|
+
const double panelW = 250;
|
|
625
|
+
const double gap = 10;
|
|
626
|
+
const double estHeight = 130;
|
|
627
|
+
final double minTop = padding.top + 8;
|
|
628
|
+
final double maxTop = size.height - padding.bottom - estHeight - 8;
|
|
629
|
+
|
|
630
|
+
double left = rect.left;
|
|
631
|
+
if (left + panelW > size.width - 8) left = size.width - 8 - panelW;
|
|
632
|
+
if (left < 8) left = 8;
|
|
633
|
+
|
|
634
|
+
// Prefer below the element; fall back to above; then clamp on-screen.
|
|
635
|
+
double top = rect.bottom + gap;
|
|
636
|
+
if (top > maxTop) top = rect.top - gap - estHeight;
|
|
637
|
+
top = top.clamp(minTop, maxTop < minTop ? minTop : maxTop);
|
|
638
|
+
|
|
639
|
+
return Positioned(
|
|
640
|
+
left: left,
|
|
641
|
+
top: top,
|
|
642
|
+
child: _InspectorInfoCard(
|
|
643
|
+
info: info,
|
|
644
|
+
dark: dark,
|
|
645
|
+
maxWidth: panelW,
|
|
646
|
+
onCopy: () {
|
|
647
|
+
unawaited(_copySelection());
|
|
648
|
+
setState(() => _infoCardHidden = true);
|
|
649
|
+
},
|
|
650
|
+
onClose: () => setState(() => _infoCardHidden = true),
|
|
651
|
+
),
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
562
655
|
(Size, EdgeInsets) _sizeAndPadding(BuildContext context) {
|
|
563
656
|
final MediaQueryData? mq = MediaQuery.maybeOf(context);
|
|
564
657
|
if (mq != null) return (mq.size, mq.padding);
|
|
@@ -592,6 +685,7 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
592
685
|
? appTheme.dark
|
|
593
686
|
: appTheme.light,
|
|
594
687
|
};
|
|
688
|
+
final bool dark = shellTheme.brightness == Brightness.dark;
|
|
595
689
|
|
|
596
690
|
return Directionality(
|
|
597
691
|
textDirection: _platformTextDirection(),
|
|
@@ -602,69 +696,72 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
602
696
|
return Stack(
|
|
603
697
|
clipBehavior: Clip.none,
|
|
604
698
|
children: <Widget>[
|
|
605
|
-
|
|
699
|
+
// While inspecting, the app must not react to clicks — only the
|
|
700
|
+
// inspector selects/highlights. AbsorbPointer swallows pointer
|
|
701
|
+
// events for the app subtree. Picking still works: the inspector
|
|
702
|
+
// calls hitTest directly on the content render box (via
|
|
703
|
+
// _contentKey), which is a CHILD of this AbsorbPointer, so the
|
|
704
|
+
// direct call bypasses the absorption.
|
|
705
|
+
AbsorbPointer(
|
|
706
|
+
absorbing: _active,
|
|
707
|
+
child: KeyedSubtree(key: _contentKey, child: widget.child),
|
|
708
|
+
),
|
|
606
709
|
if (_active)
|
|
607
710
|
Positioned.fill(
|
|
608
|
-
child:
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
711
|
+
child: MouseRegion(
|
|
712
|
+
opaque: false,
|
|
713
|
+
cursor: SystemMouseCursors.precise,
|
|
714
|
+
onHover: (PointerHoverEvent ev) =>
|
|
715
|
+
_onInspectorHover(ev.position),
|
|
716
|
+
onExit: (_) => _clearHover(),
|
|
717
|
+
child: Listener(
|
|
718
|
+
behavior: HitTestBehavior.opaque,
|
|
719
|
+
onPointerDown: (PointerDownEvent ev) =>
|
|
720
|
+
_onInspectorTap(ev.position),
|
|
721
|
+
child: IgnorePointer(
|
|
722
|
+
child: CustomPaint(
|
|
723
|
+
key: _overlayKey,
|
|
724
|
+
painter: _HighlightPainter(
|
|
725
|
+
rect: _highlightRect,
|
|
726
|
+
hoverRect: _hoverRect,
|
|
727
|
+
),
|
|
728
|
+
size: Size.infinite,
|
|
729
|
+
),
|
|
617
730
|
),
|
|
618
731
|
),
|
|
619
732
|
),
|
|
620
733
|
),
|
|
734
|
+
// Glass info card anchored to the selected element (click).
|
|
735
|
+
if (_active &&
|
|
736
|
+
!_infoCardHidden &&
|
|
737
|
+
_selectedInfo != null &&
|
|
738
|
+
_highlightRect != null)
|
|
739
|
+
_buildInfoCard(size, padding, dark),
|
|
621
740
|
if (_active)
|
|
622
741
|
Positioned(
|
|
623
742
|
bottom: padding.bottom + 12,
|
|
624
743
|
right: 12,
|
|
625
|
-
child: const IgnorePointer(
|
|
626
|
-
child: _InspectorStatusPill(),
|
|
627
|
-
),
|
|
744
|
+
child: const IgnorePointer(child: _InspectorStatusPill()),
|
|
628
745
|
),
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
child:
|
|
635
|
-
child:
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
? scheme.onError
|
|
644
|
-
: scheme.onInverseSurface;
|
|
645
|
-
return Material(
|
|
646
|
-
elevation: 12,
|
|
647
|
-
shadowColor: Colors.black45,
|
|
648
|
-
borderRadius: BorderRadius.circular(12),
|
|
649
|
-
color: bg,
|
|
650
|
-
child: Padding(
|
|
651
|
-
padding: const EdgeInsets.symmetric(
|
|
652
|
-
horizontal: 16,
|
|
653
|
-
vertical: 12,
|
|
654
|
-
),
|
|
655
|
-
child: Text(
|
|
656
|
-
_copyFeedbackText!,
|
|
657
|
-
style: theme.textTheme.bodyMedium?.copyWith(
|
|
658
|
-
color: fg,
|
|
659
|
-
fontWeight: FontWeight.w500,
|
|
660
|
-
),
|
|
746
|
+
Positioned(
|
|
747
|
+
left: 0,
|
|
748
|
+
right: 0,
|
|
749
|
+
bottom: padding.bottom + 24,
|
|
750
|
+
child: IgnorePointer(
|
|
751
|
+
child: Center(
|
|
752
|
+
child: AnimatedSwitcher(
|
|
753
|
+
duration: const Duration(milliseconds: 180),
|
|
754
|
+
child: _copyFeedbackText == null
|
|
755
|
+
? const SizedBox.shrink()
|
|
756
|
+
: _InspectorToast(
|
|
757
|
+
key: ValueKey<String>(_copyFeedbackText!),
|
|
758
|
+
message: _copyFeedbackText!,
|
|
759
|
+
isError: _copyFeedbackIsError,
|
|
661
760
|
),
|
|
662
|
-
),
|
|
663
|
-
);
|
|
664
|
-
},
|
|
665
761
|
),
|
|
666
762
|
),
|
|
667
763
|
),
|
|
764
|
+
),
|
|
668
765
|
],
|
|
669
766
|
);
|
|
670
767
|
},
|
|
@@ -674,13 +771,323 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
674
771
|
}
|
|
675
772
|
}
|
|
676
773
|
|
|
774
|
+
/// Frosted-glass info card shown next to the selected element. Adapts to light
|
|
775
|
+
/// and dark themes; offers Copy ID / Copy JSON / Copy for AI.
|
|
776
|
+
class _InspectorInfoCard extends StatelessWidget {
|
|
777
|
+
const _InspectorInfoCard({
|
|
778
|
+
required this.info,
|
|
779
|
+
required this.dark,
|
|
780
|
+
required this.maxWidth,
|
|
781
|
+
required this.onCopy,
|
|
782
|
+
required this.onClose,
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
final DevInspectorInfo info;
|
|
786
|
+
final bool dark;
|
|
787
|
+
final double maxWidth;
|
|
788
|
+
final VoidCallback onCopy;
|
|
789
|
+
final VoidCallback onClose;
|
|
790
|
+
|
|
791
|
+
@override
|
|
792
|
+
Widget build(BuildContext context) {
|
|
793
|
+
// Real frosted glass. Dark: a light tint that darkens downward. Light: a
|
|
794
|
+
// frosted WHITE — higher opacity so it reads as crisp glass instead of a
|
|
795
|
+
// washed-out tint over the light content behind it.
|
|
796
|
+
final Color glassTop = dark
|
|
797
|
+
? Colors.white.withValues(alpha: 0.10)
|
|
798
|
+
: Colors.white.withValues(alpha: 0.82);
|
|
799
|
+
final Color glassBottom = dark
|
|
800
|
+
? Colors.black.withValues(alpha: 0.30)
|
|
801
|
+
: Colors.white.withValues(alpha: 0.62);
|
|
802
|
+
final Color border = (dark ? Colors.white : Colors.black).withValues(
|
|
803
|
+
alpha: dark ? 0.14 : 0.08,
|
|
804
|
+
);
|
|
805
|
+
final Color fg = dark ? Colors.white : const Color(0xFF1C1C1E);
|
|
806
|
+
final Color faint = fg.withValues(alpha: 0.45);
|
|
807
|
+
|
|
808
|
+
return Material(
|
|
809
|
+
type: MaterialType.transparency,
|
|
810
|
+
child: ConstrainedBox(
|
|
811
|
+
constraints: BoxConstraints(maxWidth: maxWidth),
|
|
812
|
+
child: ClipRRect(
|
|
813
|
+
borderRadius: BorderRadius.circular(16),
|
|
814
|
+
child: BackdropFilter(
|
|
815
|
+
filter: ImageFilter.blur(sigmaX: 26, sigmaY: 26),
|
|
816
|
+
child: Container(
|
|
817
|
+
decoration: BoxDecoration(
|
|
818
|
+
gradient: LinearGradient(
|
|
819
|
+
begin: Alignment.topLeft,
|
|
820
|
+
end: Alignment.bottomRight,
|
|
821
|
+
colors: [glassTop, glassBottom],
|
|
822
|
+
),
|
|
823
|
+
borderRadius: BorderRadius.circular(16),
|
|
824
|
+
border: Border.all(color: border),
|
|
825
|
+
boxShadow: [
|
|
826
|
+
BoxShadow(
|
|
827
|
+
color: Colors.black.withValues(alpha: dark ? 0.45 : 0.18),
|
|
828
|
+
blurRadius: 28,
|
|
829
|
+
offset: const Offset(0, 10),
|
|
830
|
+
),
|
|
831
|
+
],
|
|
832
|
+
),
|
|
833
|
+
child: Stack(
|
|
834
|
+
children: [
|
|
835
|
+
Column(
|
|
836
|
+
mainAxisSize: MainAxisSize.min,
|
|
837
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
838
|
+
children: [
|
|
839
|
+
// Header: role chip (close button floats in the corner).
|
|
840
|
+
Padding(
|
|
841
|
+
padding: const EdgeInsets.fromLTRB(12, 10, 38, 8),
|
|
842
|
+
child: Align(
|
|
843
|
+
alignment: Alignment.centerLeft,
|
|
844
|
+
child: Container(
|
|
845
|
+
padding: const EdgeInsets.symmetric(
|
|
846
|
+
horizontal: 8,
|
|
847
|
+
vertical: 3,
|
|
848
|
+
),
|
|
849
|
+
decoration: BoxDecoration(
|
|
850
|
+
color: _highlightColor.withValues(alpha: 0.20),
|
|
851
|
+
borderRadius: BorderRadius.circular(6),
|
|
852
|
+
),
|
|
853
|
+
child: Text(
|
|
854
|
+
info.widgetType,
|
|
855
|
+
overflow: TextOverflow.ellipsis,
|
|
856
|
+
style: TextStyle(
|
|
857
|
+
color: fg,
|
|
858
|
+
fontWeight: FontWeight.w700,
|
|
859
|
+
fontSize: 12.5,
|
|
860
|
+
),
|
|
861
|
+
),
|
|
862
|
+
),
|
|
863
|
+
),
|
|
864
|
+
),
|
|
865
|
+
Divider(height: 1, color: border),
|
|
866
|
+
// Essential, non-redundant fields only: color (as hex) and
|
|
867
|
+
// size. Type is the chip; text/labels are skipped (already
|
|
868
|
+
// visible on screen); the rest lives in the copied context.
|
|
869
|
+
Padding(
|
|
870
|
+
padding: const EdgeInsets.fromLTRB(12, 10, 12, 6),
|
|
871
|
+
child: Column(
|
|
872
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
873
|
+
children: [
|
|
874
|
+
if (info.colorHex != null)
|
|
875
|
+
_field(
|
|
876
|
+
t.devInspector.fieldColor,
|
|
877
|
+
info.colorHex!,
|
|
878
|
+
faint,
|
|
879
|
+
fg,
|
|
880
|
+
swatch: _parseHex(info.colorHex!),
|
|
881
|
+
),
|
|
882
|
+
_field(
|
|
883
|
+
t.devInspector.fieldSize,
|
|
884
|
+
info.sizeLabel,
|
|
885
|
+
faint,
|
|
886
|
+
fg,
|
|
887
|
+
),
|
|
888
|
+
],
|
|
889
|
+
),
|
|
890
|
+
),
|
|
891
|
+
Divider(height: 1, color: border),
|
|
892
|
+
// Single copy action — subtle, so it doesn't shout.
|
|
893
|
+
Padding(
|
|
894
|
+
padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
|
|
895
|
+
child: _CardAction(
|
|
896
|
+
label: t.devInspector.copyJson,
|
|
897
|
+
fg: fg,
|
|
898
|
+
onTap: onCopy,
|
|
899
|
+
),
|
|
900
|
+
),
|
|
901
|
+
],
|
|
902
|
+
),
|
|
903
|
+
// Close button pinned to the top-right corner.
|
|
904
|
+
Positioned(
|
|
905
|
+
top: 6,
|
|
906
|
+
right: 6,
|
|
907
|
+
child: _CardIconBtn(
|
|
908
|
+
icon: KasyIcons.close,
|
|
909
|
+
color: faint,
|
|
910
|
+
onTap: onClose,
|
|
911
|
+
),
|
|
912
|
+
),
|
|
913
|
+
],
|
|
914
|
+
),
|
|
915
|
+
),
|
|
916
|
+
),
|
|
917
|
+
),
|
|
918
|
+
),
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
Widget _field(
|
|
923
|
+
String label,
|
|
924
|
+
String value,
|
|
925
|
+
Color labelColor,
|
|
926
|
+
Color valueColor, {
|
|
927
|
+
Color? swatch,
|
|
928
|
+
}) {
|
|
929
|
+
return Padding(
|
|
930
|
+
padding: const EdgeInsets.only(bottom: 6),
|
|
931
|
+
child: Row(
|
|
932
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
933
|
+
children: [
|
|
934
|
+
SizedBox(
|
|
935
|
+
width: 66,
|
|
936
|
+
child: Text(
|
|
937
|
+
label,
|
|
938
|
+
overflow: TextOverflow.ellipsis,
|
|
939
|
+
style: TextStyle(
|
|
940
|
+
color: labelColor,
|
|
941
|
+
fontSize: 11,
|
|
942
|
+
fontWeight: FontWeight.w500,
|
|
943
|
+
),
|
|
944
|
+
),
|
|
945
|
+
),
|
|
946
|
+
const SizedBox(width: 8),
|
|
947
|
+
if (swatch != null) ...[
|
|
948
|
+
Container(
|
|
949
|
+
width: 13,
|
|
950
|
+
height: 13,
|
|
951
|
+
margin: const EdgeInsets.only(top: 1, right: 6),
|
|
952
|
+
decoration: BoxDecoration(
|
|
953
|
+
color: swatch,
|
|
954
|
+
borderRadius: BorderRadius.circular(3),
|
|
955
|
+
border: Border.all(color: labelColor.withValues(alpha: 0.4)),
|
|
956
|
+
),
|
|
957
|
+
),
|
|
958
|
+
],
|
|
959
|
+
Expanded(
|
|
960
|
+
child: Text(
|
|
961
|
+
value,
|
|
962
|
+
style: TextStyle(
|
|
963
|
+
color: valueColor,
|
|
964
|
+
fontSize: 12,
|
|
965
|
+
fontWeight: FontWeight.w500,
|
|
966
|
+
),
|
|
967
|
+
),
|
|
968
|
+
),
|
|
969
|
+
],
|
|
970
|
+
),
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/// `#RRGGBB` or `#AARRGGBB` → Color, for the color swatch. Null if unparseable.
|
|
975
|
+
Color? _parseHex(String hex) {
|
|
976
|
+
var h = hex.replaceAll('#', '').trim();
|
|
977
|
+
if (h.length == 6) h = 'FF$h';
|
|
978
|
+
if (h.length != 8) return null;
|
|
979
|
+
final v = int.tryParse(h, radix: 16);
|
|
980
|
+
return v == null ? null : Color(v);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
class _CardIconBtn extends StatelessWidget {
|
|
985
|
+
const _CardIconBtn({
|
|
986
|
+
required this.icon,
|
|
987
|
+
required this.color,
|
|
988
|
+
required this.onTap,
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
final IconData icon;
|
|
992
|
+
final Color color;
|
|
993
|
+
final VoidCallback onTap;
|
|
994
|
+
|
|
995
|
+
@override
|
|
996
|
+
Widget build(BuildContext context) {
|
|
997
|
+
return InkResponse(
|
|
998
|
+
onTap: onTap,
|
|
999
|
+
radius: 18,
|
|
1000
|
+
child: Padding(
|
|
1001
|
+
padding: const EdgeInsets.all(6),
|
|
1002
|
+
child: Icon(icon, color: color, size: 16),
|
|
1003
|
+
),
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/// Compact, subtle action chip inside the info card — a quiet neutral fill that
|
|
1009
|
+
/// brightens slightly on hover, so it never competes with the inspected UI.
|
|
1010
|
+
class _CardAction extends StatefulWidget {
|
|
1011
|
+
const _CardAction({
|
|
1012
|
+
required this.label,
|
|
1013
|
+
required this.fg,
|
|
1014
|
+
required this.onTap,
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
final String label;
|
|
1018
|
+
final Color fg;
|
|
1019
|
+
final VoidCallback onTap;
|
|
1020
|
+
|
|
1021
|
+
@override
|
|
1022
|
+
State<_CardAction> createState() => _CardActionState();
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
class _CardActionState extends State<_CardAction> {
|
|
1026
|
+
bool _hovered = false;
|
|
1027
|
+
|
|
1028
|
+
@override
|
|
1029
|
+
Widget build(BuildContext context) {
|
|
1030
|
+
final Color bg = widget.fg.withValues(alpha: _hovered ? 0.12 : 0.06);
|
|
1031
|
+
return MouseRegion(
|
|
1032
|
+
onEnter: (_) => setState(() => _hovered = true),
|
|
1033
|
+
onExit: (_) => setState(() => _hovered = false),
|
|
1034
|
+
cursor: SystemMouseCursors.click,
|
|
1035
|
+
child: GestureDetector(
|
|
1036
|
+
onTap: widget.onTap,
|
|
1037
|
+
child: AnimatedContainer(
|
|
1038
|
+
duration: const Duration(milliseconds: 120),
|
|
1039
|
+
padding: const EdgeInsets.symmetric(vertical: 9),
|
|
1040
|
+
alignment: Alignment.center,
|
|
1041
|
+
decoration: BoxDecoration(
|
|
1042
|
+
color: bg,
|
|
1043
|
+
borderRadius: BorderRadius.circular(9),
|
|
1044
|
+
),
|
|
1045
|
+
child: Text(
|
|
1046
|
+
widget.label,
|
|
1047
|
+
style: TextStyle(
|
|
1048
|
+
color: widget.fg.withValues(alpha: 0.75),
|
|
1049
|
+
fontSize: 12,
|
|
1050
|
+
fontWeight: FontWeight.w500,
|
|
1051
|
+
),
|
|
1052
|
+
),
|
|
1053
|
+
),
|
|
1054
|
+
),
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
677
1059
|
class _HighlightPainter extends CustomPainter {
|
|
678
|
-
_HighlightPainter({required this.rect});
|
|
1060
|
+
_HighlightPainter({required this.rect, this.hoverRect});
|
|
679
1061
|
|
|
680
1062
|
final Rect? rect;
|
|
1063
|
+
final Rect? hoverRect;
|
|
681
1064
|
|
|
682
1065
|
@override
|
|
683
1066
|
void paint(Canvas canvas, Size size) {
|
|
1067
|
+
// Hover preview first (under the selection): soft translucent fill + thin
|
|
1068
|
+
// stroke. Skipped when it coincides with the locked selection.
|
|
1069
|
+
final Rect? hover = hoverRect;
|
|
1070
|
+
if (hover != null && !hover.isEmpty && hover != rect) {
|
|
1071
|
+
final RRect hoverRRect = RRect.fromRectAndRadius(
|
|
1072
|
+
hover,
|
|
1073
|
+
const Radius.circular(_highlightCornerRadius),
|
|
1074
|
+
);
|
|
1075
|
+
canvas.drawRRect(
|
|
1076
|
+
hoverRRect,
|
|
1077
|
+
Paint()
|
|
1078
|
+
..color = _highlightColor.withValues(alpha: 0.12)
|
|
1079
|
+
..style = PaintingStyle.fill,
|
|
1080
|
+
);
|
|
1081
|
+
canvas.drawRRect(
|
|
1082
|
+
hoverRRect,
|
|
1083
|
+
Paint()
|
|
1084
|
+
..color = _highlightColor.withValues(alpha: 0.6)
|
|
1085
|
+
..style = PaintingStyle.stroke
|
|
1086
|
+
..strokeWidth = 1.0,
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Locked selection: solid stroke.
|
|
684
1091
|
final Rect? r = rect;
|
|
685
1092
|
if (r == null || r.isEmpty) return;
|
|
686
1093
|
final Paint paint = Paint()
|
|
@@ -697,7 +1104,7 @@ class _HighlightPainter extends CustomPainter {
|
|
|
697
1104
|
|
|
698
1105
|
@override
|
|
699
1106
|
bool shouldRepaint(_HighlightPainter oldDelegate) =>
|
|
700
|
-
oldDelegate.rect != rect;
|
|
1107
|
+
oldDelegate.rect != rect || oldDelegate.hoverRect != hoverRect;
|
|
701
1108
|
}
|
|
702
1109
|
|
|
703
1110
|
/// Minimal status pill tucked in the bottom-right corner while the inspector
|
|
@@ -748,6 +1155,59 @@ class _InspectorStatusPill extends StatelessWidget {
|
|
|
748
1155
|
}
|
|
749
1156
|
}
|
|
750
1157
|
|
|
1158
|
+
/// Modern confirmation pill shown briefly after a copy. Mirrors the toast used
|
|
1159
|
+
/// by the WebDevicePreview chrome: dark frosted capsule, leading status icon,
|
|
1160
|
+
/// Inter text. Success shows a green check; errors show a red alert.
|
|
1161
|
+
class _InspectorToast extends StatelessWidget {
|
|
1162
|
+
const _InspectorToast({
|
|
1163
|
+
super.key,
|
|
1164
|
+
required this.message,
|
|
1165
|
+
required this.isError,
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
final String message;
|
|
1169
|
+
final bool isError;
|
|
1170
|
+
|
|
1171
|
+
@override
|
|
1172
|
+
Widget build(BuildContext context) {
|
|
1173
|
+
return DecoratedBox(
|
|
1174
|
+
decoration: BoxDecoration(
|
|
1175
|
+
color: const Color(0xF02C2C2E),
|
|
1176
|
+
borderRadius: BorderRadius.circular(20),
|
|
1177
|
+
boxShadow: const <BoxShadow>[
|
|
1178
|
+
BoxShadow(
|
|
1179
|
+
color: Color(0x40000000),
|
|
1180
|
+
blurRadius: 12,
|
|
1181
|
+
offset: Offset(0, 3),
|
|
1182
|
+
),
|
|
1183
|
+
],
|
|
1184
|
+
),
|
|
1185
|
+
child: Padding(
|
|
1186
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
|
|
1187
|
+
child: Row(
|
|
1188
|
+
mainAxisSize: MainAxisSize.min,
|
|
1189
|
+
children: <Widget>[
|
|
1190
|
+
Icon(
|
|
1191
|
+
isError ? KasyIcons.error : KasyIcons.checkCircle,
|
|
1192
|
+
color: isError ? const Color(0xFFFF453A) : const Color(0xFF34C759),
|
|
1193
|
+
size: 15,
|
|
1194
|
+
),
|
|
1195
|
+
const SizedBox(width: 8),
|
|
1196
|
+
Text(
|
|
1197
|
+
message,
|
|
1198
|
+
style: GoogleFonts.inter(
|
|
1199
|
+
fontSize: 12,
|
|
1200
|
+
fontWeight: FontWeight.w500,
|
|
1201
|
+
color: Colors.white,
|
|
1202
|
+
),
|
|
1203
|
+
),
|
|
1204
|
+
],
|
|
1205
|
+
),
|
|
1206
|
+
),
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
751
1211
|
class _PulsingDot extends StatefulWidget {
|
|
752
1212
|
const _PulsingDot();
|
|
753
1213
|
@override
|