kasy-cli 1.37.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/scaffold/CHANGELOG.json +9 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
- package/lib/scaffold/backends/patch-base-hashes.json +4 -4
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
- package/templates/firebase/AGENTS.md +20 -10
- package/templates/firebase/DESIGN_SYSTEM.md +13 -0
- package/templates/firebase/README.en.md +1 -1
- package/templates/firebase/README.es.md +1 -1
- package/templates/firebase/README.md +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +57 -16
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
- package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
- package/templates/firebase/lib/components/kasy_sidebar.dart +397 -28
- package/templates/firebase/lib/components/kasy_web_header.dart +11 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +2 -213
- package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -4
- package/templates/firebase/lib/core/chrome/web_header_scope.dart +20 -0
- package/templates/firebase/lib/core/data/api/user_api.dart +4 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
- package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +41 -0
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
- package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +498 -466
- package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -7
- package/templates/firebase/lib/features/home/home_components_page.dart +16 -6
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +7 -0
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +23 -13
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
- package/templates/firebase/lib/features/settings/settings_page.dart +51 -38
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +549 -376
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +266 -141
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +23 -30
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +30 -49
- package/templates/firebase/lib/i18n/en.i18n.json +23 -9
- package/templates/firebase/lib/i18n/es.i18n.json +23 -9
- package/templates/firebase/lib/i18n/pt.i18n.json +23 -9
- package/templates/firebase/lib/router.dart +43 -25
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/admin_shell_chrome_test.dart +104 -0
- package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
|
@@ -16,7 +16,10 @@ import 'package:kasy_kit/i18n/translations.g.dart';
|
|
|
16
16
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
17
|
|
|
18
18
|
// Figma sidebar is 223 wide; we run a touch wider for breathing room.
|
|
19
|
-
|
|
19
|
+
/// Open (expanded) width of the rail. Exposed so hosts that present it as a
|
|
20
|
+
/// drawer can size the drawer to match (e.g. the admin console on mobile).
|
|
21
|
+
const double kasySidebarWidth = 248.0;
|
|
22
|
+
const double _kWidthOpen = kasySidebarWidth;
|
|
20
23
|
const double _kWidthCollapsed = 64.0;
|
|
21
24
|
const double _kPadH = 16.0; // px-4
|
|
22
25
|
// Tighter horizontal gutter for the narrow collapsed rail, so a 64px rail keeps
|
|
@@ -208,11 +211,68 @@ enum KasySidebarCollapseMode {
|
|
|
208
211
|
collapsed,
|
|
209
212
|
}
|
|
210
213
|
|
|
214
|
+
/// One row in the sidebar's generic ([KasySidebar.items]) mode: an icon, a
|
|
215
|
+
/// label and a tap callback. Reuses the exact same row recipe as the app's real
|
|
216
|
+
/// navigation, so a host (e.g. the admin console) can drive the SAME sidebar
|
|
217
|
+
/// with its own screens instead of the Bart-connected app tabs.
|
|
218
|
+
///
|
|
219
|
+
/// When [children] is non-empty the row becomes an expandable group (the same
|
|
220
|
+
/// dropdown recipe as the connected "Income" submenu): tapping the row toggles
|
|
221
|
+
/// the sub-items, and [onTap] is ignored. A leaf row uses [onTap] to navigate
|
|
222
|
+
/// and [selected] to show the active pill.
|
|
223
|
+
class KasySidebarItem {
|
|
224
|
+
final IconData icon;
|
|
225
|
+
final String label;
|
|
226
|
+
|
|
227
|
+
/// Leaf tap (navigate). Ignored when [children] is non-empty (a group only
|
|
228
|
+
/// expands/collapses on tap).
|
|
229
|
+
final VoidCallback? onTap;
|
|
230
|
+
|
|
231
|
+
/// Highlights this leaf as the active screen.
|
|
232
|
+
final bool selected;
|
|
233
|
+
|
|
234
|
+
/// Sub-rows. When non-empty this item renders as an expandable group instead
|
|
235
|
+
/// of a leaf.
|
|
236
|
+
final List<KasySidebarSubItem> children;
|
|
237
|
+
|
|
238
|
+
const KasySidebarItem({
|
|
239
|
+
required this.icon,
|
|
240
|
+
required this.label,
|
|
241
|
+
this.onTap,
|
|
242
|
+
this.selected = false,
|
|
243
|
+
this.children = const [],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
bool get isGroup => children.isNotEmpty;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// A sub-row under an expandable [KasySidebarItem] group (text-only, like the
|
|
250
|
+
/// connected "Income" submenu). [selected] highlights the active screen.
|
|
251
|
+
class KasySidebarSubItem {
|
|
252
|
+
final String label;
|
|
253
|
+
final VoidCallback onTap;
|
|
254
|
+
final bool selected;
|
|
255
|
+
const KasySidebarSubItem({
|
|
256
|
+
required this.label,
|
|
257
|
+
required this.onTap,
|
|
258
|
+
this.selected = false,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
211
262
|
/// A SaaS-style sidebar modelled on the HeroUI Figma kit: brand logo + panel
|
|
212
263
|
/// toggle, a workspace selector, a segmented control, a navigable list with an
|
|
213
264
|
/// active pill, and a pinned ⌘K search row. Collapses to an icon rail (with
|
|
214
265
|
/// tooltips and a hover submenu popup) on narrow viewports or via the toggle.
|
|
215
266
|
///
|
|
267
|
+
/// Three modes, in priority order:
|
|
268
|
+
/// 1. **Generic** — pass [items] (+ [sectionLabel], [footerItems]) to drive the
|
|
269
|
+
/// rail with an arbitrary screen list (leaves and/or expandable groups). Used
|
|
270
|
+
/// by the admin console so it shares this exact component (logo, collapse,
|
|
271
|
+
/// tooltips, profile) instead of a bespoke copy.
|
|
272
|
+
/// 2. **Connected** — pass [routes]/[onTapItem]/[currentItem] for the real app
|
|
273
|
+
/// tabs (Bart navigation).
|
|
274
|
+
/// 3. **Showcase** — the HeroUI demo items (default).
|
|
275
|
+
///
|
|
216
276
|
/// Pass [onSettingsTap] to respond when the user taps Settings.
|
|
217
277
|
class KasySidebar extends StatefulWidget {
|
|
218
278
|
const KasySidebar({
|
|
@@ -232,8 +292,23 @@ class KasySidebar extends StatefulWidget {
|
|
|
232
292
|
this.profileGradient = KasyAvatarGradients.indigo,
|
|
233
293
|
this.onProfileTap,
|
|
234
294
|
this.notificationsUnread = 0,
|
|
295
|
+
this.items,
|
|
296
|
+
this.sectionLabel,
|
|
297
|
+
this.footerItems = const [],
|
|
235
298
|
});
|
|
236
299
|
|
|
300
|
+
/// Generic nav rows. When non-null the sidebar runs in generic mode (renders
|
|
301
|
+
/// these — leaves highlighted by their own [KasySidebarItem.selected], groups
|
|
302
|
+
/// expandable) instead of connected/showcase.
|
|
303
|
+
final List<KasySidebarItem>? items;
|
|
304
|
+
|
|
305
|
+
/// Optional uppercase section label shown above [items] (e.g. "ADMIN").
|
|
306
|
+
final String? sectionLabel;
|
|
307
|
+
|
|
308
|
+
/// Rows pinned at the bottom of [items] mode (above the profile block), like
|
|
309
|
+
/// the connected layout's Help/Logout — e.g. a "Back to app" action.
|
|
310
|
+
final List<KasySidebarItem> footerItems;
|
|
311
|
+
|
|
237
312
|
/// Unread notification count. When greater than zero, the Notifications nav
|
|
238
313
|
/// item shows an unread dot (mirrors the bottom-bar badge). Purely an unread
|
|
239
314
|
/// indicator — not tied to push (which is native-only).
|
|
@@ -254,7 +329,7 @@ class KasySidebar extends StatefulWidget {
|
|
|
254
329
|
/// gradient-fill avatar ([profileGradient]) is shown instead.
|
|
255
330
|
final Widget? profileAvatar;
|
|
256
331
|
|
|
257
|
-
/// Gradient used for the profile avatar when no [
|
|
332
|
+
/// Gradient used for the profile avatar when no [profileAvatar] is given.
|
|
258
333
|
final KasyAvatarGradientData profileGradient;
|
|
259
334
|
|
|
260
335
|
/// Tap on the profile block (open account menu / profile).
|
|
@@ -305,6 +380,11 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
305
380
|
|
|
306
381
|
bool _incomeExpanded = false;
|
|
307
382
|
|
|
383
|
+
// Expanded groups in generic [items] mode, keyed by the group's label (labels
|
|
384
|
+
// are unique per rail). A group is also shown expanded whenever one of its
|
|
385
|
+
// children is the active screen (so the open submenu always reflects the URL).
|
|
386
|
+
final Set<String> _expandedItemGroups = <String>{};
|
|
387
|
+
|
|
308
388
|
// Showcase state.
|
|
309
389
|
int _showcaseTab = 0; // 0 = Layers, 1 = Assets
|
|
310
390
|
late String _activeItemId;
|
|
@@ -399,29 +479,19 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
399
479
|
? Border(right: BorderSide(color: c.border, width: 0.5))
|
|
400
480
|
: Border(left: BorderSide(color: c.border, width: 0.5));
|
|
401
481
|
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
),
|
|
416
|
-
];
|
|
417
|
-
|
|
418
|
-
final Widget content = _connected
|
|
419
|
-
? ValueListenableBuilder<int>(
|
|
420
|
-
valueListenable: widget.currentItem!,
|
|
421
|
-
builder: (_, currentIndex, _) =>
|
|
422
|
-
_buildConnectedContent(context, c, currentIndex),
|
|
423
|
-
)
|
|
424
|
-
: _buildShowcaseContent(context, c);
|
|
482
|
+
// No drop shadow: the rail separates from the content with the crisp 0.5px
|
|
483
|
+
// edge hairline only (it also bled at the seam when the content area didn't
|
|
484
|
+
// overpaint it). The hairline aligns with the web header's bottom border, so
|
|
485
|
+
// the chrome reads as one clean line — no soft contour around the rail.
|
|
486
|
+
final Widget content = widget.items != null
|
|
487
|
+
? _buildItemsContent(context, c)
|
|
488
|
+
: _connected
|
|
489
|
+
? ValueListenableBuilder<int>(
|
|
490
|
+
valueListenable: widget.currentItem!,
|
|
491
|
+
builder: (_, currentIndex, _) =>
|
|
492
|
+
_buildConnectedContent(context, c, currentIndex),
|
|
493
|
+
)
|
|
494
|
+
: _buildShowcaseContent(context, c);
|
|
425
495
|
|
|
426
496
|
return Material(
|
|
427
497
|
type: MaterialType.transparency,
|
|
@@ -429,7 +499,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
429
499
|
duration: const Duration(milliseconds: 220),
|
|
430
500
|
curve: Curves.easeInOut,
|
|
431
501
|
width: _collapsed ? _kWidthCollapsed : _kWidthOpen,
|
|
432
|
-
decoration: BoxDecoration(color: c.bg
|
|
502
|
+
decoration: BoxDecoration(color: c.bg),
|
|
433
503
|
foregroundDecoration: BoxDecoration(border: edgeBorder),
|
|
434
504
|
clipBehavior: Clip.hardEdge,
|
|
435
505
|
child: content,
|
|
@@ -463,7 +533,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
463
533
|
if (!_collapsed) ...[
|
|
464
534
|
_buildWorkspaceSelector(c),
|
|
465
535
|
const SizedBox(height: _kHeaderGap),
|
|
466
|
-
_buildTabs(
|
|
536
|
+
_buildTabs(),
|
|
467
537
|
const SizedBox(height: _kNavGap),
|
|
468
538
|
],
|
|
469
539
|
for (final item in _kShowcaseItems)
|
|
@@ -503,6 +573,305 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
503
573
|
);
|
|
504
574
|
}
|
|
505
575
|
|
|
576
|
+
// ── Generic layout (host-provided items, e.g. admin console) ────────────────
|
|
577
|
+
|
|
578
|
+
/// Same chrome as the connected layout (logo band, dividers, profile block,
|
|
579
|
+
/// collapse + tooltips) but driven by [KasySidebar.items]/[footerItems], so a
|
|
580
|
+
/// host reuses this exact component with its own screens.
|
|
581
|
+
Widget _buildItemsContent(BuildContext context, _SidebarColors c) {
|
|
582
|
+
final List<KasySidebarItem> items = widget.items!;
|
|
583
|
+
return SizedBox.expand(
|
|
584
|
+
child: Column(
|
|
585
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
586
|
+
children: [
|
|
587
|
+
_buildTopBand(c),
|
|
588
|
+
_buildDivider(c),
|
|
589
|
+
Expanded(
|
|
590
|
+
child: Padding(
|
|
591
|
+
padding: EdgeInsets.symmetric(horizontal: _railPadH),
|
|
592
|
+
child: SingleChildScrollView(
|
|
593
|
+
child: Column(
|
|
594
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
595
|
+
children: [
|
|
596
|
+
// Top gap inside the scroll view so the list scrolls flush
|
|
597
|
+
// under the top divider (symmetric with the bottom).
|
|
598
|
+
const SizedBox(height: _kDividerGap),
|
|
599
|
+
if (!_collapsed && widget.sectionLabel != null) ...[
|
|
600
|
+
_buildSectionLabel(widget.sectionLabel!, c),
|
|
601
|
+
const SizedBox(height: _kItemGap),
|
|
602
|
+
],
|
|
603
|
+
for (final item in items)
|
|
604
|
+
if (item.isGroup)
|
|
605
|
+
_buildItemsGroup(context, item, c)
|
|
606
|
+
else
|
|
607
|
+
_buildItemRow(
|
|
608
|
+
c,
|
|
609
|
+
icon: item.icon,
|
|
610
|
+
label: item.label,
|
|
611
|
+
isActive: item.selected,
|
|
612
|
+
onTap: () => _selectItemsLeaf(item.onTap),
|
|
613
|
+
),
|
|
614
|
+
],
|
|
615
|
+
),
|
|
616
|
+
),
|
|
617
|
+
),
|
|
618
|
+
),
|
|
619
|
+
_buildDivider(c),
|
|
620
|
+
const SizedBox(height: _kFooterGap),
|
|
621
|
+
Padding(
|
|
622
|
+
padding: EdgeInsets.fromLTRB(_railPadH, 0, _railPadH, _kPadBottom),
|
|
623
|
+
child: Column(
|
|
624
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
625
|
+
children: [
|
|
626
|
+
for (final f in widget.footerItems)
|
|
627
|
+
_buildItemRow(
|
|
628
|
+
c,
|
|
629
|
+
icon: f.icon,
|
|
630
|
+
label: f.label,
|
|
631
|
+
isActive: false,
|
|
632
|
+
onTap: () => _selectItemsLeaf(f.onTap),
|
|
633
|
+
),
|
|
634
|
+
if (widget.showProfile) _buildProfile(c),
|
|
635
|
+
],
|
|
636
|
+
),
|
|
637
|
+
),
|
|
638
|
+
],
|
|
639
|
+
),
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ── Generic expandable group (items mode) ────────────────────────────────
|
|
644
|
+
|
|
645
|
+
/// Navigating to a top-level (or footer) row collapses any open submenu group
|
|
646
|
+
/// — matching the connected Income dropdown, which closes when you move to
|
|
647
|
+
/// another screen. A group that holds the active screen re-opens on its own
|
|
648
|
+
/// (via [KasySidebarSubItem.selected]), so this only closes a manually-opened
|
|
649
|
+
/// one you're navigating away from.
|
|
650
|
+
void _selectItemsLeaf(VoidCallback? onTap) {
|
|
651
|
+
if (_expandedItemGroups.isNotEmpty) {
|
|
652
|
+
setState(() => _expandedItemGroups.clear());
|
|
653
|
+
}
|
|
654
|
+
onTap?.call();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/// An expandable group row in [items] mode — the same dropdown recipe as the
|
|
658
|
+
/// connected "Income" submenu, but driven by [KasySidebarItem.children].
|
|
659
|
+
/// Shown expanded when the user toggled it open OR when one of its children
|
|
660
|
+
/// is the active screen (so the open submenu always reflects the URL).
|
|
661
|
+
Widget _buildItemsGroup(
|
|
662
|
+
BuildContext context,
|
|
663
|
+
KasySidebarItem item,
|
|
664
|
+
_SidebarColors c,
|
|
665
|
+
) {
|
|
666
|
+
final bool hasActiveChild = item.children.any((s) => s.selected);
|
|
667
|
+
final bool expanded =
|
|
668
|
+
_expandedItemGroups.contains(item.label) || hasActiveChild;
|
|
669
|
+
final Color iconColor = expanded ? c.textActive : c.textMuted;
|
|
670
|
+
|
|
671
|
+
// Collapsed icon rail: the children live in a hover popup (same as Income).
|
|
672
|
+
if (_collapsed) {
|
|
673
|
+
String activeLabel = '';
|
|
674
|
+
for (final s in item.children) {
|
|
675
|
+
if (s.selected) {
|
|
676
|
+
activeLabel = s.label;
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return Padding(
|
|
681
|
+
padding: const EdgeInsets.only(bottom: _kItemGap),
|
|
682
|
+
child: _ProHoverPopupIcon(
|
|
683
|
+
icon: item.icon,
|
|
684
|
+
iconBg: hasActiveChild ? c.activeBg : Colors.transparent,
|
|
685
|
+
iconColor: iconColor,
|
|
686
|
+
subItems: [for (final s in item.children) s.label],
|
|
687
|
+
activeSubItem: activeLabel,
|
|
688
|
+
colors: c,
|
|
689
|
+
anchoredLeft: widget.side == KasySidebarSide.left,
|
|
690
|
+
onSubItemTap: (label) {
|
|
691
|
+
for (final s in item.children) {
|
|
692
|
+
if (s.label == label) {
|
|
693
|
+
s.onTap();
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
),
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return Column(
|
|
703
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
704
|
+
children: [
|
|
705
|
+
KasyHover(
|
|
706
|
+
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
707
|
+
hoverColor: c.activeBg,
|
|
708
|
+
pressColor: c.textActive,
|
|
709
|
+
focusable: true,
|
|
710
|
+
focusGapColor: c.bg,
|
|
711
|
+
onTap: () => setState(() {
|
|
712
|
+
if (!_expandedItemGroups.remove(item.label)) {
|
|
713
|
+
_expandedItemGroups.add(item.label);
|
|
714
|
+
}
|
|
715
|
+
}),
|
|
716
|
+
child: Container(
|
|
717
|
+
constraints: const BoxConstraints(minHeight: _kItemMinH),
|
|
718
|
+
padding: const EdgeInsets.symmetric(
|
|
719
|
+
horizontal: _kItemHPad,
|
|
720
|
+
vertical: _kItemVPad,
|
|
721
|
+
),
|
|
722
|
+
decoration: BoxDecoration(
|
|
723
|
+
color: hasActiveChild ? c.activeBg : Colors.transparent,
|
|
724
|
+
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
725
|
+
),
|
|
726
|
+
child: Row(
|
|
727
|
+
children: [
|
|
728
|
+
Icon(item.icon, size: _kIconSize, color: iconColor),
|
|
729
|
+
const SizedBox(width: _kIconGap),
|
|
730
|
+
Expanded(
|
|
731
|
+
child: Text(
|
|
732
|
+
item.label,
|
|
733
|
+
maxLines: 1,
|
|
734
|
+
overflow: TextOverflow.ellipsis,
|
|
735
|
+
style: context.kasyTextTheme.rowTitle.copyWith(
|
|
736
|
+
color: c.textActive,
|
|
737
|
+
),
|
|
738
|
+
),
|
|
739
|
+
),
|
|
740
|
+
AnimatedRotation(
|
|
741
|
+
turns: expanded ? 0.5 : 0,
|
|
742
|
+
duration: const Duration(milliseconds: 200),
|
|
743
|
+
child: Icon(
|
|
744
|
+
KasyIcons.chevronDown,
|
|
745
|
+
size: _kIconSize,
|
|
746
|
+
color: iconColor,
|
|
747
|
+
),
|
|
748
|
+
),
|
|
749
|
+
],
|
|
750
|
+
),
|
|
751
|
+
),
|
|
752
|
+
),
|
|
753
|
+
AnimatedCrossFade(
|
|
754
|
+
duration: const Duration(milliseconds: 200),
|
|
755
|
+
crossFadeState:
|
|
756
|
+
expanded ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
|
757
|
+
firstChild: _buildItemsSubTree(item.children, c),
|
|
758
|
+
secondChild: const SizedBox.shrink(),
|
|
759
|
+
),
|
|
760
|
+
const SizedBox(height: _kItemGap),
|
|
761
|
+
],
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/// The connector tree under an expanded [items]-mode group (mirrors the
|
|
766
|
+
/// connected Income tree, but generic over [KasySidebarSubItem]).
|
|
767
|
+
Widget _buildItemsSubTree(
|
|
768
|
+
List<KasySidebarSubItem> subItems,
|
|
769
|
+
_SidebarColors c,
|
|
770
|
+
) {
|
|
771
|
+
final double lineH = _treeLineHeight(subItems.length);
|
|
772
|
+
return Padding(
|
|
773
|
+
padding: const EdgeInsets.only(left: _kSubIndent),
|
|
774
|
+
child: SizedBox(
|
|
775
|
+
width: 172,
|
|
776
|
+
child: Stack(
|
|
777
|
+
clipBehavior: Clip.none,
|
|
778
|
+
children: [
|
|
779
|
+
Positioned(
|
|
780
|
+
left: -_kTreeConnectorW,
|
|
781
|
+
top: 0,
|
|
782
|
+
child: Container(
|
|
783
|
+
width: 1.5,
|
|
784
|
+
height: lineH,
|
|
785
|
+
decoration: BoxDecoration(
|
|
786
|
+
color: c.divider,
|
|
787
|
+
borderRadius: BorderRadius.circular(2),
|
|
788
|
+
),
|
|
789
|
+
),
|
|
790
|
+
),
|
|
791
|
+
Column(
|
|
792
|
+
children: [
|
|
793
|
+
for (int i = 0; i < subItems.length; i++)
|
|
794
|
+
_buildItemsSubItem(subItems[i], i == subItems.length - 1, c),
|
|
795
|
+
],
|
|
796
|
+
),
|
|
797
|
+
],
|
|
798
|
+
),
|
|
799
|
+
),
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
Widget _buildItemsSubItem(
|
|
804
|
+
KasySidebarSubItem sub,
|
|
805
|
+
bool isLast,
|
|
806
|
+
_SidebarColors c,
|
|
807
|
+
) {
|
|
808
|
+
final bool isActive = sub.selected;
|
|
809
|
+
final Color textColor = isActive ? c.textActive : c.textMuted;
|
|
810
|
+
|
|
811
|
+
return Padding(
|
|
812
|
+
padding: EdgeInsets.only(bottom: isLast ? 0 : _kSubItemGap),
|
|
813
|
+
child: Stack(
|
|
814
|
+
clipBehavior: Clip.none,
|
|
815
|
+
children: [
|
|
816
|
+
Positioned(
|
|
817
|
+
left: -_kTreeConnectorW,
|
|
818
|
+
top: _kSubItemH / 2 - 4,
|
|
819
|
+
child: Container(
|
|
820
|
+
width: _kTreeConnectorW,
|
|
821
|
+
height: 8,
|
|
822
|
+
decoration: BoxDecoration(
|
|
823
|
+
border: Border(
|
|
824
|
+
left: BorderSide(color: c.divider, width: 1.5),
|
|
825
|
+
bottom: BorderSide(color: c.divider, width: 1.5),
|
|
826
|
+
),
|
|
827
|
+
borderRadius: const BorderRadius.only(
|
|
828
|
+
bottomLeft: Radius.circular(8),
|
|
829
|
+
),
|
|
830
|
+
),
|
|
831
|
+
),
|
|
832
|
+
),
|
|
833
|
+
KasyHover(
|
|
834
|
+
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
835
|
+
hoverColor: c.activeBg,
|
|
836
|
+
pressColor: c.textActive,
|
|
837
|
+
focusable: true,
|
|
838
|
+
focusGapColor: c.bg,
|
|
839
|
+
onTap: sub.onTap,
|
|
840
|
+
child: Container(
|
|
841
|
+
height: _kSubItemH,
|
|
842
|
+
padding: const EdgeInsets.symmetric(
|
|
843
|
+
horizontal: _kItemHPad,
|
|
844
|
+
vertical: 8,
|
|
845
|
+
),
|
|
846
|
+
// Active sub-item is shown by its LABEL only (bold + active color),
|
|
847
|
+
// never a filled pill — exactly like the connected Income submenu
|
|
848
|
+
// on Home. The pill is reserved for top-level screens.
|
|
849
|
+
decoration: BoxDecoration(
|
|
850
|
+
color: Colors.transparent,
|
|
851
|
+
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
852
|
+
),
|
|
853
|
+
child: Align(
|
|
854
|
+
alignment: Alignment.centerLeft,
|
|
855
|
+
child: Text(
|
|
856
|
+
sub.label,
|
|
857
|
+
maxLines: 1,
|
|
858
|
+
overflow: TextOverflow.ellipsis,
|
|
859
|
+
// Active sub-item: heavier weight (w700) for a clear "you are
|
|
860
|
+
// here", no pill / accent — emphasis comes from the bold name.
|
|
861
|
+
style: context.textTheme.labelMedium?.copyWith(
|
|
862
|
+
fontWeight: isActive ? FontWeight.w700 : FontWeight.w500,
|
|
863
|
+
color: textColor,
|
|
864
|
+
letterSpacing: -0.24,
|
|
865
|
+
),
|
|
866
|
+
),
|
|
867
|
+
),
|
|
868
|
+
),
|
|
869
|
+
),
|
|
870
|
+
],
|
|
871
|
+
),
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
506
875
|
// ── Connected layout (real navigation) ──────────────────────────────────────
|
|
507
876
|
|
|
508
877
|
Widget _buildConnectedContent(
|
|
@@ -712,7 +1081,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
712
1081
|
/// The showcase segment is the shared [KasyTabs] component (primary pill,
|
|
713
1082
|
/// fill mode) so the sidebar demos the real design-system control rather than
|
|
714
1083
|
/// a bespoke copy.
|
|
715
|
-
Widget _buildTabs(
|
|
1084
|
+
Widget _buildTabs() {
|
|
716
1085
|
return KasyTabs(
|
|
717
1086
|
tabs: const ['Layers', 'Assets'],
|
|
718
1087
|
selectedIndex: _showcaseTab,
|
|
@@ -40,12 +40,19 @@ class KasyWebHeader extends StatelessWidget {
|
|
|
40
40
|
/// Called when the search field is submitted (Enter).
|
|
41
41
|
final ValueChanged<String>? onSearchSubmitted;
|
|
42
42
|
|
|
43
|
-
/// Notifications (bell) action. When null the bell is disabled.
|
|
43
|
+
/// Notifications (bell) action. When null the bell is disabled. Ignored when
|
|
44
|
+
/// [notifications] is provided.
|
|
44
45
|
final VoidCallback? onNotifications;
|
|
45
46
|
|
|
46
|
-
/// Shows the unread dot on the notifications bell.
|
|
47
|
+
/// Shows the unread dot on the notifications bell. Ignored when
|
|
48
|
+
/// [notifications] is provided.
|
|
47
49
|
final bool showNotificationBadge;
|
|
48
50
|
|
|
51
|
+
/// Custom notifications control. When set, it replaces the built-in bell —
|
|
52
|
+
/// pass a data-aware widget (e.g. a bell that opens a recent-notifications
|
|
53
|
+
/// dropdown) so the header itself stays a pure presentational component.
|
|
54
|
+
final Widget? notifications;
|
|
55
|
+
|
|
49
56
|
/// Primary quick-create action. When null the button is disabled.
|
|
50
57
|
final VoidCallback? onCreate;
|
|
51
58
|
|
|
@@ -79,6 +86,7 @@ class KasyWebHeader extends StatelessWidget {
|
|
|
79
86
|
this.onSearchSubmitted,
|
|
80
87
|
this.onNotifications,
|
|
81
88
|
this.showNotificationBadge = false,
|
|
89
|
+
this.notifications,
|
|
82
90
|
this.onCreate,
|
|
83
91
|
this.createLabel = 'Create',
|
|
84
92
|
this.avatarGradient = KasyAvatarGradients.orange,
|
|
@@ -120,7 +128,7 @@ class KasyWebHeader extends StatelessWidget {
|
|
|
120
128
|
_buildThemeToggle(context),
|
|
121
129
|
const SizedBox(width: KasySpacing.md),
|
|
122
130
|
],
|
|
123
|
-
_buildNotifications(context),
|
|
131
|
+
notifications ?? _buildNotifications(context),
|
|
124
132
|
const SizedBox(width: KasySpacing.md),
|
|
125
133
|
KasyButton(
|
|
126
134
|
label: createLabel,
|