kasy-cli 1.39.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.
@@ -1,4 +1,13 @@
1
1
  {
2
+ "1.39.1": {
3
+ "modules": {
4
+ "core": {
5
+ "pt": "Ajuste fino da proporção da web: a escala passou de 0.95 para 0.93 (a interface fica um pouco menor, mais perto do tamanho do app nativo). É só calibragem do mesmo knob único (kWebViewportScale) em todos os breakpoints; o nativo continua em 1.0 e dá pra desligar tudo numa linha (kWebViewportScaleEnabled).",
6
+ "en": "Fine-tuned the web proportion: the scale went from 0.95 to 0.93 (the interface is a touch smaller, closer to the native app size). It is just calibration of the same single knob (kWebViewportScale) across every breakpoint; native stays at 1.0 and the whole thing can be turned off in one line (kWebViewportScaleEnabled).",
7
+ "es": "Ajuste fino de la proporción web: la escala pasó de 0.95 a 0.93 (la interfaz queda un poco más pequeña, más cerca del tamaño del app nativo). Es solo calibración del mismo knob único (kWebViewportScale) en todos los breakpoints; el nativo se mantiene en 1.0 y todo se puede apagar en una línea (kWebViewportScaleEnabled)."
8
+ }
9
+ }
10
+ },
2
11
  "1.39.0": {
3
12
  "modules": {
4
13
  "core": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.39.0",
3
+ "version": "1.39.1",
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"
@@ -64,8 +64,8 @@ Typography collapses `large`+`xlarge` into one "desktop" tier (same sizes); the
64
64
  - **Native mobile** is straightforward: it's the Mobile breakpoint and never
65
65
  scales the viewport.
66
66
  - **Web render scale** (`WebViewportScale`) is a *separate* concern: Flutter web
67
- renders ~10% large, so **desktop only** scales the whole UI to `0.95`. Tablet
68
- and mobile web render at natural size, like native. This is NOT the typography
67
+ renders ~10% large at every width, so the whole web UI (phone, tablet, desktop)
68
+ is scaled to `0.93`; native renders at natural `1.0`. This is NOT the typography
69
69
  scale — don't conflate the two.
70
70
  - **Screen width follows content type** (two tiers): read/form screens (Settings,
71
71
  Notifications) are contained + centred at `kKasyContentMaxWidth` (600) — set
@@ -107,11 +107,12 @@ separate concern from type sizes, sitting in a layer ON TOP of the responsive
107
107
  system — it does NOT replace breakpoints, the type ramp or spacing (turn it off
108
108
  and the app is still fully responsive, just larger). Flutter web renders ~10%
109
109
  larger than an equivalent native/HTML app at 100% zoom, **at every width**. So the
110
- whole web app — phone, tablet and desktop — is scaled to `0.95` (~5%; a true
110
+ whole web app — phone, tablet and desktop — is scaled to `0.93` (~7%; a true
111
111
  viewport scale, so layout, scrolling and hit-testing stay correct), making web
112
112
  read close to the native baseline with **no size jump across breakpoints**. `0.90`
113
113
  fully undoes the 10% but reads small on a monitor (native sizes are tuned for a
114
- phone held close), so `0.95` (the midpoint) is the calibrated default. Two knobs:
114
+ phone held close), so `0.93` (a touch past the midpoint, still short of a full
115
+ undo) is the calibrated default. Two knobs:
115
116
  `kWebViewportScaleEnabled` (master on/off — `false` makes web render at native
116
117
  `1.0`) and `kWebViewportScale` (the value). Two things are never scaled, both at
117
118
  native `1.0`: **native** itself (gated on `kIsWeb`, so iOS/Android/macOS/Windows
@@ -1,11 +1,14 @@
1
1
  /// Kasy UI components — Material-backed wrappers with a stable kit API.
2
2
  ///
3
- /// Includes screen chrome ([KasyAppBar], [KasyOverlayScaffold], [kasyOverlayPaddedSlivers]).
3
+ /// Includes screen chrome ([KasyAppBar], [KasyOverlayScaffold], [kasyOverlayPaddedSlivers])
4
+ /// and its per-screen desktop config ([KasyAppBarConfig], [KasyAppBarConfigurator]).
4
5
  ///
5
6
  /// Import:
6
7
  /// import 'package:kasy_kit/components/components.dart';
7
8
  library;
8
9
 
10
+ export 'package:kasy_kit/core/chrome/app_bar_config.dart';
11
+ export 'package:kasy_kit/core/chrome/app_bar_scope.dart';
9
12
  export 'kasy_accordion.dart';
10
13
  export 'kasy_alert.dart';
11
14
  export 'kasy_app_bar.dart';
@@ -810,6 +810,12 @@ class KasyOverlayScaffold extends StatelessWidget {
810
810
  /// the overlay pattern (bar floats over full-height scroll content).
811
811
  final bool hideAppBarOnScroll;
812
812
 
813
+ /// Desktop only: adapt the shell's application bar for this screen. Receives the
814
+ /// shell default; return e.g. `bar.only(search: true)`. No-op on phone/tablet
815
+ /// (there the page chrome above is what shows). Shortcut for wrapping the body
816
+ /// in a [KasyAppBarConfigurator].
817
+ final KasyAppBarConfigure? applicationBar;
818
+
813
819
  const KasyOverlayScaffold({
814
820
  super.key,
815
821
  required this.title,
@@ -826,6 +832,7 @@ class KasyOverlayScaffold extends StatelessWidget {
826
832
  this.onRefresh,
827
833
  this.refreshIndicatorDisplacement = 48,
828
834
  this.hideAppBarOnScroll = false,
835
+ this.applicationBar,
829
836
  });
830
837
 
831
838
  @override
@@ -856,7 +863,7 @@ class KasyOverlayScaffold extends StatelessWidget {
856
863
  child: scrollView,
857
864
  );
858
865
  }
859
- return Scaffold(
866
+ final Widget scaffold = Scaffold(
860
867
  backgroundColor: bg,
861
868
  body: Stack(
862
869
  fit: StackFit.expand,
@@ -878,6 +885,9 @@ class KasyOverlayScaffold extends StatelessWidget {
878
885
  ],
879
886
  ),
880
887
  );
888
+ final KasyAppBarConfigure? configure = applicationBar;
889
+ if (configure == null) return scaffold;
890
+ return KasyAppBarConfigurator(configure: configure, child: scaffold);
881
891
  }
882
892
  }
883
893
 
@@ -41,6 +41,20 @@ class _WebContentWrapperState extends State<WebContentWrapper> {
41
41
  skipTraversal: true,
42
42
  );
43
43
 
44
+ /// Shell-wide default for the desktop application bar: empty search, theme
45
+ /// toggle, the data-aware notifications bell, quick-create, and no avatar (the
46
+ /// sidebar owns the profile). Screens adapt this via [KasyAppBarConfigurator].
47
+ late final KasyAppBarConfig _defaultAppBarConfig = KasyAppBarConfig(
48
+ onToggleTheme: () => ThemeProvider.of(context).toggle(),
49
+ notifications: const WebNotificationsBell(),
50
+ onCreate: () {},
51
+ showAvatar: false,
52
+ );
53
+
54
+ /// Live config the bar renders; starts at the default, screens publish into it.
55
+ late final ValueNotifier<KasyAppBarConfig> _appBarConfig =
56
+ ValueNotifier<KasyAppBarConfig>(_defaultAppBarConfig);
57
+
44
58
  @override
45
59
  void initState() {
46
60
  super.initState();
@@ -53,6 +67,7 @@ class _WebContentWrapperState extends State<WebContentWrapper> {
53
67
  kasyContentFocusTarget = null;
54
68
  }
55
69
  _contentFocus.dispose();
70
+ _appBarConfig.dispose();
56
71
  super.dispose();
57
72
  }
58
73
 
@@ -71,21 +86,22 @@ class _WebContentWrapperState extends State<WebContentWrapper> {
71
86
  // page-level KasyAppBar hides on desktop here (the application bar owns the
72
87
  // chrome). Outside this scope (full-screen pushed routes) the bar stays visible.
73
88
  return KasyAppBarScope(
89
+ config: _appBarConfig,
90
+ defaultConfig: _defaultAppBarConfig,
74
91
  child: ColoredBox(
75
92
  color: context.colors.background,
76
93
  child: Column(
77
94
  crossAxisAlignment: CrossAxisAlignment.stretch,
78
95
  children: [
79
- // The sidebar carries the user profile here, so the header drops its
80
- // avatar (showAvatar: false) to avoid duplicating the account chip.
96
+ // One application bar for the whole shell; each screen tunes it through
97
+ // a KasyAppBarConfigurator that publishes into [_appBarConfig].
81
98
  FocusTraversalOrder(
82
99
  order: const NumericFocusOrder(2),
83
100
  child: FocusTraversalGroup(
84
- child: KasyAppBar.application(
85
- onToggleTheme: () => ThemeProvider.of(context).toggle(),
86
- notifications: const WebNotificationsBell(),
87
- onCreate: () {},
88
- showAvatar: false,
101
+ child: ValueListenableBuilder<KasyAppBarConfig>(
102
+ valueListenable: _appBarConfig,
103
+ builder: (context, config, _) =>
104
+ KasyAppBar.fromConfig(config),
89
105
  ),
90
106
  ),
91
107
  ),
@@ -63,7 +63,7 @@ class KasyAppBarConfig {
63
63
  this.showCreate = true,
64
64
  this.createLabel = 'Create',
65
65
  this.onCreate,
66
- this.showAvatar = false,
66
+ this.showAvatar = true,
67
67
  this.avatar,
68
68
  this.avatarGradient,
69
69
  this.onAvatarTap,
@@ -1,21 +1,28 @@
1
1
  import 'package:flutter/foundation.dart' show kIsWeb;
2
2
  import 'package:flutter/material.dart';
3
3
  import 'package:flutter_riverpod/flutter_riverpod.dart';
4
+ import 'package:go_router/go_router.dart';
4
5
  import 'package:kasy_kit/components/components.dart';
5
6
  import 'package:kasy_kit/core/data/api/analytics_api.dart';
7
+ import 'package:kasy_kit/core/rating/models/review.dart';
6
8
  import 'package:kasy_kit/core/rating/providers/rating_repository.dart';
7
9
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
8
10
  import 'package:kasy_kit/core/theme/theme.dart';
9
11
  import 'package:kasy_kit/i18n/translations.g.dart';
10
12
  import 'package:logger/logger.dart';
11
13
 
12
- /// Shows the in-app review dialog. Prefer a stable [context] (not a sheet that
14
+ /// Shows the in-app review funnel. Prefer a stable [context] (not a sheet that
13
15
  /// will be popped before the async work finishes).
14
16
  ///
15
- /// A clean [KasyDialog] focused on a single goal: a positive store rating. A
16
- /// gold star, a warm title/message and one action (write a review on the
17
- /// Android/Play or iOS App Store). Dismissing via the dialog's own close button
18
- /// just defers the next ask and keeps the user where they are.
17
+ /// Two-step "rating protection" funnel:
18
+ /// 1. A sentiment gate ("Enjoying the app?") with a positive / negative pick.
19
+ /// 2a. Positive a gold-star [KasyDialog] that sends the user to the store
20
+ /// (App Store / Play Store) to write a public review.
21
+ /// 2b. Negative → the private `/feedback` screen, so an unhappy user vents to
22
+ /// us instead of leaving a low public rating.
23
+ ///
24
+ /// Dismissing any step just defers the next ask and keeps the user where they
25
+ /// are. Returns true once the funnel was shown (regardless of the branch taken).
19
26
  Future<bool> showReviewDialog(
20
27
  BuildContext context,
21
28
  WidgetRef ref, {
@@ -23,7 +30,7 @@ Future<bool> showReviewDialog(
23
30
  }) async {
24
31
  // Store reviews are native-only (App Store / Play Store). In production the
25
32
  // auto prompt never shows on web (nowhere to send the user). But a forced
26
- // call — the admin preview — still shows the dialog on web so its design can
33
+ // call — the admin preview — still shows the funnel on web so its design can
27
34
  // be reviewed there; only the store action won't do anything.
28
35
  if (kIsWeb && !force) {
29
36
  return false;
@@ -46,26 +53,102 @@ Future<bool> showReviewDialog(
46
53
  return false;
47
54
  }
48
55
 
49
- await showKasyDialog<void>(
56
+ // Defer the next ask up front, so any outcome (dismiss, positive, negative)
57
+ // counts as "asked" and we don't pester the user again right away.
58
+ await rating.delay();
59
+ analytics.logEvent('rating_funnel_open', {});
60
+ if (!context.mounted) {
61
+ return false;
62
+ }
63
+
64
+ // Step 1 — sentiment gate.
65
+ final bool? enjoying = await _askEnjoying(context, analytics);
66
+ if (enjoying == null) {
67
+ return true; // dismissed without choosing
68
+ }
69
+
70
+ // Step 2b — negative branch: keep the bad review private.
71
+ if (!enjoying) {
72
+ analytics.logEvent('rating_funnel_negative', {});
73
+ if (context.mounted) {
74
+ await context.push('/feedback');
75
+ }
76
+ return true;
77
+ }
78
+
79
+ // Step 2a — positive branch: send the happy user to the store.
80
+ analytics.logEvent('rating_funnel_positive', {});
81
+ if (!context.mounted) {
82
+ return true;
83
+ }
84
+ await _askStoreReview(context, ratingRepository, rating, analytics);
85
+ return true;
86
+ }
87
+
88
+ /// Step 1: the sentiment gate. Returns true (enjoying), false (could be better)
89
+ /// or null (dismissed without choosing).
90
+ Future<bool?> _askEnjoying(BuildContext context, AnalyticsApi analytics) {
91
+ return showKasyDialog<bool>(
50
92
  context: context,
51
93
  builder: (dialogContext) {
52
- ratingRepository.delay();
53
- final translations = Translations.of(dialogContext).review_popup;
94
+ final t = Translations.of(dialogContext).review_popup;
95
+ return KasyDialog(
96
+ leadingIcon: KasyIcons.favorite,
97
+ iconTone: KasyDialogIconTone.info,
98
+ title: t.question_title,
99
+ titleCentered: true,
100
+ message: t.question_description,
101
+ onClose: () {
102
+ analytics.logEvent('rating_funnel_dismiss', {});
103
+ Navigator.of(dialogContext).pop();
104
+ },
105
+ footer: Column(
106
+ crossAxisAlignment: CrossAxisAlignment.stretch,
107
+ children: [
108
+ KasyButton(
109
+ label: t.question_positive,
110
+ expand: true,
111
+ onPressed: () => Navigator.of(dialogContext).pop(true),
112
+ ),
113
+ const SizedBox(height: KasySpacing.sm),
114
+ KasyButton(
115
+ label: t.question_negative,
116
+ variant: KasyButtonVariant.soft,
117
+ expand: true,
118
+ onPressed: () => Navigator.of(dialogContext).pop(false),
119
+ ),
120
+ ],
121
+ ),
122
+ );
123
+ },
124
+ );
125
+ }
126
+
127
+ /// Step 2a (positive branch): the celebratory store-review dialog.
128
+ Future<void> _askStoreReview(
129
+ BuildContext context,
130
+ RatingRepository ratingRepository,
131
+ Review rating,
132
+ AnalyticsApi analytics,
133
+ ) {
134
+ return showKasyDialog<void>(
135
+ context: context,
136
+ builder: (dialogContext) {
137
+ final t = Translations.of(dialogContext).review_popup;
54
138
  return KasyDialog(
55
139
  leadingIcon: KasyIcons.star,
56
140
  // Gold/amber star via KasyDialog's default `warning` tone — the design
57
141
  // system's amber (#F5A524 light / #F7B750 dark), the natural colour for
58
142
  // a rating star.
59
- title: translations.title,
143
+ title: t.title,
60
144
  titleCentered: true,
61
- message: translations.description,
145
+ message: t.description,
62
146
  onClose: () {
63
147
  analytics.logEvent('rating_popup_close', {});
64
- rating.delay();
65
148
  Navigator.of(dialogContext).pop();
66
149
  },
67
150
  footer: KasyButton(
68
- label: translations.rate_button,
151
+ label: t.rate_button,
69
152
  expand: true,
70
153
  onPressed: () {
71
154
  analytics.logEvent('rating_popup_show', {});
@@ -78,5 +161,4 @@ Future<bool> showReviewDialog(
78
161
  );
79
162
  },
80
163
  );
81
- return true;
82
164
  }
@@ -7,11 +7,12 @@ import 'package:kasy_kit/core/web_screen_width.dart';
7
7
  ///
8
8
  /// Flutter web renders ~10% larger than an equivalent native/HTML app at the
9
9
  /// browser's 100% zoom, at any width — so the whole web UI feels oversized.
10
- /// `0.95` walks back ~5% so the web app reads close to the native baseline
11
- /// without the user touching browser zoom — a gentle correction (the exact
12
- /// midpoint between full size and a total undo) that takes the "oversized" edge
13
- /// off Flutter web without making things feel small (0.90 fully undoes the 10%
14
- /// but reads small on a monitor one number to nudge). On desktop it also acts
10
+ /// `0.93` walks back ~7% so the web app reads close to the native baseline
11
+ /// without the user touching browser zoom — a gentle correction (a bit stronger
12
+ /// than the midpoint between full size and a total undo, but still short of
13
+ /// fully undoing the 10%) that takes the "oversized" edge off Flutter web
14
+ /// without making things feel small (0.90 fully undoes the 10% but reads small
15
+ /// on a monitor — one number to nudge). On desktop it also acts
15
16
  /// as a cap: a
16
17
  /// screen below the design target (high OS scale) reduces it further to pin the
17
18
  /// layout (see [kWebViewportScaleTargetWidth]); phone/tablet take the flat cap
@@ -24,11 +25,11 @@ import 'package:kasy_kit/core/web_screen_width.dart';
24
25
  /// - The in-app DEVICE PREVIEW — it simulates a native device, so it must show
25
26
  /// the native 1.0 truth; the scale is skipped once the preview frame is up
26
27
  /// (see main.dart, gated on `webDevicePreviewActiveNotifier`).
27
- const double kWebViewportScale = 0.95;
28
+ const double kWebViewportScale = 0.93;
28
29
 
29
30
  /// Master on/off for the web render scale — the single knob.
30
31
  ///
31
- /// `true` (default): the web app is rendered ~5% smaller than native via
32
+ /// `true` (default): the web app is rendered ~7% smaller than native via
32
33
  /// [kWebViewportScale], correcting Flutter web's oversized feel on desktop while
33
34
  /// native stays at true 1.0. This is a deliberate, web-only density correction
34
35
  /// (the same technique `responsive_framework`'s autoScale productizes), applied
@@ -101,6 +101,15 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
101
101
  label: 'Application no avatar (desktop)',
102
102
  builder: _buildAppBarApplicationNoAvatarVariant,
103
103
  ),
104
+ // Per-screen presets — same bar, a screen picking its own chrome.
105
+ ComponentPreviewVariant(
106
+ label: 'Search only (desktop)',
107
+ builder: _buildAppBarApplicationSearchOnlyVariant,
108
+ ),
109
+ ComponentPreviewVariant(
110
+ label: 'Notifications only (desktop)',
111
+ builder: _buildAppBarApplicationNotificationsOnlyVariant,
112
+ ),
104
113
  ],
105
114
  );
106
115
  case 'Button':
@@ -1942,16 +1951,42 @@ Widget _buildAppBarMenuVariant(BuildContext context) {
1942
1951
  );
1943
1952
  }
1944
1953
 
1954
+ /// A fully-wired desktop config for previews — every element active, so each
1955
+ /// variant can switch one thing on/off and read clearly.
1956
+ KasyAppBarConfig _demoAppBarConfig() => KasyAppBarConfig(
1957
+ onToggleTheme: () {},
1958
+ onNotifications: () {},
1959
+ onCreate: () {},
1960
+ onAvatarTap: () {},
1961
+ );
1962
+
1945
1963
  Widget _buildAppBarApplicationVariant(BuildContext context) {
1946
- return const _ApplicationBarPreview();
1964
+ return _ApplicationBarPreview(config: _demoAppBarConfig());
1947
1965
  }
1948
1966
 
1949
1967
  Widget _buildAppBarApplicationBadgeVariant(BuildContext context) {
1950
- return const _ApplicationBarPreview(showBadge: true);
1968
+ return _ApplicationBarPreview(
1969
+ config: _demoAppBarConfig().copyWith(showNotificationBadge: true),
1970
+ );
1951
1971
  }
1952
1972
 
1953
1973
  Widget _buildAppBarApplicationNoAvatarVariant(BuildContext context) {
1954
- return const _ApplicationBarPreview(showAvatar: false);
1974
+ return _ApplicationBarPreview(
1975
+ config: _demoAppBarConfig().copyWith(showAvatar: false),
1976
+ );
1977
+ }
1978
+
1979
+ // Per-screen presets: the same component, different chrome chosen by a screen.
1980
+ Widget _buildAppBarApplicationSearchOnlyVariant(BuildContext context) {
1981
+ return _ApplicationBarPreview(
1982
+ config: _demoAppBarConfig().only(search: true, themeToggle: true),
1983
+ );
1984
+ }
1985
+
1986
+ Widget _buildAppBarApplicationNotificationsOnlyVariant(BuildContext context) {
1987
+ return _ApplicationBarPreview(
1988
+ config: _demoAppBarConfig().only(notifications: true, themeToggle: true),
1989
+ );
1955
1990
  }
1956
1991
 
1957
1992
  Widget _buildButtonSizesVariant(BuildContext context) {
@@ -3884,10 +3919,11 @@ class _AccordionPreviewState extends State<_AccordionPreview> {
3884
3919
  /// bar + faux sidebar + content) so the preview reads as the desktop chrome it
3885
3920
  /// is — the responsive desktop half of [KasyAppBar].
3886
3921
  class _ApplicationBarPreview extends StatelessWidget {
3887
- final bool showBadge;
3888
- final bool showAvatar;
3922
+ /// The exact config a screen would publish — rendered through the same
3923
+ /// [KasyAppBar.fromConfig] the shell uses, so the preview mirrors real usage.
3924
+ final KasyAppBarConfig config;
3889
3925
 
3890
- const _ApplicationBarPreview({this.showBadge = false, this.showAvatar = true});
3926
+ const _ApplicationBarPreview({required this.config});
3891
3927
 
3892
3928
  /// Width the mock window is laid out at. The application bar is desktop chrome
3893
3929
  /// (220px search + actions), so it needs a desktop-class width — we render at
@@ -3916,13 +3952,7 @@ class _ApplicationBarPreview extends StatelessWidget {
3916
3952
  children: [
3917
3953
  const _BrowserTopBar(),
3918
3954
  // The real bar, flush — no corner radius of its own.
3919
- KasyAppBar.application(
3920
- showNotificationBadge: showBadge,
3921
- showAvatar: showAvatar,
3922
- onNotifications: () {},
3923
- onCreate: () {},
3924
- onAvatarTap: () {},
3925
- ),
3955
+ KasyAppBar.fromConfig(config),
3926
3956
  const SizedBox(
3927
3957
  height: 150,
3928
3958
  child: Row(
@@ -5,6 +5,7 @@ import 'package:kasy_kit/components/kasy_app_bar.dart';
5
5
  import 'package:kasy_kit/core/home_widgets/home_widget_mywidget_service.dart';
6
6
  import 'package:kasy_kit/core/theme/theme.dart';
7
7
  import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
8
+ import 'package:kasy_kit/core/widgets/responsive_layout.dart';
8
9
  import 'package:kasy_kit/features/settings/ui/widgets/admin_card.dart';
9
10
  import 'package:kasy_kit/i18n/translations.g.dart';
10
11
 
@@ -18,6 +19,9 @@ class AdminHomeWidgets extends ConsumerWidget {
18
19
  child: KasyOverlayScaffold(
19
20
  title: t.settings.admin.home_widgets_title,
20
21
  onBack: () => context.pop(),
22
+ // Contain + center the single utility card on desktop so it never
23
+ // stretches edge-to-edge, matching Notifications / Reminders.
24
+ maxContentWidth: kKasyContentMaxWidth,
21
25
  slivers: [
22
26
  SliverList.list(
23
27
  children: [
@@ -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": {
@@ -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+67
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
+ }