kasy-cli 1.34.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 (141) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +24 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/new.js +11 -9
  5. package/lib/commands/release-version.js +234 -0
  6. package/lib/commands/update.js +27 -0
  7. package/lib/scaffold/CHANGELOG.json +9 -0
  8. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  9. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  10. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  11. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  12. package/lib/scaffold/backends/firebase/setup-from-scratch.js +35 -21
  13. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  14. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  15. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  16. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  17. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +82 -3
  18. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  19. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  20. package/lib/scaffold/generate.js +53 -4
  21. package/lib/utils/i18n/messages-en.js +23 -0
  22. package/lib/utils/i18n/messages-es.js +23 -0
  23. package/lib/utils/i18n/messages-pt.js +23 -0
  24. package/package.json +5 -2
  25. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  26. package/templates/firebase/AGENTS.md +83 -0
  27. package/templates/firebase/DESIGN_SYSTEM.md +37 -2
  28. package/templates/firebase/docs/auth-setup.en.md +2 -0
  29. package/templates/firebase/docs/auth-setup.es.md +2 -0
  30. package/templates/firebase/docs/auth-setup.pt.md +2 -0
  31. package/templates/firebase/firebase.json +56 -1
  32. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  33. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  34. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  35. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  36. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  37. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  38. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  39. package/templates/firebase/lib/components/kasy_app_bar.dart +31 -16
  40. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  41. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  42. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  43. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  44. package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
  45. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  46. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  47. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  48. package/templates/firebase/lib/components/kasy_toast.dart +107 -41
  49. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  50. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  51. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  52. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  53. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  54. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  55. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  56. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  57. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  58. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  59. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  60. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +5 -3
  61. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  62. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  63. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  64. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  65. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  66. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  67. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  68. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  69. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  70. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  71. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  72. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  73. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  74. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  75. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  76. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  77. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +1 -2
  78. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  79. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  80. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  81. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  82. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  83. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +205 -0
  84. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  85. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  86. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  87. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  88. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  89. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  90. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  91. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  92. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  93. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +59 -0
  94. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  95. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  96. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  97. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  98. package/templates/firebase/lib/features/home/home_components_page.dart +4 -3
  99. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  100. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +154 -56
  101. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  102. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +8 -3
  103. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  104. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -1
  105. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +8 -3
  106. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  107. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  108. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  109. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  110. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +43 -15
  111. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  112. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  113. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  114. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  115. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  116. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  117. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  118. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  119. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  120. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  121. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  122. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  123. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  124. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  125. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  126. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  127. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  128. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  129. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  130. package/templates/firebase/lib/i18n/en.i18n.json +49 -3
  131. package/templates/firebase/lib/i18n/es.i18n.json +49 -3
  132. package/templates/firebase/lib/i18n/pt.i18n.json +49 -3
  133. package/templates/firebase/lib/main.dart +11 -2
  134. package/templates/firebase/lib/router.dart +92 -13
  135. package/templates/firebase/pubspec.yaml +1 -1
  136. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  137. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  138. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  139. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  140. package/templates/firebase/web/index.html +162 -14
  141. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
@@ -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,
@@ -3,6 +3,7 @@ import 'dart:async';
3
3
  import 'package:bart/bart/bart_model.dart';
4
4
  import 'package:bart/bart/widgets/side_bar/custom_sidebar.dart';
5
5
  import 'package:flutter/material.dart';
6
+ import 'package:kasy_kit/components/kasy_app_bar.dart';
6
7
  import 'package:kasy_kit/components/kasy_avatar.dart';
7
8
  import 'package:kasy_kit/components/kasy_avatar_presets.dart';
8
9
  import 'package:kasy_kit/core/theme/theme.dart';
@@ -15,8 +16,11 @@ import 'package:kasy_kit/i18n/translations.g.dart';
15
16
 
16
17
  // Figma sidebar is 223 wide; we run a touch wider for breathing room.
17
18
  const double _kWidthOpen = 248.0;
18
- const double _kWidthCollapsed = 78.0;
19
+ const double _kWidthCollapsed = 64.0;
19
20
  const double _kPadH = 16.0; // px-4
