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.
- package/lib/scaffold/CHANGELOG.json +9 -0
- package/package.json +1 -1
- package/templates/firebase/AGENTS.md +2 -2
- package/templates/firebase/DESIGN_SYSTEM.md +3 -2
- package/templates/firebase/lib/components/components.dart +4 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +11 -1
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +23 -7
- package/templates/firebase/lib/core/chrome/app_bar_config.dart +1 -1
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +96 -14
- package/templates/firebase/lib/core/web_viewport_scale.dart +8 -7
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +43 -13
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart +4 -0
- package/templates/firebase/lib/i18n/en.i18n.json +6 -2
- package/templates/firebase/lib/i18n/es.i18n.json +6 -2
- package/templates/firebase/lib/i18n/pt.i18n.json +6 -2
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/app_bar_config_test.dart +70 -0
|
@@ -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
|
@@ -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
|
|
68
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
80
|
-
//
|
|
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:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
showAvatar: false,
|
|
101
|
+
child: ValueListenableBuilder<KasyAppBarConfig>(
|
|
102
|
+
valueListenable: _appBarConfig,
|
|
103
|
+
builder: (context, config, _) =>
|
|
104
|
+
KasyAppBar.fromConfig(config),
|
|
89
105
|
),
|
|
90
106
|
),
|
|
91
107
|
),
|
|
@@ -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
|
|
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
|
-
///
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
///
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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:
|
|
143
|
+
title: t.title,
|
|
60
144
|
titleCentered: true,
|
|
61
|
-
message:
|
|
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:
|
|
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.
|
|
11
|
-
/// without the user touching browser zoom — a gentle correction (
|
|
12
|
-
/// midpoint between full size and a total undo
|
|
13
|
-
///
|
|
14
|
-
///
|
|
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.
|
|
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 ~
|
|
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
|
|
1964
|
+
return _ApplicationBarPreview(config: _demoAppBarConfig());
|
|
1947
1965
|
}
|
|
1948
1966
|
|
|
1949
1967
|
Widget _buildAppBarApplicationBadgeVariant(BuildContext context) {
|
|
1950
|
-
return
|
|
1968
|
+
return _ApplicationBarPreview(
|
|
1969
|
+
config: _demoAppBarConfig().copyWith(showNotificationBadge: true),
|
|
1970
|
+
);
|
|
1951
1971
|
}
|
|
1952
1972
|
|
|
1953
1973
|
Widget _buildAppBarApplicationNoAvatarVariant(BuildContext context) {
|
|
1954
|
-
return
|
|
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
|
-
|
|
3888
|
-
|
|
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({
|
|
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.
|
|
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(
|
package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart
CHANGED
|
@@ -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
|
-
"
|
|
464
|
-
"
|
|
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
|
-
"
|
|
464
|
-
"
|
|
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
|
-
"
|
|
464
|
-
"
|
|
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+
|
|
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
|
+
}
|