kasy-cli 1.38.0 → 1.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/lib/scaffold/CHANGELOG.json +14 -0
  2. package/lib/scaffold/backends/api/patch/README.md +15 -0
  3. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
  4. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
  5. package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
  6. package/lib/scaffold/backends/patch-base-hashes.json +6 -6
  7. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  8. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
  9. package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
  10. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
  11. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  12. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
  13. package/lib/scaffold/shared/generator-utils.js +12 -6
  14. package/package.json +1 -1
  15. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  16. package/templates/firebase/DESIGN_SYSTEM.md +22 -8
  17. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  18. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  19. package/templates/firebase/assets/icons/facebook.svg +49 -0
  20. package/templates/firebase/assets/icons/google.svg +1 -0
  21. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  22. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  23. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  24. package/templates/firebase/lib/components/components.dart +1 -1
  25. package/templates/firebase/lib/components/kasy_app_bar.dart +314 -14
  26. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  27. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  28. package/templates/firebase/lib/components/kasy_sidebar.dart +18 -6
  29. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  30. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  31. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +27 -18
  32. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -10
  33. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  34. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  35. package/templates/firebase/lib/core/data/api/user_api.dart +11 -0
  36. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  37. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +18 -35
  38. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  39. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  40. package/templates/firebase/lib/core/states/user_state_notifier.dart +28 -1
  41. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  42. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  43. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +51 -19
  44. package/templates/firebase/lib/core/web_viewport_scale.dart +64 -35
  45. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  46. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  47. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  48. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  49. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  50. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  51. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  52. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  53. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  54. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  55. package/templates/firebase/lib/features/home/home_components_page.dart +253 -125
  56. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +231 -57
  57. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  58. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  59. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  60. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +111 -57
  61. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  62. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -4
  63. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  64. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  65. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  66. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +2 -2
  67. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  68. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  69. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  70. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  71. package/templates/firebase/lib/features/settings/settings_page.dart +53 -32
  72. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +895 -111
  73. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  74. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  75. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +171 -41
  76. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +1 -1
  77. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  78. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +48 -47
  79. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  80. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  81. package/templates/firebase/lib/i18n/en.i18n.json +749 -712
  82. package/templates/firebase/lib/i18n/es.i18n.json +749 -712
  83. package/templates/firebase/lib/i18n/pt.i18n.json +749 -712
  84. package/templates/firebase/lib/main.dart +20 -7
  85. package/templates/firebase/lib/router.dart +32 -26
  86. package/templates/firebase/pubspec.yaml +2 -1
  87. package/templates/firebase/test/admin_shell_chrome_test.dart +11 -5
  88. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  89. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  90. package/templates/firebase/tool/design_check.dart +9 -0
  91. package/templates/firebase/assets/icons/apple.png +0 -0
  92. package/templates/firebase/assets/icons/facebook.png +0 -0
  93. package/templates/firebase/assets/icons/google.png +0 -0
  94. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  95. package/templates/firebase/lib/components/kasy_web_header.dart +0 -218
  96. package/templates/firebase/lib/core/chrome/web_header_scope.dart +0 -20
  97. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  98. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  99. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  100. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  101. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -179
  102. 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 = 41;
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
- const double singleLineTextHeight = 19;
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?.copyWith(
491
- color: fieldTextColor,
492
- fontSize: 15,
493
- ) ??
494
- TextStyle(fontSize: 15, color: fieldTextColor);
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
- builder: (context, ref, _) {
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, and scopes scroll tracking to the
131
- // mobile shell only (detail screens push on the root navigator, so
132
- // their scrolls never reach this listener).
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 NotificationListener<ScrollUpdateNotification>(
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/kasy_web_header.dart';
3
- import 'package:kasy_kit/core/chrome/web_header_scope.dart';
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 [KasyWebHeader] at the top of
18
- /// the content area, above the routed page. On phone/tablet it is transparent
19
- /// (returns the page untouched) — there the page keeps its own [KasyAppBar].
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.
@@ -65,10 +67,10 @@ class _WebContentWrapperState extends State<WebContentWrapper> {
65
67
  // header (2) → content (3). Each block is its own FocusTraversalGroup so its
66
68
  // internal order stays natural (e.g. header: search → theme → … → create).
67
69
  //
68
- // KasyWebHeaderScope marks this subtree as "has a web header above", so the
69
- // page-level KasyAppBar hides on desktop here (the header owns the chrome).
70
- // Outside this scope (full-screen pushed routes) the app bar stays visible.
71
- return KasyWebHeaderScope(
70
+ // KasyAppBarScope marks this subtree as "has an application bar above", so the
71
+ // page-level KasyAppBar hides on desktop here (the application bar owns the
72
+ // chrome). Outside this scope (full-screen pushed routes) the bar stays visible.
73
+ return KasyAppBarScope(
72
74
  child: ColoredBox(
73
75
  color: context.colors.background,
74
76
  child: Column(
@@ -79,7 +81,7 @@ class _WebContentWrapperState extends State<WebContentWrapper> {
79
81
  FocusTraversalOrder(
80
82
  order: const NumericFocusOrder(2),
81
83
  child: FocusTraversalGroup(
82
- child: KasyWebHeader(
84
+ child: KasyAppBar.application(
83
85
  onToggleTheme: () => ThemeProvider.of(context).toggle(),
84
86
  notifications: const WebNotificationsBell(),
85
87
  onCreate: () {},
@@ -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 = false,
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
- if (_climbPastSet.contains(typeName)) return true;
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
- if (_skipSet.contains(typeName)) return true;
132
- return _privateFrameworkSet.contains(typeName);
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
- final privateAwareAncestors = _extractAncestors(element, includePrivate: true);
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.reversed) {
224
+ for (final type in privateAwareAncestors) {
188
225
  if (regex.hasMatch(type)) return _sanitizeType(type);
189
226
  }
190
- for (final type in ancestors.reversed) {
191
- if (RegExp(_screenRegex).hasMatch(type)) return _sanitizeType(type);
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(String widgetType, List<String> ancestors) {
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.