kasy-cli 1.37.0 → 1.38.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 (53) hide show
  1. package/lib/scaffold/CHANGELOG.json +9 -0
  2. package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
  3. package/lib/scaffold/backends/patch-base-hashes.json +4 -4
  4. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
  5. package/package.json +1 -1
  6. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  7. package/templates/firebase/AGENTS.md +20 -10
  8. package/templates/firebase/DESIGN_SYSTEM.md +13 -0
  9. package/templates/firebase/README.en.md +1 -1
  10. package/templates/firebase/README.es.md +1 -1
  11. package/templates/firebase/README.md +1 -1
  12. package/templates/firebase/lib/components/kasy_app_bar.dart +57 -16
  13. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
  14. package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
  15. package/templates/firebase/lib/components/kasy_sidebar.dart +397 -28
  16. package/templates/firebase/lib/components/kasy_web_header.dart +11 -3
  17. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +2 -213
  18. package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
  19. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -4
  20. package/templates/firebase/lib/core/chrome/web_header_scope.dart +20 -0
  21. package/templates/firebase/lib/core/data/api/user_api.dart +4 -0
  22. package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
  23. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
  24. package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
  25. package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
  26. package/templates/firebase/lib/core/states/user_state_notifier.dart +41 -0
  27. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
  28. package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
  29. package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
  30. package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
  31. package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
  32. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +498 -466
  33. package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
  34. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -7
  35. package/templates/firebase/lib/features/home/home_components_page.dart +16 -6
  36. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +7 -0
  37. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +23 -13
  38. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -0
  39. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
  40. package/templates/firebase/lib/features/settings/settings_page.dart +51 -38
  41. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +549 -376
  42. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
  43. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +266 -141
  44. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +23 -30
  45. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +30 -49
  46. package/templates/firebase/lib/i18n/en.i18n.json +23 -9
  47. package/templates/firebase/lib/i18n/es.i18n.json +23 -9
  48. package/templates/firebase/lib/i18n/pt.i18n.json +23 -9
  49. package/templates/firebase/lib/router.dart +43 -25
  50. package/templates/firebase/pubspec.yaml +1 -1
  51. package/templates/firebase/test/admin_shell_chrome_test.dart +104 -0
  52. package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
  53. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
@@ -1,11 +1,21 @@
1
1
  import 'package:kasy_kit/features/subscriptions/ui/component/premium_page_factory.dart';
2
2
 
3
- /// Debug-only GoRouter paths (registered when [kDebugMode] is true).
3
+ /// The four "Ferramentas" sub-screens are real admin SECTIONS: each is its own
4
+ /// branch in the console's [StatefulShellRoute] (so the rail persists and it
5
+ /// gets the standard header chrome) and is reached from the sidebar's
6
+ /// "Ferramentas" submenu, not pushed. These paths are the branch locations —
7
+ /// `adminSections()` reads them so the router and the rail can't drift.
4
8
  ///
5
- /// Pushed from the admin console (AdminPage); popping returns to it.
6
- const String adminRoutePaywalls = '/admin/paywalls';
9
+ /// Send push and Paywalls are admin actions (production); Components and Debug
10
+ /// are developer-only (their branches register only when [kDebugMode] is true).
11
+ const String adminRouteSendPush = '/admin/tools/send-push';
12
+ const String adminRoutePaywalls = '/admin/tools/paywalls';
13
+ const String adminRouteComponents = '/admin/tools/components';
14
+ const String adminRouteDebug = '/admin/tools/debug';
15
+
16
+ /// Full-screen pushed routes (debug-only), reached from inside the Debug
17
+ /// section as drill-downs (they cover the shell and pop back to it).
7
18
  const String adminRouteHomeWidgets = '/admin/home-widgets';
8
- const String adminRouteSendPush = '/admin/send-push';
9
19
 
10
20
  String adminRoutePremiumPreview(String variant) => '/admin/premium/$variant';
11
21
 
@@ -1,8 +1,7 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:flutter_riverpod/flutter_riverpod.dart';
3
- import 'package:go_router/go_router.dart';
4
- import 'package:kasy_kit/components/kasy_app_bar.dart';
5
3
  import 'package:kasy_kit/components/kasy_button.dart';
