style(mobile): SceneTile shared component, 2-col grid w 5 ekranach scen
Jan feedback po pierwszym overhaulu: layout 2-col tile pasuje, ale aktywnie tylko na ScenesScreen - reszta ekranow scen (SiteScenes, PerformerScenes, StudioScenes, TagScenes) dalej w full-width row layoucie. Wyciagniety SceneTile do mobile/src/components/SceneTile.tsx ze wsparciem: - secondLine: 'studio' | 'performers' | 'date' | 'none' - per-ekran dobor metadanej (Studio na SiteScenes/Performer, performers na Studio, etc) - seenSince: ISO timestamp - pokazuje NEW badge gdy scene.created_at > seen (uzywane na Performer/Studio screens dla NEW od ostatniego markFavoriteSeen) - onLongPress: opcjonalny custom handler (default = animated preview) Refaktor 5 ekranow: - ScenesScreen: usuwa lokalna kopie SceneTile, import shared - SiteScenesScreen: SceneRow -> SceneTile (numColumns=2, secondLine='studio') - PerformerScenesScreen: FavoriteSceneRow -> SceneTile (numColumns=2) - StudioScenesScreen: FavoriteSceneRow -> SceneTile (numColumns=2, performers) - TagScenesScreen: lokalna SceneRow -> SceneTile FavoriteSceneRow component zostaje (legacy import w PerformerScenes - nie ruszamy bo moze byc uzyty w innym kontekscie). gridRow style scaffold (gap+ marginBottom) dodany w kazdym StyleSheet osobno bo te ekrany maja rozne paddingHorizontal w container. OTA: 9eea7ac6-df72-460e-9660-22bf6c39c3ac live, runtime 1.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aa647dcf97
commit
177a45eee7
6 changed files with 239 additions and 144 deletions
216
mobile/src/components/SceneTile.tsx
Normal file
216
mobile/src/components/SceneTile.tsx
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
/**
|
||||
* SceneTile — 2-col 16:9 grid item używany w listach scen
|
||||
* (Scenes, SiteScenes, PerformerScenes, StudioScenes, TagScenes).
|
||||
*
|
||||
* Per impeccable.style/slop + Jan feedback "większe miniaturki, mniej tekstu":
|
||||
* - Thumb wypełnia szerokość kolumny (aspect 16:9)
|
||||
* - Title 1 linijka, weight 600, letter-spacing -0.2
|
||||
* - Pod tytułem: 1 linia uppercase micro-meta (studio | performers | tag — wybór per ekran)
|
||||
* - Overlay na thumb: fav (top-left), duration (bottom-right), NEW (top-right gdy seenSince),
|
||||
* ✓watched (top-right gdy finished)
|
||||
* - Long-press → animated preview (gdy playback_source ma animated_thumbnail_url)
|
||||
*
|
||||
* Zostawia Pressable do parenta dla custom onLongPress (np. delete-from-favorites) —
|
||||
* default onLongPress robi preview (jak w ScenesScreen).
|
||||
*
|
||||
* Used inline w 2-column FlatList:
|
||||
* <FlatList
|
||||
* numColumns={2}
|
||||
* columnWrapperStyle={{ gap: 10, marginBottom: 14 }}
|
||||
* renderItem={({ item }) => <SceneTile scene={item} secondLine="studio" />}
|
||||
* />
|
||||
*/
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import React, { useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import type { RootStackParamList } from '../navigation';
|
||||
import { theme } from '../theme';
|
||||
import type { SceneOut } from '../types';
|
||||
import { Thumb } from './Thumb';
|
||||
|
||||
export type SecondLine = 'studio' | 'performers' | 'date' | 'none';
|
||||
|
||||
interface Props {
|
||||
scene: SceneOut;
|
||||
secondLine?: SecondLine;
|
||||
/**
|
||||
* Pokazuj NEW badge gdy scene.created_at > seenSince. Używane na ekranach
|
||||
* Performer/Studio scenes — gdy user owner'ował fav, last_seen_at z favorite
|
||||
* jest porównywany ze sceną.
|
||||
*/
|
||||
seenSince?: string;
|
||||
/**
|
||||
* Custom long-press handler (np. removal from favorites). Default = animated
|
||||
* preview gdy thumb ma animated wariant.
|
||||
*/
|
||||
onLongPress?: () => void;
|
||||
}
|
||||
|
||||
export function SceneTile({ scene, secondLine = 'studio', seenSince, onLongPress }: Props) {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const [isPreviewing, setIsPreviewing] = useState(false);
|
||||
|
||||
const animatedUrl = scene.playback_sources.find((s) => s.animated_thumbnail_url)
|
||||
?.animated_thumbnail_url;
|
||||
const staticUrl = scene.playback_sources.find((s) => s.thumbnail_url)?.thumbnail_url;
|
||||
const displayUrl = isPreviewing && animatedUrl ? animatedUrl : staticUrl ?? animatedUrl;
|
||||
|
||||
const startPreview = () => {
|
||||
if (onLongPress) {
|
||||
onLongPress();
|
||||
return;
|
||||
}
|
||||
if (!animatedUrl) return;
|
||||
setIsPreviewing(true);
|
||||
Haptics.selectionAsync().catch(() => {});
|
||||
};
|
||||
|
||||
const dim = scene.finished === true;
|
||||
const isNew = !!(seenSince && scene.created_at && scene.created_at > seenSince);
|
||||
const dur = scene.duration_sec;
|
||||
const durLabel =
|
||||
dur && dur > 0
|
||||
? dur >= 3600
|
||||
? `${Math.floor(dur / 3600)}h${String(Math.floor((dur % 3600) / 60)).padStart(2, '0')}`
|
||||
: `${Math.floor(dur / 60)}m`
|
||||
: null;
|
||||
|
||||
const meta = (() => {
|
||||
if (secondLine === 'none') return null;
|
||||
if (secondLine === 'studio') return scene.studio?.name || null;
|
||||
if (secondLine === 'performers') {
|
||||
if (scene.performers.length === 0) return null;
|
||||
const names = scene.performers.slice(0, 2).map((p) => p.canonical_name).join(', ');
|
||||
return scene.performers.length > 2 ? `${names} +${scene.performers.length - 2}` : names;
|
||||
}
|
||||
if (secondLine === 'date') return scene.release_date;
|
||||
return null;
|
||||
})();
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.tile}
|
||||
onPress={() => navigation.navigate('SceneDetail', { id: scene.id })}
|
||||
onLongPress={startPreview}
|
||||
onPressOut={() => setIsPreviewing(false)}
|
||||
delayLongPress={180}
|
||||
>
|
||||
<View style={[styles.thumbWrap, dim && styles.thumbDim]}>
|
||||
<Thumb url={displayUrl} style={styles.thumb} />
|
||||
{scene.is_favorite ? (
|
||||
<View style={styles.favBadge}>
|
||||
<Text style={styles.favBadgeText}>★</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{isNew ? (
|
||||
<View style={styles.newBadge}>
|
||||
<Text style={styles.newBadgeText}>NEW</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{dim ? (
|
||||
<View style={styles.watchedBadge}>
|
||||
<Text style={styles.watchedText}>✓</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{durLabel ? (
|
||||
<View style={styles.durBadge}>
|
||||
<Text style={styles.durText}>{durLabel}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<Text style={[styles.title, dim && styles.titleDim]} numberOfLines={1}>
|
||||
{scene.title}
|
||||
</Text>
|
||||
{meta ? (
|
||||
<Text style={styles.meta} numberOfLines={1}>
|
||||
{meta}
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tile: { flex: 1 },
|
||||
thumbWrap: {
|
||||
width: '100%',
|
||||
aspectRatio: 16 / 9,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
backgroundColor: theme.bgElevated,
|
||||
},
|
||||
thumb: { width: '100%', height: '100%' },
|
||||
thumbDim: { opacity: 0.45 },
|
||||
favBadge: {
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
left: 6,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 1,
|
||||
borderRadius: 6,
|
||||
},
|
||||
favBadgeText: { color: theme.accent, fontSize: 11, fontWeight: '700' },
|
||||
newBadge: {
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 6,
|
||||
backgroundColor: theme.accent,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
},
|
||||
newBadgeText: { color: theme.fg, fontSize: 9, fontWeight: '800', letterSpacing: 0.6 },
|
||||
watchedBadge: {
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 6,
|
||||
backgroundColor: 'rgba(0,0,0,0.78)',
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 999,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
watchedText: { color: theme.fg, fontSize: 11, fontWeight: '700' },
|
||||
durBadge: {
|
||||
position: 'absolute',
|
||||
bottom: 6,
|
||||
right: 6,
|
||||
backgroundColor: 'rgba(0,0,0,0.78)',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
},
|
||||
durText: {
|
||||
color: theme.fg,
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
fontVariant: ['tabular-nums'],
|
||||
},
|
||||
title: {
|
||||
color: theme.fg,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginTop: 8,
|
||||
letterSpacing: -0.2,
|
||||
},
|
||||
titleDim: { color: theme.muted },
|
||||
meta: {
|
||||
color: theme.muted,
|
||||
fontSize: 11,
|
||||
marginTop: 2,
|
||||
letterSpacing: 0.3,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
});
|
||||
|
||||
export const sceneTileGridProps = {
|
||||
numColumns: 2 as const,
|
||||
columnWrapperStyle: { gap: 10, marginBottom: 14 },
|
||||
};
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { useClient } from '../ClientContext';
|
||||
import { FavoriteSceneRow } from '../components/FavoriteSceneRow';
|
||||
import { MoviePosterCard } from '../components/MoviePosterCard';
|
||||
import { SceneTile } from '../components/SceneTile';
|
||||
import { ErrorBoundary } from '../ErrorBoundary';
|
||||
import type { RootStackParamList } from '../navigation';
|
||||
import { theme } from '../theme';
|
||||
|
|
@ -270,9 +271,11 @@ export function PerformerScenesScreen() {
|
|||
key="scenes-list"
|
||||
data={sortedScenes}
|
||||
keyExtractor={(s) => s.id}
|
||||
numColumns={2}
|
||||
renderItem={({ item }) => (
|
||||
<FavoriteSceneRow scene={item} seenSince={seenSince} secondLine="studio" />
|
||||
<SceneTile scene={item} seenSince={seenSince} secondLine="studio" />
|
||||
)}
|
||||
columnWrapperStyle={styles.gridRow}
|
||||
refreshing={scenesQuery.isRefetching}
|
||||
onRefresh={scenesQuery.refetch}
|
||||
ListHeaderComponent={
|
||||
|
|
@ -417,6 +420,7 @@ function TabButton({
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 12, paddingTop: 8 },
|
||||
gridRow: { gap: 10, marginBottom: 14 },
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { SceneTile } from '../components/SceneTile';
|
||||
import { Thumb } from '../components/Thumb';
|
||||
import { useClient } from '../ClientContext';
|
||||
import { theme } from '../theme';
|
||||
|
|
@ -223,80 +224,6 @@ function FavoritesButton() {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* SceneTile — 2-col 16:9 grid item, minimal text (video portal).
|
||||
*
|
||||
* Per impeccable.style/slop + Jan feedback "większe miniaturki, mniej tekstu":
|
||||
* - Thumb wypełnia szerokość kolumny (16:9 aspect ratio)
|
||||
* - Title 1 linijka, system font, weight 600
|
||||
* - Overlay: studio (mono small) + duration → tylko na thumb, NIE pod
|
||||
* - Wyrzucone: performers, release_date, sources count, "✓ watched" string
|
||||
* (zastąpione check badge na thumb)
|
||||
* - Long-press → animated preview (zachowane)
|
||||
*/
|
||||
function SceneTile({ scene }: { scene: SceneOut }) {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Scenes'>>();
|
||||
const [isPreviewing, setIsPreviewing] = useState(false);
|
||||
|
||||
const animatedUrl = scene.playback_sources.find((s) => s.animated_thumbnail_url)
|
||||
?.animated_thumbnail_url;
|
||||
const staticUrl = scene.playback_sources.find((s) => s.thumbnail_url)?.thumbnail_url;
|
||||
const displayUrl = isPreviewing && animatedUrl ? animatedUrl : staticUrl ?? animatedUrl;
|
||||
|
||||
const startPreview = () => {
|
||||
if (!animatedUrl) return;
|
||||
setIsPreviewing(true);
|
||||
Haptics.selectionAsync().catch(() => {});
|
||||
};
|
||||
|
||||
const dim = scene.finished === true;
|
||||
const dur = scene.duration_sec;
|
||||
const durLabel =
|
||||
dur && dur > 0
|
||||
? dur >= 3600
|
||||
? `${Math.floor(dur / 3600)}h${String(Math.floor((dur % 3600) / 60)).padStart(2, '0')}`
|
||||
: `${Math.floor(dur / 60)}m`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.tile}
|
||||
onPress={() => navigation.navigate('SceneDetail', { id: scene.id })}
|
||||
onLongPress={startPreview}
|
||||
onPressOut={() => setIsPreviewing(false)}
|
||||
delayLongPress={180}
|
||||
>
|
||||
<View style={[styles.tileThumbWrap, dim && styles.tileThumbDim]}>
|
||||
<Thumb url={displayUrl} style={styles.tileThumb} />
|
||||
{scene.is_favorite ? (
|
||||
<View style={styles.tileFavBadge}>
|
||||
<Text style={styles.tileFavBadgeText}>★</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{durLabel ? (
|
||||
<View style={styles.tileDurBadge}>
|
||||
<Text style={styles.tileDurText}>{durLabel}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{dim ? (
|
||||
<View style={styles.tileWatchedBadge}>
|
||||
<Text style={styles.tileWatchedText}>✓</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<Text style={[styles.tileTitle, dim && styles.tileTitleDim]} numberOfLines={1}>
|
||||
{scene.title}
|
||||
</Text>
|
||||
{scene.studio?.name ? (
|
||||
<Text style={styles.tileStudio} numberOfLines={1}>
|
||||
{scene.studio.name}
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function SceneRow({ scene }: { scene: SceneOut }) {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Scenes'>>();
|
||||
|
|
@ -506,71 +433,7 @@ const styles = StyleSheet.create({
|
|||
lineHeight: 15,
|
||||
},
|
||||
|
||||
// 2-col grid (SceneTile) — wprowadzone 2026-05-29 (UI overhaul, Jan feedback
|
||||
// "większe miniaturki, mniej tekstu"). Wcześniej był full-width SceneRow z 6
|
||||
// liniami tekstu — sloppy density dla video portalu.
|
||||
// 2-col grid wrapper — SceneTile sam ma styling tile/thumb/etc, tu tylko
|
||||
// odstęp między tilami w wierszu i pod wierszem.
|
||||
gridRow: { gap: 10, marginBottom: 14 },
|
||||
tile: { flex: 1 },
|
||||
tileThumbWrap: {
|
||||
width: '100%',
|
||||
aspectRatio: 16 / 9,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
backgroundColor: theme.bgElevated,
|
||||
},
|
||||
tileThumb: { width: '100%', height: '100%' },
|
||||
tileThumbDim: { opacity: 0.45 },
|
||||
tileFavBadge: {
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
left: 6,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 1,
|
||||
borderRadius: 6,
|
||||
},
|
||||
tileFavBadgeText: { color: theme.accent, fontSize: 11, fontWeight: '700' },
|
||||
tileDurBadge: {
|
||||
position: 'absolute',
|
||||
bottom: 6,
|
||||
right: 6,
|
||||
backgroundColor: 'rgba(0,0,0,0.78)',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
},
|
||||
tileDurText: {
|
||||
color: theme.fg,
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
fontVariant: ['tabular-nums'],
|
||||
},
|
||||
tileWatchedBadge: {
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 6,
|
||||
backgroundColor: 'rgba(0,0,0,0.78)',
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 999,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
tileWatchedText: { color: theme.fg, fontSize: 11, fontWeight: '700' },
|
||||
tileTitle: {
|
||||
color: theme.fg,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginTop: 8,
|
||||
letterSpacing: -0.2,
|
||||
},
|
||||
tileTitleDim: { color: theme.muted },
|
||||
tileStudio: {
|
||||
color: theme.muted,
|
||||
fontSize: 11,
|
||||
marginTop: 2,
|
||||
letterSpacing: 0.3,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
TextInput,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SceneTile } from '../components/SceneTile';
|
||||
import { Thumb } from '../components/Thumb';
|
||||
import { useClient } from '../ClientContext';
|
||||
import type { RootStackParamList } from '../navigation';
|
||||
|
|
@ -107,7 +108,9 @@ export function SiteScenesScreen() {
|
|||
<FlatList
|
||||
data={items}
|
||||
keyExtractor={(s) => s.id}
|
||||
renderItem={({ item }) => <SceneRow scene={item} />}
|
||||
numColumns={2}
|
||||
renderItem={({ item }) => <SceneTile scene={item} secondLine="studio" />}
|
||||
columnWrapperStyle={styles.gridRow}
|
||||
refreshing={isRefetching}
|
||||
onRefresh={refetch}
|
||||
onEndReached={() => {
|
||||
|
|
@ -353,6 +356,7 @@ const styles = StyleSheet.create({
|
|||
muted: { color: theme.muted, textAlign: 'center', marginTop: 24, fontSize: 14 },
|
||||
error: { color: theme.bad, padding: 12 },
|
||||
toolbar: { flexDirection: 'row', gap: 8, marginBottom: 8, alignItems: 'center' },
|
||||
gridRow: { gap: 10, marginBottom: 14 },
|
||||
filterBtn: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
View,
|
||||
} from 'react-native';
|
||||
import { FavoriteSceneRow } from '../components/FavoriteSceneRow';
|
||||
import { SceneTile } from '../components/SceneTile';
|
||||
import { useClient } from '../ClientContext';
|
||||
import type { RootStackParamList } from '../navigation';
|
||||
import { theme } from '../theme';
|
||||
|
|
@ -136,9 +137,11 @@ export function StudioScenesScreen() {
|
|||
<FlatList
|
||||
data={sortedItems}
|
||||
keyExtractor={(s) => s.id}
|
||||
numColumns={2}
|
||||
renderItem={({ item }) => (
|
||||
<FavoriteSceneRow scene={item} seenSince={seenSince} secondLine="performers" />
|
||||
<SceneTile scene={item} seenSince={seenSince} secondLine="performers" />
|
||||
)}
|
||||
columnWrapperStyle={styles.gridRow}
|
||||
refreshing={isRefetching}
|
||||
onRefresh={refetch}
|
||||
ListHeaderComponent={
|
||||
|
|
@ -157,6 +160,7 @@ export function StudioScenesScreen() {
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 12, paddingTop: 8 },
|
||||
gridRow: { gap: 10, marginBottom: 14 },
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SceneTile } from '../components/SceneTile';
|
||||
import { useClient } from '../ClientContext';
|
||||
import type { RootStackParamList } from '../navigation';
|
||||
import { theme } from '../theme';
|
||||
|
|
@ -46,7 +47,9 @@ export function TagScenesScreen() {
|
|||
<FlatList
|
||||
data={data?.items ?? []}
|
||||
keyExtractor={(s) => s.id}
|
||||
renderItem={({ item }) => <SceneRow scene={item} />}
|
||||
numColumns={2}
|
||||
renderItem={({ item }) => <SceneTile scene={item} secondLine="studio" />}
|
||||
columnWrapperStyle={styles.gridRow}
|
||||
refreshing={isRefetching}
|
||||
onRefresh={refetch}
|
||||
ListHeaderComponent={
|
||||
|
|
@ -99,6 +102,7 @@ function SceneRow({ scene }: { scene: SceneOut }) {
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: theme.bg, paddingHorizontal: 12, paddingTop: 8 },
|
||||
gridRow: { gap: 10, marginBottom: 14 },
|
||||
subtitle: { color: theme.muted, marginBottom: 8, paddingHorizontal: 4 },
|
||||
row: {
|
||||
backgroundColor: theme.card,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue