kasy-cli 1.39.0 → 1.40.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 (33) hide show
  1. package/lib/scaffold/CHANGELOG.json +23 -0
  2. package/package.json +1 -1
  3. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +27 -27
  4. package/templates/firebase/AGENTS.md +2 -2
  5. package/templates/firebase/DESIGN_SYSTEM.md +3 -2
  6. package/templates/firebase/lib/components/components.dart +6 -1
  7. package/templates/firebase/lib/components/kasy_app_bar.dart +11 -1
  8. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +25 -7
  9. package/templates/firebase/lib/components/kasy_menu.dart +902 -0
  10. package/templates/firebase/lib/components/kasy_popover.dart +267 -0
  11. package/templates/firebase/lib/components/kasy_sidebar.dart +20 -10
  12. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +23 -7
  13. package/templates/firebase/lib/core/chrome/app_bar_config.dart +1 -1
  14. package/templates/firebase/lib/core/navigation/kasy_route_observer.dart +8 -0
  15. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +96 -14
  16. package/templates/firebase/lib/core/theme/texts.dart +25 -0
  17. package/templates/firebase/lib/core/web_viewport_scale.dart +8 -7
  18. package/templates/firebase/lib/features/home/home_components_page.dart +23 -4
  19. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +363 -13
  20. package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart +4 -0
  21. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +38 -94
  22. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +32 -107
  23. package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +21 -21
  24. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +35 -27
  25. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +22 -17
  26. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +12 -7
  27. package/templates/firebase/lib/i18n/en.i18n.json +6 -2
  28. package/templates/firebase/lib/i18n/es.i18n.json +6 -2
  29. package/templates/firebase/lib/i18n/pt.i18n.json +6 -2
  30. package/templates/firebase/lib/router.dart +2 -0
  31. package/templates/firebase/pubspec.yaml +1 -1
  32. package/templates/firebase/test/app_bar_config_test.dart +70 -0
  33. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +0 -81
@@ -2,7 +2,8 @@ import 'dart:async';
2
2
 
3
3
  import 'package:flutter/material.dart';
4
4
  import 'package:flutter_riverpod/flutter_riverpod.dart';
5
- import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
5
+ import 'package:kasy_kit/components/kasy_menu.dart';
6
+ import 'package:kasy_kit/components/kasy_popover.dart';
6
7
  import 'package:kasy_kit/core/home_widgets/home_widget_mywidget_service.dart';
7
8
  import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
8
9
  import 'package:kasy_kit/core/theme/theme.dart';
@@ -63,118 +64,42 @@ class LanguageSwitcher extends ConsumerWidget {
63
64
  WidgetRef ref,
64
65
  AppLocale current,
65
66
  ) {
66
- final String sheetTitle = context.t.settings.language_title;
67
- showKasyBottomSheet<void>(
67
+ showKasyMenu<void>(
68
68
  context: context,
69
- builder: (sheetContext) => KasySheetSurface(
70
- child: Padding(
71
- // No horizontal padding: option rows go full-bleed so the highlight
72
- // spans the whole sheet (no inset pill). Title and rows carry their
73
- // own horizontal inset instead.
74
- padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
75
- child: Column(
76
- mainAxisSize: MainAxisSize.min,
77
- children: <Widget>[
78
- Padding(
79
- padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
80
- child: Align(
81
- alignment: Alignment.centerLeft,
82
- child: Text(
83
- sheetTitle,
84
- style: sheetContext.textTheme.titleMedium?.copyWith(
85
- color: sheetContext.colors.onSurface,
86
- ),
87
- ),
69
+ anchorContext: context,
70
+ // Desktop: hang the menu off the right edge of the settings row, where the
71
+ // current value sits a native desktop menu, not a centered modal.
72
+ desktopAlign: KasyPopoverAlign.end,
73
+ sections: [
74
+ KasyMenuSection(
75
+ label: context.t.settings.language_title,
76
+ selectionMode: KasyMenuSelectionMode.single,
77
+ items: AppLocale.values
78
+ .map(
79
+ (AppLocale locale) => KasyMenuItem(
80
+ title: locale.nativeName,
81
+ selected: locale == current,
82
+ onTap: () => _applyLocale(ref, locale),
88
83
  ),
89
- ),
90
- const SizedBox(height: KasySpacing.xs),
91
- ...AppLocale.values.map((AppLocale locale) {
92
- final bool isSelected = locale == current;
93
- return _LocaleOptionTile(
94
- locale: locale,
95
- isSelected: isSelected,
96
- onTap: () async {
97
- // Close the sheet FIRST. Awaiting work before pop
98
- // races with the rebuild triggered by setLocale and
99
- // crashed Navigator.pop with !_debugLocked.
100
- Navigator.pop(sheetContext);
101
- // Await setLocale so the locale bundle finishes loading
102
- // BEFORE the widget tries to read its translations.
103
- // Slang lazy-loads non-base locales and its loadLocale
104
- // returns immediately when another caller (this setLocale)
105
- // is already loading — so a parallel updateForLocale would
106
- // see translations not yet in the map and silently fall
107
- // back to the base locale (English). That was the root
108
- // cause of "first language tap doesn't update the widget".
109
- await LocaleSettings.setLocale(locale);
110
- unawaited(
111
- ref
112
- .read(sharedPreferencesProvider)
113
- .setAppLocale(locale.languageCode),
114
- );
115
- unawaited(
116
- ref
117
- .read(myWidgetHomeWidgetProvider.notifier)
118
- .updateForLocale(locale),
119
- );
120
- },
121
- );
122
- }),
123
- ],
124
- ),
84
+ )
85
+ .toList(),
125
86
  ),
126
- ),
87
+ ],
127
88
  );
128
89
  }
129
- }
130
90
 
131
- class _LocaleOptionTile extends StatelessWidget {
132
- const _LocaleOptionTile({
133
- required this.locale,
134
- required this.isSelected,
135
- required this.onTap,
136
- });
137
-
138
- final AppLocale locale;
139
- final bool isSelected;
140
- final VoidCallback onTap;
141
-
142
- @override
143
- Widget build(BuildContext context) {
144
- final Color primary = context.colors.primary;
145
- final Color fg = isSelected ? primary : context.colors.onSurface;
146
-
147
- return KasyHover(
148
- onTap: onTap,
149
- focusable: true,
150
- // Full-bleed rectangular highlight (default radius): the sheet rounds its
151
- // own corners, so options span edge-to-edge like a native menu/list.
152
- padding: const EdgeInsets.symmetric(
153
- horizontal: KasySpacing.md,
154
- vertical: KasySpacing.smd,
155
- ),
156
- child: Row(
157
- children: <Widget>[
158
- Expanded(
159
- child: Text(
160
- locale.nativeName,
161
- style: context.textTheme.bodyLarge?.copyWith(
162
- color: fg,
163
- fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
164
- ),
165
- ),
166
- ),
167
- if (isSelected)
168
- Container(
169
- width: 10,
170
- height: 10,
171
- decoration: BoxDecoration(
172
- color: primary,
173
- shape: BoxShape.circle,
174
- ),
175
- ),
176
- ],
177
- ),
91
+ // The menu closes itself before this runs (KasyMenuItem pops first), so the
92
+ // awaited setLocale never races the rebuild it triggers. setLocale is awaited
93
+ // so Slang finishes loading the locale bundle before any widget reads its
94
+ // translations — otherwise a parallel updateForLocale sees an unloaded map and
95
+ // silently falls back to English (the old "first tap doesn't update" bug).
96
+ Future<void> _applyLocale(WidgetRef ref, AppLocale locale) async {
97
+ await LocaleSettings.setLocale(locale);
98
+ unawaited(
99
+ ref.read(sharedPreferencesProvider).setAppLocale(locale.languageCode),
100
+ );
101
+ unawaited(
102
+ ref.read(myWidgetHomeWidgetProvider.notifier).updateForLocale(locale),
178
103
  );
179
104
  }
180
105
  }
@@ -137,30 +137,30 @@ class _ComparisonTable extends StatelessWidget {
137
137
  return const SizedBox.shrink();
138
138
  }
139
139
 
140
- return Material(
140
+ // Flat bordered surface (no elevation), clipped to the rounded shape so the
141
+ // tinted header corners follow the radius. No Material wrapper: the Container
142
+ // paints the surface, border and radius itself.
143
+ return ClipRRect(
141
144
  borderRadius: KasyRadius.lgBorderRadius,
142
- child: ClipRRect(
143
- borderRadius: KasyRadius.lgBorderRadius,
144
- child: Container(
145
- decoration: BoxDecoration(
146
- color: context.colors.surface,
147
- borderRadius: KasyRadius.lgBorderRadius,
148
- border: Border.all(
149
- color: context.colors.onBackground.withValues(alpha: 0.3),
150
- ),
145
+ child: Container(
146
+ decoration: BoxDecoration(
147
+ color: context.colors.surface,
148
+ borderRadius: KasyRadius.lgBorderRadius,
149
+ border: Border.all(
150
+ color: context.colors.onBackground.withValues(alpha: 0.3),
151
151
  ),
152
- child: Column(
153
- children: [
154
- _buildHeader(context),
155
- ...rows.asMap().entries.map(
156
- (entry) => _buildRow(
157
- context,
158
- entry.value,
159
- isLast: entry.key == rows.length - 1,
160
- ),
152
+ ),
153
+ child: Column(
154
+ children: [
155
+ _buildHeader(context),
156
+ ...rows.asMap().entries.map(
157
+ (entry) => _buildRow(
158
+ context,
159
+ entry.value,
160
+ isLast: entry.key == rows.length - 1,
161
161
  ),
162
- ],
163
- ),
162
+ ),
163
+ ],
164
164
  ),
165
165
  ),
166
166
  );
@@ -1,7 +1,8 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:flutter_riverpod/flutter_riverpod.dart';
3
3
  import 'package:kasy_kit/core/theme/theme.dart';
4
- import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
4
+ import 'package:kasy_kit/core/widgets/kasy_pressable_depth.dart';
5
+ import 'package:kasy_kit/i18n/translations.g.dart';
5
6
 
6
7
  class AppCloseButtonComponent extends ConsumerWidget {
7
8
  final ValueNotifier<bool> showCloseBtn;
@@ -36,34 +37,41 @@ class AppCloseButton extends StatelessWidget {
36
37
  @override
37
38
  Widget build(BuildContext context) {
38
39
  final VoidCallback? onClose = onTap;
39
- return Material(
40
- color: Colors.transparent,
41
- child: KasyFocusRing(
42
- enabled: onClose != null,
43
- onActivate: onClose,
44
- borderRadius: BorderRadius.circular(999),
45
- child: InkWell(
46
- canRequestFocus: false,
47
- onTapUp: (_) {
48
- onClose?.call();
49
- },
50
- child: Ink(
51
- width: 32,
52
- height: 32,
53
- decoration: BoxDecoration(
54
- shape: BoxShape.circle,
55
- color: context.colors.onBackground.withValues(alpha: 0.6),
56
- ),
57
- child: Center(
58
- child: Icon(
59
- KasyIcons.close,
60
- color: context.colors.background,
61
- size: KasyIconSize.lg,
62
- ),
63
- ),
64
- ),
40
+
41
+ // Dark translucent scrim keeps the glyph readable over any hero image.
42
+ final Widget circle = Container(
43
+ width: 32,
44
+ height: 32,
45
+ decoration: BoxDecoration(
46
+ shape: BoxShape.circle,
47
+ color: context.colors.onBackground.withValues(alpha: 0.6),
48
+ ),
49
+ child: Center(
50
+ child: Icon(
51
+ KasyIcons.close,
52
+ color: context.colors.background,
53
+ size: KasyIconSize.lg,
65
54
  ),
66
55
  ),
67
56
  );
57
+
58
+ // Disabled (no handler): render the inert scrim, no interaction.
59
+ if (onClose == null) {
60
+ return circle;
61
+ }
62
+
63
+ // KasyPressableDepth gives the kit's press-in depth, a web hover highlight
64
+ // and a keyboard focus ring (no Material ripple), plus a 44x44 tap target
65
+ // around the 32px visual. A light veil lifts the dark scrim on hover/press.
66
+ final BorderRadius pill = BorderRadius.circular(999);
67
+ return KasyPressableDepth(
68
+ onPressed: onClose,
69
+ semanticLabel: t.common.close,
70
+ focusable: true,
71
+ clipBorderRadius: pill,
72
+ focusBorderRadius: pill,
73
+ pressOverlayColor: context.colors.background.withValues(alpha: 0.2),
74
+ child: circle,
75
+ );
68
76
  }
69
77
  }
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
4
4
  import 'package:flutter_animate/flutter_animate.dart';
5
5
  import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
6
6
  import 'package:kasy_kit/core/theme/theme.dart';
7
- import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
7
+ import 'package:kasy_kit/core/widgets/kasy_hover.dart';
8
8
  import 'package:kasy_kit/features/subscriptions/ui/widgets/selectable_row.dart';
9
9
 
10
10
 
@@ -65,24 +65,29 @@ class _SelectableColGroupState<T> extends State<SelectableColGroup<T>> {
65
65
  children: [
66
66
  for (var i = 0; i < widget.items.length; i++)
67
67
  Expanded(
68
- child: KasyFocusRing(
69
- onActivate: () => _selectIndex(i),
68
+ child: KasyHover(
69
+ onTap: () => _selectIndex(i),
70
+ focusable: true,
70
71
  borderRadius: KasyRadius.lgBorderRadius,
71
- child: GestureDetector(
72
- onTap: () => _selectIndex(i),
73
- child: Animate(
74
- effects: [
75
- FadeEffect(
76
- delay: Duration(milliseconds: 100 + 100 * i),
77
- duration: const Duration(milliseconds: 500),
78
- ),
79
- ],
80
- child: SelectableCol.fromOption(
81
- key: ValueKey('item_$i'),
82
- widget.items[i],
83
- selected: selectedIndex == i,
84
- brightness: widget.brightness,
72
+ // The card paints its own animated selection border, so suppress
73
+ // the hover/press fill: KasyHover only adds the web click cursor
74
+ // and the keyboard focus ring. The heavy selection haptic is
75
+ // fired by _selectIndex, so opt out of the default tap haptic.
76
+ hapticEnabled: false,
77
+ hoverEnabled: false,
78
+ pressEnabled: false,
79
+ child: Animate(
80
+ effects: [
81
+ FadeEffect(
82
+ delay: Duration(milliseconds: 100 + 100 * i),
83
+ duration: const Duration(milliseconds: 500),
85
84
  ),
85
+ ],
86
+ child: SelectableCol.fromOption(
87
+ key: ValueKey('item_$i'),
88
+ widget.items[i],
89
+ selected: selectedIndex == i,
90
+ brightness: widget.brightness,
86
91
  ),
87
92
  ),
88
93
  ),
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
2
2
  import 'package:flutter_animate/flutter_animate.dart';
3
3
  import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
4
4
  import 'package:kasy_kit/core/theme/theme.dart';
5
- import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
5
+ import 'package:kasy_kit/core/widgets/kasy_hover.dart';
6
6
 
7
7
  typedef OnSelectItem<T> = void Function(T data);
8
8
 
@@ -51,12 +51,18 @@ class _SelectableRowGroupState<T> extends State<SelectableRowGroup<T>> {
51
51
  return Column(
52
52
  children: [
53
53
  for (var i = 0; i < widget.items.length; i++)
54
- KasyFocusRing(
55
- onActivate: () => _selectIndex(i),
54
+ KasyHover(
55
+ onTap: () => _selectIndex(i),
56
+ focusable: true,
56
57
  borderRadius: KasyRadius.mdBorderRadius,
57
- child: GestureDetector(
58
- onTap: () => _selectIndex(i),
59
- child: Padding(
58
+ // The card paints its own animated selection border, so suppress the
59
+ // hover/press fill: KasyHover only contributes the web click cursor
60
+ // and the keyboard focus ring. The heavy selection haptic is fired
61
+ // by _selectIndex, so opt out of the default tap haptic.
62
+ hapticEnabled: false,
63
+ hoverEnabled: false,
64
+ pressEnabled: false,
65
+ child: Padding(
60
66
  padding: EdgeInsets.only(
61
67
  bottom: i == widget.items.length - 1 ? 0 : KasySpacing.sm,
62
68
  ),
@@ -75,7 +81,6 @@ class _SelectableRowGroupState<T> extends State<SelectableRowGroup<T>> {
75
81
  ),
76
82
  ),
77
83
  ),
78
- ),
79
84
  ),
80
85
  ],
81
86
  );
@@ -460,8 +460,12 @@
460
460
  "cancel_button": "Cancel"
461
461
  },
462
462
  "review_popup": {
463
- "title": "Enjoying the app?",
464
- "description": "If the app has been helpful, a store review makes all the difference. It takes a few seconds and really helps us grow.",
463
+ "question_title": "Enjoying the app?",
464
+ "question_description": "Your answer helps us improve.",
465
+ "question_positive": "Yes, I am",
466
+ "question_negative": "Could be better",
467
+ "title": "Glad you like it!",
468
+ "description": "A store review makes all the difference. It takes a few seconds and really helps us grow.",
465
469
  "rate_button": "Write a review"
466
470
  },
467
471
  "navigation": {
@@ -460,8 +460,12 @@
460
460
  "cancel_button": "Cancelar"
461
461
  },
462
462
  "review_popup": {
463
- "title": "¿Te gusta la app?",
464
- "description": "Si la app te ha sido útil, una reseña en la tienda marca la diferencia. Toma unos segundos y nos ayuda mucho a crecer.",
463
+ "question_title": "¿Te gusta la app?",
464
+ "question_description": "Tu respuesta nos ayuda a mejorar.",
465
+ "question_positive": "Sí, me gusta",
466
+ "question_negative": "Podría mejorar",
467
+ "title": "¡Qué bueno que te guste!",
468
+ "description": "Una reseña en la tienda marca la diferencia. Toma unos segundos y nos ayuda mucho a crecer.",
465
469
  "rate_button": "Escribir una reseña"
466
470
  },
467
471
  "navigation": {
@@ -460,8 +460,12 @@
460
460
  "cancel_button": "Cancelar"
461
461
  },
462
462
  "review_popup": {
463
- "title": "Está gostando do app?",
464
- "description": "Se o app tem te ajudado, uma avaliação na loja faz toda a diferença. Leva poucos segundos e ajuda demais a gente a crescer.",
463
+ "question_title": "Está gostando do app?",
464
+ "question_description": "Sua resposta ajuda a gente a melhorar.",
465
+ "question_positive": "Sim, estou gostando",
466
+ "question_negative": "Poderia melhorar",
467
+ "title": "Que bom que curtiu!",
468
+ "description": "Uma avaliação na loja faz toda a diferença. Leva poucos segundos e ajuda demais a gente a crescer.",
465
469
  "rate_button": "Escrever uma avaliação"
466
470
  },
467
471
  "navigation": {
@@ -9,6 +9,7 @@ import 'package:kasy_kit/core/data/api/analytics_api.dart';
9
9
  import 'package:kasy_kit/core/data/models/user.dart';
10
10
  import 'package:kasy_kit/core/navigation/kasy_navigation_config.dart';
11
11
  import 'package:kasy_kit/core/navigation/kasy_page_transition.dart';
12
+ import 'package:kasy_kit/core/navigation/kasy_route_observer.dart';
12
13
  import 'package:kasy_kit/core/security/biometric_guard.dart';
13
14
  import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
14
15
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
@@ -156,6 +157,7 @@ GoRouter generateRouter({
156
157
  observers: [
157
158
  AnalyticsObserver(analyticsApi: MixpanelAnalyticsApi.instance()),
158
159
  KasyChromeVisibilityObserver(),
160
+ kasyRouteObserver,
159
161
 
160
162
  ...?observers,
161
163
  ],
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
16
16
  # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17
17
  # In Windows, build-name is used as the major, minor, and patch parts
18
18
  # of the product and file versions while build-number is used as the build suffix.
19
- version: 1.0.0+66
19
+ version: 1.0.0+70
20
20
 
21
21
  environment:
22
22
  sdk: ^3.11.0
@@ -0,0 +1,70 @@
1
+ import 'package:flutter_test/flutter_test.dart';
2
+ import 'package:kasy_kit/components/components.dart';
3
+
4
+ /// The per-screen desktop bar override is just data: a screen transforms the
5
+ /// shell default into its own [KasyAppBarConfig]. These lock that logic.
6
+ void main() {
7
+ group('KasyAppBarConfig', () {
8
+ test('bare config shows every element (mirrors the shell default)', () {
9
+ const KasyAppBarConfig c = KasyAppBarConfig();
10
+ expect(c.showSearch, isTrue);
11
+ expect(c.showThemeToggle, isTrue);
12
+ expect(c.showNotifications, isTrue);
13
+ expect(c.showCreate, isTrue);
14
+ expect(c.showAvatar, isTrue);
15
+ });
16
+
17
+ test('only() shows just the listed elements but keeps their wiring', () {
18
+ void bell() {}
19
+ void toggle() {}
20
+ final KasyAppBarConfig base = KasyAppBarConfig(
21
+ onNotifications: bell,
22
+ onToggleTheme: toggle,
23
+ );
24
+
25
+ final KasyAppBarConfig cfg = base.only(notifications: true, themeToggle: true);
26
+
27
+ expect(cfg.showNotifications, isTrue);
28
+ expect(cfg.showThemeToggle, isTrue);
29
+ expect(cfg.showSearch, isFalse);
30
+ expect(cfg.showCreate, isFalse);
31
+ expect(cfg.showAvatar, isFalse);
32
+ // The shell's bell/theme callbacks survive the override.
33
+ expect(cfg.onNotifications, same(bell));
34
+ expect(cfg.onToggleTheme, same(toggle));
35
+ });
36
+
37
+ test('search preset = search + theme only', () {
38
+ const KasyAppBarConfig cfg = KasyAppBarConfig.search();
39
+ expect(cfg.showSearch, isTrue);
40
+ expect(cfg.showThemeToggle, isTrue);
41
+ expect(cfg.showNotifications, isFalse);
42
+ expect(cfg.showCreate, isFalse);
43
+ expect(cfg.showAvatar, isFalse);
44
+ });
45
+
46
+ test('minimal preset = theme toggle only', () {
47
+ const KasyAppBarConfig cfg = KasyAppBarConfig.minimal();
48
+ expect(cfg.showThemeToggle, isTrue);
49
+ expect(cfg.showSearch, isFalse);
50
+ expect(cfg.showNotifications, isFalse);
51
+ expect(cfg.showCreate, isFalse);
52
+ expect(cfg.showAvatar, isFalse);
53
+ });
54
+
55
+ test('copyWith with no changes is value-equal (no churn on the bar)', () {
56
+ const KasyAppBarConfig a = KasyAppBarConfig();
57
+ final KasyAppBarConfig b = a.copyWith();
58
+ expect(a, equals(b));
59
+ expect(a.hashCode, equals(b.hashCode));
60
+ });
61
+
62
+ test('copyWith changes only the named field', () {
63
+ const KasyAppBarConfig a = KasyAppBarConfig();
64
+ final KasyAppBarConfig b = a.copyWith(showSearch: false);
65
+ expect(b.showSearch, isFalse);
66
+ expect(b.showCreate, isTrue);
67
+ expect(a == b, isFalse);
68
+ });
69
+ });
70
+ }
@@ -1,81 +0,0 @@
1
- import 'package:flutter/material.dart';
2
- import 'package:kasy_kit/core/theme/theme.dart';
3
- import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
4
-
5
- typedef PremiumWideBannerOnTap = void Function();
6
-
7
- class PremiumWideBanner extends StatelessWidget {
8
- final Color bgColor;
9
- final String imagePath;
10
- final String smallText;
11
- final String title;
12
- final PremiumWideBannerOnTap onTap;
13
-
14
- const PremiumWideBanner({
15
- super.key,
16
- required this.bgColor,
17
- required this.imagePath,
18
- required this.smallText,
19
- required this.title,
20
- required this.onTap,
21
- });
22
-
23
- @override
24
- Widget build(BuildContext context) {
25
- return LayoutBuilder(builder: (context, constraints) {
26
- return Material(
27
- color: Colors.transparent,
28
- child: Ink(
29
- // height: 116,
30
- width: constraints.maxWidth,
31
- decoration: BoxDecoration(
32
- color: bgColor,
33
- // border: Border.all(color: borderColor),
34
- ),
35
- child: KasyFocusRing(
36
- onActivate: () => onTap(),
37
- borderRadius: BorderRadius.zero,
38
- child: InkWell(
39
- canRequestFocus: false,
40
- onTap: () => onTap(),
41
- child: Row(
42
- children: [
43
- Image.asset(
44
- imagePath,
45
- width: constraints.maxWidth * 0.32,
46
- // height: 116,
47
- fit: BoxFit.cover,
48
- ),
49
- const SizedBox(width: KasySpacing.lg),
50
- Expanded(
51
- child: Column(
52
- crossAxisAlignment: CrossAxisAlignment.start,
53
- mainAxisAlignment: MainAxisAlignment.center,
54
- children: [
55
- Text(
56
- smallText.toUpperCase(),
57
- style: context.kasyTextTheme.sectionLabel.copyWith(
58
- color: context.colors.premiumBannerText,
59
- ),
60
- ),
61
- const SizedBox(height: KasySpacing.sm),
62
- Text(
63
- title,
64
- style: context.textTheme.titleLarge?.copyWith(
65
- color: context.colors.onPrimary,
66
- ),
67
- overflow: TextOverflow.clip,
68
- ),
69
- ],
70
- ),
71
- ),
72
- const SizedBox(width: KasySpacing.lg),
73
- ],
74
- ),
75
- ),
76
- ),
77
- ),
78
- );
79
- });
80
- }
81
- }