kasy-cli 1.25.0 → 1.27.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.
Files changed (31) hide show
  1. package/lib/commands/new.js +63 -3
  2. package/lib/utils/i18n/messages-en.js +4 -0
  3. package/lib/utils/i18n/messages-es.js +4 -0
  4. package/lib/utils/i18n/messages-pt.js +4 -0
  5. package/package.json +1 -1
  6. package/templates/firebase/lib/components/kasy_app_bar.dart +12 -6
  7. package/templates/firebase/lib/components/kasy_avatar.dart +17 -10
  8. package/templates/firebase/lib/components/kasy_checkbox.dart +24 -15
  9. package/templates/firebase/lib/components/kasy_date_picker.dart +27 -20
  10. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +19 -14
  11. package/templates/firebase/lib/components/kasy_tabs.dart +75 -65
  12. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +98 -2
  13. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +45 -2
  14. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +9 -4
  15. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -0
  16. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +2 -0
  17. package/templates/firebase/lib/features/home/home_feed.dart +21 -5
  18. package/templates/firebase/lib/features/home/home_image_grid.dart +83 -58
  19. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +124 -101
  20. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +1 -0
  21. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +85 -85
  22. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +67 -62
  23. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +37 -23
  24. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +14 -6
  25. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +9 -3
  26. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +24 -16
  27. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +29 -22
  28. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +19 -12
  29. package/templates/firebase/lib/i18n/en.i18n.json +2 -1
  30. package/templates/firebase/lib/i18n/es.i18n.json +2 -1
  31. package/templates/firebase/lib/i18n/pt.i18n.json +2 -1
@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
6
6
  import 'package:kasy_kit/components/kasy_sidebar.dart';
7
7
  import 'package:kasy_kit/core/bottom_menu/bottom_router.dart';
8
8
  import 'package:kasy_kit/core/bottom_menu/kasy_bottom_bar_factory.dart';
9
+ import 'package:kasy_kit/core/bottom_menu/web_content_wrapper.dart';
9
10
  import 'package:kasy_kit/core/data/models/user.dart';
10
11
  import 'package:kasy_kit/core/states/logout_action.dart';
11
12
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
@@ -220,17 +221,112 @@ class _FocusableSidebarState extends State<_FocusableSidebar> {
220
221
  super.dispose();
221
222
  }
222
223
 
224
+ // "Skip to content" moves the primary focus into the routed content, so the
225
+ // next Tab steps to the first real control there instead of the sidebar.
226
+ void _skipToContent() => kasyContentFocusTarget?.requestFocus();
227
+
223
228
  @override
224
229
  Widget build(BuildContext context) {
230
+ // Reading-order group (NOT an ordered policy): this is the exact structure
231
+ // that made the anchor hold the initial focus. The anchor sits at (0,0) and
232
+ // is skipped by Tab; the skip link is positioned at the very top, so reading
233
+ // order makes it the FIRST Tab stop, then the sidebar items, then (via the
234
+ // scaffold) the header and content. Swapping in an OrderedTraversalPolicy
235
+ // here broke the anchor, so we keep reading order and rely on position.
225
236
  return FocusTraversalGroup(
226
- // The anchor is a zero-size sibling so it never affects the sidebar's
227
- // layout; it exists only to hold the initial keyboard focus (see above).
228
237
  child: Stack(
238
+ clipBehavior: Clip.none,
229
239
  children: [
230
240
  widget.child,
241
+ // Zero-size sibling; only holds the initial keyboard focus.
231
242
  Focus(focusNode: _anchor, child: const SizedBox.shrink()),
243
+ // Topmost on screen, so reading order makes it the first Tab stop.
244
+ Positioned(
245
+ top: KasySpacing.sm,
246
+ left: KasySpacing.sm,
247
+ child: _SkipToContentLink(onSkip: _skipToContent),
248
+ ),
232
249
  ],
233
250
  ),
234
251
  );
235
252
  }
236
253
  }
