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
@@ -199,13 +199,26 @@ class _MyAppState extends ConsumerState<MyApp> {
199
199
  facebookEventApiProvider,
200
200
  ],
201
201
  onReady: FocusVisibility(
202
- child: WebViewportScale.wrap(
203
- DevicePreview.appBuilder(
204
- context,
205
- ResponsiveTextTheme(
206
- child: child ?? const SizedBox.shrink(),
207
- ),
208
- ),
202
+ child: ValueListenableBuilder<bool>(
203
+ valueListenable: webDevicePreviewActiveNotifier,
204
+ builder: (context, devicePreviewActive, _) {
205
+ final Widget app = DevicePreview.appBuilder(
206
+ context,
207
+ ResponsiveTextTheme(
208
+ child: child ?? const SizedBox.shrink(),
209
+ ),
210
+ );
211
+ // The web render-scale correction applies to the REAL
212
+ // web app on every breakpoint. Once the device preview
213
+ // frame is actually on screen it simulates a NATIVE
214
+ // device, so it renders at native 1.0 — skip the scale.
215
+ // Gated on the *active* (frame-built) notifier, not the
216
+ // raw toggle, so the scale only drops when the frame is
217
+ // up (no flash of big unframed app while it builds).
218
+ return devicePreviewActive
219
+ ? app
220
+ : WebViewportScale.wrap(app);
221
+ },
209
222
  ),
210
223
  ),
211
224
  onError: (_, error) => InitializationErrorPage(error: error),
@@ -26,9 +26,7 @@ import 'package:kasy_kit/features/local_reminders/ui/reminder_page.dart';
26
26
  import 'package:kasy_kit/features/onboarding/ui/onboarding_page.dart';
27
27
  import 'package:kasy_kit/features/settings/ui/components/admin/admin_home_widgets.dart';
28
28
  import 'package:kasy_kit/features/settings/ui/components/admin/admin_page.dart';
29
- import 'package:kasy_kit/features/settings/ui/components/admin/admin_paywalls.dart';
30
29
  import 'package:kasy_kit/features/settings/ui/components/admin/admin_routes.dart';
31
- import 'package:kasy_kit/features/settings/ui/components/admin/send_push_notification_page.dart';
32
30
  import 'package:kasy_kit/features/subscriptions/ui/component/premium_page_factory.dart';
33
31
  import 'package:kasy_kit/features/subscriptions/ui/premium_page.dart';
34
32
  import 'package:logger/logger.dart';
@@ -71,6 +69,16 @@ String? _authRedirect(Ref ref, GoRouterState state) {
71
69
  // redirect yet — we re-run once the state resolves (refresh listenable).
72
70
  if (user.isLoading) return null;
73
71
 
72
+ // Admin area is role-gated at the ROUTING layer (URLs included), not just by
73
+ // hiding UI: in production, /admin and every /admin/* section open only for an
74
+ // authenticated admin (role == "admin"). Anyone else — including someone who
75
+ // types the URL directly — is bounced home. Open in debug for development.
76
+ // This sits on top of the server-side data rules that ultimately protect the
77
+ // data; here it guarantees the screens themselves are never even reachable.
78
+ if (state.uri.path.startsWith('/admin') && !kDebugMode && !user.isAdmin) {
79
+ return '/';
80
+ }
81
+
74
82
  final String loc = state.matchedLocation;
75
83
  final bool onAuthRoute = _authRoutes.contains(loc);
76
84
  final bool onboardingDone = ref
@@ -212,7 +220,11 @@ GoRouter generateRouter({
212
220
  path: '/onboarding',
213
221
  pageBuilder: (context, state) => kasyTransitionPage(
214
222
  key: state.pageKey,
215
- child: const OnboardingPage(),
223
+ // ?preview=true opens the flow as a side-effect-free walkthrough from
224
+ // the admin Debug screen (no account creation, returns to Debug).
225
+ child: OnboardingPage(
226
+ preview: state.uri.queryParameters['preview'] == 'true',
227
+ ),
216
228
  ),
217
229
  ),
218
230
  GoRoute(
@@ -296,25 +308,65 @@ GoRouter generateRouter({
296
308
  child: const RecoverPasswordPage(),
297
309
  ),
298
310
  ),
299
- // Admin console: registered always so administrators reach it in release
300
- // builds too. Access is gated inside AdminPage (admin role || debug).
301
- GoRoute(
302
- name: 'admin',
303
- path: '/admin',
304
- pageBuilder: (context, state) => kasyTransitionPage(
311
+ // Admin console: a StatefulShellRoute so every section is a real,
312
+ // URL-addressable screen that keeps its own state while the navigation
313
+ // rail persists across them. The top-level sections (/admin, /admin/users,
314
+ // /admin/requests) and the four "Ferramentas" sub-screens (/admin/tools/*)
315
+ // are ALL branches — each its own URL, reached from the sidebar. Registered
316
+ // always (admins reach it in release too); the redirect above blocks
317
+ // /admin* for non-admins in production. adminSections() is the single
318
+ // source the sidebar reads too, so branches and nav rows stay aligned.
319
+ StatefulShellRoute.indexedStack(
320
+ // Enter /admin with the app's standard page transition
321
+ // (KasyNavigationConfig.push), same as every other route — pageBuilder
322
+ // (not builder), otherwise go_router falls back to its default platform
323
+ // transition. Switching sections is instant (IndexedStack), like tabs.
324
+ pageBuilder: (context, state, navigationShell) => kasyTransitionPage(
305
325
  key: state.pageKey,
306
- child: const AdminPage(),
326
+ child: AdminShell(navigationShell: navigationShell),
307
327
  ),
328
+ branches: [
329
+ for (final section in adminSections())
330
+ StatefulShellBranch(
331
+ routes: [
332
+ GoRoute(
333
+ path: section.path,
334
+ pageBuilder: (context, state) => kasyTransitionPage(
335
+ key: state.pageKey,
336
+ child: section.build(),
337
+ ),
338
+ ),
339
+ ],
340
+ ),
341
+ ],
308
342
  ),
309
- if (kDebugMode) ...[
310
- GoRoute(
311
- name: 'admin_paywalls',
312
- path: adminRoutePaywalls,
313
- pageBuilder: (context, state) => kasyTransitionPage(
343
+ // Drill-downs pushed full-screen from inside the console (their own back
344
+ // button) — the redirect above keeps /admin* admin-only.
345
+ //
346
+ // Paywall variant preview is pushed from the Paywalls section, which ships
347
+ // in production, so it's registered always.
348
+ GoRoute(
349
+ name: 'admin_premium_preview',
350
+ path: '/admin/premium/:variant',
351
+ pageBuilder: (context, state) {
352
+ final paywall = paywallFactoryFromAdminRoute(
353
+ state.pathParameters['variant'],
354
+ );
355
+ if (paywall == null || !withRevenuecat) {
356
+ return kasyTransitionPage(
357
+ key: state.pageKey,
358
+ child: const PageNotFound(),
359
+ );
360
+ }
361
+ return kasyTransitionPage(
314
362
  key: state.pageKey,
315
- child: const AdminPaywalls(),
316
- ),
317
- ),
363
+ child: PremiumPage(paywall: paywall),
364
+ );
365
+ },
366
+ ),
367
+ // Home-widgets panel: developer-only, pushed from the Debug section's
368
+ // (debug-gated) tile — its route registers only in kDebugMode.
369
+ if (kDebugMode)
318
370
  GoRoute(
319
371
  name: 'admin_home_widgets',
320
372
  path: adminRouteHomeWidgets,
@@ -323,34 +375,6 @@ GoRouter generateRouter({
323
375
  child: const AdminHomeWidgets(),
324
376
  ),
325
377
  ),
326
- GoRoute(
327
- name: 'admin_premium_preview',
328
- path: '/admin/premium/:variant',
329
- pageBuilder: (context, state) {
330
- final paywall = paywallFactoryFromAdminRoute(
331
- state.pathParameters['variant'],
332
- );
333
- if (paywall == null || !withRevenuecat) {
334
- return kasyTransitionPage(
335
- key: state.pageKey,
336
- child: const PageNotFound(),
337
- );
338
- }
339
- return kasyTransitionPage(
340
- key: state.pageKey,
341
- child: PremiumPage(paywall: paywall),
342
- );
343
- },
344
- ),
345
- ],
346
- GoRoute(
347
- name: 'send_push',
348
- path: adminRouteSendPush,
349
- pageBuilder: (context, state) => kasyTransitionPage(
350
- key: state.pageKey,
351
- child: const SendPushNotificationPage(),
352
- ),
353
- ),
354
378
  GoRoute(
355
379
  name: '404',
356
380
  path: '/404',
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
16
16
  # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17
17
  # In Windows, build-name is used as the major, minor, and patch parts
18
18
  # of the product and file versions while build-number is used as the build suffix.
19
- version: 1.0.0+58
19
+ version: 1.0.0+66
20
20
 
21
21
  environment:
22
22
  sdk: ^3.11.0
@@ -59,6 +59,7 @@ dependencies:
59
59
  flutter_native_splash: ^2.4.7
60
60
  flutter_riverpod: ^3.1.0
61
61
  flutter_secure_storage: ^9.2.4
62
+ flutter_svg: ^2.3.0
62
63
  flutter_timezone: ^5.0.1
63
64
  freezed_annotation: ^3.1.0
64
65
  go_router: ^17.1.0
@@ -0,0 +1,110 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter_test/flutter_test.dart';
3
+ import 'package:go_router/go_router.dart';
4
+ import 'package:kasy_kit/components/kasy_app_bar.dart';
5
+ import 'package:kasy_kit/components/kasy_sidebar.dart';
6
+ import 'package:kasy_kit/core/data/models/user.dart';
7
+ import 'package:kasy_kit/core/states/models/user_state.dart';
8
+ import 'package:kasy_kit/features/settings/ui/components/admin/admin_page.dart';
9
+ import 'package:kasy_kit/features/settings/ui/components/admin/admin_routes.dart';
10
+ import 'package:kasy_kit/i18n/translations.g.dart';
11
+
12
+ import 'test_utils.dart';
13
+
14
+ /// Mirrors the real admin shell: every section (top-level AND the "Ferramentas"
15
+ /// sub-screens) is its own branch, so they all share the same chrome and a
16
+ /// persistent rail. Branch order/paths match [adminSections] so the shell's
17
+ /// currentIndex lines up; the branch bodies are placeholders so the test stays
18
+ /// focused on the chrome (sidebar + header), not on each page's providers.
19
+ GoRouter _adminTestRouter(String initial) => GoRouter(
20
+ initialLocation: initial,
21
+ routes: [
22
+ GoRoute(path: '/', builder: (_, _) => const SizedBox()),
23
+ StatefulShellRoute.indexedStack(
24
+ builder: (c, s, shell) => AdminShell(navigationShell: shell),
25
+ branches: [
26
+ for (final section in adminSections())
27
+ StatefulShellBranch(
28
+ routes: [
29
+ GoRoute(
30
+ path: section.path,
31
+ builder: (_, _) =>
32
+ Center(child: Text('section-${section.id.name}')),
33
+ ),
34
+ ],
35
+ ),
36
+ ],
37
+ ),
38
+ ],
39
+ );
40
+
41
+ UserState _adminUser() => UserState(
42
+ user: User.authenticated(
43
+ id: '1',
44
+ email: 'admin@email.com',
45
+ name: 'Admin',
46
+ onboarded: true,
47
+ role: 'admin',
48
+ creationDate: DateTime.now(),
49
+ ),
50
+ );
51
+
52
+ /// The desktop application bar is a [KasyAppBar.application] — same class as the
53
+ /// page bar, told apart by [KasyAppBar.isApplication].
54
+ final Finder _applicationBar = find.byWidgetPredicate(
55
+ (Widget w) => w is KasyAppBar && w.isApplication,
56
+ );
57
+
58
+ void main() {
59
+ testWidgets(
60
+ 'admin section (desktop) renders the real KasySidebar + application bar',
61
+ (tester) async {
62
+ tester.view.physicalSize = const Size(1400, 900);
63
+ tester.view.devicePixelRatio = 1.0;
64
+ addTearDown(tester.view.resetPhysicalSize);
65
+ addTearDown(tester.view.resetDevicePixelRatio);
66
+
67
+ await tester.pumpPage(
68
+ userState: _adminUser(),
69
+ routerConfig: _adminTestRouter('/admin'),
70
+ );
71
+ await tester.pump(const Duration(milliseconds: 300));
72
+
73
+ // The console uses the app's real chrome components, not a bespoke copy.
74
+ expect(find.byType(KasySidebar), findsOneWidget);
75
+ expect(_applicationBar, findsOneWidget);
76
+ expect(find.text('section-overview'), findsOneWidget);
77
+ },
78
+ );
79
+
80
+ testWidgets(
81
+ 'a Ferramentas sub-screen (desktop) is a real section: same rail + header, '
82
+ 'submenu open',
83
+ (tester) async {
84
+ tester.view.physicalSize = const Size(1400, 900);
85
+ tester.view.devicePixelRatio = 1.0;
86
+ addTearDown(tester.view.resetPhysicalSize);
87
+ addTearDown(tester.view.resetDevicePixelRatio);
88
+
89
+ await tester.pumpPage(
90
+ userState: _adminUser(),
91
+ routerConfig: _adminTestRouter(adminRouteSendPush),
92
+ );
93
+ await tester.pump(const Duration(milliseconds: 300));
94
+
95
+ // Tools sub-screens get the SAME chrome as every other section now (the
96
+ // whole point of this change): the persistent rail AND the application bar.
97
+ expect(find.byType(KasySidebar), findsOneWidget);
98
+ expect(_applicationBar, findsOneWidget);
99
+ expect(find.text('section-sendPush'), findsOneWidget);
100
+
101
+ // The rail shows the "Ferramentas" group, auto-expanded because one of its
102
+ // children (Send push) is the active screen.
103
+ expect(find.text(t.admin_console.tabs.tools), findsOneWidget);
104
+ expect(
105
+ find.text(t.home.features_page.send_push_title),
106
+ findsWidgets,
107
+ );
108
+ },
109
+ );
110
+ }
@@ -0,0 +1,77 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter_test/flutter_test.dart';
3
+ import 'package:kasy_kit/components/kasy_text_field.dart';
4
+ import 'package:kasy_kit/core/theme/colors.dart';
5
+ import 'package:kasy_kit/core/theme/providers/theme_provider.dart';
6
+ import 'package:kasy_kit/core/theme/responsive_text_theme.dart';
7
+ import 'package:kasy_kit/core/theme/texts.dart';
8
+ import 'package:kasy_kit/core/theme/universal_theme.dart';
9
+ import 'package:shared_preferences/shared_preferences.dart';
10
+
11
+ import '../device_test_utils.dart';
12
+
13
+ /// A single-line [KasyTextField] must render at exactly
14
+ /// [KasyTextField.singleLineHeight] on EVERY breakpoint. The height is locked
15
+ /// with a forced strut (see kasy_text_field.dart) precisely so it cannot drift
16
+ /// between renderers (web CanvasKit vs native) or viewport widths. Run this on
17
+ /// the VM and with `--platform chrome` to cover both renderers.
18
+ void main() {
19
+ Widget appWithField() {
20
+ return Builder(builder: (context) {
21
+ return MaterialApp(
22
+ theme: ThemeProvider.of(context).light,
23
+ home: const ResponsiveTextTheme(
24
+ child: Scaffold(
25
+ body: Center(
26
+ child: SizedBox(
27
+ width: 320,
28
+ child: KasyTextField(
29
+ hint: 'Email',
30
+ variant: KasyTextFieldVariant.flat,
31
+ ),
32
+ ),
33
+ ),
34
+ ),
35
+ ),
36
+ );
37
+ });
38
+ }
39
+
40
+ testWidgets('single-line field height stays locked across breakpoints',
41
+ (tester) async {
42
+ SharedPreferences.setMockInitialValues({});
43
+ final sharedPrefs = await SharedPreferences.getInstance();
44
+
45
+ final widget = ThemeProvider(
46
+ notifier: AppTheme.uniform(
47
+ sharedPreferences: sharedPrefs,
48
+ themeFactory: const UniversalThemeFactory(),
49
+ lightColors: KasyColors.light(),
50
+ darkColors: KasyColors.dark(),
51
+ textTheme: KasyTextTheme.build(),
52
+ defaultMode: ThemeMode.light,
53
+ ),
54
+ child: appWithField(),
55
+ );
56
+
57
+ const breakpoints = <Device>[
58
+ Device(name: 'mobile', width: 375, height: 812, pixelDensity: 3),
59
+ Device(name: 'tablet', width: 800, height: 1024, pixelDensity: 2),
60
+ Device(name: 'desktop', width: 1400, height: 900, pixelDensity: 1),
61
+ ];
62
+
63
+ for (final device in breakpoints) {
64
+ await tester.setScreenSize(device);
65
+ await tester.pumpWidget(widget);
66
+ await tester.pumpAndSettle();
67
+ final double boxHeight =
68
+ tester.getSize(find.byType(TextField).first).height;
69
+ expect(
70
+ boxHeight,
71
+ KasyTextField.singleLineHeight,
72
+ reason: 'field box should be ${KasyTextField.singleLineHeight}px on '
73
+ '${device.name} (${device.width}px wide), got $boxHeight',
74
+ );
75
+ }
76
+ });
77
+ }
@@ -1,28 +1,34 @@
1
1
  import 'package:flutter_test/flutter_test.dart';
2
2
  import 'package:kasy_kit/core/web_viewport_scale.dart';
3
3
 
4
- /// Locks the desktop web scaling behaviour so a future change to the constants or
5
- /// the formula can't silently regress it. The headline rule, and the bug this
6
- /// guards against: the desktop compensation must key off the SCREEN width (OS
7
- /// scale), NOT the window width merely resizing the browser window must not
8
- /// shrink the UI; the extra shrink happens only when the screen itself is small.
4
+ /// Locks the web scaling behaviour so a future change to the constants or the
5
+ /// formula can't silently regress it. Two headline rules:
6
+ /// - the flat cap applies on EVERY web breakpoint (phone/tablet/desktop) so the
7
+ /// whole web app matches native, with no size jump at the desktop breakpoint;
8
+ /// - the desktop high-OS-scale compensation must key off the SCREEN width, NOT
9
+ /// the window width — merely resizing the browser window must not shrink the
10
+ /// UI further; the extra shrink happens only when the screen itself is small.
9
11
  void main() {
10
12
  group('webViewportEffectiveScale', () {
11
- test('no scaling below the desktop breakpoint (mobile/tablet web == native)',
13
+ test('flat cap applies on tablet/phone web too (no jump at the breakpoint)',
12
14
  () {
13
- // The window width decides the breakpoint (the layout follows the window),
14
- // so below it the scale is 1.0 regardless of the screen same as native
15
- // and the device preview.
16
- expect(webViewportEffectiveScale(375), 1.0); // phone
17
- expect(webViewportEffectiveScale(900), 1.0); // tablet
18
- expect(webViewportEffectiveScale(900, screenWidth: 1470), 1.0);
15
+ // Below the desktop breakpoint the scale is the flat cap (not 1.0): the
16
+ // same ~10% web oversize is corrected, and there is no size jump when the
17
+ // window crosses the desktop breakpoint. Compensation is desktop-only, so
18
+ // the screen width is ignored here.
19
+ expect(webViewportEffectiveScale(375), kWebViewportScale); // phone
20
+ expect(webViewportEffectiveScale(900), kWebViewportScale); // tablet
21
+ expect(webViewportEffectiveScale(900, screenWidth: 1470), kWebViewportScale);
22
+ // A small screen does NOT compensate below the breakpoint — that is a
23
+ // desktop-only concern; tablet/phone always take the flat cap.
24
+ expect(webViewportEffectiveScale(900, screenWidth: 300), kWebViewportScale);
19
25
  expect(
20
26
  webViewportEffectiveScale(kWebViewportScaleDesktopBreakpoint - 1),
21
- 1.0,
27
+ kWebViewportScale,
22
28
  );
23
29
  });
24
30
 
25
- test('desktop on a normal/large screen stays at the flat cap (0.95)', () {
31
+ test('desktop on a normal/large screen stays at the flat cap', () {
26
32
  // Unknown screen (null) → flat cap.
27
33
  expect(webViewportEffectiveScale(1280), kWebViewportScale);
28
34
  expect(webViewportEffectiveScale(1500), kWebViewportScale);
@@ -43,7 +49,7 @@ void main() {
43
49
  });
44
50
 
45
51
  test('high OS scale (small SCREEN) compensates below the cap', () {
46
- // Drop below 0.95 only because the screen is small — independent of window.
52
+ // Drop below the cap only because the screen is small — independent of window.
47
53
  expect(webViewportEffectiveScale(1024, screenWidth: 1024), closeTo(0.80, 1e-4));
48
54
  expect(webViewportEffectiveScale(1097, screenWidth: 1097), closeTo(0.857, 1e-3));
49
55
  // Even a wide window on a small (high-scale) screen compensates.
@@ -58,7 +64,8 @@ void main() {
58
64
  });
59
65
 
60
66
  test('the maxScale override is respected as the cap', () {
61
- expect(webViewportEffectiveScale(1920, screenWidth: 1920, maxScale: 0.9), 0.9);
67
+ // Use a non-default cap so this genuinely exercises the override.
68
+ expect(webViewportEffectiveScale(1920, screenWidth: 1920, maxScale: 0.8), 0.8);
62
69
  });
63
70
 
64
71
  test('an absurdly small screen is clamped to the 0.5 floor', () {
@@ -83,4 +83,7 @@ class FakeUserApi implements UserApi {
83
83
  Future<void> updateLocale(String userId, String locale) {
84
84
  return Future.value();
85
85
  }
86
+
87
+ @override
88
+ Stream<String?> watchRole(String id) => const Stream.empty();
86
89
  }
@@ -77,6 +77,15 @@ final List<_Rule> _rules = <_Rule>[
77
77
  RegExp(r'Color\(0x|(?<![A-Za-z])Colors\.(?!transparent\b)(?!white)(?!black)[A-Za-z]'),
78
78
  'Use a colour token (context.colors.*) instead of a raw Color/Colors value.',
79
79
  ),
80
+ _Rule(
81
+ 'hardcoded-radius',
82
+ // A numeric literal right after `circular(` — catches BorderRadius.circular(16),
83
+ // Radius.circular(8) and the only/vertical/horizontal forms (they nest a
84
+ // Radius.circular). `circular(KasyRadius.lg)` starts with a letter, so it
85
+ // passes; `circular(size / 2)` likewise (calibrated, not a token miss).
86
+ RegExp(r'\bcircular\(\s*\d'),
87
+ 'Use a KasyRadius.* token instead of a literal corner radius.',
88
+ ),
80
89
  ];
81
90
 
82
91
  // The icon rule needs two signals on the same line, so it is handled separately.