Клавиатура ноутбука с реальными пропорциями клавиш, стрелками половинной высоты и наклоном в перспективе с растворением вдаль. Клавиши нажимаются касанием: клавиша проседает и вспыхивает цветом подсветки, а onKey сообщает о нажатии и отпускании. Отдельные клавиши можно подсветить заранее через lit.
В вебе с listen клавиатура повторяет нажатия на настоящей клавиатуре. Анимации идут на Reanimated; при «Уменьшении движения» нажатия отображаются мгновенно, без анимации. Работает на iOS, Android и в вебе.
Поставьте зависимости:
npm install expo-linear-gradient react-native-reanimatedpnpm add expo-linear-gradient react-native-reanimatedyarn add expo-linear-gradient react-native-reanimatedbun add expo-linear-gradient react-native-reanimatedСкопируйте эти файлы в src/shared/components/organisms/keyboard/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import React, { useCallback, useMemo, useState } from "react";
import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
import { Keyboard, type KeyboardCaps } from "@/shared/components/organisms/keyboard";
import { Showcase, useCustomize } from "~/showcase";
const ACCENTS = [
{ name: "Бирюзовый", value: "#66d9e8" },
{ name: "Янтарь", value: "#f59e0b" },
{ name: "Мята", value: "#10b981" },
{ name: "Роза", value: "#f43f5e" },
];
const CUSTOMIZE = [
{ key: "tiltAngle", label: "Наклон", type: "slider", min: 0, max: 60, step: 1, unit: "°", default: 34 },
{ key: "width", label: "Ширина", type: "slider", min: 600, max: 1408, step: 8, unit: "px", default: 1408 },
{ key: "accent", label: "Цвет подсветки", type: "color", default: "#66d9e8" },
{ key: "lit", label: "Подсвеченные клавиши", type: "text", default: "", maxLength: 24 },
{
key: "caps",
label: "Клавиши",
type: "select",
options: [
{ value: "auto", label: "По теме" },
{ value: "dark", label: "Тёмные" },
{ value: "light", label: "Светлые" },
],
default: "auto",
},
{ key: "tilt", label: "Перспектива", type: "toggle", default: true },
] as const;
const RU = "йцукенгшщзфывапролдячсмить";
const EN = "qwertyuiopasdfghjklzxcvbnm";
function toCodes(text: string) {
const latin = Array.from(text.toLowerCase(), (ch) => (RU.includes(ch) ? EN[RU.indexOf(ch)] : ch)).join("");
return Array.from(new Set(latin.toUpperCase().replace(/[^A-Z0-9]/g, "").split("")))
.filter(Boolean)
.map((ch) => (/\d/.test(ch) ? `Digit${ch}` : `Key${ch}`));
}
const MONO = Platform.select({ ios: "Menlo", android: "monospace", default: "ui-monospace, SFMono-Regular, Menlo, monospace" });
export default function KeyboardScreen() {
const custom = useCustomize(CUSTOMIZE);
const [accent, setAccent] = useState(ACCENTS[0]!.value);
const [prevAccent, setPrevAccent] = useState(custom.accent);
if (prevAccent !== custom.accent) {
setPrevAccent(custom.accent);
setAccent(custom.accent);
}
const lit = useMemo(() => toCodes(custom.lit), [custom.lit]);
const [last, setLast] = useState<string | null>(null);
const handleKey = useCallback((code: string, down: boolean) => {
if (down) setLast(code);
}, []);
return (
<Showcase>
<View style={styles.container}>
<View style={styles.stack}>
<View style={[styles.board, { maxWidth: custom.width }]}>
<Keyboard
accent={accent}
listen
onKey={handleKey}
lit={lit}
caps={custom.caps as KeyboardCaps}
tilt={custom.tilt}
tiltAngle={custom.tiltAngle}
/>
</View>
<View style={styles.footer}>
<Text style={styles.hint}>{last ? `нажата ${last}` : "нажмите любую клавишу"}</Text>
<View style={styles.swatches}>
{ACCENTS.map((item) => {
const selected = accent === item.value;
return (
<Pressable
key={item.value}
accessibilityLabel={item.name}
onPress={() => setAccent(item.value)}
style={[
styles.swatch,
{
backgroundColor: item.value,
borderColor: selected ? "#fafafa" : "transparent",
transform: [{ scale: selected ? 1.1 : 1 }],
},
]}
/>
);
})}
</View>
</View>
</View>
</View>
</Showcase>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingVertical: 48,
backgroundColor: "#0a0a0a",
},
stack: {
width: "100%",
alignItems: "center",
gap: 32,
paddingHorizontal: 12,
paddingVertical: 32,
},
board: {
width: "100%",
},
footer: {
alignItems: "center",
gap: 16,
},
hint: {
fontFamily: MONO,
fontSize: 12,
lineHeight: 16,
color: "rgba(250,250,250,0.5)",
},
swatches: {
flexDirection: "row",
gap: 12,
},
swatch: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
},
});lit?Тип readonly string[]
accent?Тип string
listen?Тип boolean
caps?Тип KeyboardCaps
tilt?Тип boolean
tiltAngle?Тип number
fadeColor?Тип string
onKey?Тип (code: string, down: boolean) => void
style?Тип StyleProp<ViewStyle>