254
+
255
+ /// The "skip to content" link (WCAG 2.4.1 "Bypass Blocks"). It is the first Tab
256
+ /// stop on every screen: pressing Tab once reveals it above the sidebar, Enter
257
+ /// jumps focus into the content, and pressing Tab again moves on to the sidebar.
258
+ /// It only paints while focused via the keyboard, so pointer/touch users never
259
+ /// see it. Mirrors the pattern used by Stripe, GitHub, etc.
260
+ class _SkipToContentLink extends StatefulWidget {
261
+ final VoidCallback onSkip;
262
+
263
+ const _SkipToContentLink({required this.onSkip});
264
+
265
+ @override
266
+ State<_SkipToContentLink> createState() => _SkipToContentLinkState();
267
+ }
268
+
269
+ class _SkipToContentLinkState extends State<_SkipToContentLink> {
270
+ bool _show = false;
271
+
272
+ static const Map<ShortcutActivator, Intent> _shortcuts =
273
+ <ShortcutActivator, Intent>{
274
+ SingleActivator(LogicalKeyboardKey.enter): ActivateIntent(),
275
+ SingleActivator(LogicalKeyboardKey.numpadEnter): ActivateIntent(),
276
+ SingleActivator(LogicalKeyboardKey.space): ActivateIntent(),
277
+ };
278
+
279
+ @override
280
+ Widget build(BuildContext context) {
281
+ final KasyColors c = context.colors;
282
+ return FocusableActionDetector(
283
+ shortcuts: _shortcuts,
284
+ actions: <Type, Action<Intent>>{
285
+ ActivateIntent: CallbackAction<ActivateIntent>(
286
+ onInvoke: (_) {
287
+ widget.onSkip();
288
+ return null;
289
+ },
290
+ ),
291
+ },
292
+ onShowFocusHighlight: (bool show) {
293
+ if (mounted && show != _show) setState(() => _show = show);
294
+ },
295
+ child: IgnorePointer(
296
+ ignoring: !_show,
297
+ child: AnimatedOpacity(
298
+ opacity: _show ? 1 : 0,
299
+ duration: const Duration(milliseconds: 120),
300
+ child: GestureDetector(
301
+ onTap: widget.onSkip,
302
+ child: Container(
303
+ padding: const EdgeInsets.symmetric(
304
+ horizontal: KasySpacing.md,
305
+ vertical: KasySpacing.sm,
306
+ ),
307
+ decoration: BoxDecoration(
308
+ color: c.surface,
309
+ borderRadius: BorderRadius.circular(KasyRadius.md),
310
+ border: Border.all(color: c.primary, width: 1.5),
311
+ boxShadow: <BoxShadow>[
312
+ BoxShadow(
313
+ color: c.onSurface.withValues(alpha: 0.18),
314
+ blurRadius: 16,
315
+ offset: const Offset(0, 4),
316
+ ),
317
+ ],
318
+ ),
319
+ child: Text(
320
+ context.t.navigation.skip_to_content,
321
+ style: context.textTheme.bodyMedium?.copyWith(
322
+ color: c.onSurface,
323
+ fontWeight: FontWeight.w600,
324
+ ),
325
+ ),
326
+ ),
327
+ ),
328
+ ),
329
+ ),
330
+ );
331
+ }
332
+ }
@@ -3,19 +3,57 @@ import 'package:kasy_kit/components/kasy_web_header.dart';
3
3
  import 'package:kasy_kit/core/theme/theme.dart';
4
4
  import 'package:kasy_kit/core/widgets/responsive_layout.dart';
5
5
 
6
+ /// Focus target of the currently mounted content, exposed to the "skip to
7
+ /// content" link (which lives in the shell, outside the routed page).
8
+ ///
9
+ /// Each [WebContentWrapper] owns its OWN [FocusNode] and publishes it here on
10
+ /// mount, so during a route transition (where the old and new page briefly
11
+ /// coexist) there is never a single node attached to two widgets. The link
12
+ /// always jumps to the most recently mounted page.
13
+ FocusNode? kasyContentFocusTarget;
14
+
6
15
  /// On desktop (≥ [DeviceType.large]) this puts the [KasyWebHeader] at the top of
