@@ -9,6 +9,10 @@ import { PullToRefresh } from "@shared/components/ui/PullToRefresh";
99import { Button } from "@shared/components/ui/Button" ;
1010import { ConfirmDialog } from "@shared/components/ui/ConfirmDialog" ;
1111import { Skeleton } from "@shared/components/ui/Skeleton" ;
12+ import {
13+ DataState ,
14+ type DataStateQueryLike ,
15+ } from "@shared/components/ui/DataState" ;
1216import { useToast } from "@shared/hooks/useToast" ;
1317import { showUndoToast } from "@shared/lib/ui/undoToast" ;
1418import { WorkoutTemplatesSection } from "../components/WorkoutTemplatesSection" ;
@@ -36,6 +40,7 @@ import { useWorkouts } from "../hooks/useWorkouts";
3640import { recoveryConflictsForExercise } from "@sergeant/fizruk-domain" ;
3741import type { RawExerciseDef } from "@sergeant/fizruk-domain/data" ;
3842import type {
43+ Workout ,
3944 WorkoutFinishSummary ,
4045 WorkoutGroup ,
4146 WorkoutItem ,
@@ -446,6 +451,33 @@ export function Workouts() {
446451 // re-render automatically once the engine writes new state.
447452 const handlePullRefresh = useCallback ( ( ) => requestCloudPull ( 2500 ) , [ ] ) ;
448453
454+ // DataState contract for the workout journal:
455+ // - `useWorkouts` flips `loaded` from false → true after the first
456+ // hydration tick. While `loaded === false` we feed `data: undefined`
457+ // so DataState renders the skeleton slot; from the second tick on
458+ // `data` is the real list (possibly empty), which lets the journal
459+ // render its own "порожньо" empty-state without flashing it during
460+ // mount.
461+ // - `isLoading` mirrors the inverted `loaded` flag so a future
462+ // stale-revalidate (cloud pull → re-merge) keeps the list visible.
463+ const journalQuery : DataStateQueryLike < readonly Workout [ ] > = {
464+ data : workoutsLoaded ? workouts : undefined ,
465+ isLoading : ! workoutsLoaded ,
466+ } ;
467+
468+ const workoutsLoadingSkeleton = (
469+ < div
470+ className = "space-y-3"
471+ role = "status"
472+ aria-live = "polite"
473+ aria-label = "Завантажуємо тренування"
474+ >
475+ < Skeleton className = "h-28 w-full" />
476+ < Skeleton className = "h-20 w-full" />
477+ < Skeleton className = "h-20 w-full" />
478+ </ div >
479+ ) ;
480+
449481 return (
450482 < PullToRefresh onRefresh = { handlePullRefresh } variant = "fizruk" >
451483 < div className = "max-w-4xl mx-auto px-4 pt-4 page-tabbar-pad" >
@@ -511,51 +543,46 @@ export function Workouts() {
511543 />
512544 ) : null }
513545
514- { view === "log" && ! workoutsLoaded && (
515- // First-paint placeholder while `useWorkouts` is still rehydrating
516- // from `localStorage` (one tick on mount). Prevents the "порожньо"
517- // empty-state from flashing before real data renders — matches the
518- // Skeleton pattern already used in Finyk.
519- < div
520- className = "space-y-3"
521- role = "status"
522- aria-live = "polite"
523- aria-label = "Завантажуємо тренування"
524- >
525- < Skeleton className = "h-28 w-full" />
526- < Skeleton className = "h-20 w-full" />
527- < Skeleton className = "h-20 w-full" />
528- </ div >
529- ) }
530- { view === "log" && workoutsLoaded && (
531- < WorkoutJournalSection
532- activeWorkout = { activeWorkout }
533- activeDuration = { activeDuration }
534- workouts = { workouts }
535- activeWorkoutId = { activeWorkoutId }
536- setActiveWorkoutId = { setActiveWorkoutId }
537- retroOpen = { retroOpen }
538- setRetroOpen = { setRetroOpen }
539- retroDate = { retroDate }
540- setRetroDate = { setRetroDate }
541- retroTime = { retroTime }
542- setRetroTime = { setRetroTime }
543- createWorkout = { createWorkout }
544- setMode = { setView }
545- musclesUk = { musclesUk }
546- recBy = { rec . by }
547- lastByExerciseId = { lastByExerciseId }
548- setRestTimer = { setRestTimer }
549- updateWorkout = { updateWorkout }
550- updateItem = { updateItem }
551- removeItem = { removeItemWithUndo }
552- setFinishFlash = { setFinishFlash }
553- endWorkout = { endWorkout }
554- summarizeWorkoutForFinish = { summarizeWorkoutForFinish }
555- submitRetroWorkout = { submitRetroWorkout }
556- deleteWorkout = { deleteWorkout }
557- restoreWorkout = { restoreWorkout }
558- />
546+ { view === "log" && (
547+ // DataState contract: `data === undefined` triggers the skeleton
548+ // slot. `useWorkouts` flips `loaded` from false → true after one
549+ // tick on mount (it rehydrates from `localStorage` / SQLite),
550+ // so on first paint we feed `data: undefined` and from the
551+ // second tick onwards `data` is the real list — even when it
552+ // happens to be empty. This prevents the "порожньо" empty-state
553+ // inside `WorkoutJournalSection` from flashing during hydration.
554+ < DataState query = { journalQuery } skeleton = { workoutsLoadingSkeleton } >
555+ { ( ) => (
556+ < WorkoutJournalSection
557+ activeWorkout = { activeWorkout }
558+ activeDuration = { activeDuration }
559+ workouts = { workouts }
560+ activeWorkoutId = { activeWorkoutId }
561+ setActiveWorkoutId = { setActiveWorkoutId }
562+ retroOpen = { retroOpen }
563+ setRetroOpen = { setRetroOpen }
564+ retroDate = { retroDate }
565+ setRetroDate = { setRetroDate }
566+ retroTime = { retroTime }
567+ setRetroTime = { setRetroTime }
568+ createWorkout = { createWorkout }
569+ setMode = { setView }
570+ musclesUk = { musclesUk }
571+ recBy = { rec . by }
572+ lastByExerciseId = { lastByExerciseId }
573+ setRestTimer = { setRestTimer }
574+ updateWorkout = { updateWorkout }
575+ updateItem = { updateItem }
576+ removeItem = { removeItemWithUndo }
577+ setFinishFlash = { setFinishFlash }
578+ endWorkout = { endWorkout }
579+ summarizeWorkoutForFinish = { summarizeWorkoutForFinish }
580+ submitRetroWorkout = { submitRetroWorkout }
581+ deleteWorkout = { deleteWorkout }
582+ restoreWorkout = { restoreWorkout }
583+ />
584+ ) }
585+ </ DataState >
559586 ) }
560587
561588 { view === "templates" && (
0 commit comments