kasy-cli 1.34.0 → 1.36.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 +18 -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 +215 -178
  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 +226 -105
  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
@@ -3,8 +3,10 @@ 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';
9
+ import 'package:kasy_kit/components/kasy_tabs.dart';
8
10
  import 'package:kasy_kit/core/theme/theme.dart';
9
11
  import 'package:kasy_kit/core/widgets/kasy_hover.dart';
10
12
  import 'package:kasy_kit/i18n/translations.g.dart';
@@ -15,8 +17,11 @@ import 'package:kasy_kit/i18n/translations.g.dart';
15
17
 
16
18
  // Figma sidebar is 223 wide; we run a touch wider for breathing room.
17
19
  const double _kWidthOpen = 248.0;
18
- const double _kWidthCollapsed = 78.0;
20
+ const double _kWidthCollapsed = 64.0;
19
21
  const double _kPadH = 16.0; // px-4
22
+ // Tighter horizontal gutter for the narrow collapsed rail, so a 64px rail keeps
23
+ // the 20px icons (44px active pill) centered without clipping.
24
+ const double _kCollapsedPadH = 10.0;
20
25
  const double _kPadBottom = 16.0; // pb-4
21
26
  // Top band that holds the logo. Equals the web header height (kasyWebHeaderHeight
22
27
  // = 68) so the sidebar's first divider lines up with the header's bottom border.
