kasy-cli 1.37.1 → 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.
- package/lib/scaffold/CHANGELOG.json +9 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
- package/lib/scaffold/backends/patch-base-hashes.json +2 -2
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
- package/templates/firebase/AGENTS.md +7 -1
- package/templates/firebase/DESIGN_SYSTEM.md +13 -0
- package/templates/firebase/lib/components/kasy_app_bar.dart +57 -16
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
- package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
- package/templates/firebase/lib/components/kasy_sidebar.dart +394 -25
- package/templates/firebase/lib/components/kasy_web_header.dart +11 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +2 -213
- package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -4
- package/templates/firebase/lib/core/chrome/web_header_scope.dart +20 -0
- package/templates/firebase/lib/core/data/api/user_api.dart +4 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
- package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +41 -0
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
- package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +498 -466
- package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
- package/templates/firebase/lib/features/home/home_components_page.dart +16 -6
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +7 -0
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +23 -13
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
- package/templates/firebase/lib/features/settings/settings_page.dart +51 -38
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +549 -376
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +266 -141
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +23 -30
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +30 -49
- package/templates/firebase/lib/i18n/en.i18n.json +23 -9
- package/templates/firebase/lib/i18n/es.i18n.json +23 -9
- package/templates/firebase/lib/i18n/pt.i18n.json +23 -9
- package/templates/firebase/lib/router.dart +43 -25
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/admin_shell_chrome_test.dart +104 -0
- package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
- 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
|
-
|
|
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
|
+
}
|