kasy-cli 1.32.0 → 1.35.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 (169) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +66 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/apple-web.js +222 -0
  5. package/lib/commands/configure.js +3 -91
  6. package/lib/commands/doctor.js +20 -0
  7. package/lib/commands/facebook.js +189 -0
  8. package/lib/commands/new.js +61 -11
  9. package/lib/commands/release-version.js +234 -0
  10. package/lib/commands/update.js +27 -0
  11. package/lib/scaffold/CHANGELOG.json +27 -0
  12. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  13. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  14. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  15. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  16. package/lib/scaffold/backends/firebase/setup-from-scratch.js +199 -21
  17. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  18. package/lib/scaffold/backends/supabase/deploy.js +92 -0
  19. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  20. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  21. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  22. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +92 -3
  23. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  24. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  25. package/lib/scaffold/generate.js +53 -4
  26. package/lib/scaffold/shared/generator-utils.js +18 -6
  27. package/lib/utils/apple-web.js +147 -0
  28. package/lib/utils/facebook.js +162 -0
  29. package/lib/utils/i18n/messages-en.js +85 -0
  30. package/lib/utils/i18n/messages-es.js +85 -0
  31. package/lib/utils/i18n/messages-pt.js +85 -0
  32. package/package.json +5 -2
  33. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  34. package/templates/firebase/AGENTS.md +170 -0
  35. package/templates/firebase/CLAUDE.md +16 -0
  36. package/templates/firebase/DESIGN_SYSTEM.md +269 -0
  37. package/templates/firebase/docs/auth-setup.en.md +4 -2
  38. package/templates/firebase/docs/auth-setup.es.md +4 -2
  39. package/templates/firebase/docs/auth-setup.pt.md +4 -2
  40. package/templates/firebase/firebase.json +56 -1
  41. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  42. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  43. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  44. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  45. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  46. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  47. package/templates/firebase/lib/components/components.dart +1 -0
  48. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  49. package/templates/firebase/lib/components/kasy_app_bar.dart +35 -17
  50. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  51. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  52. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  53. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  54. package/templates/firebase/lib/components/kasy_screen.dart +114 -0
  55. package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
  56. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  57. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  58. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  59. package/templates/firebase/lib/components/kasy_toast.dart +108 -73
  60. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  61. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  62. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  63. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  64. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  65. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  66. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  67. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
  68. package/templates/firebase/lib/core/config/features.dart +5 -0
  69. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  70. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  71. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  72. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  73. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +48 -124
  74. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  75. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  76. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  77. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  78. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  79. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  80. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  81. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  82. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  83. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  84. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  85. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  86. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  87. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  88. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  89. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  90. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
  91. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -8
  92. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
  93. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  94. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  95. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  96. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  97. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  98. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +266 -0
  99. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  100. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  101. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  102. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  103. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  104. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  105. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  106. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  107. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  108. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +80 -15
  109. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
  110. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  111. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  112. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  113. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -2
  114. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  115. package/templates/firebase/lib/features/home/home_components_page.dart +8 -1
  116. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  117. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +186 -56
  118. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  119. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +169 -208
  120. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  121. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
  122. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
  123. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
  124. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -4
  125. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +84 -128
  126. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  127. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  128. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  129. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
  130. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
  131. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +2 -1
  132. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  133. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +58 -21
  134. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  135. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  136. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
  137. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  138. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  139. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  140. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  141. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  142. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  143. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  144. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  145. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  146. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  147. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  148. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  149. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  150. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  151. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  152. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  153. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  154. package/templates/firebase/lib/i18n/en.i18n.json +54 -7
  155. package/templates/firebase/lib/i18n/es.i18n.json +54 -7
  156. package/templates/firebase/lib/i18n/pt.i18n.json +54 -7
  157. package/templates/firebase/lib/main.dart +11 -2
  158. package/templates/firebase/lib/router.dart +94 -13
  159. package/templates/firebase/pubspec.yaml +1 -1
  160. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  161. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  162. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  163. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  164. package/templates/firebase/tool/design_check.dart +152 -0
  165. package/templates/firebase/web/index.html +162 -14
  166. package/templates/firebase/assets/images/review.png +0 -0
  167. package/templates/firebase/assets/images/update.png +0 -0
  168. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
  169. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
