Компоненты/Стопка чипов

Стопка чипов

1 февраля 2025

Меню-стопка чипов, каждый раскрывается вбок по нажатию

Установка

npx astra-rn add stacked-chips
pnpm dlx astra-rn add stacked-chips
yarn dlx astra-rn add stacked-chips
bunx --bun astra-rn add stacked-chips

Файлы

index.tsx
НазадСквиркл на SVG-фильтреДалееАнимированные иконки темы

Вручную

Поставьте зависимости:

npm install expo-blur expo-haptics react-native-reanimated
pnpm add expo-blur expo-haptics react-native-reanimated
yarn add expo-blur expo-haptics react-native-reanimated
bun add expo-blur expo-haptics react-native-reanimated

Скопируйте эти файлы в src/shared/components/micro-interactions/stacked-chips/:

Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.

Использование

import { useRef, useState } from "react";
import { View, Text, StyleSheet, useWindowDimensions } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { StatusBar } from "expo-status-bar";
import { useFonts } from "expo-font";
import { SymbolView } from "expo-symbols";
import { StackedChips } from "@/shared/components/micro-interactions/stacked-chips";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";
import sfProRounded from "@/assets/fonts/sf-pro-rounded.ttf";
import helveticaNowDisplayMedium from "@/assets/fonts/HelveticaNowDisplayMedium.ttf";

const CUSTOMIZE = [
  { key: "create", label: "Чип 1", type: "text", default: "Create", maxLength: 20 },
  { key: "document", label: "Чип 2", type: "text", default: "Document", maxLength: 20 },
  { key: "folder", label: "Чип 3", type: "text", default: "Folder", maxLength: 20 },
  { key: "photo", label: "Чип 4", type: "text", default: "Photo", maxLength: 20 },
  { key: "primary", label: "Цвет главного", type: "color", default: "#ffffff" },
  { key: "primaryText", label: "Текст главного", type: "color", default: "#000000" },
  { key: "secondary", label: "Цвет чипа 2", type: "color", default: "#333333" },
  { key: "tertiary", label: "Цвет чипа 3", type: "color", default: "#222222" },
  { key: "quaternary", label: "Цвет чипа 4", type: "color", default: "#1a1a1a" },
  { key: "textColor", label: "Цвет текста", type: "color", default: "#ffffff" },
  { key: "step", label: "Шаг автопоказа", type: "slider", min: 200, max: 3000, step: 50, unit: "ms", default: 900 },
  { key: "hold", label: "Пауза раскрытия", type: "slider", min: 300, max: 5000, step: 50, unit: "ms", default: 1700 },
] as const;

const SAME = (value: string, original: string) => (value === original ? undefined : value);

