kasy-cli 1.37.1 → 1.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/lib/scaffold/CHANGELOG.json +23 -0
  2. package/lib/scaffold/backends/api/patch/README.md +15 -0
  3. package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
  4. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
  5. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
  6. package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
  7. package/lib/scaffold/backends/patch-base-hashes.json +6 -6
  8. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  9. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
  10. package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
  11. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
  12. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
  13. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  14. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
  15. package/lib/scaffold/shared/generator-utils.js +12 -6
  16. package/package.json +1 -1
  17. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  18. package/templates/firebase/AGENTS.md +7 -1
  19. package/templates/firebase/DESIGN_SYSTEM.md +35 -8
  20. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  21. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  22. package/templates/firebase/assets/icons/facebook.svg +49 -0
  23. package/templates/firebase/assets/icons/google.svg +1 -0
  24. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  25. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  26. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  27. package/templates/firebase/lib/components/components.dart +1 -1
  28. package/templates/firebase/lib/components/kasy_app_bar.dart +361 -20
  29. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
  30. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  31. package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
  32. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  33. package/templates/firebase/lib/components/kasy_sidebar.dart +412 -31
  34. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  35. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  36. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +29 -231
  37. package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
  38. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +19 -9
  39. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  40. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  41. package/templates/firebase/lib/core/data/api/user_api.dart +15 -0
  42. package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
  43. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
  44. package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
  45. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  46. package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
  47. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +18 -35
  48. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  49. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  50. package/templates/firebase/lib/core/states/user_state_notifier.dart +69 -1
  51. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  52. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  53. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
  54. package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
  55. package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
  56. package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
  57. package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
  58. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +547 -483
  59. package/templates/firebase/lib/core/web_viewport_scale.dart +64 -35
  60. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  61. package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
  62. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  63. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  64. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  65. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  66. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  67. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  68. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  69. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  70. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  71. package/templates/firebase/lib/features/home/home_components_page.dart +264 -126
  72. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +231 -57
  73. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  74. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  75. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  76. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +118 -57
  77. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  78. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +19 -4
  79. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  80. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  81. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  82. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
  83. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  84. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  85. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  86. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  87. package/templates/firebase/lib/features/settings/settings_page.dart +99 -65
  88. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1379 -422
  89. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
  90. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  91. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  92. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +404 -149
  93. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +24 -31
  94. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  95. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +77 -95
  96. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  97. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  98. package/templates/firebase/lib/i18n/en.i18n.json +749 -698
  99. package/templates/firebase/lib/i18n/es.i18n.json +749 -698
  100. package/templates/firebase/lib/i18n/pt.i18n.json +749 -698
  101. package/templates/firebase/lib/main.dart +20 -7
  102. package/templates/firebase/lib/router.dart +70 -46
  103. package/templates/firebase/pubspec.yaml +2 -1
  104. package/templates/firebase/test/admin_shell_chrome_test.dart +110 -0
  105. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  106. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  107. package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
  108. package/templates/firebase/tool/design_check.dart +9 -0
  109. package/templates/firebase/assets/icons/apple.png +0 -0
  110. package/templates/firebase/assets/icons/facebook.png +0 -0
  111. package/templates/firebase/assets/icons/google.png +0 -0
  112. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  113. package/templates/firebase/lib/components/kasy_web_header.dart +0 -210
  114. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  115. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  116. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  117. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  118. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -169
  119. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
  120. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
@@ -25,7 +25,7 @@ class AuthCardScaffold extends StatelessWidget {
25
25
  required this.subtitle,
26
26
  required this.children,
27
27
  this.showLogo = true,
28
- this.logoHeight = 96,
28
+ this.logoHeight = 132,
29
29
  this.maxContentWidth = 420,
30
30
  });
31
31
 
@@ -74,7 +74,7 @@ class AuthCardScaffold extends StatelessWidget {
74
74
  curve: Curves.easeOut,
75
75
  ),
76
76
  ),
77
- const SizedBox(height: KasySpacing.lg),
77
+ const SizedBox(height: KasySpacing.sm),
78
78
  ],
