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
@@ -36,8 +36,8 @@ class _OtpVerificationComponentState
36
36
  const SizedBox(height: KasySpacing.lg),
37
37
  Text(
38
38
  t.phone_auth.verification_code,
39
- style: context.textTheme.titleLarge?.copyWith(
40
- fontWeight: FontWeight.bold,
39
+ style: context.kasyTextTheme.pageTitle.copyWith(
40
+ color: context.colors.onSurface,
41
41
  ),
42
42
  textAlign: TextAlign.center,
43
43
  ),
@@ -38,8 +38,8 @@ class _PhoneInputComponentState extends ConsumerState<PhoneInputComponent> {
38
38
  const SizedBox(height: KasySpacing.lg),
39
39
  Text(
40
40
  t.phone_auth.subtitle_input,
41
- style: context.textTheme.titleLarge?.copyWith(
42
- fontWeight: FontWeight.bold,
41
+ style: context.kasyTextTheme.pageTitle.copyWith(
42
+ color: context.colors.onSurface,
43
43
  ),
44
44
  textAlign: TextAlign.center,
45
45
  ),
@@ -12,6 +12,7 @@ import 'package:kasy_kit/core/states/user_state_notifier.dart';
12
12
  import 'package:kasy_kit/core/theme/theme.dart';
13
13
  import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
14
14
  import 'package:kasy_kit/core/widgets/kasy_pressable_depth.dart';
15
+ import 'package:kasy_kit/environments.dart';
15
16
  import 'package:kasy_kit/features/authentication/providers/models/email.dart';
16
17
  import 'package:kasy_kit/features/authentication/providers/models/password.dart';
17
18
  import 'package:kasy_kit/features/authentication/providers/models/signin_state.dart';
@@ -49,7 +50,7 @@ class SigninPage extends ConsumerWidget {
49
50
  const TextStyle())
50
51
  .copyWith(
51
52
  color: context.colors.muted,
52
- fontSize: 13,
53
+ fontSize: 13, // design-check: ignore — small "forgot password" link
53
54
  fontWeight: FontWeight.w500,
54
55
  );
55
56
  return PopScope(
@@ -152,6 +153,21 @@ class SigninPage extends ConsumerWidget {
152
153
  const SocialSeparator(),
153
154
  const SizedBox(height: KasySpacing.md),
154
155
  const _SocialSigninRow(),
156
+ // Guest path: only on native in anonymous mode (on web the
157
+ // app requires sign-in), and only when there's no identity
158
+ // yet (post-logout / first run). A user who is already an
159
+ // anonymous guest opened sign-in to UPGRADE their account
160
+ // (link email/social), so "continue without account" would
161
+ // be nonsense for them — hide it.
162
+ if (!kIsWeb &&
163
+ user.idOrNull == null &&
164
+ ref
165
+ .watch(environmentProvider)
166
+ .authenticationMode ==
167
+ AuthenticationMode.anonymous) ...[
168
+ const SizedBox(height: KasySpacing.md),
169
+ const _GuestContinueButton(),
170
+ ],
155
171
  ],
156
172
  ),
157
173
  ),
@@ -222,6 +238,49 @@ class _SignupPrompt extends StatelessWidget {
222
238
  }
223
239
  }
224
240
 
241
+ /// "Continue as guest" — creates the anonymous account on demand and lets the
242
+ /// router redirect carry the user into the app. Shown only on native in
243
+ /// anonymous mode (see the build condition in [SigninPage]).
244
+ class _GuestContinueButton extends ConsumerStatefulWidget {
245
+ const _GuestContinueButton();
246
+
247
+ @override
248
+ ConsumerState<_GuestContinueButton> createState() =>
249
+ _GuestContinueButtonState();
250
+ }
251
+
252
+ class _GuestContinueButtonState extends ConsumerState<_GuestContinueButton> {
253
+ bool _loading = false;
254
+
255
+ Future<void> _continue() async {
256
+ setState(() => _loading = true);
257
+ try {
258
+ await ref.read(userStateNotifierProvider.notifier).continueAsGuest();
259
+ // Success: the router redirect navigates to home reactively — nothing
260
+ // else to do here. The widget is disposed as we leave the page.
261
+ } catch (_) {
262
+ if (!mounted) return;
263
+ setState(() => _loading = false);
264
+ showKasyToast(
265
+ context,
266
+ title: t.auth.signin.error_title,
267
+ tone: KasyToastTone.danger,
268
+ );
269
+ }
270
+ }
271
+
272
+ @override
273
+ Widget build(BuildContext context) {
274
+ return KasyButton(
275
+ label: t.auth.signin.continue_without,
276
+ variant: KasyButtonVariant.tertiary,
277
+ expand: true,
278
+ isLoading: _loading,
279
+ onPressed: _loading ? null : _continue,
280
+ );
281
+ }
282
+ }
283
+
225
284
  class _SocialSigninRow extends ConsumerWidget {
226
285
  const _SocialSigninRow();
227
286
 
@@ -237,6 +296,10 @@ class _SocialSigninRow extends ConsumerWidget {
237
296
  ? withAppleWebSignin
238
297
  : (defaultTargetPlatform == TargetPlatform.iOS ||
239
298
  defaultTargetPlatform == TargetPlatform.macOS);
299
+ // Facebook on web works on the Firebase backend (signInWithPopup); on Supabase
300
+ // the web flow isn't wired yet (roadmap), so withFacebookWebSignin ships false
301
+ // there. Native (iOS/Android) always shows it.
302
+ const bool showFacebook = !kIsWeb || withFacebookWebSignin;
240
303
  return Row(
241
304
  children: [
242
305
  Expanded(
@@ -263,22 +326,24 @@ class _SocialSigninRow extends ConsumerWidget {
263
326
  ),
264
327
  ),
265
328
  ],
266
- const SizedBox(width: KasySpacing.sm),
267
- Expanded(
268
- child: _SocialSigninTile(
269
- label: t.auth.signin.facebook,
270
- icon: Image.asset(
271
- 'assets/icons/facebook.png',
272
- width: 20,
273
- height: 20,
329
+ if (showFacebook) ...[
330
+ const SizedBox(width: KasySpacing.sm),
331
+ Expanded(
332
+ child: _SocialSigninTile(
333
+ label: t.auth.signin.facebook,
334
+ icon: Image.asset(
335
+ 'assets/icons/facebook.png',
336
+ width: 20,
337
+ height: 20,
338
+ ),
339
+ onPressed: isSending
340
+ ? null
341
+ : () => ref
342
+ .read(signinStateProvider.notifier)
343
+ .signinWithFacebook(),
274
344
  ),
275
- onPressed: isSending
276
- ? null
277
- : () => ref
278
- .read(signinStateProvider.notifier)
279
- .signinWithFacebook(),
280
345
  ),
281
- ),
346
+ ],
282
347
  ],
283
348
  );
284
349
  }
@@ -216,6 +216,10 @@ class _SocialSignupRow extends ConsumerWidget {
216
216
  ? withAppleWebSignin
217
217
  : (defaultTargetPlatform == TargetPlatform.iOS ||
218
218
  defaultTargetPlatform == TargetPlatform.macOS);
219
+ // Facebook on web works on the Firebase backend (signInWithPopup); on Supabase
220
+ // the web flow isn't wired yet (roadmap), so withFacebookWebSignin ships false
221
+ // there. Native (iOS/Android) always shows it.
222
+ const bool showFacebook = !kIsWeb || withFacebookWebSignin;
219
223
  return Row(
220
224
  children: [
221
225
  Expanded(
@@ -242,22 +246,24 @@ class _SocialSignupRow extends ConsumerWidget {
242
246
  ),
243
247
  ),
244
248
  ],
245
- const SizedBox(width: KasySpacing.sm),
246
- Expanded(
247
- child: _SocialSignupTile(
248
- label: t.auth.signin.facebook,
249
- icon: Image.asset(
250
- 'assets/icons/facebook.png',
251
- width: 20,
252
- height: 20,
249
+ if (showFacebook) ...[
250
+ const SizedBox(width: KasySpacing.sm),
251
+ Expanded(
252
+ child: _SocialSignupTile(
253
+ label: t.auth.signin.facebook,
254
+ icon: Image.asset(
255
+ 'assets/icons/facebook.png',
256
+ width: 20,
257
+ height: 20,
258
+ ),
259
+ onPressed: isSending
260
+ ? null
261
+ : () => ref
262
+ .read(signinStateProvider.notifier)
263
+ .signinWithFacebook(),
253
264
  ),
254
- onPressed: isSending
255
- ? null
256
- : () => ref
257
- .read(signinStateProvider.notifier)
258
- .signinWithFacebook(),
259
265
  ),
260
- ),
266
+ ],
261
267
  ],
262
268
  );
263
269
  }
@@ -79,8 +79,8 @@ class AuthCardScaffold extends StatelessWidget {
79
79
  Text(
80
80
  title,
81
81
  textAlign: TextAlign.center,
82
- style: context.textTheme.headlineSmall?.copyWith(
83
- fontWeight: FontWeight.w700,
82
+ style: context.kasyTextTheme.pageTitle.copyWith(
83
+ color: context.colors.onSurface,
84
84
  ),
85
85
  ),
86
86
  const SizedBox(height: KasySpacing.xs),
@@ -42,8 +42,8 @@ class RecoverPasswordSent extends StatelessWidget {
42
42
  Text(
43
43
  t.recover_password_result.title,
44
44
  textAlign: TextAlign.center,
45
- style: context.textTheme.headlineSmall?.copyWith(
46
- fontWeight: FontWeight.w700,
45
+ style: context.kasyTextTheme.pageTitle.copyWith(
46
+ color: context.colors.onSurface,
47
47
  ),
48
48
  ),
49
49
  const SizedBox(height: KasySpacing.smd),
@@ -40,7 +40,6 @@ class AddFeatureButton extends StatelessWidget {
40
40
  Text(
41
41
  title,
42
42
  style: context.textTheme.titleMedium?.copyWith(
43
- fontWeight: FontWeight.w600,
44
43
  color: context.colors.onPrimary,
45
44
  ),
46
45
  ),
@@ -60,7 +60,6 @@ class _FeatureCardState extends State<FeatureCard> {
60
60
  Text(
61
61
  widget.title,
62
62
  style: context.textTheme.titleMedium?.copyWith(
63
- fontSize: 16,
64
63
  fontWeight: FontWeight.w700,
65
64
  ),
66
65
  ),
@@ -251,7 +250,7 @@ class _VoteCardState extends State<VoteCard>
251
250
  key: ValueKey('votes-${widget.id}-${widget.votes}'),
252
251
  widget.votes.toString(),
253
252
  style: context.textTheme.labelLarge?.copyWith(
254
- fontSize: 15,
253
+ fontSize: 15, // design-check: ignore — vote-count badge
255
254
  height: 1,
256
255
  fontWeight: FontWeight.w800,
257
256
  color: widget.textColor,
@@ -1,8 +1,12 @@
1
1
  import 'dart:ui' show ImageFilter;
2
2
 
3
3
  import 'package:flutter/material.dart';
4
+ import 'package:google_fonts/google_fonts.dart';
4
5
  import 'package:kasy_kit/components/kasy_app_bar.dart';
6
+ import 'package:kasy_kit/components/kasy_tabs.dart';
5
7
  import 'package:kasy_kit/core/theme/theme.dart';
8
+ import 'package:kasy_kit/core/theme/type_scale.dart';
9
+ import 'package:kasy_kit/core/widgets/responsive_layout.dart';
6
10
 
7
11
  class DesignSystemPage extends StatelessWidget {
8
12
  const DesignSystemPage({super.key});
@@ -576,12 +580,10 @@ class _VariantList extends StatelessWidget {
576
580
 
577
581
  @override
578
582
  Widget build(BuildContext context) {
579
- final KasyColors light = KasyColors.light();
580
- final KasyColors dark = KasyColors.dark();
581
583
  return _TokenCard(
582
584
  children: [
583
585
  for (int i = 0; i < variants.length; i++) ...[
584
- _VariantRow(variant: variants[i], light: light, dark: dark),
586
+ _VariantRow(variant: variants[i]),
585
587
  if (i < variants.length - 1) const _TokenDivider(),
586
588
  ],
587
589
  ],
@@ -591,18 +593,14 @@ class _VariantList extends StatelessWidget {
591
593
 
592
594
  class _VariantRow extends StatelessWidget {
593
595
  final _Variant variant;
594
- final KasyColors light;
595
- final KasyColors dark;
596
- const _VariantRow({
597
- required this.variant,
598
- required this.light,
599
- required this.dark,
600
- });
596
+ const _VariantRow({required this.variant});
601
597
 
602
598
  @override
603
599
  Widget build(BuildContext context) {
604
- final Color lightColor = variant.pick(light);
605
- final Color darkColor = variant.pick(dark);
600
+ // Single swatch resolved against the CURRENT theme. The Design System app
601
+ // bar already toggles light/dark, so each row shows the token in the active
602
+ // theme instead of doubling it up with a light + dark chip on a surface.
603
+ final Color color = variant.pick(context.colors);
606
604
  return Padding(
607
605
  padding: const EdgeInsets.symmetric(
608
606
  horizontal: KasySpacing.md,
@@ -610,9 +608,7 @@ class _VariantRow extends StatelessWidget {
610
608
  ),
611
609
  child: Row(
612
610
  children: [
613
- _ThemeChip(surface: light.surface, token: lightColor, border: variant.border),
614
- const SizedBox(width: KasySpacing.xs),
615
- _ThemeChip(surface: dark.surface, token: darkColor, border: variant.border),
611
+ _Swatch(color: color, border: variant.border),
616
612
  const SizedBox(width: KasySpacing.smd),
617
613
  Expanded(
618
614
  child: Column(
@@ -626,7 +622,7 @@ class _VariantRow extends StatelessWidget {
626
622
  ),
627
623
  const SizedBox(height: 2),
628
624
  Text(
629
- '${_hexOf(lightColor)} · ${_hexOf(darkColor)}',
625
+ _hexOf(color),
630
626
  style: context.textTheme.bodySmall?.copyWith(
631
627
  color: context.colors.muted,
632
628
  fontFeatures: const [FontFeature.tabularFigures()],
@@ -641,34 +637,24 @@ class _VariantRow extends StatelessWidget {
641
637
  }
642
638
  }
643
639
 
644
- class _ThemeChip extends StatelessWidget {
645
- final Color surface;
646
- final Color token;
640
+ class _Swatch extends StatelessWidget {
641
+ final Color color;
642
+
643
+ /// Force a stronger hairline (near-surface / transparent tones); a subtle
644
+ /// hairline is always drawn so every swatch reads cleanly against the card.
647
645
  final bool border;
648
- const _ThemeChip({
649
- required this.surface,
650
- required this.token,
651
- this.border = false,
652
- });
646
+ const _Swatch({required this.color, this.border = false});
653
647
 
654
648
  @override
655
649
  Widget build(BuildContext context) {
656
650
  return Container(
657
651
  width: 28,
658
652
  height: 28,
659
- padding: const EdgeInsets.all(3),
660
653
  decoration: BoxDecoration(
661
- color: surface,
654
+ color: color,
662
655
  borderRadius: BorderRadius.circular(KasyRadius.xs),
663
- border: Border.all(color: context.colors.outline.withValues(alpha: 0.5)),
664
- ),
665
- child: DecoratedBox(
666
- decoration: BoxDecoration(
667
- color: token,
668
- borderRadius: BorderRadius.circular(KasyRadius.xs - 1),
669
- border: border
670
- ? Border.all(color: context.colors.outline.withValues(alpha: 0.5))
671
- : null,
656
+ border: Border.all(
657
+ color: context.colors.outline.withValues(alpha: border ? 0.5 : 0.28),
672
658
  ),
673
659
  ),
674
660
  );
@@ -679,48 +665,98 @@ class _ThemeChip extends StatelessWidget {
679
665
  // Typography
680
666
  // ---------------------------------------------------------------------------
681
667
 
682
- class _TypographySection extends StatelessWidget {
668
+ /// One breakpoint shown by the typography preview: its label, the [DeviceType]
669
+ /// it maps to, and a short range caption.
670
+ class _Breakpoint {
671
+ final String label;
672
+ final DeviceType device;
673
+ final String range;
674
+ const _Breakpoint(this.label, this.device, this.range);
675
+ }
676
+
677
+ const List<_Breakpoint> _breakpoints = [
678
+ _Breakpoint('Mobile', DeviceType.small, '< 768px'),
679
+ _Breakpoint('Tablet', DeviceType.medium, '768 - 1024px'),
680
+ _Breakpoint('Desktop', DeviceType.large, '>= 1024px'),
681
+ ];
682
+
683
+ class _TypographySection extends StatefulWidget {
683
684
  const _TypographySection();
684
685
 
686
+ @override
687
+ State<_TypographySection> createState() => _TypographySectionState();
688
+ }
689
+
690
+ class _TypographySectionState extends State<_TypographySection> {
691
+ /// Selected breakpoint tab. Null until the first build, where it defaults to
692
+ /// the device currently viewing the screen; a tap pins it from then on.
693
+ int? _index;
694
+
695
+ static const List<_TypeRole> _roles = [
696
+ _TypeRole('Heading 1', KasyTypeScale.heading1, FontWeight.w800, 'ExtraBold'),
697
+ _TypeRole('Heading 2', KasyTypeScale.heading2, FontWeight.w700, 'Bold'),
698
+ _TypeRole('Heading 3', KasyTypeScale.heading3, FontWeight.w600, 'SemiBold'),
699
+ _TypeRole('Heading 4', KasyTypeScale.heading4, FontWeight.w600, 'SemiBold'),
700
+ _TypeRole('Body base', KasyTypeScale.bodyBase, FontWeight.w400, 'Regular'),
701
+ _TypeRole('Body base medium', KasyTypeScale.bodyBase, FontWeight.w500, 'Medium'),
702
+ _TypeRole('Body sm', KasyTypeScale.bodySm, FontWeight.w400, 'Regular'),
703
+ _TypeRole('Body sm medium', KasyTypeScale.bodySm, FontWeight.w500, 'Medium'),
704
+ _TypeRole('Body xs', KasyTypeScale.bodyXs, FontWeight.w400, 'Regular'),
705
+ _TypeRole('Body xs medium', KasyTypeScale.bodyXs, FontWeight.w500, 'Medium'),
706
+ _TypeRole('Link base', KasyTypeScale.bodyBase, FontWeight.w500, 'Medium · Underlined', underline: true),
707
+ _TypeRole('Link sm', KasyTypeScale.bodySm, FontWeight.w500, 'Medium · Underlined', underline: true),
708
+ _TypeRole('Text field base', KasyTypeScale.bodyBase, FontWeight.w400, 'Regular'),
709
+ _TypeRole('Text field sm', KasyTypeScale.bodySm, FontWeight.w400, 'Regular'),
710
+ _TypeRole('Button base', KasyTypeScale.bodyBase, FontWeight.w500, 'Medium'),
711
+ _TypeRole('Button sm', KasyTypeScale.bodySm, FontWeight.w500, 'Medium'),
712
+ ];
713
+
714
+ int _defaultIndexFor(double width) => switch (DeviceType.fromWidth(width)) {
715
+ DeviceType.small => 0,
716
+ DeviceType.medium => 1,
717
+ DeviceType.large || DeviceType.xlarge => 2,
718
+ };
719
+
685
720
  @override
686
721
  Widget build(BuildContext context) {
687
722
  final Color fg = context.colors.onSurface;
688
- final List<_TypeRole> roles = [
689
- _TypeRole('Heading 1', KasyTextTheme.heading1, 'ExtraBold · 36/40'),
690
- _TypeRole('Heading 2', KasyTextTheme.heading2, 'Bold · 24/32'),
691
- _TypeRole('Heading 3', KasyTextTheme.heading3, 'SemiBold · 20/28'),
692
- _TypeRole('Heading 4', KasyTextTheme.heading4, 'SemiBold · 16/24'),
693
- _TypeRole('Body base', KasyTextTheme.bodyBase, 'Regular · 16/24'),
694
- _TypeRole('Body base medium', KasyTextTheme.bodyBaseMedium, 'Medium · 16/24'),
695
- _TypeRole('Body sm', KasyTextTheme.bodySm, 'Regular · 14/20'),
696
- _TypeRole('Body sm medium', KasyTextTheme.bodySmMedium, 'Medium · 14/20'),
697
- _TypeRole('Body xs', KasyTextTheme.bodyXs, 'Regular · 12/16'),
698
- _TypeRole('Body xs medium', KasyTextTheme.bodyXsMedium, 'Medium · 12/16'),
699
- _TypeRole('Link base', KasyTextTheme.linkBase, 'Medium · 16/24 · Underlined'),
700
- _TypeRole('Link sm', KasyTextTheme.linkSm, 'Medium · 14/20 · Underlined'),
701
- _TypeRole('Text field base', KasyTextTheme.textFieldBase, 'Regular · 16/24'),
702
- _TypeRole('Text field sm', KasyTextTheme.textFieldSm, 'Regular · 14/20'),
703
- _TypeRole('Button base', KasyTextTheme.buttonBase, 'Medium · 16/24'),
704
- _TypeRole('Button sm', KasyTextTheme.buttonSm, 'Medium · 14/20'),
705
- ];
723
+ final double width = MediaQuery.sizeOf(context).width;
724
+ final int index = _index ?? _defaultIndexFor(width);
725
+ final _Breakpoint bp = _breakpoints[index];
706
726
 
707
727
  return Column(
708
728
  crossAxisAlignment: CrossAxisAlignment.stretch,
709
729
  children: [
710
730
  Text(
711
- "Based on Tailwind's scale: Inter, neutral tracking and predictable "
712
- 'rhythm. Functional and scalable rather than decorative.',
731
+ 'Inter, neutral tracking, predictable rhythm. Each role has an '
732
+ 'explicit size per breakpoint: headings step down from desktop to '
733
+ 'mobile, while body and labels stay constant for stable reading.',
713
734
  style: context.textTheme.bodyMedium?.copyWith(
714
735
  color: context.colors.muted,
715
736
  height: 1.5,
716
737
  ),
717
738
  ),
718
739
  const SizedBox(height: KasySpacing.md),
740
+ KasyTabs(
741
+ tabs: [for (final b in _breakpoints) b.label],
742
+ selectedIndex: index,
743
+ mode: KasyTabsMode.fill,
744
+ onTabSelected: (i) => setState(() => _index = i),
745
+ ),
746
+ const SizedBox(height: KasySpacing.sm),
747
+ Text(
748
+ '${bp.label} · ${bp.range}',
749
+ style: context.textTheme.bodySmall?.copyWith(
750
+ color: context.colors.muted,
751
+ fontFeatures: const [FontFeature.tabularFigures()],
752
+ ),
753
+ ),
754
+ const SizedBox(height: KasySpacing.md),
719
755
  _TokenCard(
720
756
  children: [
721
- for (int i = 0; i < roles.length; i++) ...[
722
- _TypeRow(role: roles[i], foreground: fg),
723
- if (i < roles.length - 1) const _TokenDivider(),
757
+ for (int i = 0; i < _roles.length; i++) ...[
758
+ _TypeRow(role: _roles[i], foreground: fg, device: bp.device),
759
+ if (i < _roles.length - 1) const _TokenDivider(),
724
760
  ],
725
761
  ],
726
762
  ),
@@ -729,20 +765,45 @@ class _TypographySection extends StatelessWidget {
729
765
  }
730
766
  }
731
767
 
768
+ /// Formats a size: integers stay integer (16), fractions show one decimal (17.0).
769
+ String _fmt(double v) =>
770
+ v == v.roundToDouble() ? v.toInt().toString() : v.toStringAsFixed(1);
771
+
732
772
  class _TypeRole {
733
773
  final String name;
734
- final TextStyle style;
735
- final String spec;
736
- const _TypeRole(this.name, this.style, this.spec);
774
+ final RampSize ramp;
775
+ final FontWeight weight;
776
+
777
+ /// Human-readable weight (and decoration) description for the spec line.
778
+ final String label;
779
+ final bool underline;
780
+ const _TypeRole(
781
+ this.name,
782
+ this.ramp,
783
+ this.weight,
784
+ this.label, {
785
+ this.underline = false,
786
+ });
737
787
  }
738
788
 
739
789
  class _TypeRow extends StatelessWidget {
740
790
  final _TypeRole role;
741
791
  final Color foreground;
742
- const _TypeRow({required this.role, required this.foreground});
792
+
793
+ /// The breakpoint whose size this row previews.
794
+ final DeviceType device;
795
+
796
+ const _TypeRow({
797
+ required this.role,
798
+ required this.foreground,
799
+ required this.device,
800
+ });
743
801
 
744
802
  @override
745
803
  Widget build(BuildContext context) {
804
+ final double size = role.ramp.size(device);
805
+ final double line = role.ramp.lineHeight(device);
806
+
746
807
  return Padding(
747
808
  padding: const EdgeInsets.symmetric(
748
809
  horizontal: KasySpacing.md,
@@ -753,13 +814,20 @@ class _TypeRow extends StatelessWidget {
753
814
  children: [
754
815
  Text(
755
816
  role.name,
756
- style: role.style.copyWith(color: foreground),
817
+ style: GoogleFonts.inter(
818
+ fontSize: size,
819
+ fontWeight: role.weight,
820
+ height: role.ramp.lineHeightRatio,
821
+ letterSpacing: 0,
822
+ color: foreground,
823
+ decoration: role.underline ? TextDecoration.underline : null,
824
+ ),
757
825
  maxLines: 1,
758
826
  overflow: TextOverflow.ellipsis,
759
827
  ),
760
828
  const SizedBox(height: 4),
761
829
  Text(
762
- 'Inter · ${role.spec}',
830
+ 'Inter · ${role.label} · ${_fmt(size)}/${_fmt(line)}',
763
831
  style: context.textTheme.bodySmall?.copyWith(
764
832
  color: context.colors.muted,
765
833
  fontFeatures: const [FontFeature.tabularFigures()],
@@ -1054,9 +1122,8 @@ class _EffectGroupLabel extends StatelessWidget {
1054
1122
  Widget build(BuildContext context) {
1055
1123
  return Text(
1056
1124
  label,
1057
- style: context.textTheme.titleSmall?.copyWith(
1125
+ style: context.kasyTextTheme.sectionTitle.copyWith(
1058
1126
  color: context.colors.onSurface,
1059
- fontWeight: FontWeight.w700,
1060
1127
  ),
1061
1128
  );
1062
1129
  }
@@ -91,7 +91,14 @@ class HomeComponentsPage extends StatelessWidget {
91
91
  Flexible(
92
92
  child: Text(
93
93
  row.name,
94
- style: context.textTheme.titleMedium,
94
+ // Lighter weight (w500) to match the
95
+ // app's row/list scale instead of the
96
+ // heavier heading weight.
97
+ style: context.kasyTextTheme.rowTitle
98
+ .copyWith(
99
+ color:
100
+ context.colors.onSurface,
101
+ ),
95
102
  ),
96
103
  ),
97
104
  if (_kReadyComponents.contains(
@@ -142,9 +142,7 @@ class _HomeComponentsPreviewPageState extends State<HomeComponentsPreviewPage>
142
142
  Widget _buildFooterVariantTitle(BuildContext context) {
143
143
  final Color labelBase = context.colors.onBackground;
144
144
  final TextStyle baseStyle =
145
- context.textTheme.bodyMedium?.copyWith(
146
- fontWeight: FontWeight.w600,
147
- fontSize: 17,
145
+ context.textTheme.titleMedium?.copyWith(
148
146
  letterSpacing: 0.1,
149
147
  ) ??
150
148
  const TextStyle(