kasy-cli 1.37.1 → 1.39.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 (120) hide show
  1. package/lib/scaffold/CHANGELOG.json +23 -0
  2. package/lib/scaffold/backends/api/patch/README.md +15 -0
  3. package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
  4. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
  5. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
  6. package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
  7. package/lib/scaffold/backends/patch-base-hashes.json +6 -6
  8. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  9. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
  10. package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
  11. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
  12. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
  13. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  14. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
  15. package/lib/scaffold/shared/generator-utils.js +12 -6
  16. package/package.json +1 -1
  17. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  18. package/templates/firebase/AGENTS.md +7 -1
  19. package/templates/firebase/DESIGN_SYSTEM.md +35 -8
  20. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  21. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  22. package/templates/firebase/assets/icons/facebook.svg +49 -0
  23. package/templates/firebase/assets/icons/google.svg +1 -0
  24. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  25. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  26. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  27. package/templates/firebase/lib/components/components.dart +1 -1
  28. package/templates/firebase/lib/components/kasy_app_bar.dart +361 -20
  29. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
  30. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  31. package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
  32. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  33. package/templates/firebase/lib/components/kasy_sidebar.dart +412 -31
  34. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  35. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  36. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +29 -231
  37. package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
  38. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +19 -9
  39. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  40. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  41. package/templates/firebase/lib/core/data/api/user_api.dart +15 -0
  42. package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
  43. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
  44. package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
  45. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  46. package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
  47. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +18 -35
  48. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  49. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  50. package/templates/firebase/lib/core/states/user_state_notifier.dart +69 -1
  51. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  52. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  53. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
  54. package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
  55. package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
  56. package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
  57. package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
  58. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +547 -483
  59. package/templates/firebase/lib/core/web_viewport_scale.dart +64 -35
  60. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  61. package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
  62. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  63. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  64. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  65. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  66. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  67. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  68. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  69. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  70. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  71. package/templates/firebase/lib/features/home/home_components_page.dart +264 -126
  72. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +231 -57
  73. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  74. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  75. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  76. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +118 -57
  77. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  78. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +19 -4
  79. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  80. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  81. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  82. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
  83. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  84. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  85. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  86. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  87. package/templates/firebase/lib/features/settings/settings_page.dart +99 -65
  88. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1379 -422
  89. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
  90. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  91. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  92. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +404 -149
  93. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +24 -31
  94. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  95. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +77 -95
  96. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  97. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  98. package/templates/firebase/lib/i18n/en.i18n.json +749 -698
  99. package/templates/firebase/lib/i18n/es.i18n.json +749 -698
  100. package/templates/firebase/lib/i18n/pt.i18n.json +749 -698
  101. package/templates/firebase/lib/main.dart +20 -7
  102. package/templates/firebase/lib/router.dart +70 -46
  103. package/templates/firebase/pubspec.yaml +2 -1
  104. package/templates/firebase/test/admin_shell_chrome_test.dart +110 -0
  105. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  106. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  107. package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
  108. package/templates/firebase/tool/design_check.dart +9 -0
  109. package/templates/firebase/assets/icons/apple.png +0 -0
  110. package/templates/firebase/assets/icons/facebook.png +0 -0
  111. package/templates/firebase/assets/icons/google.png +0 -0
  112. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  113. package/templates/firebase/lib/components/kasy_web_header.dart +0 -210
  114. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  115. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  116. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  117. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  118. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -169
  119. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
  120. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
