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
@@ -61,10 +61,14 @@ export const onNotificationCreated = onDocumentCreated(
61
61
  ? { imageUrl: notificationEntity.image_url }
62
62
  : undefined,
63
63
  };
64
- const userDevices = await userDevicesRepository.getDevices([userId]);
64
+ const allDevices = await userDevicesRepository.getDevices([userId]);
65
+ // Skip installs without a push token (notifications not enabled yet): no
66
+ // point sending, and — crucially — an empty token fails as "invalid" which
67
+ // would delete the install in the cleanup below.
68
+ const userDevices = allDevices.filter((userDevice) => !!userDevice.token);
65
69
  const tokens = userDevices.map((userDevice) => userDevice.token);
66
70
  if (tokens.length === 0) {
67
- logger.info(`No device found for user ${userId}`);
71
+ logger.info(`No device with a push token for user ${userId}`);
68
72
  return;
69
73
  }
70
74
  const notificationApi = NotificationsApi.create();
@@ -19,6 +19,7 @@ export 'kasy_checkbox.dart';
19
19
  export 'kasy_chip.dart';
20
20
  export 'kasy_date_picker.dart';
21
21
  export 'kasy_dialog.dart';
22
+ export 'kasy_drop_down.dart';
22
23
  export 'kasy_image_viewer.dart';
23
24
  export 'kasy_otp_verification_bottom_sheet.dart';
24
25
  export 'kasy_screen.dart';
@@ -31,4 +32,3 @@ export 'kasy_text_area.dart';
31
32
  export 'kasy_text_field.dart';
32
33
  export 'kasy_text_field_otp.dart';
33
34
  export 'kasy_toast.dart';
34
- export 'kasy_web_header.dart';
@@ -19,6 +19,11 @@
19
19
  /// [KasyAppBarStyle.subpageSimple] (back only), [KasyAppBarStyle.subpageActions]
20
20
  /// (custom [trailing]).
21
21
  ///
22
+ /// **Desktop:** [KasyAppBar.application] renders the application chrome (search,
23
+ /// quick-create, notifications, profile) for viewports ≥ 1024px — the responsive
24
+ /// other half of the same component (formerly a separate web header). The shell
25
+ /// places it above content; the page bar then hides on desktop.
26
+ ///
22
27
  /// Barrel: [components.dart].
23
28
 
24
29
  library;
@@ -26,8 +31,13 @@ library;
26
31
  import 'package:flutter/foundation.dart' show kIsWeb;
27
32
  import 'package:flutter/material.dart';
28
33
  import 'package:flutter/services.dart' show SystemUiOverlayStyle;
34
+ import 'package:kasy_kit/components/kasy_avatar.dart';
35
+ import 'package:kasy_kit/components/kasy_avatar_presets.dart';
36
+ import 'package:kasy_kit/components/kasy_button.dart';
37
+ import 'package:kasy_kit/components/kasy_text_field.dart';
38
+ import 'package:kasy_kit/core/chrome/app_bar_config.dart';
39
+ import 'package:kasy_kit/core/chrome/app_bar_scope.dart';
29
40
  import 'package:kasy_kit/core/chrome/chrome_visibility.dart';
30
- import 'package:kasy_kit/core/chrome/web_header_scope.dart';
31
41
  import 'package:kasy_kit/core/theme/theme.dart';
32
42
  import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
33
43
  import 'package:kasy_kit/core/widgets/responsive_layout.dart';
@@ -35,14 +45,22 @@ import 'package:kasy_kit/core/widgets/responsive_layout.dart';
35
45
  /// Inner toolbar band height (orbit hit targets, title baseline).
36
46
  const double kasyAppBarToolbarRowHeight = 44;
37
47
 
38
- /// Effective toolbar band height. [KasyAppBar] serves phone and tablet only — on
39
- /// desktop the richer web header takes over — so the band keeps a single
40
- /// compact height across every viewport where the app bar appears.
48
+ /// Effective toolbar band height. The page chrome serves phone and tablet — on
49
+ /// desktop [KasyAppBar.application] takes over — so the band keeps a single
50
+ /// compact height across every viewport where the page bar appears.
41
51
  double kasyAppBarToolbarRowHeightOf(BuildContext context) =>
