From 3433cd60fee691e2d13ab883689cad09c2da5f3e Mon Sep 17 00:00:00 2001 From: houseassassin Date: Wed, 20 May 2026 12:17:51 +0300 Subject: [PATCH] feat: UI component library (GlassCard, Button, Input, QRModal) Co-Authored-By: Claude Sonnet 4.6 --- index.html | 1 + src/components/ui/Button.tsx | 26 +++++++++++++++++++++++ src/components/ui/GlassCard.tsx | 33 +++++++++++++++++++++++++++++ src/components/ui/Input.tsx | 25 ++++++++++++++++++++++ src/components/ui/QRModal.tsx | 35 +++++++++++++++++++++++++++++++ src/components/ui/StatusBadge.tsx | 11 ++++++++++ 6 files changed, 131 insertions(+) create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/GlassCard.tsx create mode 100644 src/components/ui/Input.tsx create mode 100644 src/components/ui/QRModal.tsx create mode 100644 src/components/ui/StatusBadge.tsx diff --git a/index.html b/index.html index 83a9424..e51dab8 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ aura-store +
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..a299632 --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,26 @@ +import { motion } from "framer-motion"; +import { cn } from "../../lib/utils"; + +interface Props extends React.ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger"; + loading?: boolean; +} + +export function Button({ children, variant = "primary", loading, className, ...props }: Props) { + const base = "px-5 py-2.5 rounded-xl font-semibold text-sm transition-all duration-200 disabled:opacity-50"; + const variants = { + primary: "bg-gradient-to-r from-accent to-accent2 text-white hover:scale-[1.02] active:scale-[0.98]", + secondary: "border border-white/20 text-white hover:bg-white/10", + danger: "border border-red-500/40 text-red-400 hover:bg-red-500/10", + }; + return ( + + {loading ? "..." : children} + + ); +} diff --git a/src/components/ui/GlassCard.tsx b/src/components/ui/GlassCard.tsx new file mode 100644 index 0000000..561d013 --- /dev/null +++ b/src/components/ui/GlassCard.tsx @@ -0,0 +1,33 @@ +import { motion } from "framer-motion"; +import { cn } from "../../lib/utils"; + +interface Props { + children: React.ReactNode; + className?: string; + onClick?: () => void; + animate?: boolean; +} + +export function GlassCard({ children, className, onClick, animate = true }: Props) { + const Comp = animate ? motion.div : "div"; + const animProps = animate ? { + initial: { opacity: 0, y: 20 }, + whileInView: { opacity: 1, y: 0 }, + viewport: { once: true }, + transition: { duration: 0.4 }, + } : {}; + return ( + + {children} + + ); +} diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 0000000..9ca6df6 --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,25 @@ +import { cn } from "../../lib/utils"; + +interface Props extends React.InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className, ...props }: Props) { + return ( +
+ {label && } + + {error && {error}} +
+ ); +} diff --git a/src/components/ui/QRModal.tsx b/src/components/ui/QRModal.tsx new file mode 100644 index 0000000..cffa5ad --- /dev/null +++ b/src/components/ui/QRModal.tsx @@ -0,0 +1,35 @@ +import { useEffect, useRef } from "react"; +import { X } from "lucide-react"; + +declare const QRCode: any; + +interface Props { text: string; title?: string; onClose: () => void; } + +export function QRModal({ text, title = "QR-код", onClose }: Props) { + const ref = useRef(null); + + useEffect(() => { + if (!ref.current) return; + ref.current.innerHTML = ""; + new QRCode(ref.current, { + text, width: 220, height: 220, + colorDark: "#ffffff", colorLight: "#0d0b1e", + correctLevel: (QRCode as any).CorrectLevel?.M ?? 0, + }); + }, [text]); + + return ( +
+
+
+ {title} + +
+
+
+
+

Отсканируйте в VPN-приложении

+
+
+ ); +} diff --git a/src/components/ui/StatusBadge.tsx b/src/components/ui/StatusBadge.tsx new file mode 100644 index 0000000..67fd5df --- /dev/null +++ b/src/components/ui/StatusBadge.tsx @@ -0,0 +1,11 @@ +interface Props { active: boolean; activeLabel?: string; inactiveLabel?: string; } +export function StatusBadge({ active, activeLabel = "Активна", inactiveLabel = "Неактивна" }: Props) { + return ( +
+
+ + {active ? activeLabel : inactiveLabel} + +
+ ); +}