kasy-cli 1.38.0 → 1.39.1
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/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/AGENTS.md +2 -2
- package/templates/firebase/DESIGN_SYSTEM.md +23 -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 +5 -2
- package/templates/firebase/lib/components/kasy_app_bar.dart +325 -15
- 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 +34 -16
- 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 +95 -30
- 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 +66 -36
- 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 +263 -59
- 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_home_widgets.dart +4 -0
- 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 +753 -712
- package/templates/firebase/lib/i18n/es.i18n.json +753 -712
- package/templates/firebase/lib/i18n/pt.i18n.json +753 -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/app_bar_config_test.dart +70 -0
- 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
|
@@ -3,8 +3,14 @@ import 'dart:math';
|
|
|
3
3
|
import 'package:flutter/material.dart';
|
|
4
4
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
5
5
|
import 'package:kasy_kit/components/kasy_avatar.dart';
|
|
6
|
+
import 'package:kasy_kit/components/kasy_button.dart';
|
|
7
|
+
import 'package:kasy_kit/components/kasy_card.dart';
|
|
8
|
+
import 'package:kasy_kit/components/kasy_drop_down.dart';
|
|
9
|
+
import 'package:kasy_kit/components/kasy_skeleton.dart';
|
|
6
10
|
import 'package:kasy_kit/components/kasy_status_tag.dart';
|
|
11
|
+
import 'package:kasy_kit/components/kasy_text_field.dart';
|
|
7
12
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
13
|
+
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
8
14
|
import 'package:kasy_kit/features/settings/ui/components/admin/admin_users_api.dart';
|
|
9
15
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
10
16
|
|
|
@@ -120,50 +126,90 @@ class _AdminUsersTabState extends ConsumerState<AdminUsersTab> {
|
|
|
120
126
|
final async = ref.watch(_usersResultProvider);
|
|
121
127
|
|
|
122
128
|
return _HCenter(
|
|
123
|
-
child:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
final
|
|
128
|
-
final
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
129
|
+
child: LayoutBuilder(
|
|
130
|
+
builder: (context, constraints) {
|
|
131
|
+
// Mobile: a horizontal table is cramped, so each user becomes a
|
|
132
|
+
// vertical card. Tablet/desktop keep the denser table.
|
|
133
|
+
final bool cards = constraints.maxWidth < 560;
|
|
134
|
+
final Widget content = async.when(
|
|
135
|
+
// Show the skeleton on a manual refresh too, so tapping Refresh
|
|
136
|
+
// gives clear feedback even when nothing actually changed.
|
|
137
|
+
skipLoadingOnRefresh: false,
|
|
138
|
+
loading: () => _LoadingState(cards: cards),
|
|
139
|
+
error: (e, _) => _ErrorState(onRetry: _refresh),
|
|
140
|
+
data: (result) {
|
|
141
|
+
final processed = _process(result.users);
|
|
142
|
+
final total = processed.length;
|
|
143
|
+
final pageCount = max(1, (total / _pageSize).ceil());
|
|
144
|
+
final page = _page.clamp(0, pageCount - 1);
|
|
145
|
+
final start = page * _pageSize;
|
|
146
|
+
final end = min(start + _pageSize, total);
|
|
147
|
+
final visible = processed.sublist(start, end);
|
|
148
|
+
|
|
149
|
+
return Column(
|
|
150
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
151
|
+
children: [
|
|
152
|
+
_Toolbar(
|
|
153
|
+
controller: _searchCtrl,
|
|
154
|
+
resultCount: total,
|
|
155
|
+
subscribersOnly: _subscribersOnly,
|
|
156
|
+
onSearch: _onSearch,
|
|
157
|
+
onSubscribersOnly: _setSubscribersOnly,
|
|
158
|
+
onRefresh: _refresh,
|
|
159
|
+
),
|
|
160
|
+
if (result.truncated)
|
|
161
|
+
_TruncatedNote(count: result.users.length),
|
|
162
|
+
// Column headers only make sense for the table layout.
|
|
163
|
+
if (!cards)
|
|
164
|
+
_TableHeader(sortCol: _sortCol, asc: _asc, onSort: _onSort)
|
|
165
|
+
else
|
|
166
|
+
const SizedBox(height: KasySpacing.md),
|
|
167
|
+
Expanded(
|
|
168
|
+
child: visible.isEmpty
|
|
169
|
+
? _EmptyState(searching: _search.trim().isNotEmpty)
|
|
170
|
+
: _TableBody(
|
|
171
|
+
users: visible,
|
|
172
|
+
cards: cards,
|
|
173
|
+
rangeFrom: start + 1,
|
|
174
|
+
rangeTo: end,
|
|
175
|
+
total: total,
|
|
176
|
+
page: page,
|
|
177
|
+
pageCount: pageCount,
|
|
178
|
+
onPage: (p) => setState(() => _page = p),
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
],
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Mobile keeps the borderless cards (they carry their own surface).
|
|
187
|
+
// Tablet/desktop wrap the whole table in a surface panel so it reads
|
|
188
|
+
// as a deliberate card, not loose rows on the page background.
|
|
189
|
+
if (cards) return content;
|
|
190
|
+
return Padding(
|
|
191
|
+
padding: const EdgeInsets.fromLTRB(
|
|
192
|
+
KasySpacing.pageHorizontalGutter,
|
|
193
|
+
KasySpacing.belowChromeContentGap,
|
|
194
|
+
KasySpacing.pageHorizontalGutter,
|
|
195
|
+
KasySpacing.md,
|
|
196
|
+
),
|
|
197
|
+
child: DecoratedBox(
|
|
198
|
+
decoration: BoxDecoration(
|
|
199
|
+
color: context.colors.surface,
|
|
200
|
+
borderRadius: BorderRadius.circular(18),
|
|
201
|
+
border: Border.all(
|
|
202
|
+
color: context.colors.outline.withValues(
|
|
203
|
+
alpha: context.isDark ? 0.45 : 0.6,
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
boxShadow: [KasyShadows.component(context)],
|
|
152
207
|
),
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
: _TableBody(
|
|
157
|
-
users: visible,
|
|
158
|
-
rangeFrom: start + 1,
|
|
159
|
-
rangeTo: end,
|
|
160
|
-
total: total,
|
|
161
|
-
page: page,
|
|
162
|
-
pageCount: pageCount,
|
|
163
|
-
onPage: (p) => setState(() => _page = p),
|
|
164
|
-
),
|
|
208
|
+
child: ClipRRect(
|
|
209
|
+
borderRadius: BorderRadius.circular(18),
|
|
210
|
+
child: content,
|
|
165
211
|
),
|
|
166
|
-
|
|
212
|
+
),
|
|
167
213
|
);
|
|
168
214
|
},
|
|
169
215
|
),
|
|
@@ -354,89 +400,20 @@ class _FilterButton extends StatelessWidget {
|
|
|
354
400
|
@override
|
|
355
401
|
Widget build(BuildContext context) {
|
|
356
402
|
final u = t.admin_console.users;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
color: context.colors.surface,
|
|
370
|
-
elevation: 6,
|
|
371
|
-
shadowColor: Colors.black.withValues(alpha: 0.12),
|
|
372
|
-
shape: RoundedRectangleBorder(
|
|
373
|
-
borderRadius: BorderRadius.circular(KasyRadius.md),
|
|
374
|
-
side: BorderSide(color: context.colors.outline.withValues(alpha: 0.5)),
|
|
375
|
-
),
|
|
376
|
-
itemBuilder: (_) => [
|
|
377
|
-
_filterItem(context, value: false, label: u.filter_all, selected: !active),
|
|
378
|
-
_filterItem(context, value: true, label: u.filter_subscribers, selected: active),
|
|
379
|
-
],
|
|
380
|
-
child: Container(
|
|
381
|
-
width: _filterWidth,
|
|
382
|
-
height: 38,
|
|
383
|
-
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
384
|
-
decoration: BoxDecoration(
|
|
385
|
-
color: active
|
|
386
|
-
? context.colors.primary.withValues(alpha: 0.10)
|
|
387
|
-
: context.colors.surface,
|
|
388
|
-
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
389
|
-
border: Border.all(
|
|
390
|
-
color: active
|
|
391
|
-
? context.colors.primary.withValues(alpha: 0.45)
|
|
392
|
-
: context.colors.outline.withValues(alpha: 0.5),
|
|
393
|
-
),
|
|
394
|
-
),
|
|
395
|
-
child: Row(
|
|
396
|
-
children: [
|
|
397
|
-
Icon(Icons.tune_rounded, size: 16, color: accent),
|
|
398
|
-
const SizedBox(width: 8),
|
|
399
|
-
Expanded(
|
|
400
|
-
child: Text(
|
|
401
|
-
active ? u.filter_subscribers : u.filter_all,
|
|
402
|
-
maxLines: 1,
|
|
403
|
-
overflow: TextOverflow.ellipsis,
|
|
404
|
-
style: context.textTheme.bodySmall?.copyWith(
|
|
405
|
-
color: accent,
|
|
406
|
-
fontWeight: FontWeight.w600,
|
|
407
|
-
),
|
|
408
|
-
),
|
|
409
|
-
),
|
|
410
|
-
Icon(Icons.keyboard_arrow_down_rounded, size: 18, color: accent),
|
|
411
|
-
],
|
|
412
|
-
),
|
|
413
|
-
),
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
PopupMenuItem<bool> _filterItem(
|
|
418
|
-
BuildContext context, {
|
|
419
|
-
required bool value,
|
|
420
|
-
required String label,
|
|
421
|
-
required bool selected,
|
|
422
|
-
}) {
|
|
423
|
-
return PopupMenuItem<bool>(
|
|
424
|
-
value: value,
|
|
425
|
-
height: 42,
|
|
426
|
-
child: Row(
|
|
427
|
-
children: [
|
|
428
|
-
Expanded(
|
|
429
|
-
child: Text(
|
|
430
|
-
label,
|
|
431
|
-
style: context.textTheme.bodyMedium?.copyWith(
|
|
432
|
-
color: context.colors.onSurface,
|
|
433
|
-
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
|
|
434
|
-
),
|
|
435
|
-
),
|
|
436
|
-
),
|
|
437
|
-
if (selected)
|
|
438
|
-
Icon(Icons.check_rounded, size: 16, color: context.colors.primary),
|
|
403
|
+
// Kit dropdown: the floating panel lives under the app Overlay so it always
|
|
404
|
+
// follows the real light/dark theme (the old PopupMenuButton rendered the
|
|
405
|
+
// menu light even in dark mode).
|
|
406
|
+
return SizedBox(
|
|
407
|
+
width: _filterWidth,
|
|
408
|
+
child: KasyDropDown<bool>(
|
|
409
|
+
value: subscribersOnly,
|
|
410
|
+
leadingIcon: Icons.tune_rounded,
|
|
411
|
+
variant: KasyTextFieldVariant.flat,
|
|
412
|
+
items: [
|
|
413
|
+
KasyDropDownItem(value: false, label: u.filter_all),
|
|
414
|
+
KasyDropDownItem(value: true, label: u.filter_subscribers),
|
|
439
415
|
],
|
|
416
|
+
onChanged: onChanged,
|
|
440
417
|
),
|
|
441
418
|
);
|
|
442
419
|
}
|
|
@@ -455,23 +432,18 @@ class _IconAction extends StatelessWidget {
|
|
|
455
432
|
@override
|
|
456
433
|
Widget build(BuildContext context) {
|
|
457
434
|
// Icon-only control: a tooltip is the right call here (no visible label).
|
|
435
|
+
// KasyButton.iconOnly gives the kit's outlined icon button with proper web
|
|
436
|
+
// hover, focus ring and cursor (no Material ripple).
|
|
458
437
|
return Tooltip(
|
|
459
438
|
message: tooltip,
|
|
460
|
-
child:
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
469
|
-
border: Border.all(
|
|
470
|
-
color: context.colors.outline.withValues(alpha: 0.5),
|
|
471
|
-
),
|
|
472
|
-
),
|
|
473
|
-
child: Icon(icon, size: 18, color: context.colors.onSurface),
|
|
474
|
-
),
|
|
439
|
+
child: KasyButton.iconOnly(
|
|
440
|
+
icon: icon,
|
|
441
|
+
variant: KasyButtonVariant.outline,
|
|
442
|
+
size: KasyButtonSize.small,
|
|
443
|
+
iconOnlyLayoutExtent: 38,
|
|
444
|
+
iconGlyphSize: 18,
|
|
445
|
+
onPressed: onTap,
|
|
446
|
+
semanticLabel: tooltip,
|
|
475
447
|
),
|
|
476
448
|
);
|
|
477
449
|
}
|
|
@@ -533,9 +505,9 @@ class _TableHeader extends StatelessWidget {
|
|
|
533
505
|
bottom:
|
|
534
506
|
BorderSide(color: context.colors.outline.withValues(alpha: 0.3)),
|
|
535
507
|
),
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
508
|
+
// Design-system neutral surface (solid) so the header band reads as a
|
|
509
|
+
// distinct table head instead of blending into the white panel.
|
|
510
|
+
color: context.colors.surfaceNeutralSoft,
|
|
539
511
|
),
|
|
540
512
|
padding: const EdgeInsets.symmetric(
|
|
541
513
|
horizontal: KasySpacing.pageHorizontalGutter,
|
|
@@ -600,8 +572,9 @@ class _HeaderCell extends StatelessWidget {
|
|
|
600
572
|
@override
|
|
601
573
|
Widget build(BuildContext context) {
|
|
602
574
|
final color = active ? context.colors.onSurface : context.colors.muted;
|
|
603
|
-
return
|
|
575
|
+
return KasyHover(
|
|
604
576
|
onTap: onTap,
|
|
577
|
+
focusable: true,
|
|
605
578
|
borderRadius: BorderRadius.circular(KasyRadius.xs),
|
|
606
579
|
child: Padding(
|
|
607
580
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
@@ -640,6 +613,7 @@ class _HeaderCell extends StatelessWidget {
|
|
|
640
613
|
|
|
641
614
|
class _TableBody extends StatelessWidget {
|
|
642
615
|
final List<AdminUser> users;
|
|
616
|
+
final bool cards;
|
|
643
617
|
final int rangeFrom;
|
|
644
618
|
final int rangeTo;
|
|
645
619
|
final int total;
|
|
@@ -649,6 +623,7 @@ class _TableBody extends StatelessWidget {
|
|
|
649
623
|
|
|
650
624
|
const _TableBody({
|
|
651
625
|
required this.users,
|
|
626
|
+
required this.cards,
|
|
652
627
|
required this.rangeFrom,
|
|
653
628
|
required this.rangeTo,
|
|
654
629
|
required this.total,
|
|
@@ -663,49 +638,77 @@ class _TableBody extends StatelessWidget {
|
|
|
663
638
|
return Column(
|
|
664
639
|
children: [
|
|
665
640
|
Expanded(
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
641
|
+
// Soft fade at the bottom edge so rows/cards dissolve into the pager
|
|
642
|
+
// instead of being sliced by a hard line — reads far cleaner when the
|
|
643
|
+
// last item is mid-scroll behind the pinned footer.
|
|
644
|
+
child: ShaderMask(
|
|
645
|
+
shaderCallback: (rect) => const LinearGradient(
|
|
646
|
+
begin: Alignment.bottomCenter,
|
|
647
|
+
end: Alignment.topCenter,
|
|
648
|
+
colors: [Colors.transparent, Colors.black],
|
|
649
|
+
stops: [0.0, 0.045],
|
|
650
|
+
).createShader(rect),
|
|
651
|
+
blendMode: BlendMode.dstIn,
|
|
652
|
+
child: ListView.separated(
|
|
653
|
+
// Scrolling the results means the user stopped typing in the
|
|
654
|
+
// search field above, so dismiss the keyboard to show more rows.
|
|
655
|
+
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
|
656
|
+
padding: cards
|
|
657
|
+
? const EdgeInsets.fromLTRB(
|
|
658
|
+
KasySpacing.pageHorizontalGutter,
|
|
659
|
+
0,
|
|
660
|
+
KasySpacing.pageHorizontalGutter,
|
|
661
|
+
KasySpacing.lg,
|
|
662
|
+
)
|
|
663
|
+
: const EdgeInsets.only(bottom: KasySpacing.md),
|
|
664
|
+
itemCount: users.length,
|
|
665
|
+
separatorBuilder: cards
|
|
666
|
+
? (_, _) => const SizedBox(height: KasySpacing.sm)
|
|
667
|
+
: (_, _) => Divider(
|
|
668
|
+
height: 1,
|
|
669
|
+
thickness: 1,
|
|
670
|
+
indent: KasySpacing.pageHorizontalGutter,
|
|
671
|
+
endIndent: KasySpacing.pageHorizontalGutter,
|
|
672
|
+
color: context.colors.outline.withValues(alpha: 0.18),
|
|
673
|
+
),
|
|
674
|
+
itemBuilder: (_, i) =>
|
|
675
|
+
cards ? _UserCard(user: users[i]) : _UserRow(user: users[i]),
|
|
678
676
|
),
|
|
679
|
-
itemBuilder: (_, i) => _UserRow(user: users[i]),
|
|
680
677
|
),
|
|
681
678
|
),
|
|
682
679
|
Container(
|
|
683
680
|
decoration: BoxDecoration(
|
|
684
681
|
border: Border(
|
|
685
682
|
top: BorderSide(
|
|
686
|
-
color: context.colors.outline.withValues(alpha: 0.
|
|
683
|
+
color: context.colors.outline.withValues(alpha: 0.15),
|
|
687
684
|
),
|
|
688
685
|
),
|
|
689
686
|
),
|
|
690
687
|
padding: EdgeInsets.fromLTRB(
|
|
691
688
|
KasySpacing.pageHorizontalGutter,
|
|
692
|
-
KasySpacing.
|
|
689
|
+
KasySpacing.md,
|
|
693
690
|
KasySpacing.pageHorizontalGutter,
|
|
694
691
|
MediaQuery.paddingOf(context).bottom + KasySpacing.xl,
|
|
695
692
|
),
|
|
696
693
|
child: LayoutBuilder(
|
|
697
694
|
builder: (context, c) {
|
|
698
|
-
final pager = _Pagination(
|
|
699
|
-
page: page,
|
|
700
|
-
pageCount: pageCount,
|
|
701
|
-
onPage: onPage,
|
|
702
|
-
);
|
|
703
695
|
final label = Text(
|
|
704
696
|
u.results(from: rangeFrom, to: rangeTo, total: total),
|
|
705
697
|
style: context.textTheme.bodySmall?.copyWith(
|
|
706
698
|
color: context.colors.muted,
|
|
707
699
|
),
|
|
708
700
|
);
|
|
701
|
+
// A single page has no "previous/next" to offer — showing the
|
|
702
|
+
// disabled controls is just dead weight. Keep only the quiet
|
|
703
|
+
// count, centered, so the bar stays clean (pro dashboard style).
|
|
704
|
+
if (pageCount <= 1) {
|
|
705
|
+
return Center(child: label);
|
|
706
|
+
}
|
|
707
|
+
final pager = _Pagination(
|
|
708
|
+
page: page,
|
|
709
|
+
pageCount: pageCount,
|
|
710
|
+
onPage: onPage,
|
|
711
|
+
);
|
|
709
712
|
if (c.maxWidth < 460) {
|
|
710
713
|
return Column(
|
|
711
714
|
children: [
|
|
@@ -802,32 +805,38 @@ class _PageNum extends StatelessWidget {
|
|
|
802
805
|
|
|
803
806
|
@override
|
|
804
807
|
Widget build(BuildContext context) {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
: context.colors.outline.withValues(alpha: 0.5),
|
|
820
|
-
),
|
|
808
|
+
final Widget content = Container(
|
|
809
|
+
constraints: const BoxConstraints(minWidth: 32),
|
|
810
|
+
height: 32,
|
|
811
|
+
alignment: Alignment.center,
|
|
812
|
+
padding: const EdgeInsets.symmetric(horizontal: 6),
|
|
813
|
+
decoration: BoxDecoration(
|
|
814
|
+
// Unselected pages stay transparent (border only) so KasyHover's overlay
|
|
815
|
+
// shows through on web hover; the current page keeps the solid primary.
|
|
816
|
+
color: selected ? context.colors.primary : Colors.transparent,
|
|
817
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
818
|
+
border: Border.all(
|
|
819
|
+
color: selected
|
|
820
|
+
? context.colors.primary
|
|
821
|
+
: context.colors.outline.withValues(alpha: 0.5),
|
|
821
822
|
),
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
823
|
+
),
|
|
824
|
+
child: Text(
|
|
825
|
+
'$number',
|
|
826
|
+
style: context.textTheme.labelMedium?.copyWith(
|
|
827
|
+
color: selected ? context.colors.onPrimary : context.colors.onSurface,
|
|
828
|
+
fontWeight: FontWeight.w700,
|
|
828
829
|
),
|
|
829
830
|
),
|
|
830
831
|
);
|
|
832
|
+
// Current page: not tappable, no hover. Other pages get the kit hover.
|
|
833
|
+
if (selected) return content;
|
|
834
|
+
return KasyHover(
|
|
835
|
+
onTap: onTap,
|
|
836
|
+
focusable: true,
|
|
837
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
838
|
+
child: content,
|
|
839
|
+
);
|
|
831
840
|
}
|
|
832
841
|
}
|
|
833
842
|
|
|
@@ -877,23 +886,27 @@ class _NavBtn extends StatelessWidget {
|
|
|
877
886
|
),
|
|
878
887
|
);
|
|
879
888
|
final ic = Icon(icon, size: 18, color: color);
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
888
|
-
border: Border.all(
|
|
889
|
-
color: context.colors.outline.withValues(alpha: enabled ? 0.5 : 0.25),
|
|
890
|
-
),
|
|
891
|
-
),
|
|
892
|
-
child: Row(
|
|
893
|
-
mainAxisSize: MainAxisSize.min,
|
|
894
|
-
children: leading ? [ic, const SizedBox(width: 2), text] : [text, const SizedBox(width: 2), ic],
|
|
889
|
+
final Widget content = Container(
|
|
890
|
+
height: 32,
|
|
891
|
+
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
892
|
+
decoration: BoxDecoration(
|
|
893
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
894
|
+
border: Border.all(
|
|
895
|
+
color: context.colors.outline.withValues(alpha: enabled ? 0.5 : 0.25),
|
|
895
896
|
),
|
|
896
897
|
),
|
|
898
|
+
child: Row(
|
|
899
|
+
mainAxisSize: MainAxisSize.min,
|
|
900
|
+
children: leading ? [ic, const SizedBox(width: 2), text] : [text, const SizedBox(width: 2), ic],
|
|
901
|
+
),
|
|
902
|
+
);
|
|
903
|
+
// Disabled (prev/next at the list edges): no hover, cursor or tap.
|
|
904
|
+
if (!enabled) return content;
|
|
905
|
+
return KasyHover(
|
|
906
|
+
onTap: onTap,
|
|
907
|
+
focusable: true,
|
|
908
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
909
|
+
child: content,
|
|
897
910
|
);
|
|
898
911
|
}
|
|
899
912
|
}
|
|
@@ -943,6 +956,7 @@ class _UserRow extends StatelessWidget {
|
|
|
943
956
|
children: [
|
|
944
957
|
KasyAvatar(
|
|
945
958
|
diameter: 34,
|
|
959
|
+
image: _avatarImage(user),
|
|
946
960
|
initials: hasName ? user.name : (hasEmail ? user.email : null),
|
|
947
961
|
tone: _avatarTone(
|
|
948
962
|
hasName ? user.name : (hasEmail ? user.email : null),
|
|
@@ -1014,7 +1028,7 @@ class _UserRow extends StatelessWidget {
|
|
|
1014
1028
|
SizedBox(
|
|
1015
1029
|
width: 88,
|
|
1016
1030
|
child: Text(
|
|
1017
|
-
user.createdAt != null ?
|
|
1031
|
+
user.createdAt != null ? _formatJoined(user.createdAt!) : '—',
|
|
1018
1032
|
style: context.textTheme.bodySmall?.copyWith(
|
|
1019
1033
|
color: context.colors.muted,
|
|
1020
1034
|
),
|
|
@@ -1025,14 +1039,128 @@ class _UserRow extends StatelessWidget {
|
|
|
1025
1039
|
);
|
|
1026
1040
|
}
|
|
1027
1041
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/// Mobile card for a single user — the same data as a table row, stacked
|
|
1045
|
+
/// vertically so it reads comfortably on a narrow screen.
|
|
1046
|
+
class _UserCard extends StatelessWidget {
|
|
1047
|
+
final AdminUser user;
|
|
1048
|
+
const _UserCard({required this.user});
|
|
1049
|
+
|
|
1050
|
+
@override
|
|
1051
|
+
Widget build(BuildContext context) {
|
|
1052
|
+
final u = t.admin_console.users;
|
|
1053
|
+
final bool hasName = user.name?.isNotEmpty == true;
|
|
1054
|
+
final bool hasEmail = user.email?.isNotEmpty == true;
|
|
1055
|
+
final String primaryText =
|
|
1056
|
+
hasName ? user.name! : (hasEmail ? user.email! : u.anonymous);
|
|
1057
|
+
final String? subText = hasName && hasEmail ? user.email : null;
|
|
1058
|
+
final bool isAnonymous = !hasName && !hasEmail;
|
|
1059
|
+
|
|
1060
|
+
return KasyCard(
|
|
1061
|
+
// Default elevated (white surface + shadow over the F5F5F5 page) so the
|
|
1062
|
+
// card reads as a real card, not a transparent outline that blends into
|
|
1063
|
+
// the screen.
|
|
1064
|
+
padding: const EdgeInsets.all(KasySpacing.md),
|
|
1065
|
+
child: Column(
|
|
1066
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1067
|
+
children: [
|
|
1068
|
+
Row(
|
|
1069
|
+
children: [
|
|
1070
|
+
KasyAvatar(
|
|
1071
|
+
diameter: 38,
|
|
1072
|
+
image: _avatarImage(user),
|
|
1073
|
+
initials: hasName ? user.name : (hasEmail ? user.email : null),
|
|
1074
|
+
tone: _avatarTone(
|
|
1075
|
+
hasName ? user.name : (hasEmail ? user.email : null),
|
|
1076
|
+
),
|
|
1077
|
+
),
|
|
1078
|
+
const SizedBox(width: KasySpacing.sm),
|
|
1079
|
+
Expanded(
|
|
1080
|
+
child: Column(
|
|
1081
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1082
|
+
mainAxisSize: MainAxisSize.min,
|
|
1083
|
+
children: [
|
|
1084
|
+
Text(
|
|
1085
|
+
primaryText,
|
|
1086
|
+
maxLines: 1,
|
|
1087
|
+
overflow: TextOverflow.ellipsis,
|
|
1088
|
+
style: isAnonymous
|
|
1089
|
+
? context.textTheme.bodyMedium?.copyWith(
|
|
1090
|
+
color: context.colors.muted,
|
|
1091
|
+
fontStyle: FontStyle.italic,
|
|
1092
|
+
)
|
|
1093
|
+
: context.kasyTextTheme.rowTitle.copyWith(
|
|
1094
|
+
color: context.colors.onSurface,
|
|
1095
|
+
),
|
|
1096
|
+
),
|
|
1097
|
+
if (subText != null) ...[
|
|
1098
|
+
const SizedBox(height: 1),
|
|
1099
|
+
Text(
|
|
1100
|
+
subText,
|
|
1101
|
+
maxLines: 1,
|
|
1102
|
+
overflow: TextOverflow.ellipsis,
|
|
1103
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
1104
|
+
color: context.colors.muted,
|
|
1105
|
+
),
|
|
1106
|
+
),
|
|
1107
|
+
],
|
|
1108
|
+
],
|
|
1109
|
+
),
|
|
1110
|
+
),
|
|
1111
|
+
],
|
|
1112
|
+
),
|
|
1113
|
+
const SizedBox(height: KasySpacing.smd),
|
|
1114
|
+
Divider(
|
|
1115
|
+
height: 1,
|
|
1116
|
+
color: context.colors.outline.withValues(alpha: 0.25),
|
|
1117
|
+
),
|
|
1118
|
+
const SizedBox(height: KasySpacing.smd),
|
|
1119
|
+
Row(
|
|
1120
|
+
children: [
|
|
1121
|
+
Expanded(
|
|
1122
|
+
child: Wrap(
|
|
1123
|
+
spacing: KasySpacing.xs,
|
|
1124
|
+
runSpacing: KasySpacing.xs,
|
|
1125
|
+
children: [
|
|
1126
|
+
KasyStatusTag(
|
|
1127
|
+
label: hasEmail ? u.status_active : u.status_inactive,
|
|
1128
|
+
tone: hasEmail
|
|
1129
|
+
? KasyStatusTagTone.success
|
|
1130
|
+
: KasyStatusTagTone.neutral,
|
|
1131
|
+
),
|
|
1132
|
+
KasyStatusTag(
|
|
1133
|
+
label: user.subscriber ? u.plan_subscriber : u.plan_free,
|
|
1134
|
+
tone: user.subscriber
|
|
1135
|
+
? KasyStatusTagTone.primary
|
|
1136
|
+
: KasyStatusTagTone.neutral,
|
|
1137
|
+
),
|
|
1138
|
+
],
|
|
1139
|
+
),
|
|
1140
|
+
),
|
|
1141
|
+
const SizedBox(width: KasySpacing.sm),
|
|
1142
|
+
Text(
|
|
1143
|
+
user.createdAt != null ? _formatJoined(user.createdAt!) : '—',
|
|
1144
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
1145
|
+
color: context.colors.muted,
|
|
1146
|
+
),
|
|
1147
|
+
),
|
|
1148
|
+
],
|
|
1149
|
+
),
|
|
1150
|
+
],
|
|
1151
|
+
),
|
|
1152
|
+
);
|
|
1033
1153
|
}
|
|
1034
1154
|
}
|
|
1035
1155
|
|
|
1156
|
+
/// Shared joined-date formatter (dd/MM/yy) for the table row and mobile card.
|
|
1157
|
+
String _formatJoined(DateTime dt) {
|
|
1158
|
+
final d = dt.toLocal();
|
|
1159
|
+
return '${d.day.toString().padLeft(2, '0')}/'
|
|
1160
|
+
'${d.month.toString().padLeft(2, '0')}/'
|
|
1161
|
+
'${d.year.toString().substring(2)}';
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1036
1164
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1037
1165
|
// Avatar tone — a stable design-system tone per user (variety, not random).
|
|
1038
1166
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1049,6 +1177,14 @@ KasyAvatarTone _avatarTone(String? seed) {
|
|
|
1049
1177
|
return tones[seed.trimLeft().codeUnitAt(0) % tones.length];
|
|
1050
1178
|
}
|
|
1051
1179
|
|
|
1180
|
+
/// The user's photo when they have one, else null so [KasyAvatar] falls back to
|
|
1181
|
+
/// initials. The avatarPath is the public URL the backend already stores.
|
|
1182
|
+
ImageProvider? _avatarImage(AdminUser user) {
|
|
1183
|
+
final path = user.avatarPath;
|
|
1184
|
+
if (path == null || path.isEmpty) return null;
|
|
1185
|
+
return NetworkImage(path);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1052
1188
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1053
1189
|
// Loading / empty / error states (big icon over text)
|
|
1054
1190
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1123,18 +1259,109 @@ class _IconBubble extends StatelessWidget {
|
|
|
1123
1259
|
}
|
|
1124
1260
|
}
|
|
1125
1261
|
|
|
1262
|
+
/// Skeleton placeholder that mirrors the real layout (table rows on wide
|
|
1263
|
+
/// viewports, cards on mobile) so loading reads as the product, not a spinner.
|
|
1126
1264
|
class _LoadingState extends StatelessWidget {
|
|
1127
|
-
|
|
1265
|
+
final bool cards;
|
|
1266
|
+
const _LoadingState({required this.cards});
|
|
1128
1267
|
|
|
1129
1268
|
@override
|
|
1130
1269
|
Widget build(BuildContext context) {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1270
|
+
final int count = cards ? 5 : 7;
|
|
1271
|
+
return KasySkeletonGroup(
|
|
1272
|
+
child: SingleChildScrollView(
|
|
1273
|
+
physics: const NeverScrollableScrollPhysics(),
|
|
1274
|
+
padding: const EdgeInsets.fromLTRB(
|
|
1275
|
+
KasySpacing.pageHorizontalGutter,
|
|
1276
|
+
KasySpacing.lg,
|
|
1277
|
+
KasySpacing.pageHorizontalGutter,
|
|
1278
|
+
0,
|
|
1279
|
+
),
|
|
1280
|
+
child: Column(
|
|
1281
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
1282
|
+
children: [
|
|
1283
|
+
for (int i = 0; i < count; i++) ...[
|
|
1284
|
+
if (cards)
|
|
1285
|
+
const _UserCardSkeleton()
|
|
1286
|
+
else
|
|
1287
|
+
const _UserRowSkeleton(),
|
|
1288
|
+
SizedBox(height: cards ? KasySpacing.sm : KasySpacing.md),
|
|
1289
|
+
],
|
|
1290
|
+
],
|
|
1291
|
+
),
|
|
1292
|
+
),
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
class _UserRowSkeleton extends StatelessWidget {
|
|
1298
|
+
const _UserRowSkeleton();
|
|
1299
|
+
|
|
1300
|
+
@override
|
|
1301
|
+
Widget build(BuildContext context) {
|
|
1302
|
+
return const Row(
|
|
1303
|
+
children: [
|
|
1304
|
+
KasySkeleton.circle(size: 34),
|
|
1305
|
+
SizedBox(width: 10),
|
|
1306
|
+
Expanded(
|
|
1307
|
+
child: Column(
|
|
1308
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1309
|
+
mainAxisSize: MainAxisSize.min,
|
|
1310
|
+
children: [
|
|
1311
|
+
KasySkeleton(width: 140, height: 12),
|
|
1312
|
+
SizedBox(height: 6),
|
|
1313
|
+
KasySkeleton(width: 90, height: 10),
|
|
1314
|
+
],
|
|
1315
|
+
),
|
|
1316
|
+
),
|
|
1317
|
+
SizedBox(width: 12),
|
|
1318
|
+
KasySkeleton(width: 56, height: 20),
|
|
1319
|
+
SizedBox(width: 12),
|
|
1320
|
+
KasySkeleton(width: 40, height: 12),
|
|
1321
|
+
],
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
class _UserCardSkeleton extends StatelessWidget {
|
|
1327
|
+
const _UserCardSkeleton();
|
|
1328
|
+
|
|
1329
|
+
@override
|
|
1330
|
+
Widget build(BuildContext context) {
|
|
1331
|
+
return const KasyCard(
|
|
1332
|
+
padding: EdgeInsets.all(KasySpacing.md),
|
|
1333
|
+
child: Column(
|
|
1334
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1335
|
+
children: [
|
|
1336
|
+
Row(
|
|
1337
|
+
children: [
|
|
1338
|
+
KasySkeleton.circle(size: 38),
|
|
1339
|
+
SizedBox(width: KasySpacing.sm),
|
|
1340
|
+
Expanded(
|
|
1341
|
+
child: Column(
|
|
1342
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1343
|
+
mainAxisSize: MainAxisSize.min,
|
|
1344
|
+
children: [
|
|
1345
|
+
KasySkeleton(width: 150, height: 13),
|
|
1346
|
+
SizedBox(height: 6),
|
|
1347
|
+
KasySkeleton(width: 100, height: 10),
|
|
1348
|
+
],
|
|
1349
|
+
),
|
|
1350
|
+
),
|
|
1351
|
+
],
|
|
1352
|
+
),
|
|
1353
|
+
SizedBox(height: KasySpacing.md),
|
|
1354
|
+
Row(
|
|
1355
|
+
children: [
|
|
1356
|
+
KasySkeleton(width: 64, height: 22),
|
|
1357
|
+
SizedBox(width: 8),
|
|
1358
|
+
KasySkeleton(width: 64, height: 22),
|
|
1359
|
+
Spacer(),
|
|
1360
|
+
KasySkeleton(width: 44, height: 12),
|
|
1361
|
+
],
|
|
1362
|
+
),
|
|
1363
|
+
],
|
|
1136
1364
|
),
|
|
1137
|
-
title: t.admin_console.users.loading,
|
|
1138
1365
|
);
|
|
1139
1366
|
}
|
|
1140
1367
|
}
|
|
@@ -1167,26 +1394,11 @@ class _ErrorState extends StatelessWidget {
|
|
|
1167
1394
|
icon: const _IconBubble(Icons.error_outline_rounded),
|
|
1168
1395
|
title: u.title,
|
|
1169
1396
|
subtitle: u.error,
|
|
1170
|
-
action:
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
decoration: BoxDecoration(
|
|
1176
|
-
color: context.colors.surface,
|
|
1177
|
-
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
1178
|
-
border: Border.all(
|
|
1179
|
-
color: context.colors.outline.withValues(alpha: 0.5),
|
|
1180
|
-
),
|
|
1181
|
-
),
|
|
1182
|
-
child: Text(
|
|
1183
|
-
u.retry,
|
|
1184
|
-
style: context.textTheme.labelMedium?.copyWith(
|
|
1185
|
-
color: context.colors.onSurface,
|
|
1186
|
-
fontWeight: FontWeight.w600,
|
|
1187
|
-
),
|
|
1188
|
-
),
|
|
1189
|
-
),
|
|
1397
|
+
action: KasyButton(
|
|
1398
|
+
label: u.retry,
|
|
1399
|
+
variant: KasyButtonVariant.outline,
|
|
1400
|
+
size: KasyButtonSize.small,
|
|
1401
|
+
onPressed: onRetry,
|
|
1190
1402
|
),
|
|
1191
1403
|
);
|
|
1192
1404
|
}
|