7
16
  /// the content area, above the routed page. On phone/tablet it is transparent
8
17
  /// (returns the page untouched) — there the page keeps its own [KasyAppBar].
9
18
  ///
10
19
  /// Wrap each routed page with this in the bottom router so the header is present
11
20
  /// across navigation without touching individual pages.
12
- class WebContentWrapper extends StatelessWidget {
21
+ class WebContentWrapper extends StatefulWidget {
13
22
  final Widget child;
14
23
 
15
24
  const WebContentWrapper({super.key, required this.child});
16
25
 
26
+ @override
27
+ State<WebContentWrapper> createState() => _WebContentWrapperState();
28
+ }
29
+
30
+ class _WebContentWrapperState extends State<WebContentWrapper> {
31
+ // skipTraversal: the "skip to content" link focuses this node directly, but it
32
+ // stays out of the Tab order (like a tabindex="-1" skip target on the web) so
33
+ // the next Tab lands on the first real control inside the content.
34
+ final FocusNode _contentFocus = FocusNode(
35
+ debugLabel: 'skipToContentTarget',
36
+ skipTraversal: true,
37
+ );
38
+
39
+ @override
40
+ void initState() {
41
+ super.initState();
42
+ kasyContentFocusTarget = _contentFocus;
43
+ }
44
+
45
+ @override
46
+ void dispose() {
47
+ if (kasyContentFocusTarget == _contentFocus) {
48
+ kasyContentFocusTarget = null;
49
+ }
50
+ _contentFocus.dispose();
51
+ super.dispose();
52
+ }
53
+
17
54
  @override
18
55
  Widget build(BuildContext context) {
56
+ final Widget child = widget.child;
19
57
  final bool isDesktop =
20
58
  MediaQuery.sizeOf(context).width >= DeviceType.large.breakpoint;
21
59
  if (!isDesktop) return child;
@@ -44,7 +82,12 @@ class WebContentWrapper extends StatelessWidget {
44
82
  Expanded(
45
83
  child: FocusTraversalOrder(
46
84
  order: const NumericFocusOrder(3),
47
- child: FocusTraversalGroup(child: child),
85
+ child: FocusTraversalGroup(
86
+ // Focus target for "skip to content": focusing it moves the
87
+ // primary focus into the content; the next Tab then steps to the
88
+ // first real control here.
89
+ child: Focus(focusNode: _contentFocus, child: child),
90
+ ),
48
91
  ),
49
92
  ),
50
93
  ],
@@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
5
5
  import 'package:kasy_kit/core/data/models/user.dart';
6
6
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
7
7
  import 'package:kasy_kit/core/theme/theme.dart';
8
+ import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
8
9
  import 'package:kasy_kit/features/ai_chat/api/ai_chat_conversation_entity.dart';
9
10
  import 'package:kasy_kit/features/ai_chat/ui/widgets/ai_chat_avatars.dart';
10
11
  import 'package:kasy_kit/i18n/translations.g.dart';
@@ -104,10 +105,14 @@ class _AiConversationTileState extends ConsumerState<AiConversationTile> {
104
105
  button: true,
105
106
  selected: widget.selected,
106
107
  label: title,
107
- child: GestureDetector(
108
- behavior: HitTestBehavior.opaque,
109
- onTap: widget.onTap,
110
- child: tile,
108
+ child: KasyFocusRing(
109
+ onActivate: widget.onTap,
110
+ borderRadius: BorderRadius.circular(16),
111
+ child: GestureDetector(
112
+ behavior: HitTestBehavior.opaque,
113
+ onTap: widget.onTap,
114
+ child: tile,
115
+ ),
111
116
  ),
112
117
  );
113
118
 
@@ -20,6 +20,7 @@ class AddFeatureButton extends StatelessWidget {
20
20
  onTap: onPressed,
21
21
  borderRadius: KasyRadius.mdBorderRadius,
22
22
  pressColor: context.colors.onPrimary,
23
+ focusable: true,
23
24
  child: Container(
24
25
  padding: const EdgeInsets.symmetric(
25
26
  horizontal: KasySpacing.lg,
@@ -191,6 +191,8 @@ class _VoteCardState extends State<VoteCard>
191
191
  borderRadius: KasyRadius.smBorderRadius,
192
192
  pressColor: widget.textColor,
193
193
  hapticEnabled: false,
194
+ focusable: true,
195
+ focusGapColor: context.colors.surface,
194
196
  child: AnimatedContainer(
195
197
  duration: colorDuration,
196
198
  curve: Curves.easeOut,
@@ -199,22 +199,36 @@ class _FilterRow extends StatelessWidget {
199
199
 
200
200
  const _FilterRow({required this.selected, required this.onSelect});
201
201
 
202
+ // Breathing room around the row so the keyboard focus ring (which paints a
203
+ // few px outside the card) is never clipped by the horizontal list's edges.
204
+ static const double _ringInset = 8;
205
+
202
206
  @override
203
207
  Widget build(BuildContext context) {
204
208
  const List<HomeCategory> all = HomeCategory.values;
205
209
 
210
+ // Smaller, denser cards on phones; full size on tablet/desktop.
211
+ final bool isMobile = MediaQuery.sizeOf(context).width < 768;
212
+ final double thumbSize = isMobile ? 56 : 79;
213
+ final double cardWidth = isMobile ? 232 : 302;
214
+ final double cardHeight = thumbSize + KasySpacing.md * 2;
215
+
206
216
  return SizedBox(
207
- height: 111,
217
+ height: cardHeight + _ringInset * 2,
208
218
  child: ListView.separated(
209
219
  scrollDirection: Axis.horizontal,
210
- padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
220
+ padding: const EdgeInsets.symmetric(
221
+ horizontal: KasySpacing.md,
222
+ vertical: _ringInset,
223
+ ),
211
224
  itemCount: all.length,
212
225
  separatorBuilder: (_, _) => const SizedBox(width: KasySpacing.md),
213
226
  itemBuilder: (BuildContext context, int i) => SizedBox(
214
- width: 302,
227
+ width: cardWidth,
215
228
  child: _FilterCard(
216
229
  category: all[i],
217
230
  selected: all[i] == selected,
231
+ thumbSize: thumbSize,
218
232
  onTap: () => onSelect(all[i]),
219
233
  ),
220
234
  ),
@@ -226,11 +240,13 @@ class _FilterRow extends StatelessWidget {
226
240
  class _FilterCard extends StatelessWidget {
227
241
  final HomeCategory category;
228
242
  final bool selected;
243
+ final double thumbSize;
229
244
  final VoidCallback onTap;
230
245
 
231
246
  const _FilterCard({
232
247
  required this.category,
233
248
  required this.selected,
249
+ required this.thumbSize,
234
250
  required this.onTap,
235
251
  });
236
252
 
@@ -250,8 +266,8 @@ class _FilterCard extends StatelessWidget {
250
266
  ClipRRect(
251
267
  borderRadius: BorderRadius.circular(KasyRadius.lg),
252
268
  child: SizedBox(
253
- width: 79,
254
- height: 79,
269
+ width: thumbSize,
270
+ height: thumbSize,
255
271
  child: KasyNetworkImage(url: _thumbUrl(data.thumbnailId)),
256
272
  ),
257
273
  ),
@@ -6,6 +6,7 @@ import 'package:kasy_kit/components/kasy_image_viewer.dart';
6
6
  import 'package:kasy_kit/components/kasy_skeleton.dart';
7
7
  import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
8
8
  import 'package:kasy_kit/core/theme/theme.dart';
9
+ import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
9
10
 
10
11
  /// A single image shown in the masonry feed.
11
12
  class HomePhoto {
@@ -22,7 +23,11 @@ class HomePhoto {
22
23
  /// Network image with a synced shimmer skeleton while loading and a graceful
23
24
  /// fallback on error. Adapts to light/dark via [KasySkeleton]'s own palette.
24
25
  class KasyNetworkImage extends StatelessWidget {
25
- const KasyNetworkImage({super.key, required this.url, this.fit = BoxFit.cover});
26
+ const KasyNetworkImage({
27
+ super.key,
28
+ required this.url,
29
+ this.fit = BoxFit.cover,
30
+ });
26
31
 
27
32
  final String url;
28
33
  final BoxFit fit;
@@ -33,24 +38,18 @@ class KasyNetworkImage extends StatelessWidget {
33
38
  url,
34
39
  fit: fit,
35
40
  gaplessPlayback: true,
36
- loadingBuilder: (
37
- BuildContext context,
38
- Widget child,
39
- ImageChunkEvent? progress,
40
- ) {
41
- if (progress == null) return child;
42
- return const _ImageSkeleton();
43
- },
44
- errorBuilder: (
45
- BuildContext context,
46
- Object error,
47
- StackTrace? stackTrace,
48
- ) {
49
- return ColoredBox(
50
- color: context.colors.surfaceNeutralSoft,
51
- child: Icon(KasyIcons.gallery, color: context.colors.muted),
52
- );
53
- },
41
+ loadingBuilder:
42
+ (BuildContext context, Widget child, ImageChunkEvent? progress) {
43
+ if (progress == null) return child;
44
+ return const _ImageSkeleton();
45
+ },
46
+ errorBuilder:
47
+ (BuildContext context, Object error, StackTrace? stackTrace) {
48
+ return ColoredBox(
49
+ color: context.colors.surfaceNeutralSoft,
50
+ child: Icon(KasyIcons.gallery, color: context.colors.muted),
51
+ );
52
+ },
54
53
  );
55
54
  }
56
55
  }
@@ -80,7 +79,16 @@ class HomeImageGrid extends StatelessWidget {
80
79
 
81
80
  static const double _gap = KasySpacing.smd;
82
81
  static const List<double> _ratios = <double>[
83
- 0.72, 1.0, 0.8, 1.32, 0.75, 1.0, 0.7, 1.25, 0.85, 1.4,
82
+ 0.72,
83
+ 1.0,
84
+ 0.8,
85
+ 1.32,
86
+ 0.75,
87
+ 1.0,
88
+ 0.7,
89
+ 1.25,
90
+ 0.85,
91
+ 1.4,
84
92
  ];
85
93
 
86
94
  int _columnsFor(double width) {
@@ -97,8 +105,10 @@ class HomeImageGrid extends StatelessWidget {
97
105
  final int columns = _columnsFor(width);
98
106
  final double columnWidth = (width - _gap * (columns - 1)) / columns;
99
107
 
100
- final List<List<Widget>> buckets =
101
- List<List<Widget>>.generate(columns, (_) => <Widget>[]);
108
+ final List<List<Widget>> buckets = List<List<Widget>>.generate(
109
+ columns,
110
+ (_) => <Widget>[],
111
+ );
102
112
  final List<double> heights = List<double>.filled(columns, 0);
103
113
 
104
114
  for (int i = 0; i < photos.length; i++) {
@@ -110,8 +120,9 @@ class HomeImageGrid extends StatelessWidget {
110
120
  if (buckets[shortest].isNotEmpty) {
111
121
  buckets[shortest].add(const SizedBox(height: _gap));
112
122
  }
113
- buckets[shortest]
114
- .add(_PhotoTile(photo: photos[i], aspectRatio: ratio));
123
+ buckets[shortest].add(
124
+ _PhotoTile(photo: photos[i], aspectRatio: ratio),
125
+ );
115
126
  heights[shortest] += columnWidth / ratio + _gap;
116
127
  }
117
128
 
@@ -170,12 +181,16 @@ class _PhotoTileState extends State<_PhotoTile> {
170
181
  // Tapping the photo opens the full-screen viewer; the Hero gives a
171
182
  // smooth zoom into and out of it.
172
183
  Positioned.fill(
173
- child: GestureDetector(
174
- onTap: _openViewer,
175
- behavior: HitTestBehavior.opaque,
176
- child: Hero(
177
- tag: photo.id,
178
- child: KasyNetworkImage(url: photo.url),
184
+ child: KasyFocusRing(
185
+ onActivate: _openViewer,
186
+ borderRadius: BorderRadius.circular(KasyRadius.xl),
187
+ child: GestureDetector(
188
+ onTap: _openViewer,
189
+ behavior: HitTestBehavior.opaque,
190
+ child: Hero(
191
+ tag: photo.id,
192
+ child: KasyNetworkImage(url: photo.url),
193
+ ),
179
194
  ),
180
195
  ),
181
196
  ),
@@ -291,13 +306,17 @@ class _LikeButtonState extends State<_LikeButton>
291
306
  );
292
307
  _scale = TweenSequence<double>(<TweenSequenceItem<double>>[
293
308
  TweenSequenceItem<double>(
294
- tween: Tween<double>(begin: 1.0, end: 1.35)
295
- .chain(CurveTween(curve: Curves.easeOut)),
309
+ tween: Tween<double>(
310
+ begin: 1.0,
311
+ end: 1.35,
312
+ ).chain(CurveTween(curve: Curves.easeOut)),
296
313
  weight: 40,
297
314
  ),
298
315
  TweenSequenceItem<double>(
299
- tween: Tween<double>(begin: 1.35, end: 1.0)
300
- .chain(CurveTween(curve: Curves.elasticOut)),
316
+ tween: Tween<double>(
317
+ begin: 1.35,
318
+ end: 1.0,
319
+ ).chain(CurveTween(curve: Curves.elasticOut)),
301
320
  weight: 60,
302
321
  ),
303
322
  ]).animate(_controller);
@@ -321,30 +340,36 @@ class _LikeButtonState extends State<_LikeButton>
321
340
  @override
322
341
  Widget build(BuildContext context) {
323
342
  final bool dark = context.isDark;
324
- final Color heart =
325
- widget.liked ? context.colors.danger : context.colors.primary;
326
-
327
- return GestureDetector(
328
- onTap: widget.onTap,
329
- behavior: HitTestBehavior.opaque,
330
- child: ClipOval(
331
- child: BackdropFilter(
332
- filter: ui.ImageFilter.blur(sigmaX: 8, sigmaY: 8),
333
- child: Container(
334
- width: 36,
335
- height: 36,
336
- alignment: Alignment.center,
337
- decoration: BoxDecoration(
338
- color: context.colors.surface
339
- .withValues(alpha: dark ? 0.5 : 0.7),
340
- shape: BoxShape.circle,
341
- ),
342
- child: ScaleTransition(
343
- scale: _scale,
344
- child: Icon(
345
- widget.liked ? KasyIcons.favoriteFilled : KasyIcons.favorite,
346
- size: 18,
347
- color: heart,
343
+ final Color heart = widget.liked
344
+ ? context.colors.danger
345
+ : context.colors.primary;
346
+
347
+ return KasyFocusRing(
348
+ onActivate: widget.onTap,
349
+ borderRadius: BorderRadius.circular(999),
350
+ child: GestureDetector(
351
+ onTap: widget.onTap,
352
+ behavior: HitTestBehavior.opaque,
353
+ child: ClipOval(
354
+ child: BackdropFilter(
355
+ filter: ui.ImageFilter.blur(sigmaX: 8, sigmaY: 8),
356
+ child: Container(
357
+ width: 36,
358
+ height: 36,
359
+ alignment: Alignment.center,
360
+ decoration: BoxDecoration(
361
+ color: context.colors.surface.withValues(
362
+ alpha: dark ? 0.5 : 0.7,
363
+ ),
364
+ shape: BoxShape.circle,
365
+ ),
366
+ child: ScaleTransition(
367
+ scale: _scale,
368
+ child: Icon(
369
+ widget.liked ? KasyIcons.favoriteFilled : KasyIcons.favorite,
370
+ size: 18,
371
+ color: heart,
372
+ ),
348
373
  ),
349
374
  ),
350
375
  ),