fix(mobile): dark-mode white-on-white search input text

Same root cause as post-new.tsx/story-new.tsx: search.tsx imported the
static light-only colors/typography instead of useTheme(). Bigger
refactor than the other two since TabButton/PetRow/Loading/EmptyState
are separate top-level components, not closures inside the screen -
they now receive styles/colors as props from the theme-aware
makeStyles(colors) built in SearchScreen, rather than importing the
static tokens themselves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Sonnet 5 committed 2026-09-19 09:44:12 +02:00
1 parent 9185e54402
commit be2ab20273
2 files changed
+67 -46

No files matched your search

+65 -44
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import { View, Text, Pressable, FlatList, ActivityIndicator, StyleSheet } from "react-native";
import { useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
@@ -7,14 +7,16 @@ import { Search as SearchIcon, Layers2 } from "lucide-react-native";
import { Image } from "expo-image";
import { useTRPC } from "../../src/api/trpc";
import { useActivePet } from "../../src/context/ActivePetContext";
import { useTheme } from "../../src/context/ThemeContext";
import { useDebounce } from "../../src/hooks/useDebounce";
import { TextField } from "../../src/components/TextField";
import { PetAvatar } from "../../src/components/PetAvatar";
import { FollowButton } from "../../src/components/FollowButton";
import { getMediaUrl } from "../../src/lib/media-url";
import { colors, typography, radii } from "../../src/theme/tokens";
import { radii, getTypography, type ColorTokens } from "../../src/theme/tokens";
type Tab = "pets" | "people" | "tags";
type Styles = ReturnType<typeof makeStyles>;
type SearchPet = {
id: string;
@@ -35,6 +37,8 @@ export default function SearchScreen() {
const trpc = useTRPC();
const router = useRouter();
const { activePetId } = useActivePet();
const { colors } = useTheme();
const styles = useMemo(() => makeStyles(colors), [colors]);
const [query, setQuery] = useState("");
const [tab, setTab] = useState<Tab>("pets");
@@ -70,39 +74,39 @@ export default function SearchScreen() {
</View>
<View style={styles.tabs}>
<TabButton label={t("search.petsTab")} active={tab === "pets"} onPress={() => setTab("pets")} />
<TabButton label={t("search.peopleTab")} active={tab === "people"} onPress={() => setTab("people")} />
<TabButton label={t("search.tagsTab")} active={tab === "tags"} onPress={() => setTab("tags")} />
<TabButton styles={styles} label={t("search.petsTab")} active={tab === "pets"} onPress={() => setTab("pets")} />
<TabButton styles={styles} label={t("search.peopleTab")} active={tab === "people"} onPress={() => setTab("people")} />
<TabButton styles={styles} label={t("search.tagsTab")} active={tab === "tags"} onPress={() => setTab("tags")} />
</View>
{tab === "pets" &&
(!enabled ? (
<EmptyState text={t("search.minChars")} />
<EmptyState styles={styles} text={t("search.minChars")} />
) : loadingPets ? (
<Loading />
<Loading styles={styles} colors={colors} />
) : (
<FlatList
data={petResults?.pets ?? []}
keyExtractor={(pet) => pet.id}
contentContainerStyle={styles.list}
ListEmptyComponent={<EmptyState text={t("search.noPetsFor", { query: trimmed })} />}
ListEmptyComponent={<EmptyState styles={styles} text={t("search.noPetsFor", { query: trimmed })} />}
renderItem={({ item }) => (
<PetRow pet={item} activePetId={activePetId} onPress={() => router.push(`/pets/${item.id}`)} />
<PetRow styles={styles} pet={item} activePetId={activePetId} onPress={() => router.push(`/pets/${item.id}`)} />
)}
/>
))}
{tab === "people" &&
(!enabled ? (
<EmptyState text={t("search.minChars")} />
<EmptyState styles={styles} text={t("search.minChars")} />
) : loadingPeople ? (
<Loading />
<Loading styles={styles} colors={colors} />
) : (
<FlatList
data={ownerResults?.owners ?? []}
keyExtractor={(owner) => owner.id}
contentContainerStyle={styles.list}
ListEmptyComponent={<EmptyState text={t("search.noPeopleFor", { query: trimmed })} />}
ListEmptyComponent={<EmptyState styles={styles} text={t("search.noPeopleFor", { query: trimmed })} />}
renderItem={({ item: owner }) => {
const displayName = owner.username
? `@${owner.username}`
@@ -113,6 +117,7 @@ export default function SearchScreen() {
{owner.pets.map((pet) => (
<PetRow
key={pet.id}
styles={styles}
pet={pet}
activePetId={activePetId}
onPress={() => router.push(`/pets/${pet.id}`)}
@@ -127,16 +132,16 @@ export default function SearchScreen() {
{tab === "tags" &&
(normalizedTag.length < 1 ? (
<EmptyState text={t("search.enterHashtag")} />
<EmptyState styles={styles} text={t("search.enterHashtag")} />
) : loadingTags ? (
<Loading />
<Loading styles={styles} colors={colors} />
) : (
<FlatList
data={tagResults?.posts ?? []}
keyExtractor={(post) => post.id}
numColumns={3}
contentContainerStyle={styles.tagGrid}
ListEmptyComponent={<EmptyState text={t("search.noPostsForTag", { tag: normalizedTag })} />}
ListEmptyComponent={<EmptyState styles={styles} text={t("search.noPostsForTag", { tag: normalizedTag })} />}
renderItem={({ item: post }) => {
const firstImage = post.images[0];
const isCarousel = post.images.length > 1;
@@ -166,7 +171,17 @@ export default function SearchScreen() {
);
}
function TabButton({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
function TabButton({
styles,
label,
active,
onPress,
}: {
styles: Styles;
label: string;
active: boolean;
onPress: () => void;
}) {
return (
<Pressable style={[styles.tabButton, active && styles.tabButtonActive]} onPress={onPress}>
<Text style={[styles.tabButtonText, active && styles.tabButtonTextActive]}>{label}</Text>
@@ -175,11 +190,13 @@ function TabButton({ label, active, onPress }: { label: string; active: boolean;
}
function PetRow({
styles,
pet,
activePetId,
onPress,
indented,
}: {
styles: Styles;
pet: SearchPet;
activePetId: string | null;
onPress: () => void;
@@ -201,7 +218,7 @@ function PetRow({
);
}
function Loading() {
function Loading({ styles, colors }: { styles: Styles; colors: ColorTokens }) {
return (
<View style={styles.center}>
<ActivityIndicator color={colors.marigoldOrange} />
@@ -209,36 +226,40 @@ function Loading() {
);
}
function EmptyState({ text }: { text: string }) {
function EmptyState({ styles, text }: { styles: Styles; text: string }) {
return (
<View style={styles.center}>
<Text style={typography.body}>{text}</Text>
<Text style={styles.emptyText}>{text}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.porchWhite },
searchBar: { paddingHorizontal: 16, paddingTop: 12, justifyContent: "center" },
searchIcon: { position: "absolute", left: 28, zIndex: 1 },
searchInput: { paddingLeft: 36 },
tabs: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingVertical: 12 },
tabButton: { paddingHorizontal: 14, height: 36, borderRadius: radii.lg, justifyContent: "center", backgroundColor: colors.whisperGray },
tabButtonActive: { backgroundColor: colors.marigoldOrange },
tabButtonText: { ...typography.label, textTransform: "none", letterSpacing: 0, color: colors.ink },
tabButtonTextActive: { color: colors.pureWhite },
list: { padding: 16, flexGrow: 1 },
center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
petRow: { flexDirection: "row", alignItems: "center", gap: 10, paddingVertical: 8 },
petRowIndented: { marginLeft: 16 },
petRowMain: { flex: 1, flexDirection: "row", alignItems: "center", gap: 10 },
petRowText: { flex: 1 },
petName: { ...typography.body, fontFamily: typography.title.fontFamily },
petSpecies: { ...typography.label, textTransform: "none", letterSpacing: 0 },
ownerBlock: { marginBottom: 16 },
ownerName: { ...typography.body, fontFamily: typography.title.fontFamily, marginBottom: 4 },
tagGrid: { padding: 2 },
tagTile: { flex: 1, aspectRatio: 1, padding: 2 },
tagImage: { width: "100%", height: "100%", borderRadius: radii.sm },
tagCarouselBadge: { position: "absolute", top: 6, right: 6 },
});
function makeStyles(colors: ColorTokens) {
const typography = getTypography(colors);
return StyleSheet.create({
container: { flex: 1, backgroundColor: colors.porchWhite },
searchBar: { paddingHorizontal: 16, paddingTop: 12, justifyContent: "center" },
searchIcon: { position: "absolute", left: 28, zIndex: 1 },
searchInput: { paddingLeft: 36 },
tabs: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingVertical: 12 },
tabButton: { paddingHorizontal: 14, height: 36, borderRadius: radii.lg, justifyContent: "center", backgroundColor: colors.whisperGray },
tabButtonActive: { backgroundColor: colors.marigoldOrange },
tabButtonText: { ...typography.label, textTransform: "none", letterSpacing: 0, color: colors.ink },
tabButtonTextActive: { color: colors.pureWhite },
list: { padding: 16, flexGrow: 1 },
center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
emptyText: { ...typography.body },
petRow: { flexDirection: "row", alignItems: "center", gap: 10, paddingVertical: 8 },
petRowIndented: { marginLeft: 16 },
petRowMain: { flex: 1, flexDirection: "row", alignItems: "center", gap: 10 },
petRowText: { flex: 1 },
petName: { ...typography.body, fontFamily: typography.title.fontFamily },
petSpecies: { ...typography.label, textTransform: "none", letterSpacing: 0 },
ownerBlock: { marginBottom: 16 },
ownerName: { ...typography.body, fontFamily: typography.title.fontFamily, marginBottom: 4 },
tagGrid: { padding: 2 },
tagTile: { flex: 1, aspectRatio: 1, padding: 2 },
tagImage: { width: "100%", height: "100%", borderRadius: radii.sm },
tagCarouselBadge: { position: "absolute", top: 6, right: 6 },
});
}
+2 -2
View File
@@ -123,5 +123,5 @@ Neu 7:
- [x] Nutzer Feedback - DarkMode - Neuer Beitrag - Text die getippt werden sind weiß auf weißem grund.
→ Ursache gefunden: `app/(app)/post-new.tsx` gehörte noch zu den nicht auf Dark Mode migrierten Screens — es importierte die statischen, Light-only `colors`/`typography` aus `theme/tokens.ts` direkt, statt sie per `useTheme()` zu beziehen (gleiche Bug-Klasse wie die bereits behobenen Fälle aus dem Darkmode-Audit, siehe README/CLAUDE.md-Kommentar in `theme/tokens.ts`: "dark-mode support has to be added file-by-file"). Der Container-Hintergrund blieb dadurch immer hell (`colors.porchWhite`), während die geteilte `TextField`-Komponente **korrekt** theme-abhängige Textfarbe zieht (`colors.ink`, im Dark Mode hell) — macht bei aktivem Dark Mode aus hellem Text auf hellem Hintergrund den gemeldeten "weiß auf weiß"-Effekt in der Caption-Eingabe.
Fix: `post-new.tsx` auf `useTheme()` + `makeStyles(colors)`-Muster umgestellt (identisch zu `milestone-new.tsx`). Gleiches Muster/gleicher Fund auch in `story-new.tsx` entdeckt (dort ohne Texteingabe, macht sich als falscher heller Hintergrund statt unlesbarem Text bemerkbar) — gleich mitbehoben.
**Noch offen, nicht mitbehoben (größerer Umbau nötig):** `app/(app)/search.tsx` hat denselben Fehler (Suchfeld nutzt `TextField` + statische Farben) — dort referenzieren mehrere separate Unterkomponenten (`TabButton`, `PetRow`, `Loading`, `EmptyState`) die statischen `colors`/`typography` direkt außerhalb der Hauptkomponente, bräuchte also eigenes `useTheme()` pro Unterkomponente statt nur den einen `makeStyles`-Umbau. Separater Fix, auf Rückmeldung ob gewünscht.
Typecheck + Lint sauber (0 neue Warnungen, 6 vorbestehende `exhaustive-deps`-Warnungen unverändert).
Typecheck + Lint sauber (0 neue Warnungen, 6 vorbestehende `exhaustive-deps`-Warnungen unverändert).
**Nachtrag:** `app/(app)/search.tsx` (gleicher Fehler im Suchfeld) auf Nachfrage ebenfalls behoben — dort bekommen die separaten Unterkomponenten (`TabButton`, `PetRow`, `Loading`, `EmptyState`) `styles`/`colors` jetzt als Props von der Hauptkomponente durchgereicht, statt selbst die statischen Tokens zu importieren. Typecheck + Lint sauber (1 vorbestehende, unveränderte `alt-text`-Warnung bei `expo-image`).