kasy-cli 1.38.0 → 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 (102) hide show
  1. package/lib/scaffold/CHANGELOG.json +14 -0
  2. package/lib/scaffold/backends/api/patch/README.md +15 -0
  3. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
  4. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
  5. package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
  6. package/lib/scaffold/backends/patch-base-hashes.json +6 -6
  7. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  8. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
  9. package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
  10. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
  11. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  12. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
  13. package/lib/scaffold/shared/generator-utils.js +12 -6
  14. package/package.json +1 -1
  15. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  16. package/templates/firebase/DESIGN_SYSTEM.md +22 -8
  17. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  18. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  19. package/templates/firebase/assets/icons/facebook.svg +49 -0
  20. package/templates/firebase/assets/icons/google.svg +1 -0
  21. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  22. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  23. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  24. package/templates/firebase/lib/components/components.dart +1 -1
  25. package/templates/firebase/lib/components/kasy_app_bar.dart +314 -14
  26. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  27. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  28. package/templates/firebase/lib/components/kasy_sidebar.dart +18 -6
  29. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  30. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  31. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +27 -18
  32. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -10
  33. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  34. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  35. package/templates/firebase/lib/core/data/api/user_api.dart +11 -0
  36. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  37. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +18 -35
  38. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  39. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  40. package/templates/firebase/lib/core/states/user_state_notifier.dart +28 -1
  41. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  42. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  43. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +51 -19
  44. package/templates/firebase/lib/core/web_viewport_scale.dart +64 -35
  45. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  46. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  47. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  48. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  49. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  50. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  51. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  52. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  53. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  54. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  55. package/templates/firebase/lib/features/home/home_components_page.dart +253 -125
  56. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +231 -57
  57. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  58. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  59. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  60. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +111 -57
  61. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  62. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -4
  63. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  64. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  65. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  66. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +2 -2
  67. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  68. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  69. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  70. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  71. package/templates/firebase/lib/features/settings/settings_page.dart +53 -32
  72. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +895 -111
  73. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  74. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  75. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +171 -41
  76. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +1 -1
  77. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  78. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +48 -47
  79. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  80. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  81. package/templates/firebase/lib/i18n/en.i18n.json +749 -712
  82. package/templates/firebase/lib/i18n/es.i18n.json +749 -712
  83. package/templates/firebase/lib/i18n/pt.i18n.json +749 -712
  84. package/templates/firebase/lib/main.dart +20 -7
  85. package/templates/firebase/lib/router.dart +32 -26
  86. package/templates/firebase/pubspec.yaml +2 -1
  87. package/templates/firebase/test/admin_shell_chrome_test.dart +11 -5
  88. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  89. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  90. package/templates/firebase/tool/design_check.dart +9 -0
  91. package/templates/firebase/assets/icons/apple.png +0 -0
  92. package/templates/firebase/assets/icons/facebook.png +0 -0
  93. package/templates/firebase/assets/icons/google.png +0 -0
  94. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  95. package/templates/firebase/lib/components/kasy_web_header.dart +0 -218
  96. package/templates/firebase/lib/core/chrome/web_header_scope.dart +0 -20
  97. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  98. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  99. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  100. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  101. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -179
  102. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
@@ -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),
@@ -220,7 +220,11 @@ GoRouter generateRouter({
220
220
  path: '/onboarding',
221
221
  pageBuilder: (context, state) => kasyTransitionPage(
222
222
  key: state.pageKey,
223
- 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
+ ),
224
228
  ),
225
229
  ),
