kasy-cli 1.23.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/bin/kasy.js +26 -4
- 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 +9 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +1 -0
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +30 -18
- package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +9 -1
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +8 -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
package/bin/kasy.js
CHANGED
|
@@ -463,9 +463,15 @@ function buildProgram(language) {
|
|
|
463
463
|
if (p[p.length - 1] === 'lib') p = p.slice(0, -1);
|
|
464
464
|
prefix = p.join(path.sep) || null;
|
|
465
465
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
466
|
+
// Build a single, pre-quoted command string. Passing a quoted string
|
|
467
|
+
// (instead of an args array) with shell:true keeps a prefix that contains
|
|
468
|
+
// spaces intact — e.g. C:\Users\John Silva\.kasy — which the array form
|
|
469
|
+
// splits into a broken path (npm reads "Silva\.kasy" as a second package).
|
|
470
|
+
// It also avoids Node's DEP0190 arg-concatenation warning.
|
|
471
|
+
const quote = (s) => `"${s}"`;
|
|
472
|
+
let cmd = 'npm install -g kasy-cli@latest';
|
|
473
|
+
if (prefix) cmd += ` --prefix ${quote(prefix)}`;
|
|
474
|
+
const result = spawnSync(cmd, { stdio: 'inherit', shell: true });
|
|
469
475
|
if (result.status === 0) {
|
|
470
476
|
const after = readVersion();
|
|
471
477
|
let msg;
|
|
@@ -486,9 +492,25 @@ function buildProgram(language) {
|
|
|
486
492
|
.description(t('cli.command.uninstall.description'))
|
|
487
493
|
.action(() => {
|
|
488
494
|
const { spawnSync } = require('node:child_process');
|
|
495
|
+
const path = require('node:path');
|
|
489
496
|
printCompactHeader(t);
|
|
490
497
|
console.log(kleur.cyan(t('cli.command.uninstall.running')) + '\n');
|
|
491
|
-
|
|
498
|
+
// Remove from the SAME prefix the CLI was installed into (~/.kasy), the
|
|
499
|
+
// way `kasy upgrade` derives it — otherwise npm targets its default global
|
|
500
|
+
// prefix and leaves the real copy in place. Quote it so a path with
|
|
501
|
+
// spaces (C:\Users\John Silva\.kasy) survives shell:true.
|
|
502
|
+
const segs = __dirname.split(path.sep);
|
|
503
|
+
const nm = segs.lastIndexOf('node_modules');
|
|
504
|
+
let prefix = null;
|
|
505
|
+
if (nm > 0) {
|
|
506
|
+
let p = segs.slice(0, nm);
|
|
507
|
+
if (p[p.length - 1] === 'lib') p = p.slice(0, -1);
|
|
508
|
+
prefix = p.join(path.sep) || null;
|
|
509
|
+
}
|
|
510
|
+
const quote = (s) => `"${s}"`;
|
|
511
|
+
let cmd = 'npm uninstall -g kasy-cli';
|
|
512
|
+
if (prefix) cmd += ` --prefix ${quote(prefix)}`;
|
|
513
|
+
const result = spawnSync(cmd, { stdio: 'inherit', shell: true });
|
|
492
514
|
if (result.status === 0) {
|
|
493
515
|
console.log('\n' + kleur.green('✓ ' + t('cli.command.uninstall.done')) + '\n');
|
|
494
516
|
} else {
|
|
@@ -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,
|
|
@@ -746,6 +748,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
746
748
|
hoverColor: c.activeBg,
|
|
747
749
|
pressColor: c.textActive,
|
|
748
750
|
focusable: true,
|
|
751
|
+
focusGapColor: c.bg,
|
|
749
752
|
onTap: widget.onProfileTap ?? () {},
|
|
750
753
|
child: Padding(
|
|
751
754
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
|
@@ -877,6 +880,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
877
880
|
hoverColor: c.activeBg,
|
|
878
881
|
pressColor: c.textActive,
|
|
879
882
|
focusable: true,
|
|
883
|
+
focusGapColor: c.bg,
|
|
880
884
|
onTap: onTap,
|
|
881
885
|
child: Container(
|
|
882
886
|
constraints: const BoxConstraints(minHeight: _kItemMinH),
|
|
@@ -947,6 +951,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
947
951
|
hoverColor: c.activeBg,
|
|
948
952
|
pressColor: c.textActive,
|
|
949
953
|
focusable: true,
|
|
954
|
+
focusGapColor: c.bg,
|
|
950
955
|
onTap: () => _activateItem(item.id),
|
|
951
956
|
child: Container(
|
|
952
957
|
constraints: const BoxConstraints(minHeight: _kItemMinH),
|
|
@@ -1068,6 +1073,8 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
1068
1073
|
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
1069
1074
|
hoverColor: c.activeBg,
|
|
1070
1075
|
pressColor: c.textActive,
|
|
1076
|
+
focusable: true,
|
|
1077
|
+
focusGapColor: c.bg,
|
|
1071
1078
|
onTap: () => _activateSubItem(label),
|
|
1072
1079
|
child: Container(
|
|
1073
1080
|
height: _kSubItemH,
|
|
@@ -1295,6 +1302,8 @@ class _ProTooltipIconState extends State<_ProTooltipIcon> {
|
|
|
1295
1302
|
borderRadius: BorderRadius.circular(_kItemRadius),
|
|
1296
1303
|
hoverColor: widget.activeBg,
|
|
1297
1304
|
pressColor: widget.activeBg,
|
|
1305
|
+
focusable: true,
|
|
1306
|
+
focusGapColor: widget.colors.bg,
|
|
1298
1307
|
onTap: widget.onTap,
|
|
1299
1308
|
child: Container(
|
|
1300
1309
|
padding: const EdgeInsets.symmetric(
|
|
@@ -152,20 +152,27 @@ class BottomMenu extends StatelessWidget {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
///
|
|
156
|
-
///
|
|
155
|
+
/// Keeps the initial keyboard Tab focus anchored on the sidebar — on every
|
|
156
|
+
/// screen, like Stripe/Linear.
|
|
157
157
|
///
|
|
158
158
|
/// Why this exists: Bart renders each page inside a nested [Navigator]
|
|
159
159
|
/// (see bart's nested_navigator.dart), which has its OWN FocusScope and claims
|
|
160
160
|
/// the primary focus the moment a route mounts. A plain `autofocus` on a sidebar
|
|
161
161
|
/// item loses that race — the Navigator overwrites it in the same frame.
|
|
162
162
|
///
|
|
163
|
-
/// The fix is a
|
|
164
|
-
///
|
|
165
|
-
///
|
|
166
|
-
///
|
|
167
|
-
///
|
|
168
|
-
///
|
|
163
|
+
/// The fix is a tiny non-traversable [Focus] anchor inside the sidebar. A
|
|
164
|
+
/// post-frame callback (runs AFTER the Navigator has claimed focus) moves focus
|
|
165
|
+
/// to that anchor, pulling the primary focus out of the Navigator's scope and
|
|
166
|
+
/// back onto the sidebar. Because the anchor sets `skipTraversal: true`, it is
|
|
167
|
+
/// skipped by Tab, so the very first Tab lands on the first real sidebar item
|
|
168
|
+
/// and then flows on to the header and content — nothing is trapped. (A
|
|
169
|
+
/// [FocusScope] would have worked for the anchoring, but it adds a traversal
|
|
170
|
+
/// boundary that wraps Tab back to the sidebar's start instead of moving on to
|
|
171
|
+
/// the content, which is the opposite of what we want.)
|
|
172
|
+
///
|
|
173
|
+
/// It re-anchors whenever [currentItem] changes (a tab navigation) so a fresh
|
|
174
|
+
/// screen also starts at the sidebar. The ring still only paints during keyboard
|
|
175
|
+
/// navigation, so this is invisible to mouse/touch users.
|
|
169
176
|
class _FocusableSidebar extends StatefulWidget {
|
|
170
177
|
final Widget child;
|
|
171
178
|
final ValueNotifier<int> currentItem;
|
|
@@ -177,7 +184,10 @@ class _FocusableSidebar extends StatefulWidget {
|
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
class _FocusableSidebarState extends State<_FocusableSidebar> {
|
|
180
|
-
final
|
|
187
|
+
final FocusNode _anchor = FocusNode(
|
|
188
|
+
debugLabel: 'sidebarFocusAnchor',
|
|
189
|
+
skipTraversal: true,
|
|
190
|
+
);
|
|
181
191
|
|
|
182
192
|
@override
|
|
183
193
|
void initState() {
|
|
@@ -199,26 +209,28 @@ class _FocusableSidebarState extends State<_FocusableSidebar> {
|
|
|
199
209
|
// which claims focus for its own scope while the route is mounting.
|
|
200
210
|
void _anchorFocus() {
|
|
201
211
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
202
|
-
if (
|
|
203
|
-
// Only seize focus when nothing inside the sidebar already holds it, so a
|
|
204
|
-
// keyboard user mid-navigation in the sidebar isn't yanked back to start.
|
|
205
|
-
if (_scope.hasFocus) return;
|
|
206
|
-
_scope.requestFocus();
|
|
212
|
+
if (mounted) _anchor.requestFocus();
|
|
207
213
|
});
|
|
208
214
|
}
|
|
209
215
|
|
|
210
216
|
@override
|
|
211
217
|
void dispose() {
|
|
212
218
|
widget.currentItem.removeListener(_anchorFocus);
|
|
213
|
-
|
|
219
|
+
_anchor.dispose();
|
|
214
220
|
super.dispose();
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
@override
|
|
218
224
|
Widget build(BuildContext context) {
|
|
219
|
-
return
|
|
220
|
-
|
|
221
|
-
|
|
225
|
+
return FocusTraversalGroup(
|
|
226
|
+
// The anchor is a zero-size sibling so it never affects the sidebar's
|
|
227
|
+
// layout; it exists only to hold the initial keyboard focus (see above).
|
|
228
|
+
child: Stack(
|
|
229
|
+
children: [
|
|
230
|
+
widget.child,
|
|
231
|
+
Focus(focusNode: _anchor, child: const SizedBox.shrink()),
|
|
232
|
+
],
|
|
233
|
+
),
|
|
222
234
|
);
|
|
223
235
|
}
|
|
224
236
|
}
|
|
@@ -27,12 +27,20 @@ class KasyFocusRing extends StatefulWidget {
|
|
|
27
27
|
/// When false the ring never takes focus or paints (disabled controls).
|
|
28
28
|
final bool enabled;
|
|
29
29
|
|
|
30
|
+
/// Colour of the hair-line gap between the control and the ring. Defaults to
|
|
31
|
+
/// [KasyColors.background] (correct on full-page backgrounds). Pass the actual
|
|
32
|
+
/// surface colour the control sits on (e.g. the sidebar/header `surface`) so
|
|
33
|
+
/// the gap blends in instead of painting a visible halo — this matters in dark
|
|
34
|
+
/// mode, where `background` is darker than `surface`.
|
|
35
|
+
final Color? gapColor;
|
|
36
|
+
|
|
30
37
|
const KasyFocusRing({
|
|
31
38
|
super.key,
|
|
32
39
|
required this.child,
|
|
33
40
|
required this.borderRadius,
|
|
34
41
|
this.onActivate,
|
|
35
42
|
this.enabled = true,
|
|
43
|
+
this.gapColor,
|
|
36
44
|
});
|
|
37
45
|
|
|
38
46
|
@override
|
|
@@ -57,7 +65,7 @@ class _KasyFocusRingState extends State<KasyFocusRing> {
|
|
|
57
65
|
// A hair-line gap in the canvas colour lifts the ring just off the control
|
|
58
66
|
// so it reads as a crisp outline (same focus language as KasyTextField) and
|
|
59
67
|
// stays visible even on primary-coloured buttons (e.g. the chat send orb).
|
|
60
|
-
final Color gapColor = context.colors.background;
|
|
68
|
+
final Color gapColor = widget.gapColor ?? context.colors.background;
|
|
61
69
|
final VoidCallback? onActivate = widget.onActivate;
|
|
62
70
|
|
|
63
71
|
return FocusableActionDetector(
|
|
@@ -50,6 +50,7 @@ class KasyHover extends ConsumerStatefulWidget {
|
|
|
50
50
|
this.hoverColor,
|
|
51
51
|
this.pressColor,
|
|
52
52
|
this.focusable = false,
|
|
53
|
+
this.focusGapColor,
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
final Widget child;
|
|
@@ -112,6 +113,12 @@ class KasyHover extends ConsumerStatefulWidget {
|
|
|
112
113
|
/// call sites stay plain, non-focusable rows.
|
|
113
114
|
final bool focusable;
|
|
114
115
|
|
|
116
|
+
/// Gap colour forwarded to the focus ring (only used when [focusable]). Pass
|
|
117
|
+
/// the surface colour the row sits on so the keyboard ring's hair-line gap
|
|
118
|
+
/// blends in instead of showing a darker halo (notably in dark mode). When
|
|
119
|
+
/// null the ring falls back to [KasyColors.background].
|
|
120
|
+
final Color? focusGapColor;
|
|
121
|
+
|
|
115
122
|
@override
|
|
116
123
|
ConsumerState<KasyHover> createState() => _KasyHoverState();
|
|
117
124
|
}
|
|
@@ -209,6 +216,7 @@ class _KasyHoverState extends ConsumerState<KasyHover> {
|
|
|
209
216
|
? KasyFocusRing(
|
|
210
217
|
borderRadius: widget.borderRadius,
|
|
211
218
|
onActivate: widget.onTap,
|
|
219
|
+
gapColor: widget.focusGapColor,
|
|
212
220
|
child: content,
|
|
213
221
|
)
|
|
214
222
|
: content;
|
|
@@ -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(
|