42
52
  kasyAppBarToolbarRowHeight;
43
53
 
44
54
  const double kasyAppBarTitleFontScale = 0.92;
45
55
 
56
+ /// Height of the desktop application bar band ([KasyAppBar.application]): 36px
57
+ /// content + 16px top/bottom. minHeight, not fixed, so the search field never
58
+ /// overflows the row.
59
+ const double kasyAppBarApplicationHeight = 68;
60
+
61
+ /// Fixed width of the search field in the desktop application bar.
62
+ const double kasyAppBarApplicationSearchWidth = 220;
63
+
46
64
  /// Tight vertical padding inside the frost, above/below [kasyAppBarToolbarRowHeight].
47
65
  const double kasyAppBarChromePaddingTop = KasySpacing.xs;
48
66
  const double kasyAppBarChromePaddingBottom = KasySpacing.sm;
@@ -57,11 +75,11 @@ enum KasyAppBarScrollTreatment { overlay, intrinsicScroll }
57
75
 
58
76
  /// Vertical inset for scroll/viewport when using [KasyAppBarScrollTreatment.overlay].
59
77
  double kasyAppBarBodyTopOverlap(BuildContext context) {
60
- // On desktop the app bar hides only inside the web-header scope (the shell), so
61
- // no overlap there. Outside it (a full-screen pushed route) the bar is visible,
62
- // so reserve its height like on phone/tablet.
78
+ // On desktop the page bar hides only inside the application-bar scope (the
79
+ // shell), so no overlap there. Outside it (a full-screen pushed route) the bar
80
+ // is visible, so reserve its height like on phone/tablet.
63
81
  if (MediaQuery.sizeOf(context).width >= DeviceType.large.breakpoint &&
64
- KasyWebHeaderScope.of(context)) {
82
+ KasyAppBarScope.of(context)) {
65
83
  return 0;
66
84
  }
67
85
  return MediaQuery.paddingOf(context).top +
@@ -259,6 +277,10 @@ enum KasyAppBarStyle {
259
277
  rootTab,
260
278
  }
261
279
 
280
+ /// Internal: which chrome a [KasyAppBar] renders — the phone/tablet page bar or
281
+ /// the desktop application bar ([KasyAppBar.application]).
282
+ enum _KasyAppBarVariant { page, application }
283
+
262
284
  /// Implements the frosted toolbar; usage patterns described in this file header.
263
285
  class KasyAppBar extends StatelessWidget {
264
286
  final String title;
@@ -297,6 +319,65 @@ class KasyAppBar extends StatelessWidget {
297
319
  /// appears where the bar was.
298
320
  final bool hideOnScroll;
299
321
 
322
+ // --- Application chrome (desktop, via [KasyAppBar.application]) ---
323
+ // These carry the desktop header (search, quick-create, notifications,
324
+ // profile). They are inert in the default (page) constructor.
325
+
326
+ /// Controller for the search field. Optional — omit for a display-only header.
327
+ final TextEditingController? searchController;
328
+
329
+ /// Placeholder shown in the search field.
330
+ final String searchHint;
331
+
332
+ /// Called as the user types in the search field.
333
+ final ValueChanged<String>? onSearchChanged;
334
+
335
+ /// Called when the search field is submitted (Enter).
336
+ final ValueChanged<String>? onSearchSubmitted;
337
+
338
+ /// Notifications (bell) action. Ignored when [notifications] is provided.
339
+ final VoidCallback? onNotifications;
340
+
341
+ /// Shows the unread dot on the bell. Ignored when [notifications] is provided.
342
+ final bool showNotificationBadge;
343
+
344
+ /// Custom notifications control — replaces the built-in bell with a data-aware
345
+ /// widget so the bar itself stays presentational.
346
+ final Widget? notifications;
347
+
348
+ /// Primary quick-create action. When null the button is disabled.
349
+ final VoidCallback? onCreate;
350
+
351
+ /// Label for the create button.
352
+ final String createLabel;
353
+
354
+ /// Gradient for the profile avatar fallback (when [avatar] is null).
355
+ final KasyAvatarGradientData avatarGradient;
356
+
357
+ /// Custom avatar widget (e.g. the signed-in user's photo). When null and
358
+ /// [showAvatar] is true, a gradient-fill avatar is shown.
359
+ final Widget? avatar;
360
+
361
+ /// Whether the profile avatar is shown (false when the sidebar owns it).
362
+ final bool showAvatar;
363
+
364
+ /// Profile avatar tap (open menu / profile).
365
+ final VoidCallback? onAvatarTap;
366
+
367
+ /// Theme toggle for the application bar (sun/moon ghost button before the bell).
368
+ final VoidCallback? onToggleTheme;
369
+
370
+ /// Whether the search field is shown (application bar).
371
+ final bool showSearch;
372
+
373
+ /// Whether the notifications control is shown (application bar).
374
+ final bool showNotifications;
375
+
376
+ /// Whether the quick-create button is shown (application bar).
377
+ final bool showCreate;
378
+
379
+ final _KasyAppBarVariant _variant;
380
+
300
381
  const KasyAppBar({
301
382
  super.key,
302
383
  required this.title,
@@ -309,16 +390,107 @@ class KasyAppBar extends StatelessWidget {
309
390
  this.toolbarHeight,
310
391
  this.topInset,
311
392
  this.hideOnScroll = false,
312
- });
393
+ }) : _variant = _KasyAppBarVariant.page,
394
+ searchController = null,
395
+ searchHint = 'Search...',
396
+ onSearchChanged = null,
397
+ onSearchSubmitted = null,
398
+ onNotifications = null,
399
+ showNotificationBadge = false,
400
+ notifications = null,
401
+ onCreate = null,
402
+ createLabel = 'Create',
403
+ avatarGradient = KasyAvatarGradients.orange,
404
+ avatar = null,
405
+ showAvatar = true,
406
+ onAvatarTap = null,
407
+ onToggleTheme = null,
408
+ showSearch = true,
409
+ showNotifications = true,
410
+ showCreate = true;
411
+
412
+ /// Desktop application chrome (viewport ≥ 1024px): global search, quick-create,
413
+ /// notifications and profile, sitting to the right of the sidebar. The
414
+ /// responsive counterpart of the phone/tablet page chrome (the default
415
+ /// constructor) — same component, the other half. The shell places this above
416
+ /// content inside a [KasyAppBarScope]; the page bar then hides on desktop.
417
+ const KasyAppBar.application({
418
+ super.key,
419
+ this.searchController,
420
+ this.searchHint = 'Search...',
421
+ this.onSearchChanged,
422
+ this.onSearchSubmitted,
423
+ this.onNotifications,
424
+ this.showNotificationBadge = false,
425
+ this.notifications,
426
+ this.onCreate,
427
+ this.createLabel = 'Create',
428
+ this.avatarGradient = KasyAvatarGradients.orange,
429
+ this.avatar,
430
+ this.showAvatar = true,
431
+ this.onAvatarTap,
432
+ this.onToggleTheme,
433
+ this.showSearch = true,
434
+ this.showNotifications = true,
435
+ this.showCreate = true,
436
+ }) : _variant = _KasyAppBarVariant.application,
437
+ title = '',
438
+ style = KasyAppBarStyle.rootTab,
439
+ onBack = null,
440
+ trailing = null,
441
+ leading = null,
442
+ useSafeArea = true,
443
+ onThemeToggle = null,
444
+ toolbarHeight = null,
445
+ topInset = null,
446
+ hideOnScroll = false;
447
+
448
+ /// Builds the desktop application bar from a [KasyAppBarConfig] — the bridge
449
+ /// the shell uses to render whatever a screen published. Theme/search/create/
450
+ /// notifications appear per the config's `showX` flags.
451
+ factory KasyAppBar.fromConfig(KasyAppBarConfig config, {Key? key}) {
452
+ return KasyAppBar.application(
453
+ key: key,
454
+ showSearch: config.showSearch,
455
+ searchController: config.searchController,
456
+ searchHint: config.searchHint,
457
+ onSearchChanged: config.onSearchChanged,
458
+ onSearchSubmitted: config.onSearchSubmitted,
459
+ onToggleTheme: config.showThemeToggle ? config.onToggleTheme : null,
460
+ showNotifications: config.showNotifications,
461
+ notifications: config.notifications,
462
+ onNotifications: config.onNotifications,
463
+ showNotificationBadge: config.showNotificationBadge,
464
+ showCreate: config.showCreate,
465
+ createLabel: config.createLabel,
466
+ onCreate: config.onCreate,
467
+ showAvatar: config.showAvatar,
468
+ avatar: config.avatar,
469
+ avatarGradient: config.avatarGradient ?? KasyAvatarGradients.orange,
470
+ onAvatarTap: config.onAvatarTap,
471
+ );
472
+ }
473
+
474
+ /// True for the desktop application bar ([KasyAppBar.application]); false for
475
+ /// the phone/tablet page bar. Lets tests assert which chrome is mounted without
476
+ /// reaching for a private type.
477
+ bool get isApplication => _variant == _KasyAppBarVariant.application;
313
478
 
314
479
  @override
315
480
  Widget build(BuildContext context) {
316
- // On desktop the web header owns the top chrome, so the page app bar hides —
317
- // but ONLY when there actually is a web header above (i.e. inside the shell's
318
- // KasyWebHeaderScope). A full-screen route pushed over the shell has no web
319
- // header, so the bar stays visible and its back button is never lost.
481
+ return switch (_variant) {
482
+ _KasyAppBarVariant.application => _buildApplicationChrome(context),
483
+ _KasyAppBarVariant.page => _buildPageChrome(context),
484
+ };
485
+ }
486
+
487
+ Widget _buildPageChrome(BuildContext context) {
488
+ // On desktop the application bar owns the top chrome, so the page bar hides —
489
+ // but ONLY when there actually is one above (i.e. inside the shell's
490
+ // KasyAppBarScope). A full-screen route pushed over the shell has none, so
491
+ // the bar stays visible and its back button is never lost.
320
492
  if (MediaQuery.sizeOf(context).width >= DeviceType.large.breakpoint &&
321
- KasyWebHeaderScope.of(context)) {
493
+ KasyAppBarScope.of(context)) {
322
494
  return const SizedBox.shrink();
323
495
  }
324
496
  final Color orbFg = context.colors.onSurface;
@@ -482,6 +654,134 @@ class KasyAppBar extends StatelessWidget {
482
654
  );
483
655
  }
484
656
  }
657
+
658
+ /// Desktop application chrome: search + theme + notifications + create + avatar,
659
+ /// matching [KasySidebar]'s surface fill and hairline border so the two read as
660
+ /// one continuous chrome across the top of the shell.
661
+ Widget _buildApplicationChrome(BuildContext context) {
662
+ final KasyColors c = context.colors;
663
+
664
+ // Trailing controls, each added with a leading gap so spacing never doubles
665
+ // up when an element is hidden (e.g. no create button → no stray gap).
666
+ final List<Widget> trailing = <Widget>[];
667
+ void addTrailing(Widget w) {
668
+ if (trailing.isNotEmpty) {
669
+ trailing.add(const SizedBox(width: KasySpacing.md));
670
+ }
671
+ trailing.add(w);
672
+ }
673
+
674
+ if (onToggleTheme != null) addTrailing(_buildApplicationThemeToggle(context));
675
+ if (showNotifications) {
676
+ addTrailing(notifications ?? _buildApplicationNotifications(context));
677
+ }
678
+ if (showCreate) {
679
+ addTrailing(KasyButton(
680
+ label: createLabel,
681
+ variant: KasyButtonVariant.neutral,
682
+ size: KasyButtonSize.small,
683
+ onPressed: onCreate,
684
+ ));
685
+ }
686
+ if (showAvatar) {
687
+ addTrailing(avatar ??
688
+ KasyAvatar.gradientFill(
689
+ size: KasyAvatarSize.small,
690
+ diameter: 36,
691
+ gradient: avatarGradient,
692
+ showShadow: false,
693
+ onTap: onAvatarTap,
694
+ ));
695
+ }
696
+
697
+ return DecoratedBox(
698
+ decoration: BoxDecoration(
699
+ color: c.surface,
700
+ border: Border(
701
+ bottom: BorderSide(color: c.border, width: 0.5),
702
+ ),
703
+ ),
704
+ child: ConstrainedBox(
705
+ constraints: const BoxConstraints(minHeight: kasyAppBarApplicationHeight),
706
+ child: Padding(
707
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
708
+ child: Row(
709
+ children: [
710
+ if (showSearch)
711
+ SizedBox(
712
+ width: kasyAppBarApplicationSearchWidth,
713
+ child: _buildApplicationSearch(context),
714
+ ),
715
+ const Spacer(),
716
+ ...trailing,
717
+ ],
718
+ ),
719
+ ),
720
+ ),
721
+ );
722
+ }
723
+
724
+ Widget _buildApplicationSearch(BuildContext context) {
725
+ return KasyTextField(
726
+ variant: KasyTextFieldVariant.flat,
727
+ controller: searchController,
728
+ hint: searchHint,
729
+ onChanged: onSearchChanged,
730
+ onSubmitted: onSearchSubmitted,
731
+ prefix: Icon(
732
+ KasyIcons.search,
733
+ size: KasyIconSize.md,
734
+ color: context.colors.muted,
735
+ ),
736
+ contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
737
+ );
738
+ }
739
+
740
+ Widget _buildApplicationThemeToggle(BuildContext context) {
741
+ final bool isDark = Theme.of(context).brightness == Brightness.dark;
742
+ return KasyButton.iconOnly(
743
+ icon: isDark ? KasyIcons.lightMode : KasyIcons.darkMode,
744
+ variant: KasyButtonVariant.ghost,
745
+ size: KasyButtonSize.small,
746
+ iconOnlyLayoutExtent: 36,
747
+ iconGlyphSize: KasyIconSize.md,
748
+ onPressed: onToggleTheme,
749
+ semanticLabel: isDark ? 'Light mode' : 'Dark mode',
750
+ );
751
+ }
752
+
753
+ Widget _buildApplicationNotifications(BuildContext context) {
754
+ final KasyColors c = context.colors;
755
+ final Widget bell = KasyButton.iconOnly(
756
+ icon: KasyIcons.notification,
757
+ variant: KasyButtonVariant.ghost,
758
+ size: KasyButtonSize.small,
759
+ iconOnlyLayoutExtent: 36,
760
+ iconGlyphSize: KasyIconSize.md,
761
+ onPressed: onNotifications,
762
+ semanticLabel: 'Notifications',
763
+ );
764
+ if (!showNotificationBadge) return bell;
765
+ return Stack(
766
+ clipBehavior: Clip.none,
767
+ children: [
768
+ bell,
769
+ Positioned(
770
+ top: 8,
771
+ right: 8,
772
+ child: Container(
773
+ width: 8,
774
+ height: 8,
775
+ decoration: BoxDecoration(
776
+ color: c.error,
777
+ shape: BoxShape.circle,
778
+ border: Border.all(color: c.background, width: 1.5),
779
+ ),
780
+ ),
781
+ ),
782
+ ],
783
+ );
784
+ }
485
785
  }
486
786
 
487
787
  /// Full-screen scaffold: frosted [KasyAppBar] pinned over scroll content.
@@ -119,6 +119,10 @@ class KasyCard extends StatelessWidget {
119
119
  onPressed: onTap!,
120
120
  semanticLabel: semanticLabel ?? 'Card',
121
121
  clipBorderRadius: resolvedRadius,
122
+ // Subtle overlay → a hover highlight on web (pointer) and a soft press
123
+ // flash everywhere. A tappable card is an actionable control, so it
124
+ // should feel interactive on hover like any web control.
125
+ pressOverlayColor: c.onSurface.withValues(alpha: dark ? 0.06 : 0.04),
122
126
  // A tappable card is an actionable control, so make it a keyboard
123
127
  // tab-stop with the standard focus ring (matches buttons/links).
124
128
  focusable: true,