Ссылка прямо в тексте, которая показывает над собой карточку с картинкой. На телефоне карточка появляется по долгому нажатию и ведётся пальцем, на вебе — при наведении курсора. Карточка пружинисто следует за указателем по горизонтали, наклоняется в сторону движения, а картинка внутри слегка смещается навстречу — получается эффект параллакса. Короткий тап открывает href.
Оберните текст в LinkPreviewProvider, чтобы карточка рисовалась поверх соседних строк и не обрезалась. Пружины и наклон считаются на UI-потоке Reanimated; при «Уменьшении движения» наклон и параллакс отключаются, остаётся простое появление. Работает на iOS, Android и в вебе.
Поставьте зависимости:
npm install react-native-reanimated react-native-gesture-handlerpnpm add react-native-reanimated react-native-gesture-handleryarn add react-native-reanimated react-native-gesture-handlerbun add react-native-reanimated react-native-gesture-handlerСкопируйте эти файлы в src/shared/components/molecules/link-preview/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import React from "react";
import { StyleSheet, Text, View, useWindowDimensions } from "react-native";
import { LinkPreview, LinkPreviewProvider } from "@/shared/components/molecules/link-preview";
import { PHOTO_SOURCES } from "~/assets/photos";
import { Showcase, useCustomize } from "~/showcase";
const TITLE_SIZE = 36;
const CUSTOMIZE = [
{ key: "titleSize", label: "Размер заголовка", type: "slider", min: 20, max: 64, step: 1, unit: "px", default: TITLE_SIZE },
{ key: "kicker", label: "Надпись сверху", type: "text", default: "Сейчас читаю", maxLength: 40 },
{ key: "title", label: "Заголовок", type: "text", default: "Шпион × Семья", maxLength: 40 },
{ key: "width", label: "Ширина превью", type: "slider", min: 120, max: 400, step: 10, unit: "px", default: 260 },
{ key: "height", label: "Высота превью", type: "slider", min: 90, max: 320, step: 10, unit: "px", default: 190 },
{ key: "radius", label: "Скругление превью", type: "slider", min: 0, max: 32, step: 1, unit: "px", default: 12 },
{ key: "tilt", label: "Наклон при движении", type: "slider", min: 0, max: 20, step: 1, unit: "°", default: 8 },
{
key: "photos",
label: "Картинки",
type: "images",
count: 4,
default: [
PHOTO_SOURCES["cinematic-carousel-2"],
PHOTO_SOURCES["property-detail-v1-7"],
PHOTO_SOURCES["property-detail-v1-8"],
PHOTO_SOURCES["property-detail-v1-6"],
],
},
] as const;
export default function LinkPreviewScreen() {
const custom = useCustomize(CUSTOMIZE);
const { width } = useWindowDimensions();
const [titlePhoto, levPhoto, anyaPhoto, yaraPhoto] = custom.photos;
const shared = { radius: custom.radius, tilt: custom.tilt };
const titleSize = custom.titleSize === TITLE_SIZE ? (width >= 640 ? 36 : 30) : custom.titleSize;
return (
<Showcase>
<LinkPreviewProvider style={styles.page}>
<View style={styles.frame}>
<View style={styles.inner}>
<View style={styles.heading}>
<Text style={styles.kicker}>{custom.kicker.toUpperCase()}</Text>
<LinkPreview
href="#"
src={titlePhoto!}
alt={custom.title}
width={custom.width}
height={custom.height}
{...shared}
style={[styles.title, { fontSize: titleSize, lineHeight: titleSize * 1.2, letterSpacing: -0.025 * titleSize }]}
>
{custom.title}
</LinkPreview>
</View>
<Text style={styles.paragraph}>
Чтобы сохранить мир, лучший разведчик под кодовым именем{" "}
<LinkPreview href="#" src={levPhoto!} alt="Лев Форов" {...shared} style={styles.inline}>
Лев
</LinkPreview>{" "}
должен создать фиктивную семью и проникнуть в элитную школу. Сам того не зная, он берёт в дочери девочку-телепата по имени{" "}
<LinkPreview href="#" src={anyaPhoto!} alt="Аня Форова" {...shared} style={styles.inline}>
Аня
</LinkPreview>{" "}
и в жёны профессиональную убийцу по имени{" "}
<LinkPreview href="#" src={yaraPhoto!} alt="Яра Форова" {...shared} style={styles.inline}>
Яра
</LinkPreview>
. Вместе они разбираются с «мелочами» повседневной жизни, скрывая свои тайны друг от друга.
</Text>
</View>
</View>
</LinkPreviewProvider>
</Showcase>
);
}
const styles = StyleSheet.create({
page: { flex: 1, backgroundColor: "#0a0a0a" },
frame: { flex: 1, justifyContent: "center", paddingVertical: 48 },
inner: { alignItems: "center", gap: 40, paddingHorizontal: 24, paddingVertical: 48 },
heading: { alignItems: "center" },
kicker: {
marginBottom: 12,
fontSize: 12,
lineHeight: 16,
fontWeight: "500",
letterSpacing: 2.16,
color: "#a1a1a1",
},
title: { fontWeight: "500" },
paragraph: {
maxWidth: 672,
textAlign: "center",
fontSize: 18,
lineHeight: 29.25,
color: "rgba(250,250,250,0.7)",
},
inline: { fontSize: 18, lineHeight: 29.25 },
});href?Тип string
srcТип LinkPreviewSource
alt?Тип string
width?Тип number
height?Тип number
radius?Тип number
tilt?Тип number
childrenТип ReactNode
style?Тип StyleProp<TextStyle>
containerStyle?Тип StyleProp<ViewStyle>
onPress?Тип () => void