- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
113 lines
2.5 KiB
TypeScript
113 lines
2.5 KiB
TypeScript
import { OTPInput } from "input-otp-native";
|
|
import React, { useEffect } from "react";
|
|
import { View, Text } from "react-native";
|
|
import Animated, {
|
|
useAnimatedStyle,
|
|
withRepeat,
|
|
withTiming,
|
|
withSequence,
|
|
useSharedValue,
|
|
} from "react-native-reanimated";
|
|
import { useCSSVariable } from "uniwind";
|
|
|
|
import { cn } from "@turbostarter/ui";
|
|
|
|
import type { SlotProps } from "input-otp-native";
|
|
|
|
function InputOTPGroup({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof View>) {
|
|
return (
|
|
<View
|
|
data-slot="input-otp-group"
|
|
className={cn("flex-row items-center justify-center", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function InputOTPSlot({
|
|
char,
|
|
isActive,
|
|
hasFakeCaret,
|
|
className,
|
|
index,
|
|
max,
|
|
...props
|
|
}: React.ComponentProps<typeof View> &
|
|
SlotProps & { index?: number; max?: number }) {
|
|
const isFirst = index === 0;
|
|
const isLast = index === (max ?? 6) - 1;
|
|
|
|
return (
|
|
<View
|
|
className={cn(
|
|
"border-input relative flex size-12 items-center justify-center border transition-all outline-none",
|
|
"dark:bg-input/30",
|
|
{
|
|
"border-ring": isActive,
|
|
"rounded-l-md": isFirst,
|
|
"rounded-r-md": isLast,
|
|
},
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{char !== null && (
|
|
<Text className="text-foreground font-sans-medium text-xl">{char}</Text>
|
|
)}
|
|
{hasFakeCaret && <FakeCaret />}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function FakeCaret() {
|
|
const opacity = useSharedValue(1);
|
|
const foregroundColor = useCSSVariable("--foreground");
|
|
|
|
useEffect(() => {
|
|
opacity.value = withRepeat(
|
|
withSequence(
|
|
withTiming(0, { duration: 500 }),
|
|
withTiming(1, { duration: 500 }),
|
|
),
|
|
-1,
|
|
true,
|
|
);
|
|
}, [opacity]);
|
|
|
|
const animatedStyle = useAnimatedStyle(() => ({
|
|
opacity: opacity.value,
|
|
}));
|
|
|
|
const baseStyle = {
|
|
width: 2,
|
|
height: 20,
|
|
borderRadius: 1,
|
|
...(foregroundColor && { backgroundColor: foregroundColor.toString() }),
|
|
};
|
|
|
|
return (
|
|
<View className="absolute h-full w-full items-center justify-center">
|
|
<Animated.View style={[baseStyle, animatedStyle]} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function InputOTPSeparator({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof View>) {
|
|
return (
|
|
<View
|
|
{...props}
|
|
className={cn("w-1 items-center justify-center", className)}
|
|
>
|
|
<View className="bg-muted-foreground h-0.5 w-1 rounded-sm" />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export { OTPInput as InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|