kasy-cli 1.8.1 → 1.8.2
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/package.json +1 -1
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_swipe_action.dart +143 -0
- package/templates/firebase/lib/features/home/home_components_page.dart +3 -0
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +186 -0
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +12 -42
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.8.0": {
|
|
3
|
+
"modules": {
|
|
4
|
+
"components": {
|
|
5
|
+
"pt": "Novo componente KasySwipeAction (arrastar para excluir/arquivar) + tela de notificações com excluir tudo e swipe-to-delete",
|
|
6
|
+
"en": "New KasySwipeAction component (swipe to delete/archive) + notifications screen with delete-all and swipe-to-delete",
|
|
7
|
+
"es": "Nuevo componente KasySwipeAction (deslizar para eliminar/archivar) + pantalla de notificaciones con eliminar todo y swipe-to-delete"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
2
11
|
"1.5.0": {
|
|
3
12
|
"modules": {
|
|
4
13
|
"widget": {
|
package/package.json
CHANGED
|
@@ -21,6 +21,7 @@ export 'kasy_chip.dart';
|
|
|
21
21
|
export 'kasy_dialog.dart';
|
|
22
22
|
export 'kasy_otp_verification_bottom_sheet.dart';
|
|
23
23
|
export 'kasy_skeleton.dart';
|
|
24
|
+
export 'kasy_swipe_action.dart';
|
|
24
25
|
export 'kasy_text_area.dart';
|
|
25
26
|
export 'kasy_text_field.dart';
|
|
26
27
|
export 'kasy_text_field_otp.dart';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:kasy_kit/core/theme/theme.dart';
|
|
3
|
+
|
|
4
|
+
/// Visual tone for the action revealed behind a [KasySwipeAction].
|
|
5
|
+
enum KasySwipeActionTone {
|
|
6
|
+
/// Destructive action (delete, remove). Red background, white icon.
|
|
7
|
+
destructive,
|
|
8
|
+
|
|
9
|
+
/// Neutral action (archive, snooze). Surface tint, on-surface icon.
|
|
10
|
+
neutral,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// Direction in which the user must swipe to reveal the action.
|
|
14
|
+
enum KasySwipeActionDirection {
|
|
15
|
+
/// Swipe from right to left (common for delete on mobile lists).
|
|
16
|
+
endToStart,
|
|
17
|
+
|
|
18
|
+
/// Swipe from left to right.
|
|
19
|
+
startToEnd,
|
|
20
|
+
|
|
21
|
+
/// Both directions allowed.
|
|
22
|
+
both,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Wraps any [child] so the user can swipe it horizontally to trigger an
|
|
26
|
+
/// action — most commonly a delete. Visual chrome and dismiss threshold come
|
|
27
|
+
/// from the design system; the parent only supplies the action callback.
|
|
28
|
+
///
|
|
29
|
+
/// ```dart
|
|
30
|
+
/// KasySwipeAction(
|
|
31
|
+
/// key: ValueKey(item.id),
|
|
32
|
+
/// onDismissed: () => repository.delete(item),
|
|
33
|
+
/// child: NotificationTile(notification: item),
|
|
34
|
+
/// );
|
|
35
|
+
/// ```
|
|
36
|
+
///
|
|
37
|
+
/// For a confirmation step (e.g. asking before deleting), supply
|
|
38
|
+
/// [confirmDismiss] returning `true` to proceed. Returning `false` (or
|
|
39
|
+
/// awaiting a dialog that returns `false`) snaps the card back into place.
|
|
40
|
+
class KasySwipeAction extends StatelessWidget {
|
|
41
|
+
final Widget child;
|
|
42
|
+
final VoidCallback onDismissed;
|
|
43
|
+
final KasySwipeActionTone tone;
|
|
44
|
+
final KasySwipeActionDirection direction;
|
|
45
|
+
final IconData icon;
|
|
46
|
+
final String? label;
|
|
47
|
+
|
|
48
|
+
/// Optional confirmation hook. Return `true` to dismiss, `false` to cancel.
|
|
49
|
+
final Future<bool> Function()? confirmDismiss;
|
|
50
|
+
|
|
51
|
+
const KasySwipeAction({
|
|
52
|
+
required Key super.key,
|
|
53
|
+
required this.child,
|
|
54
|
+
required this.onDismissed,
|
|
55
|
+
this.tone = KasySwipeActionTone.destructive,
|
|
56
|
+
this.direction = KasySwipeActionDirection.endToStart,
|
|
57
|
+
this.icon = KasyIcons.trash,
|
|
58
|
+
this.label,
|
|
59
|
+
}) : confirmDismiss = null;
|
|
60
|
+
|
|
61
|
+
const KasySwipeAction.withConfirm({
|
|
62
|
+
required Key super.key,
|
|
63
|
+
required this.child,
|
|
64
|
+
required this.onDismissed,
|
|
65
|
+
required Future<bool> Function() confirm,
|
|
66
|
+
this.tone = KasySwipeActionTone.destructive,
|
|
67
|
+
this.direction = KasySwipeActionDirection.endToStart,
|
|
68
|
+
this.icon = KasyIcons.trash,
|
|
69
|
+
this.label,
|
|
70
|
+
}) : confirmDismiss = confirm;
|
|
71
|
+
|
|
72
|
+
DismissDirection _flutterDirection() {
|
|
73
|
+
switch (direction) {
|
|
74
|
+
case KasySwipeActionDirection.endToStart:
|
|
75
|
+
return DismissDirection.endToStart;
|
|
76
|
+
case KasySwipeActionDirection.startToEnd:
|
|
77
|
+
return DismissDirection.startToEnd;
|
|
78
|
+
case KasySwipeActionDirection.both:
|
|
79
|
+
return DismissDirection.horizontal;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Color _backgroundColor(BuildContext context) {
|
|
84
|
+
switch (tone) {
|
|
85
|
+
case KasySwipeActionTone.destructive:
|
|
86
|
+
return context.colors.error;
|
|
87
|
+
case KasySwipeActionTone.neutral:
|
|
88
|
+
return context.colors.surfaceNeutralSoft;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Color _foregroundColor(BuildContext context) {
|
|
93
|
+
switch (tone) {
|
|
94
|
+
case KasySwipeActionTone.destructive:
|
|
95
|
+
return context.colors.onError;
|
|
96
|
+
case KasySwipeActionTone.neutral:
|
|
97
|
+
return context.colors.onSurface;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@override
|
|
102
|
+
Widget build(BuildContext context) {
|
|
103
|
+
final Color bg = _backgroundColor(context);
|
|
104
|
+
final Color fg = _foregroundColor(context);
|
|
105
|
+
final DismissDirection flutterDirection = _flutterDirection();
|
|
106
|
+
|
|
107
|
+
Widget buildBackground(Alignment alignment) {
|
|
108
|
+
final children = <Widget>[
|
|
109
|
+
Icon(icon, color: fg),
|
|
110
|
+
if (label != null) ...[
|
|
111
|
+
const SizedBox(width: KasySpacing.sm),
|
|
112
|
+
Text(
|
|
113
|
+
label!,
|
|
114
|
+
style: context.textTheme.labelLarge?.copyWith(color: fg),
|
|
115
|
+
),
|
|
116
|
+
],
|
|
117
|
+
];
|
|
118
|
+
return Container(
|
|
119
|
+
alignment: alignment,
|
|
120
|
+
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.lg),
|
|
121
|
+
color: bg,
|
|
122
|
+
child: Row(
|
|
123
|
+
mainAxisSize: MainAxisSize.min,
|
|
124
|
+
children: alignment == Alignment.centerRight
|
|
125
|
+
? children
|
|
126
|
+
: children.reversed.toList(),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return Dismissible(
|
|
132
|
+
key: key!,
|
|
133
|
+
direction: flutterDirection,
|
|
134
|
+
confirmDismiss: confirmDismiss == null
|
|
135
|
+
? null
|
|
136
|
+
: (_) => confirmDismiss!.call(),
|
|
137
|
+
onDismissed: (_) => onDismissed(),
|
|
138
|
+
background: buildBackground(Alignment.centerLeft),
|
|
139
|
+
secondaryBackground: buildBackground(Alignment.centerRight),
|
|
140
|
+
child: child,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -188,6 +188,7 @@ const Set<String> _kReadyComponents = <String>{
|
|
|
188
188
|
'TextField',
|
|
189
189
|
'Sidebar',
|
|
190
190
|
'Skeleton',
|
|
191
|
+
'SwipeAction',
|
|
191
192
|
'TextFieldOTP',
|
|
192
193
|
'Toast',
|
|
193
194
|
};
|
|
@@ -208,6 +209,7 @@ const Set<String> _kWebReadyComponents = <String>{
|
|
|
208
209
|
'Hover',
|
|
209
210
|
'Sidebar',
|
|
210
211
|
'Skeleton',
|
|
212
|
+
'SwipeAction',
|
|
211
213
|
'TextArea',
|
|
212
214
|
'TextField',
|
|
213
215
|
'TextFieldOTP',
|
|
@@ -238,6 +240,7 @@ const List<_CatalogRow> _kCatalog = [
|
|
|
238
240
|
_CatalogRow('Hover'),
|
|
239
241
|
_CatalogRow('Sidebar'),
|
|
240
242
|
_CatalogRow('Skeleton'),
|
|
243
|
+
_CatalogRow('SwipeAction'),
|
|
241
244
|
_CatalogRow('TextArea'),
|
|
242
245
|
_CatalogRow('TextField'),
|
|
243
246
|
_CatalogRow('TextFieldOTP'),
|
|
@@ -490,6 +490,24 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
490
490
|
),
|
|
491
491
|
],
|
|
492
492
|
);
|
|
493
|
+
case 'SwipeAction':
|
|
494
|
+
return const ComponentPreviewDefinition(
|
|
495
|
+
title: 'SwipeAction',
|
|
496
|
+
variants: [
|
|
497
|
+
ComponentPreviewVariant(
|
|
498
|
+
label: 'Destructive (delete)',
|
|
499
|
+
builder: _buildSwipeActionDestructive,
|
|
500
|
+
),
|
|
501
|
+
ComponentPreviewVariant(
|
|
502
|
+
label: 'Neutral (archive)',
|
|
503
|
+
builder: _buildSwipeActionNeutral,
|
|
504
|
+
),
|
|
505
|
+
ComponentPreviewVariant(
|
|
506
|
+
label: 'With confirm',
|
|
507
|
+
builder: _buildSwipeActionWithConfirm,
|
|
508
|
+
),
|
|
509
|
+
],
|
|
510
|
+
);
|
|
493
511
|
default:
|
|
494
512
|
return null;
|
|
495
513
|
}
|
|
@@ -7710,3 +7728,171 @@ class _SkeletonGridCell extends StatelessWidget {
|
|
|
7710
7728
|
);
|
|
7711
7729
|
}
|
|
7712
7730
|
}
|
|
7731
|
+
|
|
7732
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7733
|
+
// SwipeAction — wrap any tile so the user can swipe it to reveal an action
|
|
7734
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7735
|
+
|
|
7736
|
+
Widget _buildSwipeActionDestructive(BuildContext context) =>
|
|
7737
|
+
const _SwipeActionPreview(tone: KasySwipeActionTone.destructive);
|
|
7738
|
+
|
|
7739
|
+
Widget _buildSwipeActionNeutral(BuildContext context) =>
|
|
7740
|
+
const _SwipeActionPreview(
|
|
7741
|
+
tone: KasySwipeActionTone.neutral,
|
|
7742
|
+
icon: Icons.archive_outlined,
|
|
7743
|
+
);
|
|
7744
|
+
|
|
7745
|
+
Widget _buildSwipeActionWithConfirm(BuildContext context) =>
|
|
7746
|
+
const _SwipeActionPreview(
|
|
7747
|
+
tone: KasySwipeActionTone.destructive,
|
|
7748
|
+
useConfirm: true,
|
|
7749
|
+
);
|
|
7750
|
+
|
|
7751
|
+
class _SwipeActionPreview extends StatefulWidget {
|
|
7752
|
+
final KasySwipeActionTone tone;
|
|
7753
|
+
final IconData? icon;
|
|
7754
|
+
final bool useConfirm;
|
|
7755
|
+
|
|
7756
|
+
const _SwipeActionPreview({
|
|
7757
|
+
required this.tone,
|
|
7758
|
+
this.icon,
|
|
7759
|
+
this.useConfirm = false,
|
|
7760
|
+
});
|
|
7761
|
+
|
|
7762
|
+
@override
|
|
7763
|
+
State<_SwipeActionPreview> createState() => _SwipeActionPreviewState();
|
|
7764
|
+
}
|
|
7765
|
+
|
|
7766
|
+
class _SwipeActionPreviewState extends State<_SwipeActionPreview> {
|
|
7767
|
+
late List<_SwipePreviewItem> _items = _initial();
|
|
7768
|
+
|
|
7769
|
+
List<_SwipePreviewItem> _initial() => List.generate(
|
|
7770
|
+
4,
|
|
7771
|
+
(i) => _SwipePreviewItem(
|
|
7772
|
+
id: i,
|
|
7773
|
+
title: 'Item ${i + 1}',
|
|
7774
|
+
subtitle: 'Swipe left to ${widget.useConfirm ? 'try to ' : ''}remove',
|
|
7775
|
+
),
|
|
7776
|
+
);
|
|
7777
|
+
|
|
7778
|
+
void _reset() => setState(() => _items = _initial());
|
|
7779
|
+
|
|
7780
|
+
Future<bool> _confirm() async {
|
|
7781
|
+
bool confirmed = false;
|
|
7782
|
+
await showKasyConfirmDialog(
|
|
7783
|
+
context,
|
|
7784
|
+
title: 'Remove item?',
|
|
7785
|
+
message: 'This is just a preview — nothing is actually deleted.',
|
|
7786
|
+
cancelLabel: 'Cancel',
|
|
7787
|
+
confirmLabel: 'Remove',
|
|
7788
|
+
destructive: true,
|
|
7789
|
+
onConfirm: () => confirmed = true,
|
|
7790
|
+
);
|
|
7791
|
+
return confirmed;
|
|
7792
|
+
}
|
|
7793
|
+
|
|
7794
|
+
@override
|
|
7795
|
+
Widget build(BuildContext context) {
|
|
7796
|
+
if (_items.isEmpty) {
|
|
7797
|
+
return Padding(
|
|
7798
|
+
padding: const EdgeInsets.all(KasySpacing.lg),
|
|
7799
|
+
child: Column(
|
|
7800
|
+
children: [
|
|
7801
|
+
Text(
|
|
7802
|
+
'All items removed',
|
|
7803
|
+
style: context.textTheme.bodyMedium,
|
|
7804
|
+
textAlign: TextAlign.center,
|
|
7805
|
+
),
|
|
7806
|
+
const SizedBox(height: KasySpacing.sm),
|
|
7807
|
+
KasyButton(
|
|
7808
|
+
label: 'Reset preview',
|
|
7809
|
+
variant: KasyButtonVariant.outline,
|
|
7810
|
+
onPressed: _reset,
|
|
7811
|
+
),
|
|
7812
|
+
],
|
|
7813
|
+
),
|
|
7814
|
+
);
|
|
7815
|
+
}
|
|
7816
|
+
return Column(
|
|
7817
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
7818
|
+
children: [
|
|
7819
|
+
for (final item in _items)
|
|
7820
|
+
widget.useConfirm
|
|
7821
|
+
? KasySwipeAction.withConfirm(
|
|
7822
|
+
key: ValueKey('swipe_confirm_${item.id}'),
|
|
7823
|
+
onDismissed: () =>
|
|
7824
|
+
setState(() => _items.removeWhere((i) => i.id == item.id)),
|
|
7825
|
+
confirm: _confirm,
|
|
7826
|
+
tone: widget.tone,
|
|
7827
|
+
icon: widget.icon ?? KasyIcons.trash,
|
|
7828
|
+
child: _SwipePreviewTile(item: item),
|
|
7829
|
+
)
|
|
7830
|
+
: KasySwipeAction(
|
|
7831
|
+
key: ValueKey('swipe_${item.id}'),
|
|
7832
|
+
onDismissed: () =>
|
|
7833
|
+
setState(() => _items.removeWhere((i) => i.id == item.id)),
|
|
7834
|
+
tone: widget.tone,
|
|
7835
|
+
icon: widget.icon ?? KasyIcons.trash,
|
|
7836
|
+
child: _SwipePreviewTile(item: item),
|
|
7837
|
+
),
|
|
7838
|
+
],
|
|
7839
|
+
);
|
|
7840
|
+
}
|
|
7841
|
+
}
|
|
7842
|
+
|
|
7843
|
+
class _SwipePreviewItem {
|
|
7844
|
+
final int id;
|
|
7845
|
+
final String title;
|
|
7846
|
+
final String subtitle;
|
|
7847
|
+
const _SwipePreviewItem({
|
|
7848
|
+
required this.id,
|
|
7849
|
+
required this.title,
|
|
7850
|
+
required this.subtitle,
|
|
7851
|
+
});
|
|
7852
|
+
}
|
|
7853
|
+
|
|
7854
|
+
class _SwipePreviewTile extends StatelessWidget {
|
|
7855
|
+
final _SwipePreviewItem item;
|
|
7856
|
+
const _SwipePreviewTile({required this.item});
|
|
7857
|
+
|
|
7858
|
+
@override
|
|
7859
|
+
Widget build(BuildContext context) {
|
|
7860
|
+
return Container(
|
|
7861
|
+
padding: const EdgeInsets.all(KasySpacing.md),
|
|
7862
|
+
decoration: BoxDecoration(
|
|
7863
|
+
color: context.colors.surface,
|
|
7864
|
+
border: Border(
|
|
7865
|
+
bottom: BorderSide(color: context.colors.outline, width: 0.5),
|
|
7866
|
+
),
|
|
7867
|
+
),
|
|
7868
|
+
child: Row(
|
|
7869
|
+
children: [
|
|
7870
|
+
Container(
|
|
7871
|
+
width: 40,
|
|
7872
|
+
height: 40,
|
|
7873
|
+
decoration: BoxDecoration(
|
|
7874
|
+
color: context.colors.surfaceNeutralSoft,
|
|
7875
|
+
borderRadius: BorderRadius.circular(KasyRadius.md),
|
|
7876
|
+
),
|
|
7877
|
+
child: Icon(KasyIcons.assistant, color: context.colors.onSurface),
|
|
7878
|
+
),
|
|
7879
|
+
const SizedBox(width: KasySpacing.md),
|
|
7880
|
+
Expanded(
|
|
7881
|
+
child: Column(
|
|
7882
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
7883
|
+
children: [
|
|
7884
|
+
Text(item.title, style: context.textTheme.bodyMedium),
|
|
7885
|
+
Text(
|
|
7886
|
+
item.subtitle,
|
|
7887
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
7888
|
+
color: context.colors.muted,
|
|
7889
|
+
),
|
|
7890
|
+
),
|
|
7891
|
+
],
|
|
7892
|
+
),
|
|
7893
|
+
),
|
|
7894
|
+
],
|
|
7895
|
+
),
|
|
7896
|
+
);
|
|
7897
|
+
}
|
|
7898
|
+
}
|
|
@@ -154,13 +154,18 @@ class _NotificationsPageState extends ConsumerState<NotificationsPage> {
|
|
|
154
154
|
return switch (item) {
|
|
155
155
|
_HeaderItem(:final label) => _GroupLabel(label: label),
|
|
156
156
|
_TileItem(:final notification, :final animationIndex) =>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
KasySwipeAction(
|
|
158
|
+
key: ValueKey(
|
|
159
|
+
'notification_${notification.id ?? animationIndex}',
|
|
160
|
+
),
|
|
161
|
+
onDismissed: () => _deleteOne(context, notification),
|
|
162
|
+
child: NotificationTileComponent(
|
|
163
|
+
notification: notification,
|
|
164
|
+
index: animationIndex,
|
|
165
|
+
onTap: (n) => ref
|
|
166
|
+
.read(notificationsProvider.notifier)
|
|
167
|
+
.onTapNotification(n),
|
|
168
|
+
),
|
|
164
169
|
),
|
|
165
170
|
};
|
|
166
171
|
},
|
|
@@ -262,38 +267,3 @@ class _GroupLabel extends StatelessWidget {
|
|
|
262
267
|
}
|
|
263
268
|
}
|
|
264
269
|
|
|
265
|
-
/// Wraps [NotificationTileComponent] in a [Dismissible] so the user can
|
|
266
|
-
/// swipe a notification to the left to delete it.
|
|
267
|
-
class _SwipeToDeleteTile extends StatelessWidget {
|
|
268
|
-
final app.Notification notification;
|
|
269
|
-
final int index;
|
|
270
|
-
final void Function(app.Notification) onTap;
|
|
271
|
-
final void Function(app.Notification) onDismissed;
|
|
272
|
-
|
|
273
|
-
const _SwipeToDeleteTile({
|
|
274
|
-
required this.notification,
|
|
275
|
-
required this.index,
|
|
276
|
-
required this.onTap,
|
|
277
|
-
required this.onDismissed,
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
@override
|
|
281
|
-
Widget build(BuildContext context) {
|
|
282
|
-
return Dismissible(
|
|
283
|
-
key: ValueKey('notification_${notification.id ?? index}'),
|
|
284
|
-
direction: DismissDirection.endToStart,
|
|
285
|
-
background: Container(
|
|
286
|
-
alignment: Alignment.centerRight,
|
|
287
|
-
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.lg),
|
|
288
|
-
color: context.colors.error,
|
|
289
|
-
child: Icon(KasyIcons.trash, color: context.colors.onError),
|
|
290
|
-
),
|
|
291
|
-
onDismissed: (_) => onDismissed(notification),
|
|
292
|
-
child: NotificationTileComponent(
|
|
293
|
-
notification: notification,
|
|
294
|
-
index: index,
|
|
295
|
-
onTap: onTap,
|
|
296
|
-
),
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
}
|