kasy-cli 1.19.1 → 1.20.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.
Files changed (44) hide show
  1. package/bin/kasy.js +1 -0
  2. package/lib/commands/new.js +9 -0
  3. package/lib/commands/run.js +22 -6
  4. package/lib/scaffold/backends/firebase/setup-from-scratch.js +91 -2
  5. package/lib/scaffold/engine.js +5 -0
  6. package/lib/scaffold/generate.js +4 -0
  7. package/lib/scaffold/shared/generator-utils.js +38 -1
  8. package/lib/utils/flutter-run.js +16 -4
  9. package/lib/utils/i18n/messages-en.js +2 -1
  10. package/lib/utils/i18n/messages-es.js +2 -1
  11. package/lib/utils/i18n/messages-pt.js +2 -1
  12. package/package.json +1 -1
  13. package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
  14. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  15. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  16. package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
  17. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
  18. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
  19. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
  20. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  21. package/templates/firebase/lib/components/kasy_date_picker.dart +14 -8
  22. package/templates/firebase/lib/components/kasy_sidebar_pro.dart +1148 -0
  23. package/templates/firebase/lib/components/kasy_tabs.dart +156 -43
  24. package/templates/firebase/lib/components/kasy_text_field.dart +39 -29
  25. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +90 -69
  26. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +30 -7
  27. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +8 -1
  28. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +439 -232
  29. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +198 -83
  30. package/templates/firebase/lib/core/icons/kasy_icons.dart +1 -0
  31. package/templates/firebase/lib/core/theme/colors.dart +6 -2
  32. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +119 -19
  33. package/templates/firebase/lib/core/widgets/kasy_hover.dart +68 -27
  34. package/templates/firebase/lib/features/home/home_components_page.dart +3 -0
  35. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +202 -79
  36. package/templates/firebase/lib/features/settings/settings_page.dart +27 -146
  37. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +16 -3
  38. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +22 -5
  39. package/templates/firebase/lib/i18n/en.i18n.json +3 -1
  40. package/templates/firebase/lib/i18n/es.i18n.json +3 -1
  41. package/templates/firebase/lib/i18n/pt.i18n.json +3 -1
  42. package/templates/firebase/pubspec.yaml +6 -4
  43. package/templates/firebase/web/index.html +7 -17
  44. package/templates/firebase/lib/firebase_options.dart +0 -75
