mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
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:
Vendored
+32
-1
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
@@ -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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user