kasy-cli 1.24.0 → 1.26.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/commands/new.js +53 -3
- package/lib/utils/flutter-install.js +26 -1
- package/lib/utils/i18n/messages-en.js +3 -0
- package/lib/utils/i18n/messages-es.js +3 -0
- package/lib/utils/i18n/messages-pt.js +3 -0
- package/package.json +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +12 -6
- package/templates/firebase/lib/components/kasy_avatar.dart +17 -10
- package/templates/firebase/lib/components/kasy_card.dart +4 -0
- package/templates/firebase/lib/components/kasy_checkbox.dart +24 -15
- package/templates/firebase/lib/components/kasy_date_picker.dart +27 -20
- package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +19 -14
- package/templates/firebase/lib/components/kasy_sidebar.dart +6 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +76 -65
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +103 -3
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +45 -2
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +37 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +9 -4
- 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/feedbacks/ui/widgets/add_feature_button.dart +1 -0
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +2 -0
- package/templates/firebase/lib/features/home/home_feed.dart +21 -5
- package/templates/firebase/lib/features/home/home_image_grid.dart +83 -58
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +124 -101
- package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +1 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +85 -85
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +67 -62
- package/templates/firebase/lib/features/settings/settings_page.dart +17 -1
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +37 -23
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +63 -48
- package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +14 -6
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +2 -0
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +9 -3
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +24 -16
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +29 -22
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +19 -12
- package/templates/firebase/lib/i18n/en.i18n.json +2 -1
- package/templates/firebase/lib/i18n/es.i18n.json +2 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +2 -1
package/lib/commands/new.js
CHANGED
|
@@ -195,6 +195,56 @@ async function confirmIdentities(backend, gcloudAccount, tr, cancel) {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Try to get gcloud ready (installed + logged in) without making the user leave
|
|
200
|
+
* the flow. Mirrors the machine-prep we do for Node/Flutter/Firebase: offer to
|
|
201
|
+
* auto-install gcloud (winget on Windows, brew on macOS), make it visible to
|
|
202
|
+
* this process, then run the interactive Google login (their own account — that
|
|
203
|
+
* part can't be automated). On any failure it returns the latest check so the
|
|
204
|
+
* caller keeps its existing fallback (cancel in Quick mode, or ask for a project
|
|
205
|
+
* id elsewhere). Never throws.
|
|
206
|
+
*/
|
|
207
|
+
async function ensureGcloudReady(tr) {
|
|
208
|
+
const { spawnSync } = require('node:child_process');
|
|
209
|
+
const { augmentedEnv } = require('../utils/env-tools');
|
|
210
|
+
|
|
211
|
+
let check = await checkGcloudAuth();
|
|
212
|
+
if (check.ok) return check;
|
|
213
|
+
|
|
214
|
+
// 1) Not installed → offer to install it for them.
|
|
215
|
+
if (check.missing === 'gcloud') {
|
|
216
|
+
const guide = getGcloudInstallInstructions();
|
|
217
|
+
if (guide.install) {
|
|
218
|
+
const doInstall = await ui.confirm({
|
|
219
|
+
message: tr('new.firebase.create.gcloudInstallConfirm'),
|
|
220
|
+
initialValue: true,
|
|
221
|
+
});
|
|
222
|
+
if (doInstall) {
|
|
223
|
+
const spinner = ui.timedSpinner();
|
|
224
|
+
spinner.start(tr('new.firebase.create.gcloudInstalling'));
|
|
225
|
+
spawnSync(guide.install, { stdio: 'pipe', shell: true });
|
|
226
|
+
spinner.stop(tr('new.firebase.create.gcloudInstalling'));
|
|
227
|
+
// checkGcloudAuth / the later billing+project calls run with the plain
|
|
228
|
+
// process PATH, which doesn't see a tool winget just dropped on the
|
|
229
|
+
// machine PATH. Inject the known gcloud dir so the rest of the flow works.
|
|
230
|
+
const env = augmentedEnv();
|
|
231
|
+
if (env.PATH) process.env.PATH = env.PATH;
|
|
232
|
+
if (process.platform === 'win32' && env.Path) process.env.Path = env.Path;
|
|
233
|
+
check = await checkGcloudAuth();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 2) Installed but not logged in → run the interactive login (their account).
|
|
239
|
+
if (!check.ok && check.missing === 'auth') {
|
|
240
|
+
ui.log.info(tr('new.firebase.create.gcloudAuthOpening'));
|
|
241
|
+
spawnSync('gcloud auth login', { stdio: 'inherit', shell: true });
|
|
242
|
+
check = await checkGcloudAuth();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return check;
|
|
246
|
+
}
|
|
247
|
+
|
|
198
248
|
function printPrerequisites(tr, backend, firebaseSetupMode = 'existing', checkResults = []) {
|
|
199
249
|
const gcloudOk = checkResults.every(
|
|
200
250
|
(r) => !r.name?.includes('gcloud') || r.ok
|
|
@@ -644,7 +694,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
644
694
|
// ── gcloud is required for Quick mode (creates GCP project + enables Auth via API) ─
|
|
645
695
|
// Failing here, before the project is generated, is much friendlier than dying mid-flow.
|
|
646
696
|
if (isQuick) {
|
|
647
|
-
const gcloudCheck = await
|
|
697
|
+
const gcloudCheck = await ensureGcloudReady(tr);
|
|
648
698
|
if (!gcloudCheck.ok) {
|
|
649
699
|
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
650
700
|
if (gcloudCheck.missing === 'gcloud') {
|
|
@@ -815,7 +865,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
815
865
|
onCancel: cancel,
|
|
816
866
|
});
|
|
817
867
|
}
|
|
818
|
-
const gcloudCheck = await
|
|
868
|
+
const gcloudCheck = await ensureGcloudReady(tr);
|
|
819
869
|
if (!gcloudCheck.ok) {
|
|
820
870
|
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
821
871
|
if (gcloudCheck.missing === 'gcloud') {
|
|
@@ -1019,7 +1069,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1019
1069
|
|
|
1020
1070
|
// ── Firebase: create from scratch for Supabase/API (push notifications only) ─
|
|
1021
1071
|
if ((backend === 'supabase' || backend === 'api') && firebaseSetupMode === 'create') {
|
|
1022
|
-
const gcloudCheck = await
|
|
1072
|
+
const gcloudCheck = await ensureGcloudReady(tr);
|
|
1023
1073
|
if (!gcloudCheck.ok) {
|
|
1024
1074
|
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
1025
1075
|
if (gcloudCheck.missing === 'gcloud') {
|
|
@@ -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) {
|
|
@@ -238,6 +238,9 @@ module.exports = {
|
|
|
238
238
|
'new.firebase.create.installAfter': 'Then log in',
|
|
239
239
|
'new.firebase.create.installUrl': 'Or download from',
|
|
240
240
|
'new.firebase.create.authCommand': 'Run: gcloud auth login',
|
|
241
|
+
'new.firebase.create.gcloudInstallConfirm': 'Install the Google Cloud CLI (gcloud) automatically now?',
|
|
242
|
+
'new.firebase.create.gcloudInstalling': 'Installing the Google Cloud CLI…',
|
|
243
|
+
'new.firebase.create.gcloudAuthOpening': 'Opening the Google sign-in in your browser — log in with your account…',
|
|
241
244
|
'new.firebase.create.fallbackHint': 'For now, enter an existing Firebase Project ID to continue (or install gcloud and run again):',
|
|
242
245
|
'new.firebase.q.billingAccount': 'Which billing account to link to the project?',
|
|
243
246
|
'new.firebase.q.billingAccount.hint': 'Choose the account with available quota (up to 3 projects per account)',
|
|
@@ -238,6 +238,9 @@ module.exports = {
|
|
|
238
238
|
'new.firebase.create.installAfter': 'Luego inicia sesión',
|
|
239
239
|
'new.firebase.create.installUrl': 'O descarga en',
|
|
240
240
|
'new.firebase.create.authCommand': 'Ejecuta: gcloud auth login',
|
|
241
|
+
'new.firebase.create.gcloudInstallConfirm': '¿Instalar el Google Cloud CLI (gcloud) automáticamente ahora?',
|
|
242
|
+
'new.firebase.create.gcloudInstalling': 'Instalando el Google Cloud CLI…',
|
|
243
|
+
'new.firebase.create.gcloudAuthOpening': 'Abriendo el inicio de sesión de Google en el navegador — entra con tu cuenta…',
|
|
241
244
|
'new.firebase.create.fallbackHint': 'Por ahora, ingresa un Firebase Project ID existente para continuar (o instala gcloud y ejecuta de nuevo):',
|
|
242
245
|
'new.firebase.q.billingAccount': '¿Qué cuenta de facturación vincular al proyecto?',
|
|
243
246
|
'new.firebase.q.billingAccount.hint': 'Elige la cuenta con cuota disponible (hasta 3 proyectos por cuenta)',
|
|
@@ -238,6 +238,9 @@ module.exports = {
|
|
|
238
238
|
'new.firebase.create.installAfter': 'Depois faca login',
|
|
239
239
|
'new.firebase.create.installUrl': 'Ou baixe em',
|
|
240
240
|
'new.firebase.create.authCommand': 'Execute: gcloud auth login',
|
|
241
|
+
'new.firebase.create.gcloudInstallConfirm': 'Instalar o Google Cloud CLI (gcloud) automaticamente agora?',
|
|
242
|
+
'new.firebase.create.gcloudInstalling': 'Instalando o Google Cloud CLI…',
|
|
243
|
+
'new.firebase.create.gcloudAuthOpening': 'Abrindo o login do Google no navegador — entre com a sua conta…',
|
|
241
244
|
'new.firebase.create.fallbackHint': 'Por enquanto, informe um Firebase Project ID existente para continuar (ou instale o gcloud e execute novamente):',
|
|
242
245
|
'new.firebase.q.billingAccount': 'Qual conta de faturamento vincular ao projeto?',
|
|
243
246
|
'new.firebase.q.billingAccount.hint': 'Escolha a conta com cota disponível (até 3 projetos por conta)',
|
package/package.json
CHANGED
|
@@ -29,6 +29,7 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
|
|
29
29
|
import 'package:flutter/material.dart';
|
|
30
30
|
import 'package:flutter/services.dart' show SystemUiOverlayStyle;
|
|
31
31
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
32
|
+
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
32
33
|
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
33
34
|
|
|
34
35
|
/// Inner toolbar band height (orbit hit targets, title baseline).
|
|
@@ -468,12 +469,17 @@ class KasyAppBarChromeTap extends StatelessWidget {
|
|
|
468
469
|
button: true,
|
|
469
470
|
enabled: true,
|
|
470
471
|
label: label,
|
|
471
|
-
child:
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
472
|
+
child: KasyFocusRing(
|
|
473
|
+
onActivate: onPressed,
|
|
474
|
+
// Circular icon button, so a large radius keeps the focus ring round.
|
|
475
|
+
borderRadius: BorderRadius.circular(999),
|
|
476
|
+
child: GestureDetector(
|
|
477
|
+
behavior: HitTestBehavior.opaque,
|
|
478
|
+
onTap: onPressed,
|
|
479
|
+
child: ConstrainedBox(
|
|
480
|
+
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
|
481
|
+
child: Center(child: child),
|
|
482
|
+
),
|
|
477
483
|
),
|
|
478
484
|
),
|
|
479
485
|
);
|
|
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
|
|
4
4
|
import 'package:flutter/material.dart';
|
|
5
5
|
import 'package:kasy_kit/components/kasy_avatar_presets.dart';
|
|
6
6
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
7
|
+
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
7
8
|
|
|
8
9
|
/// Logical sizes for [KasyAvatar] (diameter in logical pixels).
|
|
9
10
|
enum KasyAvatarSize {
|
|
@@ -609,16 +610,22 @@ class _KasyAvatarPressableState extends State<_KasyAvatarPressable>
|
|
|
609
610
|
final Widget inner = Semantics(
|
|
610
611
|
button: true,
|
|
611
612
|
label: widget.semanticLabel,
|
|
612
|
-
child:
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
child:
|
|
621
|
-
|
|
613
|
+
child: KasyFocusRing(
|
|
614
|
+
onActivate: _tap,
|
|
615
|
+
// The avatar is circular, so a large radius keeps the focus ring
|
|
616
|
+
// hugging the circle (radius >= half the diameter).
|
|
617
|
+
borderRadius: BorderRadius.circular(widget.minSide),
|
|
618
|
+
child: GestureDetector(
|
|
619
|
+
behavior: HitTestBehavior.opaque,
|
|
620
|
+
onTap: _tap,
|
|
621
|
+
child: ConstrainedBox(
|
|
622
|
+
constraints: BoxConstraints(
|
|
623
|
+
minWidth: widget.minSide,
|
|
624
|
+
minHeight: widget.minSide,
|
|
625
|
+
),
|
|
626
|
+
child: Center(
|
|
627
|
+
child: Transform.scale(scale: _s, child: widget.child),
|
|
628
|
+
),
|
|
622
629
|
),
|
|
623
630
|
),
|
|
624
631
|
),
|
|
@@ -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
|
}
|
|
@@ -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
|
/// Shape of the checkbox box.
|
|
5
6
|
enum KasyCheckboxShape {
|
|
@@ -169,21 +170,29 @@ class _KasyCheckboxState extends State<KasyCheckbox>
|
|
|
169
170
|
final bool hasLabel =
|
|
170
171
|
widget.label != null || widget.description != null;
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
173
|
+
final VoidCallback? onTap =
|
|
174
|
+
widget._isDisabled ? null : () => widget.onChanged!(!widget.value);
|
|
175
|
+
|
|
176
|
+
return KasyFocusRing(
|
|
177
|
+
enabled: onTap != null,
|
|
178
|
+
onActivate: onTap,
|
|
179
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
180
|
+
child: GestureDetector(
|
|
181
|
+
onTap: onTap,
|
|
182
|
+
behavior: HitTestBehavior.opaque,
|
|
183
|
+
child: hasLabel
|
|
184
|
+
? Row(
|
|
185
|
+
children: [
|
|
186
|
+
box,
|
|
187
|
+
const SizedBox(width: KasySpacing.smd),
|
|
188
|
+
Expanded(child: _LabelColumn(widget: widget)),
|
|
189
|
+
],
|
|
190
|
+
)
|
|
191
|
+
: Padding(
|
|
192
|
+
padding: const EdgeInsets.all(11),
|
|
193
|
+
child: box,
|
|
194
|
+
),
|
|
195
|
+
),
|
|
187
196
|
);
|
|
188
197
|
}
|
|
189
198
|
}
|
|
@@ -3,6 +3,7 @@ import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
|
|
|
3
3
|
import 'package:kasy_kit/components/kasy_dialog.dart';
|
|
4
4
|
import 'package:kasy_kit/components/kasy_text_field.dart';
|
|
5
5
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
6
|
+
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
6
7
|
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
7
8
|
|
|
8
9
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1038,26 +1039,32 @@ class _KasyDatePickerState extends State<KasyDatePicker>
|
|
|
1038
1039
|
cursor: widget.enabled
|
|
1039
1040
|
? SystemMouseCursors.click
|
|
1040
1041
|
: SystemMouseCursors.basic,
|
|
1041
|
-
child:
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1042
|
+
child: KasyFocusRing(
|
|
1043
|
+
onActivate: _toggleCalendar,
|
|
1044
|
+
// Match the text field corner radius so the ring hugs the trigger.
|
|
1045
|
+
borderRadius: BorderRadius.circular(KasyRadius.md),
|
|
1046
|
+
child: GestureDetector(
|
|
1047
|
+
behavior: HitTestBehavior.opaque,
|
|
1048
|
+
onTap: _toggleCalendar,
|
|
1049
|
+
child: IgnorePointer(
|
|
1050
|
+
child: KasyTextField(
|
|
1051
|
+
key: _fieldKey,
|
|
1052
|
+
controller: _displayController,
|
|
1053
|
+
focusNode: _fieldFocusNode,
|
|
1054
|
+
readOnly: true,
|
|
1055
|
+
enabled: widget.enabled,
|
|
1056
|
+
hint: _resolvedPlaceholder,
|
|
1057
|
+
isInvalid: hasInvalidState,
|
|
1058
|
+
variant: widget.variant,
|
|
1059
|
+
focusBorder: widget.focusBorder,
|
|
1060
|
+
// No caret, no selection handles, no "blue text" when the
|
|
1061
|
+
// trigger is focused while the calendar is open — keeps the
|
|
1062
|
+
// field reading as a button, not an editable input.
|
|
1063
|
+
enableInteractiveSelection: false,
|
|
1064
|
+
suffix: widget.showSuffix
|
|
1065
|
+
? const Icon(KasyIcons.calendar)
|
|
1066
|
+
: null,
|
|
1067
|
+
),
|
|
1061
1068
|
),
|
|
1062
1069
|
),
|
|
1063
1070
|
),
|
|
@@ -4,6 +4,7 @@ import 'package:kasy_kit/components/kasy_button.dart';
|
|
|
4
4
|
import 'package:kasy_kit/components/kasy_text_field_otp.dart';
|
|
5
5
|
import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
|
|
6
6
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
7
|
+
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
7
8
|
|
|
8
9
|
/// Built-in layouts for [showKasyOtpVerificationBottomSheet].
|
|
9
10
|
enum KasyOtpVerificationSheetPreset {
|
|
@@ -209,21 +210,25 @@ class _SheetResendCodeLinkState extends State<_SheetResendCodeLink> {
|
|
|
209
210
|
|
|
210
211
|
@override
|
|
211
212
|
Widget build(BuildContext context) {
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
child:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
opacity: _pressed ? 0.55 : 1.0,
|
|
213
|
+
return KasyFocusRing(
|
|
214
|
+
onActivate: _handleTap,
|
|
215
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
216
|
+
child: GestureDetector(
|
|
217
|
+
onTap: _handleTap,
|
|
218
|
+
onTapCancel: () => setState(() => _pressed = false),
|
|
219
|
+
child: AnimatedScale(
|
|
220
|
+
scale: _pressed ? 0.93 : 1.0,
|
|
221
221
|
duration: const Duration(milliseconds: 120),
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
222
|
+
curve: Curves.easeOutCubic,
|
|
223
|
+
child: AnimatedOpacity(
|
|
224
|
+
opacity: _pressed ? 0.55 : 1.0,
|
|
225
|
+
duration: const Duration(milliseconds: 120),
|
|
226
|
+
child: Text(
|
|
227
|
+
'Resend code',
|
|
228
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
229
|
+
color: context.colors.onBackground,
|
|
230
|
+
fontWeight: FontWeight.w700,
|
|
231
|
+
),
|
|
227
232
|
),
|
|
228
233
|
),
|
|
229
234
|
),
|
|
@@ -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
|
|
@@ -506,35 +507,40 @@ class _PrimaryTabState extends State<_PrimaryTab> {
|
|
|
506
507
|
),
|
|
507
508
|
);
|
|
508
509
|
|
|
509
|
-
final Widget gesture =
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
510
|
+
final Widget gesture = KasyFocusRing(
|
|
511
|
+
enabled: widget.onTap != null,
|
|
512
|
+
onActivate: widget.onTap,
|
|
513
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
514
|
+
child: GestureDetector(
|
|
515
|
+
onTap: widget.onTap,
|
|
516
|
+
behavior: HitTestBehavior.opaque,
|
|
517
|
+
child: Padding(
|
|
518
|
+
padding: verticalLayout
|
|
519
|
+
// Fill+icon: 12px all sides — per Figma spec.
|
|
520
|
+
? const EdgeInsets.all(12)
|
|
521
|
+
// Default: 6px vertical, 12px horizontal — per Figma spec.
|
|
522
|
+
: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
|
523
|
+
child: verticalLayout
|
|
524
|
+
? Column(
|
|
525
|
+
mainAxisSize: MainAxisSize.min,
|
|
526
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
527
|
+
children: [
|
|
528
|
+
iconWidget,
|
|
529
|
+
const SizedBox(height: 6),
|
|
530
|
+
labelWidget,
|
|
531
|
+
],
|
|
532
|
+
)
|
|
533
|
+
: Row(
|
|
534
|
+
mainAxisSize: MainAxisSize.min,
|
|
535
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
536
|
+
children: [
|
|
537
|
+
if (item.icon != null) iconWidget,
|
|
538
|
+
if (item.icon != null && hasLabel)
|
|
539
|
+
const SizedBox(width: 6),
|
|
540
|
+
if (hasLabel) labelWidget,
|
|
541
|
+
],
|
|
542
|
+
),
|
|
543
|
+
),
|
|
538
544
|
),
|
|
539
545
|
);
|
|
540
546
|
|
|
@@ -597,45 +603,50 @@ class _SecondaryTabState extends State<_SecondaryTab> {
|
|
|
597
603
|
// selected color so it reads as interactive (mobile keeps the flat look).
|
|
598
604
|
final Color fg = (selected || _hovered) ? c.onSurface : c.muted;
|
|
599
605
|
|
|
600
|
-
final Widget gesture =
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
bottom:
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
],
|
|
627
|
-
if (hasLabel)
|
|
628
|
-
Opacity(
|
|
629
|
-
opacity: disabled ? 0.4 : 1.0,
|
|
630
|
-
child: Text(
|
|
631
|
-
item.label,
|
|
632
|
-
// Use labelLarge as defined in the Kasy theme (14px/w600).
|
|
633
|
-
style: context.textTheme.labelLarge?.copyWith(
|
|
606
|
+
final Widget gesture = KasyFocusRing(
|
|
607
|
+
enabled: widget.onTap != null,
|
|
608
|
+
onActivate: widget.onTap,
|
|
609
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
610
|
+
child: GestureDetector(
|
|
611
|
+
onTap: widget.onTap,
|
|
612
|
+
behavior: HitTestBehavior.opaque,
|
|
613
|
+
child: Padding(
|
|
614
|
+
// top:4 bottom:6 horizontal:12 — per Figma spec.
|
|
615
|
+
// Extra bottom padding visually balances the 2px underline indicator.
|
|
616
|
+
padding: const EdgeInsets.only(
|
|
617
|
+
top: 4,
|
|
618
|
+
bottom: 6,
|
|
619
|
+
left: 12,
|
|
620
|
+
right: 12,
|
|
621
|
+
),
|
|
622
|
+
child: Row(
|
|
623
|
+
mainAxisSize: MainAxisSize.min,
|
|
624
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
625
|
+
children: [
|
|
626
|
+
if (item.icon != null) ...[
|
|
627
|
+
Opacity(
|
|
628
|
+
opacity: disabled ? 0.4 : 1.0,
|
|
629
|
+
child: Icon(
|
|
630
|
+
item.icon,
|
|
631
|
+
size: 16,
|
|
634
632
|
color: fg,
|
|
635
633
|
),
|
|
636
634
|
),
|
|
637
|
-
|
|
638
|
-
|
|
635
|
+
if (hasLabel) const SizedBox(width: 6),
|
|
636
|
+
],
|
|
637
|
+
if (hasLabel)
|
|
638
|
+
Opacity(
|
|
639
|
+
opacity: disabled ? 0.4 : 1.0,
|
|
640
|
+
child: Text(
|
|
641
|
+
item.label,
|
|
642
|
+
// Use labelLarge as defined in the Kasy theme (14px/w600).
|
|
643
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
644
|
+
color: fg,
|
|
645
|
+
),
|
|
646
|
+
),
|
|
647
|
+
),
|
|
648
|
+
],
|
|
649
|
+
),
|
|
639
650
|
),
|
|
640
651
|
),
|
|
641
652
|
);
|