kasy-cli 1.38.0 → 1.39.1
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/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/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 +2 -2
- package/templates/firebase/DESIGN_SYSTEM.md +23 -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 +5 -2
- package/templates/firebase/lib/components/kasy_app_bar.dart +325 -15
- package/templates/firebase/lib/components/kasy_card.dart +4 -0
- package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +18 -6
- 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 +27 -18
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +34 -16
- 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 +11 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +95 -30
- 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 +28 -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/web_device_preview/web_device_preview.dart +51 -19
- package/templates/firebase/lib/core/web_viewport_scale.dart +66 -36
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
- 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 +253 -125
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +263 -59
- 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 +111 -57
- package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -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 +2 -2
- 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 +53 -32
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart +4 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +895 -111
- 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 +171 -41
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +1 -1
- 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 +48 -47
- 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 +753 -712
- package/templates/firebase/lib/i18n/es.i18n.json +753 -712
- package/templates/firebase/lib/i18n/pt.i18n.json +753 -712
- package/templates/firebase/lib/main.dart +20 -7
- package/templates/firebase/lib/router.dart +32 -26
- package/templates/firebase/pubspec.yaml +2 -1
- package/templates/firebase/test/admin_shell_chrome_test.dart +11 -5
- package/templates/firebase/test/app_bar_config_test.dart +70 -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/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 -218
- package/templates/firebase/lib/core/chrome/web_header_scope.dart +0 -20
- 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 -179
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
|
@@ -29,7 +29,16 @@ class KasyTextField extends StatefulWidget {
|
|
|
29
29
|
/// changing this value grows or shrinks the visible box on every platform.
|
|
30
30
|
/// Matches the medium [KasyButton] height (45) so fields, the DatePicker
|
|
31
31
|
/// trigger and the primary action all share one control height.
|
|
32
|
-
static const double singleLineHeight =
|
|
32
|
+
static const double singleLineHeight = 45;
|
|
33
|
+
|
|
34
|
+
/// The single-line field renders its text at a fixed size and a *forced* line
|
|
35
|
+
/// box (see the [StrutStyle] on the inner field) so the box height is the same
|
|
36
|
+
/// on every renderer and breakpoint. Flutter web (CanvasKit) and native
|
|
37
|
+
/// otherwise disagree on a font's intrinsic line metrics by ~1px, and since
|
|
38
|
+
/// the box height is `text line + padding`, that variance would leak into the
|
|
39
|
+
/// visible height. Forcing the line to [fieldLineHeight] removes it entirely.
|
|
40
|
+
static const double fieldFontSize = 15;
|
|
41
|
+
static const double fieldLineHeight = 23;
|
|
33
42
|
|
|
34
43
|
final TextEditingController? controller;
|
|
35
44
|
final FocusNode? focusNode;
|
|
@@ -301,7 +310,10 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
301
310
|
// padding. So to hit [singleLineHeight] we back out the padding from it
|
|
302
311
|
// (subtract one text line, halve). This is the only lever that actually
|
|
303
312
|
// stretches the visible box — constraints/SizedBox just pad around it.
|
|
304
|
-
|
|
313
|
+
// The text line is forced to exactly [fieldLineHeight] (see the strut on the
|
|
314
|
+
// inner field), so this is deterministic on every renderer/platform:
|
|
315
|
+
// box = fieldLineHeight + 2×padding = singleLineHeight, always (45 → 11px).
|
|
316
|
+
const double singleLineTextHeight = KasyTextField.fieldLineHeight;
|
|
305
317
|
final double singleLineVerticalPadding =
|
|
306
318
|
((KasyTextField.singleLineHeight - singleLineTextHeight) / 2)
|
|
307
319
|
.clamp(0.0, 60.0);
|
|
@@ -487,11 +499,14 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
487
499
|
|
|
488
500
|
final Color fieldTextColor = dimDisabled(context.colors.onSurface);
|
|
489
501
|
final TextStyle fieldTextStyle =
|
|
490
|
-
context.textTheme.bodyLarge
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
502
|
+
(context.textTheme.bodyLarge ?? const TextStyle()).copyWith(
|
|
503
|
+
color: fieldTextColor,
|
|
504
|
+
fontSize: KasyTextField.fieldFontSize,
|
|
505
|
+
// Pin the line-height so the text box is a fixed [fieldLineHeight]px;
|
|
506
|
+
// paired with the forced strut on the field below, this makes the control
|
|
507
|
+
// height identical on every renderer (web CanvasKit vs native) and size.
|
|
508
|
+
height: KasyTextField.fieldLineHeight / KasyTextField.fieldFontSize,
|
|
509
|
+
);
|
|
495
510
|
final InputDecoration decoration = InputDecoration(
|
|
496
511
|
isDense: true,
|
|
497
512
|
hintText: widget.hint,
|
|
@@ -583,6 +598,13 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
583
598
|
maxLength: widget.maxLength,
|
|
584
599
|
buildCounter: hasCounter ? _hideInputCounter : null,
|
|
585
600
|
style: fieldTextStyle,
|
|
601
|
+
// Force the line box to exactly [fieldLineHeight] regardless of the font's
|
|
602
|
+
// intrinsic metrics, so the field is the same height on every renderer
|
|
603
|
+
// (web CanvasKit and native disagree by ~1px otherwise) and breakpoint.
|
|
604
|
+
strutStyle: StrutStyle.fromTextStyle(
|
|
605
|
+
fieldTextStyle,
|
|
606
|
+
forceStrutHeight: true,
|
|
607
|
+
),
|
|
586
608
|
decoration: decoration,
|
|
587
609
|
enableInteractiveSelection: widget.enableInteractiveSelection,
|
|
588
610
|
);
|
|
@@ -125,26 +125,35 @@ class BottomMenu extends StatelessWidget {
|
|
|
125
125
|
},
|
|
126
126
|
child: ResponsiveLayout(
|
|
127
127
|
small: Consumer(
|
|
128
|
-
|
|
128
|
+
// The scaffold is passed as `child` so it is built ONCE and reused
|
|
129
|
+
// across rebuilds of this Consumer. Toggling the setting below must
|
|
130
|
+
// NOT rebuild BartScaffold: if it did, Bart's MenuRouter (an
|
|
131
|
+
// InheritedWidget) re-runs its constructor and snaps the highlighted
|
|
132
|
+
// tab back to `initialRoute` — a value captured at mount, so possibly
|
|
133
|
+
// Home — even while the nested navigator still shows the current tab
|
|
134
|
+
// (the bottom bar marked "Início" while sitting on Settings).
|
|
135
|
+
child: NotificationListener<ScrollUpdateNotification>(
|
|
136
|
+
onNotification: (notification) {
|
|
137
|
+
KasyChromeVisibility.instance.handleScrollUpdate(notification);
|
|
138
|
+
return false; // let the notification keep bubbling
|
|
139
|
+
},
|
|
140
|
+
child: bart.BartScaffold(
|
|
141
|
+
routesBuilder: subRoutes,
|
|
142
|
+
bottomBar: kasyPaddedSurfaceBottomBar(),
|
|
143
|
+
initialRoute: resolvedInitialRoute,
|
|
144
|
+
showBottomBarOnStart: showBottomBarOnStart,
|
|
145
|
+
scaffoldOptions: scaffoldOptions,
|
|
146
|
+
onRouteChanged: _rememberActiveTab,
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
builder: (context, ref, child) {
|
|
129
150
|
// Watching the provider here keeps the persisted on/off setting in
|
|
130
|
-
// sync with the controller
|
|
131
|
-
//
|
|
132
|
-
//
|
|
151
|
+
// sync with the controller (the notifier writes the effective value
|
|
152
|
+
// into KasyChromeVisibility.instance.enabled), and scopes it to the
|
|
153
|
+
// mobile shell only. Returning the cached `child` keeps the scaffold
|
|
154
|
+
// out of this rebuild.
|
|
133
155
|
ref.watch(hideChromeOnScrollProvider);
|
|
134
|
-
return
|
|
135
|
-
onNotification: (notification) {
|
|
136
|
-
KasyChromeVisibility.instance.handleScrollUpdate(notification);
|
|
137
|
-
return false; // let the notification keep bubbling
|
|
138
|
-
},
|
|
139
|
-
child: bart.BartScaffold(
|
|
140
|
-
routesBuilder: subRoutes,
|
|
141
|
-
bottomBar: kasyPaddedSurfaceBottomBar(),
|
|
142
|
-
initialRoute: resolvedInitialRoute,
|
|
143
|
-
showBottomBarOnStart: showBottomBarOnStart,
|
|
144
|
-
scaffoldOptions: scaffoldOptions,
|
|
145
|
-
onRouteChanged: _rememberActiveTab,
|
|
146
|
-
),
|
|
147
|
-
);
|
|
156
|
+
return child!;
|
|
148
157
|
},
|
|
149
158
|
),
|
|
150
159
|
medium: connectedScaffold(),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
|
-
import 'package:kasy_kit/components/
|
|
3
|
-
import 'package:kasy_kit/core/chrome/
|
|
2
|
+
import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
3
|
+
import 'package:kasy_kit/core/chrome/app_bar_config.dart';
|
|
4
|
+
import 'package:kasy_kit/core/chrome/app_bar_scope.dart';
|
|
4
5
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
5
6
|
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
6
7
|
import 'package:kasy_kit/features/notifications/ui/widgets/web_notifications_bell.dart';
|
|
@@ -14,9 +15,10 @@ import 'package:kasy_kit/features/notifications/ui/widgets/web_notifications_bel
|
|
|
14
15
|
/// always jumps to the most recently mounted page.
|
|
15
16
|
FocusNode? kasyContentFocusTarget;
|
|
16
17
|
|
|
17
|
-
/// On desktop (≥ [DeviceType.large]) this puts the [
|
|
18
|
-
/// the content area, above the routed page. On phone/tablet it is
|
|
19
|
-
/// (returns the page untouched) — there the page keeps its own
|
|
18
|
+
/// On desktop (≥ [DeviceType.large]) this puts the [KasyAppBar.application] at the
|
|
19
|
+
/// top of the content area, above the routed page. On phone/tablet it is
|
|
20
|
+
/// transparent (returns the page untouched) — there the page keeps its own
|
|
21
|
+
/// [KasyAppBar].
|
|
20
22
|
///
|
|
21
23
|
/// Wrap each routed page with this in the bottom router so the header is present
|
|
22
24
|
/// across navigation without touching individual pages.
|
|
@@ -39,6 +41,20 @@ class _WebContentWrapperState extends State<WebContentWrapper> {
|
|
|
39
41
|
skipTraversal: true,
|
|
40
42
|
);
|
|
41
43
|
|
|
44
|
+
/// Shell-wide default for the desktop application bar: empty search, theme
|
|
45
|
+
/// toggle, the data-aware notifications bell, quick-create, and no avatar (the
|
|
46
|
+
/// sidebar owns the profile). Screens adapt this via [KasyAppBarConfigurator].
|
|
47
|
+
late final KasyAppBarConfig _defaultAppBarConfig = KasyAppBarConfig(
|
|
48
|
+
onToggleTheme: () => ThemeProvider.of(context).toggle(),
|
|
49
|
+
notifications: const WebNotificationsBell(),
|
|
50
|
+
onCreate: () {},
|
|
51
|
+
showAvatar: false,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/// Live config the bar renders; starts at the default, screens publish into it.
|
|
55
|
+
late final ValueNotifier<KasyAppBarConfig> _appBarConfig =
|
|
56
|
+
ValueNotifier<KasyAppBarConfig>(_defaultAppBarConfig);
|
|
57
|
+
|
|
42
58
|
@override
|
|
43
59
|
void initState() {
|
|
44
60
|
super.initState();
|
|
@@ -51,6 +67,7 @@ class _WebContentWrapperState extends State<WebContentWrapper> {
|
|
|
51
67
|
kasyContentFocusTarget = null;
|
|
52
68
|
}
|
|
53
69
|
_contentFocus.dispose();
|
|
70
|
+
_appBarConfig.dispose();
|
|
54
71
|
super.dispose();
|
|
55
72
|
}
|
|
56
73
|
|
|
@@ -65,25 +82,26 @@ class _WebContentWrapperState extends State<WebContentWrapper> {
|
|
|
65
82
|
// header (2) → content (3). Each block is its own FocusTraversalGroup so its
|
|
66
83
|
// internal order stays natural (e.g. header: search → theme → … → create).
|
|
67
84
|
//
|
|
68
|
-
//
|
|
69
|
-
// page-level KasyAppBar hides on desktop here (the
|
|
70
|
-
// Outside this scope (full-screen pushed routes) the
|
|
71
|
-
return
|
|
85
|
+
// KasyAppBarScope marks this subtree as "has an application bar above", so the
|
|
86
|
+
// page-level KasyAppBar hides on desktop here (the application bar owns the
|
|
87
|
+
// chrome). Outside this scope (full-screen pushed routes) the bar stays visible.
|
|
88
|
+
return KasyAppBarScope(
|
|
89
|
+
config: _appBarConfig,
|
|
90
|
+
defaultConfig: _defaultAppBarConfig,
|
|
72
91
|
child: ColoredBox(
|
|
73
92
|
color: context.colors.background,
|
|
74
93
|
child: Column(
|
|
75
94
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
76
95
|
children: [
|
|
77
|
-
//
|
|
78
|
-
//
|
|
96
|
+
// One application bar for the whole shell; each screen tunes it through
|
|
97
|
+
// a KasyAppBarConfigurator that publishes into [_appBarConfig].
|
|
79
98
|
FocusTraversalOrder(
|
|
80
99
|
order: const NumericFocusOrder(2),
|
|
81
100
|
child: FocusTraversalGroup(
|
|
82
|
-
child:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
showAvatar: false,
|
|
101
|
+
child: ValueListenableBuilder<KasyAppBarConfig>(
|
|
102
|
+
valueListenable: _appBarConfig,
|
|
103
|
+
builder: (context, config, _) =>
|
|
104
|
+
KasyAppBar.fromConfig(config),
|
|
87
105
|
),
|
|
88
106
|
),
|
|
89
107
|
),
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import 'package:flutter/widgets.dart';
|
|
2
|
+
import 'package:kasy_kit/components/kasy_avatar_presets.dart';
|
|
3
|
+
|
|
4
|
+
/// Declares what the desktop application bar ([KasyAppBar.application]) shows for
|
|
5
|
+
/// a given screen.
|
|
6
|
+
///
|
|
7
|
+
/// The shell (`WebContentWrapper`) renders one application bar above all content
|
|
8
|
+
/// and seeds it with an app-wide default. A screen overrides that default by
|
|
9
|
+
/// wrapping its body in a [KasyAppBarConfigurator] (or, for screens built on
|
|
10
|
+
/// `KasyOverlayScaffold`, by passing `applicationBar:`). So each screen decides
|
|
11
|
+
/// its own desktop chrome — search here, just notifications there — instead of
|
|
12
|
+
/// every screen sharing one fixed bar.
|
|
13
|
+
///
|
|
14
|
+
/// Each element has a `showX` flag (presets toggle these) plus the data/callbacks
|
|
15
|
+
/// it needs when shown. A bare [KasyAppBarConfig] mirrors the app default.
|
|
16
|
+
@immutable
|
|
17
|
+
class KasyAppBarConfig {
|
|
18
|
+
// Search ------------------------------------------------------------------
|
|
19
|
+
final bool showSearch;
|
|
20
|
+
final TextEditingController? searchController;
|
|
21
|
+
final String searchHint;
|
|
22
|
+
final ValueChanged<String>? onSearchChanged;
|
|
23
|
+
final ValueChanged<String>? onSearchSubmitted;
|
|
24
|
+
|
|
25
|
+
// Theme toggle ------------------------------------------------------------
|
|
26
|
+
final bool showThemeToggle;
|
|
27
|
+
final VoidCallback? onToggleTheme;
|
|
28
|
+
|
|
29
|
+
// Notifications -----------------------------------------------------------
|
|
30
|
+
final bool showNotifications;
|
|
31
|
+
|
|
32
|
+
/// Custom notifications control (e.g. a data-aware bell). Replaces the built-in
|
|
33
|
+
/// bell when set and [showNotifications] is true.
|
|
34
|
+
final Widget? notifications;
|
|
35
|
+
final VoidCallback? onNotifications;
|
|
36
|
+
final bool showNotificationBadge;
|
|
37
|
+
|
|
38
|
+
// Create ------------------------------------------------------------------
|
|
39
|
+
final bool showCreate;
|
|
40
|
+
final String createLabel;
|
|
41
|
+
final VoidCallback? onCreate;
|
|
42
|
+
|
|
43
|
+
// Profile -----------------------------------------------------------------
|
|
44
|
+
final bool showAvatar;
|
|
45
|
+
final Widget? avatar;
|
|
46
|
+
final KasyAvatarGradientData? avatarGradient;
|
|
47
|
+
final VoidCallback? onAvatarTap;
|
|
48
|
+
|
|
49
|
+
/// Full control: every element shown by default (matches the app shell). Set a
|
|
50
|
+
/// `showX` to false to drop that element, or pass data/callbacks to wire it.
|
|
51
|
+
const KasyAppBarConfig({
|
|
52
|
+
this.showSearch = true,
|
|
53
|
+
this.searchController,
|
|
54
|
+
this.searchHint = 'Search...',
|
|
55
|
+
this.onSearchChanged,
|
|
56
|
+
this.onSearchSubmitted,
|
|
57
|
+
this.showThemeToggle = true,
|
|
58
|
+
this.onToggleTheme,
|
|
59
|
+
this.showNotifications = true,
|
|
60
|
+
this.notifications,
|
|
61
|
+
this.onNotifications,
|
|
62
|
+
this.showNotificationBadge = false,
|
|
63
|
+
this.showCreate = true,
|
|
64
|
+
this.createLabel = 'Create',
|
|
65
|
+
this.onCreate,
|
|
66
|
+
this.showAvatar = true,
|
|
67
|
+
this.avatar,
|
|
68
|
+
this.avatarGradient,
|
|
69
|
+
this.onAvatarTap,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/// Preset: only the theme toggle — the quietest bar (settings-style screens).
|
|
73
|
+
const KasyAppBarConfig.minimal({VoidCallback? onToggleTheme})
|
|
74
|
+
: this(
|
|
75
|
+
showSearch: false,
|
|
76
|
+
showThemeToggle: true,
|
|
77
|
+
onToggleTheme: onToggleTheme,
|
|
78
|
+
showNotifications: false,
|
|
79
|
+
showCreate: false,
|
|
80
|
+
showAvatar: false,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
/// Preset: search + theme, no quick-create (browse / list screens).
|
|
84
|
+
const KasyAppBarConfig.search({
|
|
85
|
+
TextEditingController? controller,
|
|
86
|
+
String hint = 'Search...',
|
|
87
|
+
ValueChanged<String>? onChanged,
|
|
88
|
+
ValueChanged<String>? onSubmitted,
|
|
89
|
+
VoidCallback? onToggleTheme,
|
|
90
|
+
}) : this(
|
|
91
|
+
showSearch: true,
|
|
92
|
+
searchController: controller,
|
|
93
|
+
searchHint: hint,
|
|
94
|
+
onSearchChanged: onChanged,
|
|
95
|
+
onSearchSubmitted: onSubmitted,
|
|
96
|
+
showThemeToggle: true,
|
|
97
|
+
onToggleTheme: onToggleTheme,
|
|
98
|
+
showNotifications: false,
|
|
99
|
+
showCreate: false,
|
|
100
|
+
showAvatar: false,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
/// Show ONLY the listed elements (everything else hidden), keeping each one's
|
|
104
|
+
/// data and callbacks from the shell default — so a screen can say "just the
|
|
105
|
+
/// notifications and theme here" without rebuilding the bell or theme wiring.
|
|
106
|
+
///
|
|
107
|
+
/// ```dart
|
|
108
|
+
/// configure: (bar) => bar.only(search: true), // browse screen
|
|
109
|
+
/// configure: (bar) => bar.only(notifications: true, themeToggle: true),
|
|
110
|
+
/// ```
|
|
111
|
+
KasyAppBarConfig only({
|
|
112
|
+
bool search = false,
|
|
113
|
+
bool themeToggle = false,
|
|
114
|
+
bool notifications = false,
|
|
115
|
+
bool create = false,
|
|
116
|
+
bool avatar = false,
|
|
117
|
+
}) {
|
|
118
|
+
return copyWith(
|
|
119
|
+
showSearch: search,
|
|
120
|
+
showThemeToggle: themeToggle,
|
|
121
|
+
showNotifications: notifications,
|
|
122
|
+
showCreate: create,
|
|
123
|
+
showAvatar: avatar,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Copy with selective overrides — handy for adapting the shell default.
|
|
128
|
+
KasyAppBarConfig copyWith({
|
|
129
|
+
bool? showSearch,
|
|
130
|
+
TextEditingController? searchController,
|
|
131
|
+
String? searchHint,
|
|
132
|
+
ValueChanged<String>? onSearchChanged,
|
|
133
|
+
ValueChanged<String>? onSearchSubmitted,
|
|
134
|
+
bool? showThemeToggle,
|
|
135
|
+
VoidCallback? onToggleTheme,
|
|
136
|
+
bool? showNotifications,
|
|
137
|
+
Widget? notifications,
|
|
138
|
+
VoidCallback? onNotifications,
|
|
139
|
+
bool? showNotificationBadge,
|
|
140
|
+
bool? showCreate,
|
|
141
|
+
String? createLabel,
|
|
142
|
+
VoidCallback? onCreate,
|
|
143
|
+
bool? showAvatar,
|
|
144
|
+
Widget? avatar,
|
|
145
|
+
KasyAvatarGradientData? avatarGradient,
|
|
146
|
+
VoidCallback? onAvatarTap,
|
|
147
|
+
}) {
|
|
148
|
+
return KasyAppBarConfig(
|
|
149
|
+
showSearch: showSearch ?? this.showSearch,
|
|
150
|
+
searchController: searchController ?? this.searchController,
|
|
151
|
+
searchHint: searchHint ?? this.searchHint,
|
|
152
|
+
onSearchChanged: onSearchChanged ?? this.onSearchChanged,
|
|
153
|
+
onSearchSubmitted: onSearchSubmitted ?? this.onSearchSubmitted,
|
|
154
|
+
showThemeToggle: showThemeToggle ?? this.showThemeToggle,
|
|
155
|
+
onToggleTheme: onToggleTheme ?? this.onToggleTheme,
|
|
156
|
+
showNotifications: showNotifications ?? this.showNotifications,
|
|
157
|
+
notifications: notifications ?? this.notifications,
|
|
158
|
+
onNotifications: onNotifications ?? this.onNotifications,
|
|
159
|
+
showNotificationBadge: showNotificationBadge ?? this.showNotificationBadge,
|
|
160
|
+
showCreate: showCreate ?? this.showCreate,
|
|
161
|
+
createLabel: createLabel ?? this.createLabel,
|
|
162
|
+
onCreate: onCreate ?? this.onCreate,
|
|
163
|
+
showAvatar: showAvatar ?? this.showAvatar,
|
|
164
|
+
avatar: avatar ?? this.avatar,
|
|
165
|
+
avatarGradient: avatarGradient ?? this.avatarGradient,
|
|
166
|
+
onAvatarTap: onAvatarTap ?? this.onAvatarTap,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@override
|
|
171
|
+
bool operator ==(Object other) {
|
|
172
|
+
return other is KasyAppBarConfig &&
|
|
173
|
+
other.showSearch == showSearch &&
|
|
174
|
+
other.searchController == searchController &&
|
|
175
|
+
other.searchHint == searchHint &&
|
|
176
|
+
other.onSearchChanged == onSearchChanged &&
|
|
177
|
+
other.onSearchSubmitted == onSearchSubmitted &&
|
|
178
|
+
other.showThemeToggle == showThemeToggle &&
|
|
179
|
+
other.onToggleTheme == onToggleTheme &&
|
|
180
|
+
other.showNotifications == showNotifications &&
|
|
181
|
+
other.notifications == notifications &&
|
|
182
|
+
other.onNotifications == onNotifications &&
|
|
183
|
+
other.showNotificationBadge == showNotificationBadge &&
|
|
184
|
+
other.showCreate == showCreate &&
|
|
185
|
+
other.createLabel == createLabel &&
|
|
186
|
+
other.onCreate == onCreate &&
|
|
187
|
+
other.showAvatar == showAvatar &&
|
|
188
|
+
other.avatar == avatar &&
|
|
189
|
+
other.avatarGradient == avatarGradient &&
|
|
190
|
+
other.onAvatarTap == onAvatarTap;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@override
|
|
194
|
+
int get hashCode => Object.hashAll(<Object?>[
|
|
195
|
+
showSearch,
|
|
196
|
+
searchController,
|
|
197
|
+
searchHint,
|
|
198
|
+
onSearchChanged,
|
|
199
|
+
onSearchSubmitted,
|
|
200
|
+
showThemeToggle,
|
|
201
|
+
onToggleTheme,
|
|
202
|
+
showNotifications,
|
|
203
|
+
notifications,
|
|
204
|
+
onNotifications,
|
|
205
|
+
showNotificationBadge,
|
|
206
|
+
showCreate,
|
|
207
|
+
createLabel,
|
|
208
|
+
onCreate,
|
|
209
|
+
showAvatar,
|
|
210
|
+
avatar,
|
|
211
|
+
avatarGradient,
|
|
212
|
+
onAvatarTap,
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import 'package:flutter/widgets.dart';
|
|
2
|
+
import 'package:kasy_kit/core/chrome/app_bar_config.dart';
|
|
3
|
+
|
|
4
|
+
/// Transforms the shell's default application-bar config into a screen's version.
|
|
5
|
+
/// Receives the shell default (with its wired bell, theme toggle, etc.) so a
|
|
6
|
+
/// screen only toggles/overrides what it needs and inherits the rest.
|
|
7
|
+
typedef KasyAppBarConfigure = KasyAppBarConfig Function(KasyAppBarConfig base);
|
|
8
|
+
|
|
9
|
+
/// Marks the subtree that sits BELOW the desktop application bar
|
|
10
|
+
/// ([KasyAppBar.application]) — i.e. the shell content area, provided by
|
|
11
|
+
/// [WebContentWrapper] — and carries the config the bar renders from.
|
|
12
|
+
///
|
|
13
|
+
/// Two jobs:
|
|
14
|
+
/// 1. Presence ([of]) lets the page-level [KasyAppBar] hide on desktop INSIDE the
|
|
15
|
+
/// shell (where the application bar owns the chrome) but stay visible OUTSIDE
|
|
16
|
+
/// it (a full-screen pushed route), so the back button is never lost.
|
|
17
|
+
/// 2. [config] is the live config the bar listens to; [defaultConfig] is the
|
|
18
|
+
/// shell's app-wide default. A [KasyAppBarConfigurator] in the routed page
|
|
19
|
+
/// publishes `configure(defaultConfig)` into [config] to override the bar for
|
|
20
|
+
/// that screen.
|
|
21
|
+
class KasyAppBarScope extends InheritedWidget {
|
|
22
|
+
/// Live config the application bar renders from. The bar listens; screens write.
|
|
23
|
+
final ValueNotifier<KasyAppBarConfig> config;
|
|
24
|
+
|
|
25
|
+
/// The shell's app-wide default — the starting point screen overrides build on.
|
|
26
|
+
final KasyAppBarConfig defaultConfig;
|
|
27
|
+
|
|
28
|
+
const KasyAppBarScope({
|
|
29
|
+
super.key,
|
|
30
|
+
required this.config,
|
|
31
|
+
required this.defaultConfig,
|
|
32
|
+
required super.child,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/// True when a [KasyAppBarScope] is an ancestor (no rebuild dependency —
|
|
36
|
+
/// presence is fixed for a given subtree).
|
|
37
|
+
static bool of(BuildContext context) => maybeOf(context) != null;
|
|
38
|
+
|
|
39
|
+
/// The nearest scope, or null outside the shell (e.g. full-screen routes).
|
|
40
|
+
static KasyAppBarScope? maybeOf(BuildContext context) =>
|
|
41
|
+
context.getInheritedWidgetOfExactType<KasyAppBarScope>();
|
|
42
|
+
|
|
43
|
+
@override
|
|
44
|
+
bool updateShouldNotify(KasyAppBarScope oldWidget) =>
|
|
45
|
+
config != oldWidget.config || defaultConfig != oldWidget.defaultConfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Wrap a routed page's body with this to override the desktop application bar
|
|
49
|
+
/// for that screen. On phone/tablet (no scope above) it is a transparent
|
|
50
|
+
/// pass-through, so it is always safe to add.
|
|
51
|
+
///
|
|
52
|
+
/// ```dart
|
|
53
|
+
/// KasyAppBarConfigurator(
|
|
54
|
+
/// configure: (bar) => bar.only(search: true),
|
|
55
|
+
/// child: MyScreen(),
|
|
56
|
+
/// )
|
|
57
|
+
/// ```
|
|
58
|
+
class KasyAppBarConfigurator extends StatefulWidget {
|
|
59
|
+
/// How this screen adapts the shell default. Return [base] unchanged to keep
|
|
60
|
+
/// the default, or e.g. `base.only(...)` / `base.copyWith(...)`.
|
|
61
|
+
final KasyAppBarConfigure configure;
|
|
62
|
+
final Widget child;
|
|
63
|
+
|
|
64
|
+
const KasyAppBarConfigurator({
|
|
65
|
+
super.key,
|
|
66
|
+
required this.configure,
|
|
67
|
+
required this.child,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
State<KasyAppBarConfigurator> createState() => _KasyAppBarConfiguratorState();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class _KasyAppBarConfiguratorState extends State<KasyAppBarConfigurator> {
|
|
75
|
+
void _publish() {
|
|
76
|
+
if (!mounted) return;
|
|
77
|
+
final KasyAppBarScope? scope = KasyAppBarScope.maybeOf(context);
|
|
78
|
+
if (scope == null) return; // phone/tablet: no application bar to configure.
|
|
79
|
+
// Always build from the ORIGINAL default (not the current value), so the
|
|
80
|
+
// override is stable across rebuilds. The setter is a no-op when the result
|
|
81
|
+
// is unchanged (==), so an inline-built config does not churn the bar.
|
|
82
|
+
//
|
|
83
|
+
// Published post-frame: the bar (a sibling above this page in the shell
|
|
84
|
+
// Column) has already built this frame, so it picks the override up next.
|
|
85
|
+
scope.config.value = widget.configure(scope.defaultConfig);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@override
|
|
89
|
+
void didChangeDependencies() {
|
|
90
|
+
super.didChangeDependencies();
|
|
91
|
+
WidgetsBinding.instance.addPostFrameCallback((_) => _publish());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
void didUpdateWidget(covariant KasyAppBarConfigurator oldWidget) {
|
|
96
|
+
super.didUpdateWidget(oldWidget);
|
|
97
|
+
WidgetsBinding.instance.addPostFrameCallback((_) => _publish());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@override
|
|
101
|
+
Widget build(BuildContext context) => widget.child;
|
|
102
|
+
}
|
|
@@ -78,6 +78,17 @@ class UserApi {
|
|
|
78
78
|
'authFunctions-deleteUserAccount',
|
|
79
79
|
);
|
|
80
80
|
await callable.call();
|
|
81
|
+
} on FirebaseFunctionsException catch (err, stacktrace) {
|
|
82
|
+
// Surface the REAL cause in the logs instead of a generic message, so a
|
|
83
|
+
// failed deletion is diagnosable at a glance:
|
|
84
|
+
// code 'not-found' → the function isn't deployed yet (run a deploy)
|
|
85
|
+
// code 'unauthenticated'→ the user's token didn't reach the function
|
|
86
|
+
// code 'internal' → the function ran but threw (see its own logs)
|
|
87
|
+
_logger.e(
|
|
88
|
+
"delete user error [${err.code}] ${err.message}",
|
|
89
|
+
stackTrace: stacktrace,
|
|
90
|
+
);
|
|
91
|
+
throw ApiError(message: err.message ?? "Error while deleting user");
|
|
81
92
|
} catch (err, stacktrace) {
|
|
82
93
|
_logger.e("delete user error $err", stackTrace: stacktrace);
|
|
83
94
|
throw ApiError(message: "Error while deleting user");
|
|
@@ -6,7 +6,15 @@ import 'package:kasy_kit/core/dev_inspector/dev_inspector_info.dart';
|
|
|
6
6
|
class DevInspectorService {
|
|
7
7
|
DevInspectorService._();
|
|
8
8
|
|
|
9
|
-
static const _screenRegex = r'(Page|Screen|Route|View)$';
|
|
9
|
+
static const _screenRegex = r'(Page|Screen|Route|View|Tab|Shell)$';
|
|
10
|
+
|
|
11
|
+
/// Strips generic type arguments so `PopupMenuItem<bool>` matches against
|
|
12
|
+
/// `PopupMenuItem`, `Router<Object>` against `Router`, etc. Set / regex
|
|
13
|
+
/// lookups all compare against this base name.
|
|
14
|
+
static String _baseName(String typeName) {
|
|
15
|
+
final i = typeName.indexOf('<');
|
|
16
|
+
return i < 0 ? typeName : typeName.substring(0, i);
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
// Widgets we refuse to return — even after climbing.
|
|
12
20
|
static const _skipSet = <String>{
|
|
@@ -64,9 +72,32 @@ class DevInspectorService {
|
|
|
64
72
|
|
|
65
73
|
// Context propagation
|
|
66
74
|
'Theme', 'DefaultTextStyle', 'IconTheme', 'IconButtonTheme',
|
|
75
|
+
'CupertinoTheme', 'InheritedCupertinoTheme', 'CupertinoUserInterfaceLevel',
|
|
76
|
+
'ResponsiveTextTheme',
|
|
67
77
|
'Localizations', 'Directionality', 'Title',
|
|
68
78
|
'DefaultSelectionStyle', 'SelectionContainer', 'AnnotatedRegion',
|
|
69
79
|
|
|
80
|
+
// App-root / navigation / overlay scaffolding (framework + go_router +
|
|
81
|
+
// project shell). Selecting these is never useful — they're plumbing.
|
|
82
|
+
'Router', 'InheritedGoRouter', 'GoRouterStateRegistryScope',
|
|
83
|
+
'StatefulNavigationShell', 'Navigator', 'Overlay', 'OverlayPortal',
|
|
84
|
+
'CustomMultiChildLayout', 'CustomSingleChildLayout', 'LayoutId',
|
|
85
|
+
'ScaffoldMessenger', 'TapRegion', 'TapRegionSurface',
|
|
86
|
+
'ShortcutRegistrar', 'DefaultTextEditingShortcuts', 'ExcludeFocus',
|
|
87
|
+
'FocusVisibility', 'Initializer',
|
|
88
|
+
|
|
89
|
+
// Framework app-root + dev wrappers (the engine's View/RootWidget, the app
|
|
90
|
+
// widget, Riverpod/i18n providers, and the device_preview chrome used on
|
|
91
|
+
// web). None is ever a meaningful selection or a "screen".
|
|
92
|
+
'RootWidget', 'View', 'RawView', 'ViewAnchor',
|
|
93
|
+
'WidgetsApp', 'MaterialApp', 'CupertinoApp', 'MyApp',
|
|
94
|
+
'RootRestorationScope', 'SharedAppData',
|
|
95
|
+
'TranslationProvider', 'InheritedLocaleData',
|
|
96
|
+
'ProviderScope', 'UncontrolledProviderScope',
|
|
97
|
+
'ThemeProvider', 'ChangeNotifierProvider', 'MediaQueryObserver',
|
|
98
|
+
'WebDevicePreview', 'DevicePreview', 'DeviceFrame', 'VirtualKeyboard',
|
|
99
|
+
'RotatedBox',
|
|
100
|
+
|
|
70
101
|
// Routing / storage
|
|
71
102
|
'KeyedSubtree', 'AutomaticKeepAlive', 'KeepAlive', 'TickerMode',
|
|
72
103
|
'Offstage', 'PageStorage', 'RestorationScope',
|
|
@@ -93,12 +124,8 @@ class DevInspectorService {
|
|
|
93
124
|
'RichText', 'RawMaterialButton',
|
|
94
125
|
};
|
|
95
126
|
|
|
96
|
-
static bool _shouldClimbPast(String typeName)
|
|
97
|
-
|
|
98
|
-
return typeName.startsWith('NotificationListener<') ||
|
|
99
|
-
typeName.startsWith('Listener<') ||
|
|
100
|
-
typeName.startsWith('Builder<');
|
|
101
|
-
}
|
|
127
|
+
static bool _shouldClimbPast(String typeName) =>
|
|
128
|
+
_climbPastSet.contains(_baseName(typeName));
|
|
102
129
|
|
|
103
130
|
// Used by ancestor-list builders (kept as-is — affects displayed tree only).
|
|
104
131
|
static bool _isGenericUi(String typeName) => _shouldClimbPast(typeName);
|
|
@@ -125,11 +152,16 @@ class DevInspectorService {
|
|
|
125
152
|
'_PrimaryScrollControllerScope',
|
|
126
153
|
'_TapRegionRegistry', '_TapRegionSurface',
|
|
127
154
|
'_KeyedSubtree',
|
|
155
|
+
// Semantics / pointer / scroll internals that wrap every tappable widget.
|
|
156
|
+
// Stopping here yields a useless `_GestureSemantics`/`_ScrollableScope`
|
|
157
|
+
// instead of the real widget.
|
|
158
|
+
'_GestureSemantics', '_InkResponseStateWidget', '_ScrollableScope',
|
|
159
|
+
'_RenderScrollSemantics',
|
|
128
160
|
};
|
|
129
161
|
|
|
130
162
|
static bool _shouldSkip(String typeName) {
|
|
131
|
-
|
|
132
|
-
return _privateFrameworkSet.contains(
|
|
163
|
+
final base = _baseName(typeName);
|
|
164
|
+
return _skipSet.contains(base) || _privateFrameworkSet.contains(base);
|
|
133
165
|
}
|
|
134
166
|
|
|
135
167
|
static List<String> _extractProperties(Element element) {
|
|
@@ -182,18 +214,26 @@ class DevInspectorService {
|
|
|
182
214
|
return routeName;
|
|
183
215
|
}
|
|
184
216
|
|
|
185
|
-
|
|
217
|
+
// Scan innermost-first so the most specific screen wins (e.g. the active
|
|
218
|
+
// `AdminUsersTab` rather than the surrounding `AdminShell`).
|
|
219
|
+
final privateAwareAncestors = _extractAncestors(
|
|
220
|
+
element,
|
|
221
|
+
includePrivate: true,
|
|
222
|
+
);
|
|
186
223
|
final regex = RegExp('^_?[A-Za-z0-9]+$_screenRegex');
|
|
187
|
-
for (final type in privateAwareAncestors
|
|
224
|
+
for (final type in privateAwareAncestors) {
|
|
188
225
|
if (regex.hasMatch(type)) return _sanitizeType(type);
|
|
189
226
|
}
|
|
190
|
-
for (final type in ancestors
|
|
191
|
-
if (
|
|
227
|
+
for (final type in ancestors) {
|
|
228
|
+
if (regex.hasMatch(type)) return _sanitizeType(type);
|
|
192
229
|
}
|
|
193
230
|
return null;
|
|
194
231
|
}
|
|
195
232
|
|
|
196
|
-
static String? _extractSemanticWidget(
|
|
233
|
+
static String? _extractSemanticWidget(
|
|
234
|
+
String widgetType,
|
|
235
|
+
List<String> ancestors,
|
|
236
|
+
) {
|
|
197
237
|
final chain = <String>[widgetType, ...ancestors];
|
|
198
238
|
final featureRegex = RegExp(
|
|
199
239
|
r'(Page|Screen|Card|Tile|Item|Section|Panel|Dialog|Modal|Header|Footer|Widget|Component)$',
|
|
@@ -203,7 +243,7 @@ class DevInspectorService {
|
|
|
203
243
|
for (final type in chain) {
|
|
204
244
|
if (_shouldSkip(type)) continue;
|
|
205
245
|
if (_shouldClimbPast(type)) continue;
|
|
206
|
-
if (featureRegex.hasMatch(type)) return type;
|
|
246
|
+
if (featureRegex.hasMatch(_baseName(type))) return type;
|
|
207
247
|
}
|
|
208
248
|
|
|
209
249
|
// 2) Fallback to first non-generic non-skipped widget.
|