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.
- package/lib/scaffold/CHANGELOG.json +14 -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/DESIGN_SYSTEM.md +22 -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 +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +314 -14
- 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 +12 -10
- 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 +18 -35
- 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 +64 -35
- 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 +231 -57
- 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_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 +749 -712
- package/templates/firebase/lib/i18n/es.i18n.json +749 -712
- package/templates/firebase/lib/i18n/pt.i18n.json +749 -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/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
|
@@ -61,10 +61,14 @@ export const onNotificationCreated = onDocumentCreated(
|
|
|
61
61
|
? { imageUrl: notificationEntity.image_url }
|
|
62
62
|
: undefined,
|
|
63
63
|
};
|
|
64
|
-
const
|
|
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
|
|
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.
|
|
39
|
-
/// desktop
|
|
40
|
-
/// compact height across every viewport where the
|
|
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
|
|
61
|
-
// no overlap there. Outside it (a full-screen pushed route) the bar
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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,
|