kasy-cli 1.37.0 → 1.38.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 (53) hide show
  1. package/lib/scaffold/CHANGELOG.json +9 -0
  2. package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
  3. package/lib/scaffold/backends/patch-base-hashes.json +4 -4
  4. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
  5. package/package.json +1 -1
  6. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  7. package/templates/firebase/AGENTS.md +20 -10
  8. package/templates/firebase/DESIGN_SYSTEM.md +13 -0
  9. package/templates/firebase/README.en.md +1 -1
  10. package/templates/firebase/README.es.md +1 -1
  11. package/templates/firebase/README.md +1 -1
  12. package/templates/firebase/lib/components/kasy_app_bar.dart +57 -16
  13. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
  14. package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
  15. package/templates/firebase/lib/components/kasy_sidebar.dart +397 -28
  16. package/templates/firebase/lib/components/kasy_web_header.dart +11 -3
  17. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +2 -213
  18. package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
  19. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -4
  20. package/templates/firebase/lib/core/chrome/web_header_scope.dart +20 -0
  21. package/templates/firebase/lib/core/data/api/user_api.dart +4 -0
  22. package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
  23. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
  24. package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
  25. package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
  26. package/templates/firebase/lib/core/states/user_state_notifier.dart +41 -0
  27. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
  28. package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
  29. package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
  30. package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
  31. package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
  32. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +498 -466
  33. package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
  34. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -7
  35. package/templates/firebase/lib/features/home/home_components_page.dart +16 -6
  36. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +7 -0
  37. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +23 -13
  38. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -0
  39. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
  40. package/templates/firebase/lib/features/settings/settings_page.dart +51 -38
  41. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +549 -376
  42. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
  43. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +266 -141
  44. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +23 -30
  45. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +30 -49
  46. package/templates/firebase/lib/i18n/en.i18n.json +23 -9
  47. package/templates/firebase/lib/i18n/es.i18n.json +23 -9
  48. package/templates/firebase/lib/i18n/pt.i18n.json +23 -9
  49. package/templates/firebase/lib/router.dart +43 -25
  50. package/templates/firebase/pubspec.yaml +1 -1
  51. package/templates/firebase/test/admin_shell_chrome_test.dart +104 -0
  52. package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
  53. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