4
+ import 'package:kasy_kit/components/kasy_tabs.dart';
6
5
  import 'package:kasy_kit/components/kasy_text_area.dart';
7
6
  import 'package:kasy_kit/components/kasy_text_field.dart';
8
7
  import 'package:kasy_kit/core/theme/theme.dart';
@@ -10,7 +9,6 @@ import 'package:kasy_kit/core/toast/toast_service.dart';
10
9
  import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
11
10
  import 'package:kasy_kit/features/notifications/api/notifications_api.dart';
12
11
  import 'package:kasy_kit/features/settings/ui/components/admin/send_push_notifier.dart';
13
- import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
14
12
  import 'package:kasy_kit/i18n/translations.g.dart';
15
13
  import 'package:package_info_plus/package_info_plus.dart';
16
14
 
@@ -78,6 +76,21 @@ class _SendPushNotificationPageState
78
76
  setState(() => _emails.remove(email));
79
77
  }
80
78
 
79
+ /// Clears every field after a successful send. The screen is a console
80
+ /// section (reached from the sidebar), so there is nothing to pop — we reset
81
+ /// in place so the admin can fire another notification right away.
82
+ void _resetForm() {
83
+ _titleCtrl.clear();
84
+ _bodyCtrl.clear();
85
+ _imageCtrl.clear();
86
+ _routeCtrl.clear();
87
+ _emailCtrl.clear();
88
+ setState(() {
89
+ _emails.clear();
90
+ _sendToAll = false;
91
+ });
92
+ }
93
+
81
94
  bool get _canSend {
82
95
  if (_titleCtrl.text.trim().isEmpty) return false;
83
96
  if (_bodyCtrl.text.trim().isEmpty) return false;
@@ -128,159 +141,271 @@ class _SendPushNotificationPageState
128
141
  ref
129
142
  .read(toastProvider)
130
143
  .success(title: tr.send_push_title, text: tr.send_push_success);
131
- context.pop();
144
+ _resetForm();
132
145
  }
133
146
  });
134
147
 