export default function App() {
  const [fontLoaded] = useFonts({
    SfProRounded: sfProRounded,
    HelveticaNowDisplay: helveticaNowDisplayMedium,
  });
  const [createOpen, setCreateOpen] = useState(false);
  const [documentOpen, setDocumentOpen] = useState(false);
  const [folderOpen, setFolderOpen] = useState(false);
  const custom = useCustomize(CUSTOMIZE);
  const compact = useWindowDimensions().width < 440;
  const timingRef = useRef({ step: custom.step, hold: custom.hold });
  timingRef.current = { step: custom.step, hold: custom.hold };
  const textTint = SAME(custom.textColor, "#ffffff");
  const labelColor = textTint ? { color: textTint } : null;
  const iconTint = textTint ?? "#fff";

  useAutoplay(async ({ wait }) => {
    await wait(500);
    setCreateOpen(true);
    await wait(timingRef.current.step);
    setDocumentOpen(true);
    await wait(timingRef.current.step);
    setFolderOpen(true);
    await wait(timingRef.current.hold);
    setFolderOpen(false);
    await wait(300);
    setDocumentOpen(false);
    await wait(350);
    setCreateOpen(false);
    await wait(1200);
  });

  return (
    <Showcase>
      <GestureHandlerRootView style={styles.container}>
        <StatusBar style="light" />

        <StackedChips open={createOpen}>
          <StackedChips.Trigger onPress={() => setCreateOpen((v) => !v)}>
            <View
              style={[
                styles.chip,
                styles.chipPrimary,
                { backgroundColor: SAME(custom.primary, "#ffffff") ?? "#fff" },
              ]}
            >
              <SymbolView
                name="plus"
                size={16}
                tintColor={SAME(custom.primaryText, "#000000") ?? "#000"}
              />
              <Text
                style={[
                  styles.chipText,
                  fontLoaded && { fontFamily: "SfProRounded" },
                  {
                    color: SAME(custom.primaryText, "#000000") ?? "#000",
                  },
                ]}
              >
                {custom.create}
              </Text>
            </View>
          </StackedChips.Trigger>

          <StackedChips.Content>
            <StackedChips open={documentOpen}>
              <StackedChips.Trigger onPress={() => setDocumentOpen((v) => !v)}>
                <View
                  style={[
                    styles.chip,
                    styles.chipSecondary,
                    compact && styles.chipCompact,
                    {
                      marginLeft: 20,
                    },
                    { backgroundColor: SAME(custom.secondary, "#333333") ?? "#333" },
                  ]}
                >
                  <SymbolView name="doc.fill" size={16} tintColor={iconTint} />
                  <Text
                    style={[
                      styles.chipText,
                      fontLoaded && { fontFamily: "SfProRounded" },
                      labelColor,
                    ]}
                  >
                    {custom.document}
                  </Text>
                </View>
              </StackedChips.Trigger>

              <StackedChips.Content>
                <StackedChips open={folderOpen}>
                  <StackedChips.Trigger onPress={() => setFolderOpen((v) => !v)}>
                    <View
                      style={[
                        styles.chip,
                        styles.chipTertiary,
                        compact && styles.chipCompact,
                        { backgroundColor: SAME(custom.tertiary, "#222222") ?? "#222" },
                      ]}
                    >
                      <SymbolView
                        name="folder.fill"
                        size={16}
                        tintColor={iconTint}
                      />
                      <Text
                        style={[
                          styles.chipText,
                          fontLoaded && { fontFamily: "SfProRounded" },
                          labelColor,
                        ]}
                      >
                        {custom.folder}
                      </Text>
                    </View>
                  </StackedChips.Trigger>

                  <StackedChips.Content>
                    <View
                      style={[
                        styles.chip,
                        styles.chipQuaternary,
                        compact && styles.chipCompactLast,
                        { backgroundColor: custom.quaternary },
                      ]}
                    >
                      <SymbolView
                        name="photo.fill"
                        size={16}
                        tintColor={iconTint}
                      />
                      <Text
                        style={[
                          styles.chipText,
                          fontLoaded && { fontFamily: "SfProRounded" },
                          labelColor,
                        ]}
                      >
                        {custom.photo}
                      </Text>
                    </View>
                  </StackedChips.Content>
                </StackedChips>
              </StackedChips.Content>
            </StackedChips>
          </StackedChips.Content>
        </StackedChips>
      </GestureHandlerRootView>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#0a0a0a",
    justifyContent: "center",
    paddingHorizontal: 12,
  },
  chip: {
    flexDirection: "row",
    alignItems: "center",
    gap: 10,
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderRadius: 50,
  },
  chipPrimary: {
    backgroundColor: "#fff",
  },
  chipSecondary: {
    backgroundColor: "#333",
    marginLeft: 20,
    paddingHorizontal: 34,
  },
  chipTertiary: {
    backgroundColor: "#222",
    marginLeft: 20,
    paddingHorizontal: 34,
  },
  chipQuaternary: {
    backgroundColor: "#1a1a1a",
    marginLeft: 20,
  },
  chipCompact: {
    marginLeft: 12,
    paddingHorizontal: 20,
  },
  chipCompactLast: {
    marginLeft: 12,
  },
  chipText: {
    fontSize: 15,
    fontWeight: "600",
    color: "#fff",
  },
});

Пропсы

Пропс
Описание
children

Тип ReactNode

open?

Тип boolean

TriggerProps

Пропс
Описание
children

Тип ReactNode

onPress?

Тип () => void