From 0c6421c8de74f4affd5c1000d3e713e8da0bbe99 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sun, 6 Sep 2026 16:30:02 +0200 Subject: [PATCH] fix(mobile): dark mode on Explore/Notifications, breed dropdown overlay - Explore and Notifications tabs now read colors from ThemeContext instead of the static light-only tokens - OptionPicker's option list is position:absolute so it overlays following content instead of pushing it down, with zIndex on the component root (and FlatList's ListHeaderComponentStyle on Explore) so it actually paints above sibling content instead of underneath it - check off found.md's resolved items, record the three newly reported ones --- mobile/app/(app)/(tabs)/explore.tsx | 63 +++++++++----- mobile/app/(app)/(tabs)/notifications.tsx | 55 +++++++----- mobile/found.md | 11 +++ mobile/src/components/OptionPicker.tsx | 101 ++++++++++++++-------- 4 files changed, 149 insertions(+), 81 deletions(-) create mode 100644 mobile/found.md diff --git a/mobile/app/(app)/(tabs)/explore.tsx b/mobile/app/(app)/(tabs)/explore.tsx index d3019ae..f32bf21 100644 --- a/mobile/app/(app)/(tabs)/explore.tsx +++ b/mobile/app/(app)/(tabs)/explore.tsx @@ -1,13 +1,14 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { View, Text, FlatList, Pressable, ScrollView, ActivityIndicator, StyleSheet } from "react-native"; import { useRouter } from "expo-router"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useTRPC } from "../../../src/api/trpc"; +import { useTheme } from "../../../src/context/ThemeContext"; import { PetAvatar } from "../../../src/components/PetAvatar"; import { Card } from "../../../src/components/Card"; import { OptionPicker, type PickerOption } from "../../../src/components/OptionPicker"; import { formatPetName } from "../../../src/lib/format"; -import { colors, radii, typography } from "../../../src/theme/tokens"; +import { radii, getTypography, type ColorTokens, type Typography } from "../../../src/theme/tokens"; import type { ExplorePet } from "../../../src/api/types"; /** @@ -19,6 +20,8 @@ import type { ExplorePet } from "../../../src/api/types"; */ export default function ExploreScreen() { const trpc = useTRPC(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); const [speciesId, setSpeciesId] = useState(null); const [speciesName, setSpeciesName] = useState(null); const [breedId, setBreedId] = useState(null); @@ -57,6 +60,11 @@ export default function ExploreScreen() { numColumns={2} columnWrapperStyle={styles.row} contentContainerStyle={styles.list} + // FlatList wraps ListHeaderComponent in its own internal container, + // a sibling of the row containers — zIndex on `filters` itself only + // ranks it among ITS OWN children, so the breed dropdown's absolute + // overlay still painted underneath the grid without this. + ListHeaderComponentStyle={styles.headerWrapper} renderItem={({ item }) => } onEndReachedThreshold={0.5} onEndReached={() => { @@ -90,7 +98,7 @@ export default function ExploreScreen() { ListEmptyComponent={ !isLoading ? ( - + {breedName ? `Noch keine ${breedName}.` : speciesName ? `Noch keine ${speciesName}.` : "Keine Tiere gefunden."} @@ -101,6 +109,8 @@ export default function ExploreScreen() { } function SpeciesTab({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) { + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); return ( {label} @@ -118,6 +128,8 @@ function BreedFilter({ onSelect: (b: PickerOption | null) => void; }) { const trpc = useTRPC(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); const [open, setOpen] = useState(false); const { data: breeds = [], isLoading } = useQuery(trpc.pets.listBreeds.queryOptions({ speciesId, locale: "de" })); @@ -144,6 +156,8 @@ function BreedFilter({ function ExplorePetCard({ pet }: { pet: ExplorePet }) { const router = useRouter(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); return ( router.push(`/pets/${pet.id}`)}> @@ -157,21 +171,28 @@ function ExplorePetCard({ pet }: { pet: ExplorePet }) { ); } -const styles = StyleSheet.create({ - list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, - row: { gap: 12 }, - cardPress: { flex: 1 }, - card: { alignItems: "center", gap: 6, marginBottom: 12 }, - center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite }, - errorText: { ...typography.body, color: colors.alertRed }, - name: { ...typography.body, fontFamily: typography.title.fontFamily, fontSize: 14 }, - followers: { ...typography.label, textTransform: "none", letterSpacing: 0 }, - footer: { marginVertical: 16 }, - filters: { gap: 10, marginBottom: 12 }, - speciesTabs: { gap: 8 }, - tab: { paddingHorizontal: 14, height: 36, borderRadius: radii.lg, justifyContent: "center", backgroundColor: colors.whisperGray }, - tabActive: { backgroundColor: colors.marigoldOrange }, - tabText: { ...typography.label, textTransform: "none", letterSpacing: 0, color: colors.ink }, - tabTextActive: { color: colors.pureWhite }, - breedFilter: { width: 220 }, -}); +function makeStyles(colors: ColorTokens) { + const typography: Typography = getTypography(colors); + return StyleSheet.create({ + list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, + row: { gap: 12 }, + cardPress: { flex: 1 }, + card: { alignItems: "center", gap: 6, marginBottom: 12 }, + center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite }, + errorText: { ...typography.body, color: colors.alertRed }, + name: { ...typography.body, fontFamily: typography.title.fontFamily }, + followers: { ...typography.label, textTransform: "none", letterSpacing: 0 }, + footer: { marginVertical: 16 }, + // zIndex above the grid rows below it, so the breed dropdown (position: + // absolute inside OptionPicker) overlays the grid instead of being + // painted behind it — rows have no explicit zIndex (default 0). + headerWrapper: { zIndex: 10 }, + filters: { gap: 10, marginBottom: 12 }, + speciesTabs: { gap: 8 }, + tab: { paddingHorizontal: 14, height: 36, borderRadius: radii.lg, justifyContent: "center", backgroundColor: colors.whisperGray }, + tabActive: { backgroundColor: colors.marigoldOrange }, + tabText: { ...typography.label, textTransform: "none", letterSpacing: 0, color: colors.ink }, + tabTextActive: { color: colors.pureWhite }, + breedFilter: { width: 220 }, + }); +} diff --git a/mobile/app/(app)/(tabs)/notifications.tsx b/mobile/app/(app)/(tabs)/notifications.tsx index fd45f40..066abf1 100644 --- a/mobile/app/(app)/(tabs)/notifications.tsx +++ b/mobile/app/(app)/(tabs)/notifications.tsx @@ -1,12 +1,13 @@ -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { View, Text, FlatList, Pressable, ActivityIndicator, StyleSheet } from "react-native"; import { useFocusEffect } from "expo-router"; import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTRPC } from "../../../src/api/trpc"; import { useActivePet } from "../../../src/context/ActivePetContext"; +import { useTheme } from "../../../src/context/ThemeContext"; import { PetAvatar } from "../../../src/components/PetAvatar"; import { formatPetName, formatRelativeTime } from "../../../src/lib/format"; -import { colors, typography } from "../../../src/theme/tokens"; +import { getTypography, type ColorTokens, type Typography } from "../../../src/theme/tokens"; import type { NotificationItem } from "../../../src/api/types"; const ACTOR_MESSAGES: Partial string>> = { @@ -37,6 +38,8 @@ export default function NotificationsScreen() { const trpc = useTRPC(); const queryClient = useQueryClient(); const { activePetId } = useActivePet(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage, error } = useInfiniteQuery({ ...trpc.notifications.list.infiniteQueryOptions( @@ -113,37 +116,41 @@ export default function NotificationsScreen() { )} - {getNotificationText(item)} + {getNotificationText(item)} {formatRelativeTime(item.createdAt)} )} ListEmptyComponent={ - Keine Benachrichtigungen. + Keine Benachrichtigungen. } /> ); } -const styles = StyleSheet.create({ - list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, - separator: { height: 8 }, - center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite }, - errorText: { ...typography.body, color: colors.alertRed }, - row: { flexDirection: "row", gap: 12, padding: 12, borderRadius: 12 }, - rowUnread: { backgroundColor: colors.marigoldTint }, - rowText: { flex: 1, gap: 2 }, - timestamp: { ...typography.label, textTransform: "none", letterSpacing: 0 }, - footer: { marginVertical: 16 }, - selfIcon: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: colors.marigoldTint, - alignItems: "center", - justifyContent: "center", - }, - selfIconText: { fontSize: 18 }, -}); +function makeStyles(colors: ColorTokens) { + const typography: Typography = getTypography(colors); + return StyleSheet.create({ + list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, + separator: { height: 8 }, + center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite }, + errorText: { ...typography.body, color: colors.alertRed }, + bodyText: { ...typography.body }, + row: { flexDirection: "row", gap: 12, padding: 12, borderRadius: 12 }, + rowUnread: { backgroundColor: colors.marigoldTint }, + rowText: { flex: 1, gap: 2 }, + timestamp: { ...typography.label, textTransform: "none", letterSpacing: 0 }, + footer: { marginVertical: 16 }, + selfIcon: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: colors.marigoldTint, + alignItems: "center", + justifyContent: "center", + }, + selfIconText: { fontSize: 18 }, + }); +} diff --git a/mobile/found.md b/mobile/found.md new file mode 100644 index 0000000..a62a5c8 --- /dev/null +++ b/mobile/found.md @@ -0,0 +1,11 @@ +TODO: +- [x] Fehler: Explore ohne Filterfunktion +- [x] Benachrichtigung - Button klein Alles Gelesen markieren. +- [x] Head-Bereich verdeckt Handy-Infos nicht lesbarer Bereich Uhrzeit, WIFI Battery - Bitte GoogleRichtlinen beachten +- [x] Dark-Mode ? wo ? + +TODO Neu: +- [x] Explore Filter ist jetzt vorhanden, der Rassen-Dropdown, schiebt die Vorhanden elemente nach unten statt darüber zu lappen. +- [x] Explore Seite ist nur Halb im Darkmode +- [x] Benachrichtiungsseite nicht wirklich im Darkmode +- diff --git a/mobile/src/components/OptionPicker.tsx b/mobile/src/components/OptionPicker.tsx index cfe198e..9f9d17b 100644 --- a/mobile/src/components/OptionPicker.tsx +++ b/mobile/src/components/OptionPicker.tsx @@ -1,6 +1,8 @@ +import { useMemo } from "react"; import { View, Text, Pressable, ScrollView, ActivityIndicator, StyleSheet } from "react-native"; import { ChevronDown } from "lucide-react-native"; -import { colors, radii, typography } from "../theme/tokens"; +import { useTheme } from "../context/ThemeContext"; +import { radii, getTypography, type ColorTokens, type Typography } from "../theme/tokens"; export type PickerOption = { id: string; name: string }; @@ -16,15 +18,26 @@ type Props = { onSelect: (option: PickerOption) => void; }; +const BUTTON_HEIGHT = 48; +const LIST_GAP = 4; + /** - * A dropdown-style button that expands into a scrollable option list below - * it — extracted from onboarding/PetCreateForm once explore.tsx's breed - * filter needed the exact same thing. Deliberately a plain ScrollView + map, - * not a FlatList: this is meant to sit inside a screen-level ScrollView, and + * A dropdown-style button that expands into a scrollable option list — + * extracted from onboarding/PetCreateForm once explore.tsx's breed filter + * needed the exact same thing. Deliberately a plain ScrollView + map, not a + * FlatList: this is meant to sit inside a screen-level ScrollView, and * nesting a VirtualizedList inside a ScrollView with the same orientation * breaks touch/scroll handling (RN's "VirtualizedLists should never be * nested..." warning) — these option lists are at most a few dozen items, * so virtualization buys nothing here anyway. + * + * The option list is position:absolute so it overlays whatever comes after + * it instead of pushing it down (found.md: the breed dropdown on Explore was + * shoving the pet grid down the screen). zIndex is applied to the whole + * component root — not just the list — because the sibling content it needs + * to overlay renders after it in the same parent's paint order, and RN only + * lets zIndex reorder stacking among siblings, not push a child above its + * own subtree's siblings from the inside. */ export function OptionPicker({ open, @@ -37,8 +50,11 @@ export function OptionPicker({ emptyLabel, onSelect, }: Props) { + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); + return ( - + {selectedName ?? placeholder} @@ -53,7 +69,7 @@ export function OptionPicker({ {options.map((option) => ( onSelect(option)}> - {option.name} + {option.name} ))} @@ -64,32 +80,45 @@ export function OptionPicker({ ); } -const styles = StyleSheet.create({ - button: { - height: 48, - borderRadius: radii.lg, - borderWidth: 1, - borderColor: colors.hairlineGray, - paddingHorizontal: 14, - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - backgroundColor: colors.pureWhite, - }, - buttonDisabled: { opacity: 0.5 }, - value: { ...typography.body }, - placeholder: { ...typography.body, color: colors.quietGray }, - list: { - borderWidth: 1, - borderColor: colors.hairlineGray, - borderRadius: radii.lg, - marginTop: 4, - maxHeight: 220, - overflow: "hidden", - backgroundColor: colors.pureWhite, - }, - scrollView: { maxHeight: 220 }, - row: { paddingHorizontal: 14, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.hairlineGray }, - loading: { padding: 16 }, - empty: { ...typography.body, color: colors.quietGray, padding: 16 }, -}); +function makeStyles(colors: ColorTokens) { + const typography: Typography = getTypography(colors); + return StyleSheet.create({ + root: { zIndex: 0 }, + rootOpen: { zIndex: 30 }, + button: { + height: BUTTON_HEIGHT, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: colors.hairlineGray, + paddingHorizontal: 14, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + backgroundColor: colors.pureWhite, + }, + buttonDisabled: { opacity: 0.5 }, + value: { ...typography.body }, + placeholder: { ...typography.body, color: colors.quietGray }, + list: { + position: "absolute", + top: BUTTON_HEIGHT + LIST_GAP, + left: 0, + right: 0, + borderWidth: 1, + borderColor: colors.hairlineGray, + borderRadius: radii.lg, + maxHeight: 220, + overflow: "hidden", + backgroundColor: colors.pureWhite, + elevation: 8, + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + }, + scrollView: { maxHeight: 220 }, + row: { paddingHorizontal: 14, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.hairlineGray }, + loading: { padding: 16 }, + empty: { ...typography.body, color: colors.quietGray, padding: 16 }, + }); +}