kasy-cli 1.35.0 → 1.36.1
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.
- package/lib/scaffold/CHANGELOG.json +23 -0
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +13 -13
- package/templates/firebase/lib/components/kasy_sidebar.dart +38 -72
- package/templates/firebase/lib/core/states/user_state_notifier.dart +18 -2
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +3 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +73 -50
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +8 -2
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +15 -4
- package/templates/firebase/test/features/notifications/ui/notifications_page_test.dart +2 -2
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.36.1": {
|
|
3
|
+
"modules": {
|
|
4
|
+
"onboarding": {
|
|
5
|
+
"pt": "Onboarding mais resistente a falhas: se a escrita no backend falhar (por exemplo, o documento do usuário ainda não foi criado logo após o cadastro), o loader não trava mais. O onboarding sempre conclui e o estado fica salvo localmente, e uma resposta que não salvou é registrada no log sem interromper a experiência.",
|
|
6
|
+
"en": "More resilient onboarding: if a backend write fails (e.g. the user document isn't created yet right after sign-up), the loader no longer gets stuck. Onboarding always completes with the state saved locally, and an answer that failed to save is logged without interrupting the flow.",
|
|
7
|
+
"es": "Onboarding más resistente a fallos: si una escritura en el backend falla (por ejemplo, el documento del usuario aún no se creó justo tras el registro), el loader ya no se queda atascado. El onboarding siempre concluye con el estado guardado localmente, y una respuesta que no se guardó se registra en el log sin interrumpir la experiencia."
|
|
8
|
+
},
|
|
9
|
+
"components": {
|
|
10
|
+
"pt": "Sidebar: a variante \"collapsed rail\" agora fica fina de verdade em tela estreita (mobile) no showcase, em vez de abrir larga. Só a gaveta explícita (isDrawer) força a abertura larga. O app real não muda (a sidebar só aparece de tablet pra cima).",
|
|
11
|
+
"en": "Sidebar: the \"collapsed rail\" variant now actually stays thin on narrow (mobile) widths in the showcase, instead of opening wide. Only the explicit drawer (isDrawer) forces the wide open. The real app is unchanged (the sidebar only shows from tablet up).",
|
|
12
|
+
"es": "Sidebar: la variante \"collapsed rail\" ahora se mantiene fina de verdad en anchos estrechos (móvil) en el showcase, en vez de abrirse ancha. Solo el cajón explícito (isDrawer) fuerza la apertura ancha. La app real no cambia (la sidebar solo aparece de tablet en adelante)."
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"1.36.0": {
|
|
17
|
+
"modules": {
|
|
18
|
+
"components": {
|
|
19
|
+
"pt": "Sidebar melhorada: nova linha de busca opcional acima do rodapé (showSearch), variante de gaveta (isDrawer) e modo de demonstração (showcase) pra mais flexibilidade de navegação. As abas internas agora usam o componente compartilhado KasyTabs, deixando o visual mais consistente e o código mais enxuto.",
|
|
20
|
+
"en": "Improved sidebar: new optional search row above the footer (showSearch), a drawer variant (isDrawer) and a showcase mode for more navigation flexibility. The internal tabs now use the shared KasyTabs component, making the look more consistent and the code leaner.",
|
|
21
|
+
"es": "Sidebar mejorada: nueva fila de búsqueda opcional sobre el pie (showSearch), variante de cajón (isDrawer) y modo de demostración (showcase) para más flexibilidad de navegación. Las pestañas internas ahora usan el componente compartido KasyTabs, dejando el aspecto más consistente y el código más limpio."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
2
25
|
"1.35.0": {
|
|
3
26
|
"modules": {
|
|
4
27
|
"core": {
|
package/package.json
CHANGED
|
@@ -58,16 +58,16 @@ assets/assets/icons/google_play_games.png,1772398653083,533c1d87d7be8690ab173ef4
|
|
|
58
58
|
assets/assets/icons/google.png,1772398653083,f423e7e7be1e06008d45617d07f095f04da7fdcab9a56523f9e0633828e464e0
|
|
59
59
|
assets/assets/icons/facebook.png,1772398653083,79ac67e449c1db63319d43329ca91f682b4e0bc6a0883b0dfb2a849e13ae6eb6
|
|
60
60
|
assets/assets/icons/apple.png,1772398653083,2a737d8801ca81452b2978bb2e1ac72136acf1a4c338f2de2f77b65e9f6de1fa
|
|
61
|
-
version.json,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
assets/
|
|
65
|
-
assets/AssetManifest.bin.json,
|
|
66
|
-
assets/
|
|
67
|
-
assets/shaders/
|
|
68
|
-
|
|
69
|
-
flutter_bootstrap.js,
|
|
70
|
-
main.dart.
|
|
71
|
-
main.dart.
|
|
72
|
-
assets/NOTICES,
|
|
73
|
-
main.dart.js,
|
|
61
|
+
version.json,1781384419657,1c93a56a819f36903677817066dbda0bae5785563a39d3a0a486246cf67107ac
|
|
62
|
+
flutter_service_worker.js,1781384420789,baeeaf9f4b8e6f40d3b0549429ceb70ca01f120db6a7ef2f60d5809233dc2205
|
|
63
|
+
assets/FontManifest.json,1781384419796,4d84ab517c27984d36f9a3c8be6f2a72788c0c3985c1d5874297fef0a53407ca
|
|
64
|
+
assets/AssetManifest.bin,1781384419795,78bccb08a36307a400711a1d7e6868bd5f89a24e63439fb5b6747660323e4475
|
|
65
|
+
assets/AssetManifest.bin.json,1781384419795,50a11b51c3fc4cbed1b5ec3a4f466c6a6b75c481d08d4782c8191f99bdbdba9c
|
|
66
|
+
assets/shaders/stretch_effect.frag,1781384419911,1a7d4ac2be40cf0a459dfb390ef08bcd740f37913ffdee8de3c2ea836a18410e
|
|
67
|
+
assets/shaders/ink_sparkle.frag,1781384419911,1c8e222328206d1e06754f76fb53947aad38d62180aafad5298a3c6f510b173d
|
|
68
|
+
index.html,1781384363218,bb8142eb84e9e44049957c3edf86006e1ae5f194c397a8326ce505b68fdf58f4
|
|
69
|
+
flutter_bootstrap.js,1781384363207,c9fd7e9f06eccf37ccbe2a39e88047779c1a67c67fe18febf6971479b7d6b64c
|
|
70
|
+
main.dart.js_1.part.js,1781384401644,7be2ce26bfb76e0142df09977ff2bbb058e5bee8c33d1ebf6c49aff2158d94d0
|
|
71
|
+
main.dart.js_3.part.js,1781384401652,6f1d83d415ac91cc4ca02b7ccefeae40d609713c7609b636502b68501dd44a38
|
|
72
|
+
assets/NOTICES,1781384419796,a18efc42c5ed99b56481537d0f229ddd3add671c548a893aaf8766f30c854158
|
|
73
|
+
main.dart.js,1781384402163,46f09db6091efb87263aad9c85327da8097b61c8c528564b1f7359829aef790b
|
|
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|
|
6
6
|
import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
7
7
|
import 'package:kasy_kit/components/kasy_avatar.dart';
|
|
8
8
|
import 'package:kasy_kit/components/kasy_avatar_presets.dart';
|
|
9
|
+
import 'package:kasy_kit/components/kasy_tabs.dart';
|
|
9
10
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
10
11
|
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
11
12
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
@@ -60,7 +61,6 @@ class _SidebarColors {
|
|
|
60
61
|
required this.border,
|
|
61
62
|
required this.divider,
|
|
62
63
|
required this.activeBg,
|
|
63
|
-
required this.segmentThumb,
|
|
64
64
|
required this.textMuted,
|
|
65
65
|
required this.textActive,
|
|
66
66
|
required this.logout,
|
|
@@ -69,9 +69,9 @@ class _SidebarColors {
|
|
|
69
69
|
|
|
70
70
|
/// Maps the HeroUI Figma tokens onto the global [KasyColors]:
|
|
71
71
|
/// `surface → surface`, `foreground → onSurface`, `foreground/muted → muted`,
|
|
72
|
-
/// `border → border`, `separator → separator`, `default →
|
|
73
|
-
/// (hover/active fill
|
|
74
|
-
///
|
|
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.
|
|
75
75
|
factory _SidebarColors.fromContext(BuildContext context) {
|
|
76
76
|
final c = context.colors;
|
|
77
77
|
final bool dark = context.isDark;
|
|
@@ -83,8 +83,6 @@ class _SidebarColors {
|
|
|
83
83
|
divider: c.border,
|
|
84
84
|
// Hover / active item fill + tabs track + kbd chip (default/default).
|
|
85
85
|
activeBg: c.surfaceNeutralSoft,
|
|
86
|
-
// Selected segment thumb (HeroUI `segment`): lifts off the track.
|
|
87
|
-
segmentThumb: c.segment,
|
|
88
86
|
textMuted: c.muted,
|
|
89
87
|
textActive: c.onSurface,
|
|
90
88
|
logout: c.error,
|
|
@@ -96,7 +94,6 @@ class _SidebarColors {
|
|
|
96
94
|
final Color border;
|
|
97
95
|
final Color divider;
|
|
98
96
|
final Color activeBg;
|
|
99
|
-
final Color segmentThumb;
|
|
100
97
|
final Color textMuted;
|
|
101
98
|
final Color textActive;
|
|
102
99
|
final Color logout;
|
|
@@ -208,6 +205,7 @@ class KasySidebar extends StatefulWidget {
|
|
|
208
205
|
this.onLogout,
|
|
209
206
|
this.initiallyCollapsed = false,
|
|
210
207
|
this.isDrawer = false,
|
|
208
|
+
this.showSearch = false,
|
|
211
209
|
this.side = KasySidebarSide.left,
|
|
212
210
|
this.routes,
|
|
213
211
|
this.onTapItem,
|
|
@@ -260,6 +258,11 @@ class KasySidebar extends StatefulWidget {
|
|
|
260
258
|
/// opening the sidebar from an app-bar menu button on a phone.
|
|
261
259
|
final bool isDrawer;
|
|
262
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
|
+
|
|
263
266
|
/// The screen edge this sidebar is anchored to.
|
|
264
267
|
final KasySidebarSide side;
|
|
265
268
|
|
|
@@ -299,19 +302,15 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
299
302
|
/// Viewport width below which the sidebar auto-collapses (tablet breakpoint).
|
|
300
303
|
static const double _kBreakpoint = 1024.0;
|
|
301
304
|
|
|
302
|
-
/// Below this (mobile), the rail
|
|
303
|
-
///
|
|
304
|
-
///
|
|
305
|
+
/// Below this (mobile), the rail hides its collapse toggle: on a phone the
|
|
306
|
+
/// thin-vs-wide choice is a fixed pre-configuration (set via [isDrawer] for a
|
|
307
|
+
/// wide drawer, or the collapsed default for a thin rail), not a live toggle.
|
|
308
|
+
/// Width does NOT force wide here — a collapsed config stays thin on mobile.
|
|
305
309
|
static const double _kMobileBreakpoint = 768.0;
|
|
306
310
|
|
|
307
311
|
bool _isMobile(BuildContext context) =>
|
|
308
312
|
MediaQuery.sizeOf(context).width < _kMobileBreakpoint;
|
|
309
313
|
|
|
310
|
-
/// Drawer presentation — always wide, no collapse toggle. True on a phone-
|
|
311
|
-
/// width viewport or when explicitly opened as a drawer ([isDrawer]).
|
|
312
|
-
bool _wideDrawer(BuildContext context) =>
|
|
313
|
-
widget.isDrawer || _isMobile(context);
|
|
314
|
-
|
|
315
314
|
/// True when wired to Bart's navigation (real, tappable screens).
|
|
316
315
|
bool get _connected =>
|
|
317
316
|
widget.routes != null &&
|
|
@@ -379,9 +378,10 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
379
378
|
|
|
380
379
|
@override
|
|
381
380
|
Widget build(BuildContext context) {
|
|
382
|
-
//
|
|
383
|
-
// explicit choice, falling back to the
|
|
384
|
-
|
|
381
|
+
// Only an explicit drawer ([isDrawer]) forces wide; everything else honours
|
|
382
|
+
// the user's explicit choice, falling back to the auto-collapse on narrow
|
|
383
|
+
// viewports. Mobile is NOT forced wide — a collapsed config stays thin there.
|
|
384
|
+
_collapsed = !widget.isDrawer &&
|
|
385
385
|
(_collapsePreference ?? _isViewportNarrow(context));
|
|
386
386
|
|
|
387
387
|
final c = _colors;
|
|
@@ -577,6 +577,15 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
577
577
|
child: Column(
|
|
578
578
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
579
579
|
children: [
|
|
580
|
+
if (widget.showSearch)
|
|
581
|
+
_buildItemRow(
|
|
582
|
+
c,
|
|
583
|
+
icon: KasyIcons.search,
|
|
584
|
+
label: 'Search',
|
|
585
|
+
isActive: false,
|
|
586
|
+
onTap: () {},
|
|
587
|
+
trailing: [_buildKbd(c)],
|
|
588
|
+
),
|
|
580
589
|
_buildNavItem(context, _kHelpItem, c),
|
|
581
590
|
_buildItemRow(
|
|
582
591
|
c,
|
|
@@ -610,8 +619,9 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
610
619
|
MediaQuery.sizeOf(context).width >= _kBreakpoint
|
|
611
620
|
? _kTopBandHeight
|
|
612
621
|
: kasyAppBarBodyTopOverlap(context);
|
|
613
|
-
// No collapse toggle on
|
|
614
|
-
|
|
622
|
+
// No collapse toggle on a drawer (always wide) or on mobile (the thin/wide
|
|
623
|
+
// choice is a fixed pre-configuration there, not a live toggle).
|
|
624
|
+
final bool showToggle = !widget.isDrawer && !_isMobile(context);
|
|
615
625
|
final bool anchoredLeft = widget.side == KasySidebarSide.left;
|
|
616
626
|
// Brand wordmark — same artwork as the splash screen.
|
|
617
627
|
final Widget logo = Image.asset(
|
|
@@ -700,59 +710,15 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
700
710
|
|
|
701
711
|
// ── Segmented tabs (Layers / Assets) ────────────────────────────────────────
|
|
702
712
|
|
|
713
|
+
/// The showcase segment is the shared [KasyTabs] component (primary pill,
|
|
714
|
+
/// fill mode) so the sidebar demos the real design-system control rather than
|
|
715
|
+
/// a bespoke copy.
|
|
703
716
|
Widget _buildTabs(_SidebarColors c) {
|
|
704
|
-
return
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
borderRadius: BorderRadius.circular(24),
|
|
710
|
-
),
|
|
711
|
-
child: Row(
|
|
712
|
-
children: [
|
|
713
|
-
_buildTab(c, 'Layers', 0),
|
|
714
|
-
const SizedBox(width: 2),
|
|
715
|
-
_buildTab(c, 'Assets', 1),
|
|
716
|
-
],
|
|
717
|
-
),
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
Widget _buildTab(_SidebarColors c, String label, int index) {
|
|
722
|
-
final bool selected = _showcaseTab == index;
|
|
723
|
-
return Expanded(
|
|
724
|
-
child: MouseRegion(
|
|
725
|
-
cursor: SystemMouseCursors.click,
|
|
726
|
-
child: GestureDetector(
|
|
727
|
-
behavior: HitTestBehavior.opaque,
|
|
728
|
-
onTap: () => setState(() => _showcaseTab = index),
|
|
729
|
-
child: AnimatedContainer(
|
|
730
|
-
duration: const Duration(milliseconds: 180),
|
|
731
|
-
curve: Curves.easeOut,
|
|
732
|
-
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
733
|
-
alignment: Alignment.center,
|
|
734
|
-
decoration: BoxDecoration(
|
|
735
|
-
color: selected ? c.segmentThumb : Colors.transparent,
|
|
736
|
-
borderRadius: BorderRadius.circular(24),
|
|
737
|
-
boxShadow: selected
|
|
738
|
-
? const [
|
|
739
|
-
BoxShadow(
|
|
740
|
-
color: Color(0x0F000000),
|
|
741
|
-
blurRadius: 8,
|
|
742
|
-
offset: Offset(0, 2),
|
|
743
|
-
),
|
|
744
|
-
]
|
|
745
|
-
: null,
|
|
746
|
-
),
|
|
747
|
-
child: Text(
|
|
748
|
-
label,
|
|
749
|
-
style: context.kasyTextTheme.rowTitle.copyWith(
|
|
750
|
-
color: selected ? c.textActive : c.textMuted,
|
|
751
|
-
),
|
|
752
|
-
),
|
|
753
|
-
),
|
|
754
|
-
),
|
|
755
|
-
),
|
|
717
|
+
return KasyTabs(
|
|
718
|
+
tabs: const ['Layers', 'Assets'],
|
|
719
|
+
selectedIndex: _showcaseTab,
|
|
720
|
+
onTabSelected: (index) => setState(() => _showcaseTab = index),
|
|
721
|
+
mode: KasyTabsMode.fill,
|
|
756
722
|
);
|
|
757
723
|
}
|
|
758
724
|
|
|
@@ -108,8 +108,24 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
108
108
|
state = state.copyWith(user: const User.anonymous(onboarded: true));
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
try {
|
|
112
|
+
final newUser = await _userRepository.setOnboarded(state.user);
|
|
113
|
+
state = state.copyWith(user: newUser);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// The user document may not exist yet: the backend's onUserRegistration
|
|
116
|
+
// trigger runs asynchronously and can lag a just-created anonymous account
|
|
117
|
+
// (Firestore update() throws on a missing doc). Onboarding is already
|
|
118
|
+
// remembered locally (flag above), so reflect it in state and move on
|
|
119
|
+
// instead of letting this bubble up and trap the onboarding loader.
|
|
120
|
+
_logger.w('setOnboarded failed, keeping local onboarded state: $e');
|
|
121
|
+
state = state.copyWith(
|
|
122
|
+
user: switch (state.user) {
|
|
123
|
+
final AuthenticatedUserData u => u.copyWith(onboarded: true),
|
|
124
|
+
final AnonymousUserData u => u.copyWith(onboarded: true),
|
|
125
|
+
final LoadingUserData _ => state.user,
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
}
|
|
113
129
|
}
|
|
114
130
|
|
|
115
131
|
/// Finish onboarding (or "continue as guest" from the sign-in screen) by
|
|
@@ -273,7 +273,9 @@ class _GuestContinueButtonState extends ConsumerState<_GuestContinueButton> {
|
|
|
273
273
|
Widget build(BuildContext context) {
|
|
274
274
|
return KasyButton(
|
|
275
275
|
label: t.auth.signin.continue_without,
|
|
276
|
-
variant:
|
|
276
|
+
// Most subtle variant in the button preview: it's a tertiary action that
|
|
277
|
+
// should sit quietly below sign-in / social, not compete with them.
|
|
278
|
+
variant: KasyButtonVariant.ghost,
|
|
277
279
|
expand: true,
|
|
278
280
|
isLoading: _loading,
|
|
279
281
|
onPressed: _loading ? null : _continue,
|
|
@@ -555,16 +555,24 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
555
555
|
title: 'Sidebar',
|
|
556
556
|
variants: [
|
|
557
557
|
ComponentPreviewVariant(
|
|
558
|
-
label: '
|
|
559
|
-
builder:
|
|
558
|
+
label: 'App navigation',
|
|
559
|
+
builder: _buildSidebarAppNav,
|
|
560
560
|
),
|
|
561
561
|
ComponentPreviewVariant(
|
|
562
|
-
label: '
|
|
562
|
+
label: 'With search',
|
|
563
|
+
builder: _buildSidebarWithSearch,
|
|
564
|
+
),
|
|
565
|
+
ComponentPreviewVariant(
|
|
566
|
+
label: 'Collapsed rail',
|
|
563
567
|
builder: _buildSidebarCollapsed,
|
|
564
568
|
),
|
|
565
569
|
ComponentPreviewVariant(
|
|
566
|
-
label: '
|
|
567
|
-
builder:
|
|
570
|
+
label: 'Mobile drawer',
|
|
571
|
+
builder: _buildSidebarMobileDrawer,
|
|
572
|
+
),
|
|
573
|
+
ComponentPreviewVariant(
|
|
574
|
+
label: 'Workspace showcase',
|
|
575
|
+
builder: _buildSidebarShowcase,
|
|
568
576
|
),
|
|
569
577
|
],
|
|
570
578
|
);
|
|
@@ -621,63 +629,67 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
621
629
|
// Sidebar — interactive preview
|
|
622
630
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
623
631
|
|
|
624
|
-
|
|
625
|
-
|
|
632
|
+
// The live Home configuration (real pages, highlight-only nav). The default
|
|
633
|
+
// preview — what actually ships in the app.
|
|
634
|
+
Widget _buildSidebarAppNav(BuildContext context) => const _SidebarPreview();
|
|
635
|
+
|
|
636
|
+
// Same Home config plus the opt-in ⌘K search row pinned above the footer.
|
|
637
|
+
Widget _buildSidebarWithSearch(BuildContext context) =>
|
|
638
|
+
const _SidebarPreview(showSearch: true);
|
|
626
639
|
|
|
640
|
+
// Home config starting as the narrow icon rail (tooltips on hover).
|
|
627
641
|
Widget _buildSidebarCollapsed(BuildContext context) =>
|
|
628
642
|
const _SidebarPreview(initiallyCollapsed: true);
|
|
629
643
|
|
|
630
|
-
|
|
631
|
-
|
|
644
|
+
// The mobile pattern: always wide, collapse toggle hidden (isDrawer).
|
|
645
|
+
Widget _buildSidebarMobileDrawer(BuildContext context) =>
|
|
646
|
+
const _SidebarPreview(isDrawer: true);
|
|
647
|
+
|
|
648
|
+
// The rich SaaS showcase (workspace selector, KasyTabs segment, layers list).
|
|
649
|
+
Widget _buildSidebarShowcase(BuildContext context) =>
|
|
650
|
+
const _SidebarPreview(showcase: true);
|
|
632
651
|
|
|
652
|
+
/// Launches a [KasySidebar] as a full-height drawer (left + right buttons) so
|
|
653
|
+
/// the full-bleed chrome can be inspected without crushing the preview card.
|
|
654
|
+
/// Connected variants mirror the live Home; [showcase] swaps to the HeroUI
|
|
655
|
+
/// workspace demo.
|
|
633
656
|
class _SidebarPreview extends StatelessWidget {
|
|
634
657
|
const _SidebarPreview({
|
|
635
|
-
|
|
636
|
-
this.
|
|
658
|
+
this.showcase = false,
|
|
659
|
+
this.initiallyCollapsed = false,
|
|
660
|
+
this.isDrawer = false,
|
|
661
|
+
this.showSearch = false,
|
|
637
662
|
});
|
|
638
663
|
|
|
664
|
+
final bool showcase;
|
|
639
665
|
final bool initiallyCollapsed;
|
|
640
|
-
final bool
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
? KasySidebarSide.right
|
|
658
|
-
: KasySidebarSide.left,
|
|
659
|
-
),
|
|
660
|
-
),
|
|
661
|
-
),
|
|
662
|
-
transitionBuilder: (ctx, anim, secAnim, child) {
|
|
663
|
-
final curved = CurvedAnimation(
|
|
664
|
-
parent: anim,
|
|
665
|
-
curve: Curves.easeOutCubic,
|
|
666
|
-
);
|
|
667
|
-
return SlideTransition(
|
|
668
|
-
position: Tween<Offset>(
|
|
669
|
-
begin: Offset(fromEnd ? 1.0 : -1.0, 0),
|
|
670
|
-
end: Offset.zero,
|
|
671
|
-
).animate(curved),
|
|
672
|
-
child: FadeTransition(
|
|
673
|
-
opacity: Tween<double>(begin: 0.85, end: 1.0).animate(curved),
|
|
674
|
-
child: child,
|
|
675
|
-
),
|
|
676
|
-
);
|
|
677
|
-
},
|
|
666
|
+
final bool isDrawer;
|
|
667
|
+
final bool showSearch;
|
|
668
|
+
|
|
669
|
+
Widget _sidebar({required bool fromEnd}) {
|
|
670
|
+
final KasySidebarSide side =
|
|
671
|
+
fromEnd ? KasySidebarSide.right : KasySidebarSide.left;
|
|
672
|
+
if (showcase) {
|
|
673
|
+
return KasySidebar(
|
|
674
|
+
initiallyCollapsed: initiallyCollapsed,
|
|
675
|
+
side: side,
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
return _DemoSidebar(
|
|
679
|
+
isDrawer: isDrawer,
|
|
680
|
+
initiallyCollapsed: initiallyCollapsed,
|
|
681
|
+
showSearch: showSearch,
|
|
682
|
+
side: side,
|
|
678
683
|
);
|
|
679
684
|
}
|
|
680
685
|
|
|
686
|
+
void _open(BuildContext context, {required bool fromEnd}) =>
|
|
687
|
+
_openSidebarDrawer(
|
|
688
|
+
context,
|
|
689
|
+
fromEnd: fromEnd,
|
|
690
|
+
child: _sidebar(fromEnd: fromEnd),
|
|
691
|
+
);
|
|
692
|
+
|
|
681
693
|
@override
|
|
682
694
|
Widget build(BuildContext context) {
|
|
683
695
|
return Column(
|
|
@@ -714,9 +726,17 @@ class _SidebarPreview extends StatelessWidget {
|
|
|
714
726
|
/// highlight-only navigation, so the showcase mirrors the actual app. Shared by
|
|
715
727
|
/// the sidebar previews and the app-bar drawer demo.
|
|
716
728
|
class _DemoSidebar extends StatefulWidget {
|
|
717
|
-
const _DemoSidebar({
|
|
729
|
+
const _DemoSidebar({
|
|
730
|
+
this.isDrawer = false,
|
|
731
|
+
this.initiallyCollapsed = false,
|
|
732
|
+
this.showSearch = false,
|
|
733
|
+
this.side = KasySidebarSide.left,
|
|
734
|
+
});
|
|
718
735
|
|
|
719
736
|
final bool isDrawer;
|
|
737
|
+
final bool initiallyCollapsed;
|
|
738
|
+
final bool showSearch;
|
|
739
|
+
final KasySidebarSide side;
|
|
720
740
|
|
|
721
741
|
@override
|
|
722
742
|
State<_DemoSidebar> createState() => _DemoSidebarState();
|
|
@@ -763,6 +783,9 @@ class _DemoSidebarState extends State<_DemoSidebar> {
|
|
|
763
783
|
currentItem: _current,
|
|
764
784
|
onTapItem: (i) => _current.value = i, // highlight only
|
|
765
785
|
isDrawer: widget.isDrawer,
|
|
786
|
+
initiallyCollapsed: widget.initiallyCollapsed,
|
|
787
|
+
showSearch: widget.showSearch,
|
|
788
|
+
side: widget.side,
|
|
766
789
|
);
|
|
767
790
|
}
|
|
768
791
|
}
|
|
@@ -8,6 +8,7 @@ import 'package:kasy_kit/features/notifications/repositories/notifications_repos
|
|
|
8
8
|
import 'package:kasy_kit/features/onboarding/models/user_info.dart';
|
|
9
9
|
import 'package:kasy_kit/features/onboarding/providers/onboarding_model.dart';
|
|
10
10
|
import 'package:kasy_kit/features/onboarding/repositories/user_infos_repository.dart';
|
|
11
|
+
import 'package:logger/logger.dart';
|
|
11
12
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
12
13
|
|
|
13
14
|
part 'onboarding_provider.g.dart';
|
|
@@ -83,13 +84,18 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
83
84
|
await userStateNotifier.continueAsGuest();
|
|
84
85
|
|
|
85
86
|
// Now that the account exists, flush the answers collected during the
|
|
86
|
-
// questions (gender, age, …) that had nowhere to go before.
|
|
87
|
+
// questions (gender, age, …) that had nowhere to go before. Best-effort:
|
|
88
|
+
// a failed profile write must never block onboarding from finishing.
|
|
87
89
|
final userId = ref.read(userStateNotifierProvider).user.idOrNull;
|
|
88
90
|
final pending = state.pendingUserInfo;
|
|
89
91
|
if (userId != null && pending.isNotEmpty) {
|
|
90
92
|
final repository = ref.read(userInfosRepositoryProvider);
|
|
91
93
|
for (final info in pending) {
|
|
92
|
-
|
|
94
|
+
try {
|
|
95
|
+
await repository.save(userId, info);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
Logger().w('Failed to save onboarding answer: $e');
|
|
98
|
+
}
|
|
93
99
|
}
|
|
94
100
|
state = state.copyWith(pendingUserInfo: const []);
|
|
95
101
|
}
|
|
@@ -27,10 +27,21 @@ class _OnboardingJournalLoaderState extends ConsumerState<OnboardingLoader> {
|
|
|
27
27
|
// Run the real work (which now lazily creates the guest account) while the
|
|
28
28
|
// loader animation plays, so its network latency is hidden under the
|
|
29
29
|
// minimum display time instead of being added on top of it.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
//
|
|
31
|
+
// The completion call MUST always run, even if a backend write fails (e.g.
|
|
32
|
+
// setting the onboarded flag on a user document the backend trigger has not
|
|
33
|
+
// created yet). Otherwise the user is trapped on this loader forever. The
|
|
34
|
+
// account already exists and onboarding is remembered locally, so moving
|
|
35
|
+
// on is safe.
|
|
36
|
+
final minimumDisplay = Future<void>.delayed(
|
|
37
|
+
const Duration(milliseconds: 3500),
|
|
38
|
+
);
|
|
39
|
+
try {
|
|
40
|
+
await ref.onboardingNotifier.onOnboardingCompleted();
|
|
41
|
+
} catch (e) {
|
|
42
|
+
debugPrint('OnboardingLoader: completion error (continuing anyway): $e');
|
|
43
|
+
}
|
|
44
|
+
await minimumDisplay;
|
|
34
45
|
if (!mounted) return;
|
|
35
46
|
widget.onCompleted();
|
|
36
47
|
}
|
|
@@ -81,10 +81,10 @@ void main() {
|
|
|
81
81
|
|
|
82
82
|
// Confirm dialog appears with destructive action.
|
|
83
83
|
expect(find.text('Delete all notifications?'), findsOneWidget);
|
|
84
|
-
expect(find.text('
|
|
84
|
+
expect(find.text('Yes, delete'), findsOneWidget);
|
|
85
85
|
expect(find.text('Cancel'), findsOneWidget);
|
|
86
86
|
|
|
87
|
-
await tester.tap(find.text('
|
|
87
|
+
await tester.tap(find.text('Yes, delete'));
|
|
88
88
|
await tester.pumpAndSettle();
|
|
89
89
|
|
|
90
90
|
// After deletion, no notification tiles remain.
|