226
230
  GoRoute(
@@ -311,8 +315,7 @@ GoRouter generateRouter({
311
315
  // are ALL branches — each its own URL, reached from the sidebar. Registered
312
316
  // always (admins reach it in release too); the redirect above blocks
313
317
  // /admin* for non-admins in production. adminSections() is the single
314
- // source the sidebar reads too, so branches and nav rows stay aligned (the
315
- // debug-only sections drop out of both together).
318
+ // source the sidebar reads too, so branches and nav rows stay aligned.
316
319
  StatefulShellRoute.indexedStack(
317
320
  // Enter /admin with the app's standard page transition
318
321
  // (KasyNavigationConfig.push), same as every other route — pageBuilder
@@ -338,8 +341,32 @@ GoRouter generateRouter({
338
341
  ],
339
342
  ),
340
343
  // Drill-downs pushed full-screen from inside the console (their own back
341
- // button), debug-only — the redirect above keeps /admin* admin-only.
342
- if (kDebugMode) ...[
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(
362
+ key: state.pageKey,
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)
343
370
  GoRoute(
344
371
  name: 'admin_home_widgets',
345
372
  path: adminRouteHomeWidgets,
@@ -348,27 +375,6 @@ GoRouter generateRouter({
348
375
  child: const AdminHomeWidgets(),
349
376
  ),
350
377
  ),
351
- // Paywall variant preview — pushed from the Paywalls section.
352
- GoRoute(
353
- name: 'admin_premium_preview',
354
- path: '/admin/premium/:variant',
355
- pageBuilder: (context, state) {
356
- final paywall = paywallFactoryFromAdminRoute(
357
- state.pathParameters['variant'],
358
- );
359
- if (paywall == null || !withRevenuecat) {
360
- return kasyTransitionPage(
361
- key: state.pageKey,
362
- child: const PageNotFound(),
363
- );
364
- }
365
- return kasyTransitionPage(
366
- key: state.pageKey,
367
- child: PremiumPage(paywall: paywall),
368
- );
369
- },
370
- ),
371
- ],
372
378
  GoRoute(
373
379
  name: '404',
374
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+60
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
@@ -1,8 +1,8 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:flutter_test/flutter_test.dart';
3
3
  import 'package:go_router/go_router.dart';
4
+ import 'package:kasy_kit/components/kasy_app_bar.dart';
4
5
  import 'package:kasy_kit/components/kasy_sidebar.dart';
5
- import 'package:kasy_kit/components/kasy_web_header.dart';
6
6
  import 'package:kasy_kit/core/data/models/user.dart';
7
7
  import 'package:kasy_kit/core/states/models/user_state.dart';
8
8
  import 'package:kasy_kit/features/settings/ui/components/admin/admin_page.dart';
@@ -49,9 +49,15 @@ UserState _adminUser() => UserState(
49
49
  ),
50
50
  );
51
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
+
52
58
  void main() {
53
59
  testWidgets(
54
- 'admin section (desktop) renders the real KasySidebar + KasyWebHeader',
60
+ 'admin section (desktop) renders the real KasySidebar + application bar',
55
61
  (tester) async {
56
62
  tester.view.physicalSize = const Size(1400, 900);
57
63
  tester.view.devicePixelRatio = 1.0;
@@ -66,7 +72,7 @@ void main() {
66
72
 
67
73
  // The console uses the app's real chrome components, not a bespoke copy.
68
74
  expect(find.byType(KasySidebar), findsOneWidget);
69
- expect(find.byType(KasyWebHeader), findsOneWidget);
75
+ expect(_applicationBar, findsOneWidget);
70
76
  expect(find.text('section-overview'), findsOneWidget);
71
77
  },
72
78
  );
@@ -87,9 +93,9 @@ void main() {
87
93
  await tester.pump(const Duration(milliseconds: 300));
88
94
 
89
95
  // Tools sub-screens get the SAME chrome as every other section now (the
90
- // whole point of this change): the persistent rail AND the web header.
96
+ // whole point of this change): the persistent rail AND the application bar.
91
97
  expect(find.byType(KasySidebar), findsOneWidget);
92
- expect(find.byType(KasyWebHeader), findsOneWidget);
98
+ expect(_applicationBar, findsOneWidget);
93
99
  expect(find.text('section-sendPush'), findsOneWidget);
94
100
 
95
101
  // The rail shows the "Ferramentas" group, auto-expanded because one of its
@@ -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', () {
@@ -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.
@@ -1,218 +0,0 @@
1
- /// Application top header for the **web desktop breakpoint** (viewport ≥ 1024px).
2
- ///
3
- /// This is the *application* chrome (global search, quick-create, notifications,
4
- /// profile) — distinct from [KasyAppBar], which is *page* chrome (title / back /
5
- /// theme) used on phone and tablet. On desktop the sidebar handles navigation,
6
- /// so this header carries global actions instead of a title or back button.
7
- ///
8
- /// Composed entirely from existing kit widgets: a [KasyTextField] search box
9
- /// (fixed 220px), a ghost [KasyButton.iconOnly] for notifications, a neutral
10
- /// pill [KasyButton] for create, and a gradient [KasyAvatar].
11
- ///
12
- /// Barrel: [components.dart].
13
- library;
14
-
15
- import 'package:flutter/material.dart';
16
- import 'package:kasy_kit/components/kasy_avatar.dart';
17
- import 'package:kasy_kit/components/kasy_avatar_presets.dart';
18
- import 'package:kasy_kit/components/kasy_button.dart';
19
- import 'package:kasy_kit/components/kasy_text_field.dart';
20
- import 'package:kasy_kit/core/theme/theme.dart';
21
-
22
- /// Total header height (matches the design: 36px content band + 16px top/bottom).
23
- const double kasyWebHeaderHeight = 68;
24
-
25
- /// Fixed width of the leading search field (matches the design).
26
- const double kasyWebHeaderSearchWidth = 220;
27
-
28
- /// Desktop application header. Place it at the top of the content area (to the
29
- /// right of the sidebar) on viewports ≥ 1024px.
30
- class KasyWebHeader extends StatelessWidget {
31
- /// Controller for the search field. Optional — omit for a display-only header.
32
- final TextEditingController? searchController;
33
-
34
- /// Placeholder shown in the search field.
35
- final String searchHint;
36
-
37
- /// Called as the user types in the search field.
38
- final ValueChanged<String>? onSearchChanged;
39
-
40
- /// Called when the search field is submitted (Enter).
41
- final ValueChanged<String>? onSearchSubmitted;
42
-
43
- /// Notifications (bell) action. When null the bell is disabled. Ignored when
44
- /// [notifications] is provided.
45
- final VoidCallback? onNotifications;
46
-
47
- /// Shows the unread dot on the notifications bell. Ignored when
48
- /// [notifications] is provided.
49
- final bool showNotificationBadge;
50
-
51
- /// Custom notifications control. When set, it replaces the built-in bell —
52
- /// pass a data-aware widget (e.g. a bell that opens a recent-notifications
53
- /// dropdown) so the header itself stays a pure presentational component.
54
- final Widget? notifications;
55
-
56
- /// Primary quick-create action. When null the button is disabled.
57
- final VoidCallback? onCreate;
58
-
59
- /// Label for the create button.
60
- final String createLabel;
61
-
62
- /// Gradient used for the profile avatar fallback (when [avatar] is null).
63
- final KasyAvatarGradientData avatarGradient;
64
-
65
- /// Custom avatar widget — pass the signed-in user's avatar (e.g.
66
- /// `KasyUserAvatar`) to show their real photo. When null (and [showAvatar] is
67
- /// true), a gradient-fill avatar is shown instead.
68
- final Widget? avatar;
69
-
70
- /// Whether the profile avatar is shown at all. Set false for a header that
71
- /// carries no account chip (e.g. when the sidebar already owns the profile).
72
- final bool showAvatar;
73
-
74
- /// Profile avatar tap (open menu / profile). When null the avatar is inert.
75
- final VoidCallback? onAvatarTap;
76
-
77
- /// Theme toggle. When set, a sun/moon ghost button is shown before the bell
78
- /// (on desktop the web header replaces the app bar's theme toggle).
79
- final VoidCallback? onToggleTheme;
80
-
81
- const KasyWebHeader({
82
- super.key,
83
- this.searchController,
84
- this.searchHint = 'Search...',
85
- this.onSearchChanged,
86
- this.onSearchSubmitted,
87
- this.onNotifications,
88
- this.showNotificationBadge = false,
89
- this.notifications,
90
- this.onCreate,
91
- this.createLabel = 'Create',
92
- this.avatarGradient = KasyAvatarGradients.orange,
93
- this.avatar,
94
- this.showAvatar = true,
95
- this.onAvatarTap,
96
- this.onToggleTheme,
97
- });
98
-
99
- @override
100
- Widget build(BuildContext context) {
101
- final KasyColors c = context.colors;
102
- return DecoratedBox(
103
- decoration: BoxDecoration(
104
- // Matches KasySidebar exactly: surface fill + the same `border` hairline
105
- // used by the sidebar's vertical edge line, at the same 0.5px width — so
106
- // the header's bottom border and the sidebar's top divider read as one
107
- // continuous line across the whole chrome in light and dark.
108
- color: c.surface,
109
- border: Border(
110
- bottom: BorderSide(color: c.border, width: 0.5),
111
- ),
112
- ),
113
- // minHeight (not a fixed height) keeps the band at 68 while letting the
114
- // search field size naturally, so it never overflows the toolbar row.
115
- child: ConstrainedBox(
116
- constraints: const BoxConstraints(minHeight: kasyWebHeaderHeight),
117
- child: Padding(
118
- // Design gutters: 20px horizontal; vertical breathes around the field.
119
- padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
120
- child: Row(
121
- children: [
122
- SizedBox(
123
- width: kasyWebHeaderSearchWidth,
124
- child: _buildSearch(context),
125
- ),
126
- const Spacer(),
127
- if (onToggleTheme != null) ...[
128
- _buildThemeToggle(context),
129
- const SizedBox(width: KasySpacing.md),
130
- ],
131
- notifications ?? _buildNotifications(context),
132
- const SizedBox(width: KasySpacing.md),
133
- KasyButton(
134
- label: createLabel,
135
- variant: KasyButtonVariant.neutral,
136
- size: KasyButtonSize.small,
137
- onPressed: onCreate,
138
- ),
139
- if (showAvatar) ...[
140
- const SizedBox(width: KasySpacing.md),
141
- avatar ??
142
- KasyAvatar.gradientFill(
143
- size: KasyAvatarSize.small,
144
- diameter: 36,
145
- gradient: avatarGradient,
146
- showShadow: false,
147
- onTap: onAvatarTap,
148
- ),
149
- ],
150
- ],
151
- ),
152
- ),
153
- ),
154
- );
155
- }
156
-
157
- Widget _buildSearch(BuildContext context) {
158
- return KasyTextField(
159
- variant: KasyTextFieldVariant.flat,
160
- controller: searchController,
161
- hint: searchHint,
162
- onChanged: onSearchChanged,
163
- onSubmitted: onSearchSubmitted,
164
- prefix: Icon(
165
- KasyIcons.search,
166
- size: KasyIconSize.md,
167
- color: context.colors.muted,
168
- ),
169
- contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
170
- );
171
- }
172
-
173
- Widget _buildThemeToggle(BuildContext context) {
174
- final bool isDark = Theme.of(context).brightness == Brightness.dark;
175
- return KasyButton.iconOnly(
176
- icon: isDark ? KasyIcons.lightMode : KasyIcons.darkMode,
177
- variant: KasyButtonVariant.ghost,
178
- size: KasyButtonSize.small,
179
- iconOnlyLayoutExtent: 36,
180
- iconGlyphSize: KasyIconSize.md,
181
- onPressed: onToggleTheme,
182
- semanticLabel: isDark ? 'Light mode' : 'Dark mode',
183
- );
184
- }
185
-
186
- Widget _buildNotifications(BuildContext context) {
187
- final KasyColors c = context.colors;
188
- final Widget bell = KasyButton.iconOnly(
189
- icon: KasyIcons.notification,
190
- variant: KasyButtonVariant.ghost,
191
- size: KasyButtonSize.small,
192
- iconOnlyLayoutExtent: 36,
193
- iconGlyphSize: KasyIconSize.md,
194
- onPressed: onNotifications,
195
- semanticLabel: 'Notifications',
196
- );
197
- if (!showNotificationBadge) return bell;
198
- return Stack(
199
- clipBehavior: Clip.none,
200
- children: [
201
- bell,
202
- Positioned(
203
- top: 8,
204
- right: 8,
205
- child: Container(
206
- width: 8,
207
- height: 8,
208
- decoration: BoxDecoration(
209
- color: c.error,
210
- shape: BoxShape.circle,
211
- border: Border.all(color: c.background, width: 1.5),
212
- ),
213
- ),
214
- ),
215
- ],
216
- );
217
- }
218
- }
@@ -1,20 +0,0 @@
1
- import 'package:flutter/widgets.dart';
2
-
3
- /// Marks the subtree that sits BELOW the desktop web header ([KasyWebHeader]) —
4
- /// i.e. the shell content area, provided by [WebContentWrapper].
5
- ///
6
- /// [KasyAppBar] uses it to decide whether to hide on desktop: INSIDE this scope
7
- /// the web header already owns the top chrome, so the page app bar hides; OUTSIDE
8
- /// it (a full-screen pushed route with no web header above) the app bar stays
9
- /// visible — so the back button is never lost on desktop.
10
- class KasyWebHeaderScope extends InheritedWidget {
11
- const KasyWebHeaderScope({super.key, required super.child});
12
-
13
- /// True when a [KasyWebHeaderScope] is an ancestor (no rebuild dependency —
14
- /// presence is fixed for a given subtree).
15
- static bool of(BuildContext context) =>
16
- context.getInheritedWidgetOfExactType<KasyWebHeaderScope>() != null;
17
-
18
- @override
19
- bool updateShouldNotify(KasyWebHeaderScope oldWidget) => false;
20
- }
@@ -1,19 +0,0 @@
1
- import 'package:flutter/material.dart';
2
- import 'package:flutter_riverpod/flutter_riverpod.dart';
3
- import 'package:kasy_kit/features/authentication/providers/models/signin_state.dart';
4
- import 'package:kasy_kit/features/authentication/providers/signin_state_provider.dart';
5
- import 'package:kasy_kit/features/authentication/ui/widgets/round_signin.dart';
6
-
7
- class AppleSigninComponent extends ConsumerWidget {
8
- const AppleSigninComponent({super.key});
9
-
10
- @override
11
- Widget build(BuildContext context, WidgetRef ref) {
12
- // watch keeps the provider alive during async sign-in
13
- final state = ref.watch(signinStateProvider);
14
- final isSending = state is SigninStateSending;
15
- return SocialSigninButton.apple(
16
- () { if (!isSending) ref.read(signinStateProvider.notifier).signinWithApple(); },
17
- );
18
- }
19
- }
@@ -1,32 +0,0 @@
1
- import 'package:flutter/material.dart';
2
- import 'package:flutter_riverpod/flutter_riverpod.dart';
3
- import 'package:kasy_kit/features/authentication/providers/models/signin_state.dart';
4
- import 'package:kasy_kit/features/authentication/providers/signin_state_provider.dart';
5
- import 'package:kasy_kit/features/authentication/ui/widgets/round_signin.dart';
6
-
7
- class GoogleSignInComponent extends ConsumerWidget {
8
- const GoogleSignInComponent({super.key});
9
-
10
- @override
11
- Widget build(BuildContext context, WidgetRef ref) {
12
- // watch keeps the provider alive during async sign-in
13
- final state = ref.watch(signinStateProvider);
14
- final isSending = state is SigninStateSending;
15
- return SocialSigninButton.google(
16
- () { if (!isSending) ref.read(signinStateProvider.notifier).signinWithGoogle(); },
17
- );
18
- }
19
- }
20
-
21
- class GooglePlayGamesSignInComponent extends ConsumerWidget {
22
- const GooglePlayGamesSignInComponent({super.key});
23
-
24
- @override
25
- Widget build(BuildContext context, WidgetRef ref) {
26
- final state = ref.watch(signinStateProvider);
27
- final isSending = state is SigninStateSending;
28
- return SocialSigninButton.googlePlayGames(
29
- () { if (!isSending) ref.read(signinStateProvider.notifier).signinWithGooglePlayGames(); },
30
- );
31
- }
32
- }