feat(db): mesh data model — meshes, members, invites, audit log
- 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>
This commit is contained in:
112
packages/ui/mobile/src/components/input-otp.tsx
Normal file
112
packages/ui/mobile/src/components/input-otp.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user