diff --git a/mobile/src/components/SceneGridSkeleton.tsx b/mobile/src/components/SceneGridSkeleton.tsx
new file mode 100644
index 0000000..f2a5079
--- /dev/null
+++ b/mobile/src/components/SceneGridSkeleton.tsx
@@ -0,0 +1,81 @@
+/**
+ * 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%',
+ },
+});
diff --git a/mobile/src/screens/ScenesScreen.tsx b/mobile/src/screens/ScenesScreen.tsx
index 0c332a3..19984d3 100644
--- a/mobile/src/screens/ScenesScreen.tsx
+++ b/mobile/src/screens/ScenesScreen.tsx
@@ -15,6 +15,7 @@ import {
import { Image } from 'expo-image';
import * as Haptics from 'expo-haptics';
import { SceneTile } from '../components/SceneTile';
+import { SceneGridSkeleton } from '../components/SceneGridSkeleton';
import { Thumb } from '../components/Thumb';
import { useClient } from '../ClientContext';
import { theme } from '../theme';
@@ -119,9 +120,11 @@ export function ScenesScreen() {
- {isLoading && }
{error instanceof Error && {error.message}}
+ {isLoading ? (
+
+ ) : (
s.id}
@@ -145,6 +148,7 @@ export function ScenesScreen() {
ListEmptyComponent={!isLoading ? no scenes : null}
contentContainerStyle={{ paddingBottom: 24 }}
/>
+ )}
- {isLoading && }
{error instanceof Error && {error.message}}
+ {isLoading ? (
+
+ ) : (
s.id}
@@ -62,6 +64,7 @@ export function TagScenesScreen() {
ListEmptyComponent={!isLoading ? no scenes : null}
contentContainerStyle={{ paddingBottom: 24 }}
/>
+ )}
);
}