135
- final Widget submitButton = _canSend
136
- ? KasyButton(
137
- label: tr.send_push_send_button,
138
- expand: true,
139
- isLoading: state is AsyncLoading,
140
- onPressed: _send,
148
+ Widget buildSubmit({required bool expand}) {
149
+ final KasyButton button = KasyButton(
150
+ label: tr.send_push_send_button,
151
+ expand: expand,
152
+ isLoading: state is AsyncLoading,
153
+ onPressed: _canSend ? _send : null,
154
+ );
155
+ // Disabled: tapping explains what's missing instead of doing nothing.
156
+ return _canSend
157
+ ? button
158
+ : GestureDetector(onTap: _onSendAttempt, child: button);
159
+ }
160
+
161
+ // Recipients: a segmented audience selector (everyone / specific) over the
162
+ // e-mail list, instead of a lone switch — reads as a deliberate choice.
163
+ final Widget recipients = Column(
164
+ crossAxisAlignment: CrossAxisAlignment.stretch,
165
+ mainAxisSize: MainAxisSize.min,
166
+ children: [
167
+ KasyTabs(
168
+ tabs: [tr.send_push_audience_all, tr.send_push_audience_specific],
169
+ selectedIndex: _sendToAll ? 0 : 1,
170
+ onTabSelected: (i) => setState(() => _sendToAll = i == 0),
171
+ mode: KasyTabsMode.fill,
172
+ ),
173
+ const SizedBox(height: KasySpacing.md),
174
+ if (_sendToAll)
175
+ Text(
176
+ tr.send_push_audience_all_hint,
177
+ style: context.textTheme.bodySmall?.copyWith(
178
+ color: context.colors.muted,
179
+ height: 1.35,
180
+ ),
141
181
  )
142
- : GestureDetector(
143
- onTap: _onSendAttempt,
144
- child: KasyButton(
145
- label: tr.send_push_send_button,
146
- expand: true,
147
- onPressed: null,
182
+ else
183
+ _EmailsSection(
184
+ emails: _emails,
185
+ controller: _emailCtrl,
186
+ focusNode: _emailFocus,
187
+ onAdd: _addEmail,
188
+ onRemove: _removeEmail,
189
+ label: tr.send_push_email_label,
190
+ hint: tr.send_push_email_hint,
191
+ ),
192
+ ],
193
+ );
194
+
195
+ final Widget contentFields = Column(
196
+ mainAxisSize: MainAxisSize.min,
197
+ children: [
198
+ KasyTextField(
199
+ label: tr.send_push_title_label,
200
+ hint: tr.send_push_title_hint,
201
+ controller: _titleCtrl,
202
+ focusNode: _titleFocus,
203
+ maxLength: 64,
204
+ showRequiredIndicator: true,
205
+ textInputAction: TextInputAction.next,
206
+ onChanged: (_) => setState(() {}),
207
+ onEditingComplete: () => _bodyFocus.requestFocus(),
208
+ ),
209
+ const SizedBox(height: KasySpacing.md),
210
+ KasyTextArea(
211
+ label: tr.send_push_body_label,
212
+ hint: tr.send_push_body_hint,
213
+ controller: _bodyCtrl,
214
+ focusNode: _bodyFocus,
215
+ maxLength: 250,
216
+ showRequiredIndicator: true,
217
+ minLines: 3,
218
+ maxLines: 5,
219
+ onChanged: (_) => setState(() {}),
220
+ ),
221
+ const SizedBox(height: KasySpacing.md),
222
+ KasyTextField(
223
+ label: tr.send_push_image_label,
224
+ hint: tr.send_push_image_hint,
225
+ controller: _imageCtrl,
226
+ focusNode: _imageFocus,
227
+ onChanged: (_) => setState(() {}),
228
+ textInputAction: TextInputAction.next,
229
+ onEditingComplete: () => _routeFocus.requestFocus(),
230
+ ),
231
+ ],
232
+ );
233
+
234
+ final Widget advancedFields = KasyTextField(
235
+ label: tr.send_push_route_label,
236
+ hint: tr.send_push_route_hint,
237
+ controller: _routeCtrl,
238
+ focusNode: _routeFocus,
239
+ textInputAction: TextInputAction.done,
240
+ onEditingComplete: () => _routeFocus.unfocus(),
241
+ );
242
+
243
+ final Widget formColumn = Column(
244
+ crossAxisAlignment: CrossAxisAlignment.stretch,
245
+ children: [
246
+ _FormSection(label: tr.send_push_section_recipients, child: recipients),
247
+ const SizedBox(height: KasySpacing.lg),
248
+ _FormSection(label: tr.send_push_section_content, child: contentFields),
249
+ const SizedBox(height: KasySpacing.lg),
250
+ _FormSection(label: tr.send_push_section_advanced, child: advancedFields),
251
+ // Send closes the form (route is optional), so the button flows right
252
+ // after the last section and scrolls with the content — not pinned.
253
+ const SizedBox(height: KasySpacing.lg),
254
+ buildSubmit(expand: true),
255
+ ],
256
+ );
257
+
258
+ final Widget previewPanel = Column(
259
+ crossAxisAlignment: CrossAxisAlignment.stretch,
260
+ mainAxisSize: MainAxisSize.min,
261
+ children: [
262
+ Padding(
263
+ padding: const EdgeInsets.only(
264
+ left: KasySpacing.xs,
265
+ bottom: KasySpacing.smd,
266
+ ),
267
+ child: Text(
268
+ tr.send_push_preview_label.toUpperCase(),
269
+ style: context.kasyTextTheme.sectionLabel.copyWith(
270
+ color: context.colors.muted,
148
271
  ),
149
- );
272
+ ),
273
+ ),
274
+ _NotificationPreview(
275
+ appName: _appName,
276
+ title: _titleCtrl.text,
277
+ body: _bodyCtrl.text,
278
+ imageUrl: _imageCtrl.text,
279
+ ),
280
+ ],
281
+ );
150
282
 
151
- return Scaffold(
152
- // resizeToAvoidBottomInset: true (default) — button rises with keyboard.
153
- body: Stack(
154
- fit: StackFit.expand,
155
- children: [
156
- Positioned.fill(
157
- child: ScrollConfiguration(
158
- behavior: const KasyKitScrollBehavior(),
159
- child: CustomScrollView(
160
- // Drag down to dismiss keyboard the professional standard.
161
- keyboardDismissBehavior:
162
- ScrollViewKeyboardDismissBehavior.onDrag,
163
- slivers: kasyOverlayPaddedSlivers(
164
- context,
165
- contentPadding: EdgeInsets.fromLTRB(
166
- KasySpacing.pageHorizontalGutter,
167
- KasySpacing.belowChromeContentGap,
168
- KasySpacing.pageHorizontalGutter,
169
- MediaQuery.paddingOf(context).bottom +
170
- 52.0 +
171
- KasySpacing.md * 2,
172
- ),
173
- slivers: [
174
- SliverToBoxAdapter(
175
- child: Column(
176
- crossAxisAlignment: CrossAxisAlignment.stretch,
177
- children: [
178
- Padding(
179
- padding: const EdgeInsets.only(
180
- left: KasySpacing.xs,
181
- bottom: KasySpacing.sm,
182
- ),
183
- child: Text(
184
- tr.send_push_preview_label.toUpperCase(),
185
- style: context.kasyTextTheme.sectionLabel.copyWith(
186
- color: context.colors.muted,
187
- ),
188
- ),
189
- ),
190
- _NotificationPreview(
191
- appName: _appName,
192
- title: _titleCtrl.text,
193
- body: _bodyCtrl.text,
194
- imageUrl: _imageCtrl.text,
195
- ),
196
- const SizedBox(height: KasySpacing.lg),
197
- SettingsSwitchTile(
198
- icon: KasyIcons.notificationActive,
199
- title: tr.send_push_to_all,
200
- value: _sendToAll,
201
- onChanged: (v) => setState(() => _sendToAll = v),
202
- ),
203
- const SettingsDivider(),
204
- const SizedBox(height: KasySpacing.lg),
205
- if (!_sendToAll) ...[
206
- _EmailsSection(
207
- emails: _emails,
208
- controller: _emailCtrl,
209
- focusNode: _emailFocus,
210
- onAdd: _addEmail,
211
- onRemove: _removeEmail,
212
- label: tr.send_push_email_label,
213
- hint: tr.send_push_email_hint,
214
- ),
215
- const SizedBox(height: KasySpacing.lg),
216
- ],
217
- KasyTextField(
218
- label: tr.send_push_title_label,
219
- hint: tr.send_push_title_hint,
220
- controller: _titleCtrl,
221
- focusNode: _titleFocus,
222
- maxLength: 64,
223
- showRequiredIndicator: true,
224
- textInputAction: TextInputAction.next,
225
- onChanged: (_) => setState(() {}),
226
- onEditingComplete: () => _bodyFocus.requestFocus(),
227
- ),
228
- const SizedBox(height: KasySpacing.md),
229
- KasyTextArea(
230
- label: tr.send_push_body_label,
231
- hint: tr.send_push_body_hint,
232
- controller: _bodyCtrl,
233
- focusNode: _bodyFocus,
234
- maxLength: 250,
235
- showRequiredIndicator: true,
236
- minLines: 3,
237
- maxLines: 5,
238
- onChanged: (_) => setState(() {}),
239
- ),
240
- const SizedBox(height: KasySpacing.md),
241
- KasyTextField(
242
- label: tr.send_push_image_label,
243
- hint: tr.send_push_image_hint,
244
- controller: _imageCtrl,
245
- focusNode: _imageFocus,
246
- onChanged: (_) => setState(() {}),
247
- textInputAction: TextInputAction.next,
248
- onEditingComplete: () => _routeFocus.requestFocus(),
249
- ),
250
- const SizedBox(height: KasySpacing.md),
251
- KasyTextField(
252
- label: tr.send_push_route_label,
253
- hint: tr.send_push_route_hint,
254
- controller: _routeCtrl,
255
- focusNode: _routeFocus,
256
- textInputAction: TextInputAction.done,
257
- onEditingComplete: () => _routeFocus.unfocus(),
283
+ const double gutter = KasySpacing.pageHorizontalGutter;
284
+
285
+ // Body-only: the admin shell supplies the chrome (web header on desktop,
286
+ // titled app bar on tablet/phone). Wide viewports get two columns (form +
287
+ // a fixed live preview); narrow stacks the preview on top. The send button
288
+ // flows at the end of the form and scrolls with it (not pinned).
289
+ return LayoutBuilder(
290
+ builder: (context, constraints) {
291
+ final bool wide = constraints.maxWidth >= 820;
292
+ // Clear the home indicator / nav bar below the last control.
293
+ final double bottomInset =
294
+ MediaQuery.paddingOf(context).bottom + KasySpacing.xl;
295
+
296
+ SingleChildScrollView scroll({
297
+ required EdgeInsets padding,
298
+ required Widget child,
299
+ }) => SingleChildScrollView(
300
+ // Drag down to dismiss keyboard — the professional standard.
301
+ keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
302
+ padding: padding,
303
+ child: child,
304
+ );
305
+
306
+ // Wide: only the form scrolls; the live preview stays FIXED beside it.
307
+ // Narrow: the preview sits on top and the whole stack scrolls.
308
+ final Widget bodyContent = wide
309
+ ? Center(
310
+ child: ConstrainedBox(
311
+ constraints: const BoxConstraints(maxWidth: 1100),
312
+ child: Row(
313
+ crossAxisAlignment: CrossAxisAlignment.start,
314
+ children: [
315
+ Expanded(
316
+ child: scroll(
317
+ padding: EdgeInsets.fromLTRB(
318
+ gutter,
319
+ KasySpacing.belowChromeContentGap,
320
+ 0,
321
+ bottomInset,
258
322
  ),
259
- ],
323
+ child: formColumn,
324
+ ),
260
325
  ),
261
- ),
326
+ const SizedBox(width: KasySpacing.xl),
327
+ Padding(
328
+ padding: const EdgeInsets.fromLTRB(
329
+ 0,
330
+ KasySpacing.belowChromeContentGap,
331
+ gutter,
332
+ KasySpacing.xl,
333
+ ),
334
+ child: SizedBox(width: 340, child: previewPanel),
335
+ ),
336
+ ],
337
+ ),
338
+ ),
339
+ )
340
+ : scroll(
341
+ padding: EdgeInsets.fromLTRB(
342
+ gutter,
343
+ KasySpacing.belowChromeContentGap,
344
+ gutter,
345
+ bottomInset,
346
+ ),
347
+ child: Column(
348
+ crossAxisAlignment: CrossAxisAlignment.stretch,
349
+ children: [
350
+ previewPanel,
351
+ const SizedBox(height: KasySpacing.lg),
352
+ formColumn,
262
353
  ],
263
354
  ),
264
- ),
355
+ );
356
+
357
+ return ScrollConfiguration(
358
+ behavior: const KasyKitScrollBehavior(),
359
+ child: bodyContent,
360
+ );
361
+ },
362
+ );
363
+ }
364
+ }
365
+
366
+ /// A labelled card grouping form fields — the console's clean section look
367
+ /// (uppercase label + surface card with a hairline border and the component
368
+ /// shadow), so Send push reads as the same product as the rest of the admin.
369
+ class _FormSection extends StatelessWidget {
370
+ final String label;
371
+ final Widget child;
372
+ const _FormSection({required this.label, required this.child});
373
+
374
+ @override
375
+ Widget build(BuildContext context) {
376
+ return Column(
377
+ crossAxisAlignment: CrossAxisAlignment.stretch,
378
+ mainAxisSize: MainAxisSize.min,
379
+ children: [
380
+ Padding(
381
+ padding: const EdgeInsets.only(
382
+ left: KasySpacing.xs,
383
+ bottom: KasySpacing.smd,
384
+ ),
385
+ child: Text(
386
+ label.toUpperCase(),
387
+ style: context.kasyTextTheme.sectionLabel.copyWith(
388
+ color: context.colors.muted,
265
389
  ),
266
390
  ),
267
- Positioned(
268
- top: 0,
269
- left: 0,
270
- right: 0,
271
- child: KasyAppBar(
272
- title: tr.send_push_title,
273
- onBack: () => context.pop(),
391
+ ),
392
+ DecoratedBox(
393
+ decoration: BoxDecoration(
394
+ color: context.colors.surface,
395
+ borderRadius: BorderRadius.circular(18),
396
+ border: Border.all(
397
+ color: context.colors.outline.withValues(
398
+ alpha: context.isDark ? 0.45 : 0.6,
399
+ ),
274
400
  ),
401
+ boxShadow: [KasyShadows.component(context)],
275
402
  ),
276
- Positioned(
277
- left: KasySpacing.pageHorizontalGutter,
278
- right: KasySpacing.pageHorizontalGutter,
279
- bottom: MediaQuery.paddingOf(context).bottom + KasySpacing.sm,
280
- child: submitButton,
403
+ child: Padding(
404
+ padding: const EdgeInsets.all(KasySpacing.md),
405
+ child: child,
281
406
  ),
282
- ],
283
- ),
407
+ ),
408
+ ],
284
409
  );
285
410
  }
286
411
  }
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
5
5
  import 'package:flutter_riverpod/flutter_riverpod.dart';
6
6
  import 'package:image_picker/image_picker.dart';
7
7
  import 'package:kasy_kit/components/kasy_avatar.dart';
8
+ import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
8
9
  import 'package:kasy_kit/core/data/entities/upload_result.dart';
9
10
  import 'package:kasy_kit/core/data/models/user.dart';
10
11
  import 'package:kasy_kit/core/data/repositories/user_repository.dart';
@@ -12,6 +13,7 @@ import 'package:kasy_kit/core/states/models/user_state.dart';
12
13
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
13
14
  import 'package:kasy_kit/core/theme/theme.dart';
14
15
  import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
16
+ import 'package:kasy_kit/core/widgets/kasy_hover.dart';
15
17
  import 'package:kasy_kit/features/settings/ui/widgets/avatar_utils.dart';
16
18
  import 'package:kasy_kit/features/settings/ui/widgets/round_progress.dart';
17
19
  import 'package:kasy_kit/i18n/translations.g.dart';
@@ -144,14 +146,10 @@ class _EditableUserAvatarState extends ConsumerState<EditableUserAvatar> {
144
146
  BuildContext context, {
145
147
  required bool hasAvatar,
146
148
  }) {
147
- return showModalBottomSheet<_AvatarAction>(
149
+ return showKasyBottomSheet<_AvatarAction>(
148
150
  context: context,
149
- useRootNavigator: true,
150
- backgroundColor: context.colors.surface,
151
- shape: const RoundedRectangleBorder(
152
- borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
153
- ),
154
- builder: (sheetContext) => SafeArea(
151
+ builder: (sheetContext) => KasySheetSurface(
152
+ showDragHandle: false,
155
153
  child: Padding(
156
154
  padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
157
155
  child: Column(
@@ -321,31 +319,26 @@ class _BottomSheetTile extends StatelessWidget {
321
319
  @override
322
320
  Widget build(BuildContext context) {
323
321
  final Color fg = color ?? context.colors.onSurface;
324
- return KasyFocusRing(
325
- onActivate: onTap,
322
+ return KasyHover(
323
+ onTap: onTap,
324
+ focusable: true,
325
+ focusGapColor: context.colors.surface,
326
326
  borderRadius: BorderRadius.circular(KasyRadius.sm),
327
- gapColor: context.colors.surface,
328
- child: InkWell(
329
- canRequestFocus: false,
330
- onTap: onTap,
331
- child: Padding(
332
- padding: const EdgeInsets.symmetric(
333
- horizontal: KasySpacing.md,
334
- vertical: KasySpacing.smd,
335
- ),
336
- child: Row(
337
- children: [
338
- if (icon != null) ...[
339
- Icon(icon, size: KasyIconSize.lg, color: fg),
340
- const SizedBox(width: KasySpacing.sm),
341
- ],
342
- Text(
343
- label,
344
- style: context.textTheme.bodyLarge?.copyWith(color: fg),
345
- ),
346
- ],
327
+ padding: const EdgeInsets.symmetric(
328
+ horizontal: KasySpacing.md,
329
+ vertical: KasySpacing.smd,
330
+ ),
331
+ child: Row(
332
+ children: [
333
+ if (icon != null) ...[
334
+ Icon(icon, size: KasyIconSize.lg, color: fg),
335
+ const SizedBox(width: KasySpacing.sm),
336
+ ],
337
+ Text(
338
+ label,
339
+ style: context.textTheme.bodyLarge?.copyWith(color: fg),
347
340
  ),
348
- ),
341
+ ],
349
342
  ),
350
343
  );
351
344
  }
@@ -2,10 +2,12 @@ import 'dart:async';
2
2
 
3
3
  import 'package:flutter/material.dart';
4
4
  import 'package:flutter_riverpod/flutter_riverpod.dart';
5
+ import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
5
6
  import 'package:kasy_kit/core/home_widgets/home_widget_mywidget_service.dart';
6
7
  import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
7
8
  import 'package:kasy_kit/core/theme/theme.dart';
8
9
  import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
10
+ import 'package:kasy_kit/core/widgets/kasy_hover.dart';
9
11
  import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
10
12
  import 'package:kasy_kit/i18n/app_locale_display.dart';
11
13
  import 'package:kasy_kit/i18n/translations.g.dart';
@@ -63,14 +65,9 @@ class LanguageSwitcher extends ConsumerWidget {
63
65
  AppLocale current,
64
66
  ) {
65
67
  final String sheetTitle = context.t.settings.language_title;
66
- showModalBottomSheet<void>(
68
+ showKasyBottomSheet<void>(
67
69
  context: context,
68
- useRootNavigator: true,
69
- backgroundColor: context.colors.surface,
70
- shape: const RoundedRectangleBorder(
71
- borderRadius: BorderRadius.vertical(top: Radius.circular(KasyRadius.lg)),
72
- ),
73
- builder: (sheetContext) => SafeArea(
70
+ builder: (sheetContext) => KasySheetSurface(
74
71
  child: Padding(
75
72
  padding: const EdgeInsets.fromLTRB(
76
73
  KasySpacing.md,
@@ -81,15 +78,6 @@ class LanguageSwitcher extends ConsumerWidget {
81
78
  child: Column(
82
79
  mainAxisSize: MainAxisSize.min,
83
80
  children: <Widget>[
84
- Container(
85
- width: 36,
86
- height: 4,
87
- margin: const EdgeInsets.only(bottom: KasySpacing.md),
88
- decoration: BoxDecoration(
89
- color: sheetContext.colors.onSurface.withValues(alpha: 0.18),
90
- borderRadius: BorderRadius.circular(2),
91
- ),
92
- ),
93
81
  Align(
94
82
  alignment: Alignment.centerLeft,
95
83
  child: Text(
@@ -156,42 +144,35 @@ class _LocaleOptionTile extends StatelessWidget {
156
144
  final Color primary = context.colors.primary;
157
145
  final Color fg = isSelected ? primary : context.colors.onSurface;
158
146
 
159
- return KasyFocusRing(
160
- onActivate: onTap,
147
+ return KasyHover(
148
+ onTap: onTap,
149
+ focusable: true,
161
150
  borderRadius: BorderRadius.circular(KasyRadius.sm),
162
- child: InkWell(
163
- canRequestFocus: false,
164
- onTap: onTap,
165
- borderRadius: BorderRadius.circular(KasyRadius.sm),
166
- child: Padding(
167
- padding: const EdgeInsets.symmetric(
168
- horizontal: KasySpacing.xs,
169
- vertical: KasySpacing.smd,
170
- ),
171
- child: Row(
172
- children: <Widget>[
173
- Expanded(
174
- child: Text(
175
- locale.nativeName,
176
- style: context.textTheme.bodyLarge?.copyWith(
177
- color: fg,
178
- fontWeight:
179
- isSelected ? FontWeight.w600 : FontWeight.w400,
180
- ),
181
- ),
151
+ padding: const EdgeInsets.symmetric(
152
+ horizontal: KasySpacing.xs,
153
+ vertical: KasySpacing.smd,
154
+ ),
155
+ child: Row(
156
+ children: <Widget>[
157
+ Expanded(
158
+ child: Text(
159
+ locale.nativeName,
160
+ style: context.textTheme.bodyLarge?.copyWith(
161
+ color: fg,
162
+ fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
182
163
  ),
183
- if (isSelected)
184
- Container(
185
- width: 10,
186
- height: 10,
187
- decoration: BoxDecoration(
188
- color: primary,
189
- shape: BoxShape.circle,
190
- ),
191
- ),
192
- ],
164
+ ),
193
165
  ),
194
- ),
166
+ if (isSelected)
167
+ Container(
168
+ width: 10,
169
+ height: 10,
170
+ decoration: BoxDecoration(
171
+ color: primary,
172
+ shape: BoxShape.circle,
173
+ ),
174
+ ),
175
+ ],
195
176
  ),
196
177
  );
197
178
  }