kasy-cli 1.24.0 → 1.25.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.
- package/lib/utils/flutter-install.js +26 -1
- package/package.json +1 -1
- package/templates/firebase/lib/components/kasy_card.dart +4 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +6 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +1 -0
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +37 -1
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +1 -0
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +10 -5
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +1 -0
- package/templates/firebase/lib/features/settings/settings_page.dart +17 -1
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +63 -48
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +2 -0
|
@@ -43,12 +43,24 @@ $ProgressPreference = 'SilentlyContinue'
|
|
|
43
43
|
$dest = Join-Path $env:LOCALAPPDATA 'flutter'
|
|
44
44
|
$bin = Join-Path $dest 'bin'
|
|
45
45
|
|
|
46
|
+
function Sync-Path {
|
|
47
|
+
# Pull the persistent PATH (Machine + User) into THIS session so a tool just
|
|
48
|
+
# installed by winget (Git) becomes visible right away.
|
|
49
|
+
$m = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
|
50
|
+
$u = [Environment]::GetEnvironmentVariable('Path', 'User')
|
|
51
|
+
$env:Path = (@($m, $u) | Where-Object { $_ }) -join ';'
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
# 1. Git — Flutter uses it internally and won't run without it.
|
|
47
55
|
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
|
48
56
|
if (Get-Command winget -ErrorAction SilentlyContinue) {
|
|
49
57
|
winget install --id Git.Git -e --source winget --silent --accept-source-agreements --accept-package-agreements --disable-interactivity | Out-Null
|
|
58
|
+
Sync-Path
|
|
50
59
|
}
|
|
51
60
|
}
|
|
61
|
+
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
|
62
|
+
throw 'Git is required for Flutter but is not available. Install Git and run this again.'
|
|
63
|
+
}
|
|
52
64
|
|
|
53
65
|
# 2. Flutter SDK — download + unzip, unless it's already there.
|
|
54
66
|
if (-not (Test-Path (Join-Path $bin 'flutter.bat'))) {
|
|
@@ -67,8 +79,21 @@ if (-not (Test-Path (Join-Path $bin 'flutter.bat'))) {
|
|
|
67
79
|
[System.IO.Compression.ZipFile]::ExtractToDirectory($zip, $env:LOCALAPPDATA)
|
|
68
80
|
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
|
69
81
|
}
|
|
82
|
+
if (-not (Test-Path (Join-Path $bin 'flutter.bat'))) {
|
|
83
|
+
throw 'Flutter download/extract did not produce flutter.bat.'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# 3. Make flutter usable in THIS session and warm it up. The very first flutter
|
|
87
|
+
# command downloads the bundled Dart SDK (a few hundred MB), which can take
|
|
88
|
+
# minutes — doing it here, inside the installer, keeps the caller's follow-up
|
|
89
|
+
# "flutter --version" check fast instead of timing out on the cold first run.
|
|
90
|
+
if ($env:Path -notlike "*$bin*") { $env:Path = "$bin;$env:Path" }
|
|
91
|
+
& (Join-Path $bin 'flutter.bat') --version 2>&1 | Out-Null
|
|
92
|
+
if ($LASTEXITCODE -ne 0) {
|
|
93
|
+
throw 'Flutter was installed but its first run failed (Dart SDK bootstrap). Check your internet connection and run this again.'
|
|
94
|
+
}
|
|
70
95
|
|
|
71
|
-
#
|
|
96
|
+
# 4. Persist flutter\\bin on the User PATH so every future terminal finds it.
|
|
72
97
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
|
73
98
|
if (-not $userPath) { $userPath = '' }
|
|
74
99
|
if (($userPath -split ';') -notcontains $bin) {
|
package/package.json
CHANGED
|
@@ -119,6 +119,10 @@ class KasyCard extends StatelessWidget {
|
|
|
119
119
|
onPressed: onTap!,
|
|
120
120
|
semanticLabel: semanticLabel ?? 'Card',
|
|
121
121
|
clipBorderRadius: resolvedRadius,
|
|
122
|
+
// A tappable card is an actionable control, so make it a keyboard
|
|
123
|
+
// tab-stop with the standard focus ring (matches buttons/links).
|
|
124
|
+
focusable: true,
|
|
125
|
+
focusBorderRadius: resolvedRadius,
|
|
122
126
|
child: margined,
|
|
123
127
|
);
|
|
124
128
|
}
|
|
@@ -579,6 +579,8 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
579
579
|
borderRadius: BorderRadius.circular(_kToggleSize / 2),
|
|
580
580
|
hoverColor: c.activeBg,
|
|
581
581
|
pressColor: c.textActive,
|
|
582
|
+
focusable: true,
|
|
583
|
+
focusGapColor: c.bg,
|
|
582
584
|
onTap: _toggleCollapse,
|
|
583
585
|
child: Container(
|
|
584
586
|
width: _kToggleSize,
|
|
@@ -1071,6 +1073,8 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
1071
1073
|
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
1072
1074
|
hoverColor: c.activeBg,
|
|
1073
1075
|
pressColor: c.textActive,
|
|
1076
|
+
focusable: true,
|
|
1077
|
+
focusGapColor: c.bg,
|
|
1074
1078
|
onTap: () => _activateSubItem(label),
|
|
1075
1079
|
child: Container(
|
|
1076
1080
|
height: _kSubItemH,
|
|
@@ -1298,6 +1302,8 @@ class _ProTooltipIconState extends State<_ProTooltipIcon> {
|
|
|
1298
1302
|
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
1299
1303
|
hoverColor: widget.activeBg,
|
|
1300
1304
|
pressColor: widget.activeBg,
|
|
1305
|
+
focusable: true,
|
|
1306
|
+
focusGapColor: widget.colors.bg,
|
|
1301
1307
|
onTap: widget.onTap,
|
|
1302
1308
|
child: Container(
|
|
1303
1309
|
padding: const EdgeInsets.symmetric(
|
|
@@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
|
|
|
6
6
|
import 'package:flutter/services.dart';
|
|
7
7
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
8
8
|
import 'package:kasy_kit/core/haptics/haptic_feedback_notifier.dart';
|
|
9
|
+
import 'package:kasy_kit/core/theme/theme.dart';
|
|
10
|
+
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
9
11
|
|
|
10
12
|
/// Press-in with slight overshoot-back (same tactility as [KasyAlert] actions).
|
|
11
13
|
///
|
|
@@ -26,6 +28,22 @@ class KasyPressableDepth extends ConsumerStatefulWidget {
|
|
|
26
28
|
/// If false, does not call [HapticFeedback.lightImpact] on tap.
|
|
27
29
|
final bool hapticFeedbackEnabled;
|
|
28
30
|
|
|
31
|
+
/// When true, the control becomes a keyboard tab-stop: it is wrapped in a
|
|
32
|
+
/// [KasyFocusRing] so Tab shows the focus outline and Enter/Space activate it.
|
|
33
|
+
/// Defaults to false because [KasyButton] already wraps its own ring around
|
|
34
|
+
/// the child — turning this on there would paint a second ring. Use it for
|
|
35
|
+
/// direct, standalone uses (e.g. a text link) that need keyboard access.
|
|
36
|
+
final bool focusable;
|
|
37
|
+
|
|
38
|
+
/// Corner radius for the focus ring (only used when [focusable]). Defaults to
|
|
39
|
+
/// [clipBorderRadius] when set, otherwise a small radius hugging the visual.
|
|
40
|
+
final BorderRadius? focusBorderRadius;
|
|
41
|
+
|
|
42
|
+
/// Gap colour for the focus ring (only used when [focusable]). Pass the
|
|
43
|
+
/// surface the control sits on so the ring's hair-line gap blends in instead
|
|
44
|
+
/// of showing a halo (notably in dark mode). Null falls back to `background`.
|
|
45
|
+
final Color? focusGapColor;
|
|
46
|
+
|
|
29
47
|
const KasyPressableDepth({
|
|
30
48
|
super.key,
|
|
31
49
|
required this.child,
|
|
@@ -34,6 +52,9 @@ class KasyPressableDepth extends ConsumerStatefulWidget {
|
|
|
34
52
|
this.pressOverlayColor,
|
|
35
53
|
this.clipBorderRadius,
|
|
36
54
|
this.hapticFeedbackEnabled = true,
|
|
55
|
+
this.focusable = false,
|
|
56
|
+
this.focusBorderRadius,
|
|
57
|
+
this.focusGapColor,
|
|
37
58
|
});
|
|
38
59
|
|
|
39
60
|
@override
|
|
@@ -142,6 +163,21 @@ class _KasyPressableDepthState extends ConsumerState<KasyPressableDepth>
|
|
|
142
163
|
|
|
143
164
|
@override
|
|
144
165
|
Widget build(BuildContext context) {
|
|
166
|
+
// The ring wraps the scaled visual (not the 44px tap target) so it hugs the
|
|
167
|
+
// control's shape, and sits outside Transform.scale so it doesn't pulse with
|
|
168
|
+
// the press. Keyboard activation is wired to the same handler as the tap.
|
|
169
|
+
Widget visual = _wrapScaledChild(widget.child);
|
|
170
|
+
if (widget.focusable) {
|
|
171
|
+
visual = KasyFocusRing(
|
|
172
|
+
onActivate: _handleTap,
|
|
173
|
+
borderRadius: widget.focusBorderRadius ??
|
|
174
|
+
widget.clipBorderRadius ??
|
|
175
|
+
BorderRadius.circular(KasyRadius.sm),
|
|
176
|
+
gapColor: widget.focusGapColor,
|
|
177
|
+
child: visual,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
145
181
|
final Widget inner = Semantics(
|
|
146
182
|
button: true,
|
|
147
183
|
enabled: true,
|
|
@@ -153,7 +189,7 @@ class _KasyPressableDepthState extends ConsumerState<KasyPressableDepth>
|
|
|
153
189
|
onTap: _handleTap,
|
|
154
190
|
child: ConstrainedBox(
|
|
155
191
|
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
|
156
|
-
child: Center(child:
|
|
192
|
+
child: Center(child: visual),
|
|
157
193
|
),
|
|
158
194
|
),
|
|
159
195
|
);
|
|
@@ -98,11 +98,15 @@ class SigninPage extends ConsumerWidget {
|
|
|
98
98
|
FocusScope.of(context).unfocus();
|
|
99
99
|
ref.read(signinStateProvider.notifier).signin();
|
|
100
100
|
},
|
|
101
|
-
labelTrailing:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
labelTrailing: KasyFocusRing(
|
|
102
|
+
onActivate: () => context.push('/recover_password'),
|
|
103
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
104
|
+
child: GestureDetector(
|
|
105
|
+
onTap: () => context.push('/recover_password'),
|
|
106
|
+
child: Text(
|
|
107
|
+
t.auth.signin.forgot_password,
|
|
108
|
+
style: forgotPasswordStyle,
|
|
109
|
+
),
|
|
106
110
|
),
|
|
107
111
|
),
|
|
108
112
|
validator: (value) {
|
|
@@ -191,6 +195,7 @@ class _SignupPrompt extends StatelessWidget {
|
|
|
191
195
|
KasyPressableDepth(
|
|
192
196
|
semanticLabel: t.auth.signin.signup_link,
|
|
193
197
|
onPressed: () => context.pushReplacement('/signup'),
|
|
198
|
+
focusable: true,
|
|
194
199
|
child: Text(
|
|
195
200
|
t.auth.signin.signup_link,
|
|
196
201
|
style: context.textTheme.bodyMedium?.copyWith(
|
|
@@ -180,6 +180,7 @@ class _SigninPrompt extends StatelessWidget {
|
|
|
180
180
|
KasyPressableDepth(
|
|
181
181
|
semanticLabel: t.auth.signup.signin_link,
|
|
182
182
|
onPressed: () => context.pushReplacement('/signin'),
|
|
183
|
+
focusable: true,
|
|
183
184
|
child: Text(
|
|
184
185
|
t.auth.signup.signin_link,
|
|
185
186
|
style: context.textTheme.bodyMedium?.copyWith(
|
|
@@ -12,6 +12,7 @@ import 'package:kasy_kit/core/security/biometric_ui_bundle.dart';
|
|
|
12
12
|
import 'package:kasy_kit/core/states/logout_action.dart';
|
|
13
13
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
14
14
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
15
|
+
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
15
16
|
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
16
17
|
import 'package:kasy_kit/features/settings/ui/components/avatar_component.dart';
|
|
17
18
|
import 'package:kasy_kit/features/settings/ui/components/delete_user_component.dart';
|
|
@@ -329,7 +330,18 @@ class ProfileTile extends StatelessWidget {
|
|
|
329
330
|
child: ClipRRect(
|
|
330
331
|
borderRadius: KasyRadius.smBorderRadius,
|
|
331
332
|
child: onTap != null
|
|
332
|
-
?
|
|
333
|
+
? KasyFocusRing(
|
|
334
|
+
onActivate: onTap,
|
|
335
|
+
borderRadius: KasyRadius.smBorderRadius,
|
|
336
|
+
gapColor: context.colors.surface,
|
|
337
|
+
// The InkWell keeps its tap ripple but yields focus to the ring,
|
|
338
|
+
// so the keyboard outline matches every other Kasy control.
|
|
339
|
+
child: InkWell(
|
|
340
|
+
canRequestFocus: false,
|
|
341
|
+
onTap: onTap,
|
|
342
|
+
child: content,
|
|
343
|
+
),
|
|
344
|
+
)
|
|
333
345
|
: content,
|
|
334
346
|
),
|
|
335
347
|
);
|
|
@@ -547,6 +559,8 @@ class _NavTile extends StatelessWidget {
|
|
|
547
559
|
borderRadius: KasyRadius.smBorderRadius,
|
|
548
560
|
hoverColor: c.surfaceNeutralSoft,
|
|
549
561
|
pressColor: c.onSurface,
|
|
562
|
+
focusable: true,
|
|
563
|
+
focusGapColor: c.surface,
|
|
550
564
|
onTap: onTap,
|
|
551
565
|
child: Container(
|
|
552
566
|
padding: const EdgeInsets.symmetric(
|
|
@@ -770,6 +784,8 @@ class _LogoutRow extends StatelessWidget {
|
|
|
770
784
|
return KasyHover(
|
|
771
785
|
borderRadius: KasyRadius.smBorderRadius,
|
|
772
786
|
pressColor: context.colors.error,
|
|
787
|
+
focusable: true,
|
|
788
|
+
focusGapColor: context.colors.surface,
|
|
773
789
|
onTap: onTap,
|
|
774
790
|
child: Padding(
|
|
775
791
|
padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
|
|
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
5
5
|
import 'package:kasy_kit/core/home_widgets/home_widget_mywidget_service.dart';
|
|
6
6
|
import 'package:kasy_kit/core/shared_preferences/shared_preferences.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/settings/ui/widgets/settings_tile.dart';
|
|
9
10
|
import 'package:kasy_kit/i18n/app_locale_display.dart';
|
|
10
11
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
@@ -17,31 +18,40 @@ class LanguageSwitcher extends ConsumerWidget {
|
|
|
17
18
|
@override
|
|
18
19
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
19
20
|
final AppLocale current = TranslationProvider.of(context).locale;
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
return KasyFocusRing(
|
|
22
|
+
onActivate: () => _showLanguagePicker(context, ref, current),
|
|
23
|
+
borderRadius: KasyRadius.smBorderRadius,
|
|
24
|
+
child: InkWell(
|
|
25
|
+
canRequestFocus: false,
|
|
26
|
+
onTap: () => _showLanguagePicker(context, ref, current),
|
|
27
|
+
child: Padding(
|
|
28
|
+
padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
|
|
29
|
+
child: Row(
|
|
30
|
+
children: <Widget>[
|
|
31
|
+
Icon(
|
|
32
|
+
KasyIcons.language,
|
|
33
|
+
size: 21,
|
|
34
|
+
color: context.colors.onSurface,
|
|
35
|
+
),
|
|
36
|
+
const SizedBox(width: KasySpacing.sm),
|
|
37
|
+
Expanded(
|
|
38
|
+
child: Text(
|
|
39
|
+
context.t.settings.language_title,
|
|
40
|
+
style: context.textTheme.titleMedium?.copyWith(
|
|
41
|
+
color: context.colors.onSurface,
|
|
42
|
+
),
|
|
33
43
|
),
|
|
34
44
|
),
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
Text(
|
|
46
|
+
current.nativeName,
|
|
47
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
48
|
+
color: context.colors.muted,
|
|
49
|
+
),
|
|
40
50
|
),
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
const SizedBox(width: KasySpacing.xs),
|
|
52
|
+
const SettingsListChevron(),
|
|
53
|
+
],
|
|
54
|
+
),
|
|
45
55
|
),
|
|
46
56
|
),
|
|
47
57
|
);
|
|
@@ -147,36 +157,41 @@ class _LocaleOptionTile extends StatelessWidget {
|
|
|
147
157
|
final Color primary = context.colors.primary;
|
|
148
158
|
final Color fg = isSelected ? primary : context.colors.onSurface;
|
|
149
159
|
|
|
150
|
-
return
|
|
151
|
-
|
|
160
|
+
return KasyFocusRing(
|
|
161
|
+
onActivate: onTap,
|
|
152
162
|
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
153
|
-
child:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
child: InkWell(
|
|
164
|
+
canRequestFocus: false,
|
|
165
|
+
onTap: onTap,
|
|
166
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
167
|
+
child: Padding(
|
|
168
|
+
padding: const EdgeInsets.symmetric(
|
|
169
|
+
horizontal: KasySpacing.xs,
|
|
170
|
+
vertical: KasySpacing.smd,
|
|
171
|
+
),
|
|
172
|
+
child: Row(
|
|
173
|
+
children: <Widget>[
|
|
174
|
+
Expanded(
|
|
175
|
+
child: Text(
|
|
176
|
+
locale.nativeName,
|
|
177
|
+
style: context.textTheme.bodyLarge?.copyWith(
|
|
178
|
+
color: fg,
|
|
179
|
+
fontWeight:
|
|
180
|
+
isSelected ? FontWeight.w600 : FontWeight.w400,
|
|
181
|
+
),
|
|
167
182
|
),
|
|
168
183
|
),
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
if (isSelected)
|
|
185
|
+
Container(
|
|
186
|
+
width: 10,
|
|
187
|
+
height: 10,
|
|
188
|
+
decoration: BoxDecoration(
|
|
189
|
+
color: primary,
|
|
190
|
+
shape: BoxShape.circle,
|
|
191
|
+
),
|
|
177
192
|
),
|
|
178
|
-
|
|
179
|
-
|
|
193
|
+
],
|
|
194
|
+
),
|
|
180
195
|
),
|
|
181
196
|
),
|
|
182
197
|
);
|
|
@@ -157,6 +157,8 @@ class SettingsTile extends StatelessWidget {
|
|
|
157
157
|
onTap: onTap,
|
|
158
158
|
hoverEnabled: false,
|
|
159
159
|
pressEnabled: false,
|
|
160
|
+
focusable: true,
|
|
161
|
+
borderRadius: KasyRadius.smBorderRadius,
|
|
160
162
|
semanticLabel: title,
|
|
161
163
|
padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
|
|
162
164
|
child: Row(
|