@@ -7,6 +7,12 @@ export interface UserEntityData {
7
7
  creation_date: Timestamp;
8
8
  last_update_date: Timestamp;
9
9
  locale?: string;
10
+ /**
11
+ * Set to true once the one-time welcome notification has been sent for this
12
+ * account. Guards against re-sending it when a device is re-registered after
13
+ * a logout/login cycle (which otherwise looks like a brand-new first device).
14
+ */
15
+ welcome_sent?: boolean;
10
16
  }
11
17
 
12
18
  export type NewUserEntityData = Omit<UserEntityData, "id">;
@@ -20,7 +26,8 @@ export class UserEntity {
20
26
  email,
21
27
  creation_date,
22
28
  last_update_date,
23
- locale
29
+ locale,
30
+ welcome_sent
24
31
  }: UserEntityData
25
32
  ) {
26
33
  this.id = id;
@@ -29,6 +36,7 @@ export class UserEntity {
29
36
  this.creation_date = creation_date;
30
37
  this.last_update_date = last_update_date;
31
38
  this.locale = locale;
39
+ this.welcome_sent = welcome_sent;
32
40
  }
33
41
 
34
42
  static fromData(data: UserEntityData): UserEntity {
@@ -46,6 +46,33 @@ export class UserRepository {
46
46
  });
47
47
  }
48
48
 
