Уведомления сложены в компактную стопку: видна верхняя карточка, а из-под неё выглядывают края следующих. Касание верхней карточки или кнопки «Показать все» пружинно раскладывает стопку в список, карточки разъезжаются с небольшой задержкой друг за другом. Кнопка «Скрыть» собирает их обратно.
Высота карточек измеряется через onLayout, поэтому в стопку можно положить любое содержимое. Карточка со ссылкой href открывает её через Linking. Анимации идут на UI-потоке Reanimated, а при «Уменьшении движения» стопка раскрывается без пружин. Работает на iOS, Android и в вебе.
Поставьте зависимости:
npm install react-native-reanimated react-native-svgpnpm add react-native-reanimated react-native-svgyarn add react-native-reanimated react-native-svgbun add react-native-reanimated react-native-svgСкопируйте эти файлы в src/shared/components/molecules/card-stack/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import React, { useMemo } from "react";
import { StyleSheet, View } from "react-native";
import { CardStack, DEFAULT_CARDS } from "@/shared/components/molecules/card-stack";
import { Showcase, useCustomize } from "~/showcase";
const CUSTOMIZE = [
{ key: "width", label: "Ширина", type: "slider", min: 260, max: 560, step: 4, unit: "px", default: 384 },
{ key: "count", label: "Количество карточек", type: "slider", min: 1, max: DEFAULT_CARDS.length, step: 1, default: DEFAULT_CARDS.length },
{ key: "peekOffset", label: "Выступ в стопке", type: "slider", min: 0, max: 24, step: 1, unit: "px", default: 10 },
{ key: "peekScale", label: "Уменьшение в стопке", type: "slider", min: 0, max: 0.15, step: 0.01, default: 0.05 },
{ key: "gap", label: "Отступ в списке", type: "slider", min: 0, max: 24, step: 1, unit: "px", default: 8 },
{ key: "firstTitle", label: "Заголовок первой", type: "text", default: DEFAULT_CARDS[0]!.title, maxLength: 60 },
{ key: "showLabel", label: "Кнопка раскрытия", type: "text", default: "Показать все", maxLength: 24 },
{ key: "hideLabel", label: "Кнопка сворачивания", type: "text", default: "Скрыть", maxLength: 24 },
] as const;
export default function CardStackScreen() {
const custom = useCustomize(CUSTOMIZE);
const cards = useMemo(
() =>
DEFAULT_CARDS.slice(0, custom.count).map((card, index) =>
index === 0 ? { ...card, title: custom.firstTitle } : card,
),
[custom.count, custom.firstTitle],
);
return (
<Showcase>
<View style={styles.container}>
<View style={styles.stage}>
<CardStack
cards={cards}
width={custom.width}
peekOffset={custom.peekOffset}
peekScale={custom.peekScale}
gap={custom.gap}
showLabel={custom.showLabel}
hideLabel={custom.hideLabel}
/>
</View>
</View>
</Showcase>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0a0a0a",
},
stage: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 16,
},
});cards?Тип readonly ICardItem[]
width?Тип number
peekOffset?Тип number
peekScale?Тип number
gap?Тип number
showLabel?Тип string
hideLabel?Тип string
style?Тип StyleProp<ViewStyle>