kasy-cli 1.37.1 → 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 +2 -2
- 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 +7 -1
- package/templates/firebase/DESIGN_SYSTEM.md +13 -0
- 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 +394 -25
- 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/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
|
@@ -179,6 +179,14 @@ class _DeviceSizeBuilderState extends State<DeviceSizeBuilder>
|
|
|
179
179
|
/// The maximum width for large devices.
|
|
180
180
|
const kMaxLargeLayoutWidth = 650.0;
|
|
181
181
|
|
|
182
|
+
/// Canonical max width for a single-column internal screen's content (notifications,
|
|
183
|
+
/// settings detail, reminder, ...). On desktop the content is centered within this
|
|
184
|
+
/// width instead of stretching edge-to-edge, so lists/cards never read too wide.
|
|
185
|
+
///
|
|
186
|
+
/// This is the one knob for "contained internal content" — prefer it over bespoke
|
|
187
|
+
/// per-screen `maxWidth` literals so every internal page lines up to the same ruler.
|
|
188
|
+
const double kKasyContentMaxWidth = 600.0;
|
|
189
|
+
|
|
182
190
|
/// A widget that constrains its child to a maximum width on large devices.
|
|
183
191
|
/// It uses the [LayoutBuilder] widget to get the current device width
|
|
184
192
|
/// and constrains the child to the maximum width on large devices.
|
|
@@ -28,8 +28,22 @@ class HomeComponentsPage extends StatelessWidget {
|
|
|
28
28
|
title: 'Components',
|
|
29
29
|
onBack: () => Navigator.maybePop(context),
|
|
30
30
|
),
|
|
31
|
-
Expanded(
|
|
32
|
-
|
|
31
|
+
const Expanded(child: HomeComponentsCatalog()),
|
|
32
|
+
],
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// The UI-kit catalog list on its own (no app bar / scaffold), so it can be the
|
|
39
|
+
/// body of [HomeComponentsPage] AND a section inside the admin console (which
|
|
40
|
+
/// supplies its own chrome). Fills the space it is given.
|
|
41
|
+
class HomeComponentsCatalog extends StatelessWidget {
|
|
42
|
+
const HomeComponentsCatalog({super.key});
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
Widget build(BuildContext context) {
|
|
46
|
+
return Padding(
|
|
33
47
|
padding: EdgeInsets.fromLTRB(
|
|
34
48
|
KasySpacing.pageHorizontalGutter,
|
|
35
49
|
KasySpacing.belowChromeContentGap,
|
|
@@ -158,10 +172,6 @@ class HomeComponentsPage extends StatelessWidget {
|
|
|
158
172
|
),
|
|
159
173
|
),
|
|
160
174
|
),
|
|
161
|
-
),
|
|
162
|
-
),
|
|
163
|
-
],
|
|
164
|
-
),
|
|
165
175
|
);
|
|
166
176
|
}
|
|
167
177
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
3
|
import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
4
|
+
import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
|
|
4
5
|
import 'package:kasy_kit/components/kasy_card.dart';
|
|
5
6
|
import 'package:kasy_kit/components/kasy_chip.dart';
|
|
6
7
|
import 'package:kasy_kit/components/kasy_date_picker.dart';
|
|
@@ -126,6 +127,12 @@ class _ReminderForm extends ConsumerWidget {
|
|
|
126
127
|
_FieldLabel(tr.dateLabel),
|
|
127
128
|
const SizedBox(height: KasySpacing.sm),
|
|
128
129
|
KasyDatePicker(
|
|
130
|
+
// Anchored popover on phones/tablets (the original feel),
|
|
131
|
+
// centered dialog on desktop. The call site picks by width
|
|
132
|
+
// via kasySheetUsesDialog so each presentation stays pure.
|
|
133
|
+
presentation: kasySheetUsesDialog(context)
|
|
134
|
+
? KasyDatePickerPresentation.dialog
|
|
135
|
+
: KasyDatePickerPresentation.popover,
|
|
129
136
|
value: state.date,
|
|
130
137
|
minDate: DateTime.now(),
|
|
131
138
|
onChanged: (picked) {
|
package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
|
|
3
4
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
4
5
|
import 'package:kasy_kit/features/notifications/providers/models/notification.dart';
|
|
5
6
|
import 'package:kasy_kit/features/notifications/providers/notifications_provider.dart';
|
|
@@ -8,10 +9,8 @@ import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
|
|
|
8
9
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
9
10
|
|
|
10
11
|
void showNotificationSettingsSheet(BuildContext context) {
|
|
11
|
-
|
|
12
|
+
showKasyBottomSheet<void>(
|
|
12
13
|
context: context,
|
|
13
|
-
useRootNavigator: true,
|
|
14
|
-
backgroundColor: Colors.transparent,
|
|
15
14
|
isScrollControlled: true,
|
|
16
15
|
builder: (_) => const _NotificationSettingsSheet(),
|
|
17
16
|
);
|
|
@@ -73,14 +72,18 @@ class _NotificationSettingsSheetState
|
|
|
73
72
|
final hasUnread =
|
|
74
73
|
notificationsState.value?.data.any((n) => !n.seen) ?? false;
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
final bool floating = kasySheetIsFloating(context);
|
|
76
|
+
|
|
77
|
+
final Widget card = Container(
|
|
78
|
+
margin: floating
|
|
79
|
+
? EdgeInsets.zero
|
|
80
|
+
: EdgeInsets.only(
|
|
81
|
+
left: KasySpacing.sm,
|
|
82
|
+
right: KasySpacing.sm,
|
|
83
|
+
bottom: MediaQuery.of(context).viewInsets.bottom +
|
|
84
|
+
MediaQuery.of(context).padding.bottom +
|
|
85
|
+
KasySpacing.sm,
|
|
86
|
+
),
|
|
84
87
|
decoration: BoxDecoration(
|
|
85
88
|
color: context.colors.background,
|
|
86
89
|
borderRadius: KasyRadius.lgBorderRadius,
|
|
@@ -91,8 +94,9 @@ class _NotificationSettingsSheetState
|
|
|
91
94
|
child: Column(
|
|
92
95
|
mainAxisSize: MainAxisSize.min,
|
|
93
96
|
children: [
|
|
94
|
-
// drag handle
|
|
95
|
-
|
|
97
|
+
// drag handle (bottom-sheet form only)
|
|
98
|
+
if (!floating)
|
|
99
|
+
Padding(
|
|
96
100
|
padding: const EdgeInsets.only(top: KasySpacing.smd),
|
|
97
101
|
child: Container(
|
|
98
102
|
width: 36,
|
|
@@ -165,5 +169,11 @@ class _NotificationSettingsSheetState
|
|
|
165
169
|
],
|
|
166
170
|
),
|
|
167
171
|
);
|
|
172
|
+
|
|
173
|
+
if (!floating) return card;
|
|
174
|
+
return ConstrainedBox(
|
|
175
|
+
constraints: const BoxConstraints(maxWidth: 420),
|
|
176
|
+
child: card,
|
|
177
|
+
);
|
|
168
178
|
}
|
|
169
179
|
}
|
|
@@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart';
|
|
|
5
5
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
6
6
|
import 'package:kasy_kit/components/components.dart';
|
|
7
7
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
8
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
8
9
|
import 'package:kasy_kit/features/notifications/providers/models/notification.dart'
|
|
9
10
|
as app;
|
|
10
11
|
import 'package:kasy_kit/features/notifications/providers/models/notification_list.dart';
|
|
@@ -105,6 +106,8 @@ class _NotificationsPageState extends ConsumerState<NotificationsPage> {
|
|
|
105
106
|
return KasyOverlayScaffold(
|
|
106
107
|
title: t.notifications.title,
|
|
107
108
|
appBarStyle: KasyAppBarStyle.rootTab,
|
|
109
|
+
// Contain + center the list on desktop so cards never stretch edge-to-edge.
|
|
110
|
+
maxContentWidth: kKasyContentMaxWidth,
|
|
108
111
|
hideAppBarOnScroll: true,
|
|
109
112
|
scrollController: _scrollController,
|
|
110
113
|
trailing: Builder(
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:go_router/go_router.dart';
|
|
4
|
+
import 'package:kasy_kit/components/components.dart';
|
|
5
|
+
import 'package:kasy_kit/core/theme/theme.dart';
|
|
6
|
+
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
7
|
+
import 'package:kasy_kit/features/notifications/providers/models/notification.dart'
|
|
8
|
+
as app;
|
|
9
|
+
import 'package:kasy_kit/features/notifications/providers/notifications_provider.dart';
|
|
10
|
+
import 'package:kasy_kit/features/notifications/providers/unread_notifications_count_provider.dart';
|
|
11
|
+
import 'package:kasy_kit/features/notifications/ui/widgets/notification_tile.dart';
|
|
12
|
+
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
13
|
+
|
|
14
|
+
/// Desktop web header bell that opens a recent-notifications dropdown anchored
|
|
15
|
+
/// below it (Gmail / GitHub pattern). The full list still lives at
|
|
16
|
+
/// `/notifications` — the panel's "See all" link goes there.
|
|
17
|
+
///
|
|
18
|
+
/// Owns its own overlay (OverlayPortal + LayerLink, the same popover technique
|
|
19
|
+
/// as [KasySidebar]), so [KasyWebHeader] stays a pure presentational component
|
|
20
|
+
/// and the data wiring lives here.
|
|
21
|
+
class WebNotificationsBell extends ConsumerStatefulWidget {
|
|
22
|
+
const WebNotificationsBell({super.key});
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
ConsumerState<WebNotificationsBell> createState() =>
|
|
26
|
+
_WebNotificationsBellState();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class _WebNotificationsBellState extends ConsumerState<WebNotificationsBell> {
|
|
30
|
+
/// Max number of recent notifications shown in the panel before "See all".
|
|
31
|
+
static const int _maxPreview = 5;
|
|
32
|
+
|
|
33
|
+
/// Width of the dropdown panel.
|
|
34
|
+
static const double _panelWidth = 380;
|
|
35
|
+
|
|
36
|
+
final OverlayPortalController _controller = OverlayPortalController();
|
|
37
|
+
final LayerLink _link = LayerLink();
|
|
38
|
+
|
|
39
|
+
void _toggle() {
|
|
40
|
+
if (_controller.isShowing) {
|
|
41
|
+
_controller.hide();
|
|
42
|
+
} else {
|
|
43
|
+
_controller.show();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
void _openFull() {
|
|
48
|
+
_controller.hide();
|
|
49
|
+
context.go('/notifications');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
void _onTap(app.Notification notification) {
|
|
53
|
+
_controller.hide();
|
|
54
|
+
ref.read(notificationsProvider.notifier).onTapNotification(notification);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
Widget build(BuildContext context) {
|
|
59
|
+
final int unread = ref.watch(unreadNotificationsCountProvider).value ?? 0;
|
|
60
|
+
return OverlayPortal(
|
|
61
|
+
controller: _controller,
|
|
62
|
+
overlayChildBuilder: _buildPanel,
|
|
63
|
+
child: CompositedTransformTarget(
|
|
64
|
+
link: _link,
|
|
65
|
+
child: _bell(context, unread: unread),
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Widget _bell(BuildContext context, {required int unread}) {
|
|
71
|
+
final Widget button = KasyButton.iconOnly(
|
|
72
|
+
icon: KasyIcons.notification,
|
|
73
|
+
variant: KasyButtonVariant.ghost,
|
|
74
|
+
size: KasyButtonSize.small,
|
|
75
|
+
iconOnlyLayoutExtent: 36,
|
|
76
|
+
iconGlyphSize: KasyIconSize.md,
|
|
77
|
+
onPressed: _toggle,
|
|
78
|
+
semanticLabel: 'Notifications',
|
|
79
|
+
);
|
|
80
|
+
if (unread == 0) return button;
|
|
81
|
+
return Stack(
|
|
82
|
+
clipBehavior: Clip.none,
|
|
83
|
+
children: [
|
|
84
|
+
button,
|
|
85
|
+
Positioned(
|
|
86
|
+
top: 8,
|
|
87
|
+
right: 8,
|
|
88
|
+
child: Container(
|
|
89
|
+
width: 8,
|
|
90
|
+
height: 8,
|
|
91
|
+
decoration: BoxDecoration(
|
|
92
|
+
color: context.colors.error,
|
|
93
|
+
shape: BoxShape.circle,
|
|
94
|
+
border: Border.all(color: context.colors.background, width: 1.5),
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
],
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Widget _buildPanel(BuildContext context) {
|
|
103
|
+
// Full-screen barrier so a click anywhere outside the panel dismisses it.
|
|
104
|
+
return Stack(
|
|
105
|
+
children: [
|
|
106
|
+
Positioned.fill(
|
|
107
|
+
child: GestureDetector(
|
|
108
|
+
behavior: HitTestBehavior.opaque,
|
|
109
|
+
onTap: _controller.hide,
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
CompositedTransformFollower(
|
|
113
|
+
link: _link,
|
|
114
|
+
showWhenUnlinked: false,
|
|
115
|
+
targetAnchor: Alignment.bottomRight,
|
|
116
|
+
followerAnchor: Alignment.topRight,
|
|
117
|
+
// Small gap below the bell — the icon button already has internal
|
|
118
|
+
// padding, so 6 reads "attached to the bell" without looking glued.
|
|
119
|
+
offset: const Offset(0, 6),
|
|
120
|
+
child: Align(
|
|
121
|
+
alignment: Alignment.topRight,
|
|
122
|
+
child: _panelCard(context),
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
],
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
Widget _panelCard(BuildContext context) {
|
|
130
|
+
final KasyColors c = context.colors;
|
|
131
|
+
return Material(
|
|
132
|
+
color: Colors.transparent,
|
|
133
|
+
child: Container(
|
|
134
|
+
width: _panelWidth,
|
|
135
|
+
constraints: const BoxConstraints(maxHeight: 480),
|
|
136
|
+
decoration: BoxDecoration(
|
|
137
|
+
color: c.surface,
|
|
138
|
+
// Same hairline as the sidebar/header chrome: `c.border` at 0.5px, so
|
|
139
|
+
// the whole web chrome reads as one continuous border weight.
|
|
140
|
+
border: Border.all(color: c.border, width: 0.5),
|
|
141
|
+
borderRadius: KasyRadius.lgBorderRadius,
|
|
142
|
+
// Floating dropdown → the design-system overlay shadow (menus/popovers),
|
|
143
|
+
// not the inline-card `component` shadow (which is muted on web).
|
|
144
|
+
boxShadow: KasyShadows.overlay,
|
|
145
|
+
),
|
|
146
|
+
clipBehavior: Clip.antiAlias,
|
|
147
|
+
child: Column(
|
|
148
|
+
mainAxisSize: MainAxisSize.min,
|
|
149
|
+
children: [
|
|
150
|
+
_header(context),
|
|
151
|
+
Flexible(child: _body(context)),
|
|
152
|
+
Divider(height: 1, thickness: 1, color: c.border),
|
|
153
|
+
_footer(context),
|
|
154
|
+
],
|
|
155
|
+
),
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Widget _header(BuildContext context) {
|
|
161
|
+
final int unread = ref.watch(unreadNotificationsCountProvider).value ?? 0;
|
|
162
|
+
return Padding(
|
|
163
|
+
padding: const EdgeInsets.fromLTRB(
|
|
164
|
+
KasySpacing.md,
|
|
165
|
+
KasySpacing.smd,
|
|
166
|
+
KasySpacing.sm,
|
|
167
|
+
KasySpacing.smd,
|
|
168
|
+
),
|
|
169
|
+
child: Row(
|
|
170
|
+
children: [
|
|
171
|
+
Expanded(
|
|
172
|
+
child: Text(
|
|
173
|
+
t.notifications.title,
|
|
174
|
+
style: context.textTheme.titleSmall?.copyWith(
|
|
175
|
+
fontWeight: FontWeight.w600,
|
|
176
|
+
),
|
|
177
|
+
),
|
|
178
|
+
),
|
|
179
|
+
if (unread > 0)
|
|
180
|
+
KasyButton(
|
|
181
|
+
label: t.notifications.mark_all_read,
|
|
182
|
+
variant: KasyButtonVariant.ghost,
|
|
183
|
+
size: KasyButtonSize.small,
|
|
184
|
+
onPressed: () =>
|
|
185
|
+
ref.read(notificationsProvider.notifier).readAll(),
|
|
186
|
+
),
|
|
187
|
+
],
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
Widget _body(BuildContext context) {
|
|
193
|
+
final state = ref.watch(notificationsProvider);
|
|
194
|
+
return state.when(
|
|
195
|
+
loading: () => Column(
|
|
196
|
+
mainAxisSize: MainAxisSize.min,
|
|
197
|
+
children: List<Widget>.generate(
|
|
198
|
+
3,
|
|
199
|
+
(_) => const NotificationSkeletonTile(),
|
|
200
|
+
),
|
|
201
|
+
),
|
|
202
|
+
error: (_, _) => _message(context, t.notifications.error_fetching),
|
|
203
|
+
data: (list) {
|
|
204
|
+
if (list.data.isEmpty) {
|
|
205
|
+
return _message(context, t.notifications.empty_title);
|
|
206
|
+
}
|
|
207
|
+
final items = list.data.take(_maxPreview).toList();
|
|
208
|
+
return SingleChildScrollView(
|
|
209
|
+
child: Column(
|
|
210
|
+
mainAxisSize: MainAxisSize.min,
|
|
211
|
+
children: [
|
|
212
|
+
for (int i = 0; i < items.length; i++) ...[
|
|
213
|
+
if (i > 0)
|
|
214
|
+
Divider(height: 1, thickness: 1, color: context.colors.border),
|
|
215
|
+
KasyHover(
|
|
216
|
+
onTap: () => _onTap(items[i]),
|
|
217
|
+
padding: const EdgeInsets.symmetric(
|
|
218
|
+
horizontal: KasySpacing.md,
|
|
219
|
+
vertical: KasySpacing.smd,
|
|
220
|
+
),
|
|
221
|
+
child: NotificationTile.from(context, items[i]),
|
|
222
|
+
),
|
|
223
|
+
],
|
|
224
|
+
],
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
Widget _message(BuildContext context, String text) {
|
|
232
|
+
return Padding(
|
|
233
|
+
padding: const EdgeInsets.symmetric(
|
|
234
|
+
horizontal: KasySpacing.md,
|
|
235
|
+
vertical: KasySpacing.xl,
|
|
236
|
+
),
|
|
237
|
+
child: Center(
|
|
238
|
+
child: Text(
|
|
239
|
+
text,
|
|
240
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
241
|
+
color: context.colors.muted,
|
|
242
|
+
),
|
|
243
|
+
),
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
Widget _footer(BuildContext context) {
|
|
249
|
+
return Padding(
|
|
250
|
+
padding: const EdgeInsets.all(KasySpacing.sm),
|
|
251
|
+
child: SizedBox(
|
|
252
|
+
width: double.infinity,
|
|
253
|
+
child: KasyButton(
|
|
254
|
+
label: t.notifications.see_all,
|
|
255
|
+
variant: KasyButtonVariant.neutral,
|
|
256
|
+
size: KasyButtonSize.small,
|
|
257
|
+
onPressed: _openFull,
|
|
258
|
+
),
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -15,6 +15,7 @@ import 'package:kasy_kit/core/states/logout_action.dart';
|
|
|
15
15
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
16
16
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
17
17
|
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
18
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
18
19
|
import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
|
|
19
20
|
import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
|
|
20
21
|
import 'package:kasy_kit/features/settings/ui/components/avatar_component.dart';
|
|
@@ -580,6 +581,8 @@ class _SettingsDesktopView extends ConsumerStatefulWidget {
|
|
|
580
581
|
class _SettingsDesktopViewState extends ConsumerState<_SettingsDesktopView> {
|
|
581
582
|
_DesktopSection _selected = _DesktopSection.account;
|
|
582
583
|
|
|
584
|
+
void _select(_DesktopSection s) => setState(() => _selected = s);
|
|
585
|
+
|
|
583
586
|
List<_DesktopSection> get _sections => <_DesktopSection>[
|
|
584
587
|
_DesktopSection.account,
|
|
585
588
|
_DesktopSection.preferences,
|
|
@@ -593,47 +596,57 @@ class _SettingsDesktopViewState extends ConsumerState<_SettingsDesktopView> {
|
|
|
593
596
|
final List<_DesktopSection> sections = _sections;
|
|
594
597
|
if (!sections.contains(_selected)) _selected = sections.first;
|
|
595
598
|
|
|
599
|
+
final Widget pane = _DesktopDetail(
|
|
600
|
+
section: _selected,
|
|
601
|
+
userId: widget.userId,
|
|
602
|
+
name: widget.name,
|
|
603
|
+
editableName: widget.editableName,
|
|
604
|
+
email: widget.email,
|
|
605
|
+
isAuthenticated: widget.isAuthenticated,
|
|
606
|
+
isPhone: widget.isPhone,
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const double navWidth = 248;
|
|
610
|
+
const double gap = KasySpacing.xxl;
|
|
611
|
+
const double groupWidth = navWidth + gap + kKasyContentMaxWidth;
|
|
612
|
+
|
|
596
613
|
return Center(
|
|
597
|
-
child:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
child: _DesktopDetail(
|
|
624
|
-
section: _selected,
|
|
625
|
-
userId: widget.userId,
|
|
626
|
-
name: widget.name,
|
|
627
|
-
editableName: widget.editableName,
|
|
628
|
-
email: widget.email,
|
|
629
|
-
isAuthenticated: widget.isAuthenticated,
|
|
630
|
-
isPhone: widget.isPhone,
|
|
631
|
-
),
|
|
614
|
+
child: Padding(
|
|
615
|
+
padding: const EdgeInsets.only(
|
|
616
|
+
top: KasySpacing.lg,
|
|
617
|
+
bottom: KasySpacing.xxl,
|
|
618
|
+
),
|
|
619
|
+
// Center the nav + detail as ONE unit so the whitespace is equal on
|
|
620
|
+
// both sides (Linear/Notion settings), instead of pinning the nav left
|
|
621
|
+
// and leaving the right empty. Settings are forms, so the detail keeps
|
|
622
|
+
// a comfortable reading width rather than stretching wide. On tight
|
|
623
|
+
// desktop widths the detail fills the remaining space so it never
|
|
624
|
+
// overflows.
|
|
625
|
+
child: LayoutBuilder(
|
|
626
|
+
builder: (context, constraints) {
|
|
627
|
+
final bool fits = constraints.maxWidth >= groupWidth;
|
|
628
|
+
return Row(
|
|
629
|
+
mainAxisSize: fits ? MainAxisSize.min : MainAxisSize.max,
|
|
630
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
631
|
+
children: [
|
|
632
|
+
SizedBox(
|
|
633
|
+
width: navWidth,
|
|
634
|
+
child: _DesktopNav(
|
|
635
|
+
sections: sections,
|
|
636
|
+
selected: _selected,
|
|
637
|
+
name: widget.name,
|
|
638
|
+
email: widget.email,
|
|
639
|
+
onSelect: _select,
|
|
632
640
|
),
|
|
633
641
|
),
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
642
|
+
const SizedBox(width: gap),
|
|
643
|
+
if (fits)
|
|
644
|
+
SizedBox(width: kKasyContentMaxWidth, child: pane)
|
|
645
|
+
else
|
|
646
|
+
Expanded(child: pane),
|
|
647
|
+
],
|
|
648
|
+
);
|
|
649
|
+
},
|
|
637
650
|
),
|
|
638
651
|
),
|
|
639
652
|
);
|