@@ -56,7 +61,6 @@ class _SidebarColors {
56
61
  required this.border,
57
62
  required this.divider,
58
63
  required this.activeBg,
59
- required this.segmentThumb,
60
64
  required this.textMuted,
61
65
  required this.textActive,
62
66
  required this.logout,
@@ -65,9 +69,9 @@ class _SidebarColors {
65
69
 
66
70
  /// Maps the HeroUI Figma tokens onto the global [KasyColors]:
67
71
  /// `surface → surface`, `foreground → onSurface`, `foreground/muted → muted`,
68
- /// `border → border`, `separator → separator`, `default → surfaceNeutralSoft`
69
- /// (hover/active fill + tabs track), and the selected-tab thumb to the
70
- /// dedicated `segment` token.
72
+ /// `border → border`, `separator → separator`, and `default →
73
+ /// surfaceNeutralSoft` (hover/active fill). The segmented control is the
74
+ /// shared [KasyTabs] component, which owns its own selected-thumb token.
71
75
  factory _SidebarColors.fromContext(BuildContext context) {
72
76
  final c = context.colors;
73
77
  final bool dark = context.isDark;
@@ -79,8 +83,6 @@ class _SidebarColors {
79
83
  divider: c.border,
80
84
  // Hover / active item fill + tabs track + kbd chip (default/default).
81
85
  activeBg: c.surfaceNeutralSoft,
82
- // Selected segment thumb (HeroUI `segment`): lifts off the track.
83
- segmentThumb: c.segment,
84
86
  textMuted: c.muted,
85
87
  textActive: c.onSurface,
86
88
  logout: c.error,
@@ -92,7 +94,6 @@ class _SidebarColors {
92
94
  final Color border;
93
95
  final Color divider;
94
96
  final Color activeBg;
95
- final Color segmentThumb;
96
97
  final Color textMuted;
97
98
  final Color textActive;
98
99
  final Color logout;
@@ -203,6 +204,8 @@ class KasySidebar extends StatefulWidget {
203
204
  this.onSettingsTap,
204
205
  this.onLogout,
205
206
  this.initiallyCollapsed = false,
207
+ this.isDrawer = false,
208
+ this.showSearch = false,
206
209
  this.side = KasySidebarSide.left,
207
210
  this.routes,
208
211
  this.onTapItem,
@@ -250,6 +253,16 @@ class KasySidebar extends StatefulWidget {
250
253
  /// Whether the sidebar starts in the narrow (icon-only) mode.
251
254
  final bool initiallyCollapsed;
252
255
 
256
+ /// Present the rail as a slide-in drawer (the mobile pattern): it always opens
257
+ /// wide and hides the collapse toggle, regardless of viewport width. Use when
258
+ /// opening the sidebar from an app-bar menu button on a phone.
259
+ final bool isDrawer;
260
+
261
+ /// Whether to pin a ⌘K search row above the footer in connected mode. Opt-in
262
+ /// (off by default) so the live app navigation stays lean; flip it on when the
263
+ /// sidebar should double as a command/search entry point.
264
+ final bool showSearch;
265
+
253
266
  /// The screen edge this sidebar is anchored to.
254
267
  final KasySidebarSide side;
255
268
 
@@ -269,11 +282,14 @@ class KasySidebar extends StatefulWidget {
269
282
  }
270
283
 
271
284
  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.
285
+ // User's explicit open/close preference, set by tapping the toggle button.
286
+ // Null until they touch it, so the rail follows the viewport (auto-collapse on
287
+ // narrow). Once set, the explicit choice wins over the viewport — which is
288
+ // what lets a narrow-viewport rail be reopened by tapping the toggle.
289
+ bool? _collapsePreference;
290
+
291
+ // Computed at the start of build() — the user's explicit preference when set,
292
+ // otherwise the viewport auto-collapse. All submethods read this field.
277
293
  bool _collapsed = false;
278
294
 
279
295
  bool _incomeExpanded = false;
@@ -286,6 +302,19 @@ class _KasySidebarState extends State<KasySidebar> {
286
302
  /// Viewport width below which the sidebar auto-collapses (tablet breakpoint).
287
303
  static const double _kBreakpoint = 1024.0;
288
304
 
305
+ /// Below this (mobile), the rail is meant to be a wide drawer: it always opens
306
+ /// full width and hides the collapse toggle (thinning makes no sense on a
307
+ /// phone-width sheet). The collapse affordance only exists from tablet up.
308
+ static const double _kMobileBreakpoint = 768.0;
309
+
310
+ bool _isMobile(BuildContext context) =>
311
+ MediaQuery.sizeOf(context).width < _kMobileBreakpoint;
312
+
313
+ /// Drawer presentation — always wide, no collapse toggle. True on a phone-
314
+ /// width viewport or when explicitly opened as a drawer ([isDrawer]).
315
+ bool _wideDrawer(BuildContext context) =>
316
+ widget.isDrawer || _isMobile(context);
317
+
289
318
  /// True when wired to Bart's navigation (real, tappable screens).
290
319
  bool get _connected =>
291
320
  widget.routes != null &&
@@ -296,7 +325,7 @@ class _KasySidebarState extends State<KasySidebar> {
296
325
  @override
297
326
  void initState() {
298
327
  super.initState();
299
- _userChoseCollapsed = widget.initiallyCollapsed;
328
+ _collapsePreference = widget.initiallyCollapsed ? true : null;
300
329
  // Connected mode follows Bart's currentItem (empty highlight here); the
301
330
  // showcase defaults to the active layer from the Figma reference.
302
331
  _activeItemId = _connected ? '' : 'object2';
@@ -305,13 +334,19 @@ class _KasySidebarState extends State<KasySidebar> {
305
334
  bool _isViewportNarrow(BuildContext context) =>
306
335
  MediaQuery.sizeOf(context).width < _kBreakpoint;
307
336
 
337
+ /// Horizontal gutter for the rail content — tighter when collapsed so the
338
+ /// icon rail stays narrow while keeping the icons centered.
339
+ double get _railPadH => _collapsed ? _kCollapsedPadH : _kPadH;
340
+
308
341
  _SidebarColors get _colors => _SidebarColors.fromContext(context);
309
342
 
310
343
  // ── Actions ───────────────────────────────────────────────────────────────
311
344
 
312
- // Only the user's preference toggles never the viewport-forced state.
345
+ // Flip the currently visible state and pin it as the explicit preference, so
346
+ // it overrides the viewport auto-collapse — this is what lets a narrow-
347
+ // viewport rail be expanded back open from the toggle.
313
348
  void _toggleCollapse() =>
314
- setState(() => _userChoseCollapsed = !_userChoseCollapsed);
349
+ setState(() => _collapsePreference = !_collapsed);
315
350
 
316
351
  /// Navigates to a real route via Bart and clears any static-item highlight.
317
352
  /// Moving to another screen also collapses an open submenu (e.g. Income) and
@@ -347,7 +382,10 @@ class _KasySidebarState extends State<KasySidebar> {
347
382
 
348
383
  @override
349
384
  Widget build(BuildContext context) {
350
- _collapsed = _userChoseCollapsed || _isViewportNarrow(context);
385
+ // A drawer / phone-width rail always opens wide; otherwise honour the user's
386
+ // explicit choice, falling back to the tablet auto-collapse.
387
+ _collapsed = !_wideDrawer(context) &&
388
+ (_collapsePreference ?? _isViewportNarrow(context));
351
389
 
352
390
  final c = _colors;
353
391
  final bool anchoredLeft = widget.side == KasySidebarSide.left;
@@ -409,11 +447,15 @@ class _KasySidebarState extends State<KasySidebar> {
409
447
  // Nav: workspace selector + segmented tabs + the layers list.
410
448
  Expanded(
411
449
  child: Padding(
412
- padding: const EdgeInsets.fromLTRB(_kPadH, _kDividerGap, _kPadH, 0),
450
+ padding: EdgeInsets.symmetric(horizontal: _railPadH),
413
451
  child: SingleChildScrollView(
414
452
  child: Column(
415
453
  crossAxisAlignment: CrossAxisAlignment.start,
416
454
  children: [
455
+ // Top gap lives INSIDE the scroll view so the list scrolls
456
+ // flush under the top divider (symmetric with the bottom),
457
+ // instead of clipping a gap below it.
458
+ const SizedBox(height: _kDividerGap),
417
459
  if (!_collapsed) ...[
418
460
  _buildWorkspaceSelector(c),
419
461
  const SizedBox(height: _kHeaderGap),
@@ -430,10 +472,10 @@ class _KasySidebarState extends State<KasySidebar> {
430
472
  // Pinned ⌘K search row + profile block.
431
473
  _buildDivider(c),
432
474
  Padding(
433
- padding: const EdgeInsets.fromLTRB(
434
- _kPadH,
475
+ padding: EdgeInsets.fromLTRB(
476
+ _railPadH,
435
477
  _kFooterGap,
436
- _kPadH,
478
+ _railPadH,
437
479
  _kPadBottom,
438
480
  ),
439
481
  child: Column(
@@ -481,11 +523,15 @@ class _KasySidebarState extends State<KasySidebar> {
481
523
  _buildDivider(c),
482
524
  Expanded(
483
525
  child: Padding(
484
- padding: const EdgeInsets.fromLTRB(_kPadH, _kDividerGap, _kPadH, 0),
526
+ padding: EdgeInsets.symmetric(horizontal: _railPadH),
485
527
  child: SingleChildScrollView(
486
528
  child: Column(
487
529
  crossAxisAlignment: CrossAxisAlignment.start,
488
530
  children: [
531
+ // Top gap lives INSIDE the scroll view so the list scrolls
532
+ // flush under the top divider (symmetric with the bottom),
533
+ // instead of clipping a gap below it.
534
+ const SizedBox(height: _kDividerGap),
489
535
  if (!_collapsed) ...[
490
536
  _buildSectionLabel('MAIN', c),
491
537
  const SizedBox(height: _kItemGap),
@@ -530,10 +576,19 @@ class _KasySidebarState extends State<KasySidebar> {
530
576
  _buildDivider(c),
531
577
  const SizedBox(height: _kFooterGap),
532
578
  Padding(
533
- padding: const EdgeInsets.fromLTRB(_kPadH, 0, _kPadH, _kPadBottom),
579
+ padding: EdgeInsets.fromLTRB(_railPadH, 0, _railPadH, _kPadBottom),
534
580
  child: Column(
535
581
  crossAxisAlignment: CrossAxisAlignment.start,
536
582
  children: [
583
+ if (widget.showSearch)
584
+ _buildItemRow(
585
+ c,
586
+ icon: KasyIcons.search,
587
+ label: 'Search',
588
+ isActive: false,
589
+ onTap: () {},
590
+ trailing: [_buildKbd(c)],
591
+ ),
537
592
  _buildNavItem(context, _kHelpItem, c),
538
593
  _buildItemRow(
539
594
  c,
@@ -559,25 +614,40 @@ class _KasySidebarState extends State<KasySidebar> {
559
614
  /// header height) so the divider underneath lines up with the header's bottom
560
615
  /// border. Content is vertically centred, mirroring the header's toolbar row.
561
616
  Widget _buildTopBand(_SidebarColors c) {
617
+ // Keep the first divider on the same line as the content chrome's bottom
618
+ // border: the web header (68) on desktop, but the shorter KasyAppBar on
619
+ // tablet (medium), where the page keeps its own app bar instead of the
620
+ // header. Without this the line breaks between the rail and the app bar.
621
+ final double bandHeight =
622
+ MediaQuery.sizeOf(context).width >= _kBreakpoint
623
+ ? _kTopBandHeight
624
+ : kasyAppBarBodyTopOverlap(context);
625
+ // No collapse toggle on the mobile / drawer rail (it always opens wide).
626
+ final bool showToggle = !_wideDrawer(context);
627
+ final bool anchoredLeft = widget.side == KasySidebarSide.left;
628
+ // Brand wordmark — same artwork as the splash screen.
629
+ final Widget logo = Image.asset(
630
+ c.isDark
631
+ ? 'assets/images/logo_wordmark_dark.png'
632
+ : 'assets/images/logo_wordmark_light.png',
633
+ height: 32,
634
+ fit: BoxFit.contain,
635
+ );
636
+ // Left rail: wordmark then toggle (toggle hugs the content edge). The right
637
+ // rail mirrors it so the toggle still hugs the content edge (now the left).
638
+ final List<Widget> rowChildren = <Widget>[
639
+ logo,
640
+ if (showToggle) ...[const Spacer(), _buildToggleButton(c)],
641
+ ];
562
642
  return Padding(
563
- padding: const EdgeInsets.symmetric(horizontal: _kPadH),
643
+ padding: EdgeInsets.symmetric(horizontal: _railPadH),
564
644
  child: SizedBox(
565
- height: _kTopBandHeight,
645
+ height: bandHeight,
566
646
  child: _collapsed
567
647
  ? Center(child: _buildToggleButton(c))
568
648
  : 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
- ],
649
+ children:
650
+ anchoredLeft ? rowChildren : rowChildren.reversed.toList(),
581
651
  ),
582
652
  ),
583
653
  );
@@ -618,10 +688,7 @@ class _KasySidebarState extends State<KasySidebar> {
618
688
  '3D Dog Character',
619
689
  maxLines: 1,
620
690
  overflow: TextOverflow.ellipsis,
621
- style: TextStyle(
622
- fontSize: 14,
623
- height: 20 / 14,
624
- fontWeight: FontWeight.w500,
691
+ style: context.kasyTextTheme.rowTitle.copyWith(
625
692
  color: c.textActive,
626
693
  ),
627
694
  ),
@@ -635,10 +702,7 @@ class _KasySidebarState extends State<KasySidebar> {
635
702
  '3D Design Project',
636
703
  maxLines: 1,
637
704
  overflow: TextOverflow.ellipsis,
638
- style: TextStyle(
639
- fontSize: 12,
640
- height: 16 / 12,
641
- fontWeight: FontWeight.w400,
705
+ style: context.kasyTextTheme.cardSubtitle.copyWith(
642
706
  color: c.textMuted,
643
707
  ),
644
708
  ),
@@ -648,72 +712,22 @@ class _KasySidebarState extends State<KasySidebar> {
648
712
 
649
713
  // ── Segmented tabs (Layers / Assets) ────────────────────────────────────────
650
714
 
715
+ /// The showcase segment is the shared [KasyTabs] component (primary pill,
716
+ /// fill mode) so the sidebar demos the real design-system control rather than
717
+ /// a bespoke copy.
651
718
  Widget _buildTabs(_SidebarColors c) {
652
- return Container(
653
- width: double.infinity,
654
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
655
- decoration: BoxDecoration(
656
- color: c.activeBg,
657
- borderRadius: BorderRadius.circular(24),
658
- ),
659
- child: Row(
660
- children: [
661
- _buildTab(c, 'Layers', 0),
662
- const SizedBox(width: 2),
663
- _buildTab(c, 'Assets', 1),
664
- ],
665
- ),
666
- );
667
- }
668
-
669
- Widget _buildTab(_SidebarColors c, String label, int index) {
670
- final bool selected = _showcaseTab == index;
671
- return Expanded(
672
- child: MouseRegion(
673
- cursor: SystemMouseCursors.click,
674
- child: GestureDetector(
675
- behavior: HitTestBehavior.opaque,
676
- onTap: () => setState(() => _showcaseTab = index),
677
- child: AnimatedContainer(
678
- duration: const Duration(milliseconds: 180),
679
- curve: Curves.easeOut,
680
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
681
- alignment: Alignment.center,
682
- decoration: BoxDecoration(
683
- color: selected ? c.segmentThumb : Colors.transparent,
684
- borderRadius: BorderRadius.circular(24),
685
- boxShadow: selected
686
- ? const [
687
- BoxShadow(
688
- color: Color(0x0F000000),
689
- blurRadius: 8,
690
- offset: Offset(0, 2),
691
- ),
692
- ]
693
- : null,
694
- ),
695
- child: Text(
696
- label,
697
- style: TextStyle(
698
- fontSize: 14,
699
- height: 20 / 14,
700
- fontWeight: FontWeight.w500,
701
- color: selected ? c.textActive : c.textMuted,
702
- ),
703
- ),
704
- ),
705
- ),
706
- ),
719
+ return KasyTabs(
720
+ tabs: const ['Layers', 'Assets'],
721
+ selectedIndex: _showcaseTab,
722
+ onTabSelected: (index) => setState(() => _showcaseTab = index),
723
+ mode: KasyTabsMode.fill,
707
724
  );
708
725
  }
709
726
 
710
727
  // ── ⌘K keyboard chip ─────────────────────────────────────────────────────────
711
728
 
712
729
  Widget _buildKbd(_SidebarColors c) {
713
- final TextStyle style = TextStyle(
714
- fontSize: 14,
715
- height: 20 / 14,
716
- fontWeight: FontWeight.w500,
730
+ final TextStyle style = context.kasyTextTheme.rowTitle.copyWith(
717
731
  color: c.textMuted,
718
732
  );
719
733
  return Container(
@@ -774,10 +788,7 @@ class _KasySidebarState extends State<KasySidebar> {
774
788
  widget.profileName,
775
789
  maxLines: 1,
776
790
  overflow: TextOverflow.ellipsis,
777
- style: TextStyle(
778
- fontSize: 14,
779
- height: 20 / 14,
780
- fontWeight: FontWeight.w500,
791
+ style: context.kasyTextTheme.rowTitle.copyWith(
781
792
  color: c.textActive,
782
793
  ),
783
794
  ),
@@ -785,10 +796,7 @@ class _KasySidebarState extends State<KasySidebar> {
785
796
  widget.profileEmail,
786
797
  maxLines: 1,
787
798
  overflow: TextOverflow.ellipsis,
788
- style: TextStyle(
789
- fontSize: 12,
790
- height: 16 / 12,
791
- fontWeight: FontWeight.w500,
799
+ style: context.textTheme.labelMedium?.copyWith(
792
800
  color: c.textMuted,
793
801
  ),
794
802
  ),
@@ -816,11 +824,8 @@ class _KasySidebarState extends State<KasySidebar> {
816
824
  padding: const EdgeInsets.only(left: _kItemHPad),
817
825
  child: Text(
818
826
  label,
819
- style: TextStyle(
820
- fontSize: 11,
821
- fontWeight: FontWeight.w600,
827
+ style: context.kasyTextTheme.sectionLabel.copyWith(
822
828
  color: c.textMuted,
823
- letterSpacing: 0.6,
824
829
  ),
825
830
  ),
826
831
  );
@@ -913,6 +918,7 @@ class _KasySidebarState extends State<KasySidebar> {
913
918
  iconColor: iconColor,
914
919
  activeBg: c.activeBg,
915
920
  colors: c,
921
+ anchoredLeft: widget.side == KasySidebarSide.left,
916
922
  onTap: onTap,
917
923
  ),
918
924
  ),
@@ -952,10 +958,7 @@ class _KasySidebarState extends State<KasySidebar> {
952
958
  label,
953
959
  maxLines: 1,
954
960
  overflow: TextOverflow.ellipsis,
955
- style: TextStyle(
956
- fontSize: 14,
957
- height: 20 / 14,
958
- fontWeight: FontWeight.w500,
961
+ style: context.kasyTextTheme.rowTitle.copyWith(
959
962
  color: labelColor,
960
963
  ),
961
964
  ),
@@ -989,6 +992,7 @@ class _KasySidebarState extends State<KasySidebar> {
989
992
  subItems: item.subItems,
990
993
  activeSubItem: _activeSubItem,
991
994
  colors: c,
995
+ anchoredLeft: widget.side == KasySidebarSide.left,
992
996
  onSubItemTap: _activateSubItem,
993
997
  ),
994
998
  );
@@ -1021,10 +1025,7 @@ class _KasySidebarState extends State<KasySidebar> {
1021
1025
  Expanded(
1022
1026
  child: Text(
1023
1027
  item.label,
1024
- style: TextStyle(
1025
- fontSize: 14,
1026
- height: 20 / 14,
1027
- fontWeight: FontWeight.w500,
1028
+ style: context.kasyTextTheme.rowTitle.copyWith(
1028
1029
  color: c.textActive,
1029
1030
  ),
1030
1031
  ),
@@ -1141,8 +1142,7 @@ class _KasySidebarState extends State<KasySidebar> {
1141
1142
  alignment: Alignment.centerLeft,
1142
1143
  child: Text(
1143
1144
  label,
1144
- style: TextStyle(
1145
- fontSize: 12,
1145
+ style: context.textTheme.labelMedium?.copyWith(
1146
1146
  fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
1147
1147
  color: textColor,
1148
1148
  letterSpacing: -0.24,
@@ -1169,6 +1169,7 @@ class _ProHoverPopupIcon extends StatefulWidget {
1169
1169
  required this.subItems,
1170
1170
  required this.activeSubItem,
1171
1171
  required this.colors,
1172
+ required this.anchoredLeft,
1172
1173
  required this.onSubItemTap,
1173
1174
  });
1174
1175
 
@@ -1178,6 +1179,12 @@ class _ProHoverPopupIcon extends StatefulWidget {
1178
1179
  final List<String> subItems;
1179
1180
  final String activeSubItem;
1180
1181
  final _SidebarColors colors;
1182
+
1183
+ /// Whether the rail is on the left. The popup opens toward the content side
1184
+ /// (right of the icon on a left rail, left of the icon on a right rail) so it
1185
+ /// never spills off the screen edge.
1186
+ final bool anchoredLeft;
1187
+
1181
1188
  final ValueChanged<String> onSubItemTap;
1182
1189
 
1183
1190
  @override
@@ -1236,14 +1243,15 @@ class _ProHoverPopupIconState extends State<_ProHoverPopupIcon> {
1236
1243
  }
1237
1244
 
1238
1245
  Widget _buildPopup(BuildContext context) {
1246
+ final bool left = widget.anchoredLeft;
1239
1247
  return CompositedTransformFollower(
1240
1248
  link: _layerLink,
1241
1249
  showWhenUnlinked: false,
1242
- targetAnchor: Alignment.centerRight,
1243
- followerAnchor: Alignment.centerLeft,
1244
- offset: const Offset(12, 0),
1250
+ targetAnchor: left ? Alignment.centerRight : Alignment.centerLeft,
1251
+ followerAnchor: left ? Alignment.centerLeft : Alignment.centerRight,
1252
+ offset: Offset(left ? 12 : -12, 0),
1245
1253
  child: Align(
1246
- alignment: Alignment.centerLeft,
1254
+ alignment: left ? Alignment.centerLeft : Alignment.centerRight,
1247
1255
  child: MouseRegion(
1248
1256
  onEnter: (_) => setState(() => _onPopup = true),
1249
1257
  onExit: (_) {
@@ -1292,8 +1300,7 @@ class _ProHoverPopupIconState extends State<_ProHoverPopupIcon> {
1292
1300
  ),
1293
1301
  child: Text(
1294
1302
  label,
1295
- style: TextStyle(
1296
- fontSize: 12,
1303
+ style: context.textTheme.labelMedium?.copyWith(
1297
1304
  fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
1298
1305
  color: textColor,
1299
1306
  letterSpacing: -0.24,
@@ -1320,6 +1327,7 @@ class _ProTooltipIcon extends StatefulWidget {
1320
1327
  required this.iconColor,
1321
1328
  required this.activeBg,
1322
1329
  required this.colors,
1330
+ required this.anchoredLeft,
1323
1331
  required this.onTap,
1324
1332
  });
1325
1333
 
@@ -1329,6 +1337,12 @@ class _ProTooltipIcon extends StatefulWidget {
1329
1337
  final Color iconColor;
1330
1338
  final Color activeBg;
1331
1339
  final _SidebarColors colors;
1340
+
1341
+ /// Whether the rail is on the left. The tooltip opens toward the content side
1342
+ /// (and its arrow points back at the icon) so it never spills off the screen
1343
+ /// edge on a right-anchored rail.
1344
+ final bool anchoredLeft;
1345
+
1332
1346
  final VoidCallback onTap;
1333
1347
 
1334
1348
  @override
@@ -1374,15 +1388,20 @@ class _ProTooltipIconState extends State<_ProTooltipIcon> {
1374
1388
  }
1375
1389
 
1376
1390
  Widget _buildTooltip(BuildContext context) {
1391
+ final bool left = widget.anchoredLeft;
1377
1392
  return CompositedTransformFollower(
1378
1393
  link: _layerLink,
1379
1394
  showWhenUnlinked: false,
1380
- targetAnchor: Alignment.centerRight,
1381
- followerAnchor: Alignment.centerLeft,
1382
- offset: const Offset(4, 0),
1395
+ targetAnchor: left ? Alignment.centerRight : Alignment.centerLeft,
1396
+ followerAnchor: left ? Alignment.centerLeft : Alignment.centerRight,
1397
+ offset: Offset(left ? 4 : -4, 0),
1383
1398
  child: Align(
1384
- alignment: Alignment.centerLeft,
1385
- child: _TooltipCard(label: widget.label, colors: widget.colors),
1399
+ alignment: left ? Alignment.centerLeft : Alignment.centerRight,
1400
+ child: _TooltipCard(
1401
+ label: widget.label,
1402
+ colors: widget.colors,
1403
+ pointsLeft: left,
1404
+ ),
1386
1405
  ),
1387
1406
  );
1388
1407
  }
@@ -1393,11 +1412,19 @@ class _ProTooltipIconState extends State<_ProTooltipIcon> {
1393
1412
  // ─────────────────────────────────────────────────────────────────────────────
1394
1413
 
1395
1414
  class _TooltipCard extends StatelessWidget {
1396
- const _TooltipCard({required this.label, required this.colors});
1415
+ const _TooltipCard({
1416
+ required this.label,
1417
+ required this.colors,
1418
+ this.pointsLeft = true,
1419
+ });
1397
1420
 
1398
1421
  final String label;
1399
1422
  final _SidebarColors colors;
1400
1423
 
1424
+ /// Arrow points back at the icon: left when the rail is on the left (tooltip
1425
+ /// sits to the icon's right), right when the rail is on the right.
1426
+ final bool pointsLeft;
1427
+
1401
1428
  static const double _arrowW = 13.0;
1402
1429
  static const double _arrowH = 26.0;
1403
1430
  static const double _arrowOverlap = 8.0;
@@ -1411,43 +1438,44 @@ class _TooltipCard extends StatelessWidget {
1411
1438
  final Color bg = colors.isDark ? colors.divider : colors.textActive;
1412
1439
  final Color textColor = colors.isDark ? colors.textActive : colors.bg;
1413
1440
 
1441
+ final Widget arrow = SizedBox(
1442
+ width: _arrowW,
1443
+ height: _arrowH,
1444
+ child: CustomPaint(
1445
+ painter: _TooltipArrowPainter(color: bg, pointsLeft: pointsLeft),
1446
+ ),
1447
+ );
1448
+ // Pull the card over the arrow's base so they read as one shape; the
1449
+ // direction of the overlap flips with the arrow side.
1450
+ final Widget card = Transform.translate(
1451
+ offset: Offset(pointsLeft ? -_arrowOverlap : _arrowOverlap, 0),
1452
+ child: Container(
1453
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
1454
+ decoration: BoxDecoration(
1455
+ color: bg,
1456
+ borderRadius: BorderRadius.circular(4),
1457
+ boxShadow: const [
1458
+ BoxShadow(
1459
+ color: Color(0x40000000),
1460
+ blurRadius: 5,
1461
+ offset: Offset(0, 5),
1462
+ ),
1463
+ ],
1464
+ ),
1465
+ child: Text(
1466
+ label,
1467
+ style: context.textTheme.bodyMedium?.copyWith(
1468
+ color: textColor,
1469
+ ),
1470
+ ),
1471
+ ),
1472
+ );
1473
+
1414
1474
  return Material(
1415
1475
  color: Colors.transparent,
1416
1476
  child: Row(
1417
1477
  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
- ],
1478
+ children: pointsLeft ? <Widget>[arrow, card] : <Widget>[card, arrow],
1451
1479
  ),
1452
1480
  );
1453
1481
  }
@@ -1458,22 +1486,31 @@ class _TooltipCard extends StatelessWidget {
1458
1486
  // ─────────────────────────────────────────────────────────────────────────────
1459
1487
 
1460
1488
  class _TooltipArrowPainter extends CustomPainter {
1461
- const _TooltipArrowPainter({required this.color});
1489
+ const _TooltipArrowPainter({required this.color, this.pointsLeft = true});
1462
1490
  final Color color;
1491
+ final bool pointsLeft;
1463
1492
 
1464
1493
  @override
1465
1494
  void paint(Canvas canvas, Size size) {
1466
1495
  final paint = Paint()
1467
1496
  ..color = color
1468
1497
  ..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();
1498
+ // Apex on the side it points to; base on the opposite (card) side.
1499
+ final path = pointsLeft
1500
+ ? (Path()
1501
+ ..moveTo(size.width, 0)
1502
+ ..lineTo(0, size.height / 2)
1503
+ ..lineTo(size.width, size.height)
1504
+ ..close())
1505
+ : (Path()
1506
+ ..moveTo(0, 0)
1507
+ ..lineTo(size.width, size.height / 2)
1508
+ ..lineTo(0, size.height)
1509
+ ..close());
1474
1510
  canvas.drawPath(path, paint);
1475
1511
  }
1476
1512
 
1477
1513
  @override
1478
- bool shouldRepaint(_TooltipArrowPainter old) => old.color != color;
1514
+ bool shouldRepaint(_TooltipArrowPainter old) =>
1515
+ old.color != color || old.pointsLeft != pointsLeft;
1479
1516
  }