kasy-cli 1.8.0 → 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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -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
+ }
@@ -3,7 +3,7 @@ import 'dart:async';
3
3
  import 'package:flutter/material.dart';
4
4
  import 'package:flutter/rendering.dart';
5
5
  import 'package:flutter_riverpod/flutter_riverpod.dart';
6
- import 'package:kasy_kit/components/kasy_app_bar.dart';
6
+ import 'package:kasy_kit/components/components.dart';
7
7
  import 'package:kasy_kit/core/theme/theme.dart';
8
8
  import 'package:kasy_kit/features/notifications/providers/models/notification.dart'
9
9
  as app;
@@ -66,29 +66,23 @@ class _NotificationsPageState extends ConsumerState<NotificationsPage> {
66
66
 
67
67
  Future<void> _confirmAndDeleteAll(BuildContext context) async {
68
68
  final tr = t.notifications;
69
- final confirmed = await showDialog<bool>(
70
- context: context,
71
- builder: (dialogCtx) => AlertDialog(
72
- title: Text(tr.delete_all_confirm_title),
73
- content: Text(tr.delete_all_confirm_message),
74
- actions: [
75
- TextButton(
76
- onPressed: () => Navigator.of(dialogCtx).pop(false),
77
- child: Text(tr.cancel_action),
78
- ),
79
- TextButton(
80
- onPressed: () => Navigator.of(dialogCtx).pop(true),
81
- style: TextButton.styleFrom(foregroundColor: context.colors.error),
82
- child: Text(tr.delete_action),
83
- ),
84
- ],
85
- ),
69
+ bool confirmed = false;
70
+ await showKasyConfirmDialog(
71
+ context,
72
+ title: tr.delete_all_confirm_title,
73
+ message: tr.delete_all_confirm_message,
74
+ cancelLabel: tr.cancel_action,
75
+ confirmLabel: tr.delete_action,
76
+ destructive: true,
77
+ onConfirm: () => confirmed = true,
86
78
  );
87
- if (confirmed != true || !context.mounted) return;
79
+ if (!confirmed || !context.mounted) return;
88
80
  await ref.read(notificationsProvider.notifier).deleteAll();
89
81
  if (!context.mounted) return;
90
- ScaffoldMessenger.of(context).showSnackBar(
91
- SnackBar(content: Text(tr.deleted_all)),
82
+ showKasyToast(
83
+ context,
84
+ title: tr.deleted_all,
85
+ tone: KasyToastTone.success,
92
86
  );
93
87
  }
94
88
 
@@ -96,8 +90,10 @@ class _NotificationsPageState extends ConsumerState<NotificationsPage> {
96
90
  final tr = t.notifications;
97
91
  await ref.read(notificationsProvider.notifier).delete(n);
98
92
  if (!context.mounted) return;
99
- ScaffoldMessenger.of(context).showSnackBar(
100
- SnackBar(content: Text(tr.deleted_one)),
93
+ showKasyToast(
94
+ context,
95
+ title: tr.deleted_one,
96
+ tone: KasyToastTone.success,
101
97
  );
102
98
  }
103
99
 
@@ -158,13 +154,18 @@ class _NotificationsPageState extends ConsumerState<NotificationsPage> {
158
154
  return switch (item) {
159
155
  _HeaderItem(:final label) => _GroupLabel(label: label),
160
156
  _TileItem(:final notification, :final animationIndex) =>
161
- _SwipeToDeleteTile(
162
- notification: notification,
163
- index: animationIndex,
164
- onTap: (n) => ref
165
- .read(notificationsProvider.notifier)
166
- .onTapNotification(n),
167
- onDismissed: (n) => _deleteOne(context, n),
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
+ ),
168
169
  ),
169
170
  };
170
171
  },
@@ -266,38 +267,3 @@ class _GroupLabel extends StatelessWidget {
266
267
  }
267
268
  }
268
269
 
269
- /// Wraps [NotificationTileComponent] in a [Dismissible] so the user can
270
- /// swipe a notification to the left to delete it.
271
- class _SwipeToDeleteTile extends StatelessWidget {
272
- final app.Notification notification;
273
- final int index;
274
- final void Function(app.Notification) onTap;
275
- final void Function(app.Notification) onDismissed;
276
-
277
- const _SwipeToDeleteTile({
278
- required this.notification,
279
- required this.index,
280
- required this.onTap,
281
- required this.onDismissed,
282
- });
283
-
284
- @override
285
- Widget build(BuildContext context) {
286
- return Dismissible(
287
- key: ValueKey('notification_${notification.id ?? index}'),
288
- direction: DismissDirection.endToStart,
289
- background: Container(
290
- alignment: Alignment.centerRight,
291
- padding: const EdgeInsets.symmetric(horizontal: KasySpacing.lg),
292
- color: context.colors.error,
293
- child: Icon(KasyIcons.trash, color: context.colors.onError),
294
- ),
295
- onDismissed: (_) => onDismissed(notification),
296
- child: NotificationTileComponent(
297
- notification: notification,
298
- index: index,
299
- onTap: onTap,
300
- ),
301
- );
302
- }
303
- }