@@ -1,4 +1,27 @@
1
1
  {
2
+ "1.39.0": {
3
+ "modules": {
4
+ "core": {
5
+ "pt": "Proporção da interface na web virou um ajuste único e documentado: a web renderiza ~5% menor que o nativo (escala 0.95) em todos os tamanhos de tela, deixando a proporção mais próxima do app nativo. O nativo fica intacto (1.0) e respeitando a fonte do sistema, e dá pra desligar tudo numa linha (kWebViewportScaleEnabled). É uma camada de densidade por cima do design system responsivo, não substitui breakpoints/tipografia.",
6
+ "en": "The web interface proportion is now a single, documented knob: web renders ~5% smaller than native (0.95 scale) at every screen size, bringing it closer to the native app. Native stays untouched (1.0) and respects the system font size, and the whole thing can be turned off in one line (kWebViewportScaleEnabled). It is a density layer on top of the responsive design system, not a replacement for breakpoints/typography.",
7
+ "es": "La proporción de la interfaz en web ahora es un ajuste único y documentado: la web se renderiza ~5% más pequeña que el nativo (escala 0.95) en todos los tamaños de pantalla, acercándola al app nativo. El nativo queda intacto (1.0) y respeta el tamaño de fuente del sistema, y todo se puede apagar en una línea (kWebViewportScaleEnabled). Es una capa de densidad sobre el design system responsivo, no reemplaza breakpoints/tipografía."
8
+ },
9
+ "components": {
10
+ "pt": "Campo de texto com altura idêntica em qualquer renderizador e breakpoint (a linha é travada via strut, então web CanvasKit e nativo batem certinho, sem variar ~1px); e card clicável agora tem destaque ao passar o cursor na web, parecendo um controle interativo de verdade.",
11
+ "en": "Text field with identical height across every renderer and breakpoint (the line is locked via strut, so web CanvasKit and native match exactly, with no ~1px drift); and a tappable card now has a hover highlight on web, feeling like a real interactive control.",
12
+ "es": "Campo de texto con altura idéntica en cualquier renderizador y breakpoint (la línea se fija con strut, así web CanvasKit y nativo coinciden exactamente, sin variar ~1px); y una tarjeta clicable ahora tiene resaltado al pasar el cursor en web, sintiéndose como un control interactivo real."
13
+ }
14
+ }
15
+ },
16
+ "1.38.0": {
17
+ "modules": {
18
+ "components": {
19
+ "pt": "Sidebar agora suporta submenus expansíveis (grupos com sub-itens, como o menu suspenso de Configurações na Home): clicar no grupo abre/fecha os filhos, o item ativo do submenu fica em negrito forte (sem pílula) e o submenu fecha sozinho ao navegar para outra tela.",
20
+ "en": "Sidebar now supports expandable submenus (groups with sub-items, like the Settings dropdown on Home): tapping the group opens/closes its children, the active sub-item shows in strong bold (no pill) and the submenu closes on its own when you navigate to another screen.",
21
+ "es": "La sidebar ahora admite submenús expansibles (grupos con subítems, como el menú desplegable de Ajustes en la Home): tocar el grupo abre/cierra sus hijos, el ítem activo del submenú se muestra en negrita fuerte (sin píldora) y el submenú se cierra solo al navegar a otra pantalla."
22
+ }
23
+ }
24
+ },
2
25
  "1.37.0": {
3
26
  "modules": {
4
27
  "components": {
@@ -112,6 +112,7 @@ GET /admin/users
112
112
  "email": "ana@b.com" | null,
113
113
  "name": "Ana" | null,
114
114
  "createdAt": 1700000000000, // epoch em milissegundos | null
115
+ "avatarPath": "https://.../thumb.jpg" | null, // foto do usuario, se tiver
115
116
  "subscriber": true // tem assinatura ativa?
116
117
  }
117
118
  ],
@@ -230,6 +231,20 @@ POST /users/{userId}/devices/cleanup-stale → remove devices anti
230
231
  DELETE /users/{userId}/devices → remove todos os devices do usuário (ex.: no logout)
231
232
  ```
232
233
 
234
+ > **Device sem token de push (importante):** o app registra o device **mesmo sem
235
+ > permissão de push** — nesse caso o `token` vem **vazio** (`""`). É de propósito:
236
+ > rastreia a instalação e dispara a boas-vindas (abaixo) sem depender do push; o
237
+ > token é preenchido depois, via `PUT /devices/{deviceId}`, quando o usuário ativar
238
+ > as notificações. Seu backend deve **aceitar token vazio** e, ao enviar push,
239
+ > **pular** os devices com token vazio (não os trate como inválidos nem os apague).
240
+
241
+ > **Notificação de boas-vindas:** crie-a **uma única vez por conta**, no primeiro
242
+ > registro de device (`POST /users/{userId}/devices`), de forma **independente do
243
+ > push** (vale para conta anônima também). Persista só no banco (sem disparar push) e
244
+ > use o `extra_data.deviceLocale` enviado pelo device para localizar a mensagem
245
+ > (`pt`/`es`/`en`). Referência exata: o trigger `trigger_welcome_notification` do
246
+ > backend Supabase e o `onFirstDeviceRegistered` do Firebase.
247
+
233
248
  ### Endpoints: notifications
234
249
 
235
250
  ```
@@ -44,6 +44,24 @@ class UserApi {
44
44
  }
45
45
  }
46
46
 
47
+ /// Watches the user's role so an admin promotion/demotion is picked up at
48
+ /// runtime (the shared user state refreshes when it changes). A REST backend
49
+ /// has no realtime channel, so this polls: it emits the current role at once
50
+ /// (mirroring the immediate snapshot of the Firebase/Supabase streams, which
51
+ /// the state notifier skips) and then re-checks periodically. The notifier
52
+ /// cancels the subscription on logout/dispose, which ends this generator.
53
+ Stream<String?> watchRole(String id) async* {
54
+ while (true) {
55
+ try {
56
+ final user = await get(id);
57
+ yield user?.role;
58
+ } catch (_) {
59
+ // Ignore transient network errors and keep polling.
60
+ }
61
+ await Future<void>.delayed(const Duration(seconds: 30));
62
+ }
63
+ }
64
+
47
65
  Future<void> update(UserEntity user) async {
48
66
  try {
49
67
  final data = user.toJson()..removeWhere((_, v) => v == null);
@@ -118,12 +118,17 @@ class FirebaseDeviceApi implements DeviceApi {
118
118
  await Future.delayed(const Duration(seconds: 1));
119
119
  }
120
120
  }
121
- final token = await _messaging.getToken();
122
- if (token == null) {
123
- throw ApiError(
124
- code: 0,
125
- message: 'FCM token is null check Firebase setup and notification permissions',
126
- );
121
+ // Register the installation even without a push token. On iOS the FCM
122
+ // token stays null until the user grants notification permission (and
123
+ // getToken can even throw before APNS is ready). We still create the
124
+ // device row — so the welcome notification fires and the install is
125
+ // tracked — and the token fills in later via onTokenRefresh once push is
126
+ // enabled.
127
+ String token = '';
128
+ try {
129
+ token = await _messaging.getToken() ?? '';
130
+ } catch (_) {
131
+ token = '';
127
132
  }
128
133
  final os = Platform.isAndroid
129
134
  ? OperatingSystem.android //
@@ -16,6 +16,11 @@ class AdminUser {
16
16
  final String? email;
17
17
  final String? name;
18
18
  final DateTime? createdAt;
19
+
20
+ /// Public photo URL when the user has an avatar (social login import or a
21
+ /// manual upload). Null/empty for users without a picture — the UI then
22
+ /// falls back to initials.
23
+ final String? avatarPath;
19
24
  final bool subscriber;
20
25
 
21
26
  const AdminUser({
@@ -24,6 +29,7 @@ class AdminUser {
24
29
  this.email,
25
30
  this.name,
26
31
  this.createdAt,
32
+ this.avatarPath,
27
33
  });
28
34
 
29
35
  factory AdminUser.fromMap(Map<String, dynamic> m) {
@@ -35,6 +41,7 @@ class AdminUser {
35
41
  createdAt: createdMillis is num
36
42
  ? DateTime.fromMillisecondsSinceEpoch(createdMillis.toInt())
37
43
  : null,
44
+ avatarPath: m['avatarPath'] as String?,
38
45
  subscriber: m['subscriber'] == true,
39
46
  );
40
47
  }
@@ -65,7 +72,8 @@ final adminUsersApiProvider = Provider<AdminUsersApi>(
65
72
  /// Response 200: {
66
73
  /// "users": [
67
74
  /// {"id": "...", "email": "a@b.com"|null, "name": "Ana"|null,
68
- /// "createdAt": 1700000000000 (epoch millis)|null, "subscriber": true}
75
+ /// "createdAt": 1700000000000 (epoch millis)|null,
76
+ /// "avatarPath": "https://.../thumb.jpg"|null, "subscriber": true}
69
77
  /// ],
70
78
  /// "totalUsers": 142, // true size of the collection
71
79
  /// "truncated": false // true when more users exist than returned
@@ -38,6 +38,7 @@ dependencies:
38
38
  flutter_native_splash: ^2.4.7
39
39
  flutter_riverpod: ^3.1.0
40
40
  flutter_secure_storage: ^9.2.4
41
+ flutter_svg: ^2.3.0
41
42
  flutter_timezone: ^5.0.1
42
43
  freezed_annotation: ^3.1.0
43
44
  gal: ^1.3.0
@@ -2,7 +2,7 @@
2
2
  "api/android/app/src/main/AndroidManifest.xml": "0579c57068ee50d43837ef9c7bee884b6232e92975301c8305ce389282f68291",
3
3
  "api/ios/Runner/AppDelegate.swift": "9f1027a03ad1fef2dab45464fbd7e98364b6a9cb6d2a6abe770ee9c45fa19122",
4
4
  "api/lib/core/data/api/storage_api.dart": "d77fecc6923ea9544b6ec20571dcab5c8f3d38be81ec618b0cf4f1b08be7bbae",
5
- "api/lib/core/data/api/user_api.dart": "06f491ebf4548b87431e2d6983148f4082e120ec7d6a928108cba954d16d8c56",
5
+ "api/lib/core/data/api/user_api.dart": "14166f7e3419884c514f93126a64795264f5f2a743b050a600bb37631e0d7e23",
6
6
  "api/lib/core/data/entities/json_converters.dart": "92a5efd595a03a862fa03e77ab3ca3d556523d0c9e0daca6e296e35d24b760a8",
7
7
  "api/lib/core/data/entities/user_entity.dart": "5c1e56e4be3ba2ed95cdcd276112a02b526f2a3fa7ee5bba08a3ec2b51153688",
8
8
  "api/lib/environments.dart": "c41f0843a4d627b717579eec5607b7ff5c16369bdd4b2a82e081058a20ffb7a5",
@@ -17,7 +17,7 @@
17
17
  "api/lib/features/feedbacks/api/entities/feature_vote_entity.dart": "a13ccfc493d8f5277beffc8e28f77ec322ab0c0a56358c3669353206b7c6756b",
18
18
  "api/lib/features/feedbacks/api/feature_request_api.dart": "a741395509b95e8cae135bc8bdcca1ba0d48785b1e1b201732219659d271346b",
19
19
  "api/lib/features/feedbacks/api/feature_vote_api.dart": "89dcaeea29460a7f219c2ede3759707b45066272fe6837c29a40f3d79fade4e9",
20
- "api/lib/features/notifications/api/device_api.dart": "0d6bccb6813cceec29c54220daac9d0cd35687c4fa9f10ffde07dad88b204ed3",
20
+ "api/lib/features/notifications/api/device_api.dart": "cedc21bf8db4eeaf63f9723d539d6afddde01b30332acfa9c70b22ba7de25698",
21
21
  "api/lib/features/notifications/api/entities/device_entity.dart": "54fce6274c5fe60ba663d8693e0c9d88fd6cd6fcd9ec906e021621a2e51176b7",
22
22
  "api/lib/features/notifications/api/entities/notifications_entity.dart": "f9ee62111a6f122657e105df317f1f5f803e3a525a96686c0a2728ff02cd8964",
23
23
  "api/lib/features/notifications/api/notifications_api.dart": "30cdb1bc52c3249b2cda6b1c7eb7fb6a977db5744da5a9c2980784f41e75d9e7",
@@ -25,7 +25,7 @@
25
25
  "api/lib/features/onboarding/api/user_infos_api.dart": "cf728bcef364259912ee1e87151c4f791faee0c0d49232c9af260bb5681bd429",
26
26
  "api/lib/features/onboarding/models/user_info.dart": "946fd34a33b630a34a3b6248594cbca717f4fab9d23f669510145b9032bd1321",
27
27
  "api/lib/features/onboarding/repositories/user_infos_repository.dart": "487a5177dcaed7f87e613b2b4f04329c4995274e6a52809d4917c037cb3c3c55",
28
- "api/lib/features/settings/ui/components/admin/admin_users_api.dart": "0ffb232ffb193154c0366d6f7da0ca499b3e06fed4fd71af572b8dc53ecd844e",
28
+ "api/lib/features/settings/ui/components/admin/admin_users_api.dart": "ed07e674817099691d6131e8ee7ced1332b9291f0eac207a720f20558e20e64a",
29
29
  "api/lib/features/settings/ui/widgets/avatar_utils.dart": "bb9126409bbbb245f2dec613bd096ac53c208a56bd55f3d2ab2599e43534904f",
30
30
  "api/lib/features/subscriptions/api/entities/subscription_entity.dart": "20c1d75ed9d88acb96e94a592dcb4de0f63c792302ea07df53cebbdeb6d0cf7e",
31
31
  "api/lib/features/subscriptions/api/stripe_backend_api.dart": "e370bd05211462f2fc94c69af1749eb5330f997e9ec5e73e5c4e119999bf666f",
@@ -34,7 +34,7 @@
34
34
  "supabase/android/app/src/main/AndroidManifest.xml": "0579c57068ee50d43837ef9c7bee884b6232e92975301c8305ce389282f68291",
35
35
  "supabase/ios/Runner/AppDelegate.swift": "9f1027a03ad1fef2dab45464fbd7e98364b6a9cb6d2a6abe770ee9c45fa19122",
36
36
  "supabase/lib/core/data/api/storage_api.dart": "d77fecc6923ea9544b6ec20571dcab5c8f3d38be81ec618b0cf4f1b08be7bbae",
37
- "supabase/lib/core/data/api/user_api.dart": "06f491ebf4548b87431e2d6983148f4082e120ec7d6a928108cba954d16d8c56",
37
+ "supabase/lib/core/data/api/user_api.dart": "14166f7e3419884c514f93126a64795264f5f2a743b050a600bb37631e0d7e23",
38
38
  "supabase/lib/core/data/entities/json_converters.dart": "92a5efd595a03a862fa03e77ab3ca3d556523d0c9e0daca6e296e35d24b760a8",
39
39
  "supabase/lib/core/data/entities/user_entity.dart": "5c1e56e4be3ba2ed95cdcd276112a02b526f2a3fa7ee5bba08a3ec2b51153688",
40
40
  "supabase/lib/environments.dart": "c41f0843a4d627b717579eec5607b7ff5c16369bdd4b2a82e081058a20ffb7a5",
@@ -48,7 +48,7 @@
48
48
  "supabase/lib/features/feedbacks/api/entities/feature_vote_entity.dart": "a13ccfc493d8f5277beffc8e28f77ec322ab0c0a56358c3669353206b7c6756b",
49
49
  "supabase/lib/features/feedbacks/api/feature_request_api.dart": "a741395509b95e8cae135bc8bdcca1ba0d48785b1e1b201732219659d271346b",
50
50
  "supabase/lib/features/feedbacks/api/feature_vote_api.dart": "89dcaeea29460a7f219c2ede3759707b45066272fe6837c29a40f3d79fade4e9",
51
- "supabase/lib/features/notifications/api/device_api.dart": "0d6bccb6813cceec29c54220daac9d0cd35687c4fa9f10ffde07dad88b204ed3",
51
+ "supabase/lib/features/notifications/api/device_api.dart": "cedc21bf8db4eeaf63f9723d539d6afddde01b30332acfa9c70b22ba7de25698",
52
52
  "supabase/lib/features/notifications/api/entities/device_entity.dart": "54fce6274c5fe60ba663d8693e0c9d88fd6cd6fcd9ec906e021621a2e51176b7",
53
53
  "supabase/lib/features/notifications/api/entities/notifications_entity.dart": "f9ee62111a6f122657e105df317f1f5f803e3a525a96686c0a2728ff02cd8964",
54
54
  "supabase/lib/features/notifications/api/notifications_api.dart": "30cdb1bc52c3249b2cda6b1c7eb7fb6a977db5744da5a9c2980784f41e75d9e7",
@@ -56,7 +56,7 @@
56
56
  "supabase/lib/features/onboarding/api/user_infos_api.dart": "cf728bcef364259912ee1e87151c4f791faee0c0d49232c9af260bb5681bd429",
57
57
  "supabase/lib/features/onboarding/models/user_info.dart": "946fd34a33b630a34a3b6248594cbca717f4fab9d23f669510145b9032bd1321",
58
58
  "supabase/lib/features/onboarding/repositories/user_infos_repository.dart": "487a5177dcaed7f87e613b2b4f04329c4995274e6a52809d4917c037cb3c3c55",
59
- "supabase/lib/features/settings/ui/components/admin/admin_users_api.dart": "0ffb232ffb193154c0366d6f7da0ca499b3e06fed4fd71af572b8dc53ecd844e",
59
+ "supabase/lib/features/settings/ui/components/admin/admin_users_api.dart": "ed07e674817099691d6131e8ee7ced1332b9291f0eac207a720f20558e20e64a",
60
60
  "supabase/lib/features/settings/ui/widgets/avatar_utils.dart": "bb9126409bbbb245f2dec613bd096ac53c208a56bd55f3d2ab2599e43534904f",
61
61
  "supabase/lib/features/subscriptions/api/entities/subscription_entity.dart": "20c1d75ed9d88acb96e94a592dcb4de0f63c792302ea07df53cebbdeb6d0cf7e",
62
62
  "supabase/lib/features/subscriptions/api/stripe_backend_api.dart": "e370bd05211462f2fc94c69af1749eb5330f997e9ec5e73e5c4e119999bf666f",
@@ -39,6 +39,7 @@ interface AdminUser {
39
39
  email: string | null;
40
40
  name: string | null;
41
41
  createdAt: number | null; // epoch millis
42
+ avatarPath: string | null; // public photo URL, when the user has one
42
43
  subscriber: boolean;
43
44
  }
44
45
 
@@ -105,7 +106,7 @@ Deno.serve(async (req: Request) => {
105
106
  // client re-sorts anyway, so this only decides which rows survive the cap.
106
107
  const { data: rows, error: rowsError } = await admin
107
108
  .from("users")
108
- .select("id, email, name, creation_date")
109
+ .select("id, email, name, creation_date, avatar_url")
109
110
  .order("creation_date", { ascending: false, nullsFirst: false })
110
111
  .limit(MAX_SCAN);
111
112
  if (rowsError) throw rowsError;
@@ -132,6 +133,7 @@ Deno.serve(async (req: Request) => {
132
133
  email: (d.email as string) || null,
133
134
  name: (d.name as string) || null,
134
135
  createdAt: d.creation_date ? new Date(d.creation_date).getTime() : null,
136
+ avatarPath: (d.avatar_url as string) || null,
135
137
  subscriber: activeSubscribers.has(d.id),
136
138
  }));
137
139
 
@@ -295,6 +295,9 @@ Deno.serve(async (req: Request) => {
295
295
  const staleTokenIds: string[] = [];
296
296
 
297
297
  for (const device of devices) {
298
+ // Installs without a push token (notifications not enabled yet) aren't
299
+ // sendable and must NOT be treated as stale/deleted — just skip them.
300
+ if (!device.token) continue;
298
301
  const ok = await sendToToken(projectId, accessToken, device.token, notification.title, notification.body, extraData, notification.image_url);
299
302
  if (ok) {
300
303
  sent++;
@@ -0,0 +1,62 @@
1
+ -- Decouple the welcome notification from push permission (parity with Firebase).
2
+ --
3
+ -- Before: the welcome was SKIPPED for anonymous users (no email) and read the
4
+ -- locale from public.users.locale — which is null at signup, so it always fell
5
+ -- back to English. Combined with the fact that a device only registered once a
6
+ -- push token existed (iOS required permission), users who skipped push never got
7
+ -- a welcome at all.
8
+ --
9
+ -- Now: the install registers even without a push token (client change), so this
10
+ -- trigger fires for EVERY account on first device registration, and localises
11
+ -- from the device's own reported locale (device.extra_data.deviceLocale) —
12
+ -- exactly like the Firebase `onFirstDeviceRegistered` trigger. The one-time
13
+ -- `welcome_sent` claim and notify_user = false (DB only, no push) are unchanged.
14
+
15
+ CREATE OR REPLACE FUNCTION public.trigger_welcome_notification()
16
+ RETURNS TRIGGER AS $$
17
+ DECLARE
18
+ v_locale text;
19
+ v_claimed int;
20
+ v_title text;
21
+ v_body text;
22
+ BEGIN
23
+ -- Atomically claim the one-time welcome: flip welcome_sent to true only if it
24
+ -- isn't already. ROW_COUNT is 1 for the very first claim, 0 for any later
25
+ -- device registration — including one re-created after a logout/login cycle.
26
+ UPDATE public.users
27
+ SET welcome_sent = true
28
+ WHERE id = NEW.user_id AND welcome_sent = false;
29
+
30
+ GET DIAGNOSTICS v_claimed = ROW_COUNT;
31
+ IF v_claimed = 0 THEN
32
+ RETURN NEW;
33
+ END IF;
34
+
35
+ -- Locale from the device that just registered (e.g. 'pt_BR' -> 'pt'),
36
+ -- independent of push permission. Falls back to English.
37
+ v_locale := lower(substring(coalesce(NEW.extra_data->>'deviceLocale', 'en') from 1 for 2));
38
+
39
+ IF v_locale = 'pt' THEN
40
+ v_title := 'Bem-vindo!';
41
+ v_body := 'Sua conta está pronta. Aproveite!';
42
+ ELSIF v_locale = 'es' THEN
43
+ v_title := '¡Bienvenido!';
44
+ v_body := 'Tu cuenta está lista. ¡Disfrútala!';
45
+ ELSE
46
+ v_title := 'Welcome!';
47
+ v_body := 'Your account is ready. Enjoy!';
48
+ END IF;
49
+
50
+ -- notify_user = false: user is inside the app, no push needed.
51
+ INSERT INTO public.notifications (user_id, title, body, type, notify_user)
52
+ VALUES (NEW.user_id, v_title, v_body, 'welcome', false);
53
+
54
+ RETURN NEW;
55
+ END;
56
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
57
+
58
+ -- Re-assert the trigger so a database built from these migrations is consistent.
59
+ DROP TRIGGER IF EXISTS on_first_device_registered ON public.devices;
60
+ CREATE TRIGGER on_first_device_registered
61
+ AFTER INSERT ON public.devices
62
+ FOR EACH ROW EXECUTE PROCEDURE public.trigger_welcome_notification();
@@ -40,6 +40,14 @@ class UserApi {
40
40
  }
41
41
  }
42
42
 
43
+ Stream<String?> watchRole(String id) {
44
+ return _client
45
+ .from('users')
46
+ .stream(primaryKey: ['id'])
47
+ .eq('id', id)
48
+ .map((rows) => rows.isEmpty ? null : rows.first['role'] as String?);
49
+ }
50
+
43
51
  Future<void> update(UserEntity user) async {
44
52
  try {
45
53
  final data = user.toJson()..removeWhere((_, v) => v == null);
@@ -98,12 +98,17 @@ class FirebaseDeviceApi implements DeviceApi {
98
98
  await Future.delayed(const Duration(seconds: 1));
99
99
  }
100
100
  }
101
- final token = await _messaging.getToken();
102
- if (token == null) {
103
- throw ApiError(
104
- code: 0,
105
- message: 'FCM token is null check Firebase setup and notification permissions',
106
- );
101
+ // Register the installation even without a push token. On iOS the FCM
102
+ // token stays null until the user grants notification permission (and
103
+ // getToken can even throw before APNS is ready). We still create the
104
+ // device row — so the welcome notification fires and the install is
105
+ // tracked — and the token fills in later via onTokenRefresh once push is
106
+ // enabled.
107
+ String token = '';
108
+ try {
109
+ token = await _messaging.getToken() ?? '';
110
+ } catch (_) {
111
+ token = '';
107
112
  }
108
113
  final os = Platform.isAndroid
109
114
  ? OperatingSystem.android //
@@ -16,6 +16,11 @@ class AdminUser {
16
16
  final String? email;
17
17
  final String? name;
18
18
  final DateTime? createdAt;
19
+
20
+ /// Public photo URL when the user has an avatar (social login import or a
21
+ /// manual upload). Null/empty for users without a picture — the UI then
22
+ /// falls back to initials.
23
+ final String? avatarPath;
19
24
  final bool subscriber;
20
25
 
21
26
  const AdminUser({
@@ -24,6 +29,7 @@ class AdminUser {
24
29
  this.email,
25
30
  this.name,
26
31
  this.createdAt,
32
+ this.avatarPath,
27
33
  });
28
34
 
29
35
  factory AdminUser.fromMap(Map<String, dynamic> m) {
@@ -35,6 +41,7 @@ class AdminUser {
35
41
  createdAt: createdMillis is num
36
42
  ? DateTime.fromMillisecondsSinceEpoch(createdMillis.toInt())
37
43
  : null,
44
+ avatarPath: m['avatarPath'] as String?,
38
45
  subscriber: m['subscriber'] == true,
39
46
  );
40
47
  }
@@ -45,6 +45,7 @@ dependencies:
45
45
  flutter_native_splash: ^2.4.7
46
46
  flutter_riverpod: ^3.1.0
47
47
  flutter_secure_storage: ^9.2.4
48
+ flutter_svg: ^2.3.0
48
49
  flutter_timezone: ^5.0.1
49
50
  freezed_annotation: ^3.1.0
50
51
  gal: ^1.3.0
@@ -1381,13 +1381,19 @@ async function removeFacebookSigninFromAuthPages(projectDir) {
1381
1381
  // Legacy: remove the standalone FacebookSigninComponent + its import, if present.
1382
1382
  content = content.replace(/^import 'package:[^']+\/features\/authentication\/ui\/components\/facebook_signin\.dart';\n/m, '');
1383
1383
  content = content.replace(/[ \t]*const FacebookSigninComponent\(\),\n/g, '');
1384
- // Current UI: the Facebook button is an inline _SocialSigninTile/_SocialSignupTile
1385
- // in the social row (label: t.auth.signin.facebook ... signinWithFacebook()). Strip
1386
- // the whole tile plus the SizedBox spacer that precedes it, anchored on the facebook
1387
- // label so the Google/Apple tiles are never touched. Runs before dartFix/format, so
1388
- // it matches the kit's raw formatting verbatim.
1384
+ // Current UI: the Facebook button is an inline SocialAuthButton in the social
1385
+ // row, wrapped in a collection-if (`if (showFacebook) ...[ ... ]`). Strip the
1386
+ // WHOLE if-block wrapper, SizedBox spacer and tile anchored on the facebook
1387
+ // label so the Google/Apple tiles are never touched. Runs before dartFix/format,
1388
+ // so it matches the kit's raw formatting verbatim.
1389
1389
  content = content.replace(
1390
- /\n[ \t]*const SizedBox\(width: KasySpacing\.sm\),\n[ \t]*Expanded\(\n[ \t]*child: _Social(?:Signin|Signup)Tile\(\n[ \t]*label: t\.auth\.signin\.facebook,[\s\S]*?\.signinWithFacebook\(\),\n[ \t]*\),\n[ \t]*\),/,
1390
+ /\n[ \t]*if \(showFacebook\) \.\.\.\[\n[ \t]*const SizedBox\(width: KasySpacing\.sm\),\n[ \t]*Expanded\(\n[ \t]*child: SocialAuthButton\(\n[ \t]*label: t\.auth\.signin\.facebook,[\s\S]*?\.signinWithFacebook\(\),\n[ \t]*\),\n[ \t]*\),\n[ \t]*\],/,
1391
+ '',
1392
+ );
1393
+ // …then drop the now-unused `showFacebook` declaration (+ its comment), so the
1394
+ // stripped page has no dangling variable / analyzer warning.
1395
+ content = content.replace(
1396
+ /\n[ \t]*\/\/ Facebook on web works[\s\S]*?\n[ \t]*const bool showFacebook = [^;]*;/,
1391
1397
  '',
1392
1398
  );
1393
1399
  await fs.writeFile(p, content, 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.37.1",
3
+ "version": "1.39.0",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -1,12 +1,10 @@
1
1
  stripe_success.html,1781195415353,677b2fb430839866f32a33a751aae85b4c4819123be820f3f5b2f689b8eed00e
2
2
  manifest.json,1779810782774,1b929ac7267ff738d05646106a0fac3591082b6f365051928a62518af30b3fd5
3
3
  local_notifications.js,1772398909034,0b44b177c4b8a705ad1b0e3d7532537aad7b230e9472f93c820742b7548797b8
4
- index.html,1781388256310,bb8142eb84e9e44049957c3edf86006e1ae5f194c397a8326ce505b68fdf58f4
5
- flutter_bootstrap.js,1781388256056,25f49550b20f234b84194d9e067725abdc33590343251086f6c7d147325dfb41
6
4
  flutter.js,1777393608000,b42e549a02f5c1428b0ad85dbdc663d400a6d2db10cb8aefcc0fddd592fd6ceb
7
5
  firebase-messaging-sw.js,1772484605561,dba3c939f7c240e62f63d06408d5f70e45f1168d00df98726e6a7a20cbd3b0df
8
6
  favicon.png,1779810782545,c967afd3c3d4dd165ef2e5cf3f203c3982dfff6a95881859ed163d9c38d75512
9
- .last_build_id,1781388166126,fb2a0536078d7023bcc9d41eb14cc00d74532f9465fac7aa9b88dae42e4a249c
7
+ .last_build_id,1781475668120,4a8e13af757aaae5b1160a761d56555ef4370ca7572732cbfa8d98c989424ab0
10
8
  splash/img/light-4x.png,1780546027751,abf0e55345e532a4645fe14b01c2948b07c78df50cc6da23648837dfb6271c74
11
9
  splash/img/light-3x.png,1780546027699,5f02c94032cb7053807ce89603b109f14ea2740eaa3145dc76c924cbe1d534a9
12
10
  splash/img/light-2x.png,1780546027647,3c073b9da154ac8be530f8beefa3579246f223acd819e856a2db000fdabcb67e
@@ -53,23 +51,25 @@ assets/assets/icons/google_play_games.png,1772398653083,533c1d87d7be8690ab173ef4
53
51
  assets/assets/icons/google.png,1772398653083,f423e7e7be1e06008d45617d07f095f04da7fdcab9a56523f9e0633828e464e0
54
52
  assets/assets/icons/facebook.png,1772398653083,79ac67e449c1db63319d43329ca91f682b4e0bc6a0883b0dfb2a849e13ae6eb6
55
53
  assets/assets/icons/apple.png,1772398653083,2a737d8801ca81452b2978bb2e1ac72136acf1a4c338f2de2f77b65e9f6de1fa
56
- version.json,1781388383201,55f3aab1f3d71610bf791902e59e92cd77db8963f8290193f573c4172fa6fd32
57
- flutter_service_worker.js,1781388386040,baeeaf9f4b8e6f40d3b0549429ceb70ca01f120db6a7ef2f60d5809233dc2205
58
- assets/AssetManifest.bin,1781388383326,78bccb08a36307a400711a1d7e6868bd5f89a24e63439fb5b6747660323e4475
59
- assets/AssetManifest.bin.json,1781388383326,50a11b51c3fc4cbed1b5ec3a4f466c6a6b75c481d08d4782c8191f99bdbdba9c
60
- assets/FontManifest.json,1781388383326,4d84ab517c27984d36f9a3c8be6f2a72788c0c3985c1d5874297fef0a53407ca
61
- assets/shaders/ink_sparkle.frag,1781388383423,1c8e222328206d1e06754f76fb53947aad38d62180aafad5298a3c6f510b173d
62
- assets/shaders/stretch_effect.frag,1781388383423,1a7d4ac2be40cf0a459dfb390ef08bcd740f37913ffdee8de3c2ea836a18410e
63
- assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1781388385468,12bc6bf55aad4657f62747b6c1c9b5c120a594ed3540db21729b6db3c847340d
64
- assets/fonts/MaterialIcons-Regular.otf,1781388385471,89da9939a222284d9e14e9757b4dbdb75965434cb84b0f2969495153f3a018c9
65
- main.dart.js_3.part.js,1781388360492,6f52d11c8e140736cd046ce6b541ad8d8bf20e6ef709f39d8ef4eb3dddbece8b
66
- main.dart.js_1.part.js,1781388360481,42136799a40b5acb14b5acf0a8cef39bbef13b4dc99ce808a7bc8a1f8cdd8b09
67
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w300.ttf,1781388385457,aebf5e4f6b1e6de1e61a866f813aa5b08efb4cdd108cc2831d40aad71398c88f
68
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w400.ttf,1781388385463,edcd01ee13f6ec2285fd18863c0ce0efb0876b290fb427ad5c052f753b3210c8
69
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w600.ttf,1781388385468,008f1e974c9ff4e8d40d16221226e0d34d2298a7feb2bdea8c134a9333929c9c
70
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w100.ttf,1781388385446,c16d95412fe2600bda1f53c2b8dc4bd82496f60f7c5a6ecfa180b0f866128712
71
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w500.ttf,1781388385464,3f68f958d0fa13077968a6767339c38ede8705bcadd0382078db6ec4bac96fa0
72
- assets/packages/lucide_icons_flutter/assets/lucide.ttf,1781388385448,9b000e47cddf9ada4a6b85fc9577fef7ed2ee261c9279e9a44e4753da28f83d3
73
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w200.ttf,1781388385451,1f798186cfc3e2fcd54b15aa7b8695bc7c29b781c5058029a8d80ecdfa615904
74
- assets/NOTICES,1781388383326,a18efc42c5ed99b56481537d0f229ddd3add671c548a893aaf8766f30c854158
75
- main.dart.js,1781388361400,b625f1b346dc2f1475ee99ebb1d698654e74183d737c91e1bbac020869096a84
54
+ version.json,1781481118464,be3307736a7e600c6e90593352859f1c46bc97666d5cf21a16099effd2a0fed6
55
+ flutter_service_worker.js,1781481120062,baeeaf9f4b8e6f40d3b0549429ceb70ca01f120db6a7ef2f60d5809233dc2205
56
+ assets/FontManifest.json,1781481118640,4d84ab517c27984d36f9a3c8be6f2a72788c0c3985c1d5874297fef0a53407ca
57
+ flutter_bootstrap.js,1781481074600,0955c783148666dcda1406de86f477f1f27ec80a4fc8186ba6cd9bd119d242f9
58
+ assets/AssetManifest.bin,1781481118640,78bccb08a36307a400711a1d7e6868bd5f89a24e63439fb5b6747660323e4475
59
+ index.html,1781481074612,bb8142eb84e9e44049957c3edf86006e1ae5f194c397a8326ce505b68fdf58f4
60
+ assets/AssetManifest.bin.json,1781481118640,50a11b51c3fc4cbed1b5ec3a4f466c6a6b75c481d08d4782c8191f99bdbdba9c
61
+ assets/shaders/stretch_effect.frag,1781481118746,1a7d4ac2be40cf0a459dfb390ef08bcd740f37913ffdee8de3c2ea836a18410e
62
+ assets/shaders/ink_sparkle.frag,1781481118746,1c8e222328206d1e06754f76fb53947aad38d62180aafad5298a3c6f510b173d
63
+ assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1781481119831,12bc6bf55aad4657f62747b6c1c9b5c120a594ed3540db21729b6db3c847340d
64
+ assets/fonts/MaterialIcons-Regular.otf,1781481119828,0d07767255068e38fcf81755fd997bb15d94d6ef83bb81bd0149f974bfe359a0
65
+ main.dart.js_3.part.js,1781481106966,7dd538a2118493eb8ff1a33ec5c2dd6d355a9a61290f1d8d1e6ad7bf8cc0465f
66
+ main.dart.js_1.part.js,1781481106961,a9d49c1a6aff2181d2d352ebe6a64a58c776ece7784ca86546b17be94ffe42b0
67
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w100.ttf,1781481119830,c16d95412fe2600bda1f53c2b8dc4bd82496f60f7c5a6ecfa180b0f866128712
68
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w400.ttf,1781481119832,edcd01ee13f6ec2285fd18863c0ce0efb0876b290fb427ad5c052f753b3210c8
69
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w600.ttf,1781481119834,008f1e974c9ff4e8d40d16221226e0d34d2298a7feb2bdea8c134a9333929c9c
70
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w500.ttf,1781481119830,3f68f958d0fa13077968a6767339c38ede8705bcadd0382078db6ec4bac96fa0
71
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w300.ttf,1781481119832,aebf5e4f6b1e6de1e61a866f813aa5b08efb4cdd108cc2831d40aad71398c88f
72
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w200.ttf,1781481119830,1f798186cfc3e2fcd54b15aa7b8695bc7c29b781c5058029a8d80ecdfa615904
73
+ assets/NOTICES,1781481118640,a18efc42c5ed99b56481537d0f229ddd3add671c548a893aaf8766f30c854158
74
+ assets/packages/lucide_icons_flutter/assets/lucide.ttf,1781481119830,9b000e47cddf9ada4a6b85fc9577fef7ed2ee261c9279e9a44e4753da28f83d3
75
+ main.dart.js,1781481107454,c98fad1aa11ff6a9335379794ac025a501060a6759943e74365f185cc7a5e8ce
@@ -67,6 +67,11 @@ Typography collapses `large`+`xlarge` into one "desktop" tier (same sizes); the
67
67
  renders ~10% large, so **desktop only** scales the whole UI to `0.95`. Tablet
68
68
  and mobile web render at natural size, like native. This is NOT the typography
69
69
  scale — don't conflate the two.
70
+ - **Screen width follows content type** (two tiers): read/form screens (Settings,
71
+ Notifications) are contained + centred at `kKasyContentMaxWidth` (600) — set
72
+ `KasyOverlayScaffold(maxContentWidth: kKasyContentMaxWidth)` or use `KasyScreen`;
73
+ feed/dashboard screens (Home) go full width + the 16 gutter. Mirror
74
+ Settings/Notifications, or Home. Full rule: **`DESIGN_SYSTEM.md`** → Internal-screen contract.
70
75
 
71
76
  To re-tune sizes, edit `KasyTypeScale` in `lib/core/theme/type_scale.dart` —
72
77
  nothing else. The live ramp (tabs per breakpoint) is under **Design System →
@@ -99,7 +104,8 @@ lib/
99
104
 
100
105
  1. Read `DESIGN_SYSTEM.md` (tokens, typography roles, components).
101
106
  2. Reuse a kit component before building one. Wrap new pages in `KasyScreen`
102
- (or mirror an existing page's scaffold).
107
+ (or mirror an existing page's scaffold). Pick the width tier — contained
108
+ (mirror Settings/Notifications) vs full-width (mirror Home); see `DESIGN_SYSTEM.md`.
103
109
  3. Use semantic typography roles (`pageTitle`, `sectionTitle`, `rowTitle`, …)
104
110
  and tokens for **every** size, colour and spacing.
105
111
  4. Strings via `context.t.*` (add keys to the i18n JSON, never inline text).
@@ -103,11 +103,23 @@ whole app, edit the numbers in `KasyTypeScale` — nothing else. The live ramp i
103
103
  visible under **Design System → Typography** (tabs per breakpoint).
104
104
 
105
105
  **Web render scale (`WebViewportScale`, `lib/core/web_viewport_scale.dart`).** A
106
- separate concern from type sizes: Flutter web renders ~10% larger than an
107
- equivalent HTML app, so on **desktop only** (`>= 1024`) the whole UI is scaled to
108
- `0.95` a true viewport scale, so layout, scrolling and hit-testing stay
109
- correct. Tablet and mobile web render at natural size, exactly like the native
110
- build, which never scales.
106
+ separate concern from type sizes, sitting in a layer ON TOP of the responsive
107
+ system it does NOT replace breakpoints, the type ramp or spacing (turn it off
108
+ and the app is still fully responsive, just larger). Flutter web renders ~10%
109
+ larger than an equivalent native/HTML app at 100% zoom, **at every width**. So the
110
+ whole web app — phone, tablet and desktop — is scaled to `0.95` (~5%; a true
111
+ viewport scale, so layout, scrolling and hit-testing stay correct), making web
112
+ read close to the native baseline with **no size jump across breakpoints**. `0.90`
113
+ fully undoes the 10% but reads small on a monitor (native sizes are tuned for a
114
+ phone held close), so `0.95` (the midpoint) is the calibrated default. Two knobs:
115
+ `kWebViewportScaleEnabled` (master on/off — `false` makes web render at native
116
+ `1.0`) and `kWebViewportScale` (the value). Two things are never scaled, both at
117
+ native `1.0`: **native** itself (gated on `kIsWeb`, so iOS/Android/macOS/Windows
118
+ respect the system text-size for accessibility) and the **in-app device preview**
119
+ (it simulates a native device, so the scale is skipped while it is on — a
120
+ developer previewing a phone sees the native size). On desktop a high-OS-scale
121
+ screen compensates below the cap, keyed off the screen width (see
122
+ `kWebViewportScaleTargetWidth`).
111
123
 
112
124
  ---
113
125
 
@@ -139,7 +151,7 @@ Never pass a numeric `size:` to `Icon`; use a token so re-scaling is one edit.
139
151
  ## Radius — `KasyRadius` (`lib/core/theme/radius.dart`)
140
152
 
141
153
  Semantic: `xs 4 · sm 8 · md 12 · lg 16 · xl 24 · full 999`.
142
- `KasyCard` defaults to `lg` (16).
154
+ `KasyCard` defaults to `xl` (24).
143
155
 
144
156
  ---
145
157
 
@@ -150,7 +162,9 @@ Key tokens: `primary`, `onPrimary`, `surface`, `onSurface`, `onBackground`,
150
162
  `muted`, `fieldLabel`, `outline`, `error` / `onError`, `success`, `warning`,
151
163
  `surfacePrimarySoft`, `surfaceNeutralSoft`, `surfaceErrorSoft`.
152
164
 
153
- Brand: avocado green `#D2F51E` (accent), deep green `#011820` (dark ground).
165
+ Accent: HeroUI blue `#0485F7` (white foreground) the kit's default theme
166
+ accent. The Kasy brand identity (logo, marketing) uses avocado green `#D2F51E`
167
+ and deep green `#011820`; rebranding is a one-place token swap in `KasyColors`.
154
168
  Disabled state: blend the colour toward the surface (opaque), never raw opacity.
155
169
 
156
170
  ---
@@ -193,7 +207,7 @@ a mouse click never doubles the keyboard focus ring and the border never drops
193
207
  while the calendar is open.
194
208
 
195
209
  ### `KasyCard`
196
- Elevated surface panel, radius `KasyRadius.lg` (16). The base for screen content.
210
+ Elevated surface panel, radius `KasyRadius.xl` (24). The base for screen content.
197
211
 
198
212
  ### `KasyScreen`
199
213
  Opt-in screen scaffold. A new page wrapped in it gets the internal-screen
@@ -222,6 +236,19 @@ New internal screens follow the sidebar / Home ruler:
222
236
  - Haptics only on native (`if (!kIsWeb)`); "hide chrome on scroll" only on the
223
237
  phone breakpoint (`width < 768`).
224
238
 
239
+ **Content width — two tiers (width follows content type):**
240
+ - *Read / form screens* (Settings, Notifications, Reminder) → **contained + centred**
241
+ at `kKasyContentMaxWidth` (600, in `core/widgets/responsive_layout.dart`). With the
242
+ overlay pattern, set `KasyOverlayScaffold(maxContentWidth: kKasyContentMaxWidth)`
243
+ (centres the column on desktop, keeps the 16 gutter on mobile); with `KasyScreen`
244
+ it is the default. Long lines and lone controls read badly stretched, so cap them.
245
+ The Settings desktop master/detail centres the **nav + detail as one unit**.
246
+ - *Browse / feed / dashboard screens* (Home) → **full width + the 16 gutter, on
247
+ purpose** — width helps a grid/feed. Do **not** contain these.
248
+ - Every tier shares the same family (16 gutter, `KasyCard`, sidebar + web header) —
249
+ only the max width changes. To mirror: copy **Settings / Notifications** for a
250
+ contained screen, **Home** for a full-width one.
251
+
225
252
  ---
226
253
 
227
254
  ## Keeping it consistent — the guard-rail (optional)
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="814" height="1000">
2
+ <path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/>
3
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="842.32007" height="1000.0001">
3
+ <path fill="#fff" d="M824.66636 779.30363c-15.12299 34.93724-33.02368 67.09674-53.7638 96.66374-28.27076 40.3074-51.4182 68.2078-69.25717 83.7012-27.65347 25.4313-57.2822 38.4556-89.00964 39.1963-22.77708 0-50.24539-6.4813-82.21973-19.629-32.07926-13.0861-61.55985-19.5673-88.51583-19.5673-28.27075 0-58.59083 6.4812-91.02193 19.5673-32.48053 13.1477-58.64639 19.9994-78.65196 20.6784-30.42501 1.29623-60.75123-12.0985-91.02193-40.2457-19.32039-16.8514-43.48632-45.7394-72.43607-86.6641-31.060778-43.7024-56.597041-94.37983-76.602609-152.15586C10.740416 658.44309 0 598.01283 0 539.50845c0-67.01648 14.481044-124.8172 43.486336-173.25401C66.28194 327.34823 96.60818 296.6578 134.5638 274.1276c37.95566-22.53016 78.96676-34.01129 123.1321-34.74585 24.16591 0 55.85633 7.47508 95.23784 22.166 39.27042 14.74029 64.48571 22.21538 75.54091 22.21538 8.26518 0 36.27668-8.7405 83.7629-26.16587 44.90607-16.16001 82.80614-22.85118 113.85458-20.21546 84.13326 6.78992 147.34122 39.95559 189.37699 99.70686-75.24463 45.59122-112.46573 109.4473-111.72502 191.36456.67899 63.8067 23.82643 116.90384 69.31888 159.06309 20.61664 19.56727 43.64066 34.69027 69.2571 45.4307-5.55531 16.11062-11.41933 31.54225-17.65372 46.35662zM631.70926 20.0057c0 50.01141-18.27108 96.70693-54.6897 139.92782-43.94932 51.38118-97.10817 81.07162-154.75459 76.38659-.73454-5.99983-1.16045-12.31444-1.16045-18.95003 0-48.01091 20.9006-99.39207 58.01678-141.40314 18.53027-21.27094 42.09746-38.95744 70.67685-53.0663C578.3158 9.00229 605.2903 1.31621 630.65988 0c.74076 6.68575 1.04938 13.37191 1.04938 20.00505z"/>
4
+ </svg>