49
+ /**
50
+ * Atomically claims the one-time welcome for a user.
51
+ *
52
+ * Returns true for exactly ONE caller per account — the one that should send
53
+ * the welcome notification — and false for every later call (e.g. a device
54
+ * re-registered after a logout/login cycle, or a second device registering at
55
+ * the same time). The flag flip and the read happen inside a transaction, so
56
+ * two concurrent device registrations can never both send the welcome.
57
+ */
58
+ async claimWelcome(userId: string): Promise<boolean> {
59
+ const ref = this.db.collection("users").doc(userId);
60
+ return this.db.runTransaction(async (tx) => {
61
+ const snapshot = await tx.get(ref);
62
+ if (!snapshot.exists) {
63
+ return false;
64
+ }
65
+ if (snapshot.get("welcome_sent") === true) {
66
+ return false;
67
+ }
68
+ tx.update(ref, {
69
+ "welcome_sent": true,
70
+ "last_update_date": Timestamp.now(),
71
+ });
72
+ return true;
73
+ });
74
+ }
75
+
49
76
  async list({
50
77
  limit = 100,
51
78
  startAfter,
@@ -22,6 +22,7 @@ export interface SubscriptionData {
22
22
  creationDate: Timestamp;
23
23
  lastUpdate: Timestamp;
24
24
  expirationDate?: Timestamp;
25
+ trialEnd?: Timestamp;
25
26
  store: Stores;
26
27
  productId: string;
27
28
  // Denormalized copy of the subscriber's email. Firestore is a non-relational
@@ -41,6 +42,7 @@ export class Subscription {
41
42
  creationDate,
42
43
  lastUpdate,
43
44
  expirationDate,
45
+ trialEnd,
44
46
  store,
45
47
  productId,
46
48
  email,
@@ -52,6 +54,7 @@ export class Subscription {
52
54
  this.creationDate = creationDate;
53
55
  this.lastUpdate = lastUpdate;
54
56
  this.expirationDate = expirationDate;
57
+ this.trialEnd = trialEnd;
55
58
  this.store = store;
56
59
  this.productId = productId;
57
60
  this.email = email;
@@ -275,6 +275,9 @@ async function upsertFromStripeSubscription(sub: Stripe.Subscription): Promise<v
275
275
  const expiration = periodEnd
276
276
  ? Timestamp.fromMillis(periodEnd * 1000)
277
277
  : undefined;
278
+ const trialEnd = sub.trial_end
279
+ ? Timestamp.fromMillis(sub.trial_end * 1000)
280
+ : undefined;
278
281
 
279
282
  const subscription = new Subscription(
280
283
  {
@@ -283,6 +286,7 @@ async function upsertFromStripeSubscription(sub: Stripe.Subscription): Promise<v
283
286
  creationDate: existing?.creationDate ?? now,
284
287
  lastUpdate: now,
285
288
  expirationDate: expiration,
289
+ trialEnd,
286
290
  store: Stores.STRIPE,
287
291
  productId: priceId,
288
292
  email: user.email,
@@ -21,6 +21,7 @@ export 'kasy_date_picker.dart';
21
21
  export 'kasy_dialog.dart';
22
22
  export 'kasy_image_viewer.dart';
23
23
  export 'kasy_otp_verification_bottom_sheet.dart';
24
+ export 'kasy_screen.dart';
24
25
  export 'kasy_sidebar.dart';
25
26
  export 'kasy_skeleton.dart';
26
27
  export 'kasy_status_tag.dart';
@@ -53,7 +53,6 @@ class KasyAlert extends StatelessWidget {
53
53
  final bool hasMessage = message != null && message!.trim().isNotEmpty;
54
54
  final TextStyle titleStyle =
55
55
  context.textTheme.titleMedium?.copyWith(
56
- fontWeight: FontWeight.w600,
57
56
  color: emphasizeTitleWithTone
58
57
  ? palette.accent
59
58
  : context.colors.onSurface,
@@ -186,7 +186,10 @@ class KasyTopScrollFade extends StatelessWidget {
186
186
  // On web topInset = 0; enforce a 6 px minimum so the strip still exists.
187
187
  final double solidHeight = topInset < 6 ? 6.0 : topInset;
188
188
  final double totalHeight = solidHeight + _contentFade;
189
- final Color base = context.colors.surface;
189
+ // The page background (NOT the bar's `surface`): when the bar hides on
190
+ // scroll, content must melt into the page color, not into a floating sliver
191
+ // of the bar's tint at the top.
192
+ final Color base = context.colors.background;
190
193
 
191
194
  // Same gradient the grid cards use for their caption scrim — strong at the
192
195
  // top, melting gradually to fully transparent at the bottom.
@@ -238,6 +241,12 @@ class KasyAppBar extends StatelessWidget {
238
241
  /// or wrap a custom 44×44 chrome control with [KasyAppBarChromeTap] for the
239
242
  /// same tap target semantics as the built-in orbs.
240
243
  final Widget? trailing;
244
+
245
+ /// Replaces the default leading control (back button / spacer) — e.g. a menu
246
+ /// orb that opens a [KasySidebar] drawer. Use [KasyChromeOrbIconButton] so it
247
+ /// matches the trailing orbs' size and tap target.
248
+ final Widget? leading;
249
+
241
250
  final bool useSafeArea;
242
251
 
243
252
  /// When set, called instead of [ThemeProvider.toggle] for the theme orb
@@ -266,6 +275,7 @@ class KasyAppBar extends StatelessWidget {
266
275
  this.style = KasyAppBarStyle.subpage,
267
276
  this.onBack,
268
277
  this.trailing,
278
+ this.leading,
269
279
  this.useSafeArea = true,
270
280
  this.onThemeToggle,
271
281
  this.toolbarHeight,
@@ -285,26 +295,30 @@ class KasyAppBar extends StatelessWidget {
285
295
 
286
296
  final double rowHeight =
287
297
  toolbarHeight ?? kasyAppBarToolbarRowHeightOf(context);
288
- final Widget leading = switch (style) {
289
- KasyAppBarStyle.rootTab => SizedBox(
290
- width: 44,
291
- height: rowHeight,
292
- ),
293
- _ => KasyChromeOrbIconButton(
294
- icon: KasyIcons.arrowBackIos,
295
- iconSize: KasyIconSize.md,
296
- foregroundColor: orbFg,
297
- fillColor: orbFill,
298
- onPressed: handleBack,
299
- tooltip: MaterialLocalizations.of(context).backButtonTooltip,
300
- ),
301
- };
298
+ final Widget leadingWidget = leading ??
299
+ switch (style) {
300
+ KasyAppBarStyle.rootTab => SizedBox(
301
+ width: 44,
302
+ height: rowHeight,
303
+ ),
304
+ _ => KasyChromeOrbIconButton(
305
+ icon: KasyIcons.arrowBackIos,
306
+ iconSize: KasyIconSize.md,
307
+ foregroundColor: orbFg,
308
+ fillColor: orbFill,
309
+ onPressed: handleBack,
310
+ tooltip: MaterialLocalizations.of(context).backButtonTooltip,
311
+ ),
312
+ };
302
313
 
303
314
  final TextStyle? titleRef =
304
315
  context.textTheme.headlineSmall ?? context.textTheme.titleLarge;
305
316
  final TextStyle? titleStyle = titleRef?.copyWith(
306
317
  fontSize: (titleRef.fontSize ?? 24) * kasyAppBarTitleFontScale,
307
- fontWeight: FontWeight.w900,
318
+ // w700 (not w900): a clean page-title weight that matches the Home /
319
+ // sidebar scale. w900 read as a heavy "black" that fought the rest of
320
+ // the chrome.
321
+ fontWeight: FontWeight.w700,
308
322
  letterSpacing: -0.35,
309
323
  color: context.colors.onBackground,
310
324
  );
@@ -323,7 +337,7 @@ class KasyAppBar extends StatelessWidget {
323
337
  children: [
324
338
  Row(
325
339
  children: [
326
- leading,
340
+ leadingWidget,
327
341
  const Spacer(),
328
342
  _buildTrailing(context, orbFg, orbFill),
329
343
  ],
@@ -483,6 +497,10 @@ class KasyOverlayScaffold extends StatelessWidget {
483
497
  Widget scrollView = CustomScrollView(
484
498
  controller: scrollController,
485
499
  physics: effectivePhysics,
500
+ // Dragging the form means the user is done with the focused field and
501
+ // wants to see what's below, so dismiss the keyboard. Chat views opt out
502
+ // of this wrapper on purpose (there scrolling means reading, not typing).
503
+ keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
486
504
  slivers: kasyOverlayPaddedSlivers(
487
505
  context,
488
506
  contentPadding: contentPadding,
@@ -105,7 +105,6 @@ class KasyBottomSheet extends StatelessWidget {
105
105
  title!,
106
106
  textAlign: centered ? TextAlign.center : TextAlign.start,
107
107
  style: context.textTheme.titleLarge?.copyWith(
108
- fontWeight: FontWeight.w600,
109
108
  color: context.colors.onSurface,
110
109
  ),
111
110
  ),
@@ -1457,7 +1457,6 @@ class _CalendarPanelState extends State<_CalendarPanel> {
1457
1457
  context.textTheme.labelLarge?.copyWith(
1458
1458
  color: context.colors.onSurface,
1459
1459
  fontWeight: FontWeight.w700,
1460
- fontSize: 14,
1461
1460
  );
1462
1461
 
1463
1462
  return Column(
@@ -1623,7 +1622,6 @@ class _MonthGrid extends StatelessWidget {
1623
1622
  style: context.textTheme.labelLarge?.copyWith(
1624
1623
  color: context.colors.onSurface,
1625
1624
  fontWeight: FontWeight.w700,
1626
- fontSize: 14,
1627
1625
  ),
1628
1626
  ),
1629
1627
  ),
@@ -1688,7 +1686,6 @@ class _CalendarNavRow extends StatelessWidget {
1688
1686
  style: context.textTheme.labelLarge?.copyWith(
1689
1687
  color: c.onSurface,
1690
1688
  fontWeight: FontWeight.w700,
1691
- fontSize: 14,
1692
1689
  ),
1693
1690
  ),
1694
1691
  const SizedBox(width: 4),
@@ -1903,7 +1900,6 @@ class _YearPickerContentState extends State<_YearPickerContent> {
1903
1900
  ? const Color(0xFFFCFCFC)
1904
1901
  : c.onSurface,
1905
1902
  fontWeight: FontWeight.w600,
1906
- fontSize: 14,
1907
1903
  ),
1908
1904
  ),
1909
1905
  ),
@@ -2125,7 +2121,6 @@ class _DayCell extends StatelessWidget {
2125
2121
  style: context.textTheme.labelLarge?.copyWith(
2126
2122
  color: textColor,
2127
2123
  fontWeight: FontWeight.w600,
2128
- fontSize: 14,
2129
2124
  height: 1.43,
2130
2125
  ),
2131
2126
  ),
@@ -152,7 +152,6 @@ class KasyDialog extends StatelessWidget {
152
152
 
153
153
  final TextStyle titleStyle =
154
154
  context.textTheme.titleLarge?.copyWith(
155
- fontWeight: FontWeight.w600,
156
155
  color: context.colors.onSurface,
157
156
  ) ??
158
157
  TextStyle(fontWeight: FontWeight.w600, fontSize: 20, color: context.colors.onSurface);
@@ -111,7 +111,7 @@ class _KasyOtpVerificationBottomSheetState
111
111
  Text(
112
112
  '+1 234 567 8900',
113
113
  style: sheetContext.textTheme.titleLarge?.copyWith(
114
- fontWeight: FontWeight.w800,
114
+ fontWeight: FontWeight.w700,
115
115
  color: sheetContext.colors.onSurface,
116
116
  ),
117
117
  textAlign: TextAlign.center,
@@ -0,0 +1,114 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:kasy_kit/components/kasy_card.dart';
3
+ import 'package:kasy_kit/core/theme/theme.dart';
4
+ import 'package:kasy_kit/core/widgets/page_background.dart';
5
+
6
+ /// Canonical screen scaffold. A new page that uses it inherits the internal
7
+ /// screen contract for free: the page background, a centred content column
8
+ /// capped at [maxContentWidth], the page gutter, scroll handling, and
9
+ /// (optionally) a [KasyCard] wrapper — all from design-system tokens.
10
+ ///
11
+ /// Opt-in by design: it is a thin convenience over [Scaffold] (it forwards the
12
+ /// same `appBar`, `floatingActionButton`, `bottomNavigationBar` slots), not a
13
+ /// cage. Screens that need bespoke chrome can keep using [Scaffold] directly —
14
+ /// nothing here is forced, and any value is overridable.
15
+ ///
16
+ /// ```dart
17
+ /// KasyScreen(
18
+ /// appBar: KasyAppBar.root(title: 'Settings'),
19
+ /// card: true,
20
+ /// child: Column(children: [...]),
21
+ /// )
22
+ /// ```
23
+ class KasyScreen extends StatelessWidget {
24
+ const KasyScreen({
25
+ super.key,
26
+ required this.child,
27
+ this.appBar,
28
+ this.scrollable = true,
29
+ this.maxContentWidth = 600,
30
+ this.padding,
31
+ this.card = false,
32
+ this.backgroundColor,
33
+ this.resizeToAvoidBottomInset,
34
+ this.floatingActionButton,
35
+ this.bottomNavigationBar,
36
+ });
37
+
38
+ /// Page content.
39
+ final Widget child;
40
+
41
+ /// Optional top bar. Same slot as [Scaffold.appBar]; pass any
42
+ /// [PreferredSizeWidget].
43
+ final PreferredSizeWidget? appBar;
44
+
45
+ /// Wrap the content in a scroll view (default true). Set false for screens
46
+ /// that manage their own scrolling (e.g. a full-bleed list).
47
+ final bool scrollable;
48
+
49
+ /// Max width of the content column on wide screens. The internal-screen
50
+ /// contract is ~600; content is centred within the available width so it does
51
+ /// not stretch edge-to-edge on web/desktop.
52
+ final double maxContentWidth;
53
+
54
+ /// Padding around the content. Defaults to the horizontal page gutter plus
55
+ /// vertical breathing room.
56
+ final EdgeInsetsGeometry? padding;
57
+
58
+ /// Wrap the content in an elevated [KasyCard] (radius 16), matching the Home
59
+ /// reference look.
60
+ final bool card;
61
+
62
+ /// Page background. Defaults to the theme background token.
63
+ final Color? backgroundColor;
64
+
65
+ /// Forwarded to [Scaffold.resizeToAvoidBottomInset].
66
+ final bool? resizeToAvoidBottomInset;
67
+
68
+ /// Forwarded to [Scaffold.floatingActionButton].
69
+ final Widget? floatingActionButton;
70
+
71
+ /// Forwarded to [Scaffold.bottomNavigationBar].
72
+ final Widget? bottomNavigationBar;
73
+
74
+ @override
75
+ Widget build(BuildContext context) {
76
+ final EdgeInsetsGeometry resolvedPadding = padding ??
77
+ const EdgeInsets.symmetric(
78
+ horizontal: KasySpacing.pageHorizontalGutter,
79
+ vertical: KasySpacing.lg,
80
+ );
81
+
82
+ Widget content = ConstrainedBox(
83
+ constraints: BoxConstraints(maxWidth: maxContentWidth),
84
+ child: card ? KasyCard(child: child) : child,
85
+ );
86
+
87
+ content = Align(
88
+ alignment: Alignment.topCenter,
89
+ child: Padding(padding: resolvedPadding, child: content),
90
+ );
91
+
92
+ if (scrollable) {
93
+ content = SingleChildScrollView(
94
+ physics: const ClampingScrollPhysics(),
95
+ keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
96
+ child: content,
97
+ );
98
+ }
99
+
100
+ return Scaffold(
101
+ backgroundColor: backgroundColor ?? context.colors.background,
102
+ resizeToAvoidBottomInset: resizeToAvoidBottomInset,
103
+ appBar: appBar,
104
+ floatingActionButton: floatingActionButton,
105
+ bottomNavigationBar: bottomNavigationBar,
106
+ body: Background(
107
+ bgColor: backgroundColor,
108
+ // top:false when an app bar is present — Scaffold already insets the
109
+ // body below it, so SafeArea only needs to guard the bottom/sides.
110
+ child: SafeArea(top: appBar == null, child: content),
111
+ ),
112
+ );
113
+ }
114
+ }