@@ -429,171 +429,52 @@ class HapticFeedbackSwitcher extends ConsumerWidget {
429
429
  class ThemeSwitcher extends StatelessWidget {
430
430
  const ThemeSwitcher({super.key});
431
431
 
432
- IconData _iconFor(ThemeMode mode) {
433
- switch (mode) {
434
- case ThemeMode.system:
435
- return KasyIcons.phoneAndroid;
436
- case ThemeMode.dark:
437
- return KasyIcons.darkMode;
438
- case ThemeMode.light:
439
- return KasyIcons.lightMode;
440
- }
441
- }
432
+ // Tab order mirrors the trailing pill: system, light, dark.
433
+ static const List<ThemeMode> _modes = <ThemeMode>[
434
+ ThemeMode.system,
435
+ ThemeMode.light,
436
+ ThemeMode.dark,
437
+ ];
442
438
 
443
439
  @override
444
440
  Widget build(BuildContext context) {
445
441
  final tr = context.t.settings;
446
442
  final theme = ThemeProvider.of(context);
447
- final options = <({ThemeMode mode, IconData icon, String label})>[
448
- (
449
- mode: ThemeMode.system,
450
- icon: KasyIcons.phoneAndroid,
451
- label: tr.theme_option_system,
443
+ final items = <KasyTabItem>[
444
+ KasyTabItem(
445
+ icon: KasyIcons.monitor,
446
+ semanticLabel: tr.theme_option_system,
452
447
  ),
453
- (
454
- mode: ThemeMode.light,
448
+ KasyTabItem(
455
449
  icon: KasyIcons.lightMode,
456
- label: tr.theme_option_light,
450
+ semanticLabel: tr.theme_option_light,
457
451
  ),
458
- (
459
- mode: ThemeMode.dark,
452
+ KasyTabItem(
460
453
  icon: KasyIcons.darkMode,
461
- label: tr.theme_option_dark,
454
+ semanticLabel: tr.theme_option_dark,
462
455
  ),
463
456
  ];
464
457
  return Padding(
465
458
  padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
466
- child: Column(
467
- crossAxisAlignment: CrossAxisAlignment.stretch,
468
- children: [
469
- Row(
470
- children: [
471
- Icon(
472
- _iconFor(theme.mode),
473
- size: 21,
474
- color: context.colors.onSurface,
475
- ),
476
- const SizedBox(width: KasySpacing.sm),
477
- Expanded(
478
- child: Text(
479
- tr.theme_title,
480
- style: context.textTheme.titleMedium?.copyWith(
481
- color: context.colors.onSurface,
482
- ),
483
- ),
484
- ),
485
- ],
486
- ),
487
- const SizedBox(height: KasySpacing.sm),
488
- _ThemeModeSegmented(
489
- current: theme.mode,
490
- options: options,
491
- onChanged: theme.setMode,
492
- ),
493
- ],
494
- ),
495
- );
496
- }
497
- }
498
-
499
- class _ThemeModeSegmented extends StatelessWidget {
500
- const _ThemeModeSegmented({
501
- required this.current,
502
- required this.options,
503
- required this.onChanged,
504
- });
505
-
506
- final ThemeMode current;
507
- final List<({ThemeMode mode, IconData icon, String label})> options;
508
- final ValueChanged<ThemeMode> onChanged;
509
-
510
- @override
511
- Widget build(BuildContext context) {
512
- final trackColor = context.colors.onSurface.withValues(alpha: 0.06);
513
- return Container(
514
- padding: const EdgeInsets.all(3),
515
- decoration: BoxDecoration(
516
- color: trackColor,
517
- borderRadius: BorderRadius.circular(10),
518
- ),
519
459
  child: Row(
520
460
  children: [
521
- for (final opt in options)
522
- Expanded(
523
- child: _ThemeModeSegment(
524
- icon: opt.icon,
525
- label: opt.label,
526
- selected: current == opt.mode,
527
- onTap: () => onChanged(opt.mode),
461
+ Icon(KasyIcons.palette, size: 21, color: context.colors.onSurface),
462
+ const SizedBox(width: KasySpacing.sm),
463
+ Expanded(
464
+ child: Text(
465
+ tr.theme_title,
466
+ style: context.textTheme.titleMedium?.copyWith(
467
+ color: context.colors.onSurface,
528
468
  ),
529
469
  ),
530
- ],
531
- ),
532
- );
533
- }
534
- }
535
-
536
- class _ThemeModeSegment extends StatelessWidget {
537
- const _ThemeModeSegment({
538
- required this.icon,
539
- required this.label,
540
- required this.selected,
541
- required this.onTap,
542
- });
543
-
544
- final IconData icon;
545
- final String label;
546
- final bool selected;
547
- final VoidCallback onTap;
548
-
549
- @override
550
- Widget build(BuildContext context) {
551
- final selectedBg = context.colors.surface;
552
- final selectedFg = context.colors.onSurface;
553
- final unselectedFg = context.colors.muted;
554
- return Semantics(
555
- button: true,
556
- selected: selected,
557
- label: label,
558
- child: GestureDetector(
559
- onTap: onTap,
560
- behavior: HitTestBehavior.opaque,
561
- child: AnimatedContainer(
562
- duration: const Duration(milliseconds: 160),
563
- curve: Curves.easeOut,
564
- padding: const EdgeInsets.symmetric(vertical: 8),
565
- decoration: BoxDecoration(
566
- color: selected ? selectedBg : Colors.transparent,
567
- borderRadius: BorderRadius.circular(8),
568
- boxShadow: selected
569
- ? [
570
- BoxShadow(
571
- color: Colors.black.withValues(alpha: 0.06),
572
- blurRadius: 4,
573
- offset: const Offset(0, 1),
574
- ),
575
- ]
576
- : const [],
577
470
  ),
578
- child: Column(
579
- mainAxisSize: MainAxisSize.min,
580
- children: [
581
- Icon(
582
- icon,
583
- size: 18,
584
- color: selected ? selectedFg : unselectedFg,
585
- ),
586
- const SizedBox(height: 2),
587
- Text(
588
- label,
589
- style: context.textTheme.labelSmall?.copyWith(
590
- color: selected ? selectedFg : unselectedFg,
591
- fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
592
- ),
593
- ),
594
- ],
471
+ const SizedBox(width: KasySpacing.sm),
472
+ KasyTabs.items(
473
+ items: items,
474
+ selectedIndex: _modes.indexOf(theme.mode),
475
+ onTabSelected: (i) => theme.setMode(_modes[i]),
595
476
  ),
596
- ),
477
+ ],
597
478
  ),
598
479
  );
599
480
  }
@@ -75,16 +75,26 @@ class _AdminSheet extends ConsumerWidget {
75
75
  child: Wrap(
76
76
  children: [
77
77
  ValueListenableBuilder<bool>(
78
- valueListenable: devInspectorFabEnabledNotifier,
78
+ valueListenable: devInspectorEnabledNotifier,
79
79
  builder: (context, enabled, _) {
80
80
  return SettingsSwitchTile(
81
81
  icon: KasyIcons.widgets,
82
82
  title: t.settings.admin.inspector_fab_title,
83
+ subtitle:
84
+ '${t.settings.admin.inspector_fab_subtitle_prefix} '
85
+ '${devInspectorShortcutLabel()}',
83
86
  value: enabled,
84
87
  onChanged: (v) async {
85
88
  final p = await SharedPreferences.getInstance();
86
- await p.setBool(devInspectorFabEnabledPrefKey, v);
87
- devInspectorFabEnabledNotifier.value = v;
89
+ await p.setBool(devInspectorEnabledPrefKey, v);
90
+ devInspectorEnabledNotifier.value = v;
91
+ // Turning the inspector ON closes the admin
92
+ // sheet automatically — otherwise the sheet
93
+ // would absorb taps and the user would be
94
+ // stuck. Turning OFF leaves the sheet open.
95
+ if (v && context.mounted) {
96
+ Navigator.of(context).maybePop();
97
+ }
88
98
  },
89
99
  );
90
100
  },
@@ -97,6 +107,9 @@ class _AdminSheet extends ConsumerWidget {
97
107
  return SettingsSwitchTile(
98
108
  icon: KasyIcons.phoneAndroid,
99
109
  title: t.settings.admin.device_preview_title,
110
+ subtitle:
111
+ '${t.settings.admin.inspector_fab_subtitle_prefix} '
112
+ '${webDevicePreviewShortcutLabel()}',
100
113
  value: enabled,
101
114
  onChanged: (v) async {
102
115
  final navigator = Navigator.of(
@@ -77,11 +77,13 @@ class SettingsSwitchTile extends StatelessWidget {
77
77
  required this.title,
78
78
  required this.value,
79
79
  required this.onChanged,
80
+ this.subtitle,
80
81
  this.iconBackgroundColor,
81
82
  });
82
83
 
83
84
  final IconData icon;
84
85
  final String title;
86
+ final String? subtitle;
85
87
  final bool value;
86
88
  final ValueChanged<bool> onChanged;
87
89
  final Color? iconBackgroundColor;
@@ -98,11 +100,26 @@ class SettingsSwitchTile extends StatelessWidget {
98
100
  Icon(icon, size: 21, color: context.colors.onSurface),
99
101
  const SizedBox(width: KasySpacing.sm),
100
102
  Expanded(
101
- child: Text(
102
- title,
103
- style: context.textTheme.titleMedium?.copyWith(
104
- color: context.colors.onSurface,
105
- ),
103
+ child: Column(
104
+ crossAxisAlignment: CrossAxisAlignment.start,
105
+ mainAxisSize: MainAxisSize.min,
106
+ children: <Widget>[
107
+ Text(
108
+ title,
109
+ style: context.textTheme.titleMedium?.copyWith(
110
+ color: context.colors.onSurface,
111
+ ),
112
+ ),
113
+ if (subtitle != null && subtitle!.isNotEmpty) ...[
114
+ const SizedBox(height: 2),
115
+ Text(
116
+ subtitle!,
117
+ style: context.textTheme.bodySmall?.copyWith(
118
+ color: context.colors.onSurface.withValues(alpha: 0.55),
119
+ ),
120
+ ),
121
+ ],
122
+ ],
106
123
  ),
107
124
  ),
108
125
  Transform.scale(
@@ -400,6 +400,7 @@
400
400
  "home_widgets_panel": "Home Widgets panel",
401
401
  "home_widgets_title": "Home Widgets Panel",
402
402
  "inspector_fab_title": "Widget inspector",
403
+ "inspector_fab_subtitle_prefix": "Global shortcut:",
403
404
  "update_mywidget_title": "📱 Update MyWidget Widget",
404
405
  "update_mywidget_desc": "Call manual update for MyWidget widget",
405
406
  "paywalls_title": "Paywalls Admin Panel",
@@ -494,7 +495,8 @@
494
495
  "deactivate": "Deactivate widget inspector",
495
496
  "copyForAi": "Copy for AI",
496
497
  "selectWidgetFirst": "Select a widget on screen first, then tap Copy.",
497
- "inspectorHint": "Tap the UI to select a widget (purple outline). Then copy for your AI prompt."
498
+ "inspectorHint": "Tap the UI to select a widget (purple outline). Then copy for your AI prompt.",
499
+ "statusActive": "Inspecting"
498
500
  },
499
501
  "webDevicePreview": {
500
502
  "frame": "Frame",
@@ -400,6 +400,7 @@
400
400
  "home_widgets_panel": "Panel de Home Widgets",
401
401
  "home_widgets_title": "Panel de Home Widgets",
402
402
  "inspector_fab_title": "Inspector de widgets",
403
+ "inspector_fab_subtitle_prefix": "Atajo global:",
403
404
  "update_mywidget_title": "📱 Actualizar Widget MyWidget",
404
405
  "update_mywidget_desc": "Llamar a la actualización manual para el widget MyWidget",
405
406
  "paywalls_title": "Panel de Admin de Paywalls",
@@ -494,7 +495,8 @@
494
495
  "deactivate": "Desactivar inspector de widgets",
495
496
  "copyForAi": "Copiar para IA",
496
497
  "selectWidgetFirst": "Selecciona un widget en pantalla primero y luego toca Copiar.",
497
- "inspectorHint": "Toca la interfaz para seleccionar (borde morado). Luego copia para tu prompt de IA."
498
+ "inspectorHint": "Toca la interfaz para seleccionar (borde morado). Luego copia para tu prompt de IA.",
499
+ "statusActive": "Inspeccionando"
498
500
  },
499
501
  "webDevicePreview": {
500
502
  "frame": "Frame",
@@ -400,6 +400,7 @@
400
400
  "home_widgets_panel": "Painel de Home Widgets",
401
401
  "home_widgets_title": "Painel de Home Widgets",
402
402
  "inspector_fab_title": "Inspector de widgets",
403
+ "inspector_fab_subtitle_prefix": "Atalho global:",
403
404
  "update_mywidget_title": "📱 Atualizar Widget MyWidget",
404
405
  "update_mywidget_desc": "Chamar atualização manual para o widget MyWidget",
405
406
  "paywalls_title": "Painel Admin de Paywalls",
@@ -494,7 +495,8 @@
494
495
  "deactivate": "Desativar inspetor de widgets",
495
496
  "copyForAi": "Copiar para IA",
496
497
  "selectWidgetFirst": "Selecione um widget na tela primeiro e depois toque em Copiar.",
497
- "inspectorHint": "Toque na interface para selecionar (contorno roxo). Depois copie para usar na IA."
498
+ "inspectorHint": "Toque na interface para selecionar (contorno roxo). Depois copie para usar na IA.",
499
+ "statusActive": "Inspecionando"
498
500
  },
499
501
  "webDevicePreview": {
500
502
  "frame": "Frame",
@@ -139,16 +139,18 @@ flutter_launcher_icons:
139
139
  background_color: "#01171f"
140
140
  theme_color: "#01171f"
141
141
  flutter_native_splash:
142
- color: "#FFFFFF"
143
- color_dark: "#000000"
142
+ # Match KasyColors.background (light / dark) so the splash → app transition
143
+ # has zero white/black flash on web, iOS and Android.
144
+ color: "#F7F7F7"
145
+ color_dark: "#060608"
144
146
  fullscreen: true
145
147
  ios: true
146
148
  android: true
147
149
  image: assets/images/splash_logo_light.png
148
150
  image_dark: assets/images/splash_logo_dark.png
149
151
  android_12:
150
- color: "#FFFFFF"
151
- color_dark: "#000000"
152
+ color: "#F7F7F7"
153
+ color_dark: "#060608"
152
154
  image: assets/images/splash_logo_light_android12.png
153
155
  image_dark: assets/images/splash_logo_dark_android12.png
154
156
 
@@ -46,24 +46,20 @@
46
46
 
47
47
 
48
48
 
49
+
50
+
49
51
  <style id="splash-screen-style">
50
52
  html {
51
- height: 100%;
52
- background-color: #FFFFFF;
53
- color-scheme: light dark;
53
+ height: 100%
54
54
  }
55
55
 
56
56
  body {
57
57
  margin: 0;
58
58
  min-height: 100%;
59
- background-color: #FFFFFF;
59
+ background-color: #F7F7F7;
60
60
  background-size: 100% 100%;
61
61
  }
62
62
 
63
- flutter-view, flt-glass-pane, flt-scene-host {
64
- background-color: #FFFFFF;
65
- }
66
-
67
63
  .center {
68
64
  margin: 0;
69
65
  position: absolute;
@@ -111,23 +107,16 @@
111
107
  }
112
108
 
113
109
  @media (prefers-color-scheme: dark) {
114
- html {
115
- background-color: #000000;
116
- }
117
110
  body {
118
- background-color: #000000;
111
+ background-color: #060608;
119
112
  }
120
- flutter-view, flt-glass-pane, flt-scene-host {
121
- background-color: #000000;
122
- }
123
113
  }
124
114
  </style>
125
115
  <script id="splash-screen-script">
126
116
  function removeSplashFromWeb() {
127
117
  document.getElementById("splash")?.remove();
128
118
  document.getElementById("splash-branding")?.remove();
129
- const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
130
- document.body.style.backgroundColor = isDark ? "#000000" : "#FFFFFF";
119
+ document.body.style.background = "transparent";
131
120
  }
132
121
  </script>
133
122
  </head>
@@ -145,6 +134,7 @@
145
134
 
146
135
 
147
136
 
137
+
148
138
  <script src="flutter_bootstrap.js" async=""></script>
149
139
  <script src="./local_notifications.js"></script>
150
140
 
@@ -1,75 +0,0 @@
1
- // File generated by FlutterFire CLI.
2
- // ignore_for_file: type=lint
3
- import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
4
- import 'package:flutter/foundation.dart'
5
- show defaultTargetPlatform, kIsWeb, TargetPlatform;
6
-
7
- /// Default [FirebaseOptions] for use with your Firebase apps.
8
- ///
9
- /// Example:
10
- /// ```dart
11
- /// import 'firebase_options.dart';
12
- /// // ...
13
- /// await Firebase.initializeApp(
14
- /// options: DefaultFirebaseOptions.currentPlatform,
15
- /// );
16
- /// ```
17
- class DefaultFirebaseOptions {
18
- static FirebaseOptions get currentPlatform {
19
- if (kIsWeb) {
20
- return web;
21
- }
22
- switch (defaultTargetPlatform) {
23
- case TargetPlatform.android:
24
- return android;
25
- case TargetPlatform.iOS:
26
- return ios;
27
- case TargetPlatform.macOS:
28
- throw UnsupportedError(
29
- 'DefaultFirebaseOptions have not been configured for macos - '
30
- 'you can reconfigure this by running the FlutterFire CLI again.',
31
- );
32
- case TargetPlatform.windows:
33
- throw UnsupportedError(
34
- 'DefaultFirebaseOptions have not been configured for windows - '
35
- 'you can reconfigure this by running the FlutterFire CLI again.',
36
- );
37
- case TargetPlatform.linux:
38
- throw UnsupportedError(
39
- 'DefaultFirebaseOptions have not been configured for linux - '
40
- 'you can reconfigure this by running the FlutterFire CLI again.',
41
- );
42
- default:
43
- throw UnsupportedError(
44
- 'DefaultFirebaseOptions are not supported for this platform.',
45
- );
46
- }
47
- }
48
-
49
- static const FirebaseOptions web = FirebaseOptions(
50
- apiKey: 'AIzaSyBTfbPaAAVItBMLaRLL_8Io_VySoQQj8Cw',
51
- appId: '1:613770689965:web:e9347bac3b3eae5d0f7524',
52
- messagingSenderId: '613770689965',
53
- projectId: 'fir-kit-8e56b',
54
- authDomain: 'fir-kit-8e56b.firebaseapp.com',
55
- storageBucket: 'fir-kit-8e56b.firebasestorage.app',
56
- measurementId: 'G-76HE4Q68Z3',
57
- );
58
-
59
- static const FirebaseOptions android = FirebaseOptions(
60
- apiKey: 'AIzaSyD3N3WZOSbA7zcnwyzT-Bazb0Jzq1ozzl8',
61
- appId: '1:613770689965:android:fe5b1880ab0fcf640f7524',
62
- messagingSenderId: '613770689965',
63
- projectId: 'fir-kit-8e56b',
64
- storageBucket: 'fir-kit-8e56b.firebasestorage.app',
65
- );
66
-
67
- static const FirebaseOptions ios = FirebaseOptions(
68
- apiKey: 'AIzaSyBx9gGfVZxMBaAWmpa3dprhV6LEQGC0Pe8',
69
- appId: '1:613770689965:ios:bc5f31dd7db087f00f7524',
70
- messagingSenderId: '613770689965',
71
- projectId: 'fir-kit-8e56b',
72
- storageBucket: 'fir-kit-8e56b.firebasestorage.app',
73
- iosBundleId: 'com.aicrus.firebase.kit',
74
- );
75
- }