79
79
  Text(
80
80
  title,
@@ -111,9 +111,11 @@ class AuthCardScaffold extends StatelessWidget {
111
111
  child: isMobile
112
112
  ? content
113
113
  : KasyCard(
114
- padding: const EdgeInsets.symmetric(
115
- horizontal: KasySpacing.lg,
116
- vertical: KasySpacing.xl,
114
+ padding: const EdgeInsets.fromLTRB(
115
+ KasySpacing.lg,
116
+ KasySpacing.md,
117
+ KasySpacing.lg,
118
+ KasySpacing.xl,
117
119
  ),
118
120
  child: content,
119
121
  ),
@@ -0,0 +1,83 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter_svg/flutter_svg.dart';
3
+ import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
4
+ import 'package:kasy_kit/core/theme/theme.dart';
5
+ import 'package:kasy_kit/core/widgets/kasy_hover.dart';
6
+
7
+ /// Outlined social sign-in button (Google / Apple / Facebook), shared by the
8
+ /// sign-in and sign-up screens.
9
+ ///
10
+ /// Each tile fills its slot in the social row (the parent wraps it in an
11
+ /// [Expanded]) and centers the provider glyph. It uses [KasyHover] so it has a
12
+ /// real hover highlight on web/desktop and a press fill on every platform — no
13
+ /// Material ripple, matching the rest of the kit's interactive controls. While a
14
+ /// request is in flight ([onPressed] null) it dims and stops responding to
15
+ /// pointer and keyboard.
16
+ class SocialAuthButton extends StatelessWidget {
17
+ const SocialAuthButton({
18
+ super.key,
19
+ required this.label,
20
+ required this.iconAsset,
21
+ required this.onPressed,
22
+ });
23
+
24
+ /// Accessibility label (the provider name) announced to screen readers.
25
+ final String label;
26
+
27
+ /// Path to the provider's official brand SVG bundled under `assets/icons/`
28
+ /// (e.g. `assets/icons/google.svg`). Rendered at full brand colour and at a
29
+ /// shared [KasyIconSize.lg] box so every provider stays the same size. For
30
+ /// Apple, the caller picks the black/white variant for the current theme.
31
+ final String iconAsset;
32
+
33
+ /// Tap handler. When null the button is disabled (dimmed, inert).
34
+ final VoidCallback? onPressed;
35
+
36
+ @override
37
+ Widget build(BuildContext context) {
38
+ final bool enabled = onPressed != null;
39
+ final BorderRadius radius = BorderRadius.circular(KasyRadius.sm);
40
+
41
+ // No background fill: the KasyHover overlay sits behind the transparent
42
+ // interior to provide the hover/press tint, and on the surface auth card a
43
+ // fill would be invisible anyway. The hairline border gives the outlined
44
+ // shape, consistent with KasyButton's outlined variant.
45
+ final Widget surface = Container(
46
+ height: 44,
47
+ decoration: BoxDecoration(
48
+ borderRadius: radius,
49
+ border: Border.all(
50
+ color: context.colors.outline.withValues(alpha: 0.38),
51
+ ),
52
+ ),
53
+ child: Center(
54
+ // Brand logos have different aspect ratios (Apple is tall, Google is
55
+ // square); SvgPicture defaults to BoxFit.contain, which keeps each one
56
+ // inside the shared box without distortion so the row stays balanced.
57
+ child: SvgPicture.asset(
58
+ iconAsset,
59
+ width: KasyIconSize.lg,
60
+ height: KasyIconSize.lg,
61
+ ),
62
+ ),
63
+ );
64
+
65
+ if (!enabled) {
66
+ return Opacity(opacity: 0.5, child: surface);
67
+ }
68
+
69
+ return KasyHover(
70
+ onTap: () {
71
+ KasyHaptics.medium(context);
72
+ onPressed!();
73
+ },
74
+ // KasyHover fires a light selection haptic by default; a social sign-in is
75
+ // a significant action, so opt out and fire the heavier "medium" above.
76
+ hapticEnabled: false,
77
+ focusable: true,
78
+ borderRadius: radius,
79
+ semanticLabel: label,
80
+ child: surface,
81
+ );
82
+ }
83
+ }
@@ -59,15 +59,15 @@ class _FeatureCardState extends State<FeatureCard> {
59
59
  children: [
60
60
  Text(
61
61
  widget.title,
62
- style: context.textTheme.titleMedium?.copyWith(
63
- fontWeight: FontWeight.w700,
62
+ style: context.kasyTextTheme.listRowTitle.copyWith(
63
+ color: context.colors.onSurface,
64
+ fontWeight: FontWeight.w600,
64
65
  ),
65
66
  ),
66
67
  const SizedBox(height: 2),
67
68
  Text(
68
69
  widget.description,
69
- style: context.textTheme.bodySmall?.copyWith(
70
- fontWeight: FontWeight.w400,
70
+ style: context.textTheme.bodyMedium?.copyWith(
71
71
  color: context.colors.muted,
72
72
  ),
73
73
  ),
@@ -6,6 +6,7 @@ import 'package:kasy_kit/core/theme/theme.dart';
6
6
  import 'package:kasy_kit/core/widgets/kasy_hover.dart';
7
7
  import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
8
8
  import 'package:kasy_kit/features/home/home_components_preview_registry.dart';
9
+ import 'package:kasy_kit/i18n/translations.g.dart';
9
10
 
10
11
  /// UI-kit catalog, reachable from the home dashboard.
11
12
  ///
@@ -28,139 +29,276 @@ class HomeComponentsPage extends StatelessWidget {
28
29
  title: 'Components',
29
30
  onBack: () => Navigator.maybePop(context),
30
31
  ),
32
+ const Expanded(child: HomeComponentsCatalog()),
33
+ ],
34
+ ),
35
+ );
36
+ }
37
+ }
38
+
39
+ /// The UI-kit catalog list on its own (no app bar / scaffold), so it can be the
40
+ /// body of [HomeComponentsPage] AND a section inside the admin console (which
41
+ /// supplies its own chrome). Fills the space it is given.
42
+ class HomeComponentsCatalog extends StatefulWidget {
43
+ const HomeComponentsCatalog({super.key});
44
+
45
+ @override
46
+ State<HomeComponentsCatalog> createState() => _HomeComponentsCatalogState();
47
+ }
48
+
49
+ class _HomeComponentsCatalogState extends State<HomeComponentsCatalog> {
50
+ final TextEditingController _searchCtrl = TextEditingController();
51
+ String _query = '';
52
+
53
+ @override
54
+ void dispose() {
55
+ _searchCtrl.dispose();
56
+ super.dispose();
57
+ }
58
+
59
+ /// Reset the filter when leaving for a component, so coming back shows the
60
+ /// full catalog instead of the stale search term.
61
+ void _clearSearch() {
62
+ if (!mounted) return;
63
+ if (_query.isEmpty && _searchCtrl.text.isEmpty) return;
64
+ _searchCtrl.clear();
65
+ setState(() => _query = '');
66
+ }
67
+
68
+ @override
69
+ Widget build(BuildContext context) {
70
+ final String q = _query.trim().toLowerCase();
71
+ final List<_CatalogRow> rows = q.isEmpty
72
+ ? _kCatalog
73
+ : _kCatalog
74
+ .where((r) => r.name.toLowerCase().contains(q))
75
+ .toList(growable: false);
76
+
77
+ return Padding(
78
+ padding: EdgeInsets.fromLTRB(
79
+ KasySpacing.pageHorizontalGutter,
80
+ KasySpacing.belowChromeContentGap,
81
+ KasySpacing.pageHorizontalGutter,
82
+ MediaQuery.paddingOf(context).bottom + KasySpacing.belowChromeContentGap,
83
+ ),
84
+ child: Column(
85
+ crossAxisAlignment: CrossAxisAlignment.stretch,
86
+ children: [
87
+ _ComponentsSearchField(
88
+ controller: _searchCtrl,
89
+ onChanged: (v) => setState(() => _query = v),
90
+ ),
91
+ const SizedBox(height: KasySpacing.smd),
31
92
  Expanded(
32
- child: Padding(
33
- padding: EdgeInsets.fromLTRB(
34
- KasySpacing.pageHorizontalGutter,
35
- KasySpacing.belowChromeContentGap,
36
- KasySpacing.pageHorizontalGutter,
37
- MediaQuery.paddingOf(context).bottom +
38
- KasySpacing.belowChromeContentGap,
39
- ),
40
- child: _componentsCatalogInset(
41
- context,
42
- child: Padding(
43
- padding: const EdgeInsets.symmetric(
44
- horizontal: KasySpacing.xs,
45
- ),
46
- child: ScrollConfiguration(
47
- behavior: const KasyKitScrollBehavior(),
48
- child: ListView.separated(
49
- padding: EdgeInsets.zero,
50
- itemCount: _kCatalog.length + 1,
51
- separatorBuilder: (context, index) => Divider(
52
- height: 1,
53
- thickness: 1,
54
- indent: KasySpacing.md,
55
- endIndent: KasySpacing.md,
56
- color: context.colors.outline.withValues(alpha: 0.33),
93
+ child: _componentsCatalogInset(
94
+ context,
95
+ child: rows.isEmpty
96
+ ? const _ComponentsEmptyResults()
97
+ : Padding(
98
+ padding: const EdgeInsets.symmetric(
99
+ horizontal: KasySpacing.xs,
57
100
  ),
58
- itemBuilder: (context, index) {
59
- if (index >= _kCatalog.length) {
60
- return const SizedBox(height: KasySpacing.sm);
61
- }
62
- final _CatalogRow row = _kCatalog[index];
63
- return KasyHover(
64
- hapticEnabled: false,
65
- onTap: () {
66
- KasyHaptics.selection(context);
67
- if (row.name == 'Design System') {
68
- context.push('/design-system');
69
- return;
70
- }
71
- // Skip rows without a preview yet (registry returns
72
- // null); otherwise open /components/:name, which the
73
- // router resolves back to this component's preview.
74
- if (getComponentPreviewDefinition(row.name) == null) {
75
- return;
76
- }
77
- context.push(
78
- '/components/${Uri.encodeComponent(row.name)}',
79
- );
80
- },
81
- child: Padding(
82
- padding: const EdgeInsets.symmetric(
83
- horizontal: KasySpacing.smd,
84
- vertical: 16,
85
- ),
86
- child: Row(
87
- children: [
88
- Expanded(
89
- child: Row(
90
- children: [
91
- Flexible(
92
- child: Text(
93
- row.name,
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
- ),
102
- ),
103
- ),
104
- if (_kReadyComponents.contains(
105
- row.name,
106
- )) ...[
107
- const SizedBox(width: KasySpacing.sm),
108
- Text(
109
- 'Pronto',
110
- style: context.textTheme.bodySmall
111
- ?.copyWith(
112
- color: context.colors.success,
113
- fontWeight: FontWeight.w600,
114
- ),
115
- ),
116
- ],
117
- if (_kWebReadyComponents.contains(
118
- row.name,
119
- )) ...[
120
- const SizedBox(width: KasySpacing.xs),
121
- Text(
122
- 'Web',
123
- style: context.textTheme.bodySmall
124
- ?.copyWith(
125
- color: context.colors.primary,
126
- fontWeight: FontWeight.w600,
127
- ),
128
- ),
129
- ],
130
- if (_kUrgentComponents.contains(
131
- row.name,
132
- )) ...[
133
- const SizedBox(width: KasySpacing.sm),
134
- Text(
135
- 'Prioridade: Urgente',
136
- style: context.textTheme.bodySmall
137
- ?.copyWith(
138
- color: context.colors.warning,
139
- fontWeight: FontWeight.w700,
140
- ),
141
- ),
142
- ],
143
- ],
144
- ),
145
- ),
146
- Icon(
147
- KasyIcons.chevronRight,
148
- color: context.colors.muted,
149
- size: 22,
150
- ),
151
- ],
101
+ child: ScrollConfiguration(
102
+ behavior: const KasyKitScrollBehavior(),
103
+ child: ListView.separated(
104
+ padding: EdgeInsets.zero,
105
+ itemCount: rows.length + 1,
106
+ separatorBuilder: (context, index) => Divider(
107
+ height: 1,
108
+ thickness: 1,
109
+ indent: KasySpacing.md,
110
+ endIndent: KasySpacing.md,
111
+ color: context.colors.outline.withValues(
112
+ alpha: 0.33,
152
113
  ),
153
114
  ),
154
- );
115
+ itemBuilder: (context, index) {
116
+ if (index >= rows.length) {
117
+ return const SizedBox(height: KasySpacing.sm);
118
+ }
119
+ return _catalogRow(context, rows[index]);
120
+ },
121
+ ),
122
+ ),
123
+ ),
124
+ ),
125
+ ),
126
+ ],
127
+ ),
128
+ );
129
+ }
155
130
 
156
- },
131
+ Widget _catalogRow(BuildContext context, _CatalogRow row) {
132
+ return KasyHover(
133
+ hapticEnabled: false,
134
+ onTap: () {
135
+ KasyHaptics.selection(context);
136
+ if (row.name == 'Design System') {
137
+ context.push('/design-system').whenComplete(_clearSearch);
138
+ return;
139
+ }
140
+ // Skip rows without a preview yet (registry returns null); otherwise
141
+ // open /components/:name, which the router resolves back to this
142
+ // component's preview.
143
+ if (getComponentPreviewDefinition(row.name) == null) {
144
+ return;
145
+ }
146
+ context
147
+ .push('/components/${Uri.encodeComponent(row.name)}')
148
+ .whenComplete(_clearSearch);
149
+ },
150
+ child: Padding(
151
+ padding: const EdgeInsets.symmetric(
152
+ horizontal: KasySpacing.smd,
153
+ vertical: 16,
154
+ ),
155
+ child: Row(
156
+ children: [
157
+ Expanded(
158
+ child: Row(
159
+ children: [
160
+ Flexible(
161
+ child: Text(
162
+ row.name,
163
+ // Lighter weight (w500) to match the app's row/list scale
164
+ // instead of the heavier heading weight.
165
+ style: context.kasyTextTheme.rowTitle.copyWith(
166
+ color: context.colors.onSurface,
167
+ ),
157
168
  ),
158
169
  ),
159
- ),
170
+ if (_kReadyComponents.contains(row.name)) ...[
171
+ const SizedBox(width: KasySpacing.sm),
172
+ Text(
173
+ 'Pronto',
174
+ style: context.textTheme.bodySmall?.copyWith(
175
+ color: context.colors.success,
176
+ fontWeight: FontWeight.w600,
177
+ ),
178
+ ),
179
+ ],
180
+ if (_kWebReadyComponents.contains(row.name)) ...[
181
+ const SizedBox(width: KasySpacing.xs),
182
+ Text(
183
+ 'Web',
184
+ style: context.textTheme.bodySmall?.copyWith(
185
+ color: context.colors.primary,
186
+ fontWeight: FontWeight.w600,
187
+ ),
188
+ ),
189
+ ],
190
+ if (_kUrgentComponents.contains(row.name)) ...[
191
+ const SizedBox(width: KasySpacing.sm),
192
+ Text(
193
+ 'Prioridade: Urgente',
194
+ style: context.textTheme.bodySmall?.copyWith(
195
+ color: context.colors.warning,
196
+ fontWeight: FontWeight.w700,
197
+ ),
198
+ ),
199
+ ],
200
+ ],
160
201
  ),
161
202
  ),
203
+ Icon(
204
+ KasyIcons.chevronRight,
205
+ color: context.colors.muted,
206
+ size: 22,
207
+ ),
208
+ ],
209
+ ),
210
+ ),
211
+ );
212
+ }
213
+ }
214
+
215
+ /// Compact search field above the catalog — filters the list by component name.
216
+ class _ComponentsSearchField extends StatelessWidget {
217
+ final TextEditingController controller;
218
+ final ValueChanged<String> onChanged;
219
+ const _ComponentsSearchField({
220
+ required this.controller,
221
+ required this.onChanged,
222
+ });
223
+
224
+ @override
225
+ Widget build(BuildContext context) {
226
+ return SizedBox(
227
+ height: 40,
228
+ child: TextField(
229
+ controller: controller,
230
+ onChanged: onChanged,
231
+ style: context.textTheme.bodyMedium,
232
+ decoration: InputDecoration(
233
+ hintText: t.home.dashboard.search_hint,
234
+ hintStyle: context.textTheme.bodyMedium?.copyWith(
235
+ color: context.colors.muted,
162
236
  ),
163
- ],
237
+ prefixIcon: Icon(
238
+ Icons.search_rounded,
239
+ size: 18,
240
+ color: context.colors.muted,
241
+ ),
242
+ prefixIconConstraints: const BoxConstraints(
243
+ minWidth: 40,
244
+ minHeight: 40,
245
+ ),
246
+ isDense: true,
247
+ contentPadding: const EdgeInsets.symmetric(
248
+ horizontal: 12,
249
+ vertical: 10,
250
+ ),
251
+ filled: true,
252
+ fillColor: context.colors.surface,
253
+ enabledBorder: OutlineInputBorder(
254
+ borderRadius: BorderRadius.circular(KasyRadius.sm),
255
+ borderSide: BorderSide(
256
+ color: context.colors.outline.withValues(alpha: 0.5),
257
+ ),
258
+ ),
259
+ focusedBorder: OutlineInputBorder(
260
+ borderRadius: BorderRadius.circular(KasyRadius.sm),
261
+ borderSide: BorderSide(color: context.colors.primary, width: 1.4),
262
+ ),
263
+ border: OutlineInputBorder(
264
+ borderRadius: BorderRadius.circular(KasyRadius.sm),
265
+ borderSide: BorderSide(
266
+ color: context.colors.outline.withValues(alpha: 0.5),
267
+ ),
268
+ ),
269
+ ),
270
+ ),
271
+ );
272
+ }
273
+ }
274
+
275
+ /// Shown inside the catalog surface when a search matches nothing.
276
+ class _ComponentsEmptyResults extends StatelessWidget {
277
+ const _ComponentsEmptyResults();
278
+
279
+ @override
280
+ Widget build(BuildContext context) {
281
+ return Center(
282
+ child: Padding(
283
+ padding: const EdgeInsets.all(KasySpacing.lg),
284
+ child: Column(
285
+ mainAxisSize: MainAxisSize.min,
286
+ children: [
287
+ Icon(
288
+ Icons.search_off_rounded,
289
+ size: 30,
290
+ color: context.colors.muted,
291
+ ),
292
+ const SizedBox(height: KasySpacing.sm),
293
+ Text(
294
+ t.home.dashboard.search_empty,
295
+ textAlign: TextAlign.center,
296
+ style: context.textTheme.bodySmall?.copyWith(
297
+ color: context.colors.muted,
298
+ ),
299
+ ),
300
+ ],
301
+ ),
164
302
  ),
165
303
  );
166
304
  }
@@ -185,6 +323,7 @@ const Set<String> _kReadyComponents = <String>{
185
323
  'DatePicker',
186
324
  'Design System',
187
325
  'Dialog',
326
+ 'DropDown',
188
327
  'Hover',
189
328
  'Tabs',
190
329
  'TextArea',
@@ -195,7 +334,6 @@ const Set<String> _kReadyComponents = <String>{
195
334
  'SwipeAction',
196
335
  'TextFieldOTP',
197
336
  'Toast',
198
- 'Web Header',
199
337
  };
200
338
 
201
339
  /// Components that have been adapted for web (cursor, hover, shadow, border).
@@ -212,6 +350,7 @@ const Set<String> _kWebReadyComponents = <String>{
212
350
  'DatePicker',
213
351
  'Design System',
214
352
  'Dialog',
353
+ 'DropDown',
215
354
  'Hover',
216
355
  'Sidebar',
217
356
  'Skeleton',
@@ -221,7 +360,6 @@ const Set<String> _kWebReadyComponents = <String>{
221
360
  'TextField',
222
361
  'TextFieldOTP',
223
362
  'Toast',
224
- 'Web Header',
225
363
  };
226
364
 
227
365
  const Set<String> _kUrgentComponents = <String>{
@@ -244,6 +382,7 @@ const List<_CatalogRow> _kCatalog = [
244
382
  _CatalogRow('Checkbox'),
245
383
  _CatalogRow('DatePicker'),
246
384
  _CatalogRow('Dialog'),
385
+ _CatalogRow('DropDown'),
247
386
  _CatalogRow('Hover'),
248
387
  _CatalogRow('Sidebar'),
249
388
  _CatalogRow('Skeleton'),
@@ -253,7 +392,6 @@ const List<_CatalogRow> _kCatalog = [
253
392
  _CatalogRow('TextField'),
254
393
  _CatalogRow('TextFieldOTP'),
255
394
  _CatalogRow('Toast'),
256
- _CatalogRow('Web Header'),
257
395
  // Prioridade: Urgente (_kUrgentComponents), A–Z
258
396
  _CatalogRow('Radio Group'),
259
397
  _CatalogRow('Switch'),