21
+ // Tighter horizontal gutter for the narrow collapsed rail, so a 64px rail keeps
22
+ // the 20px icons (44px active pill) centered without clipping.
23
+ const double _kCollapsedPadH = 10.0;
20
24
  const double _kPadBottom = 16.0; // pb-4
21
25
  // Top band that holds the logo. Equals the web header height (kasyWebHeaderHeight
22
26
  // = 68) so the sidebar's first divider lines up with the header's bottom border.
@@ -203,6 +207,7 @@ class KasySidebar extends StatefulWidget {
203
207
  this.onSettingsTap,
204
208
  this.onLogout,
205
209
  this.initiallyCollapsed = false,
210
+ this.isDrawer = false,
206
211
  this.side = KasySidebarSide.left,
207
212
  this.routes,
208
213
  this.onTapItem,
@@ -250,6 +255,11 @@ class KasySidebar extends StatefulWidget {
250
255
  /// Whether the sidebar starts in the narrow (icon-only) mode.
251
256
  final bool initiallyCollapsed;
252
257
 
258
+ /// Present the rail as a slide-in drawer (the mobile pattern): it always opens
259
+ /// wide and hides the collapse toggle, regardless of viewport width. Use when
260
+ /// opening the sidebar from an app-bar menu button on a phone.
261
+ final bool isDrawer;
262
+
253
263
  /// The screen edge this sidebar is anchored to.
254
264
  final KasySidebarSide side;
255
265
 
@@ -269,11 +279,14 @@ class KasySidebar extends StatefulWidget {
269
279
  }
270
280
 
271
281
  class _KasySidebarState extends State<KasySidebar> {
272
- // User's explicit open/close preference (set by tapping the toggle button).
273
- late bool _userChoseCollapsed;
274
-
275
- // Computed at the start of build() true when user chose narrow OR viewport
276
- // is below the breakpoint. All submethods read this field directly.
282
+ // User's explicit open/close preference, set by tapping the toggle button.
283
+ // Null until they touch it, so the rail follows the viewport (auto-collapse on
284
+ // narrow). Once set, the explicit choice wins over the viewport — which is
285
+ // what lets a narrow-viewport rail be reopened by tapping the toggle.
286
+ bool? _collapsePreference;
287
+
288
+ // Computed at the start of build() — the user's explicit preference when set,
289
+ // otherwise the viewport auto-collapse. All submethods read this field.
277
290
  bool _collapsed = false;
278
291
 
279
292
  bool _incomeExpanded = false;
@@ -286,6 +299,19 @@ class _KasySidebarState extends State<KasySidebar> {
286
299
  /// Viewport width below which the sidebar auto-collapses (tablet breakpoint).
287
300
  static const double _kBreakpoint = 1024.0;
288
301
 
302
+ /// Below this (mobile), the rail is meant to be a wide drawer: it always opens
303
+ /// full width and hides the collapse toggle (thinning makes no sense on a
304
+ /// phone-width sheet). The collapse affordance only exists from tablet up.
305
+ static const double _kMobileBreakpoint = 768.0;
306
+
307
+ bool _isMobile(BuildContext context) =>
308
+ MediaQuery.sizeOf(context).width < _kMobileBreakpoint;
309
+
310
+ /// Drawer presentation — always wide, no collapse toggle. True on a phone-
311
+ /// width viewport or when explicitly opened as a drawer ([isDrawer]).
312
+ bool _wideDrawer(BuildContext context) =>
313
+ widget.isDrawer || _isMobile(context);
314
+
289
315
  /// True when wired to Bart's navigation (real, tappable screens).
290
316
  bool get _connected =>
291
317
  widget.routes != null &&
@@ -296,7 +322,7 @@ class _KasySidebarState extends State<KasySidebar> {
296
322
  @override
297
323
  void initState() {
298
324
  super.initState();
299
- _userChoseCollapsed = widget.initiallyCollapsed;
325
+ _collapsePreference = widget.initiallyCollapsed ? true : null;
300
326
  // Connected mode follows Bart's currentItem (empty highlight here); the
301
327
  // showcase defaults to the active layer from the Figma reference.
302
328
  _activeItemId = _connected ? '' : 'object2';
@@ -305,13 +331,19 @@ class _KasySidebarState extends State<KasySidebar> {
305
331
  bool _isViewportNarrow(BuildContext context) =>
306
332
  MediaQuery.sizeOf(context).width < _kBreakpoint;
307
333
 
334
+ /// Horizontal gutter for the rail content — tighter when collapsed so the
335
+ /// icon rail stays narrow while keeping the icons centered.
336
+ double get _railPadH => _collapsed ? _kCollapsedPadH : _kPadH;
337
+
308
338
  _SidebarColors get _colors => _SidebarColors.fromContext(context);
309
339
 
310
340
  // ── Actions ───────────────────────────────────────────────────────────────
311
341
 
312
- // Only the user's preference toggles never the viewport-forced state.
342
+ // Flip the currently visible state and pin it as the explicit preference, so
343
+ // it overrides the viewport auto-collapse — this is what lets a narrow-
344
+ // viewport rail be expanded back open from the toggle.
313
345
  void _toggleCollapse() =>
314
- setState(() => _userChoseCollapsed = !_userChoseCollapsed);
346
+ setState(() => _collapsePreference = !_collapsed);
315
347
 
316
348
  /// Navigates to a real route via Bart and clears any static-item highlight.
317
349
  /// Moving to another screen also collapses an open submenu (e.g. Income) and
@@ -347,7 +379,10 @@ class _KasySidebarState extends State<KasySidebar> {
347
379
 
348
380
  @override
349
381
  Widget build(BuildContext context) {
350
- _collapsed = _userChoseCollapsed || _isViewportNarrow(context);
382
+ // A drawer / phone-width rail always opens wide; otherwise honour the user's
383
+ // explicit choice, falling back to the tablet auto-collapse.
384
+ _collapsed = !_wideDrawer(context) &&
385
+ (_collapsePreference ?? _isViewportNarrow(context));
351
386
 
352
387
  final c = _colors;
353
388
  final bool anchoredLeft = widget.side == KasySidebarSide.left;
@@ -409,11 +444,15 @@ class _KasySidebarState extends State<KasySidebar> {
409
444
  // Nav: workspace selector + segmented tabs + the layers list.
410
445
  Expanded(
411
446
  child: Padding(
412
- padding: const EdgeInsets.fromLTRB(_kPadH, _kDividerGap, _kPadH, 0),
447
+ padding: EdgeInsets.symmetric(horizontal: _railPadH),
413
448
  child: SingleChildScrollView(
414
449
  child: Column(
415
450
  crossAxisAlignment: CrossAxisAlignment.start,
416
451
  children: [
452
+ // Top gap lives INSIDE the scroll view so the list scrolls
453
+ // flush under the top divider (symmetric with the bottom),
454
+ // instead of clipping a gap below it.
455
+ const SizedBox(height: _kDividerGap),
417
456
  if (!_collapsed) ...[
418
457
  _buildWorkspaceSelector(c),
419
458
  const SizedBox(height: _kHeaderGap),
@@ -430,10 +469,10 @@ class _KasySidebarState extends State<KasySidebar> {
430
469
  // Pinned ⌘K search row + profile block.
431
470
  _buildDivider(c),
432
471
  Padding(
433
- padding: const EdgeInsets.fromLTRB(
434
- _kPadH,
472
+ padding: EdgeInsets.fromLTRB(
473
+ _railPadH,
435
474
  _kFooterGap,
436
- _kPadH,
475
+ _railPadH,
437
476
  _kPadBottom,
438
477
  ),
439
478
  child: Column(
@@ -481,11 +520,15 @@ class _KasySidebarState extends State<KasySidebar> {
481
520
  _buildDivider(c),
482
521
  Expanded(
483
522
  child: Padding(
484
- padding: const EdgeInsets.fromLTRB(_kPadH, _kDividerGap, _kPadH, 0),
523
+ padding: EdgeInsets.symmetric(horizontal: _railPadH),
485
524
  child: SingleChildScrollView(
486
525
  child: Column(
487
526
  crossAxisAlignment: CrossAxisAlignment.start,
488
527
  children: [
528
+ // Top gap lives INSIDE the scroll view so the list scrolls
529
+ // flush under the top divider (symmetric with the bottom),
530
+ // instead of clipping a gap below it.
531
+ const SizedBox(height: _kDividerGap),
489
532
  if (!_collapsed) ...[
490
533
  _buildSectionLabel('MAIN', c),
491
534
  const SizedBox(height: _kItemGap),
@@ -530,7 +573,7 @@ class _KasySidebarState extends State<KasySidebar> {
530
573
  _buildDivider(c),
531
574
  const SizedBox(height: _kFooterGap),
532
575
  Padding(
533
- padding: const EdgeInsets.fromLTRB(_kPadH, 0, _kPadH, _kPadBottom),
576
+ padding: EdgeInsets.fromLTRB(_railPadH, 0, _railPadH, _kPadBottom),
534
577
  child: Column(
535
578
  crossAxisAlignment: CrossAxisAlignment.start,
536
579
  children: [
@@ -559,25 +602,40 @@ class _KasySidebarState extends State<KasySidebar> {
559
602
  /// header height) so the divider underneath lines up with the header's bottom
560
603
  /// border. Content is vertically centred, mirroring the header's toolbar row.
561
604
  Widget _buildTopBand(_SidebarColors c) {
605
+ // Keep the first divider on the same line as the content chrome's bottom
606
+ // border: the web header (68) on desktop, but the shorter KasyAppBar on
607
+ // tablet (medium), where the page keeps its own app bar instead of the
608
+ // header. Without this the line breaks between the rail and the app bar.
609
+ final double bandHeight =
610
+ MediaQuery.sizeOf(context).width >= _kBreakpoint
611
+ ? _kTopBandHeight
612
+ : kasyAppBarBodyTopOverlap(context);
613
+ // No collapse toggle on the mobile / drawer rail (it always opens wide).
614
+ final bool showToggle = !_wideDrawer(context);
615
+ final bool anchoredLeft = widget.side == KasySidebarSide.left;
616
+ // Brand wordmark — same artwork as the splash screen.
617
+ final Widget logo = Image.asset(
618
+ c.isDark
619
+ ? 'assets/images/logo_wordmark_dark.png'
620
+ : 'assets/images/logo_wordmark_light.png',
621
+ height: 32,
622
+ fit: BoxFit.contain,
623
+ );
624
+ // Left rail: wordmark then toggle (toggle hugs the content edge). The right
625
+ // rail mirrors it so the toggle still hugs the content edge (now the left).
626
+ final List<Widget> rowChildren = <Widget>[
627
+ logo,
628
+ if (showToggle) ...[const Spacer(), _buildToggleButton(c)],
629
+ ];
562
630
  return Padding(
563
- padding: const EdgeInsets.symmetric(horizontal: _kPadH),
631
+ padding: EdgeInsets.symmetric(horizontal: _railPadH),
564
632
  child: SizedBox(
565
- height: _kTopBandHeight,
633
+ height: bandHeight,
566
634
  child: _collapsed
567
635
  ? Center(child: _buildToggleButton(c))
568
636
  : Row(
569
- children: [
570
- // Brand wordmark same artwork as the splash screen.
571
- Image.asset(
572
- c.isDark
573
- ? 'assets/images/logo_wordmark_dark.png'
574
- : 'assets/images/logo_wordmark_light.png',
575
- height: 32,
576
- fit: BoxFit.contain,
577
- ),
578
- const Spacer(),
579
- _buildToggleButton(c),
580
- ],
637
+ children:
638
+ anchoredLeft ? rowChildren : rowChildren.reversed.toList(),
581
639
  ),
582
640
  ),
583
641
  );
@@ -618,10 +676,7 @@ class _KasySidebarState extends State<KasySidebar> {
618
676
  '3D Dog Character',
619
677
  maxLines: 1,
620
678
  overflow: TextOverflow.ellipsis,
621
- style: TextStyle(
622
- fontSize: 14,
623
- height: 20 / 14,
624
- fontWeight: FontWeight.w500,
679
+ style: context.kasyTextTheme.rowTitle.copyWith(
625
680
  color: c.textActive,
626
681
  ),
627
682
  ),
@@ -635,10 +690,7 @@ class _KasySidebarState extends State<KasySidebar> {
635
690
  '3D Design Project',
636
691
  maxLines: 1,
637
692
  overflow: TextOverflow.ellipsis,
638
- style: TextStyle(
639
- fontSize: 12,
640
- height: 16 / 12,
641
- fontWeight: FontWeight.w400,
693
+ style: context.kasyTextTheme.cardSubtitle.copyWith(
642
694
  color: c.textMuted,
643
695
  ),
644
696
  ),
@@ -694,10 +746,7 @@ class _KasySidebarState extends State<KasySidebar> {
694
746
  ),
695
747
  child: Text(
696
748
  label,
697
- style: TextStyle(
698
- fontSize: 14,
699
- height: 20 / 14,
700
- fontWeight: FontWeight.w500,
749
+ style: context.kasyTextTheme.rowTitle.copyWith(
701
750
  color: selected ? c.textActive : c.textMuted,
702
751
  ),
703
752
  ),
@@ -710,10 +759,7 @@ class _KasySidebarState extends State<KasySidebar> {
710
759
  // ── ⌘K keyboard chip ─────────────────────────────────────────────────────────
711
760
 
712
761
  Widget _buildKbd(_SidebarColors c) {
713
- final TextStyle style = TextStyle(
714
- fontSize: 14,
715
- height: 20 / 14,
716
- fontWeight: FontWeight.w500,
762
+ final TextStyle style = context.kasyTextTheme.rowTitle.copyWith(
717
763
  color: c.textMuted,
718
764
  );
719
765
  return Container(
@@ -774,10 +820,7 @@ class _KasySidebarState extends State<KasySidebar> {
774
820
  widget.profileName,
775
821
  maxLines: 1,
776
822
  overflow: TextOverflow.ellipsis,
777
- style: TextStyle(
778
- fontSize: 14,
779
- height: 20 / 14,
780
- fontWeight: FontWeight.w500,
823
+ style: context.kasyTextTheme.rowTitle.copyWith(
781
824
  color: c.textActive,
782
825
  ),
783
826
  ),
@@ -785,10 +828,7 @@ class _KasySidebarState extends State<KasySidebar> {
785
828
  widget.profileEmail,
786
829
  maxLines: 1,
787
830
  overflow: TextOverflow.ellipsis,
788
- style: TextStyle(
789
- fontSize: 12,
790
- height: 16 / 12,
791
- fontWeight: FontWeight.w500,
831
+ style: context.textTheme.labelMedium?.copyWith(
792
832
  color: c.textMuted,
793
833
  ),
794
834
  ),
@@ -816,11 +856,8 @@ class _KasySidebarState extends State<KasySidebar> {
816
856
  padding: const EdgeInsets.only(left: _kItemHPad),
817
857
  child: Text(
818
858
  label,
819
- style: TextStyle(
820
- fontSize: 11,
821
- fontWeight: FontWeight.w600,
859
+ style: context.kasyTextTheme.sectionLabel.copyWith(
822
860
  color: c.textMuted,
823
- letterSpacing: 0.6,
824
861
  ),
825
862
  ),
826
863
  );
@@ -913,6 +950,7 @@ class _KasySidebarState extends State<KasySidebar> {
913
950
  iconColor: iconColor,
914
951
  activeBg: c.activeBg,
915
952
  colors: c,
953
+ anchoredLeft: widget.side == KasySidebarSide.left,
916
954
  onTap: onTap,
917
955
  ),
918
956
  ),
@@ -952,10 +990,7 @@ class _KasySidebarState extends State<KasySidebar> {
952
990
  label,
953
991
  maxLines: 1,
954
992
  overflow: TextOverflow.ellipsis,
955
- style: TextStyle(
956
- fontSize: 14,
957
- height: 20 / 14,
958
- fontWeight: FontWeight.w500,
993
+ style: context.kasyTextTheme.rowTitle.copyWith(
959
994
  color: labelColor,
960
995
  ),
961
996
  ),
@@ -989,6 +1024,7 @@ class _KasySidebarState extends State<KasySidebar> {
989
1024
  subItems: item.subItems,
990
1025
  activeSubItem: _activeSubItem,
991
1026
  colors: c,
1027
+ anchoredLeft: widget.side == KasySidebarSide.left,
992
1028
  onSubItemTap: _activateSubItem,
993
1029
  ),
994
1030
  );
@@ -1021,10 +1057,7 @@ class _KasySidebarState extends State<KasySidebar> {
1021
1057
  Expanded(
1022
1058
  child: Text(
1023
1059
  item.label,
1024
- style: TextStyle(
1025
- fontSize: 14,
1026
- height: 20 / 14,
1027
- fontWeight: FontWeight.w500,
1060
+ style: context.kasyTextTheme.rowTitle.copyWith(
1028
1061
  color: c.textActive,
1029
1062
  ),
1030
1063
  ),
@@ -1141,8 +1174,7 @@ class _KasySidebarState extends State<KasySidebar> {
1141
1174
  alignment: Alignment.centerLeft,
1142
1175
  child: Text(
1143
1176
  label,
1144
- style: TextStyle(
1145
- fontSize: 12,
1177
+ style: context.textTheme.labelMedium?.copyWith(
1146
1178
  fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
1147
1179
  color: textColor,
1148
1180
  letterSpacing: -0.24,
@@ -1169,6 +1201,7 @@ class _ProHoverPopupIcon extends StatefulWidget {
1169
1201
  required this.subItems,
1170
1202
  required this.activeSubItem,
1171
1203
  required this.colors,
1204
+ required this.anchoredLeft,
1172
1205
  required this.onSubItemTap,
1173
1206
  });
1174
1207
 
@@ -1178,6 +1211,12 @@ class _ProHoverPopupIcon extends StatefulWidget {
1178
1211
  final List<String> subItems;
1179
1212
  final String activeSubItem;
1180
1213
  final _SidebarColors colors;
1214
+
1215
+ /// Whether the rail is on the left. The popup opens toward the content side
1216
+ /// (right of the icon on a left rail, left of the icon on a right rail) so it
1217
+ /// never spills off the screen edge.
1218
+ final bool anchoredLeft;
1219
+
1181
1220
  final ValueChanged<String> onSubItemTap;
1182
1221
 
1183
1222
  @override
@@ -1236,14 +1275,15 @@ class _ProHoverPopupIconState extends State<_ProHoverPopupIcon> {
1236
1275
  }
1237
1276
 
1238
1277
  Widget _buildPopup(BuildContext context) {
1278
+ final bool left = widget.anchoredLeft;
1239
1279
  return CompositedTransformFollower(
1240
1280
  link: _layerLink,
1241
1281
  showWhenUnlinked: false,
1242
- targetAnchor: Alignment.centerRight,
1243
- followerAnchor: Alignment.centerLeft,
1244
- offset: const Offset(12, 0),
1282
+ targetAnchor: left ? Alignment.centerRight : Alignment.centerLeft,
1283
+ followerAnchor: left ? Alignment.centerLeft : Alignment.centerRight,
1284
+ offset: Offset(left ? 12 : -12, 0),
1245
1285
  child: Align(
1246
- alignment: Alignment.centerLeft,
1286
+ alignment: left ? Alignment.centerLeft : Alignment.centerRight,
1247
1287
  child: MouseRegion(
1248
1288
  onEnter: (_) => setState(() => _onPopup = true),
1249
1289
  onExit: (_) {
@@ -1292,8 +1332,7 @@ class _ProHoverPopupIconState extends State<_ProHoverPopupIcon> {
1292
1332
  ),
1293
1333
  child: Text(
1294
1334
  label,
1295
- style: TextStyle(
1296
- fontSize: 12,
1335
+ style: context.textTheme.labelMedium?.copyWith(
1297
1336
  fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
1298
1337
  color: textColor,
1299
1338
  letterSpacing: -0.24,
@@ -1320,6 +1359,7 @@ class _ProTooltipIcon extends StatefulWidget {
1320
1359
  required this.iconColor,
1321
1360
  required this.activeBg,
1322
1361
  required this.colors,
1362
+ required this.anchoredLeft,
1323
1363
  required this.onTap,
1324
1364
  });
1325
1365
 
@@ -1329,6 +1369,12 @@ class _ProTooltipIcon extends StatefulWidget {
1329
1369
  final Color iconColor;
1330
1370
  final Color activeBg;
1331
1371
  final _SidebarColors colors;
1372
+
1373
+ /// Whether the rail is on the left. The tooltip opens toward the content side
1374
+ /// (and its arrow points back at the icon) so it never spills off the screen
1375
+ /// edge on a right-anchored rail.
1376
+ final bool anchoredLeft;
1377
+
1332
1378
  final VoidCallback onTap;
1333
1379
 
1334
1380
  @override
@@ -1374,15 +1420,20 @@ class _ProTooltipIconState extends State<_ProTooltipIcon> {
1374
1420
  }
1375
1421
 
1376
1422
  Widget _buildTooltip(BuildContext context) {
1423
+ final bool left = widget.anchoredLeft;
1377
1424
  return CompositedTransformFollower(
1378
1425
  link: _layerLink,
1379
1426
  showWhenUnlinked: false,
1380
- targetAnchor: Alignment.centerRight,
1381
- followerAnchor: Alignment.centerLeft,
1382
- offset: const Offset(4, 0),
1427
+ targetAnchor: left ? Alignment.centerRight : Alignment.centerLeft,
1428
+ followerAnchor: left ? Alignment.centerLeft : Alignment.centerRight,
1429
+ offset: Offset(left ? 4 : -4, 0),
1383
1430
  child: Align(
1384
- alignment: Alignment.centerLeft,
1385
- child: _TooltipCard(label: widget.label, colors: widget.colors),
1431
+ alignment: left ? Alignment.centerLeft : Alignment.centerRight,
1432
+ child: _TooltipCard(
1433
+ label: widget.label,
1434
+ colors: widget.colors,
1435
+ pointsLeft: left,
1436
+ ),
1386
1437
  ),
1387
1438
  );
1388
1439
  }
@@ -1393,11 +1444,19 @@ class _ProTooltipIconState extends State<_ProTooltipIcon> {
1393
1444
  // ─────────────────────────────────────────────────────────────────────────────
1394
1445
 
1395
1446
  class _TooltipCard extends StatelessWidget {
1396
- const _TooltipCard({required this.label, required this.colors});
1447
+ const _TooltipCard({
1448
+ required this.label,
1449
+ required this.colors,
1450
+ this.pointsLeft = true,
1451
+ });
1397
1452
 
1398
1453
  final String label;
1399
1454
  final _SidebarColors colors;
1400
1455
 
1456
+ /// Arrow points back at the icon: left when the rail is on the left (tooltip
1457
+ /// sits to the icon's right), right when the rail is on the right.
1458
+ final bool pointsLeft;
1459
+
1401
1460
  static const double _arrowW = 13.0;
1402
1461
  static const double _arrowH = 26.0;
1403
1462
  static const double _arrowOverlap = 8.0;
@@ -1411,43 +1470,44 @@ class _TooltipCard extends StatelessWidget {
1411
1470
  final Color bg = colors.isDark ? colors.divider : colors.textActive;
1412
1471
  final Color textColor = colors.isDark ? colors.textActive : colors.bg;
1413
1472
 
1473
+ final Widget arrow = SizedBox(
1474
+ width: _arrowW,
1475
+ height: _arrowH,
1476
+ child: CustomPaint(
1477
+ painter: _TooltipArrowPainter(color: bg, pointsLeft: pointsLeft),
1478
+ ),
1479
+ );
1480
+ // Pull the card over the arrow's base so they read as one shape; the
1481
+ // direction of the overlap flips with the arrow side.
1482
+ final Widget card = Transform.translate(
1483
+ offset: Offset(pointsLeft ? -_arrowOverlap : _arrowOverlap, 0),
1484
+ child: Container(
1485
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
1486
+ decoration: BoxDecoration(
1487
+ color: bg,
1488
+ borderRadius: BorderRadius.circular(4),
1489
+ boxShadow: const [
1490
+ BoxShadow(
1491
+ color: Color(0x40000000),
1492
+ blurRadius: 5,
1493
+ offset: Offset(0, 5),
1494
+ ),
1495
+ ],
1496
+ ),
1497
+ child: Text(
1498
+ label,
1499
+ style: context.textTheme.bodyMedium?.copyWith(
1500
+ color: textColor,
1501
+ ),
1502
+ ),
1503
+ ),
1504
+ );
1505
+
1414
1506
  return Material(
1415
1507
  color: Colors.transparent,
1416
1508
  child: Row(
1417
1509
  mainAxisSize: MainAxisSize.min,
1418
- children: [
1419
- SizedBox(
1420
- width: _arrowW,
1421
- height: _arrowH,
1422
- child: CustomPaint(painter: _TooltipArrowPainter(color: bg)),
1423
- ),
1424
- Transform.translate(
1425
- offset: const Offset(-_arrowOverlap, 0),
1426
- child: Container(
1427
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
1428
- decoration: BoxDecoration(
1429
- color: bg,
1430
- borderRadius: BorderRadius.circular(4),
1431
- boxShadow: const [
1432
- BoxShadow(
1433
- color: Color(0x40000000),
1434
- blurRadius: 5,
1435
- offset: Offset(0, 5),
1436
- ),
1437
- ],
1438
- ),
1439
- child: Text(
1440
- label,
1441
- style: TextStyle(
1442
- fontSize: 14,
1443
- fontWeight: FontWeight.w400,
1444
- color: textColor,
1445
- height: 20 / 14,
1446
- ),
1447
- ),
1448
- ),
1449
- ),
1450
- ],
1510
+ children: pointsLeft ? <Widget>[arrow, card] : <Widget>[card, arrow],
1451
1511
  ),
1452
1512
  );
1453
1513
  }
@@ -1458,22 +1518,31 @@ class _TooltipCard extends StatelessWidget {
1458
1518
  // ─────────────────────────────────────────────────────────────────────────────
1459
1519
 
1460
1520
  class _TooltipArrowPainter extends CustomPainter {
1461
- const _TooltipArrowPainter({required this.color});
1521
+ const _TooltipArrowPainter({required this.color, this.pointsLeft = true});
1462
1522
  final Color color;
1523
+ final bool pointsLeft;
1463
1524
 
1464
1525
  @override
1465
1526
  void paint(Canvas canvas, Size size) {
1466
1527
  final paint = Paint()
1467
1528
  ..color = color
1468
1529
  ..style = PaintingStyle.fill;
1469
- final path = Path()
1470
- ..moveTo(size.width, 0)
1471
- ..lineTo(0, size.height / 2)
1472
- ..lineTo(size.width, size.height)
1473
- ..close();
1530
+ // Apex on the side it points to; base on the opposite (card) side.
1531
+ final path = pointsLeft
1532
+ ? (Path()
1533
+ ..moveTo(size.width, 0)
1534
+ ..lineTo(0, size.height / 2)
1535
+ ..lineTo(size.width, size.height)
1536
+ ..close())
1537
+ : (Path()
1538
+ ..moveTo(0, 0)
1539
+ ..lineTo(size.width, size.height / 2)
1540
+ ..lineTo(0, size.height)
1541
+ ..close());
1474
1542
  canvas.drawPath(path, paint);
1475
1543
  }
1476
1544
 
1477
1545
  @override
1478
- bool shouldRepaint(_TooltipArrowPainter old) => old.color != color;
1546
+ bool shouldRepaint(_TooltipArrowPainter old) =>
1547
+ old.color != color || old.pointsLeft != pointsLeft;
1479
1548
  }
@@ -222,7 +222,6 @@ class _KasyTextAreaState extends State<KasyTextArea> {
222
222
  color: context.colors.onSurface.withValues(
223
223
  alpha: isDisabled ? disabledTextOpacity : 1,
224
224
  ),
225
- fontWeight: FontWeight.w400,
226
225
  fontSize: 15,
227
226
  );
228
227
 
@@ -489,7 +489,6 @@ class _KasyTextFieldState extends State<KasyTextField> {
489
489
  final TextStyle fieldTextStyle =
490
490
  context.textTheme.bodyLarge?.copyWith(
491
491
  color: fieldTextColor,
492
- fontWeight: FontWeight.w400,
493
492
  fontSize: 15,
494
493
  ) ??
495
494
  TextStyle(fontSize: 15, color: fieldTextColor);
@@ -436,7 +436,6 @@ class _OTPCell extends StatelessWidget {
436
436
  color: enabled
437
437
  ? context.colors.muted.withValues(alpha: 0.64)
438
438
  : context.colors.muted.withValues(alpha: 0.58),
439
- fontWeight: FontWeight.w700,
440
439
  );
441
440
  final String? visiblePlaceholder = digit.isEmpty && placeholder != null
442
441
  ? placeholder