diff --git a/.claude/agent-memory/mobile-builder/MEMORY.md b/.claude/agent-memory/mobile-builder/MEMORY.md index f22b7033..77531b1b 100644 --- a/.claude/agent-memory/mobile-builder/MEMORY.md +++ b/.claude/agent-memory/mobile-builder/MEMORY.md @@ -35,6 +35,19 @@ - Reports barrel: `widgets/reports_page/index.dart` - Hubs: `packages/features/client/hubs/` (client_hubs_page + hub_details_page + edit_hub_page) +## Staff Profile Sections (shimmer done) +- Compliance: certificates, documents, tax_forms -- all have shimmer skeletons +- Finances: staff_bank_account, time_card -- all have shimmer skeletons +- Onboarding: attire, profile_info (personal_info_page only) -- have shimmer skeletons +- Support: faqs, privacy_security (including legal sub-pages) -- have shimmer skeletons +- Pages that intentionally keep CircularProgressIndicator (action/submit spinners): + - form_i9_page, form_w4_page (submit button spinners) + - experience_page (save button spinner) + - preferred_locations_page (save button + overlay spinner) + - certificate_upload_page, document_upload_page, attire_capture_page (form/upload pages, no initial load) + - language_selection_page (no loading state, static list) +- LegalDocumentSkeleton is shared between PrivacyPolicyPage and TermsOfServicePage + ## Key Patterns Observed - BenefitsOverviewPage also has CircularProgressIndicator (not shimmer-ified yet) - ShiftDetailsPage has a dialog-level spinner in the "applying" dialog -- this is intentional, not a page loading state diff --git a/.gitignore b/.gitignore index 6face2b0..eb271963 100644 --- a/.gitignore +++ b/.gitignore @@ -187,6 +187,9 @@ krow-workforce-export-latest/ apps/mobile/packages/data_connect/lib/src/dataconnect_generated/ apps/web/src/dataconnect-generated/ +# Legacy mobile applications +apps/mobile/legacy/* + AGENTS.md TASKS.md diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart index e7cb7754..7d254a70 100644 --- a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart @@ -10,6 +10,7 @@ import 'package:krow_domain/krow_domain.dart'; import '../blocs/availability_bloc.dart'; import '../blocs/availability_event.dart'; import '../blocs/availability_state.dart'; +import '../widgets/availability_page_skeleton/availability_page_skeleton.dart'; class AvailabilityPage extends StatefulWidget { const AvailabilityPage({super.key}); @@ -72,7 +73,7 @@ class _AvailabilityPageState extends State { child: BlocBuilder( builder: (context, state) { if (state is AvailabilityLoading) { - return const Center(child: CircularProgressIndicator()); + return const AvailabilityPageSkeleton(); } else if (state is AvailabilityLoaded) { return Stack( children: [ diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton.dart new file mode 100644 index 00000000..59e45024 --- /dev/null +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton.dart @@ -0,0 +1 @@ +export 'availability_page_skeleton/index.dart'; diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/availability_page_skeleton.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/availability_page_skeleton.dart new file mode 100644 index 00000000..b4b0bc2b --- /dev/null +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/availability_page_skeleton.dart @@ -0,0 +1,36 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'day_availability_skeleton.dart'; +import 'info_card_skeleton.dart'; +import 'quick_set_skeleton.dart'; +import 'week_navigation_skeleton.dart'; + +/// Shimmer loading skeleton for the availability page. +/// +/// Mimics the loaded layout: quick-set buttons, week navigation calendar, +/// selected day detail with time-slot rows, and an info card. +class AvailabilityPageSkeleton extends StatelessWidget { + /// Creates an [AvailabilityPageSkeleton]. + const AvailabilityPageSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + ), + child: Column( + spacing: UiConstants.space6, + children: const [ + QuickSetSkeleton(), + WeekNavigationSkeleton(), + DayAvailabilitySkeleton(), + InfoCardSkeleton(), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/day_availability_skeleton.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/day_availability_skeleton.dart new file mode 100644 index 00000000..cdf984f5 --- /dev/null +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/day_availability_skeleton.dart @@ -0,0 +1,88 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the selected day detail card (header + time slot rows). +class DayAvailabilitySkeleton extends StatelessWidget { + /// Creates a [DayAvailabilitySkeleton]. + const DayAvailabilitySkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space5), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + ), + child: Column( + children: [ + // Header: date text + toggle placeholder + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + UiShimmerLine(width: 160, height: 16), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 80, height: 12), + ], + ), + UiShimmerBox( + width: 48, + height: 28, + borderRadius: UiConstants.radiusFull, + ), + ], + ), + const SizedBox(height: UiConstants.space4), + // 3 time-slot rows (morning, afternoon, evening) + ..._buildSlotPlaceholders(), + ], + ), + ); + } + + /// Generates 3 time-slot shimmer rows. + List _buildSlotPlaceholders() { + return List.generate(3, (index) { + return Padding( + padding: EdgeInsets.only( + bottom: index < 2 ? UiConstants.space3 : 0, + ), + child: Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + ), + child: Row( + children: [ + // Icon placeholder + UiShimmerBox( + width: 40, + height: 40, + borderRadius: + UiConstants.radiusLg, + ), + const SizedBox(width: UiConstants.space3), + // Text lines + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 80, height: 14), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 120, height: 12), + ], + ), + ), + // Checkbox circle + const UiShimmerCircle(size: 24), + ], + ), + ), + ); + }); + } +} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/index.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/index.dart new file mode 100644 index 00000000..505afb28 --- /dev/null +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/index.dart @@ -0,0 +1,5 @@ +export 'availability_page_skeleton.dart'; +export 'day_availability_skeleton.dart'; +export 'info_card_skeleton.dart'; +export 'quick_set_skeleton.dart'; +export 'week_navigation_skeleton.dart'; diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/info_card_skeleton.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/info_card_skeleton.dart new file mode 100644 index 00000000..2c3ad6e0 --- /dev/null +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/info_card_skeleton.dart @@ -0,0 +1,36 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the info card at the bottom (icon + two text lines). +class InfoCardSkeleton extends StatelessWidget { + /// Creates an [InfoCardSkeleton]. + const InfoCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const UiShimmerCircle(size: 20), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + UiShimmerLine(width: 140, height: 14), + SizedBox(height: UiConstants.space1), + UiShimmerLine(height: 12), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/quick_set_skeleton.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/quick_set_skeleton.dart new file mode 100644 index 00000000..6e31c4af --- /dev/null +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/quick_set_skeleton.dart @@ -0,0 +1,45 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the quick-set section (title + 4 action buttons). +class QuickSetSkeleton extends StatelessWidget { + /// Creates a [QuickSetSkeleton]. + const QuickSetSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title line + const UiShimmerLine(width: 100, height: 14), + const SizedBox(height: UiConstants.space3), + // Row of 4 button placeholders + Row( + children: List.generate(4, (index) { + return Expanded( + child: Padding( + padding: EdgeInsets.only( + left: index == 0 ? 0 : UiConstants.space1, + right: index == 3 ? 0 : UiConstants.space1, + ), + child: UiShimmerBox( + width: double.infinity, + height: 32, + borderRadius: UiConstants.radiusLg, + ), + ), + ); + }), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/week_navigation_skeleton.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/week_navigation_skeleton.dart new file mode 100644 index 00000000..cfede807 --- /dev/null +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/widgets/availability_page_skeleton/week_navigation_skeleton.dart @@ -0,0 +1,51 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the week navigation card (month header + 7 day cells). +class WeekNavigationSkeleton extends StatelessWidget { + /// Creates a [WeekNavigationSkeleton]. + const WeekNavigationSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + ), + child: Column( + children: [ + // Navigation header: left arrow, month label, right arrow + Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const UiShimmerCircle(size: 32), + UiShimmerLine(width: 140, height: 16), + const UiShimmerCircle(size: 32), + ], + ), + ), + // 7 day cells + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(7, (_) { + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space1), + child: UiShimmerBox( + width: double.infinity, + height: 64, + borderRadius: UiConstants.radiusLg, + ), + ), + ); + }), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart index 3f6fbadc..76636878 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart @@ -10,6 +10,7 @@ import 'package:krow_domain/krow_domain.dart'; import '../bloc/clock_in_bloc.dart'; import '../bloc/clock_in_event.dart'; import '../bloc/clock_in_state.dart'; +import '../widgets/clock_in_page_skeleton/clock_in_page_skeleton.dart'; import '../widgets/commute_tracker.dart'; import '../widgets/date_selector.dart'; import '../widgets/lunch_break_modal.dart'; @@ -52,8 +53,9 @@ class _ClockInPageState extends State { builder: (BuildContext context, ClockInState state) { if (state.status == ClockInStatus.loading && state.todayShifts.isEmpty) { - return const Scaffold( - body: Center(child: CircularProgressIndicator()), + return Scaffold( + appBar: UiAppBar(title: i18n.title, showBackButton: false), + body: const SafeArea(child: ClockInPageSkeleton()), ); } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/activity_header_skeleton.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/activity_header_skeleton.dart new file mode 100644 index 00000000..4b392c5f --- /dev/null +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/activity_header_skeleton.dart @@ -0,0 +1,16 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the "Your Activity" section header text. +class ActivityHeaderSkeleton extends StatelessWidget { + /// Creates a shimmer line matching the activity header. + const ActivityHeaderSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Align( + alignment: Alignment.centerLeft, + child: UiShimmerLine(width: 120, height: 18), + ); + } +} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/clock_in_page_skeleton.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/clock_in_page_skeleton.dart new file mode 100644 index 00000000..b4c0aade --- /dev/null +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/clock_in_page_skeleton.dart @@ -0,0 +1,52 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'activity_header_skeleton.dart'; +import 'date_selector_skeleton.dart'; +import 'shift_card_skeleton.dart'; +import 'swipe_action_skeleton.dart'; + +/// Full-page shimmer skeleton shown while clock-in data loads. +/// +/// Mirrors the loaded [ClockInPage] layout: date selector, activity header, +/// two shift cards, and the swipe-to-check-in bar. +class ClockInPageSkeleton extends StatelessWidget { + /// Creates the clock-in page shimmer skeleton. + const ClockInPageSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: SingleChildScrollView( + padding: const EdgeInsets.only( + bottom: UiConstants.space24, + top: UiConstants.space6, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + // Date selector row + DateSelectorSkeleton(), + SizedBox(height: UiConstants.space5), + + // "Your Activity" header + ActivityHeaderSkeleton(), + SizedBox(height: UiConstants.space4), + + // Shift cards (show two placeholders) + ShiftCardSkeleton(), + ShiftCardSkeleton(), + + // Swipe action bar + SwipeActionSkeleton(), + ], + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/date_selector_skeleton.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/date_selector_skeleton.dart new file mode 100644 index 00000000..19ca086d --- /dev/null +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/date_selector_skeleton.dart @@ -0,0 +1,32 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the [DateSelector] row of 7 day chips. +class DateSelectorSkeleton extends StatelessWidget { + /// Creates a shimmer placeholder matching the date selector layout. + const DateSelectorSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 80, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(7, (int index) { + return Expanded( + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: UiConstants.space1, + ), + child: const UiShimmerBox( + width: double.infinity, + height: 80, + borderRadius: UiConstants.radiusLg, + ), + ), + ); + }), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/shift_card_skeleton.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/shift_card_skeleton.dart new file mode 100644 index 00000000..9665d288 --- /dev/null +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/shift_card_skeleton.dart @@ -0,0 +1,52 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single shift info card. +/// +/// Mirrors the two-column layout: left side has badge, title, and subtitle +/// lines; right side has time range and rate lines. +class ShiftCardSkeleton extends StatelessWidget { + /// Creates a shimmer placeholder for one shift card. + const ShiftCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space3), + margin: const EdgeInsets.only(bottom: UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + ), + child: const Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left column: badge + title + subtitle + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 80, height: 10), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 160, height: 14), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 200, height: 12), + ], + ), + ), + SizedBox(width: UiConstants.space3), + // Right column: time + rate + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + UiShimmerLine(width: 100, height: 12), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 60, height: 12), + ], + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/swipe_action_skeleton.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/swipe_action_skeleton.dart new file mode 100644 index 00000000..c1d1c829 --- /dev/null +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_page_skeleton/swipe_action_skeleton.dart @@ -0,0 +1,17 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the swipe-to-check-in action area. +class SwipeActionSkeleton extends StatelessWidget { + /// Creates a shimmer placeholder matching the swipe bar height. + const SwipeActionSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const UiShimmerBox( + width: double.infinity, + height: 60, + borderRadius: UiConstants.radiusLg, + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart index 78a5bf22..1e204eb8 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart @@ -8,6 +8,7 @@ import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart'; import 'package:staff_home/src/presentation/widgets/home_page/benefits_section.dart'; import 'package:staff_home/src/presentation/widgets/home_page/full_width_divider.dart'; import 'package:staff_home/src/presentation/widgets/home_page/home_header.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/staff_home_header_skeleton.dart'; import 'package:staff_home/src/presentation/widgets/home_page/placeholder_banner.dart'; import 'package:staff_home/src/presentation/widgets/home_page/home_page_skeleton.dart'; import 'package:staff_home/src/presentation/widgets/home_page/quick_actions_section.dart'; @@ -48,8 +49,13 @@ class WorkerHomePage extends StatelessWidget { children: [ BlocBuilder( buildWhen: (previous, current) => - previous.staffName != current.staffName, + previous.staffName != current.staffName || + previous.status != current.status, builder: (context, state) { + if (state.status == HomeStatus.initial || + state.status == HomeStatus.loading) { + return const StaffHomeHeaderSkeleton(); + } return HomeHeader(userName: state.staffName); }, ), diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/staff_home_header_skeleton.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/staff_home_header_skeleton.dart new file mode 100644 index 00000000..e3e7d7e1 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/staff_home_header_skeleton.dart @@ -0,0 +1,38 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the staff home header during loading. +/// +/// Mimics the avatar circle, welcome text, and user name layout. +class StaffHomeHeaderSkeleton extends StatelessWidget { + /// Creates a [StaffHomeHeaderSkeleton]. + const StaffHomeHeaderSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: Padding( + padding: const EdgeInsets.fromLTRB( + UiConstants.space4, + UiConstants.space4, + UiConstants.space4, + UiConstants.space3, + ), + child: Row( + spacing: UiConstants.space3, + children: const [ + UiShimmerCircle(size: UiConstants.space12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 80, height: 12), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 120, height: 16), + ], + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart index 8bec14f2..7f54d16b 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart @@ -10,6 +10,7 @@ import '../blocs/profile_cubit.dart'; import '../blocs/profile_state.dart'; import '../widgets/logout_button.dart'; import '../widgets/header/profile_header.dart'; +import '../widgets/profile_page_skeleton/profile_page_skeleton.dart'; import '../widgets/reliability_score_bar.dart'; import '../widgets/reliability_stats_card.dart'; import '../widgets/sections/index.dart'; @@ -63,9 +64,9 @@ class StaffProfilePage extends StatelessWidget { } }, builder: (BuildContext context, ProfileState state) { - // Show loading spinner if status is loading + // Show shimmer skeleton while profile data loads if (state.status == ProfileStatus.loading) { - return const Center(child: CircularProgressIndicator()); + return const ProfilePageSkeleton(); } if (state.status == ProfileStatus.error) { @@ -87,7 +88,7 @@ class StaffProfilePage extends StatelessWidget { final Staff? profile = state.profile; if (profile == null) { - return const Center(child: CircularProgressIndicator()); + return const ProfilePageSkeleton(); } return SingleChildScrollView( diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/index.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/index.dart new file mode 100644 index 00000000..5996ff84 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/index.dart @@ -0,0 +1,5 @@ +export 'menu_section_skeleton.dart'; +export 'profile_header_skeleton.dart'; +export 'profile_page_skeleton.dart'; +export 'reliability_score_skeleton.dart'; +export 'reliability_stats_skeleton.dart'; diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/menu_section_skeleton.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/menu_section_skeleton.dart new file mode 100644 index 00000000..ff95bbbc --- /dev/null +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/menu_section_skeleton.dart @@ -0,0 +1,87 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a profile menu section. +/// +/// Mirrors the section layout: a section title line followed by a grid of +/// square menu item placeholders. Reused for onboarding, compliance, finance, +/// and support sections. +class MenuSectionSkeleton extends StatelessWidget { + /// Creates a [MenuSectionSkeleton]. + const MenuSectionSkeleton({ + super.key, + this.itemCount = 4, + this.crossAxisCount = 3, + }); + + /// Number of menu item placeholders to display. + final int itemCount; + + /// Number of columns in the grid. + final int crossAxisCount; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Section title placeholder + Padding( + padding: const EdgeInsets.only(left: UiConstants.space1), + child: const UiShimmerLine(width: 100, height: 12), + ), + const SizedBox(height: UiConstants.space3), + // Menu items grid + LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + const double spacing = UiConstants.space3; + final double totalWidth = constraints.maxWidth; + final double totalSpacingWidth = spacing * (crossAxisCount - 1); + final double itemWidth = + (totalWidth - totalSpacingWidth) / crossAxisCount; + + return Wrap( + spacing: spacing, + runSpacing: spacing, + children: List.generate(itemCount, (int index) { + return SizedBox( + width: itemWidth, + child: const _MenuItemSkeleton(), + ); + }), + ); + }, + ), + ], + ); + } +} + +/// Single menu item shimmer: a bordered square with an icon circle and label +/// line. +class _MenuItemSkeleton extends StatelessWidget { + const _MenuItemSkeleton(); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: UiColors.bgPopup, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + ), + padding: const EdgeInsets.all(UiConstants.space2), + child: const AspectRatio( + aspectRatio: 1.0, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + UiShimmerBox(width: 36, height: 36), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 48, height: 10), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/profile_header_skeleton.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/profile_header_skeleton.dart new file mode 100644 index 00000000..60ee0ac0 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/profile_header_skeleton.dart @@ -0,0 +1,45 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the profile header section. +/// +/// Mirrors [ProfileHeader] layout: circle avatar, name line, and level badge +/// on the primary-colored background with rounded bottom corners. +class ProfileHeaderSkeleton extends StatelessWidget { + /// Creates a [ProfileHeaderSkeleton]. + const ProfileHeaderSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space5, + UiConstants.space5, + UiConstants.space16, + ), + decoration: const BoxDecoration( + color: UiColors.primary, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(UiConstants.space6), + ), + ), + child: SafeArea( + bottom: false, + child: Column( + children: [ + // Avatar placeholder + const UiShimmerCircle(size: 112), + const SizedBox(height: UiConstants.space4), + // Name placeholder + const UiShimmerLine(width: 160, height: 20), + const SizedBox(height: UiConstants.space2), + // Level badge placeholder + const UiShimmerBox(width: 100, height: 24), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/profile_page_skeleton.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/profile_page_skeleton.dart new file mode 100644 index 00000000..162a61e6 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/profile_page_skeleton.dart @@ -0,0 +1,62 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'menu_section_skeleton.dart'; +import 'profile_header_skeleton.dart'; +import 'reliability_score_skeleton.dart'; +import 'reliability_stats_skeleton.dart'; + +/// Full-page shimmer skeleton for [StaffProfilePage]. +/// +/// Mimics the loaded profile layout: header, reliability stats, score bar, +/// and four menu sections. Displayed while [ProfileCubit] fetches data. +class ProfilePageSkeleton extends StatelessWidget { + /// Creates a [ProfilePageSkeleton]. + const ProfilePageSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: SingleChildScrollView( + child: Column( + children: [ + // Header with avatar, name, and badge + const ProfileHeaderSkeleton(), + + // Content offset to overlap the header bottom radius + Transform.translate( + offset: const Offset(0, -UiConstants.space6), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + ), + child: Column( + spacing: UiConstants.space6, + children: const [ + // Reliability stats row (5 items) + ReliabilityStatsSkeleton(), + + // Reliability score bar + ReliabilityScoreSkeleton(), + + // Onboarding section (4 items, 3 columns) + MenuSectionSkeleton(itemCount: 4, crossAxisCount: 3), + + // Compliance section (3 items, 3 columns) + MenuSectionSkeleton(itemCount: 3, crossAxisCount: 3), + + // Finance section (3 items, 3 columns) + MenuSectionSkeleton(itemCount: 3, crossAxisCount: 3), + + // Support section (2 items, 3 columns) + MenuSectionSkeleton(itemCount: 2, crossAxisCount: 3), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/reliability_score_skeleton.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/reliability_score_skeleton.dart new file mode 100644 index 00000000..869c755e --- /dev/null +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/reliability_score_skeleton.dart @@ -0,0 +1,45 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the reliability score bar section. +/// +/// Mirrors [ReliabilityScoreBar] layout: a tinted container with a title line, +/// percentage line, progress bar placeholder, and description line. +class ReliabilityScoreSkeleton extends StatelessWidget { + /// Creates a [ReliabilityScoreSkeleton]. + const ReliabilityScoreSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.primary.withValues(alpha: 0.1), + borderRadius: UiConstants.radiusLg, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title row with label and percentage + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + UiShimmerLine(width: 120, height: 14), + UiShimmerLine(width: 40, height: 18), + ], + ), + const SizedBox(height: UiConstants.space2), + // Progress bar placeholder + const UiShimmerBox( + width: double.infinity, + height: 8, + borderRadius: UiConstants.radiusSm, + ), + const SizedBox(height: UiConstants.space2), + // Description line + const UiShimmerLine(width: 200, height: 10), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/reliability_stats_skeleton.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/reliability_stats_skeleton.dart new file mode 100644 index 00000000..a8d40bb4 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_page_skeleton/reliability_stats_skeleton.dart @@ -0,0 +1,54 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the reliability stats card. +/// +/// Mirrors [ReliabilityStatsCard] layout: a bordered card containing five +/// evenly-spaced stat columns, each with an icon circle, value line, and +/// label line. +class ReliabilityStatsSkeleton extends StatelessWidget { + /// Creates a [ReliabilityStatsSkeleton]. + const ReliabilityStatsSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.bgPopup, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _StatItemSkeleton(), + _StatItemSkeleton(), + _StatItemSkeleton(), + _StatItemSkeleton(), + _StatItemSkeleton(), + ], + ), + ); + } +} + +/// Single stat column shimmer: icon circle, value line, label line. +class _StatItemSkeleton extends StatelessWidget { + const _StatItemSkeleton(); + + @override + Widget build(BuildContext context) { + return const Expanded( + child: Column( + children: [ + UiShimmerBox(width: UiConstants.space10, height: UiConstants.space10), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 28, height: 14), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 36, height: 10), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart index 21e2c4c7..c393f0e0 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart @@ -11,6 +11,7 @@ import '../blocs/certificates/certificates_state.dart'; import '../widgets/add_certificate_card.dart'; import '../widgets/certificate_card.dart'; import '../widgets/certificates_header.dart'; +import '../widgets/certificates_skeleton/certificates_skeleton.dart'; /// Page for viewing and managing staff certificates. /// @@ -28,9 +29,7 @@ class CertificatesPage extends StatelessWidget { builder: (BuildContext context, CertificatesState state) { if (state.status == CertificatesStatus.loading || state.status == CertificatesStatus.initial) { - return const Scaffold( - body: Center(child: CircularProgressIndicator()), - ); + return const Scaffold(body: CertificatesSkeleton()); } if (state.status == CertificatesStatus.failure) { diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_skeleton/certificate_card_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_skeleton/certificate_card_skeleton.dart new file mode 100644 index 00000000..55b05acb --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_skeleton/certificate_card_skeleton.dart @@ -0,0 +1,38 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single certificate card. +class CertificateCardSkeleton extends StatelessWidget { + /// Creates a [CertificateCardSkeleton]. + const CertificateCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + margin: const EdgeInsets.only(bottom: UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + ), + child: const Row( + children: [ + UiShimmerCircle(size: 40), + SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 140, height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 100, height: 12), + ], + ), + ), + UiShimmerBox(width: 60, height: 28), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_skeleton/certificates_header_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_skeleton/certificates_header_skeleton.dart new file mode 100644 index 00000000..7e41aad5 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_skeleton/certificates_header_skeleton.dart @@ -0,0 +1,37 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the certificates progress header. +class CertificatesHeaderSkeleton extends StatelessWidget { + /// Creates a [CertificatesHeaderSkeleton]. + const CertificatesHeaderSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(UiConstants.space5), + decoration: const BoxDecoration(color: UiColors.primary), + child: SafeArea( + bottom: false, + child: Column( + children: [ + const SizedBox(height: UiConstants.space4), + const UiShimmerCircle(size: 64), + const SizedBox(height: UiConstants.space3), + UiShimmerLine( + width: 120, + height: 14, + ), + const SizedBox(height: UiConstants.space2), + UiShimmerLine( + width: 80, + height: 12, + ), + const SizedBox(height: UiConstants.space6), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_skeleton/certificates_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_skeleton/certificates_skeleton.dart new file mode 100644 index 00000000..30c461d9 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_skeleton/certificates_skeleton.dart @@ -0,0 +1,38 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'certificate_card_skeleton.dart'; +import 'certificates_header_skeleton.dart'; + +/// Full-page shimmer skeleton shown while certificates are loading. +class CertificatesSkeleton extends StatelessWidget { + /// Creates a [CertificatesSkeleton]. + const CertificatesSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: SingleChildScrollView( + child: Column( + children: [ + const CertificatesHeaderSkeleton(), + Transform.translate( + offset: const Offset(0, -UiConstants.space12), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + ), + child: UiShimmerList( + itemCount: 4, + spacing: UiConstants.space3, + itemBuilder: (int index) => + const CertificateCardSkeleton(), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/documents_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/documents_page.dart index 77e2a08d..353a0f70 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/documents_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/documents_page.dart @@ -10,6 +10,7 @@ import '../blocs/documents/documents_cubit.dart'; import '../blocs/documents/documents_state.dart'; import '../widgets/document_card.dart'; import '../widgets/documents_progress_card.dart'; +import '../widgets/documents_skeleton/documents_skeleton.dart'; class DocumentsPage extends StatelessWidget { const DocumentsPage({super.key}); @@ -28,11 +29,7 @@ class DocumentsPage extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, DocumentsState state) { if (state.status == DocumentsStatus.loading) { - return const Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(UiColors.primary), - ), - ); + return const DocumentsSkeleton(); } if (state.status == DocumentsStatus.failure) { return Center( diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/documents_skeleton/document_card_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/documents_skeleton/document_card_skeleton.dart new file mode 100644 index 00000000..6a5149d6 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/documents_skeleton/document_card_skeleton.dart @@ -0,0 +1,37 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single document card row. +class DocumentCardSkeleton extends StatelessWidget { + /// Creates a [DocumentCardSkeleton]. + const DocumentCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + color: UiColors.cardViewBackground, + ), + child: const Row( + children: [ + UiShimmerCircle(size: 40), + SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 160, height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 100, height: 12), + ], + ), + ), + UiShimmerBox(width: 24, height: 24), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/documents_skeleton/documents_progress_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/documents_skeleton/documents_progress_skeleton.dart new file mode 100644 index 00000000..e528ebb6 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/documents_skeleton/documents_progress_skeleton.dart @@ -0,0 +1,30 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the documents progress card. +class DocumentsProgressSkeleton extends StatelessWidget { + /// Creates a [DocumentsProgressSkeleton]. + const DocumentsProgressSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + color: UiColors.cardViewBackground, + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 140, height: 14), + SizedBox(height: UiConstants.space3), + UiShimmerBox(width: double.infinity, height: 8), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 80, height: 12), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/documents_skeleton/documents_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/documents_skeleton/documents_skeleton.dart new file mode 100644 index 00000000..8fdd205d --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/documents_skeleton/documents_skeleton.dart @@ -0,0 +1,32 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'document_card_skeleton.dart'; +import 'documents_progress_skeleton.dart'; + +/// Full-page shimmer skeleton shown while documents are loading. +class DocumentsSkeleton extends StatelessWidget { + /// Creates a [DocumentsSkeleton]. + const DocumentsSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: ListView( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + vertical: UiConstants.space6, + ), + children: [ + const DocumentsProgressSkeleton(), + const SizedBox(height: UiConstants.space4), + UiShimmerList( + itemCount: 5, + spacing: UiConstants.space3, + itemBuilder: (int index) => const DocumentCardSkeleton(), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart index bc350439..edeb738a 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart @@ -8,6 +8,7 @@ import 'package:krow_domain/krow_domain.dart'; import '../blocs/tax_forms/tax_forms_cubit.dart'; import '../blocs/tax_forms/tax_forms_state.dart'; import '../widgets/tax_forms_page/index.dart'; +import '../widgets/tax_forms_skeleton/tax_forms_skeleton.dart'; class TaxFormsPage extends StatelessWidget { const TaxFormsPage({super.key}); @@ -31,7 +32,7 @@ class TaxFormsPage extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, TaxFormsState state) { if (state.status == TaxFormsStatus.loading) { - return const Center(child: CircularProgressIndicator()); + return const TaxFormsSkeleton(); } if (state.status == TaxFormsStatus.failure) { diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_skeleton/tax_form_card_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_skeleton/tax_form_card_skeleton.dart new file mode 100644 index 00000000..ded5efe1 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_skeleton/tax_form_card_skeleton.dart @@ -0,0 +1,37 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single tax form card. +class TaxFormCardSkeleton extends StatelessWidget { + /// Creates a [TaxFormCardSkeleton]. + const TaxFormCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + color: UiColors.cardViewBackground, + ), + child: const Row( + children: [ + UiShimmerCircle(size: 40), + SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 120, height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 80, height: 12), + ], + ), + ), + UiShimmerBox(width: 60, height: 24), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_skeleton/tax_forms_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_skeleton/tax_forms_skeleton.dart new file mode 100644 index 00000000..a60e3dba --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/widgets/tax_forms_skeleton/tax_forms_skeleton.dart @@ -0,0 +1,55 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'tax_form_card_skeleton.dart'; + +/// Full-page shimmer skeleton shown while tax forms are loading. +class TaxFormsSkeleton extends StatelessWidget { + /// Creates a [TaxFormsSkeleton]. + const TaxFormsSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + vertical: UiConstants.space6, + ), + child: Column( + spacing: UiConstants.space4, + children: [ + // Info card placeholder + Container( + width: double.infinity, + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + color: UiColors.cardViewBackground, + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 180, height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(height: 12), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 200, height: 12), + ], + ), + ), + // Progress overview placeholder + const UiShimmerStatsCard(), + // Form card placeholders + UiShimmerList( + itemCount: 3, + spacing: UiConstants.space2, + itemBuilder: (int index) => const TaxFormCardSkeleton(), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart index 1d9fd651..c7a8bd8b 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart @@ -10,6 +10,7 @@ import '../blocs/bank_account_cubit.dart'; import '../blocs/bank_account_state.dart'; import '../widgets/account_card.dart'; import '../widgets/add_account_form.dart'; +import '../widgets/bank_account_skeleton/bank_account_skeleton.dart'; import '../widgets/security_notice.dart'; class BankAccountPage extends StatelessWidget { @@ -49,7 +50,7 @@ class BankAccountPage extends StatelessWidget { builder: (BuildContext context, BankAccountState state) { if (state.status == BankAccountStatus.loading && state.accounts.isEmpty) { - return const Center(child: CircularProgressIndicator()); + return const BankAccountSkeleton(); } if (state.status == BankAccountStatus.error) { diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/bank_account_skeleton/account_card_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/bank_account_skeleton/account_card_skeleton.dart new file mode 100644 index 00000000..0cedfaff --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/bank_account_skeleton/account_card_skeleton.dart @@ -0,0 +1,37 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single bank account card. +class AccountCardSkeleton extends StatelessWidget { + /// Creates an [AccountCardSkeleton]. + const AccountCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + color: UiColors.cardViewBackground, + ), + child: const Row( + children: [ + UiShimmerCircle(size: 40), + SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 140, height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 100, height: 12), + ], + ), + ), + UiShimmerBox(width: 48, height: 24), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/bank_account_skeleton/bank_account_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/bank_account_skeleton/bank_account_skeleton.dart new file mode 100644 index 00000000..539cd596 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/bank_account_skeleton/bank_account_skeleton.dart @@ -0,0 +1,32 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'account_card_skeleton.dart'; +import 'security_notice_skeleton.dart'; + +/// Full-page shimmer skeleton shown while bank accounts are loading. +class BankAccountSkeleton extends StatelessWidget { + /// Creates a [BankAccountSkeleton]. + const BankAccountSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SecurityNoticeSkeleton(), + const SizedBox(height: UiConstants.space4), + UiShimmerList( + itemCount: 2, + spacing: UiConstants.space3, + itemBuilder: (int index) => const AccountCardSkeleton(), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/bank_account_skeleton/security_notice_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/bank_account_skeleton/security_notice_skeleton.dart new file mode 100644 index 00000000..0d83d46b --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/bank_account_skeleton/security_notice_skeleton.dart @@ -0,0 +1,36 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the security notice banner. +class SecurityNoticeSkeleton extends StatelessWidget { + /// Creates a [SecurityNoticeSkeleton]. + const SecurityNoticeSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + color: UiColors.cardViewBackground, + ), + child: const Row( + children: [ + UiShimmerCircle(size: 24), + SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 160, height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(height: 12), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/pages/time_card_page.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/pages/time_card_page.dart index 80f5a327..77aecffc 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/pages/time_card_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/pages/time_card_page.dart @@ -8,6 +8,7 @@ import 'package:krow_core/core.dart'; import '../blocs/time_card_bloc.dart'; import '../widgets/month_selector.dart'; import '../widgets/shift_history_list.dart'; +import '../widgets/time_card_skeleton/time_card_skeleton.dart'; import '../widgets/time_card_summary.dart'; /// The main page for displaying the staff time card. @@ -50,7 +51,7 @@ class _TimeCardPageState extends State { }, builder: (BuildContext context, TimeCardState state) { if (state is TimeCardLoading) { - return const Center(child: CircularProgressIndicator()); + return const TimeCardSkeleton(); } else if (state is TimeCardError) { return Center( child: Padding( diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/month_selector_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/month_selector_skeleton.dart new file mode 100644 index 00000000..d6452723 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/month_selector_skeleton.dart @@ -0,0 +1,20 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the month selector row. +class MonthSelectorSkeleton extends StatelessWidget { + /// Creates a [MonthSelectorSkeleton]. + const MonthSelectorSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + UiShimmerCircle(size: 32), + UiShimmerLine(width: 120, height: 16), + UiShimmerCircle(size: 32), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/shift_history_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/shift_history_skeleton.dart new file mode 100644 index 00000000..b045392f --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/shift_history_skeleton.dart @@ -0,0 +1,42 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single shift history row. +class ShiftHistorySkeleton extends StatelessWidget { + /// Creates a [ShiftHistorySkeleton]. + const ShiftHistorySkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + color: UiColors.cardViewBackground, + ), + child: const Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 140, height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 100, height: 12), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + UiShimmerLine(width: 60, height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 40, height: 12), + ], + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/time_card_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/time_card_skeleton.dart new file mode 100644 index 00000000..3d952454 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/time_card_skeleton.dart @@ -0,0 +1,37 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'month_selector_skeleton.dart'; +import 'shift_history_skeleton.dart'; +import 'time_card_summary_skeleton.dart'; + +/// Full-page shimmer skeleton shown while time card data is loading. +class TimeCardSkeleton extends StatelessWidget { + /// Creates a [TimeCardSkeleton]. + const TimeCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + vertical: UiConstants.space6, + ), + child: Column( + children: [ + const MonthSelectorSkeleton(), + const SizedBox(height: UiConstants.space6), + const TimeCardSummarySkeleton(), + const SizedBox(height: UiConstants.space6), + UiShimmerList( + itemCount: 5, + spacing: UiConstants.space3, + itemBuilder: (int index) => const ShiftHistorySkeleton(), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/time_card_summary_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/time_card_summary_skeleton.dart new file mode 100644 index 00000000..92382057 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/time_card_skeleton/time_card_summary_skeleton.dart @@ -0,0 +1,19 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for the time card summary (hours + earnings). +class TimeCardSummarySkeleton extends StatelessWidget { + /// Creates a [TimeCardSummarySkeleton]. + const TimeCardSummarySkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + Expanded(child: UiShimmerStatsCard()), + SizedBox(width: UiConstants.space3), + Expanded(child: UiShimmerStatsCard()), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart index afcc60f4..2637c9c0 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart @@ -13,6 +13,7 @@ import '../widgets/attire_info_card.dart'; import '../widgets/attire_item_card.dart'; import '../widgets/attire_section_header.dart'; import '../widgets/attire_section_tab.dart'; +import '../widgets/attire_skeleton/attire_skeleton.dart'; class AttirePage extends StatefulWidget { const AttirePage({super.key}); @@ -49,7 +50,7 @@ class _AttirePageState extends State { }, builder: (BuildContext context, AttireState state) { if (state.status == AttireStatus.loading && state.options.isEmpty) { - return const Center(child: CircularProgressIndicator()); + return const AttireSkeleton(); } final List requiredItems = state.options diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_skeleton/attire_item_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_skeleton/attire_item_skeleton.dart new file mode 100644 index 00000000..6387185d --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_skeleton/attire_item_skeleton.dart @@ -0,0 +1,37 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single attire item card. +class AttireItemSkeleton extends StatelessWidget { + /// Creates an [AttireItemSkeleton]. + const AttireItemSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + color: UiColors.cardViewBackground, + ), + child: const Row( + children: [ + UiShimmerBox(width: 56, height: 56), + SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 120, height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(width: 80, height: 12), + ], + ), + ), + UiShimmerBox(width: 60, height: 24), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_skeleton/attire_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_skeleton/attire_skeleton.dart new file mode 100644 index 00000000..3090cfd9 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_skeleton/attire_skeleton.dart @@ -0,0 +1,63 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'attire_item_skeleton.dart'; + +/// Full-page shimmer skeleton shown while attire items are loading. +class AttireSkeleton extends StatelessWidget { + /// Creates an [AttireSkeleton]. + const AttireSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Info card placeholder + Container( + width: double.infinity, + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + color: UiColors.cardViewBackground, + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 160, height: 14), + SizedBox(height: UiConstants.space2), + UiShimmerLine(height: 12), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 200, height: 12), + ], + ), + ), + const SizedBox(height: UiConstants.space6), + // Section toggle chips placeholder + const Row( + children: [ + UiShimmerBox(width: 80, height: 32), + SizedBox(width: UiConstants.space3), + UiShimmerBox(width: 100, height: 32), + ], + ), + const SizedBox(height: UiConstants.space6), + // Section header placeholder + const UiShimmerSectionHeader(), + const SizedBox(height: UiConstants.space3), + // Attire item cards + UiShimmerList( + itemCount: 4, + spacing: UiConstants.space3, + itemBuilder: (int index) => const AttireItemSkeleton(), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart index a7cbf5cc..b450f4d7 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart @@ -7,6 +7,7 @@ import 'package:krow_core/core.dart'; import 'package:staff_profile_info/src/presentation/blocs/personal_info_bloc.dart'; import 'package:staff_profile_info/src/presentation/blocs/personal_info_state.dart'; import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/personal_info_content.dart'; +import 'package:staff_profile_info/src/presentation/widgets/personal_info_skeleton/personal_info_skeleton.dart'; /// The Personal Info page for staff onboarding. @@ -56,7 +57,7 @@ class PersonalInfoPage extends StatelessWidget { builder: (BuildContext context, PersonalInfoState state) { if (state.status == PersonalInfoStatus.loading || state.status == PersonalInfoStatus.initial) { - return const Center(child: CircularProgressIndicator()); + return const PersonalInfoSkeleton(); } if (state.staff == null) { diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_skeleton/form_field_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_skeleton/form_field_skeleton.dart new file mode 100644 index 00000000..4fd28d9a --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_skeleton/form_field_skeleton.dart @@ -0,0 +1,20 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single form field (label + input). +class FormFieldSkeleton extends StatelessWidget { + /// Creates a [FormFieldSkeleton]. + const FormFieldSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 80, height: 12), + SizedBox(height: UiConstants.space2), + UiShimmerBox(width: double.infinity, height: 48), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_skeleton/personal_info_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_skeleton/personal_info_skeleton.dart new file mode 100644 index 00000000..0a20ab5a --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_skeleton/personal_info_skeleton.dart @@ -0,0 +1,32 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'form_field_skeleton.dart'; + +/// Full-page shimmer skeleton shown while personal info is loading. +class PersonalInfoSkeleton extends StatelessWidget { + /// Creates a [PersonalInfoSkeleton]. + const PersonalInfoSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + children: [ + // Avatar placeholder + const Center(child: UiShimmerCircle(size: 80)), + const SizedBox(height: UiConstants.space6), + // Form fields + UiShimmerList( + itemCount: 5, + spacing: UiConstants.space5, + itemBuilder: (int index) => const FormFieldSkeleton(), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/widgets/faqs_skeleton/faq_item_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/widgets/faqs_skeleton/faq_item_skeleton.dart new file mode 100644 index 00000000..14407abc --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/widgets/faqs_skeleton/faq_item_skeleton.dart @@ -0,0 +1,29 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single FAQ accordion item. +class FaqItemSkeleton extends StatelessWidget { + /// Creates a [FaqItemSkeleton]. + const FaqItemSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + color: UiColors.cardViewBackground, + ), + child: const Row( + children: [ + Expanded( + child: UiShimmerLine(height: 14), + ), + SizedBox(width: UiConstants.space3), + UiShimmerCircle(size: 20), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/widgets/faqs_skeleton/faqs_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/widgets/faqs_skeleton/faqs_skeleton.dart new file mode 100644 index 00000000..5ab1e2f8 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/widgets/faqs_skeleton/faqs_skeleton.dart @@ -0,0 +1,54 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'faq_item_skeleton.dart'; + +/// Full-page shimmer skeleton shown while FAQs are loading. +class FaqsSkeleton extends StatelessWidget { + /// Creates a [FaqsSkeleton]. + const FaqsSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: Padding( + padding: const EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space5, + UiConstants.space5, + UiConstants.space24, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Search bar placeholder + UiShimmerBox( + width: double.infinity, + height: 48, + borderRadius: UiConstants.radiusLg, + ), + const SizedBox(height: UiConstants.space6), + // Category header + const UiShimmerSectionHeader(), + const SizedBox(height: UiConstants.space3), + // FAQ items + UiShimmerList( + itemCount: 3, + spacing: UiConstants.space2, + itemBuilder: (int index) => const FaqItemSkeleton(), + ), + const SizedBox(height: UiConstants.space6), + // Second category + const UiShimmerSectionHeader(), + const SizedBox(height: UiConstants.space3), + UiShimmerList( + itemCount: 3, + spacing: UiConstants.space2, + itemBuilder: (int index) => const FaqItemSkeleton(), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/widgets/faqs_widget.dart b/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/widgets/faqs_widget.dart index bda66591..80b1f00f 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/widgets/faqs_widget.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/presentation/widgets/faqs_widget.dart @@ -3,6 +3,7 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:staff_faqs/src/presentation/blocs/faqs_bloc.dart'; +import 'faqs_skeleton/faqs_skeleton.dart'; /// Widget displaying FAQs with search functionality and accordion items class FaqsWidget extends StatefulWidget { @@ -76,10 +77,7 @@ class _FaqsWidgetState extends State { // FAQ List or Empty State if (state.isLoading) - const Padding( - padding: EdgeInsets.symmetric(vertical: 48), - child: CircularProgressIndicator(), - ) + const FaqsSkeleton() else if (state.categories.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 48), diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/privacy_policy_page.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/privacy_policy_page.dart index 1f9c0379..7e2cf227 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/privacy_policy_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/privacy_policy_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import '../../blocs/legal/privacy_policy_cubit.dart'; +import '../../widgets/skeletons/legal_document_skeleton.dart'; /// Page displaying the Privacy Policy document class PrivacyPolicyPage extends StatelessWidget { @@ -24,9 +25,7 @@ class PrivacyPolicyPage extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, PrivacyPolicyState state) { if (state.isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); + return const LegalDocumentSkeleton(); } if (state.error != null) { diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/terms_of_service_page.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/terms_of_service_page.dart index e5e30c13..2be5be37 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/terms_of_service_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/legal/terms_of_service_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import '../../blocs/legal/terms_cubit.dart'; +import '../../widgets/skeletons/legal_document_skeleton.dart'; /// Page displaying the Terms of Service document class TermsOfServicePage extends StatelessWidget { @@ -24,9 +25,7 @@ class TermsOfServicePage extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, TermsState state) { if (state.isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); + return const LegalDocumentSkeleton(); } if (state.error != null) { diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/privacy_security_page.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/privacy_security_page.dart index df83b2cd..cbc8bd7b 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/privacy_security_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/pages/privacy_security_page.dart @@ -7,6 +7,7 @@ import 'package:flutter_modular/flutter_modular.dart'; import '../blocs/privacy_security_bloc.dart'; import '../widgets/legal/legal_section_widget.dart'; import '../widgets/privacy/privacy_section_widget.dart'; +import '../widgets/skeletons/privacy_security_skeleton.dart'; /// Page displaying privacy & security settings for staff class PrivacySecurityPage extends StatelessWidget { @@ -25,7 +26,7 @@ class PrivacySecurityPage extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, PrivacySecurityState state) { if (state.isLoading) { - return const UiLoadingPage(); + return const PrivacySecuritySkeleton(); } return const SingleChildScrollView( diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/skeletons/legal_document_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/skeletons/legal_document_skeleton.dart new file mode 100644 index 00000000..39176a89 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/skeletons/legal_document_skeleton.dart @@ -0,0 +1,61 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shared shimmer skeleton for legal document pages (Privacy Policy, Terms). +/// +/// Simulates a long-form text document with varied line widths. +class LegalDocumentSkeleton extends StatelessWidget { + /// Creates a [LegalDocumentSkeleton]. + const LegalDocumentSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title line + const UiShimmerLine(width: 200, height: 18), + const SizedBox(height: UiConstants.space4), + // Body text lines with varied widths + UiShimmerList( + itemCount: 4, + spacing: UiConstants.space2, + itemBuilder: (int index) => const UiShimmerLine(), + ), + const SizedBox(height: UiConstants.space5), + const UiShimmerLine(width: 180, height: 16), + const SizedBox(height: UiConstants.space3), + UiShimmerList( + itemCount: 5, + spacing: UiConstants.space2, + itemBuilder: (int index) => UiShimmerLine( + width: index == 4 ? 200 : double.infinity, + ), + ), + const SizedBox(height: UiConstants.space5), + const UiShimmerLine(width: 160, height: 16), + const SizedBox(height: UiConstants.space3), + UiShimmerList( + itemCount: 3, + spacing: UiConstants.space2, + itemBuilder: (int index) => const UiShimmerLine(), + ), + const SizedBox(height: UiConstants.space5), + const UiShimmerLine(width: 140, height: 16), + const SizedBox(height: UiConstants.space3), + UiShimmerList( + itemCount: 4, + spacing: UiConstants.space2, + itemBuilder: (int index) => UiShimmerLine( + width: index == 3 ? 160 : double.infinity, + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/skeletons/privacy_security_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/skeletons/privacy_security_skeleton.dart new file mode 100644 index 00000000..85db9d2d --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/skeletons/privacy_security_skeleton.dart @@ -0,0 +1,42 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'settings_toggle_skeleton.dart'; + +/// Full-page shimmer skeleton shown while privacy settings are loading. +class PrivacySecuritySkeleton extends StatelessWidget { + /// Creates a [PrivacySecuritySkeleton]. + const PrivacySecuritySkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return UiShimmer( + child: SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Privacy section header + const UiShimmerSectionHeader(), + const SizedBox(height: UiConstants.space4), + UiShimmerList( + itemCount: 3, + spacing: UiConstants.space4, + itemBuilder: (int index) => const SettingsToggleSkeleton(), + ), + const SizedBox(height: UiConstants.space6), + // Legal section header + const UiShimmerSectionHeader(), + const SizedBox(height: UiConstants.space4), + // Legal links + UiShimmerList( + itemCount: 2, + spacing: UiConstants.space3, + itemBuilder: (int index) => const UiShimmerListItem(), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/skeletons/settings_toggle_skeleton.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/skeletons/settings_toggle_skeleton.dart new file mode 100644 index 00000000..fc60ed97 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/presentation/widgets/skeletons/settings_toggle_skeleton.dart @@ -0,0 +1,31 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Shimmer placeholder for a single settings toggle row. +class SettingsToggleSkeleton extends StatelessWidget { + /// Creates a [SettingsToggleSkeleton]. + const SettingsToggleSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: UiConstants.space2), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UiShimmerLine(width: 140, height: 14), + SizedBox(height: UiConstants.space1), + UiShimmerLine(width: 200, height: 12), + ], + ), + ), + SizedBox(width: UiConstants.space3), + UiShimmerBox(width: 48, height: 28), + ], + ), + ); + } +}