refactor(native): replace hardcoded pixel font sizes with semantic Tailwind classes and add ScaledIcon

Replace all `text-[Npx]` arbitrary size utilities with their nearest semantic Tailwind equivalents (`text-sm`, `text-base`, `text-xs`, `text-3xl`, etc.) across every component so text participates properly in the system font-scale pipeline.

Introduce a `ScaledIcon` component backed by a `useScaledSize` hook that multiplies a base icon size by the current font-scale factor, then replace every bare Tabler icon with `ScaledIcon` so icons grow and shrink in proportion with the surrounding text.

- Add `maxFontSizeMultiplier={1.0}` to badge and label text that must not reflow at large accessibility sizes (the "Admin" pill, type/rating chips, provider name labels, and the TMDB attribution line).
This commit is contained in:
2026-03-16 12:46:55 -04:00
parent 923651a7ae
commit ed3fd54896
46 changed files with 340 additions and 137 deletions
+32 -1
View File
@@ -37,5 +37,36 @@
}, },
"[css]": { "[css]": {
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome"
} },
"tailwindCSS.classAttributes": [
"class",
"className",
"headerClassName",
"contentContainerClassName",
"columnWrapperClassName",
"endFillColorClassName",
"imageClassName",
"tintColorClassName",
"ios_backgroundColorClassName",
"thumbColorClassName",
"trackColorOnClassName",
"trackColorOffClassName",
"selectionColorClassName",
"cursorColorClassName",
"underlineColorAndroidClassName",
"placeholderTextColorClassName",
"selectionHandleColorClassName",
"colorsClassName",
"progressBackgroundColorClassName",
"titleColorClassName",
"underlayColorClassName",
"colorClassName",
"backdropColorClassName",
"backgroundColorClassName",
"statusBarBackgroundColorClassName",
"drawerBackgroundColorClassName",
"ListFooterComponentClassName",
"ListHeaderComponentClassName"
],
"tailwindCSS.classFunctions": ["useResolveClassNames"]
} }
+14 -8
View File
@@ -17,6 +17,7 @@ import Animated, {
import { useCSSVariable } from "uniwind"; import { useCSSVariable } from "uniwind";
import { AuthScreen } from "@/components/auth-screen"; import { AuthScreen } from "@/components/auth-screen";
import { Button, ButtonLabel } from "@/components/ui/button"; import { Button, ButtonLabel } from "@/components/ui/button";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import { import {
getServerUrl, getServerUrl,
@@ -172,7 +173,7 @@ export default function ServerUrlScreen() {
returnKeyType="go" returnKeyType="go"
editable={!isDisabled} editable={!isDisabled}
onSubmitEditing={handleConnect} onSubmitEditing={handleConnect}
className="flex-1 py-3 font-mono text-[15px] text-foreground" className="flex-1 py-3 font-mono text-base text-foreground"
/> />
</View> </View>
</Animated.View> </Animated.View>
@@ -188,14 +189,18 @@ export default function ServerUrlScreen() {
className="size-1.5 rounded-full bg-primary" className="size-1.5 rounded-full bg-primary"
style={dotAnimatedStyle} style={dotAnimatedStyle}
/> />
<Text className="text-[13px] text-muted-foreground"> <Text className="text-muted-foreground text-sm">
Connecting to server... Connecting to server...
</Text> </Text>
</View> </View>
) : isSuccess ? ( ) : isSuccess ? (
<View className="min-h-12 flex-row items-center justify-center gap-1.5 py-2"> <View className="min-h-12 flex-row items-center justify-center gap-1.5 py-2">
<IconCircleCheck size={16} color={statusCompletedColor} /> <ScaledIcon
<Text className="font-medium font-sans text-[13px] text-status-completed"> icon={IconCircleCheck}
size={16}
color={statusCompletedColor}
/>
<Text className="font-medium font-sans text-sm text-status-completed">
Connected Connected
</Text> </Text>
</View> </View>
@@ -216,12 +221,13 @@ export default function ServerUrlScreen() {
entering={FadeIn.duration(200)} entering={FadeIn.duration(200)}
className="flex-row items-start gap-2 px-1" className="flex-row items-start gap-2 px-1"
> >
<IconAlertCircle <ScaledIcon
icon={IconAlertCircle}
size={16} size={16}
color={destructiveColor} color={destructiveColor}
style={{ marginTop: 1 }} style={{ marginTop: 1 }}
/> />
<Text selectable className="flex-1 text-[13px] text-destructive"> <Text selectable className="flex-1 text-destructive text-sm">
{ERROR_MESSAGES[connection.error]} {ERROR_MESSAGES[connection.error]}
</Text> </Text>
</Animated.View> </Animated.View>
@@ -236,8 +242,8 @@ export default function ServerUrlScreen() {
onPress={() => Linking.openURL("https://sofa.watch")} onPress={() => Linking.openURL("https://sofa.watch")}
className="flex-row items-center gap-1.5" className="flex-row items-center gap-1.5"
> >
<IconInfoCircle size={16} color={mutedFgColor} /> <ScaledIcon icon={IconInfoCircle} size={16} color={mutedFgColor} />
<Text className="font-medium font-sans text-[13px] text-primary"> <Text className="font-medium font-sans text-primary text-sm">
Don't have a server? Don't have a server?
</Text> </Text>
</Pressable> </Pressable>
@@ -166,7 +166,7 @@ export default function SearchScreen() {
entering={FadeIn.duration(300)} entering={FadeIn.duration(300)}
className="flex-1 items-center justify-center" className="flex-1 items-center justify-center"
> >
<Text className="text-[15px] text-muted-foreground"> <Text className="text-base text-muted-foreground">
No results for "{debouncedQuery}" No results for "{debouncedQuery}"
</Text> </Text>
</Animated.View> </Animated.View>
+20 -12
View File
@@ -39,6 +39,7 @@ import { SettingsRow } from "@/components/settings/settings-row";
import { SettingsSection } from "@/components/settings/settings-section"; import { SettingsSection } from "@/components/settings/settings-section";
import { TmdbLogo } from "@/components/tmdb-logo"; import { TmdbLogo } from "@/components/tmdb-logo";
import { Image } from "@/components/ui/image"; import { Image } from "@/components/ui/image";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
@@ -305,7 +306,7 @@ export default function SettingsScreen() {
value={nameInput} value={nameInput}
accessibilityLabel="Display name" accessibilityLabel="Display name"
onChangeText={setNameInput} onChangeText={setNameInput}
className="min-h-10 flex-1 border-primary border-b py-2 font-sans text-[15px] text-foreground" className="min-h-10 flex-1 border-primary border-b py-2 font-sans text-base text-foreground"
autoFocus autoFocus
/> />
<Pressable <Pressable
@@ -331,16 +332,16 @@ export default function SettingsScreen() {
</Text> </Text>
</Pressable> </Pressable>
)} )}
<Text <Text selectable className="mt-0.5 text-muted-foreground text-sm">
selectable
className="mt-0.5 text-[13px] text-muted-foreground"
>
{session?.user?.email} {session?.user?.email}
</Text> </Text>
</View> </View>
{isAdmin && ( {isAdmin && (
<View className="rounded-full bg-primary/10 px-2 py-0.5"> <View className="rounded-full bg-primary/10 px-2 py-0.5">
<Text className="font-medium font-sans text-[10px] text-primary"> <Text
maxFontSizeMultiplier={1.0}
className="font-medium font-sans text-primary text-xs"
>
Admin Admin
</Text> </Text>
</View> </View>
@@ -485,7 +486,7 @@ export default function SettingsScreen() {
/> />
{updateCheck.data?.updateCheck?.updateAvailable && ( {updateCheck.data?.updateCheck?.updateAvailable && (
<View className="py-3.5"> <View className="py-3.5">
<Text className="font-medium font-sans text-[13px] text-status-completed"> <Text className="font-medium font-sans text-sm text-status-completed">
Update available: {updateCheck.data.updateCheck.latestVersion} Update available: {updateCheck.data.updateCheck.latestVersion}
</Text> </Text>
</View> </View>
@@ -501,11 +502,15 @@ export default function SettingsScreen() {
onPress={() => Linking.openURL(`${serverUrl}/settings`)} onPress={() => Linking.openURL(`${serverUrl}/settings`)}
className="flex-row items-center justify-center py-3.5 active:opacity-70" className="flex-row items-center justify-center py-3.5 active:opacity-70"
> >
<IconWorld size={18} color={mutedFgColor} /> <ScaledIcon icon={IconWorld} size={18} color={mutedFgColor} />
<Text className="ml-2 flex-1 text-[15px] text-foreground"> <Text className="ml-2 flex-1 text-base text-foreground">
Open in browser Open in browser
</Text> </Text>
<IconArrowUpRight size={16} color={mutedFgColor} /> <ScaledIcon
icon={IconArrowUpRight}
size={16}
color={mutedFgColor}
/>
</Pressable> </Pressable>
</SettingsSection> </SettingsSection>
</Animated.View> </Animated.View>
@@ -538,7 +543,7 @@ export default function SettingsScreen() {
onPress={() => Linking.openURL("https://github.com/jakejarvis/sofa")} onPress={() => Linking.openURL("https://github.com/jakejarvis/sofa")}
className="flex-row items-center gap-1.5 active:opacity-70" className="flex-row items-center gap-1.5 active:opacity-70"
> >
<IconBrandGithub size={14} color={mutedFgColor} /> <ScaledIcon icon={IconBrandGithub} size={14} color={mutedFgColor} />
<Text className="text-muted-foreground text-xs">GitHub</Text> <Text className="text-muted-foreground text-xs">GitHub</Text>
</Pressable> </Pressable>
</Animated.View> </Animated.View>
@@ -553,7 +558,10 @@ export default function SettingsScreen() {
className="items-center gap-2 active:opacity-70" className="items-center gap-2 active:opacity-70"
> >
<TmdbLogo height={12} /> <TmdbLogo height={12} />
<Text className="text-center text-[10px] text-muted-foreground leading-relaxed"> <Text
maxFontSizeMultiplier={1.0}
className="text-center text-muted-foreground text-xs leading-relaxed"
>
This product uses the TMDB API but is not endorsed or certified by This product uses the TMDB API but is not endorsed or certified by
TMDB. TMDB.
</Text> </Text>
+1 -1
View File
@@ -185,7 +185,7 @@ export default function ChangePasswordScreen() {
className="flex-row items-center justify-between rounded-xl border border-border bg-input px-3.5 py-3" className="flex-row items-center justify-between rounded-xl border border-border bg-input px-3.5 py-3"
style={{ borderCurve: "continuous" }} style={{ borderCurve: "continuous" }}
> >
<Text className="text-[15px] text-foreground"> <Text className="text-base text-foreground">
Sign out of other sessions Sign out of other sessions
</Text> </Text>
<Switch <Switch
+10 -5
View File
@@ -24,6 +24,7 @@ import { useCSSVariable } from "uniwind";
import { ExpandableText } from "@/components/ui/expandable-text"; import { ExpandableText } from "@/components/ui/expandable-text";
import { Image } from "@/components/ui/image"; import { Image } from "@/components/ui/image";
import { PosterCard } from "@/components/ui/poster-card"; import { PosterCard } from "@/components/ui/poster-card";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { SectionHeader } from "@/components/ui/section-header"; import { SectionHeader } from "@/components/ui/section-header";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
@@ -294,7 +295,7 @@ export default function PersonDetailScreen() {
)} )}
</View> </View>
<Text className="mt-4 text-center font-display text-[28px] text-foreground"> <Text className="mt-4 text-center font-display text-3xl text-foreground">
{person.name} {person.name}
</Text> </Text>
@@ -311,8 +312,12 @@ export default function PersonDetailScreen() {
<View className="mt-3 items-center gap-1.5"> <View className="mt-3 items-center gap-1.5">
{person.birthday ? ( {person.birthday ? (
<View className="flex-row items-center gap-1.5"> <View className="flex-row items-center gap-1.5">
<IconCalendar size={14} color={primaryColor} /> <ScaledIcon
<Text selectable className="text-[13px] text-muted-foreground"> icon={IconCalendar}
size={14}
color={primaryColor}
/>
<Text selectable className="text-muted-foreground text-sm">
{format(parseISO(person.birthday), "MMMM d, yyyy")} {format(parseISO(person.birthday), "MMMM d, yyyy")}
{(() => { {(() => {
const age = calculateAge(person.birthday, person.deathday); const age = calculateAge(person.birthday, person.deathday);
@@ -325,8 +330,8 @@ export default function PersonDetailScreen() {
) : null} ) : null}
{person.placeOfBirth ? ( {person.placeOfBirth ? (
<View className="flex-row items-center gap-1.5"> <View className="flex-row items-center gap-1.5">
<IconMapPin size={14} color={primaryColor} /> <ScaledIcon icon={IconMapPin} size={14} color={primaryColor} />
<Text selectable className="text-[13px] text-muted-foreground"> <Text selectable className="text-muted-foreground text-sm">
{person.placeOfBirth} {person.placeOfBirth}
</Text> </Text>
</View> </View>
+20 -9
View File
@@ -36,6 +36,7 @@ import {
horizontalListStyle, horizontalListStyle,
} from "@/components/ui/horizontal-list-spacing"; } from "@/components/ui/horizontal-list-spacing";
import { Image } from "@/components/ui/image"; import { Image } from "@/components/ui/image";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { SectionHeader } from "@/components/ui/section-header"; import { SectionHeader } from "@/components/ui/section-header";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
@@ -444,12 +445,15 @@ export default function TitleDetailScreen() {
</Text> </Text>
<View className="mt-1.5 flex-row flex-wrap items-center gap-2"> <View className="mt-1.5 flex-row flex-wrap items-center gap-2">
<View className="rounded-full bg-title-accent px-2 py-0.5"> <View className="rounded-full bg-title-accent px-2 py-0.5">
<Text className="font-medium font-sans text-[10px] text-title-accent-foreground"> <Text
maxFontSizeMultiplier={1.0}
className="font-medium font-sans text-title-accent-foreground text-xs"
>
{title.type === "movie" ? "Movie" : "TV"} {title.type === "movie" ? "Movie" : "TV"}
</Text> </Text>
</View> </View>
{year ? ( {year ? (
<Text className="text-[13px] text-white/70">{year}</Text> <Text className="text-sm text-white/70">{year}</Text>
) : null} ) : null}
{title.contentRating ? ( {title.contentRating ? (
<Text className="text-white/50 text-xs"> <Text className="text-white/50 text-xs">
@@ -458,7 +462,11 @@ export default function TitleDetailScreen() {
) : null} ) : null}
{title.voteAverage != null && title.voteAverage > 0 && ( {title.voteAverage != null && title.voteAverage > 0 && (
<View className="flex-row items-center gap-0.5"> <View className="flex-row items-center gap-0.5">
<IconStarFilled size={12} color={titleAccent} /> <ScaledIcon
icon={IconStarFilled}
size={12}
color={titleAccent}
/>
<Text className="text-title-accent text-xs"> <Text className="text-title-accent text-xs">
{title.voteAverage.toFixed(1)} {title.voteAverage.toFixed(1)}
</Text> </Text>
@@ -483,9 +491,7 @@ export default function TitleDetailScreen() {
key={genre} key={genre}
className="mr-2 rounded-full bg-secondary px-2.5 py-1" className="mr-2 rounded-full bg-secondary px-2.5 py-1"
> >
<Text className="text-[11px] text-muted-foreground"> <Text className="text-muted-foreground text-xs">{genre}</Text>
{genre}
</Text>
</View> </View>
))} ))}
</ScrollView> </ScrollView>
@@ -527,7 +533,11 @@ export default function TitleDetailScreen() {
<Spinner size="sm" /> <Spinner size="sm" />
) : ( ) : (
<> <>
<IconCheck size={16} color={titleAccentForeground} /> <ScaledIcon
icon={IconCheck}
size={16}
color={titleAccentForeground}
/>
<Text className="font-medium font-sans text-sm text-title-accent-foreground"> <Text className="font-medium font-sans text-sm text-title-accent-foreground">
Mark Watched Mark Watched
</Text> </Text>
@@ -586,7 +596,8 @@ export default function TitleDetailScreen() {
/> />
)} )}
<Text <Text
className="mt-1 max-w-[60px] text-center text-[10px] text-muted-foreground" maxFontSizeMultiplier={1.0}
className="mt-1 max-w-[60px] text-center text-muted-foreground text-xs"
numberOfLines={1} numberOfLines={1}
> >
{offer.providerName} {offer.providerName}
@@ -633,7 +644,7 @@ export default function TitleDetailScreen() {
{hydrateMutation.isPending && ( {hydrateMutation.isPending && (
<View className="items-center py-6"> <View className="items-center py-6">
<Spinner colorClassName="accent-title-accent" /> <Spinner colorClassName="accent-title-accent" />
<Text className="mt-2 text-[13px] text-muted-foreground"> <Text className="mt-2 text-muted-foreground text-sm">
Loading season data... Loading season data...
</Text> </Text>
</View> </View>
+4 -1
View File
@@ -49,7 +49,10 @@ export function AuthScreen({
) : ( ) : (
<SofaLogo size={48} /> <SofaLogo size={48} />
)} )}
<Text className="mt-1 font-display text-[32px] text-foreground"> <Text
maxFontSizeMultiplier={1.3}
className="mt-1 font-display text-3xl text-foreground"
>
{title} {title}
</Text> </Text>
{subtitle && ( {subtitle && (
@@ -97,7 +97,7 @@ export function ContinueWatchingCard({ item }: { item: ContinueWatchingItem }) {
<View className="absolute right-2.5 bottom-3 left-2.5"> <View className="absolute right-2.5 bottom-3 left-2.5">
<Text <Text
numberOfLines={1} numberOfLines={1}
className="font-medium font-sans text-[11px] text-white/80" className="font-medium font-sans text-white/80 text-xs"
> >
S{item.nextEpisode.seasonNumber} E S{item.nextEpisode.seasonNumber} E
{item.nextEpisode.episodeNumber} {item.nextEpisode.episodeNumber}
@@ -119,14 +119,14 @@ export function ContinueWatchingCard({ item }: { item: ContinueWatchingItem }) {
<View className="p-2.5"> <View className="p-2.5">
<Text <Text
numberOfLines={1} numberOfLines={1}
className="font-medium font-sans text-[13px] text-foreground" className="font-medium font-sans text-foreground text-sm"
> >
{item.title.title} {item.title.title}
</Text> </Text>
{item.nextEpisode && ( {item.nextEpisode && (
<Text <Text
numberOfLines={1} numberOfLines={1}
className="mt-0.5 text-[11px] text-muted-foreground" className="mt-0.5 text-muted-foreground text-xs"
> >
{item.nextEpisode.name} {item.nextEpisode.name}
</Text> </Text>
@@ -14,12 +14,12 @@ export function StatsCard({
style={{ borderCurve: "continuous" }} style={{ borderCurve: "continuous" }}
> >
<Text <Text
className="font-bold font-sans text-[24px] text-primary" className="font-bold font-sans text-2xl text-primary"
style={{ fontVariant: ["tabular-nums"] }} style={{ fontVariant: ["tabular-nums"] }}
> >
{value ?? "—"} {value ?? "—"}
</Text> </Text>
<Text className="mt-0.5 text-[11px] text-muted-foreground">{label}</Text> <Text className="mt-0.5 text-muted-foreground text-xs">{label}</Text>
</View> </View>
); );
} }
@@ -135,7 +135,7 @@ export function FilterableTitleRow({
{!showLoading && items.length === 0 && selectedGenre !== null ? ( {!showLoading && items.length === 0 && selectedGenre !== null ? (
<View className="items-center py-6"> <View className="items-center py-6">
<Text className="text-[13px] text-muted-foreground"> <Text className="text-muted-foreground text-sm">
No titles found for this genre. No titles found for this genre.
</Text> </Text>
</View> </View>
@@ -28,7 +28,7 @@ export function GenreChip({
className={`mr-2 rounded-full px-3 py-1.5 ${isSelected ? "bg-primary" : "bg-secondary"}`} className={`mr-2 rounded-full px-3 py-1.5 ${isSelected ? "bg-primary" : "bg-secondary"}`}
> >
<Text <Text
className={`font-medium font-sans text-[12px] ${isSelected ? "text-primary-foreground" : "text-foreground"}`} className={`font-medium font-sans text-xs ${isSelected ? "text-primary-foreground" : "text-foreground"}`}
> >
{label} {label}
</Text> </Text>
@@ -13,6 +13,7 @@ import Animated, {
} from "react-native-reanimated"; } from "react-native-reanimated";
import { useCSSVariable } from "uniwind"; import { useCSSVariable } from "uniwind";
import { Image } from "@/components/ui/image"; import { Image } from "@/components/ui/image";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import { orpc } from "@/lib/orpc"; import { orpc } from "@/lib/orpc";
import { toast } from "@/lib/toast"; import { toast } from "@/lib/toast";
@@ -119,7 +120,7 @@ export function HeroBanner({ item }: { item: HeroBannerItem }) {
<View className="mt-2 flex-row items-center gap-2"> <View className="mt-2 flex-row items-center gap-2">
{item.voteAverage != null && item.voteAverage > 0 && ( {item.voteAverage != null && item.voteAverage > 0 && (
<View className="flex-row items-center gap-1"> <View className="flex-row items-center gap-1">
<IconStarFilled size={12} color={primary} /> <ScaledIcon icon={IconStarFilled} size={12} color={primary} />
<Text className="text-primary text-xs"> <Text className="text-primary text-xs">
{item.voteAverage.toFixed(1)} {item.voteAverage.toFixed(1)}
</Text> </Text>
+4 -1
View File
@@ -37,7 +37,10 @@ export function HeaderAvatar() {
/> />
) : ( ) : (
<View className="flex-1 items-center justify-center bg-primary/[0.08]"> <View className="flex-1 items-center justify-center bg-primary/[0.08]">
<Text className="font-medium font-sans text-[13px] text-primary"> <Text
maxFontSizeMultiplier={1.0}
className="font-medium font-sans text-primary text-sm"
>
{user.name?.charAt(0)?.toUpperCase() ?? "?"} {user.name?.charAt(0)?.toUpperCase() ?? "?"}
</Text> </Text>
</View> </View>
@@ -20,9 +20,7 @@ export function NativeTabBar() {
}), }),
[], [],
); );
const labelTextStyle = useResolveClassNames( const labelTextStyle = useResolveClassNames("font-medium font-sans text-xs");
"font-sans font-medium text-[11.5px]",
);
return ( return (
<NativeTabs <NativeTabs
@@ -75,7 +75,7 @@ export function RecentlyViewedList() {
className="flex-1 items-center justify-center" className="flex-1 items-center justify-center"
> >
<IconSearch size={64} color={mutedForeground} /> <IconSearch size={64} color={mutedForeground} />
<Text className="mt-3 text-[15px] text-muted-foreground"> <Text className="mt-3 text-base text-muted-foreground">
Search for movies, shows, or people Search for movies, shows, or people
</Text> </Text>
</Animated.View> </Animated.View>
@@ -100,7 +100,7 @@ export function RecentlyViewedList() {
<View className="flex-row items-center justify-between bg-background px-2"> <View className="flex-row items-center justify-between bg-background px-2">
<View className="flex-row items-center gap-2"> <View className="flex-row items-center gap-2">
<IconHistory size={20} color={primaryColor} /> <IconHistory size={20} color={primaryColor} />
<Text className="font-display text-[20px] text-foreground tracking-tight"> <Text className="font-display text-foreground text-xl tracking-tight">
Recently Viewed Recently Viewed
</Text> </Text>
</View> </View>
@@ -109,7 +109,7 @@ export function RecentlyViewedList() {
hitSlop={8} hitSlop={8}
style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })} style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
> >
<Text className="text-[13px] text-primary">Clear</Text> <Text className="text-primary text-sm">Clear</Text>
</Pressable> </Pressable>
</View> </View>
</RNHostView> </RNHostView>
@@ -76,7 +76,7 @@ export function RecentlyViewedList() {
className="flex-1 items-center justify-center" className="flex-1 items-center justify-center"
> >
<IconSearch size={64} color={mutedForeground} /> <IconSearch size={64} color={mutedForeground} />
<Text className="mt-3 text-[15px] text-muted-foreground"> <Text className="mt-3 text-base text-muted-foreground">
Search for movies, shows, or people Search for movies, shows, or people
</Text> </Text>
</Animated.View> </Animated.View>
@@ -98,7 +98,7 @@ export function RecentlyViewedList() {
> >
<View className="flex-row items-center gap-2"> <View className="flex-row items-center gap-2">
<IconHistory size={20} color={primaryColor} /> <IconHistory size={20} color={primaryColor} />
<Text className="font-display text-[20px] text-foreground tracking-tight"> <Text className="font-display text-foreground text-xl tracking-tight">
Recently Viewed Recently Viewed
</Text> </Text>
</View> </View>
@@ -107,7 +107,7 @@ export function RecentlyViewedList() {
hitSlop={8} hitSlop={8}
style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })} style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
> >
<Text className="text-[13px] text-primary">Clear</Text> <Text className="text-primary text-sm">Clear</Text>
</Pressable> </Pressable>
</Animated.View> </Animated.View>
} }
@@ -53,13 +53,16 @@ export function RecentlyViewedRowContent({
<View className="flex-1"> <View className="flex-1">
<Text <Text
numberOfLines={1} numberOfLines={1}
className="font-medium font-sans text-[15px] text-foreground" className="font-medium font-sans text-base text-foreground"
> >
{item.title} {item.title}
</Text> </Text>
<View className="mt-1 flex-row items-center gap-2"> <View className="mt-1 flex-row items-center gap-2">
<View className="rounded-full bg-secondary px-2 py-0.5"> <View className="rounded-full bg-secondary px-2 py-0.5">
<Text className="text-[10px] text-muted-foreground"> <Text
maxFontSizeMultiplier={1.0}
className="text-muted-foreground text-xs"
>
{TypeLabel[item.type]} {TypeLabel[item.type]}
</Text> </Text>
</View> </View>
@@ -3,6 +3,7 @@ import { memo } from "react";
import { Pressable, View } from "react-native"; import { Pressable, View } from "react-native";
import { useCSSVariable } from "uniwind"; import { useCSSVariable } from "uniwind";
import { Image } from "@/components/ui/image"; import { Image } from "@/components/ui/image";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
@@ -80,13 +81,16 @@ export const SearchResultRow = memo(function SearchResultRow({
<View className="flex-1"> <View className="flex-1">
<Text <Text
numberOfLines={1} numberOfLines={1}
className="font-medium font-sans text-[15px] text-foreground" className="font-medium font-sans text-base text-foreground"
> >
{item.title} {item.title}
</Text> </Text>
<View className="mt-1 flex-row items-center gap-2"> <View className="mt-1 flex-row items-center gap-2">
<View className="rounded-full bg-secondary px-2 py-0.5"> <View className="rounded-full bg-secondary px-2 py-0.5">
<Text className="text-[10px] text-muted-foreground"> <Text
maxFontSizeMultiplier={1.0}
className="text-muted-foreground text-xs"
>
{item.type === "movie" {item.type === "movie"
? "Movie" ? "Movie"
: item.type === "tv" : item.type === "tv"
@@ -113,9 +117,9 @@ export const SearchResultRow = memo(function SearchResultRow({
className="ml-2 items-center justify-center self-stretch" className="ml-2 items-center justify-center self-stretch"
> >
{isAdding ? ( {isAdding ? (
<IconLoader size={22} color={primary} /> <ScaledIcon icon={IconLoader} size={22} color={primary} />
) : ( ) : (
<IconPlus size={22} color={primary} /> <ScaledIcon icon={IconPlus} size={22} color={primary} />
)} )}
</Pressable> </Pressable>
)} )}
@@ -20,6 +20,7 @@ import Animated, {
} from "react-native-reanimated"; } from "react-native-reanimated";
import { useCSSVariable } from "uniwind"; import { useCSSVariable } from "uniwind";
import type { IntegrationConfig } from "@/components/settings/integration-configs"; import type { IntegrationConfig } from "@/components/settings/integration-configs";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import { orpc } from "@/lib/orpc"; import { orpc } from "@/lib/orpc";
import { queryClient } from "@/lib/query-client"; import { queryClient } from "@/lib/query-client";
@@ -176,13 +177,13 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
className="flex-row items-center justify-between p-3" className="flex-row items-center justify-between p-3"
> >
<View className="flex-row items-center gap-3"> <View className="flex-row items-center gap-3">
<Icon <ScaledIcon
width={18} icon={Icon}
height={18} size={18}
color={connection ? primaryColor : mutedFgColor} color={connection ? primaryColor : mutedFgColor}
/> />
<View> <View>
<Text className="font-medium font-sans text-[15px] text-foreground"> <Text className="font-medium font-sans text-base text-foreground">
{label} {label}
</Text> </Text>
<Text className="mt-0.5 text-muted-foreground text-xs"> <Text className="mt-0.5 text-muted-foreground text-xs">
@@ -194,7 +195,7 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
</View> </View>
<Animated.View style={chevronStyle}> <Animated.View style={chevronStyle}>
<IconChevronDown size={18} color={mutedFgColor} /> <ScaledIcon icon={IconChevronDown} size={18} color={mutedFgColor} />
</Animated.View> </Animated.View>
</Pressable> </Pressable>
@@ -208,12 +209,13 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
{/* Requirement note */} {/* Requirement note */}
{config.requirementNote && ( {config.requirementNote && (
<View className="mb-3 flex-row items-start gap-2 rounded-lg bg-primary/[0.06] px-3 py-2.5"> <View className="mb-3 flex-row items-start gap-2 rounded-lg bg-primary/[0.06] px-3 py-2.5">
<IconInfoCircle <ScaledIcon
icon={IconInfoCircle}
size={14} size={14}
color={primaryColor} color={primaryColor}
className="mt-px" className="mt-px"
/> />
<Text className="flex-1 text-[12px] text-foreground/80"> <Text className="flex-1 text-foreground/80 text-xs">
{config.requirementNote} {config.requirementNote}
</Text> </Text>
</View> </View>
@@ -241,7 +243,8 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
</Text> </Text>
<View className="flex-row items-center rounded-lg bg-secondary/50 px-3 py-2.5"> <View className="flex-row items-center rounded-lg bg-secondary/50 px-3 py-2.5">
<Text <Text
className="flex-1 font-mono text-[11px] text-muted-foreground" maxFontSizeMultiplier={1.0}
className="flex-1 font-mono text-muted-foreground text-xs"
numberOfLines={1} numberOfLines={1}
> >
{url} {url}
@@ -254,9 +257,13 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
hitSlop={8} hitSlop={8}
> >
{copied ? ( {copied ? (
<IconCheck size={16} color="#4ade80" /> <ScaledIcon icon={IconCheck} size={16} color="#4ade80" />
) : ( ) : (
<IconCopy size={16} color={mutedFgColor} /> <ScaledIcon
icon={IconCopy}
size={16}
color={mutedFgColor}
/>
)} )}
</Pressable> </Pressable>
</View> </View>
@@ -269,8 +276,12 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
disabled={regenerateMutation.isPending} disabled={regenerateMutation.isPending}
className="flex-1 flex-row items-center justify-center gap-1.5 rounded-lg bg-secondary py-2.5 active:opacity-80" className="flex-1 flex-row items-center justify-center gap-1.5 rounded-lg bg-secondary py-2.5 active:opacity-80"
> >
<IconRefresh size={14} color={mutedFgColor} /> <ScaledIcon
<Text className="font-medium font-sans text-[13px] text-foreground"> icon={IconRefresh}
size={14}
color={mutedFgColor}
/>
<Text className="font-medium font-sans text-foreground text-sm">
Regenerate Regenerate
</Text> </Text>
</Pressable> </Pressable>
@@ -278,8 +289,8 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
onPress={handleDisconnect} onPress={handleDisconnect}
className="flex-1 flex-row items-center justify-center gap-1.5 rounded-lg bg-destructive/10 py-2.5 active:opacity-80" className="flex-1 flex-row items-center justify-center gap-1.5 rounded-lg bg-destructive/10 py-2.5 active:opacity-80"
> >
<IconTrash size={14} color="#ef4444" /> <ScaledIcon icon={IconTrash} size={14} color="#ef4444" />
<Text className="font-medium font-sans text-[13px] text-destructive"> <Text className="font-medium font-sans text-destructive text-sm">
Disconnect Disconnect
</Text> </Text>
</Pressable> </Pressable>
@@ -294,7 +305,11 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
className="flex-row items-center gap-1.5" className="flex-row items-center gap-1.5"
> >
<Animated.View style={setupChevronStyle}> <Animated.View style={setupChevronStyle}>
<IconChevronDown size={12} color={mutedFgColor} /> <ScaledIcon
icon={IconChevronDown}
size={12}
color={mutedFgColor}
/>
</Animated.View> </Animated.View>
<Text className="text-muted-foreground text-xs"> <Text className="text-muted-foreground text-xs">
Setup instructions Setup instructions
@@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query";
import { Pressable, View } from "react-native"; import { Pressable, View } from "react-native";
import { IntegrationCard } from "@/components/settings/integration-card"; import { IntegrationCard } from "@/components/settings/integration-card";
import { INTEGRATION_CONFIGS } from "@/components/settings/integration-configs"; import { INTEGRATION_CONFIGS } from "@/components/settings/integration-configs";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { SectionHeader } from "@/components/ui/section-header"; import { SectionHeader } from "@/components/ui/section-header";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
@@ -21,7 +22,7 @@ export function IntegrationsSection() {
</View> </View>
) : integrations.isError ? ( ) : integrations.isError ? (
<View className="items-center gap-2 py-4"> <View className="items-center gap-2 py-4">
<Text className="text-[13px] text-muted-foreground"> <Text className="text-muted-foreground text-sm">
Could not load integrations Could not load integrations
</Text> </Text>
<Pressable <Pressable
@@ -29,8 +30,12 @@ export function IntegrationsSection() {
className="flex-row items-center gap-1.5 rounded-lg bg-secondary px-3 py-1.5" className="flex-row items-center gap-1.5 rounded-lg bg-secondary px-3 py-1.5"
style={{ borderCurve: "continuous" }} style={{ borderCurve: "continuous" }}
> >
<IconRefresh size={14} className="accent-primary" /> <ScaledIcon
<Text className="font-medium font-sans text-[13px] text-primary"> icon={IconRefresh}
size={14}
className="accent-primary"
/>
<Text className="font-medium font-sans text-primary text-sm">
Retry Retry
</Text> </Text>
</Pressable> </Pressable>
@@ -3,6 +3,7 @@ import { IconChevronRight } from "@tabler/icons-react-native";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { Pressable, View } from "react-native"; import { Pressable, View } from "react-native";
import { useCSSVariable } from "uniwind"; import { useCSSVariable } from "uniwind";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
export function SettingsRow({ export function SettingsRow({
@@ -29,13 +30,14 @@ export function SettingsRow({
const content = ( const content = (
<> <>
{IconComponent && ( {IconComponent && (
<IconComponent <ScaledIcon
icon={IconComponent}
size={18} size={18}
color={destructive ? destructiveColor : mutedFgColor} color={destructive ? destructiveColor : mutedFgColor}
/> />
)} )}
<Text <Text
className={`flex-1 text-[15px] ${destructive ? "text-destructive" : "text-foreground"} ${IconComponent ? "ml-2" : ""}`} className={`flex-1 text-base ${destructive ? "text-destructive" : "text-foreground"} ${IconComponent ? "ml-2" : ""}`}
> >
{label} {label}
</Text> </Text>
@@ -43,14 +45,19 @@ export function SettingsRow({
{!right && value ? ( {!right && value ? (
<Text <Text
selectable={!onPress} selectable={!onPress}
className="mr-1 max-w-[180px] shrink text-right text-[14px] text-muted-foreground" className="mr-1 max-w-[180px] shrink text-right text-muted-foreground text-sm"
numberOfLines={2} numberOfLines={2}
> >
{value} {value}
</Text> </Text>
) : null} ) : null}
{!right && onPress && ( {!right && onPress && (
<IconChevronRight size={16} color={mutedFgColor} accessible={false} /> <ScaledIcon
icon={IconChevronRight}
size={16}
color={mutedFgColor}
accessible={false}
/>
)} )}
</> </>
); );
@@ -33,11 +33,14 @@ export function SettingsSection({
return ( return (
<View className="mb-6"> <View className="mb-6">
<View className="mb-2 flex-row items-center gap-2"> <View className="flex-row items-center gap-2">
<SectionHeader title={title} icon={icon} /> <SectionHeader title={title} icon={icon} />
{badge ? ( {badge ? (
<View className="mb-2.5 rounded-full bg-primary/10 px-2 py-0.5"> <View className="mb-2.5 rounded-full bg-primary/10 px-2 py-0.5">
<Text className="font-medium font-sans text-[10px] text-primary"> <Text
maxFontSizeMultiplier={1.0}
className="font-medium font-sans text-primary text-xs"
>
{badge} {badge}
</Text> </Text>
</View> </View>
@@ -43,14 +43,16 @@ export function CastCard({
</View> </View>
<Text <Text
numberOfLines={1} numberOfLines={1}
className="text-center font-medium font-sans text-[11px] text-foreground" maxFontSizeMultiplier={1.2}
className="text-center font-medium font-sans text-foreground text-xs"
> >
{person.name} {person.name}
</Text> </Text>
{person.character ? ( {person.character ? (
<Text <Text
numberOfLines={1} numberOfLines={1}
className="text-center text-[10px] text-muted-foreground" maxFontSizeMultiplier={1.0}
className="text-center text-muted-foreground text-xs"
> >
{person.character} {person.character}
</Text> </Text>
@@ -70,15 +70,18 @@ export function ContinueWatchingBanner({
<View className="absolute right-3 bottom-2.5 left-3"> <View className="absolute right-3 bottom-2.5 left-3">
<View className="flex-row items-center gap-1.5"> <View className="flex-row items-center gap-1.5">
<View className="h-1.5 w-1.5 rounded-full bg-title-accent" /> <View className="h-1.5 w-1.5 rounded-full bg-title-accent" />
<Text className="font-medium font-sans text-[10px] text-title-accent uppercase tracking-wider"> <Text
maxFontSizeMultiplier={1.0}
className="font-medium font-sans text-title-accent text-xs uppercase tracking-wider"
>
Up next Up next
</Text> </Text>
</View> </View>
<Text <Text
numberOfLines={1} numberOfLines={1}
className="mt-0.5 font-medium font-sans text-[13px] text-white" className="mt-0.5 font-medium font-sans text-sm text-white"
> >
<Text className="font-medium font-sans text-[11px] text-white/60"> <Text className="font-medium font-sans text-white/60 text-xs">
S{nextEpisode.seasonNumber} E{nextEpisode.episodeNumber} S{nextEpisode.seasonNumber} E{nextEpisode.episodeNumber}
</Text> </Text>
{" "} {" "}
@@ -3,6 +3,7 @@ import {
IconCircleDashed, IconCircleDashed,
} from "@tabler/icons-react-native"; } from "@tabler/icons-react-native";
import { Pressable, View } from "react-native"; import { Pressable, View } from "react-native";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
export function EpisodeRow({ export function EpisodeRow({
@@ -35,9 +36,13 @@ export function EpisodeRow({
style={{ borderBottomWidth: 0.5 }} style={{ borderBottomWidth: 0.5 }}
> >
{isWatched ? ( {isWatched ? (
<IconCircleCheckFilled size={22} color={accentColor} /> <ScaledIcon
icon={IconCircleCheckFilled}
size={22}
color={accentColor}
/>
) : ( ) : (
<IconCircleDashed size={22} color={mutedColor} /> <ScaledIcon icon={IconCircleDashed} size={22} color={mutedColor} />
)} )}
<View className="ml-3 flex-1"> <View className="ml-3 flex-1">
<Text <Text
@@ -48,7 +53,7 @@ export function EpisodeRow({
{episode.name ?? `Episode ${episode.episodeNumber}`} {episode.name ?? `Episode ${episode.episodeNumber}`}
</Text> </Text>
{episode.airDate ? ( {episode.airDate ? (
<Text className="mt-0.5 text-[11px] text-muted-foreground"> <Text className="mt-0.5 text-muted-foreground text-xs">
{episode.airDate} {episode.airDate}
</Text> </Text>
) : null} ) : null}
@@ -12,6 +12,7 @@ import Animated, {
} from "react-native-reanimated"; } from "react-native-reanimated";
import { useCSSVariable } from "uniwind"; import { useCSSVariable } from "uniwind";
import { EpisodeRow } from "@/components/titles/episode-row"; import { EpisodeRow } from "@/components/titles/episode-row";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import { orpc } from "@/lib/orpc"; import { orpc } from "@/lib/orpc";
import { queryClient } from "@/lib/query-client"; import { queryClient } from "@/lib/query-client";
@@ -150,7 +151,7 @@ export function SeasonAccordion({
className="flex-row items-center justify-between p-4" className="flex-row items-center justify-between p-4"
> >
<View className="flex-1"> <View className="flex-1">
<Text className="font-medium font-sans text-[15px] text-foreground"> <Text className="font-medium font-sans text-base text-foreground">
{season.name ?? `Season ${season.seasonNumber}`} {season.name ?? `Season ${season.seasonNumber}`}
</Text> </Text>
<Text className="mt-0.5 text-muted-foreground text-xs"> <Text className="mt-0.5 text-muted-foreground text-xs">
@@ -172,7 +173,7 @@ export function SeasonAccordion({
</View> </View>
<Animated.View style={chevronStyle}> <Animated.View style={chevronStyle}>
<IconChevronDown size={18} color={mutedFgColor} /> <ScaledIcon icon={IconChevronDown} size={18} color={mutedFgColor} />
</Animated.View> </Animated.View>
</Pressable> </Pressable>
@@ -6,6 +6,7 @@ import {
} from "@tabler/icons-react-native"; } from "@tabler/icons-react-native";
import { Pressable } from "react-native"; import { Pressable } from "react-native";
import { useCSSVariable } from "uniwind"; import { useCSSVariable } from "uniwind";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import * as Haptics from "@/utils/haptics"; import * as Haptics from "@/utils/haptics";
@@ -62,7 +63,12 @@ export function StatusActionButton({
accessibilityLabel="Add to watchlist" accessibilityLabel="Add to watchlist"
className="flex-row items-center gap-1.5 rounded-lg border border-title-accent/20 bg-title-accent/10 px-4 py-2" className="flex-row items-center gap-1.5 rounded-lg border border-title-accent/20 bg-title-accent/10 px-4 py-2"
> >
<IconPlus size={14} color={titleAccent} strokeWidth={2.5} /> <ScaledIcon
icon={IconPlus}
size={14}
color={titleAccent}
strokeWidth={2.5}
/>
<Text className="font-medium font-sans text-sm text-title-accent"> <Text className="font-medium font-sans text-sm text-title-accent">
Watchlist Watchlist
</Text> </Text>
@@ -84,7 +90,7 @@ export function StatusActionButton({
accessibilityLabel={`${config.label}, double tap to remove`} accessibilityLabel={`${config.label}, double tap to remove`}
className={`flex-row items-center gap-1.5 rounded-lg border px-4 py-2 ${config.bgClass}`} className={`flex-row items-center gap-1.5 rounded-lg border px-4 py-2 ${config.bgClass}`}
> >
<config.Icon size={14} color={iconColor} /> <ScaledIcon icon={config.Icon} size={14} color={iconColor} />
<Text className={`font-medium font-sans text-sm ${config.textClass}`}> <Text className={`font-medium font-sans text-sm ${config.textClass}`}>
{config.label} {config.label}
</Text> </Text>
+1 -1
View File
@@ -67,7 +67,7 @@ export function ButtonLabel({ style, className, ...props }: ButtonLabelProps) {
return ( return (
<Text <Text
className={cn( className={cn(
"text-center font-medium font-sans text-[15px]", "text-center font-medium font-sans text-base",
variant === "secondary" variant === "secondary"
? "text-secondary-foreground" ? "text-secondary-foreground"
: "text-primary-foreground", : "text-primary-foreground",
@@ -32,11 +32,11 @@ export function EmptyState({
<View accessible={false}> <View accessible={false}>
<IconComponent size={48} color={mutedForeground} /> <IconComponent size={48} color={mutedForeground} />
</View> </View>
<Text className="mt-3 text-center font-medium font-sans text-[16px] text-foreground"> <Text className="mt-3 text-center font-medium font-sans text-base text-foreground">
{title} {title}
</Text> </Text>
{description && ( {description && (
<Text className="mt-1 text-center text-[13px] text-muted-foreground"> <Text className="mt-1 text-center text-muted-foreground text-sm">
{description} {description}
</Text> </Text>
)} )}
@@ -41,7 +41,7 @@ export function ExpandableText({
{/* Hidden text without numberOfLines to measure true line count */} {/* Hidden text without numberOfLines to measure true line count */}
<Text <Text
onTextLayout={onMeasureLayout} onTextLayout={onMeasureLayout}
className="pointer-events-none absolute text-[14px] leading-[22px] opacity-0" className="pointer-events-none absolute text-sm leading-relaxed opacity-0"
> >
{text} {text}
</Text> </Text>
@@ -49,7 +49,7 @@ export function ExpandableText({
<Text <Text
selectable={Platform.OS === "ios"} selectable={Platform.OS === "ios"}
numberOfLines={expanded ? undefined : maxLines} numberOfLines={expanded ? undefined : maxLines}
className="text-[14px] text-foreground leading-[22px]" className="text-foreground text-sm leading-relaxed"
> >
{text} {text}
</Text> </Text>
@@ -61,7 +61,7 @@ export function ExpandableText({
className="mt-1" className="mt-1"
> >
<Text <Text
className="font-medium font-sans text-[13px] text-primary" className="font-medium font-sans text-primary text-sm"
style={actionColor ? { color: actionColor } : undefined} style={actionColor ? { color: actionColor } : undefined}
> >
{expanded ? "Show less" : "Show more"} {expanded ? "Show less" : "Show more"}
@@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import Animated, { SlideInUp, SlideOutUp } from "react-native-reanimated"; import Animated, { SlideInUp, SlideOutUp } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import * as Haptics from "@/utils/haptics"; import * as Haptics from "@/utils/haptics";
@@ -67,15 +68,15 @@ export function OfflineBanner() {
paddingVertical: 10, paddingVertical: 10,
}} }}
> >
<IconWifiOff size={16} color="white" /> <ScaledIcon icon={IconWifiOff} size={16} color="white" />
<Text className="font-medium font-sans text-[13px] text-white"> <Text className="font-medium font-sans text-sm text-white">
No internet connection No internet connection
</Text> </Text>
</GlassView> </GlassView>
) : ( ) : (
<View className="mx-4 flex-row items-center justify-center gap-2 rounded-xl bg-destructive px-4 py-2.5"> <View className="mx-4 flex-row items-center justify-center gap-2 rounded-xl bg-destructive px-4 py-2.5">
<IconWifiOff size={16} color="white" /> <ScaledIcon icon={IconWifiOff} size={16} color="white" />
<Text className="font-medium font-sans text-[13px] text-white"> <Text className="font-medium font-sans text-sm text-white">
No internet connection No internet connection
</Text> </Text>
</View> </View>
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import Animated, { SlideInUp, SlideOutUp } from "react-native-reanimated"; import Animated, { SlideInUp, SlideOutUp } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import * as Haptics from "@/utils/haptics"; import * as Haptics from "@/utils/haptics";
@@ -52,8 +53,8 @@ export function OfflineBanner() {
}} }}
> >
<View className="mx-4 flex-row items-center justify-center gap-2 rounded-xl bg-destructive px-4 py-2.5"> <View className="mx-4 flex-row items-center justify-center gap-2 rounded-xl bg-destructive px-4 py-2.5">
<IconWifiOff size={16} color="white" /> <ScaledIcon icon={IconWifiOff} size={16} color="white" />
<Text className="font-medium font-sans text-[13px] text-white"> <Text className="font-medium font-sans text-sm text-white">
No internet connection No internet connection
</Text> </Text>
</View> </View>
+12 -7
View File
@@ -21,6 +21,7 @@ import Animated, {
import { useCSSVariable } from "uniwind"; import { useCSSVariable } from "uniwind";
import * as ContextMenu from "zeego/context-menu"; import * as ContextMenu from "zeego/context-menu";
import { Image } from "@/components/ui/image"; import { Image } from "@/components/ui/image";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import { client, orpc } from "@/lib/orpc"; import { client, orpc } from "@/lib/orpc";
@@ -156,7 +157,7 @@ export function PosterCard({
/> />
) : ( ) : (
<View className="flex-1 items-center justify-center bg-secondary p-3"> <View className="flex-1 items-center justify-center bg-secondary p-3">
<Text className="text-center font-display text-[13px] text-foreground/[0.44]"> <Text className="text-center font-display text-foreground/[0.44] text-sm">
{title} {title}
</Text> </Text>
</View> </View>
@@ -207,7 +208,7 @@ export function PosterCard({
/> />
)} )}
<Text <Text
className="flex-1 font-medium font-sans text-[13px] text-foreground" className="flex-1 font-medium font-sans text-foreground text-sm"
numberOfLines={1} numberOfLines={1}
> >
{title} {title}
@@ -215,17 +216,21 @@ export function PosterCard({
</View> </View>
<View className="mt-1 flex-row items-center gap-1" accessible={false}> <View className="mt-1 flex-row items-center gap-1" accessible={false}>
{type === "movie" ? ( {type === "movie" ? (
<IconMovie size={12} color={primaryColor} /> <ScaledIcon icon={IconMovie} size={12} color={primaryColor} />
) : ( ) : (
<IconDeviceTv size={12} color={primaryColor} /> <ScaledIcon icon={IconDeviceTv} size={12} color={primaryColor} />
)} )}
{year ? ( {year ? (
<Text className="text-[11px] text-muted-foreground">{year}</Text> <Text className="text-muted-foreground text-xs">{year}</Text>
) : null} ) : null}
{voteAverage != null && voteAverage > 0 && ( {voteAverage != null && voteAverage > 0 && (
<View className="ml-auto flex-row items-center gap-1"> <View className="ml-auto flex-row items-center gap-1">
<IconStarFilled size={10} color={primaryColor} /> <ScaledIcon
<Text className="text-[11px] text-primary"> icon={IconStarFilled}
size={10}
color={primaryColor}
/>
<Text className="text-primary text-xs">
{voteAverage.toFixed(1)} {voteAverage.toFixed(1)}
</Text> </Text>
</View> </View>
@@ -0,0 +1,24 @@
import type { ComponentType } from "react";
import type { SvgProps } from "react-native-svg";
import { useScaledSize } from "@/hooks/use-scaled-size";
interface ScaledIconProps extends SvgProps {
icon: ComponentType<SvgProps & { size?: number }>;
size: number;
}
/**
* Renders an icon scaled proportionally to the system font scale
* (iOS Dynamic Type / Android font size), capped at 1.5x.
*
* Use this for icons that sit inline with text so they grow alongside it.
*/
export function ScaledIcon({
icon: IconComponent,
size,
...props
}: ScaledIconProps) {
const scaled = useScaledSize();
const s = scaled(size);
return <IconComponent size={s} width={s} height={s} {...props} />;
}
@@ -1,6 +1,7 @@
import type { Icon } from "@tabler/icons-react-native"; import type { Icon } from "@tabler/icons-react-native";
import { View } from "react-native"; import { View } from "react-native";
import { useCSSVariable } from "uniwind"; import { useCSSVariable } from "uniwind";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
interface SectionHeaderProps { interface SectionHeaderProps {
@@ -19,8 +20,10 @@ export function SectionHeader({
return ( return (
<View className="mb-3 flex-row items-center gap-2"> <View className="mb-3 flex-row items-center gap-2">
{IconComponent && <IconComponent size={20} color={resolvedColor} />} {IconComponent && (
<Text className="font-display text-[20px] text-foreground tracking-tight"> <ScaledIcon icon={IconComponent} size={20} color={resolvedColor} />
)}
<Text className="font-display text-foreground text-xl tracking-tight">
{title} {title}
</Text> </Text>
</View> </View>
@@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react";
import { Pressable, View } from "react-native"; import { Pressable, View } from "react-native";
import Animated, { SlideInUp, SlideOutUp } from "react-native-reanimated"; import Animated, { SlideInUp, SlideOutUp } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { queryClient } from "@/lib/query-client"; import { queryClient } from "@/lib/query-client";
@@ -60,17 +61,15 @@ export function ServerUnreachableBanner() {
const content = ( const content = (
<> <>
<IconCloudOff size={16} color="white" /> <ScaledIcon icon={IconCloudOff} size={16} color="white" />
<Text className="font-medium font-sans text-[13px] text-white"> <Text className="font-medium font-sans text-sm text-white">
Can't reach server Can't reach server
</Text> </Text>
<Pressable <Pressable
onPress={handleRetry} onPress={handleRetry}
className="ml-1 rounded-md bg-white/20 px-2 py-0.5" className="ml-1 rounded-md bg-white/20 px-2 py-0.5"
> >
<Text className="font-medium font-sans text-[12px] text-white"> <Text className="font-medium font-sans text-white text-xs">Retry</Text>
Retry
</Text>
</Pressable> </Pressable>
</> </>
); );
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
import { Pressable, View } from "react-native"; import { Pressable, View } from "react-native";
import Animated, { SlideInUp, SlideOutUp } from "react-native-reanimated"; import Animated, { SlideInUp, SlideOutUp } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { queryClient } from "@/lib/query-client"; import { queryClient } from "@/lib/query-client";
@@ -68,15 +69,15 @@ export function ServerUnreachableBanner() {
}} }}
> >
<View className="mx-4 flex-row items-center justify-center gap-2 rounded-xl bg-status-watching px-4 py-2.5"> <View className="mx-4 flex-row items-center justify-center gap-2 rounded-xl bg-status-watching px-4 py-2.5">
<IconCloudOff size={16} color="white" /> <ScaledIcon icon={IconCloudOff} size={16} color="white" />
<Text className="font-medium font-sans text-[13px] text-white"> <Text className="font-medium font-sans text-sm text-white">
Can't reach server Can't reach server
</Text> </Text>
<Pressable <Pressable
onPress={handleRetry} onPress={handleRetry}
className="ml-1 rounded-md bg-white/20 px-2 py-0.5" className="ml-1 rounded-md bg-white/20 px-2 py-0.5"
> >
<Text className="font-medium font-sans text-[12px] text-white"> <Text className="font-medium font-sans text-white text-xs">
Retry Retry
</Text> </Text>
</Pressable> </Pressable>
@@ -6,6 +6,7 @@ import {
} from "@tabler/icons-react-native"; } from "@tabler/icons-react-native";
import { View } from "react-native"; import { View } from "react-native";
import { useCSSVariable } from "uniwind"; import { useCSSVariable } from "uniwind";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
type TitleStatus = "watchlist" | "in_progress" | "completed"; type TitleStatus = "watchlist" | "in_progress" | "completed";
@@ -44,9 +45,10 @@ export function StatusBadge({ status }: { status: TitleStatus }) {
<View <View
className={`flex-row items-center gap-1.5 rounded-full px-2.5 py-1 ${bgClasses[status]}`} className={`flex-row items-center gap-1.5 rounded-full px-2.5 py-1 ${bgClasses[status]}`}
> >
<StatusIcon size={12} color={colorMap[status]} /> <ScaledIcon icon={StatusIcon} size={12} color={colorMap[status]} />
<Text <Text
className={`font-medium font-sans text-[11px] ${textClasses[status]}`} maxFontSizeMultiplier={1.0}
className={`font-medium font-sans text-xs ${textClasses[status]}`}
> >
{label} {label}
</Text> </Text>
@@ -7,6 +7,7 @@ import Animated, {
interpolate, interpolate,
useAnimatedStyle, useAnimatedStyle,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import * as Haptics from "@/utils/haptics"; import * as Haptics from "@/utils/haptics";
const ACTION_WIDTH = 80; const ACTION_WIDTH = 80;
@@ -26,7 +27,7 @@ function RightAction({ drag }: { drag: SharedValue<number> }) {
style={{ width: ACTION_WIDTH }} style={{ width: ACTION_WIDTH }}
> >
<Animated.View style={animatedStyle}> <Animated.View style={animatedStyle}>
<IconTrash size={22} color="#fff" /> <ScaledIcon icon={IconTrash} size={22} color="#fff" />
</Animated.View> </Animated.View>
</View> </View>
); );
+2 -2
View File
@@ -52,7 +52,7 @@ export const Input = forwardRef<
accessibilityLabelledBy={ctxId} accessibilityLabelledBy={ctxId}
placeholderTextColorClassName="accent-muted-foreground/70" placeholderTextColorClassName="accent-muted-foreground/70"
className={cn( className={cn(
"min-h-12 rounded-[12px] border border-border bg-input px-3.5 py-3 font-sans text-[15px] text-foreground", "min-h-12 rounded-[12px] border border-border bg-input px-3.5 py-3 font-sans text-base text-foreground",
className, className,
)} )}
style={[{ borderCurve: "continuous" }, style]} style={[{ borderCurve: "continuous" }, style]}
@@ -73,7 +73,7 @@ export function FieldError({
return ( return (
<Text <Text
selectable selectable
className={cn("text-[13px] text-destructive", className)} className={cn("text-destructive text-sm", className)}
{...props} {...props}
> >
{children} {children}
+1 -1
View File
@@ -4,7 +4,7 @@ import { cn } from "@/utils/cn";
export function Text({ export function Text({
className, className,
maxFontSizeMultiplier, maxFontSizeMultiplier = 1.5,
...props ...props
}: TextProps & { className?: string }) { }: TextProps & { className?: string }) {
return ( return (
+16
View File
@@ -0,0 +1,16 @@
import { useWindowDimensions } from "react-native";
const MAX_MULTIPLIER = 1.5;
/**
* Returns a function that scales a base pixel size proportionally
* to the system font scale (iOS Dynamic Type / Android font size).
* Capped at 1.5x to match the Text component's maxFontSizeMultiplier.
*
* At the default font scale (1.0), returned values are unchanged.
*/
export function useScaledSize() {
const { fontScale } = useWindowDimensions();
const scale = Math.min(fontScale, MAX_MULTIPLIER);
return (baseSize: number) => Math.round(baseSize * scale);
}
@@ -10,7 +10,7 @@ type ScreenOptions = NonNullable<
export function useTabScreenOptions() { export function useTabScreenOptions() {
const backgroundColor = useCSSVariable("--color-background") as string; const backgroundColor = useCSSVariable("--color-background") as string;
const headerTitleStyle = useResolveClassNames( const headerTitleStyle = useResolveClassNames(
"font-sans font-semibold text-lg text-foreground", "font-sans font-semibold text-foreground text-lg",
); );
const contentStyle = useResolveClassNames("bg-background"); const contentStyle = useResolveClassNames("bg-background");
@@ -12,7 +12,7 @@ export function useTabScreenOptions() {
"font-display text-foreground", "font-display text-foreground",
); );
const headerTitleStyle = useResolveClassNames( const headerTitleStyle = useResolveClassNames(
"font-display text-lg text-foreground", "font-display text-foreground text-lg",
); );
const contentStyle = useResolveClassNames("bg-background"); const contentStyle = useResolveClassNames("bg-background");
+32 -2
View File
@@ -46,8 +46,38 @@
"useSortedClasses": { "useSortedClasses": {
"level": "error", "level": "error",
"options": { "options": {
"attributes": ["classList"], "attributes": [
"functions": ["cn", "clsx", "cva"] "class",
"classList",
"className",
"headerClassName",
"contentContainerClassName",
"columnWrapperClassName",
"endFillColorClassName",
"imageClassName",
"tintColorClassName",
"ios_backgroundColorClassName",
"thumbColorClassName",
"trackColorOnClassName",
"trackColorOffClassName",
"selectionColorClassName",
"cursorColorClassName",
"underlineColorAndroidClassName",
"placeholderTextColorClassName",
"selectionHandleColorClassName",
"colorsClassName",
"progressBackgroundColorClassName",
"titleColorClassName",
"underlayColorClassName",
"colorClassName",
"backdropColorClassName",
"backgroundColorClassName",
"statusBarBackgroundColorClassName",
"drawerBackgroundColorClassName",
"ListFooterComponentClassName",
"ListHeaderComponentClassName"
],
"functions": ["cn", "clsx", "cva", "useResolveClassNames"]
} }
} }
} }