kasy-cli 1.39.0 → 1.40.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 +23 -0
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +27 -27
- package/templates/firebase/AGENTS.md +2 -2
- package/templates/firebase/DESIGN_SYSTEM.md +3 -2
- package/templates/firebase/lib/components/components.dart +6 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +11 -1
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +25 -7
- package/templates/firebase/lib/components/kasy_menu.dart +902 -0
- package/templates/firebase/lib/components/kasy_popover.dart +267 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +20 -10
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +23 -7
- package/templates/firebase/lib/core/chrome/app_bar_config.dart +1 -1
- package/templates/firebase/lib/core/navigation/kasy_route_observer.dart +8 -0
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +96 -14
- package/templates/firebase/lib/core/theme/texts.dart +25 -0
- package/templates/firebase/lib/core/web_viewport_scale.dart +8 -7
- package/templates/firebase/lib/features/home/home_components_page.dart +23 -4
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +363 -13
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart +4 -0
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +38 -94
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +32 -107
- package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +21 -21
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +35 -27
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +22 -17
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +12 -7
- package/templates/firebase/lib/i18n/en.i18n.json +6 -2
- package/templates/firebase/lib/i18n/es.i18n.json +6 -2
- package/templates/firebase/lib/i18n/pt.i18n.json +6 -2
- package/templates/firebase/lib/router.dart +2 -0
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/app_bar_config_test.dart +70 -0
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +0 -81
|
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|
|
2
2
|
import 'package:go_router/go_router.dart';
|
|
3
3
|
import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
4
4
|
import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
|
|
5
|
+
import 'package:kasy_kit/core/navigation/kasy_route_observer.dart';
|
|
5
6
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
6
7
|
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
7
8
|
import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
|
|
@@ -46,18 +47,34 @@ class HomeComponentsCatalog extends StatefulWidget {
|
|
|
46
47
|
State<HomeComponentsCatalog> createState() => _HomeComponentsCatalogState();
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
class _HomeComponentsCatalogState extends State<HomeComponentsCatalog>
|
|
50
|
+
class _HomeComponentsCatalogState extends State<HomeComponentsCatalog>
|
|
51
|
+
with RouteAware {
|
|
50
52
|
final TextEditingController _searchCtrl = TextEditingController();
|
|
51
53
|
String _query = '';
|
|
52
54
|
|
|
55
|
+
@override
|
|
56
|
+
void didChangeDependencies() {
|
|
57
|
+
super.didChangeDependencies();
|
|
58
|
+
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
|
59
|
+
if (route != null) {
|
|
60
|
+
kasyRouteObserver.subscribe(this, route);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
@override
|
|
54
65
|
void dispose() {
|
|
66
|
+
kasyRouteObserver.unsubscribe(this);
|
|
55
67
|
_searchCtrl.dispose();
|
|
56
68
|
super.dispose();
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
///
|
|
60
|
-
/// full catalog instead of the stale search term.
|
|
71
|
+
/// When a pushed preview pops back to the catalog, reset the filter so the
|
|
72
|
+
/// list shows the full catalog instead of the stale search term. Backed up by
|
|
73
|
+
/// the navigation future's [_clearSearch] for the admin-embedded case, whose
|
|
74
|
+
/// nested navigator the root observer does not see.
|
|
75
|
+
@override
|
|
76
|
+
void didPopNext() => _clearSearch();
|
|
77
|
+
|
|
61
78
|
void _clearSearch() {
|
|
62
79
|
if (!mounted) return;
|
|
63
80
|
if (_query.isEmpty && _searchCtrl.text.isEmpty) return;
|
|
@@ -325,6 +342,7 @@ const Set<String> _kReadyComponents = <String>{
|
|
|
325
342
|
'Dialog',
|
|
326
343
|
'DropDown',
|
|
327
344
|
'Hover',
|
|
345
|
+
'Menu',
|
|
328
346
|
'Tabs',
|
|
329
347
|
'TextArea',
|
|
330
348
|
'TextField',
|
|
@@ -352,6 +370,7 @@ const Set<String> _kWebReadyComponents = <String>{
|
|
|
352
370
|
'Dialog',
|
|
353
371
|
'DropDown',
|
|
354
372
|
'Hover',
|
|
373
|
+
'Menu',
|
|
355
374
|
'Sidebar',
|
|
356
375
|
'Skeleton',
|
|
357
376
|
'Status Tag',
|
|
@@ -384,6 +403,7 @@ const List<_CatalogRow> _kCatalog = [
|
|
|
384
403
|
_CatalogRow('Dialog'),
|
|
385
404
|
_CatalogRow('DropDown'),
|
|
386
405
|
_CatalogRow('Hover'),
|
|
406
|
+
_CatalogRow('Menu'),
|
|
387
407
|
_CatalogRow('Sidebar'),
|
|
388
408
|
_CatalogRow('Skeleton'),
|
|
389
409
|
_CatalogRow('Status Tag'),
|
|
@@ -412,7 +432,6 @@ const List<_CatalogRow> _kCatalog = [
|
|
|
412
432
|
_CatalogRow('LineChart'),
|
|
413
433
|
_CatalogRow('LinkButton'),
|
|
414
434
|
_CatalogRow('ListGroup'),
|
|
415
|
-
_CatalogRow('Menu'),
|
|
416
435
|
_CatalogRow('NumberField'),
|
|
417
436
|
_CatalogRow('NumberStepper'),
|
|
418
437
|
_CatalogRow('NumberValue'),
|
|
@@ -101,6 +101,15 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
101
101
|
label: 'Application no avatar (desktop)',
|
|
102
102
|
builder: _buildAppBarApplicationNoAvatarVariant,
|
|
103
103
|
),
|
|
104
|
+
// Per-screen presets — same bar, a screen picking its own chrome.
|
|
105
|
+
ComponentPreviewVariant(
|
|
106
|
+
label: 'Search only (desktop)',
|
|
107
|
+
builder: _buildAppBarApplicationSearchOnlyVariant,
|
|
108
|
+
),
|
|
109
|
+
ComponentPreviewVariant(
|
|
110
|
+
label: 'Notifications only (desktop)',
|
|
111
|
+
builder: _buildAppBarApplicationNotificationsOnlyVariant,
|
|
112
|
+
),
|
|
104
113
|
],
|
|
105
114
|
);
|
|
106
115
|
case 'Button':
|
|
@@ -323,6 +332,21 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
323
332
|
),
|
|
324
333
|
],
|
|
325
334
|
);
|
|
335
|
+
case 'Menu':
|
|
336
|
+
return const ComponentPreviewDefinition(
|
|
337
|
+
title: 'Menu',
|
|
338
|
+
variants: [
|
|
339
|
+
ComponentPreviewVariant(
|
|
340
|
+
label: 'Basic usage',
|
|
341
|
+
builder: _buildMenuBasicUsage,
|
|
342
|
+
),
|
|
343
|
+
ComponentPreviewVariant(
|
|
344
|
+
label: 'Selection',
|
|
345
|
+
builder: _buildMenuStyles,
|
|
346
|
+
),
|
|
347
|
+
ComponentPreviewVariant(label: 'Submenu', builder: _buildMenuSubmenu),
|
|
348
|
+
],
|
|
349
|
+
);
|
|
326
350
|
case 'Badge':
|
|
327
351
|
return const ComponentPreviewDefinition(
|
|
328
352
|
title: 'Badge',
|
|
@@ -1942,16 +1966,42 @@ Widget _buildAppBarMenuVariant(BuildContext context) {
|
|
|
1942
1966
|
);
|
|
1943
1967
|
}
|
|
1944
1968
|
|
|
1969
|
+
/// A fully-wired desktop config for previews — every element active, so each
|
|
1970
|
+
/// variant can switch one thing on/off and read clearly.
|
|
1971
|
+
KasyAppBarConfig _demoAppBarConfig() => KasyAppBarConfig(
|
|
1972
|
+
onToggleTheme: () {},
|
|
1973
|
+
onNotifications: () {},
|
|
1974
|
+
onCreate: () {},
|
|
1975
|
+
onAvatarTap: () {},
|
|
1976
|
+
);
|
|
1977
|
+
|
|
1945
1978
|
Widget _buildAppBarApplicationVariant(BuildContext context) {
|
|
1946
|
-
return
|
|
1979
|
+
return _ApplicationBarPreview(config: _demoAppBarConfig());
|
|
1947
1980
|
}
|
|
1948
1981
|
|
|
1949
1982
|
Widget _buildAppBarApplicationBadgeVariant(BuildContext context) {
|
|
1950
|
-
return
|
|
1983
|
+
return _ApplicationBarPreview(
|
|
1984
|
+
config: _demoAppBarConfig().copyWith(showNotificationBadge: true),
|
|
1985
|
+
);
|
|
1951
1986
|
}
|
|
1952
1987
|
|
|
1953
1988
|
Widget _buildAppBarApplicationNoAvatarVariant(BuildContext context) {
|
|
1954
|
-
return
|
|
1989
|
+
return _ApplicationBarPreview(
|
|
1990
|
+
config: _demoAppBarConfig().copyWith(showAvatar: false),
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// Per-screen presets: the same component, different chrome chosen by a screen.
|
|
1995
|
+
Widget _buildAppBarApplicationSearchOnlyVariant(BuildContext context) {
|
|
1996
|
+
return _ApplicationBarPreview(
|
|
1997
|
+
config: _demoAppBarConfig().only(search: true, themeToggle: true),
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
Widget _buildAppBarApplicationNotificationsOnlyVariant(BuildContext context) {
|
|
2002
|
+
return _ApplicationBarPreview(
|
|
2003
|
+
config: _demoAppBarConfig().only(notifications: true, themeToggle: true),
|
|
2004
|
+
);
|
|
1955
2005
|
}
|
|
1956
2006
|
|
|
1957
2007
|
Widget _buildButtonSizesVariant(BuildContext context) {
|
|
@@ -3884,10 +3934,11 @@ class _AccordionPreviewState extends State<_AccordionPreview> {
|
|
|
3884
3934
|
/// bar + faux sidebar + content) so the preview reads as the desktop chrome it
|
|
3885
3935
|
/// is — the responsive desktop half of [KasyAppBar].
|
|
3886
3936
|
class _ApplicationBarPreview extends StatelessWidget {
|
|
3887
|
-
|
|
3888
|
-
|
|
3937
|
+
/// The exact config a screen would publish — rendered through the same
|
|
3938
|
+
/// [KasyAppBar.fromConfig] the shell uses, so the preview mirrors real usage.
|
|
3939
|
+
final KasyAppBarConfig config;
|
|
3889
3940
|
|
|
3890
|
-
const _ApplicationBarPreview({
|
|
3941
|
+
const _ApplicationBarPreview({required this.config});
|
|
3891
3942
|
|
|
3892
3943
|
/// Width the mock window is laid out at. The application bar is desktop chrome
|
|
3893
3944
|
/// (220px search + actions), so it needs a desktop-class width — we render at
|
|
@@ -3916,13 +3967,7 @@ class _ApplicationBarPreview extends StatelessWidget {
|
|
|
3916
3967
|
children: [
|
|
3917
3968
|
const _BrowserTopBar(),
|
|
3918
3969
|
// The real bar, flush — no corner radius of its own.
|
|
3919
|
-
KasyAppBar.
|
|
3920
|
-
showNotificationBadge: showBadge,
|
|
3921
|
-
showAvatar: showAvatar,
|
|
3922
|
-
onNotifications: () {},
|
|
3923
|
-
onCreate: () {},
|
|
3924
|
-
onAvatarTap: () {},
|
|
3925
|
-
),
|
|
3970
|
+
KasyAppBar.fromConfig(config),
|
|
3926
3971
|
const SizedBox(
|
|
3927
3972
|
height: 150,
|
|
3928
3973
|
child: Row(
|
|
@@ -5387,6 +5432,311 @@ Widget _previewIntrinsicLaunch(Widget child) {
|
|
|
5387
5432
|
);
|
|
5388
5433
|
}
|
|
5389
5434
|
|
|
5435
|
+
// ── Menu (KasyMenu) previews ─────────────────────────────────────────────────
|
|
5436
|
+
// Each preset is triggered by a button — a real menu opens, anchored to it.
|
|
5437
|
+
// Most open as the default floating menu (popover, both platforms); one opens as
|
|
5438
|
+
// a bottom sheet to show the opt-in presentation.
|
|
5439
|
+
|
|
5440
|
+
// Basic usage menu — mirrors the HeroUI "Actions / Danger zone" dropdown
|
|
5441
|
+
// (Figma 17840:22571): grouped rows with an icon, title + description and a
|
|
5442
|
+
// keyboard shortcut, plus a destructive "Danger zone". The kit has no pencil
|
|
5443
|
+
// glyph yet, so Edit file borrows the note icon.
|
|
5444
|
+
const List<KasyMenuSection> _kMenuActions = [
|
|
5445
|
+
KasyMenuSection(
|
|
5446
|
+
label: 'Actions',
|
|
5447
|
+
items: [
|
|
5448
|
+
KasyMenuItem(
|
|
5449
|
+
icon: KasyIcons.add,
|
|
5450
|
+
title: 'New file',
|
|
5451
|
+
description: 'Create a new file',
|
|
5452
|
+
shortcut: '⌘ N',
|
|
5453
|
+
onTap: _triggerTapFeedback,
|
|
5454
|
+
),
|
|
5455
|
+
KasyMenuItem(
|
|
5456
|
+
icon: KasyIcons.copy,
|
|
5457
|
+
title: 'Copy link',
|
|
5458
|
+
description: 'Copy the file link',
|
|
5459
|
+
shortcut: '⌘ L',
|
|
5460
|
+
onTap: _triggerTapFeedback,
|
|
5461
|
+
),
|
|
5462
|
+
KasyMenuItem(
|
|
5463
|
+
icon: KasyIcons.note,
|
|
5464
|
+
title: 'Edit file',
|
|
5465
|
+
description: 'Make changes',
|
|
5466
|
+
shortcut: '⌘ E',
|
|
5467
|
+
onTap: _triggerTapFeedback,
|
|
5468
|
+
),
|
|
5469
|
+
],
|
|
5470
|
+
),
|
|
5471
|
+
KasyMenuSection(
|
|
5472
|
+
label: 'Danger zone',
|
|
5473
|
+
items: [
|
|
5474
|
+
KasyMenuItem(
|
|
5475
|
+
icon: KasyIcons.trash,
|
|
5476
|
+
title: 'Delete file',
|
|
5477
|
+
description: 'Move to trash',
|
|
5478
|
+
shortcut: '⌘ ⇧ K',
|
|
5479
|
+
tone: KasyMenuItemTone.danger,
|
|
5480
|
+
onTap: _triggerTapFeedback,
|
|
5481
|
+
),
|
|
5482
|
+
],
|
|
5483
|
+
),
|
|
5484
|
+
];
|
|
5485
|
+
|
|
5486
|
+
const List<KasyMenuSection> _kMenuSubmenu = [
|
|
5487
|
+
KasyMenuSection(
|
|
5488
|
+
items: [
|
|
5489
|
+
KasyMenuItem(
|
|
5490
|
+
icon: KasyIcons.share,
|
|
5491
|
+
title: 'Share',
|
|
5492
|
+
submenu: [
|
|
5493
|
+
KasyMenuSection(
|
|
5494
|
+
items: [
|
|
5495
|
+
KasyMenuItem(title: 'WhatsApp', onTap: _triggerTapFeedback),
|
|
5496
|
+
KasyMenuItem(title: 'Telegram', onTap: _triggerTapFeedback),
|
|
5497
|
+
KasyMenuItem(title: 'Email', onTap: _triggerTapFeedback),
|
|
5498
|
+
],
|
|
5499
|
+
),
|
|
5500
|
+
],
|
|
5501
|
+
),
|
|
5502
|
+
KasyMenuItem(
|
|
5503
|
+
icon: KasyIcons.copy,
|
|
5504
|
+
title: 'Copy link',
|
|
5505
|
+
onTap: _triggerTapFeedback,
|
|
5506
|
+
),
|
|
5507
|
+
],
|
|
5508
|
+
),
|
|
5509
|
+
];
|
|
5510
|
+
|
|
5511
|
+
|
|
5512
|
+
// Layout shared by the two interactive menu demos: the trigger floats centered
|
|
5513
|
+
// in the open area (so the menu has room to anchor against it) and a toggle is
|
|
5514
|
+
// pinned at the bottom, just above the variant title. Height is a slice of the
|
|
5515
|
+
// viewport, clamped, and kept under the preview height so it still centers.
|
|
5516
|
+
class _MenuDemoFrame extends StatelessWidget {
|
|
5517
|
+
final Widget trigger;
|
|
5518
|
+
final String controlTitle;
|
|
5519
|
+
final String controlSubtitle;
|
|
5520
|
+
final bool controlValue;
|
|
5521
|
+
final ValueChanged<bool> onControlChanged;
|
|
5522
|
+
|
|
5523
|
+
const _MenuDemoFrame({
|
|
5524
|
+
required this.trigger,
|
|
5525
|
+
required this.controlTitle,
|
|
5526
|
+
required this.controlSubtitle,
|
|
5527
|
+
required this.controlValue,
|
|
5528
|
+
required this.onControlChanged,
|
|
5529
|
+
});
|
|
5530
|
+
|
|
5531
|
+
@override
|
|
5532
|
+
Widget build(BuildContext context) {
|
|
5533
|
+
// Give the demo real vertical room so the control row can sit near the
|
|
5534
|
+
// bottom (like a settings row) instead of crowding under the trigger. The
|
|
5535
|
+
// trigger still lives in a plain centering Row — the menu anchors to its
|
|
5536
|
+
// render box via LayerLink, so the surrounding height never moves the menu.
|
|
5537
|
+
final double viewportHeight = MediaQuery.sizeOf(context).height;
|
|
5538
|
+
final double frameHeight = (viewportHeight - 260).clamp(320.0, 520.0);
|
|
5539
|
+
return SizedBox(
|
|
5540
|
+
height: frameHeight,
|
|
5541
|
+
child: Column(
|
|
5542
|
+
children: [
|
|
5543
|
+
const Spacer(flex: 4),
|
|
5544
|
+
Row(mainAxisAlignment: MainAxisAlignment.center, children: [trigger]),
|
|
5545
|
+
const Spacer(flex: 6),
|
|
5546
|
+
Row(
|
|
5547
|
+
children: [
|
|
5548
|
+
Expanded(
|
|
5549
|
+
child: Column(
|
|
5550
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5551
|
+
mainAxisSize: MainAxisSize.min,
|
|
5552
|
+
children: [
|
|
5553
|
+
Text(
|
|
5554
|
+
controlTitle,
|
|
5555
|
+
style: context.textTheme.titleSmall?.copyWith(
|
|
5556
|
+
color: context.colors.onSurface,
|
|
5557
|
+
),
|
|
5558
|
+
),
|
|
5559
|
+
Text(
|
|
5560
|
+
controlSubtitle,
|
|
5561
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
5562
|
+
color: context.colors.muted,
|
|
5563
|
+
),
|
|
5564
|
+
),
|
|
5565
|
+
],
|
|
5566
|
+
),
|
|
5567
|
+
),
|
|
5568
|
+
// The same switch the Settings screen uses: adaptive (Cupertino on
|
|
5569
|
+
// iOS, Material elsewhere — identical look per platform), scaled
|
|
5570
|
+
// down, themed to the primary color.
|
|
5571
|
+
Transform.scale(
|
|
5572
|
+
scale: 0.82,
|
|
5573
|
+
child: Switch.adaptive(
|
|
5574
|
+
value: controlValue,
|
|
5575
|
+
onChanged: onControlChanged,
|
|
5576
|
+
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
5577
|
+
),
|
|
5578
|
+
),
|
|
5579
|
+
],
|
|
5580
|
+
),
|
|
5581
|
+
],
|
|
5582
|
+
),
|
|
5583
|
+
);
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
|
|
5587
|
+
Widget _buildMenuBasicUsage(BuildContext context) => const _MenuBasicUsageDemo();
|
|
5588
|
+
|
|
5589
|
+
// First preview: the trigger + a "Bottom Sheet" toggle. Off → the menu opens as
|
|
5590
|
+
// a popover anchored to the button; on → it slides up as a bottom sheet. The
|
|
5591
|
+
// same compact menu either way — only the container changes.
|
|
5592
|
+
class _MenuBasicUsageDemo extends StatefulWidget {
|
|
5593
|
+
const _MenuBasicUsageDemo();
|
|
5594
|
+
|
|
5595
|
+
@override
|
|
5596
|
+
State<_MenuBasicUsageDemo> createState() => _MenuBasicUsageDemoState();
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
class _MenuBasicUsageDemoState extends State<_MenuBasicUsageDemo> {
|
|
5600
|
+
bool _bottomSheet = false;
|
|
5601
|
+
|
|
5602
|
+
@override
|
|
5603
|
+
Widget build(BuildContext context) {
|
|
5604
|
+
return _MenuDemoFrame(
|
|
5605
|
+
controlTitle: 'Bottom Sheet',
|
|
5606
|
+
controlSubtitle: 'Toggle bottom sheet presentation',
|
|
5607
|
+
controlValue: _bottomSheet,
|
|
5608
|
+
onControlChanged: (bool v) => setState(() => _bottomSheet = v),
|
|
5609
|
+
trigger: KasyMenuAnchor(
|
|
5610
|
+
sections: _kMenuActions,
|
|
5611
|
+
// Wider panel: rows carry a title + description (HeroUI menu).
|
|
5612
|
+
width: 288,
|
|
5613
|
+
presentation: _bottomSheet
|
|
5614
|
+
? KasyMenuPresentation.bottomSheet
|
|
5615
|
+
: KasyMenuPresentation.popover,
|
|
5616
|
+
builder: (BuildContext context, VoidCallback open) => KasyButton(
|
|
5617
|
+
label: 'Actions',
|
|
5618
|
+
variant: KasyButtonVariant.soft,
|
|
5619
|
+
onPressed: () {
|
|
5620
|
+
_triggerTapFeedback();
|
|
5621
|
+
open();
|
|
5622
|
+
},
|
|
5623
|
+
),
|
|
5624
|
+
),
|
|
5625
|
+
);
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
Widget _buildMenuStyles(BuildContext context) => const _MenuSelectionDemo();
|
|
5630
|
+
|
|
5631
|
+
// Second preview: a real, interactive selection menu. Tapping a "Text Style"
|
|
5632
|
+
// row toggles its check (multi-select); tapping a "Text Alignment" row moves the
|
|
5633
|
+
// filled dot (single-select). The selection persists across reopens. A "Should
|
|
5634
|
+
// Close On Select" toggle controls whether picking an item dismisses the menu:
|
|
5635
|
+
// off → keep picking, tap outside to close (HeroUI editor menu); on → one pick
|
|
5636
|
+
// closes it.
|
|
5637
|
+
class _MenuSelectionDemo extends StatefulWidget {
|
|
5638
|
+
const _MenuSelectionDemo();
|
|
5639
|
+
|
|
5640
|
+
@override
|
|
5641
|
+
State<_MenuSelectionDemo> createState() => _MenuSelectionDemoState();
|
|
5642
|
+
}
|
|
5643
|
+
|
|
5644
|
+
class _MenuSelectionDemoState extends State<_MenuSelectionDemo> {
|
|
5645
|
+
// Multi-select set + single-select value, kept on the demo so reopening the
|
|
5646
|
+
// menu shows the last selection.
|
|
5647
|
+
final Set<String> _styles = <String>{'Bold', 'Underline'};
|
|
5648
|
+
String _align = 'Right';
|
|
5649
|
+
bool _closeOnSelect = false;
|
|
5650
|
+
|
|
5651
|
+
static const List<(String, String)> _styleItems = [
|
|
5652
|
+
('Bold', '⌘ B'),
|
|
5653
|
+
('Italic', '⌘ I'),
|
|
5654
|
+
('Underline', '⌘ U'),
|
|
5655
|
+
];
|
|
5656
|
+
static const List<(String, String)> _alignItems = [
|
|
5657
|
+
('Left', '⌥ A'),
|
|
5658
|
+
('Center', '⌥ H'),
|
|
5659
|
+
('Right', '⌥ D'),
|
|
5660
|
+
];
|
|
5661
|
+
|
|
5662
|
+
void _toggleStyle(String title) {
|
|
5663
|
+
setState(() {
|
|
5664
|
+
if (!_styles.remove(title)) _styles.add(title);
|
|
5665
|
+
});
|
|
5666
|
+
}
|
|
5667
|
+
|
|
5668
|
+
void _selectAlign(String title) => setState(() => _align = title);
|
|
5669
|
+
|
|
5670
|
+
// Built from state so each tap (via setState) rebuilds the anchor and the
|
|
5671
|
+
// open menu reflects the new selection in place.
|
|
5672
|
+
List<KasyMenuSection> _buildSections() => [
|
|
5673
|
+
KasyMenuSection(
|
|
5674
|
+
label: 'Text Style',
|
|
5675
|
+
selectionMode: KasyMenuSelectionMode.multiple,
|
|
5676
|
+
items: [
|
|
5677
|
+
for (final (String title, String shortcut) in _styleItems)
|
|
5678
|
+
KasyMenuItem(
|
|
5679
|
+
title: title,
|
|
5680
|
+
shortcut: shortcut,
|
|
5681
|
+
selected: _styles.contains(title),
|
|
5682
|
+
onTap: () => _toggleStyle(title),
|
|
5683
|
+
),
|
|
5684
|
+
],
|
|
5685
|
+
),
|
|
5686
|
+
KasyMenuSection(
|
|
5687
|
+
label: 'Text Alignment',
|
|
5688
|
+
selectionMode: KasyMenuSelectionMode.single,
|
|
5689
|
+
items: [
|
|
5690
|
+
for (final (String title, String shortcut) in _alignItems)
|
|
5691
|
+
KasyMenuItem(
|
|
5692
|
+
title: title,
|
|
5693
|
+
shortcut: shortcut,
|
|
5694
|
+
selected: _align == title,
|
|
5695
|
+
onTap: () => _selectAlign(title),
|
|
5696
|
+
),
|
|
5697
|
+
],
|
|
5698
|
+
),
|
|
5699
|
+
];
|
|
5700
|
+
|
|
5701
|
+
@override
|
|
5702
|
+
Widget build(BuildContext context) {
|
|
5703
|
+
return _MenuDemoFrame(
|
|
5704
|
+
controlTitle: 'Should Close On Select',
|
|
5705
|
+
controlSubtitle: 'Toggle should close on select',
|
|
5706
|
+
controlValue: _closeOnSelect,
|
|
5707
|
+
onControlChanged: (bool v) => setState(() => _closeOnSelect = v),
|
|
5708
|
+
trigger: KasyMenuAnchor(
|
|
5709
|
+
sections: _buildSections(),
|
|
5710
|
+
closeOnSelect: _closeOnSelect,
|
|
5711
|
+
builder: (BuildContext context, VoidCallback open) => KasyButton(
|
|
5712
|
+
label: 'Text styles',
|
|
5713
|
+
variant: KasyButtonVariant.soft,
|
|
5714
|
+
onPressed: () {
|
|
5715
|
+
_triggerTapFeedback();
|
|
5716
|
+
open();
|
|
5717
|
+
},
|
|
5718
|
+
),
|
|
5719
|
+
),
|
|
5720
|
+
);
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
|
|
5724
|
+
Widget _buildMenuSubmenu(BuildContext context) {
|
|
5725
|
+
return _previewIntrinsicLaunch(
|
|
5726
|
+
KasyMenuAnchor(
|
|
5727
|
+
sections: _kMenuSubmenu,
|
|
5728
|
+
builder: (BuildContext context, VoidCallback open) => KasyButton(
|
|
5729
|
+
label: 'Share',
|
|
5730
|
+
variant: KasyButtonVariant.soft,
|
|
5731
|
+
onPressed: () {
|
|
5732
|
+
_triggerTapFeedback();
|
|
5733
|
+
open();
|
|
5734
|
+
},
|
|
5735
|
+
),
|
|
5736
|
+
),
|
|
5737
|
+
);
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5390
5740
|
const List<KasyAccordionItemData> _kAccordionItems = <KasyAccordionItemData>[
|
|
5391
5741
|
KasyAccordionItemData(
|
|
5392
5742
|
icon: KasyIcons.send,
|
package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart
CHANGED
|
@@ -5,6 +5,7 @@ import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
|
5
5
|
import 'package:kasy_kit/core/home_widgets/home_widget_mywidget_service.dart';
|
|
6
6
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
7
7
|
import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
|
|
8
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
8
9
|
import 'package:kasy_kit/features/settings/ui/widgets/admin_card.dart';
|
|
9
10
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
10
11
|
|
|
@@ -18,6 +19,9 @@ class AdminHomeWidgets extends ConsumerWidget {
|
|
|
18
19
|
child: KasyOverlayScaffold(
|
|
19
20
|
title: t.settings.admin.home_widgets_title,
|
|
20
21
|
onBack: () => context.pop(),
|
|
22
|
+
// Contain + center the single utility card on desktop so it never
|
|
23
|
+
// stretches edge-to-edge, matching Notifications / Reminders.
|
|
24
|
+
maxContentWidth: kKasyContentMaxWidth,
|
|
21
25
|
slivers: [
|
|
22
26
|
SliverList.list(
|
|
23
27
|
children: [
|
|
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
|
|
5
5
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
6
6
|
import 'package:image_picker/image_picker.dart';
|
|
7
7
|
import 'package:kasy_kit/components/kasy_avatar.dart';
|
|
8
|
-
import 'package:kasy_kit/components/
|
|
8
|
+
import 'package:kasy_kit/components/kasy_menu.dart';
|
|
9
9
|
import 'package:kasy_kit/core/data/entities/upload_result.dart';
|
|
10
10
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
11
11
|
import 'package:kasy_kit/core/data/repositories/user_repository.dart';
|
|
@@ -13,7 +13,6 @@ import 'package:kasy_kit/core/states/models/user_state.dart';
|
|
|
13
13
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
14
14
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
15
15
|
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
16
|
-
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
17
16
|
import 'package:kasy_kit/features/settings/ui/widgets/avatar_utils.dart';
|
|
18
17
|
import 'package:kasy_kit/features/settings/ui/widgets/round_progress.dart';
|
|
19
18
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
@@ -121,69 +120,56 @@ class _EditableUserAvatarState extends ConsumerState<EditableUserAvatar> {
|
|
|
121
120
|
BuildContext context,
|
|
122
121
|
UserState userState,
|
|
123
122
|
String? avatarPath,
|
|
124
|
-
)
|
|
123
|
+
) {
|
|
125
124
|
final userId = userState.user.idOrNull;
|
|
126
125
|
final cachedUrl = userId == null ? null : getCachedAvatarUrl(userId);
|
|
127
126
|
final hasAvatar =
|
|
128
127
|
_hasAvatarUrl(avatarPath) ||
|
|
129
128
|
_hasAvatarUrl(cachedUrl) ||
|
|
130
129
|
temporaryAvatarBytes != null;
|
|
131
|
-
final
|
|
132
|
-
|
|
133
|
-
switch (result) {
|
|
134
|
-
case _AvatarAction.camera:
|
|
135
|
-
final file = await ImagePicker().pickImage(source: ImageSource.camera);
|
|
136
|
-
await _uploadTask(file, userState);
|
|
137
|
-
case _AvatarAction.library:
|
|
138
|
-
final file = await ImagePicker().pickImage(source: ImageSource.gallery);
|
|
139
|
-
await _uploadTask(file, userState);
|
|
140
|
-
case _AvatarAction.remove:
|
|
141
|
-
await _removeAvatar(userState);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
Future<_AvatarAction?> _showAvatarBottomSheet(
|
|
146
|
-
BuildContext context, {
|
|
147
|
-
required bool hasAvatar,
|
|
148
|
-
}) {
|
|
149
|
-
return showKasyBottomSheet<_AvatarAction>(
|
|
130
|
+
final avatar = t.settings.avatar;
|
|
131
|
+
return showKasyMenu<void>(
|
|
150
132
|
context: context,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
133
|
+
anchorContext: context,
|
|
134
|
+
desktopWidth: 248,
|
|
135
|
+
sections: [
|
|
136
|
+
KasyMenuSection(
|
|
137
|
+
items: [
|
|
138
|
+
KasyMenuItem(
|
|
139
|
+
icon: KasyIcons.cameraAlt,
|
|
140
|
+
title: avatar.take_photo,
|
|
141
|
+
onTap: () => _pickAndUpload(ImageSource.camera, userState),
|
|
142
|
+
),
|
|
143
|
+
KasyMenuItem(
|
|
144
|
+
icon: KasyIcons.gallery,
|
|
145
|
+
title: avatar.choose_library,
|
|
146
|
+
onTap: () => _pickAndUpload(ImageSource.gallery, userState),
|
|
147
|
+
),
|
|
148
|
+
],
|
|
149
|
+
),
|
|
150
|
+
// Destructive action lives in its own ("Danger zone") group, per the
|
|
151
|
+
// HeroUI menu guidance.
|
|
152
|
+
if (hasAvatar)
|
|
153
|
+
KasyMenuSection(
|
|
154
|
+
items: [
|
|
155
|
+
KasyMenuItem(
|
|
156
|
+
icon: KasyIcons.trash,
|
|
157
|
+
title: avatar.remove_photo,
|
|
158
|
+
tone: KasyMenuItemTone.danger,
|
|
159
|
+
onTap: () => _removeAvatar(userState),
|
|
167
160
|
),
|
|
168
|
-
if (hasAvatar) ...[
|
|
169
|
-
Divider(
|
|
170
|
-
color: sheetContext.colors.onSurface.withValues(alpha: .08),
|
|
171
|
-
height: 1,
|
|
172
|
-
),
|
|
173
|
-
_BottomSheetTile(
|
|
174
|
-
icon: KasyIcons.trash,
|
|
175
|
-
label: t.settings.avatar.remove_photo,
|
|
176
|
-
color: sheetContext.colors.error,
|
|
177
|
-
onTap: () => Navigator.pop(sheetContext, _AvatarAction.remove),
|
|
178
|
-
),
|
|
179
|
-
],
|
|
180
161
|
],
|
|
181
162
|
),
|
|
182
|
-
|
|
183
|
-
),
|
|
163
|
+
],
|
|
184
164
|
);
|
|
185
165
|
}
|
|
186
166
|
|
|
167
|
+
Future<void> _pickAndUpload(ImageSource source, UserState userState) async {
|
|
168
|
+
final file = await ImagePicker().pickImage(source: source);
|
|
169
|
+
if (!mounted) return;
|
|
170
|
+
await _uploadTask(file, userState);
|
|
171
|
+
}
|
|
172
|
+
|
|
187
173
|
Future<void> _uploadTask(XFile? file, UserState userState) async {
|
|
188
174
|
if (file == null) return;
|
|
189
175
|
final userId = userState.user.idOrNull;
|
|
@@ -301,48 +287,6 @@ class _EditableUserAvatarState extends ConsumerState<EditableUserAvatar> {
|
|
|
301
287
|
}
|
|
302
288
|
}
|
|
303
289
|
|
|
304
|
-
enum _AvatarAction { camera, library, remove }
|
|
305
|
-
|
|
306
|
-
class _BottomSheetTile extends StatelessWidget {
|
|
307
|
-
final IconData? icon;
|
|
308
|
-
final String label;
|
|
309
|
-
final Color? color;
|
|
310
|
-
final VoidCallback onTap;
|
|
311
|
-
|
|
312
|
-
const _BottomSheetTile({
|
|
313
|
-
this.icon,
|
|
314
|
-
required this.label,
|
|
315
|
-
this.color,
|
|
316
|
-
required this.onTap,
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
@override
|
|
320
|
-
Widget build(BuildContext context) {
|
|
321
|
-
final Color fg = color ?? context.colors.onSurface;
|
|
322
|
-
return KasyHover(
|
|
323
|
-
onTap: onTap,
|
|
324
|
-
focusable: true,
|
|
325
|
-
focusGapColor: context.colors.surface,
|
|
326
|
-
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
327
|
-
padding: const EdgeInsets.symmetric(
|
|
328
|
-
horizontal: KasySpacing.md,
|
|
329
|
-
vertical: KasySpacing.smd,
|
|
330
|
-
),
|
|
331
|
-
child: Row(
|
|
332
|
-
children: [
|
|
333
|
-
if (icon != null) ...[
|
|
334
|
-
Icon(icon, size: KasyIconSize.lg, color: fg),
|
|
335
|
-
const SizedBox(width: KasySpacing.sm),
|
|
336
|
-
],
|
|
337
|
-
Text(
|
|
338
|
-
label,
|
|
339
|
-
style: context.textTheme.bodyLarge?.copyWith(color: fg),
|
|
340
|
-
),
|
|
341
|
-
],
|
|
342
|
-
),
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
290
|
|
|
347
291
|
/// Resolves the avatar [ImageProvider] and renders a [KasyAvatar] with
|
|
348
292
|
/// [KasyAvatarFallbackSurface.soft]. Firebase Storage URL is loaded async.
|