kasy-cli 1.25.0 → 1.27.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 +63 -3
- package/lib/utils/i18n/messages-en.js +4 -0
- package/lib/utils/i18n/messages-es.js +4 -0
- package/lib/utils/i18n/messages-pt.js +4 -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_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_tabs.dart +75 -65
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +98 -2
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +45 -2
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +9 -4
- 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/ui/components/avatar_component.dart +37 -23
- package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +14 -6
- 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,66 @@ 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, exec } = 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 → tell the user it's missing, then offer to install it.
|
|
215
|
+
if (check.missing === 'gcloud') {
|
|
216
|
+
const guide = getGcloudInstallInstructions();
|
|
217
|
+
if (guide.install) {
|
|
218
|
+
ui.log.warn(tr('new.firebase.create.gcloudMissing'));
|
|
219
|
+
const doInstall = await ui.confirm({
|
|
220
|
+
message: tr('new.firebase.create.gcloudInstallConfirm'),
|
|
221
|
+
initialValue: true,
|
|
222
|
+
});
|
|
223
|
+
if (doInstall) {
|
|
224
|
+
const spinner = ui.timedSpinner();
|
|
225
|
+
spinner.start(tr('new.firebase.create.gcloudInstalling'));
|
|
226
|
+
// Run async (NOT spawnSync): a synchronous child blocks the event loop,
|
|
227
|
+
// which freezes the spinner — the user stares at a dead screen for
|
|
228
|
+
// minutes. exec keeps the clock ticking so it's clear it's working.
|
|
229
|
+
await new Promise((resolve) => {
|
|
230
|
+
exec(
|
|
231
|
+
guide.install,
|
|
232
|
+
{ env: augmentedEnv(), timeout: 600_000, windowsHide: true, maxBuffer: 50 * 1024 * 1024 },
|
|
233
|
+
() => resolve()
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
spinner.stop(tr('new.firebase.create.gcloudInstalling'));
|
|
237
|
+
// checkGcloudAuth and the later billing/project calls use the plain
|
|
238
|
+
// process PATH, which doesn't see a tool winget just put on the machine
|
|
239
|
+
// PATH. Inject the known gcloud dir so the rest of the flow works.
|
|
240
|
+
const env = augmentedEnv();
|
|
241
|
+
if (env.PATH) process.env.PATH = env.PATH;
|
|
242
|
+
if (process.platform === 'win32' && env.Path) process.env.Path = env.Path;
|
|
243
|
+
check = await checkGcloudAuth();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 2) Installed but not logged in → run the interactive login (their account).
|
|
249
|
+
if (!check.ok && check.missing === 'auth') {
|
|
250
|
+
ui.log.info(tr('new.firebase.create.gcloudAuthOpening'));
|
|
251
|
+
spawnSync('gcloud auth login', { stdio: 'inherit', shell: true });
|
|
252
|
+
check = await checkGcloudAuth();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return check;
|
|
256
|
+
}
|
|
257
|
+
|
|
198
258
|
function printPrerequisites(tr, backend, firebaseSetupMode = 'existing', checkResults = []) {
|
|
199
259
|
const gcloudOk = checkResults.every(
|
|
200
260
|
(r) => !r.name?.includes('gcloud') || r.ok
|
|
@@ -644,7 +704,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
644
704
|
// ── gcloud is required for Quick mode (creates GCP project + enables Auth via API) ─
|
|
645
705
|
// Failing here, before the project is generated, is much friendlier than dying mid-flow.
|
|
646
706
|
if (isQuick) {
|
|
647
|
-
const gcloudCheck = await
|
|
707
|
+
const gcloudCheck = await ensureGcloudReady(tr);
|
|
648
708
|
if (!gcloudCheck.ok) {
|
|
649
709
|
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
650
710
|
if (gcloudCheck.missing === 'gcloud') {
|
|
@@ -815,7 +875,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
815
875
|
onCancel: cancel,
|
|
816
876
|
});
|
|
817
877
|
}
|
|
818
|
-
const gcloudCheck = await
|
|
878
|
+
const gcloudCheck = await ensureGcloudReady(tr);
|
|
819
879
|
if (!gcloudCheck.ok) {
|
|
820
880
|
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
821
881
|
if (gcloudCheck.missing === 'gcloud') {
|
|
@@ -1019,7 +1079,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1019
1079
|
|
|
1020
1080
|
// ── Firebase: create from scratch for Supabase/API (push notifications only) ─
|
|
1021
1081
|
if ((backend === 'supabase' || backend === 'api') && firebaseSetupMode === 'create') {
|
|
1022
|
-
const gcloudCheck = await
|
|
1082
|
+
const gcloudCheck = await ensureGcloudReady(tr);
|
|
1023
1083
|
if (!gcloudCheck.ok) {
|
|
1024
1084
|
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
1025
1085
|
if (gcloudCheck.missing === 'gcloud') {
|
|
@@ -238,6 +238,10 @@ 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.gcloudMissing': 'The Google Cloud CLI (gcloud) is required to create the Firebase project from scratch, and it is not installed yet.',
|
|
242
|
+
'new.firebase.create.gcloudInstallConfirm': 'Install the Google Cloud CLI (gcloud) automatically now?',
|
|
243
|
+
'new.firebase.create.gcloudInstalling': 'Installing the Google Cloud CLI…',
|
|
244
|
+
'new.firebase.create.gcloudAuthOpening': 'Opening the Google sign-in in your browser — log in with your account…',
|
|
241
245
|
'new.firebase.create.fallbackHint': 'For now, enter an existing Firebase Project ID to continue (or install gcloud and run again):',
|
|
242
246
|
'new.firebase.q.billingAccount': 'Which billing account to link to the project?',
|
|
243
247
|
'new.firebase.q.billingAccount.hint': 'Choose the account with available quota (up to 3 projects per account)',
|
|
@@ -238,6 +238,10 @@ 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.gcloudMissing': 'El Google Cloud CLI (gcloud) es necesario para crear el proyecto Firebase desde cero, y aún no está instalado.',
|
|
242
|
+
'new.firebase.create.gcloudInstallConfirm': '¿Instalar el Google Cloud CLI (gcloud) automáticamente ahora?',
|
|
243
|
+
'new.firebase.create.gcloudInstalling': 'Instalando el Google Cloud CLI…',
|
|
244
|
+
'new.firebase.create.gcloudAuthOpening': 'Abriendo el inicio de sesión de Google en el navegador — entra con tu cuenta…',
|
|
241
245
|
'new.firebase.create.fallbackHint': 'Por ahora, ingresa un Firebase Project ID existente para continuar (o instala gcloud y ejecuta de nuevo):',
|
|
242
246
|
'new.firebase.q.billingAccount': '¿Qué cuenta de facturación vincular al proyecto?',
|
|
243
247
|
'new.firebase.q.billingAccount.hint': 'Elige la cuenta con cuota disponible (hasta 3 proyectos por cuenta)',
|
|
@@ -238,6 +238,10 @@ 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.gcloudMissing': 'O Google Cloud CLI (gcloud) é necessário para criar o projeto Firebase do zero, e ainda não está instalado.',
|
|
242
|
+
'new.firebase.create.gcloudInstallConfirm': 'Instalar o Google Cloud CLI (gcloud) automaticamente agora?',
|
|
243
|
+
'new.firebase.create.gcloudInstalling': 'Instalando o Google Cloud CLI…',
|
|
244
|
+
'new.firebase.create.gcloudAuthOpening': 'Abrindo o login do Google no navegador — entre com a sua conta…',
|
|
241
245
|
'new.firebase.create.fallbackHint': 'Por enquanto, informe um Firebase Project ID existente para continuar (ou instale o gcloud e execute novamente):',
|
|
242
246
|
'new.firebase.q.billingAccount': 'Qual conta de faturamento vincular ao projeto?',
|
|
243
247
|
'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
|
),
|
|
@@ -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
|
),
|
|
@@ -507,35 +507,40 @@ class _PrimaryTabState extends State<_PrimaryTab> {
|
|
|
507
507
|
),
|
|
508
508
|
);
|
|
509
509
|
|
|
510
|
-
final Widget gesture =
|
|
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
|
-
|
|
538
|
-
|
|
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
|
+
),
|
|
539
544
|
),
|
|
540
545
|
);
|
|
541
546
|
|
|
@@ -598,45 +603,50 @@ class _SecondaryTabState extends State<_SecondaryTab> {
|
|
|
598
603
|
// selected color so it reads as interactive (mobile keeps the flat look).
|
|
599
604
|
final Color fg = (selected || _hovered) ? c.onSurface : c.muted;
|
|
600
605
|
|
|
601
|
-
final Widget gesture =
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
bottom:
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
],
|
|
628
|
-
if (hasLabel)
|
|
629
|
-
Opacity(
|
|
630
|
-
opacity: disabled ? 0.4 : 1.0,
|
|
631
|
-
child: Text(
|
|
632
|
-
item.label,
|
|
633
|
-
// Use labelLarge as defined in the Kasy theme (14px/w600).
|
|
634
|
-
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,
|
|
635
632
|
color: fg,
|
|
636
633
|
),
|
|
637
634
|
),
|
|
638
|
-
|
|
639
|
-
|
|
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
|
+
),
|
|
640
650
|
),
|
|
641
651
|
),
|
|
642
652
|
);
|