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.
@@ -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
- # 3. Persist flutter\\bin on the User PATH so every future terminal finds it.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
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"
@@ -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(
@@ -1,5 +1,6 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:kasy_kit/core/theme/theme.dart';
3
+ import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
3
4
 
4
5
  // ─────────────────────────────────────────────────────────────────────────────
5
6
  // Data model
@@ -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: _wrapScaledChild(widget.child)),
192
+ child: Center(child: visual),
157
193
  ),
158
194
  ),
159
195
  );
@@ -124,6 +124,7 @@ class _BackToSigninPrompt extends StatelessWidget {
124
124
  context.go('/signin');
125
125
  }
126
126
  },
127
+ focusable: true,
127
128
  child: Text(
128
129
  t.auth.recover.signin_link,
129
130
  style: context.textTheme.bodyMedium?.copyWith(
@@ -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: GestureDetector(
102
- onTap: () => context.push('/recover_password'),
103
- child: Text(
104
- t.auth.signin.forgot_password,
105
- style: forgotPasswordStyle,
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
- ? InkWell(onTap: onTap, child: content)
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 InkWell(
21
- onTap: () => _showLanguagePicker(context, ref, current),
22
- child: Padding(
23
- padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
24
- child: Row(
25
- children: <Widget>[
26
- Icon(KasyIcons.language, size: 21, color: context.colors.onSurface),
27
- const SizedBox(width: KasySpacing.sm),
28
- Expanded(
29
- child: Text(
30
- context.t.settings.language_title,
31
- style: context.textTheme.titleMedium?.copyWith(
32
- color: context.colors.onSurface,
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
- Text(
37
- current.nativeName,
38
- style: context.textTheme.bodyMedium?.copyWith(
39
- color: context.colors.muted,
45
+ Text(
46
+ current.nativeName,
47
+ style: context.textTheme.bodyMedium?.copyWith(
48
+ color: context.colors.muted,
49
+ ),
40
50
  ),
41
- ),
42
- const SizedBox(width: KasySpacing.xs),
43
- const SettingsListChevron(),
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 InkWell(
151
- onTap: onTap,
160
+ return KasyFocusRing(
161
+ onActivate: onTap,
152
162
  borderRadius: BorderRadius.circular(KasyRadius.sm),
153
- child: Padding(
154
- padding: const EdgeInsets.symmetric(
155
- horizontal: KasySpacing.xs,
156
- vertical: KasySpacing.smd,
157
- ),
158
- child: Row(
159
- children: <Widget>[
160
- Expanded(
161
- child: Text(
162
- locale.nativeName,
163
- style: context.textTheme.bodyLarge?.copyWith(
164
- color: fg,
165
- fontWeight:
166
- isSelected ? FontWeight.w600 : FontWeight.w400,
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
- if (isSelected)
171
- Container(
172
- width: 10,
173
- height: 10,
174
- decoration: BoxDecoration(
175
- color: primary,
176
- shape: BoxShape.circle,
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(