/** * SceneGridSkeleton — placeholder grid pokazywany podczas ŁADOWANIA listy scen, * zamiast spinnera. Rysuje się natychmiast (zero danych), więc ekran "ożywa" w ms * nawet gdy backend liczy kilka sekund — percepcyjna szybkość jak w dużych apkach * (YT/Netflix pokazują szkielety/placeholdery zanim dane dojdą). * * Layout 1:1 z SceneTile (2-col, thumb 16:9 + linia tytułu + linia meta), żeby * przejście skeleton→content nie "skakało". Subtelny pulse (Animated opacity loop), * bez zewnętrznych zależności. */ import React from 'react'; import { Animated, StyleSheet, View } from 'react-native'; import { theme } from '../theme'; function usepulse() { const v = React.useRef(new Animated.Value(0.4)).current; React.useEffect(() => { const loop = Animated.loop( Animated.sequence([ Animated.timing(v, { toValue: 0.85, duration: 700, useNativeDriver: true }), Animated.timing(v, { toValue: 0.4, duration: 700, useNativeDriver: true }), ]), ); loop.start(); return () => loop.stop(); }, [v]); return v; } function SkeletonTile({ opacity }: { opacity: Animated.Value }) { return ( ); } /** `count` kafelków (parzysta liczba dla 2-col siatki). */ export function SceneGridSkeleton({ count = 8 }: { count?: number }) { const opacity = usepulse(); const rows = Math.ceil(count / 2); return ( {Array.from({ length: rows }).map((_, r) => ( ))} ); } const styles = StyleSheet.create({ grid: { marginTop: 2 }, row: { flexDirection: 'row', gap: 10, marginBottom: 14 }, tile: { flex: 1 }, thumb: { width: '100%', aspectRatio: 16 / 9, borderRadius: 6, backgroundColor: theme.bgElevated, }, lineTitle: { height: 12, borderRadius: 4, backgroundColor: theme.bgElevated, marginTop: 8, width: '85%', }, lineMeta: { height: 9, borderRadius: 4, backgroundColor: theme.bgElevated, marginTop: 6, width: '55%', }, });