@@ -21,6 +21,53 @@ class DevInspectorInfo {
21
21
  final String? semanticWidget;
22
22
  final List<String> searchHints;
23
23
 
24
+ /// The widget's main color as a usable hex code (e.g. `#FC0303`), or null if
25
+ /// none is set. Raw `Color(...)` descriptions aren't useful to a dev — a hex
26
+ /// is. Text content / labels are intentionally NOT surfaced: they're already
27
+ /// visible on screen, so repeating them just adds noise.
28
+ String? get colorHex {
29
+ for (final p in properties) {
30
+ final int i = p.indexOf(':');
31
+ if (i <= 0) continue;
32
+ if (!p.substring(0, i).toLowerCase().contains('color')) continue;
33
+ final hex = _parseColorHex(p.substring(i + 1));
34
+ if (hex != null) return hex;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ static String? _parseColorHex(String raw) {
40
+ // Legacy form: Color(0xAARRGGBB)
41
+ final legacy = RegExp('0x([0-9a-fA-F]{8})').firstMatch(raw);
42
+ if (legacy != null) {
43
+ final v = legacy.group(1)!.toUpperCase();
44
+ return v.startsWith('FF') ? '#${v.substring(2)}' : '#$v';
45
+ }
46
+ // Modern form: Color(alpha: 1.0, red: 0.98, green: 0.0, blue: 0.0, …)
47
+ double? comp(String key) {
48
+ final m = RegExp('$key:\\s*([0-9.]+)').firstMatch(raw);
49
+ return m == null ? null : double.tryParse(m.group(1)!);
50
+ }
51
+
52
+ final r = comp('red');
53
+ final g = comp('green');
54
+ final b = comp('blue');
55
+ if (r == null || g == null || b == null) return null;
56
+ final a = comp('alpha');
57
+ String h(double v) =>
58
+ (v * 255).round().clamp(0, 255).toRadixString(16).padLeft(2, '0').toUpperCase();
59
+ final rgb = '${h(r)}${h(g)}${h(b)}';
60
+ return (a == null || a >= 0.999) ? '#$rgb' : '#${h(a)}$rgb';
61
+ }
62
+
63
+ /// Compact size string: `w×h` (logical pixels), e.g. `44×44`. Position is
64
+ /// intentionally omitted — it isn't useful when identifying/editing a widget.
65
+ String get sizeLabel {
66
+ final w = boundingBox.width.toStringAsFixed(0);
67
+ final h = boundingBox.height.toStringAsFixed(0);
68
+ return '$w×$h';
69
+ }
70
+
24
71
  String toAIClipboard({String? routeName}) {
25
72
  final buf = StringBuffer();
26
73
  buf.writeln('## Inspected widget');
@@ -87,7 +87,11 @@ abstract final class KasyIcons {
87
87
  static const IconData moreVert = LucideIcons.ellipsisVertical300;
88
88
  static const IconData microphone = LucideIcons.mic300;
89
89
  static const IconData northEast = LucideIcons.arrowUpRight300;
90
- static const IconData notification = LucideIcons.bell300;
90
+ // NOTE: the `bell300` glyph in lucide_icons_flutter 3.1.14+2 is corrupted in the
91
+ // Lucide300 weight font — it renders FILLED instead of outline (the same font
92
+ // renders every other 300 icon correctly). Use the base `bell` (primary Lucide
93
+ // font), which is a proper outline bell, so it matches the rest of the linear set.
94
+ static const IconData notification = LucideIcons.bell;
91
95
  static const IconData notificationActive = LucideIcons.bellRing300;
92
96
  static const IconData notificationAdd = LucideIcons.bellPlus300;
93
97
  static const IconData notificationOff = LucideIcons.bellOff300;
@@ -117,4 +121,15 @@ abstract final class KasyIcons {
117
121
  static const IconData upload = LucideIcons.upload300;
118
122
  static const IconData video = LucideIcons.video300;
119
123
  static const IconData widgets = LucideIcons.component300;
124
+
125
+ /// Device frame visible / hidden (web device preview chrome).
126
+ static const IconData deviceFrame = LucideIcons.smartphone300;
127
+ static const IconData deviceFrameOff = LucideIcons.scan300;
128
+
129
+ /// Screen orientation (web device preview chrome).
130
+ static const IconData landscape = LucideIcons.rectangleHorizontal300;
131
+ static const IconData portrait = LucideIcons.rectangleVertical300;
132
+
133
+ /// Element inspector pick tool (web device preview chrome).
134
+ static const IconData inspector = LucideIcons.squareDashedMousePointer300;
120
135
  }
@@ -60,8 +60,11 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
60
60
  return ref.read(environmentProvider).authenticationMode;
61
61
  }
62
62
 
63
+ StreamSubscription<String?>? _roleSubscription;
64
+
63
65
  @override
64
66
  UserState build() {
67
+ ref.onDispose(() => _roleSubscription?.cancel());
65
68
  return const UserState(user: User.loading());
66
69
  }
67
70
 
@@ -79,6 +82,7 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
79
82
  rethrow;
80
83
  }
81
84
  assert(state.user is! LoadingUserData, 'UserStateNotifier is not ready');
85
+ _syncRoleListener();
82
86
  await _initDeviceRegistration();
83
87
  _deviceRepository.onTokenUpdate(_onUpdateToken);
84
88
  }
@@ -89,6 +93,7 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
89
93
  Future<void> onSignin() async {
90
94
  state = const UserState(user: User.loading());
91
95
  await _loadState();
96
+ _syncRoleListener();
92
97
  await _initDeviceRegistration();
93
98
  // A successful sign-in puts the user past first-run onboarding. Remember it
94
99
  // so that after a later logout they land on the sign-in screen instead of
@@ -166,6 +171,7 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
166
171
  /// It will unregister the device from the notification service
167
172
  /// and logout the user
168
173
  Future<void> onLogout() async {
174
+ _stopRoleListener();
169
175
  final userId = state.user.idOrThrow;
170
176
  _deviceRepository.removeTokenUpdateListener();
171
177
  // Best-effort: if the network call fails we still proceed with logout so
@@ -278,6 +284,7 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
278
284
  /// Here is the function to do it.
279
285
  /// It will delete the user account and logout the user
280
286
  Future<void> deleteAccount() async {
287
+ _stopRoleListener();
281
288
  final userId = state.user.idOrThrow;
282
289
  _deviceRepository.removeTokenUpdateListener();
283
290
  try {
@@ -297,6 +304,40 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
297
304
  // continue as a guest.
298
305
  }
299
306
 
307
+ // -------------------------------
308
+ // ROLE LISTENER
309
+ // -------------------------------
310
+
311
+ void _syncRoleListener() {
312
+ final id = state.user.idOrNull;
313
+ if (id != null && state.user is AuthenticatedUserData) {
314
+ _startRoleListener(id);
315
+ } else {
316
+ _stopRoleListener();
317
+ }
318
+ }
319
+
320
+ void _startRoleListener(String userId) {
321
+ _roleSubscription?.cancel();
322
+ _roleSubscription = _userRepository
323
+ .watchRole(userId)
324
+ .skip(1) // skip initial snapshot — state was just loaded
325
+ .listen((newRole) {
326
+ final currentRole = switch (state.user) {
327
+ AuthenticatedUserData(:final role) => role,
328
+ _ => null,
329
+ };
330
+ if (newRole != currentRole) {
331
+ refresh();
332
+ }
333
+ });
334
+ }
335
+
336
+ void _stopRoleListener() {
337
+ _roleSubscription?.cancel();
338
+ _roleSubscription = null;
339
+ }
340
+
300
341
  // -------------------------------
301
342
  // PRIVATES
302
343
  // -------------------------------
@@ -78,6 +78,15 @@ class UniversalThemeFactory extends KasyThemeDataFactory {
78
78
  bottomSheetTheme: const BottomSheetThemeData(
79
79
  constraints: BoxConstraints(),
80
80
  ),
81
+ // Drawers are flat panels (like the sidebar rail), not Material 3's
82
+ // rounded, shadowed sheet: square edge, surface fill, no drop shadow.
83
+ // Set once here so every Drawer in the app is consistent — no per-use
84
+ // shape/elevation overrides needed.
85
+ drawerTheme: DrawerThemeData(
86
+ backgroundColor: colors.surface,
87
+ shape: const RoundedRectangleBorder(),
88
+ elevation: 0,
89
+ ),
81
90
  ),
82
91
  );
83
92
  }
@@ -0,0 +1,14 @@
1
+ /// Exports a device-preview screenshot PNG to the clipboard (web), with a
2
+ /// download fallback.
3
+ ///
4
+ /// The real implementation uses web-only APIs (`dart:js_interop` + `package:web`)
5
+ /// which do NOT compile on the Dart VM / native. Those are isolated in
6
+ /// `png_clipboard_web.dart` and pulled in ONLY on web via a conditional import;
7
+ /// everywhere else the no-op stub (`png_clipboard_io.dart`) is used. This keeps
8
+ /// `flutter test` (VM compile) and native builds green — the web-only code never
9
+ /// reaches them.
10
+ library;
11
+
12
+ export 'png_clipboard_io.dart'
13
+ if (dart.library.js_interop) 'png_clipboard_web.dart';
14
+ export 'png_export_result.dart';
@@ -0,0 +1,9 @@
1
+ import 'dart:typed_data';
2
+
3
+ import 'package:kasy_kit/core/web_device_preview/png_export_result.dart';
4
+
5
+ /// Non-web stub. The device preview is web-only, so this never runs at runtime —
6
+ /// it exists only so the VM / native build (and `flutter test`) compile without
7
+ /// the web interop.
8
+ Future<PngExportResult> copyOrDownloadPng(Uint8List bytes) async =>
9
+ PngExportResult.unavailable;
@@ -0,0 +1,36 @@
1
+ import 'dart:js_interop';
2
+ import 'dart:typed_data';
3
+
4
+ import 'package:kasy_kit/core/web_device_preview/png_export_result.dart';
5
+ import 'package:web/web.dart' as web;
6
+
7
+ /// Copies [bytes] (a PNG) straight to the clipboard so it can be pasted into an
8
+ /// AI chat. Falls back to a file download if the browser blocks image clipboard
9
+ /// writes.
10
+ Future<PngExportResult> copyOrDownloadPng(Uint8List bytes) async {
11
+ final blob = web.Blob(
12
+ <JSAny>[bytes.toJS].toJS,
13
+ web.BlobPropertyBag(type: 'image/png'),
14
+ );
15
+ try {
16
+ final item = web.ClipboardItem(
17
+ <String, JSAny>{'image/png': blob}.jsify()! as JSObject,
18
+ );
19
+ await web.window.navigator.clipboard
20
+ .write(<web.ClipboardItem>[item].toJS)
21
+ .toDart;
22
+ return PngExportResult.copied;
23
+ } catch (_) {
24
+ _downloadBlob(blob);
25
+ return PngExportResult.downloaded;
26
+ }
27
+ }
28
+
29
+ void _downloadBlob(web.Blob blob) {
30
+ final url = web.URL.createObjectURL(blob);
31
+ web.HTMLAnchorElement()
32
+ ..href = url
33
+ ..download = 'preview_${DateTime.now().millisecondsSinceEpoch}.png'
34
+ ..click();
35
+ web.URL.revokeObjectURL(url);
36
+ }
@@ -0,0 +1,2 @@
1
+ /// Outcome of exporting the device-preview screenshot PNG.
2
+ enum PngExportResult { copied, downloaded, unavailable }