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:
1 parent
9185e54402
commit
be2ab20273
2 files changed
+67
-46
No files matched your search
+65
-44
@@ -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
@@ -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`).
|
||||
Reference in new issue
Block a user