mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(docs): add animated product website
This commit is contained in:
86
docs/components/approval-demo.tsx
Normal file
86
docs/components/approval-demo.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Icon } from "@/components/icons";
|
||||
|
||||
type ApprovalState = "pending" | "approved" | "refused";
|
||||
|
||||
export function ApprovalDemo() {
|
||||
const [state, setState] = useState<ApprovalState>("pending");
|
||||
|
||||
return (
|
||||
<div className={`approval-demo state-${state}`}>
|
||||
<div className="approval-window-bar">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<small>Receiver requests</small>
|
||||
</div>
|
||||
<div className="approval-body" aria-live="polite">
|
||||
<div className="approval-heading">
|
||||
<div className="device-avatar">
|
||||
<span className="device-avatar-screen" />
|
||||
<span className="device-avatar-glow" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="mono-label">
|
||||
{state === "pending" ? "NEW REQUEST" : state === "approved" ? "IN PROGRESS" : "REQUEST CLOSED"}
|
||||
</span>
|
||||
<h3>
|
||||
{state === "pending" && "Maya’s phone wants to receive"}
|
||||
{state === "approved" && "Sending securely to Maya"}
|
||||
{state === "refused" && "Request refused"}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="approval-file">
|
||||
<span className="approval-file-icon">
|
||||
<Icon name="folder" />
|
||||
</span>
|
||||
<span>
|
||||
<strong>Summer photos</strong>
|
||||
<small>42 files · 1.8 GB</small>
|
||||
</span>
|
||||
{state === "approved" && <span className="approval-percent">68%</span>}
|
||||
</div>
|
||||
|
||||
{state === "pending" && (
|
||||
<div className="approval-actions">
|
||||
<button className="approval-refuse" type="button" onClick={() => setState("refused")}>
|
||||
Refuse
|
||||
</button>
|
||||
<button className="approval-accept" type="button" onClick={() => setState("approved")}>
|
||||
<Icon name="check" />
|
||||
Approve
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === "approved" && (
|
||||
<div className="approval-progress">
|
||||
<div className="approval-progress-track">
|
||||
<span />
|
||||
</div>
|
||||
<div className="approval-progress-meta">
|
||||
<span><i className="live-dot" /> Direct connection</span>
|
||||
<button type="button" onClick={() => setState("pending")}>Reset demo</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === "refused" && (
|
||||
<div className="approval-refused">
|
||||
<span><Icon name="x" /></span>
|
||||
<p>This device was not given access.</p>
|
||||
<button type="button" onClick={() => setState("pending")}>Try again</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="approval-footer">
|
||||
<Icon name="shield" />
|
||||
Approval is required by default
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
267
docs/components/approval-scene.css
Normal file
267
docs/components/approval-scene.css
Normal file
@@ -0,0 +1,267 @@
|
||||
/* Interactive receiver approval */
|
||||
.approval-demo {
|
||||
overflow: hidden;
|
||||
border: 1px solid #ded6e2;
|
||||
border-radius: 24px;
|
||||
background: #fbf9fc;
|
||||
box-shadow: 0 35px 90px rgba(49, 29, 58, 0.16);
|
||||
transform: rotate(1deg);
|
||||
transition: transform 280ms ease, border-color 280ms ease;
|
||||
}
|
||||
|
||||
.approval-demo:hover { transform: rotate(0deg) translateY(-3px); }
|
||||
.approval-demo.state-approved { border-color: rgba(45, 185, 117, 0.4); }
|
||||
.approval-demo.state-refused { border-color: rgba(221, 92, 75, 0.35); }
|
||||
|
||||
.approval-window-bar {
|
||||
display: flex;
|
||||
height: 48px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid #ded6e2;
|
||||
background: #f0ecf2;
|
||||
}
|
||||
|
||||
.approval-window-bar > span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #d1c7d5;
|
||||
}
|
||||
|
||||
.approval-window-bar > span:nth-child(1) { background: #f1a191; }
|
||||
.approval-window-bar > span:nth-child(2) { background: #f1ca72; }
|
||||
.approval-window-bar > span:nth-child(3) { background: #72cf9b; }
|
||||
|
||||
.approval-window-bar small {
|
||||
margin-left: auto;
|
||||
color: #8a7f8f;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.approval-body {
|
||||
min-height: 385px;
|
||||
padding: 35px;
|
||||
background:
|
||||
radial-gradient(circle at 90% 0%, rgba(192, 132, 252, 0.16), transparent 27%),
|
||||
white;
|
||||
}
|
||||
|
||||
.approval-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 17px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.device-avatar {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 60px;
|
||||
flex: 0 0 auto;
|
||||
border: 4px solid #352e39;
|
||||
border-radius: 13px;
|
||||
background: #211d24;
|
||||
box-shadow: 0 10px 25px rgba(42, 30, 48, 0.16);
|
||||
}
|
||||
|
||||
.device-avatar::before {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 50%;
|
||||
width: 16px;
|
||||
height: 3px;
|
||||
border-radius: 999px;
|
||||
background: #0c0a0d;
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.device-avatar-screen {
|
||||
position: absolute;
|
||||
inset: 14px 8px 8px;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(145deg, #b55efa, #6f27e9);
|
||||
}
|
||||
|
||||
.device-avatar-glow {
|
||||
position: absolute;
|
||||
inset: -5px;
|
||||
z-index: -1;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.25);
|
||||
animation: avatar-pulse 2.7s ease-out infinite;
|
||||
}
|
||||
|
||||
.approval-heading h3 {
|
||||
max-width: 360px;
|
||||
margin: 5px 0 0;
|
||||
font-size: 21px;
|
||||
letter-spacing: -0.035em;
|
||||
line-height: 1.22;
|
||||
}
|
||||
|
||||
.approval-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e5dde8;
|
||||
border-radius: 14px;
|
||||
background: #f8f5f9;
|
||||
}
|
||||
|
||||
.approval-file-icon {
|
||||
display: grid;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border-radius: 11px;
|
||||
background: var(--brand-100);
|
||||
color: var(--brand-600);
|
||||
}
|
||||
|
||||
.approval-file-icon svg { width: 21px; }
|
||||
.approval-file strong,
|
||||
.approval-file small { display: block; }
|
||||
.approval-file strong { font-size: 12px; }
|
||||
.approval-file small { color: var(--ink-muted); font-size: 9px; }
|
||||
|
||||
.approval-percent {
|
||||
margin-left: auto;
|
||||
color: var(--brand-600);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.approval-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.4fr;
|
||||
gap: 10px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.approval-actions button,
|
||||
.approval-refused button {
|
||||
min-height: 47px;
|
||||
border: 1px solid #ded4e2;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
cursor: pointer;
|
||||
transition: transform 160ms ease, background 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.approval-actions button:hover,
|
||||
.approval-refused button:hover { transform: translateY(-2px); }
|
||||
.approval-refuse { background: white; color: #625769; }
|
||||
.approval-accept {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-color: transparent !important;
|
||||
background: linear-gradient(135deg, var(--brand-500), var(--brand-700));
|
||||
box-shadow: 0 10px 24px rgba(132, 47, 238, 0.22);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.approval-accept svg { width: 17px; }
|
||||
|
||||
.approval-progress {
|
||||
margin-top: 25px;
|
||||
animation: approval-state-enter 350ms ease;
|
||||
}
|
||||
|
||||
.approval-progress-track {
|
||||
overflow: hidden;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #eee8f1;
|
||||
}
|
||||
|
||||
.approval-progress-track span {
|
||||
display: block;
|
||||
width: 68%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, var(--brand-500), var(--brand-700));
|
||||
box-shadow: 0 0 15px rgba(168, 85, 247, 0.38);
|
||||
animation: progress-fill 900ms cubic-bezier(0.25, 0.8, 0.25, 1) both;
|
||||
}
|
||||
|
||||
.approval-progress-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 13px;
|
||||
color: var(--ink-muted);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.approval-progress-meta > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.approval-progress-meta button,
|
||||
.approval-refused button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--brand-600);
|
||||
font-size: 9px;
|
||||
font-weight: 750;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.approval-refused {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 23px;
|
||||
padding: 14px;
|
||||
border: 1px solid #f0d5d0;
|
||||
border-radius: 12px;
|
||||
background: #fff8f7;
|
||||
animation: approval-state-enter 350ms ease;
|
||||
}
|
||||
|
||||
.approval-refused > span {
|
||||
display: grid;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: #fde7e3;
|
||||
color: #d85848;
|
||||
}
|
||||
|
||||
.approval-refused svg { width: 15px; }
|
||||
.approval-refused p { margin: 0; color: #705c59; font-size: 10px; }
|
||||
.approval-refused button { margin-left: auto; }
|
||||
|
||||
.approval-footer {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-top: 1px solid #ded6e2;
|
||||
background: #f2eef4;
|
||||
color: #766b7b;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.approval-footer svg { width: 15px; color: var(--brand-600); }
|
||||
|
||||
65
docs/components/brand.tsx
Normal file
65
docs/components/brand.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import Link from "next/link";
|
||||
|
||||
type BrandMarkProps = {
|
||||
className?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export function BrandMark({ className, title }: BrandMarkProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
role={title ? "img" : undefined}
|
||||
aria-hidden={title ? undefined : true}
|
||||
aria-label={title}
|
||||
>
|
||||
<path
|
||||
d="M11 8h10.6a4.8 4.8 0 0 1 4.8 4.8v25.4c0 8.7 6.1 15.2 14.5 15.2 8.3 0 14.1-6.5 14.1-15.2V12.8A4.8 4.8 0 0 1 59.8 8H62v30.4C62 52.5 53.3 61 40.9 61 28.3 61 19 52.5 19 38.4V23h-3.5v-6.4H11V8Z"
|
||||
fill="url(#brand-mark-gradient)"
|
||||
/>
|
||||
<path
|
||||
d="M40.8 25.9c-3.1 4.1-8.2 10.8-8.2 15.5 0 5.1 3.6 8.9 8.2 8.9 4.8 0 8.3-3.8 8.3-8.9 0-4.7-5.2-11.4-8.3-15.5Z"
|
||||
fill="url(#brand-drop-gradient)"
|
||||
/>
|
||||
<path d="M54.9 8h7v7l-7-7Z" fill="#E8CDFF" />
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="brand-mark-gradient"
|
||||
x1="11"
|
||||
y1="8"
|
||||
x2="64"
|
||||
y2="55"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#B45CFA" />
|
||||
<stop offset="0.5" stopColor="#9D4DF4" />
|
||||
<stop offset="1" stopColor="#7828ED" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="brand-drop-gradient"
|
||||
x1="32"
|
||||
y1="26"
|
||||
x2="51"
|
||||
y2="47"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#C06CFF" />
|
||||
<stop offset="1" stopColor="#7828ED" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Brand({ compact = false }: { compact?: boolean }) {
|
||||
return (
|
||||
<Link className="brand" href="/" aria-label="VniDrop home">
|
||||
<span className="brand-mark-shell">
|
||||
<BrandMark />
|
||||
</span>
|
||||
{!compact && <span className="brand-name">VniDrop</span>}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
216
docs/components/hero-illustration.tsx
Normal file
216
docs/components/hero-illustration.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties, PointerEvent } from "react";
|
||||
import { useRef } from "react";
|
||||
import { Icon } from "@/components/icons";
|
||||
|
||||
type StageStyle = CSSProperties & {
|
||||
"--pointer-x": string;
|
||||
"--pointer-y": string;
|
||||
};
|
||||
|
||||
export function HeroIllustration() {
|
||||
const stageRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const updateParallax = (event: PointerEvent<HTMLDivElement>) => {
|
||||
const stage = stageRef.current;
|
||||
if (!stage || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
const bounds = stage.getBoundingClientRect();
|
||||
const x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 2;
|
||||
const y = ((event.clientY - bounds.top) / bounds.height - 0.5) * 2;
|
||||
stage.style.setProperty("--pointer-x", x.toFixed(3));
|
||||
stage.style.setProperty("--pointer-y", y.toFixed(3));
|
||||
};
|
||||
|
||||
const resetParallax = () => {
|
||||
stageRef.current?.style.setProperty("--pointer-x", "0");
|
||||
stageRef.current?.style.setProperty("--pointer-y", "0");
|
||||
};
|
||||
|
||||
return (
|
||||
<figure className="hero-visual">
|
||||
<div
|
||||
ref={stageRef}
|
||||
className="hero-stage"
|
||||
style={{ "--pointer-x": "0", "--pointer-y": "0" } as StageStyle}
|
||||
onPointerMove={updateParallax}
|
||||
onPointerLeave={resetParallax}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="hero-grid" />
|
||||
<div className="hero-glow hero-glow-one" />
|
||||
<div className="hero-glow hero-glow-two" />
|
||||
<div className="hero-status hero-status-secure">
|
||||
<span className="status-icon">
|
||||
<Icon name="lock" />
|
||||
</span>
|
||||
<span>
|
||||
<strong>End-to-end encrypted</strong>
|
||||
<small>Authenticated connection</small>
|
||||
</span>
|
||||
</div>
|
||||
<div className="hero-status hero-status-route">
|
||||
<span className="status-pulse" />
|
||||
Direct route found
|
||||
</div>
|
||||
<svg className="hero-scene" viewBox="0 0 760 620">
|
||||
<defs>
|
||||
<linearGradient id="hero-purple" x1="240" y1="90" x2="620" y2="520">
|
||||
<stop stopColor="#C16BFF" />
|
||||
<stop offset="0.48" stopColor="#A855F7" />
|
||||
<stop offset="1" stopColor="#6F27E9" />
|
||||
</linearGradient>
|
||||
<linearGradient id="hero-screen" x1="88" y1="118" x2="342" y2="345">
|
||||
<stop stopColor="#25202C" />
|
||||
<stop offset="1" stopColor="#151319" />
|
||||
</linearGradient>
|
||||
<linearGradient id="hero-phone" x1="540" y1="160" x2="692" y2="465">
|
||||
<stop stopColor="#2A2530" />
|
||||
<stop offset="1" stopColor="#141217" />
|
||||
</linearGradient>
|
||||
<radialGradient id="hero-orb">
|
||||
<stop stopColor="#E3BDFF" stopOpacity="0.9" />
|
||||
<stop offset="0.45" stopColor="#A855F7" stopOpacity="0.45" />
|
||||
<stop offset="1" stopColor="#A855F7" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<filter id="hero-shadow" x="-30%" y="-30%" width="160%" height="180%">
|
||||
<feDropShadow dx="0" dy="20" stdDeviation="18" floodColor="#3F2454" floodOpacity="0.16" />
|
||||
</filter>
|
||||
<filter id="hero-glow-filter" x="-100%" y="-100%" width="300%" height="300%">
|
||||
<feGaussianBlur stdDeviation="12" />
|
||||
</filter>
|
||||
<clipPath id="laptop-screen-clip">
|
||||
<rect x="82" y="132" width="264" height="176" rx="12" />
|
||||
</clipPath>
|
||||
<clipPath id="phone-screen-clip">
|
||||
<rect x="548" y="171" width="136" height="279" rx="25" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<g className="scene-orbits">
|
||||
<ellipse cx="385" cy="325" rx="315" ry="216" fill="none" stroke="#B969F8" strokeOpacity="0.14" />
|
||||
<ellipse cx="385" cy="325" rx="245" ry="169" fill="none" stroke="#B969F8" strokeOpacity="0.12" strokeDasharray="8 14" />
|
||||
<circle cx="670" cy="215" r="5" fill="#A855F7" />
|
||||
<circle cx="117" cy="455" r="4" fill="#C98AFD" />
|
||||
<path d="m704 345 4 10 10 4-10 4-4 10-4-10-10-4 10-4Z" fill="#D9AEFF" />
|
||||
</g>
|
||||
|
||||
<g className="scene-laptop" filter="url(#hero-shadow)">
|
||||
<rect x="69" y="117" width="290" height="207" rx="19" fill="#E8E4EB" stroke="#D6CFDA" strokeWidth="2" />
|
||||
<rect x="80" y="129" width="268" height="183" rx="13" fill="url(#hero-screen)" />
|
||||
<g clipPath="url(#laptop-screen-clip)">
|
||||
<circle cx="208" cy="235" r="108" fill="#A855F7" opacity="0.07" />
|
||||
<path d="M80 278c62-45 99-24 141-60 39-34 70-39 127-31v125H80Z" fill="#A855F7" opacity="0.09" />
|
||||
<rect x="101" y="151" width="101" height="13" rx="6.5" fill="#3E3745" />
|
||||
<rect x="101" y="175" width="67" height="7" rx="3.5" fill="#5E5366" />
|
||||
<g className="laptop-files">
|
||||
<rect x="118" y="205" width="76" height="79" rx="10" fill="#F8F2FC" stroke="#C98AFD" />
|
||||
<path d="M173 205h21v21Z" fill="#E3C3FA" />
|
||||
<rect x="129" y="219" width="24" height="24" rx="6" fill="#E8D0FB" />
|
||||
<path d="m135 238 7-8 5 5 4-4 6 7Z" fill="#A855F7" opacity="0.75" />
|
||||
<rect x="129" y="253" width="46" height="6" rx="3" fill="#C3B6CB" />
|
||||
<rect x="129" y="265" width="34" height="5" rx="2.5" fill="#DDD4E2" />
|
||||
<rect x="181" y="223" width="82" height="68" rx="10" fill="#2F2935" stroke="#574D5E" />
|
||||
<path d="M242 223h21v21Z" fill="#4F4557" />
|
||||
<circle cx="205" cy="245" r="9" fill="#A855F7" opacity="0.75" />
|
||||
<rect x="195" y="264" width="49" height="6" rx="3" fill="#63586A" />
|
||||
<rect x="195" y="276" width="34" height="5" rx="2.5" fill="#4D4453" />
|
||||
</g>
|
||||
</g>
|
||||
<path d="M46 324h336l-20 23a20 20 0 0 1-15 6H81a20 20 0 0 1-15-7Z" fill="#D9D3DD" stroke="#CAC1CE" strokeWidth="2" />
|
||||
<path d="M183 324h62l-7 12h-48Z" fill="#C1B8C6" />
|
||||
</g>
|
||||
|
||||
<g className="transfer-conduit">
|
||||
<path
|
||||
className="conduit-glow"
|
||||
d="M284 358c23 134 225 151 279 35"
|
||||
fill="none"
|
||||
stroke="#B65DF9"
|
||||
strokeOpacity="0.2"
|
||||
strokeWidth="34"
|
||||
strokeLinecap="round"
|
||||
filter="url(#hero-glow-filter)"
|
||||
/>
|
||||
<path
|
||||
d="M284 358c23 134 225 151 279 35"
|
||||
fill="none"
|
||||
stroke="#ECE3F1"
|
||||
strokeWidth="19"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
className="conduit-energy"
|
||||
d="M284 358c23 134 225 151 279 35"
|
||||
fill="none"
|
||||
stroke="url(#hero-purple)"
|
||||
strokeWidth="7"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="2 17"
|
||||
/>
|
||||
<circle cx="284" cy="358" r="25" fill="#F8F2FC" stroke="#C98AFD" strokeWidth="2" />
|
||||
<circle cx="284" cy="358" r="13" fill="#A855F7" opacity="0.15" />
|
||||
<path d="m276 358 7 7 11-14" fill="none" stroke="#9333EA" strokeWidth="3" strokeLinecap="round" />
|
||||
</g>
|
||||
|
||||
<g className="approval-gate" filter="url(#hero-shadow)">
|
||||
<rect x="365" y="414" width="151" height="58" rx="18" fill="#FEFCFF" stroke="#DFCDEB" />
|
||||
<circle cx="393" cy="443" r="15" fill="#F0DDFE" />
|
||||
<path d="M393 433.5 385.5 437v5.3c0 4.5 3.2 7.6 7.5 8.9 4.3-1.3 7.5-4.4 7.5-8.9V437Z" fill="#A855F7" />
|
||||
<path d="m389.5 442 2.2 2.2 4.5-4.6" fill="none" stroke="white" strokeWidth="1.8" strokeLinecap="round" />
|
||||
<text x="418" y="439" fill="#201B24" fontSize="12" fontWeight="700">Approved</text>
|
||||
<text x="418" y="455" fill="#74697B" fontSize="10">Maya’s phone</text>
|
||||
</g>
|
||||
|
||||
<g className="scene-phone" filter="url(#hero-shadow)">
|
||||
<rect x="536" y="157" width="160" height="309" rx="36" fill="#E4DFE7" stroke="#CFC6D3" strokeWidth="2" />
|
||||
<rect x="546" y="168" width="140" height="285" rx="28" fill="url(#hero-phone)" />
|
||||
<rect x="587" y="177" width="58" height="8" rx="4" fill="#0B090D" />
|
||||
<g clipPath="url(#phone-screen-clip)">
|
||||
<circle cx="616" cy="271" r="76" fill="#A855F7" opacity="0.1" />
|
||||
<circle cx="616" cy="275" r="49" fill="url(#hero-orb)" opacity="0.55" />
|
||||
<g className="received-file">
|
||||
<rect x="579" y="224" width="74" height="92" rx="12" fill="#FCF9FD" stroke="#C98AFD" strokeWidth="1.5" />
|
||||
<path d="M630 224h23v23Z" fill="#DEC0F6" />
|
||||
<rect x="590" y="239" width="26" height="26" rx="7" fill="#E9D2FA" />
|
||||
<path d="m595 259 8-9 5 5 5-5 6 9Z" fill="#9E47EF" />
|
||||
<rect x="590" y="278" width="46" height="6" rx="3" fill="#C6BACD" />
|
||||
<rect x="590" y="291" width="34" height="5" rx="2.5" fill="#DFD6E4" />
|
||||
</g>
|
||||
<rect x="564" y="337" width="104" height="42" rx="12" fill="#2F2935" stroke="#4B4251" />
|
||||
<circle cx="581" cy="358" r="8" fill="#A855F7" />
|
||||
<path d="m577.5 358 2.2 2.2 4.5-4.6" fill="none" stroke="white" strokeWidth="1.5" />
|
||||
<text x="595" y="355" fill="#FCF9FD" fontSize="10" fontWeight="700">Received</text>
|
||||
<text x="595" y="368" fill="#A89EAF" fontSize="8">Verified safely</text>
|
||||
<rect x="564" y="393" width="104" height="5" rx="2.5" fill="#3F3745" />
|
||||
<rect x="564" y="393" width="104" height="5" rx="2.5" fill="#A855F7" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g className="transfer-packets">
|
||||
<g className="packet packet-one">
|
||||
<rect x="0" y="0" width="28" height="34" rx="6" fill="#FCF9FE" stroke="#A855F7" />
|
||||
<path d="M19 0h9v9Z" fill="#DDBCF7" />
|
||||
</g>
|
||||
<g className="packet packet-two">
|
||||
<rect x="0" y="0" width="22" height="27" rx="5" fill="#EEE0F9" stroke="#B869F4" />
|
||||
<path d="M14 0h8v8Z" fill="#CAA0E9" />
|
||||
</g>
|
||||
<circle className="packet-spark packet-spark-one" cx="0" cy="0" r="5" fill="#D795FF" />
|
||||
<circle className="packet-spark packet-spark-two" cx="0" cy="0" r="3" fill="#A855F7" />
|
||||
</g>
|
||||
</svg>
|
||||
<div className="hero-file-label">
|
||||
<span className="file-type-icon">JPG</span>
|
||||
<span>
|
||||
<strong>summer-photos</strong>
|
||||
<small>42 files · structure preserved</small>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<figcaption className="sr-only">
|
||||
An illustration of encrypted files moving from a laptop to a phone after receiver approval.
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
247
docs/components/hero-scene.css
Normal file
247
docs/components/hero-scene.css
Normal file
@@ -0,0 +1,247 @@
|
||||
/* Hero transfer scene */
|
||||
.hero-visual {
|
||||
position: relative;
|
||||
width: min(100%, 730px);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-stage {
|
||||
--pointer-x: 0;
|
||||
--pointer-y: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1.13;
|
||||
isolation: isolate;
|
||||
perspective: 1200px;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
position: absolute;
|
||||
z-index: -3;
|
||||
inset: 9% 2% 4% 7%;
|
||||
border: 1px solid rgba(168, 85, 247, 0.14);
|
||||
border-radius: 50%;
|
||||
background-image:
|
||||
linear-gradient(rgba(121, 82, 138, 0.07) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(121, 82, 138, 0.07) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
transform: rotateX(64deg) rotateZ(-8deg) translateZ(-70px);
|
||||
mask-image: radial-gradient(circle, black 10%, transparent 70%);
|
||||
}
|
||||
|
||||
.hero-glow {
|
||||
position: absolute;
|
||||
z-index: -2;
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-glow-one {
|
||||
top: 22%;
|
||||
right: 8%;
|
||||
width: 37%;
|
||||
aspect-ratio: 1;
|
||||
background: rgba(168, 85, 247, 0.23);
|
||||
transform: translate(calc(var(--pointer-x) * 12px), calc(var(--pointer-y) * 12px));
|
||||
}
|
||||
|
||||
.hero-glow-two {
|
||||
bottom: 8%;
|
||||
left: 28%;
|
||||
width: 30%;
|
||||
aspect-ratio: 1;
|
||||
background: rgba(126, 58, 236, 0.16);
|
||||
transform: translate(calc(var(--pointer-x) * -9px), calc(var(--pointer-y) * -9px));
|
||||
}
|
||||
|
||||
.hero-scene {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 1% -3% auto -1%;
|
||||
width: 105%;
|
||||
overflow: visible;
|
||||
transform: rotateY(calc(var(--pointer-x) * 1.7deg)) rotateX(calc(var(--pointer-y) * -1.7deg));
|
||||
transform-origin: center;
|
||||
transition: transform 180ms ease-out;
|
||||
}
|
||||
|
||||
.scene-laptop {
|
||||
transform: translate(calc(var(--pointer-x) * -4px), calc(var(--pointer-y) * -4px));
|
||||
transition: transform 200ms ease-out;
|
||||
}
|
||||
|
||||
.scene-phone {
|
||||
transform: translate(calc(var(--pointer-x) * 7px), calc(var(--pointer-y) * 5px));
|
||||
transition: transform 200ms ease-out;
|
||||
}
|
||||
|
||||
.scene-orbits {
|
||||
transform-origin: 385px 325px;
|
||||
animation: scene-orbits 30s linear infinite;
|
||||
}
|
||||
|
||||
.laptop-files {
|
||||
transform-origin: 190px 245px;
|
||||
animation: laptop-files 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.conduit-energy {
|
||||
animation: conduit-energy 2s linear infinite;
|
||||
}
|
||||
|
||||
.approval-gate {
|
||||
transform-origin: 440px 443px;
|
||||
animation: gate-hover 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.received-file {
|
||||
transform-origin: 616px 270px;
|
||||
animation: received-file 7s cubic-bezier(0.34, 1.2, 0.55, 1) infinite;
|
||||
}
|
||||
|
||||
.packet,
|
||||
.packet-spark {
|
||||
opacity: 0;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.packet-one {
|
||||
animation: hero-packet-one 7s cubic-bezier(0.45, 0.05, 0.35, 1) 0.6s infinite;
|
||||
}
|
||||
|
||||
.packet-two {
|
||||
animation: hero-packet-two 7s cubic-bezier(0.45, 0.05, 0.35, 1) 3s infinite;
|
||||
}
|
||||
|
||||
.packet-spark-one {
|
||||
animation: hero-spark-one 7s ease 0.6s infinite;
|
||||
}
|
||||
|
||||
.packet-spark-two {
|
||||
animation: hero-spark-two 7s ease 3s infinite;
|
||||
}
|
||||
|
||||
.hero-status {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid rgba(222, 211, 228, 0.95);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 15px 45px rgba(49, 27, 61, 0.11);
|
||||
backdrop-filter: blur(12px);
|
||||
transition: transform 180ms ease-out;
|
||||
}
|
||||
|
||||
.hero-status-secure {
|
||||
top: 2%;
|
||||
right: 5%;
|
||||
gap: 10px;
|
||||
padding: 10px 13px 10px 10px;
|
||||
border-radius: 14px;
|
||||
transform: translate(calc(var(--pointer-x) * 12px), calc(var(--pointer-y) * 8px)) rotate(2deg);
|
||||
animation: status-hover 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
display: grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
place-items: center;
|
||||
border-radius: 10px;
|
||||
background: var(--brand-100);
|
||||
color: var(--brand-600);
|
||||
}
|
||||
|
||||
.status-icon svg {
|
||||
width: 17px;
|
||||
}
|
||||
|
||||
.hero-status strong,
|
||||
.hero-status small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-status strong {
|
||||
color: #2b2530;
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hero-status small {
|
||||
color: #897f8e;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.hero-status-route {
|
||||
top: 18%;
|
||||
left: 9%;
|
||||
gap: 8px;
|
||||
padding: 7px 11px;
|
||||
border-radius: 999px;
|
||||
color: #534b57;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
transform: translate(calc(var(--pointer-x) * -8px), calc(var(--pointer-y) * -6px)) rotate(-3deg);
|
||||
}
|
||||
|
||||
.status-pulse,
|
||||
.live-dot {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #2bc477;
|
||||
box-shadow: 0 0 0 4px rgba(43, 196, 119, 0.13);
|
||||
animation: status-pulse 2.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.hero-file-label {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
bottom: 7%;
|
||||
left: 7%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
padding: 10px 13px;
|
||||
border: 1px solid rgba(222, 211, 228, 0.95);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 15px 45px rgba(49, 27, 61, 0.11);
|
||||
backdrop-filter: blur(12px);
|
||||
transform: translate(calc(var(--pointer-x) * -10px), calc(var(--pointer-y) * -7px)) rotate(-2deg);
|
||||
transition: transform 180ms ease-out;
|
||||
}
|
||||
|
||||
.file-type-icon {
|
||||
display: grid;
|
||||
width: 37px;
|
||||
height: 37px;
|
||||
place-items: center;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--brand-400), var(--brand-600));
|
||||
color: white;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 8px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.hero-file-label strong,
|
||||
.hero-file-label small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-file-label strong {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.hero-file-label small {
|
||||
color: var(--ink-muted);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
140
docs/components/icons.tsx
Normal file
140
docs/components/icons.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export type IconName =
|
||||
| "arrow"
|
||||
| "check"
|
||||
| "chevron"
|
||||
| "close"
|
||||
| "code"
|
||||
| "devices"
|
||||
| "file"
|
||||
| "folder"
|
||||
| "github"
|
||||
| "globe"
|
||||
| "lock"
|
||||
| "menu"
|
||||
| "nfc"
|
||||
| "qr"
|
||||
| "shield"
|
||||
| "spark"
|
||||
| "stop"
|
||||
| "verified"
|
||||
| "x";
|
||||
|
||||
type IconProps = SVGProps<SVGSVGElement> & {
|
||||
name: IconName;
|
||||
};
|
||||
|
||||
export function Icon({ name, ...props }: IconProps) {
|
||||
const paths: Record<IconName, React.ReactNode> = {
|
||||
arrow: (
|
||||
<>
|
||||
<path d="M5 12h14" />
|
||||
<path d="m14 7 5 5-5 5" />
|
||||
</>
|
||||
),
|
||||
check: <path d="m5 12 4 4L19 6" />,
|
||||
chevron: <path d="m9 18 6-6-6-6" />,
|
||||
close: (
|
||||
<>
|
||||
<path d="M6 6l12 12" />
|
||||
<path d="M18 6 6 18" />
|
||||
</>
|
||||
),
|
||||
code: (
|
||||
<>
|
||||
<path d="m8 9-4 3 4 3" />
|
||||
<path d="m16 9 4 3-4 3" />
|
||||
<path d="m14 5-4 14" />
|
||||
</>
|
||||
),
|
||||
devices: (
|
||||
<>
|
||||
<rect x="3" y="5" width="13" height="10" rx="2" />
|
||||
<path d="M7 19h5M9.5 15v4" />
|
||||
<rect x="17" y="8" width="4" height="10" rx="1" />
|
||||
</>
|
||||
),
|
||||
file: (
|
||||
<>
|
||||
<path d="M6 3h8l4 4v14H6z" />
|
||||
<path d="M14 3v5h4M9 13h6M9 17h4" />
|
||||
</>
|
||||
),
|
||||
folder: (
|
||||
<path d="M3 7.5A2.5 2.5 0 0 1 5.5 5H10l2 2h6.5A2.5 2.5 0 0 1 21 9.5v7a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 16.5z" />
|
||||
),
|
||||
github: (
|
||||
<path d="M12 2a10 10 0 0 0-3.2 19.5c.5.1.7-.2.7-.5v-1.9c-2.8.6-3.4-1.2-3.4-1.2-.5-1.1-1.1-1.4-1.1-1.4-.9-.6.1-.6.1-.6 1 0 1.5 1 1.5 1 .9 1.5 2.3 1.1 2.9.8.1-.6.3-1.1.6-1.3-2.2-.3-4.6-1.1-4.6-5a3.9 3.9 0 0 1 1-2.7c-.1-.3-.4-1.3.1-2.7 0 0 .9-.3 2.8 1a9.6 9.6 0 0 1 5.1 0c2-1.3 2.8-1 2.8-1 .6 1.4.2 2.4.1 2.7a3.9 3.9 0 0 1 1 2.7c0 3.9-2.4 4.7-4.6 5 .4.3.7.9.7 1.8V21c0 .3.2.6.7.5A10 10 0 0 0 12 2Z" />
|
||||
),
|
||||
globe: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18" />
|
||||
</>
|
||||
),
|
||||
lock: (
|
||||
<>
|
||||
<rect x="5" y="10" width="14" height="11" rx="3" />
|
||||
<path d="M8 10V7a4 4 0 0 1 8 0v3M12 14v3" />
|
||||
</>
|
||||
),
|
||||
menu: (
|
||||
<>
|
||||
<path d="M4 7h16M4 12h16M4 17h16" />
|
||||
</>
|
||||
),
|
||||
nfc: (
|
||||
<>
|
||||
<path d="M8 7a7 7 0 0 1 0 10M5 4a11 11 0 0 1 0 16" />
|
||||
<path d="M16 7a7 7 0 0 0 0 10M19 4a11 11 0 0 0 0 16" />
|
||||
<circle cx="12" cy="12" r="1" />
|
||||
</>
|
||||
),
|
||||
qr: (
|
||||
<>
|
||||
<rect x="3" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1" />
|
||||
<path d="M14 14h3v3h-3zM18 18h3v3h-3zM18 13h3M13 19h3v2" />
|
||||
</>
|
||||
),
|
||||
shield: (
|
||||
<>
|
||||
<path d="M12 3 4.5 6v5.5c0 4.8 3.2 8.1 7.5 9.5 4.3-1.4 7.5-4.7 7.5-9.5V6z" />
|
||||
<path d="m8.5 12 2.2 2.2 4.8-5" />
|
||||
</>
|
||||
),
|
||||
spark: (
|
||||
<path d="m12 2 1.6 6.4L20 10l-6.4 1.6L12 18l-1.6-6.4L4 10l6.4-1.6z" />
|
||||
),
|
||||
stop: <rect x="5" y="5" width="14" height="14" rx="3" />,
|
||||
verified: (
|
||||
<>
|
||||
<path d="m12 3 2 2.1 2.9-.1.1 2.9 2 2.1-2 2.1-.1 2.9-2.9-.1-2 2.1-2-2.1-2.9.1-.1-2.9-2-2.1 2-2.1.1-2.9 2.9.1z" />
|
||||
<path d="m9 10 2 2 4-4" />
|
||||
</>
|
||||
),
|
||||
x: (
|
||||
<>
|
||||
<path d="M6 6l12 12" />
|
||||
<path d="M18 6 6 18" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
{paths[name]}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
276
docs/components/illustration-motion.css
Normal file
276
docs/components/illustration-motion.css
Normal file
@@ -0,0 +1,276 @@
|
||||
@keyframes scene-orbits { to { transform: rotate(360deg); } }
|
||||
@keyframes laptop-files {
|
||||
0%, 100% { transform: translateY(0) rotate(0); }
|
||||
50% { transform: translateY(-5px) rotate(1deg); }
|
||||
}
|
||||
@keyframes conduit-energy { to { stroke-dashoffset: -38; } }
|
||||
@keyframes gate-hover {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-6px); }
|
||||
}
|
||||
@keyframes received-file {
|
||||
0%, 9% { opacity: 0.55; transform: translateY(12px) scale(0.92); }
|
||||
17%, 70% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
83%, 100% { opacity: 0.55; transform: translateY(12px) scale(0.92); }
|
||||
}
|
||||
@keyframes hero-packet-one {
|
||||
0%, 6% { opacity: 0; transform: translate(245px, 320px) scale(0.55) rotate(-8deg); }
|
||||
14% { opacity: 1; }
|
||||
43% { opacity: 1; transform: translate(380px, 427px) scale(0.82) rotate(5deg); }
|
||||
54% { opacity: 1; transform: translate(415px, 430px) scale(0.82) rotate(2deg); }
|
||||
84% { opacity: 1; transform: translate(556px, 296px) scale(0.65) rotate(10deg); }
|
||||
92%, 100% { opacity: 0; transform: translate(585px, 270px) scale(0.35) rotate(14deg); }
|
||||
}
|
||||
@keyframes hero-packet-two {
|
||||
0%, 6% { opacity: 0; transform: translate(255px, 327px) scale(0.5) rotate(8deg); }
|
||||
15% { opacity: 0.85; }
|
||||
45% { opacity: 0.9; transform: translate(382px, 440px) scale(0.75) rotate(-4deg); }
|
||||
56% { opacity: 0.9; transform: translate(414px, 436px) scale(0.75) rotate(-2deg); }
|
||||
85% { opacity: 0.8; transform: translate(560px, 308px) scale(0.55) rotate(-10deg); }
|
||||
93%, 100% { opacity: 0; transform: translate(588px, 278px) scale(0.25) rotate(-15deg); }
|
||||
}
|
||||
@keyframes hero-spark-one {
|
||||
0%, 12% { opacity: 0; transform: translate(266px, 346px) scale(0); }
|
||||
19% { opacity: 1; transform: translate(285px, 360px) scale(1); }
|
||||
29%, 100% { opacity: 0; transform: translate(310px, 380px) scale(0); }
|
||||
}
|
||||
@keyframes hero-spark-two {
|
||||
0%, 77% { opacity: 0; transform: translate(545px, 307px) scale(0); }
|
||||
87% { opacity: 1; transform: translate(575px, 279px) scale(1); }
|
||||
97%, 100% { opacity: 0; transform: translate(603px, 260px) scale(0); }
|
||||
}
|
||||
@keyframes status-hover {
|
||||
0%, 100% { margin-top: 0; }
|
||||
50% { margin-top: -7px; }
|
||||
}
|
||||
@keyframes status-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 3px rgba(43, 196, 119, 0.1); }
|
||||
50% { box-shadow: 0 0 0 7px rgba(43, 196, 119, 0); }
|
||||
}
|
||||
@keyframes flow-glow {
|
||||
0%, 100% { transform: scale(0.8); opacity: 0.6; }
|
||||
50% { transform: scale(1.2); opacity: 1; }
|
||||
}
|
||||
@keyframes flow-drop {
|
||||
0%, 7% { offset-distance: 0%; opacity: 0; }
|
||||
13% { opacity: 1; }
|
||||
47% { offset-distance: 51%; opacity: 1; }
|
||||
57% { offset-distance: 51%; opacity: 1; }
|
||||
91% { offset-distance: 100%; opacity: 1; }
|
||||
97%, 100% { offset-distance: 100%; opacity: 0; }
|
||||
}
|
||||
@keyframes gate-breathe {
|
||||
0%, 100% { transform: scale(0.98); }
|
||||
50% { transform: scale(1.04); }
|
||||
}
|
||||
@keyframes complete-ring {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.18); }
|
||||
50% { box-shadow: 0 0 0 13px rgba(168, 85, 247, 0); }
|
||||
}
|
||||
@keyframes invite-panel-enter {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
@keyframes qr-card-float {
|
||||
0%, 100% { transform: rotate(2deg) translateY(0); }
|
||||
50% { transform: rotate(-1deg) translateY(-8px); }
|
||||
}
|
||||
@keyframes qr-cell {
|
||||
from { opacity: 0; transform: scale(0.2); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
@keyframes qr-scan {
|
||||
0%, 100% { top: 40px; opacity: 0; }
|
||||
12%, 88% { opacity: 1; }
|
||||
50% { top: 222px; }
|
||||
}
|
||||
@keyframes nfc-dot {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.2); }
|
||||
50% { box-shadow: 0 0 0 19px rgba(168, 85, 247, 0); }
|
||||
}
|
||||
@keyframes nfc-wave {
|
||||
0%, 100% { opacity: 0.25; transform: translateY(-50%) scale(0.88); }
|
||||
50% { opacity: 1; transform: translateY(-50%) scale(1.04); }
|
||||
}
|
||||
@keyframes vnd-drop {
|
||||
0%, 8% { transform: translateX(-50%) translateY(-16px) rotate(-5deg); }
|
||||
20%, 78% { transform: translateX(-50%) translateY(0) rotate(-2deg); }
|
||||
88%, 100% { transform: translateX(-50%) translateY(-16px) rotate(-5deg); }
|
||||
}
|
||||
@keyframes avatar-pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.24); }
|
||||
75%, 100% { box-shadow: 0 0 0 14px rgba(168, 85, 247, 0); }
|
||||
}
|
||||
@keyframes approval-state-enter {
|
||||
from { opacity: 0; transform: translateY(7px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
@keyframes progress-fill {
|
||||
from { width: 0; }
|
||||
to { width: 68%; }
|
||||
}
|
||||
@keyframes network-dash { to { stroke-dashoffset: -44; } }
|
||||
@keyframes relay-ring {
|
||||
0% { opacity: 0.6; transform: translateX(-50%) scale(0.72); }
|
||||
85%, 100% { opacity: 0; transform: translateX(-50%) scale(1.2); }
|
||||
}
|
||||
@keyframes network-packet-top {
|
||||
0%, 8% { left: 15%; top: 40%; opacity: 0; transform: rotate(-7deg) scale(0.7); }
|
||||
15% { opacity: 1; }
|
||||
48% { left: 47%; top: 20%; opacity: 1; transform: rotate(3deg) scale(0.9); }
|
||||
85% { left: 82%; top: 40%; opacity: 1; transform: rotate(8deg) scale(0.72); }
|
||||
94%, 100% { left: 85%; top: 42%; opacity: 0; transform: rotate(10deg) scale(0.4); }
|
||||
}
|
||||
@keyframes network-packet-bottom {
|
||||
0%, 8% { left: 16%; top: 50%; opacity: 0; transform: rotate(8deg) scale(0.65); }
|
||||
15% { opacity: 0.8; }
|
||||
48% { left: 48%; top: 69%; opacity: 0.8; transform: rotate(-3deg) scale(0.8); }
|
||||
84% { left: 82%; top: 50%; opacity: 0.8; transform: rotate(-8deg) scale(0.7); }
|
||||
94%, 100% { left: 85%; top: 48%; opacity: 0; transform: rotate(-10deg) scale(0.4); }
|
||||
}
|
||||
@keyframes orbit-spin {
|
||||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
@keyframes platform-glow {
|
||||
0%, 100% { opacity: 0.45; transform: scale(0.82); }
|
||||
50% { opacity: 1; transform: scale(1.15); }
|
||||
}
|
||||
@keyframes platform-chip-float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.flow-layout {
|
||||
grid-template-columns: minmax(450px, 1fr) minmax(350px, 0.8fr);
|
||||
gap: 45px;
|
||||
}
|
||||
|
||||
.flow-device { transform: scale(0.88); }
|
||||
.flow-device-sender { transform: scale(0.88) rotate(-6deg); transform-origin: top left; }
|
||||
.flow-device-receiver { transform: scale(0.88) rotate(5deg); transform-origin: bottom right; }
|
||||
|
||||
.invite-panel {
|
||||
grid-template-columns: minmax(0, 0.85fr) minmax(420px, 1.15fr);
|
||||
}
|
||||
|
||||
.invite-copy { padding: 45px; }
|
||||
.network-visual { min-height: 500px; }
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.hero-visual {
|
||||
max-width: 720px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.flow-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.flow-visual {
|
||||
position: relative;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.invite-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.invite-art {
|
||||
min-height: 450px;
|
||||
border-top: 1px solid var(--line);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.network-visual {
|
||||
min-height: 520px;
|
||||
}
|
||||
|
||||
.platform-orbit {
|
||||
max-width: 620px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.hero-stage {
|
||||
width: 112%;
|
||||
margin-left: -6%;
|
||||
}
|
||||
|
||||
.hero-status-secure {
|
||||
top: 0;
|
||||
right: 2%;
|
||||
transform: scale(0.88) rotate(2deg);
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.hero-status-route { left: 4%; transform: scale(0.88) rotate(-3deg); transform-origin: top left; }
|
||||
.hero-file-label { bottom: 3%; left: 4%; transform: scale(0.88) rotate(-2deg); transform-origin: bottom left; }
|
||||
|
||||
.flow-visual {
|
||||
height: 540px;
|
||||
border-radius: 21px;
|
||||
}
|
||||
|
||||
.flow-device {
|
||||
width: 137px;
|
||||
height: 215px;
|
||||
}
|
||||
|
||||
.flow-device-sender { top: 24px; left: 16px; }
|
||||
.flow-device-receiver { right: 14px; bottom: 20px; }
|
||||
.flow-track { inset: 0 -2%; width: 104%; height: 100%; }
|
||||
.flow-annotation { display: none; }
|
||||
|
||||
.flow-step {
|
||||
grid-template-columns: auto 1fr;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.invite-tabs button {
|
||||
min-height: 59px;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.invite-tabs svg { width: 17px; }
|
||||
.invite-copy { padding: 42px 24px; }
|
||||
.invite-art { min-height: 390px; }
|
||||
.invite-qr-art { transform: scale(0.86) rotate(2deg); }
|
||||
.invite-nfc-art { transform: scale(0.82); }
|
||||
.invite-file-art { transform: scale(0.85); }
|
||||
|
||||
.approval-body { min-height: 370px; padding: 28px 20px; }
|
||||
.approval-heading h3 { font-size: 18px; }
|
||||
.approval-actions { grid-template-columns: 1fr 1.2fr; }
|
||||
|
||||
.network-visual {
|
||||
min-height: 430px;
|
||||
border-radius: 21px;
|
||||
}
|
||||
|
||||
.network-device {
|
||||
top: 150px;
|
||||
width: 77px;
|
||||
height: 112px;
|
||||
}
|
||||
|
||||
.network-device-left { left: 13px; }
|
||||
.network-device-right { right: 13px; }
|
||||
.network-device svg { width: 24px; }
|
||||
.network-relay-node { bottom: 29px; }
|
||||
.network-relay-node small { max-width: 160px; text-align: center; }
|
||||
.network-direct-label { top: 43px; }
|
||||
|
||||
.platform-orbit { width: 112%; margin-left: -6%; }
|
||||
.platform-center { width: 112px; height: 112px; border-radius: 30px; }
|
||||
.platform-center svg { width: 68px; height: 68px; }
|
||||
.platform-chip { padding: 8px 10px; font-size: 9px; }
|
||||
.platform-chip-1 { left: 10%; }
|
||||
.platform-chip-2 { right: 0; }
|
||||
.platform-chip-3 { right: -2%; }
|
||||
.platform-chip-4 { left: 17%; }
|
||||
}
|
||||
405
docs/components/invitation-scene.css
Normal file
405
docs/components/invitation-scene.css
Normal file
@@ -0,0 +1,405 @@
|
||||
/* Invitation methods */
|
||||
.invite-showcase {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 25px;
|
||||
background: white;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.invite-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: #f7f4f8;
|
||||
}
|
||||
|
||||
.invite-tabs button {
|
||||
display: inline-flex;
|
||||
min-height: 66px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
border: 0;
|
||||
border-right: 1px solid var(--line);
|
||||
background: transparent;
|
||||
color: var(--ink-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: color 180ms ease, background 180ms ease;
|
||||
}
|
||||
|
||||
.invite-tabs button:last-child { border-right: 0; }
|
||||
.invite-tabs button:hover { color: var(--ink); }
|
||||
.invite-tabs button.is-active {
|
||||
position: relative;
|
||||
background: white;
|
||||
color: var(--brand-600);
|
||||
}
|
||||
|
||||
.invite-tabs button.is-active::after {
|
||||
position: absolute;
|
||||
right: 20%;
|
||||
bottom: -1px;
|
||||
left: 20%;
|
||||
height: 2px;
|
||||
border-radius: 999px 999px 0 0;
|
||||
background: var(--brand-500);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.invite-tabs svg { width: 19px; }
|
||||
|
||||
.invite-panel {
|
||||
display: grid;
|
||||
min-height: 490px;
|
||||
grid-template-columns: minmax(0, 0.83fr) minmax(480px, 1.17fr);
|
||||
animation: invite-panel-enter 420ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.invite-copy {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 60px;
|
||||
}
|
||||
|
||||
.mono-label {
|
||||
color: var(--brand-600);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.11em;
|
||||
}
|
||||
|
||||
.invite-copy h3 {
|
||||
max-width: 430px;
|
||||
margin: 12px 0 18px;
|
||||
font-size: clamp(2rem, 3vw, 3.15rem);
|
||||
letter-spacing: -0.055em;
|
||||
line-height: 1.06;
|
||||
}
|
||||
|
||||
.invite-copy > p {
|
||||
max-width: 440px;
|
||||
margin-bottom: 30px;
|
||||
color: var(--ink-soft);
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.invite-detail {
|
||||
display: flex;
|
||||
gap: 11px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.invite-detail > svg {
|
||||
width: 19px;
|
||||
flex: 0 0 auto;
|
||||
color: var(--brand-600);
|
||||
}
|
||||
|
||||
.invite-detail strong,
|
||||
.invite-detail small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.invite-detail strong {
|
||||
margin-bottom: 3px;
|
||||
color: #3c3440;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.invite-detail small {
|
||||
color: var(--ink-muted);
|
||||
font-size: 9px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.invite-art {
|
||||
position: relative;
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
place-items: center;
|
||||
border-left: 1px solid var(--line);
|
||||
background:
|
||||
linear-gradient(rgba(116, 86, 130, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(116, 86, 130, 0.05) 1px, transparent 1px),
|
||||
radial-gradient(circle at center, #fbf7fd, #f2edf5);
|
||||
background-size: 24px 24px, 24px 24px, auto;
|
||||
}
|
||||
|
||||
.invite-art::before {
|
||||
position: absolute;
|
||||
width: 330px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
background: rgba(168, 85, 247, 0.14);
|
||||
content: "";
|
||||
filter: blur(55px);
|
||||
}
|
||||
|
||||
.invite-qr-art {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 265px;
|
||||
height: 265px;
|
||||
place-items: center;
|
||||
border: 1px solid #e0d5e5;
|
||||
border-radius: 25px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 30px 80px rgba(59, 35, 70, 0.18);
|
||||
transform: rotate(2deg);
|
||||
animation: qr-card-float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.invite-qr-grid {
|
||||
display: grid;
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
grid-template-columns: repeat(9, 1fr);
|
||||
grid-template-rows: repeat(9, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.invite-qr-grid span {
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.invite-qr-grid span.is-filled {
|
||||
background: #2b2230;
|
||||
animation: qr-cell 600ms ease backwards;
|
||||
}
|
||||
|
||||
.invite-qr-grid span:nth-child(3n) { animation-delay: 80ms; }
|
||||
.invite-qr-grid span:nth-child(4n) { animation-delay: 160ms; }
|
||||
.invite-qr-grid span:nth-child(7n) { animation-delay: 230ms; }
|
||||
|
||||
.qr-scan-line {
|
||||
position: absolute;
|
||||
right: 34px;
|
||||
left: 34px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--brand-500), transparent);
|
||||
box-shadow: 0 0 13px var(--brand-400);
|
||||
animation: qr-scan 3.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.qr-corner {
|
||||
position: absolute;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-color: var(--brand-500);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.qr-corner-one { top: 21px; left: 21px; border-width: 2px 0 0 2px; border-radius: 7px 0 0; }
|
||||
.qr-corner-two { top: 21px; right: 21px; border-width: 2px 2px 0 0; border-radius: 0 7px 0 0; }
|
||||
.qr-corner-three { right: 21px; bottom: 21px; border-width: 0 2px 2px 0; border-radius: 0 0 7px; }
|
||||
.qr-corner-four { bottom: 21px; left: 21px; border-width: 0 0 2px 2px; border-radius: 0 0 0 7px; }
|
||||
|
||||
.invite-nfc-art {
|
||||
position: relative;
|
||||
width: 440px;
|
||||
height: 330px;
|
||||
}
|
||||
|
||||
.nfc-device {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 155px;
|
||||
height: 300px;
|
||||
border: 7px solid #d7cfdc;
|
||||
border-radius: 30px;
|
||||
background: #211c25;
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
|
||||
.nfc-device::after {
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
left: 50%;
|
||||
width: 50px;
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
background: #09070a;
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.nfc-screen-dot {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border: 1px solid rgba(192, 132, 252, 0.55);
|
||||
border-radius: 50%;
|
||||
background: rgba(168, 85, 247, 0.2);
|
||||
transform: translate(-50%, -50%);
|
||||
animation: nfc-dot 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.nfc-tag {
|
||||
position: absolute;
|
||||
right: 25px;
|
||||
bottom: 30px;
|
||||
display: grid;
|
||||
width: 138px;
|
||||
height: 138px;
|
||||
place-items: center;
|
||||
border: 1px solid #d9cfe0;
|
||||
border-radius: 28px;
|
||||
background: white;
|
||||
box-shadow: 0 25px 70px rgba(52, 31, 62, 0.18);
|
||||
color: var(--brand-600);
|
||||
transform: rotate(8deg);
|
||||
}
|
||||
|
||||
.nfc-tag svg { width: 46px; }
|
||||
.nfc-tag small {
|
||||
position: absolute;
|
||||
bottom: 18px;
|
||||
color: var(--ink-muted);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.nfc-waves {
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
left: 145px;
|
||||
width: 185px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.nfc-waves span {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
border: 2px solid var(--brand-500);
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
animation: nfc-wave 2.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.nfc-waves span:nth-child(1) { width: 55px; height: 70px; }
|
||||
.nfc-waves span:nth-child(2) { width: 98px; height: 112px; animation-delay: 180ms; }
|
||||
.nfc-waves span:nth-child(3) { width: 142px; height: 154px; animation-delay: 360ms; }
|
||||
.nfc-waves i {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 15px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-500);
|
||||
box-shadow: 0 0 0 8px rgba(168, 85, 247, 0.13);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.invite-file-art {
|
||||
position: relative;
|
||||
width: 410px;
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.vnd-file {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 13px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
width: 218px;
|
||||
height: 270px;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border: 1px solid #d8cbe0;
|
||||
border-radius: 23px;
|
||||
background: linear-gradient(145deg, #fff, #f7f0fb);
|
||||
box-shadow: 0 30px 80px rgba(59, 35, 70, 0.2);
|
||||
transform: translateX(-50%) rotate(-2deg);
|
||||
animation: vnd-drop 4.8s cubic-bezier(0.3, 0.75, 0.3, 1) infinite;
|
||||
}
|
||||
|
||||
.vnd-fold {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-bottom: 1px solid #d1c1dc;
|
||||
border-left: 1px solid #d1c1dc;
|
||||
border-radius: 0 23px 0 16px;
|
||||
background: #e7cdf9;
|
||||
clip-path: polygon(0 0, 100% 100%, 0 100%);
|
||||
}
|
||||
|
||||
.vnd-file > svg {
|
||||
width: 58px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--brand-600);
|
||||
}
|
||||
|
||||
.vnd-file strong {
|
||||
font-family: var(--font-display);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.vnd-file small {
|
||||
color: var(--ink-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.vnd-file code {
|
||||
margin-top: 18px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 7px;
|
||||
background: var(--brand-100);
|
||||
color: var(--brand-600);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.file-tray {
|
||||
position: absolute;
|
||||
right: 37px;
|
||||
bottom: 7px;
|
||||
left: 37px;
|
||||
height: 73px;
|
||||
border: 2px solid #cabbd2;
|
||||
border-radius: 19px;
|
||||
background: rgba(237, 228, 242, 0.85);
|
||||
transform: perspective(250px) rotateX(55deg);
|
||||
}
|
||||
|
||||
.file-tray span {
|
||||
position: absolute;
|
||||
inset: 14px;
|
||||
border: 1px dashed #aa8cbc;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.file-art-orbit {
|
||||
position: absolute;
|
||||
border: 1px solid rgba(168, 85, 247, 0.2);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.file-art-orbit-one { inset: 14px 32px 44px; animation: scene-orbits 24s linear infinite; }
|
||||
.file-art-orbit-two { inset: 52px 70px 80px; border-style: dashed; animation: scene-orbits 18s linear infinite reverse; }
|
||||
|
||||
153
docs/components/invitation-showcase.tsx
Normal file
153
docs/components/invitation-showcase.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Icon, type IconName } from "@/components/icons";
|
||||
|
||||
type Method = "qr" | "nfc" | "file";
|
||||
|
||||
const methods: Array<{
|
||||
id: Method;
|
||||
icon: IconName;
|
||||
label: string;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}> = [
|
||||
{
|
||||
id: "qr",
|
||||
icon: "qr",
|
||||
label: "QR code",
|
||||
eyebrow: "IN THE SAME ROOM",
|
||||
title: "Point. Scan. Connected.",
|
||||
text: "Show the invitation on one screen and scan it from the other. No contact exchange required.",
|
||||
},
|
||||
{
|
||||
id: "nfc",
|
||||
icon: "nfc",
|
||||
label: "NFC",
|
||||
eyebrow: "THE QUICKEST TAP",
|
||||
title: "Bring devices together.",
|
||||
text: "Write or open an invitation with an NFC tag for a physical, close-range handoff.",
|
||||
},
|
||||
{
|
||||
id: "file",
|
||||
icon: "file",
|
||||
label: ".vnd file",
|
||||
eyebrow: "SEND IT ANYWHERE",
|
||||
title: "An invitation that travels.",
|
||||
text: "Save the small invitation as a .vnd file, then pass it through the channel you already use.",
|
||||
},
|
||||
];
|
||||
|
||||
function QrArt() {
|
||||
const filled = new Set([
|
||||
0, 1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 28, 29, 30, 31, 32,
|
||||
36, 38, 40, 42, 44, 48, 49, 50, 51, 52, 54, 56, 58, 60, 62, 64, 66, 67, 69,
|
||||
72, 73, 74, 76, 78, 79, 80,
|
||||
]);
|
||||
return (
|
||||
<div className="invite-qr-art">
|
||||
<div className="invite-qr-grid">
|
||||
{Array.from({ length: 81 }, (_, index) => (
|
||||
<span key={index} className={filled.has(index) ? "is-filled" : ""} />
|
||||
))}
|
||||
</div>
|
||||
<span className="qr-scan-line" />
|
||||
<span className="qr-corner qr-corner-one" />
|
||||
<span className="qr-corner qr-corner-two" />
|
||||
<span className="qr-corner qr-corner-three" />
|
||||
<span className="qr-corner qr-corner-four" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NfcArt() {
|
||||
return (
|
||||
<div className="invite-nfc-art">
|
||||
<div className="nfc-device nfc-device-left">
|
||||
<span className="nfc-screen-dot" />
|
||||
</div>
|
||||
<div className="nfc-waves">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<i />
|
||||
</div>
|
||||
<div className="nfc-tag">
|
||||
<Icon name="nfc" />
|
||||
<small>VniDrop</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FileArt() {
|
||||
return (
|
||||
<div className="invite-file-art">
|
||||
<span className="file-art-orbit file-art-orbit-one" />
|
||||
<span className="file-art-orbit file-art-orbit-two" />
|
||||
<div className="vnd-file">
|
||||
<span className="vnd-fold" />
|
||||
<Icon name="file" />
|
||||
<strong>Weekend trip</strong>
|
||||
<small>VniDrop invitation</small>
|
||||
<code>.vnd</code>
|
||||
</div>
|
||||
<div className="file-tray">
|
||||
<span />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function InvitationShowcase() {
|
||||
const [active, setActive] = useState<Method>("qr");
|
||||
const current = methods.find((method) => method.id === active) ?? methods[0];
|
||||
|
||||
return (
|
||||
<div className="invite-showcase">
|
||||
<div className="invite-tabs" role="tablist" aria-label="Invitation methods">
|
||||
{methods.map((method) => (
|
||||
<button
|
||||
key={method.id}
|
||||
id={`invite-tab-${method.id}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active === method.id}
|
||||
aria-controls="invite-panel"
|
||||
className={active === method.id ? "is-active" : ""}
|
||||
onClick={() => setActive(method.id)}
|
||||
>
|
||||
<Icon name={method.icon} />
|
||||
{method.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
id="invite-panel"
|
||||
className="invite-panel"
|
||||
role="tabpanel"
|
||||
aria-labelledby={`invite-tab-${active}`}
|
||||
key={active}
|
||||
>
|
||||
<div className="invite-copy">
|
||||
<span className="mono-label">{current.eyebrow}</span>
|
||||
<h3>{current.title}</h3>
|
||||
<p>{current.text}</p>
|
||||
<div className="invite-detail">
|
||||
<Icon name="lock" />
|
||||
<span>
|
||||
<strong>The invitation is a private access link.</strong>
|
||||
<small>Only share it with people you intend to receive the transfer.</small>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="invite-art" aria-hidden="true">
|
||||
{active === "qr" && <QrArt />}
|
||||
{active === "nfc" && <NfcArt />}
|
||||
{active === "file" && <FileArt />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
docs/components/network-illustration.tsx
Normal file
51
docs/components/network-illustration.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Icon } from "@/components/icons";
|
||||
|
||||
export function NetworkIllustration() {
|
||||
return (
|
||||
<div className="network-visual" aria-hidden="true">
|
||||
<div className="network-grid" />
|
||||
<div className="network-device network-device-left">
|
||||
<span className="network-device-top" />
|
||||
<Icon name="file" />
|
||||
<small>SENDER</small>
|
||||
</div>
|
||||
<div className="network-device network-device-right">
|
||||
<span className="network-device-top" />
|
||||
<Icon name="check" />
|
||||
<small>RECEIVER</small>
|
||||
</div>
|
||||
<svg className="network-routes" viewBox="0 0 760 430">
|
||||
<defs>
|
||||
<linearGradient id="network-route-gradient" x1="115" y1="210" x2="650" y2="210" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#D08BFF" />
|
||||
<stop offset="0.5" stopColor="#A855F7" />
|
||||
<stop offset="1" stopColor="#7C2AEF" />
|
||||
</linearGradient>
|
||||
<filter id="network-route-glow" x="-30%" y="-100%" width="160%" height="300%">
|
||||
<feGaussianBlur stdDeviation="8" />
|
||||
</filter>
|
||||
</defs>
|
||||
<path className="network-direct-glow" d="M134 227C279 92 491 92 626 227" />
|
||||
<path className="network-direct" d="M134 227C279 92 491 92 626 227" />
|
||||
<path className="network-direct-pulse" d="M134 227C279 92 491 92 626 227" />
|
||||
<path className="network-relay-base" d="M134 247c126 87 371 87 492 0" />
|
||||
<path className="network-relay-line" d="M134 247c126 87 371 87 492 0" />
|
||||
</svg>
|
||||
<div className="network-direct-label">
|
||||
<span className="live-dot" />
|
||||
DIRECT WHEN POSSIBLE
|
||||
</div>
|
||||
<div className="network-relay-node">
|
||||
<span className="relay-ring relay-ring-one" />
|
||||
<span className="relay-ring relay-ring-two" />
|
||||
<div className="relay-core">
|
||||
<Icon name="lock" />
|
||||
</div>
|
||||
<strong>Encrypted relay</strong>
|
||||
<small>Forwards packets · stores no transfer copy</small>
|
||||
</div>
|
||||
<div className="network-packet network-packet-one"><Icon name="file" /></div>
|
||||
<div className="network-packet network-packet-two"><Icon name="file" /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
190
docs/components/network-scene.css
Normal file
190
docs/components/network-scene.css
Normal file
@@ -0,0 +1,190 @@
|
||||
/* Direct / relay network scene */
|
||||
.network-visual {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 520px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #302737;
|
||||
border-radius: 27px;
|
||||
background: radial-gradient(circle at center, rgba(168, 85, 247, 0.1), transparent 47%), #151117;
|
||||
}
|
||||
|
||||
.network-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
|
||||
background-size: 30px 30px;
|
||||
mask-image: radial-gradient(circle at center, black, transparent 79%);
|
||||
}
|
||||
|
||||
.network-device {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
top: 178px;
|
||||
display: grid;
|
||||
width: 105px;
|
||||
height: 145px;
|
||||
place-items: center;
|
||||
border: 5px solid #3b3340;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(145deg, #29222d, #171419);
|
||||
box-shadow: 0 22px 60px rgba(0, 0, 0, 0.38);
|
||||
color: #d89bff;
|
||||
}
|
||||
|
||||
.network-device-left { left: 30px; transform: rotate(-5deg); }
|
||||
.network-device-right { right: 30px; transform: rotate(5deg); }
|
||||
.network-device svg { width: 31px; }
|
||||
.network-device small {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
color: #756b79;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 7px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.09em;
|
||||
}
|
||||
|
||||
.network-device-top {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 50%;
|
||||
width: 35px;
|
||||
height: 5px;
|
||||
border-radius: 999px;
|
||||
background: #080609;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.network-routes {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
inset: 34px 0 auto;
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.network-direct-glow,
|
||||
.network-direct,
|
||||
.network-direct-pulse,
|
||||
.network-relay-base,
|
||||
.network-relay-line {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.network-direct-glow {
|
||||
stroke: rgba(168, 85, 247, 0.26);
|
||||
stroke-width: 20;
|
||||
filter: url(#network-route-glow);
|
||||
}
|
||||
|
||||
.network-direct {
|
||||
stroke: #3d3343;
|
||||
stroke-width: 8;
|
||||
}
|
||||
|
||||
.network-direct-pulse {
|
||||
stroke: url(#network-route-gradient);
|
||||
stroke-width: 3;
|
||||
stroke-dasharray: 4 18;
|
||||
animation: network-dash 2.2s linear infinite;
|
||||
}
|
||||
|
||||
.network-relay-base {
|
||||
stroke: #2e2732;
|
||||
stroke-width: 5;
|
||||
}
|
||||
|
||||
.network-relay-line {
|
||||
stroke: #776681;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 4 11;
|
||||
animation: network-dash 3.4s linear infinite reverse;
|
||||
}
|
||||
|
||||
.network-direct-label {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
top: 62px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #372c3c;
|
||||
border-radius: 999px;
|
||||
background: rgba(25, 20, 28, 0.82);
|
||||
color: #9a8fa0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 7px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.network-relay-node {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
bottom: 39px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.relay-core {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
width: 66px;
|
||||
height: 66px;
|
||||
margin-bottom: 11px;
|
||||
place-items: center;
|
||||
border: 1px solid #5c426a;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(145deg, #312139, #1d1721);
|
||||
box-shadow: 0 0 45px rgba(168, 85, 247, 0.18);
|
||||
color: #d294fc;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.relay-core svg { width: 24px; transform: rotate(-45deg); }
|
||||
.network-relay-node strong { color: #d9d1dc; font-size: 10px; }
|
||||
.network-relay-node small { color: #756b79; font-size: 7px; }
|
||||
|
||||
.relay-ring {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
left: 50%;
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%);
|
||||
animation: relay-ring 3s ease-out infinite;
|
||||
}
|
||||
|
||||
.relay-ring-one { width: 100px; height: 100px; }
|
||||
.relay-ring-two { width: 136px; height: 136px; animation-delay: 1.2s; }
|
||||
|
||||
.network-packet {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
display: grid;
|
||||
width: 26px;
|
||||
height: 32px;
|
||||
place-items: center;
|
||||
border: 1px solid #aa65d2;
|
||||
border-radius: 6px;
|
||||
background: #2c2231;
|
||||
color: #d69bfa;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.network-packet svg { width: 13px; }
|
||||
.network-packet-one { animation: network-packet-top 6s ease-in-out infinite; }
|
||||
.network-packet-two { animation: network-packet-bottom 7s ease-in-out 2.2s infinite; }
|
||||
|
||||
25
docs/components/platform-orbit.tsx
Normal file
25
docs/components/platform-orbit.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { BrandMark } from "@/components/brand";
|
||||
|
||||
const platforms = ["Android", "iOS", "macOS", "Windows", "Linux"];
|
||||
|
||||
export function PlatformOrbit() {
|
||||
return (
|
||||
<div className="platform-orbit" aria-label="VniDrop supports Android, iOS, macOS, Windows, and Linux">
|
||||
<div className="platform-ring platform-ring-outer" aria-hidden="true" />
|
||||
<div className="platform-ring platform-ring-inner" aria-hidden="true" />
|
||||
<div className="platform-center" aria-hidden="true">
|
||||
<span className="platform-center-glow" />
|
||||
<BrandMark />
|
||||
</div>
|
||||
{platforms.map((platform, index) => (
|
||||
<span
|
||||
key={platform}
|
||||
className={`platform-chip platform-chip-${index + 1}`}
|
||||
>
|
||||
<i />
|
||||
{platform}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
docs/components/platform-scene.css
Normal file
103
docs/components/platform-scene.css
Normal file
@@ -0,0 +1,103 @@
|
||||
/* Cross-platform orbit */
|
||||
.platform-orbit {
|
||||
position: relative;
|
||||
width: min(100%, 610px);
|
||||
aspect-ratio: 1;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.platform-ring {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
border: 1px solid #e2d8e7;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.platform-ring::before,
|
||||
.platform-ring::after {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-400);
|
||||
box-shadow: 0 0 0 7px rgba(168, 85, 247, 0.08);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.platform-ring-outer {
|
||||
width: 88%;
|
||||
height: 88%;
|
||||
animation: orbit-spin 35s linear infinite;
|
||||
}
|
||||
|
||||
.platform-ring-outer::before { top: 13%; left: 10%; width: 7px; height: 7px; }
|
||||
.platform-ring-outer::after { right: 7%; bottom: 23%; width: 5px; height: 5px; }
|
||||
|
||||
.platform-ring-inner {
|
||||
width: 58%;
|
||||
height: 58%;
|
||||
border-style: dashed;
|
||||
animation: orbit-spin 25s linear infinite reverse;
|
||||
}
|
||||
|
||||
.platform-ring-inner::before { top: 48%; left: -4px; width: 7px; height: 7px; }
|
||||
.platform-ring-inner::after { top: 4%; right: 23%; width: 5px; height: 5px; }
|
||||
|
||||
.platform-center {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: grid;
|
||||
width: 145px;
|
||||
height: 145px;
|
||||
place-items: center;
|
||||
border: 1px solid #e5d8eb;
|
||||
border-radius: 38px;
|
||||
background: white;
|
||||
box-shadow: 0 30px 80px rgba(55, 29, 67, 0.16);
|
||||
transform: translate(-50%, -50%) rotate(-3deg);
|
||||
}
|
||||
|
||||
.platform-center svg { position: relative; z-index: 2; width: 87px; height: 87px; }
|
||||
|
||||
.platform-center-glow {
|
||||
position: absolute;
|
||||
inset: -45px;
|
||||
border-radius: 50%;
|
||||
background: rgba(168, 85, 247, 0.18);
|
||||
filter: blur(42px);
|
||||
animation: platform-glow 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.platform-chip {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #e2d9e6;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.93);
|
||||
box-shadow: 0 12px 35px rgba(53, 34, 61, 0.1);
|
||||
color: #4b4350;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
animation: platform-chip-float 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.platform-chip i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-500);
|
||||
box-shadow: 0 0 0 4px var(--brand-100);
|
||||
}
|
||||
|
||||
.platform-chip-1 { top: 8%; left: 17%; animation-delay: -1s; }
|
||||
.platform-chip-2 { top: 19%; right: 5%; animation-delay: -2.7s; }
|
||||
.platform-chip-3 { right: 1%; bottom: 22%; animation-delay: -4s; }
|
||||
.platform-chip-4 { bottom: 7%; left: 24%; animation-delay: -0.5s; }
|
||||
.platform-chip-5 { top: 45%; left: 0; animation-delay: -3.2s; }
|
||||
|
||||
52
docs/components/reveal.tsx
Normal file
52
docs/components/reveal.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
type RevealProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
export function Reveal({ children, className = "", delay = 0 }: RevealProps) {
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.classList.add("motion-ready");
|
||||
const target = targetRef.current;
|
||||
if (!target) return;
|
||||
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
if (reduceMotion || !("IntersectionObserver" in window)) {
|
||||
target.classList.add("is-visible");
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
entry.target.classList.add("is-visible");
|
||||
observer.unobserve(entry.target);
|
||||
});
|
||||
},
|
||||
{ rootMargin: "0px 0px -10%", threshold: 0.08 },
|
||||
);
|
||||
|
||||
observer.observe(target);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={targetRef}
|
||||
className={`reveal ${className}`.trim()}
|
||||
data-reveal
|
||||
style={{ "--reveal-delay": `${delay}ms` } as CSSProperties}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
docs/components/site-footer.tsx
Normal file
47
docs/components/site-footer.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import Link from "next/link";
|
||||
import { BrandMark } from "@/components/brand";
|
||||
import { Icon } from "@/components/icons";
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="site-footer">
|
||||
<div className="footer-main page-shell">
|
||||
<div className="footer-brand">
|
||||
<span className="footer-mark">
|
||||
<BrandMark />
|
||||
</span>
|
||||
<div>
|
||||
<p className="footer-name">VniDrop</p>
|
||||
<p>Send files directly. Stay in control.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer-links">
|
||||
<div>
|
||||
<p className="footer-label">Explore</p>
|
||||
<Link href="/#how-it-works">How it works</Link>
|
||||
<Link href="/#privacy">Privacy by design</Link>
|
||||
<Link href="/#platforms">Platforms</Link>
|
||||
</div>
|
||||
<div>
|
||||
<p className="footer-label">Project</p>
|
||||
<a href="https://github.com/vnidrop/vnidrop" target="_blank" rel="noreferrer">
|
||||
GitHub <Icon name="arrow" />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/vnidrop/vnidrop/blob/master/LICENSE"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Apache 2.0 <Icon name="arrow" />
|
||||
</a>
|
||||
<Link href="/privacy/">Privacy policy</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer-bottom page-shell">
|
||||
<p>© 2026 VniDrop contributors.</p>
|
||||
<p>Open source · Early development</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
71
docs/components/site-header.tsx
Normal file
71
docs/components/site-header.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Brand } from "@/components/brand";
|
||||
import { Icon } from "@/components/icons";
|
||||
|
||||
const navigation = [
|
||||
{ href: "/#how-it-works", label: "How it works" },
|
||||
{ href: "/#privacy", label: "Privacy" },
|
||||
{ href: "/#platforms", label: "Platforms" },
|
||||
];
|
||||
|
||||
export function SiteHeader() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="site-header">
|
||||
<div className="header-inner">
|
||||
<Brand />
|
||||
<nav className="desktop-nav" aria-label="Main navigation">
|
||||
{navigation.map((item) => (
|
||||
<Link key={item.href} href={item.href}>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<a
|
||||
className="button button-small button-dark header-github"
|
||||
href="https://github.com/vnidrop/vnidrop"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Icon name="github" />
|
||||
GitHub
|
||||
</a>
|
||||
<button
|
||||
className="menu-button"
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
aria-controls="mobile-navigation"
|
||||
aria-label={open ? "Close navigation" : "Open navigation"}
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
<Icon name={open ? "close" : "menu"} />
|
||||
</button>
|
||||
</div>
|
||||
<nav
|
||||
id="mobile-navigation"
|
||||
className={`mobile-nav${open ? " is-open" : ""}`}
|
||||
aria-label="Mobile navigation"
|
||||
>
|
||||
{navigation.map((item) => (
|
||||
<Link key={item.href} href={item.href} onClick={() => setOpen(false)}>
|
||||
{item.label}
|
||||
<Icon name="arrow" />
|
||||
</Link>
|
||||
))}
|
||||
<a
|
||||
href="https://github.com/vnidrop/vnidrop"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Explore on GitHub
|
||||
<Icon name="github" />
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
278
docs/components/transfer-flow.css
Normal file
278
docs/components/transfer-flow.css
Normal file
@@ -0,0 +1,278 @@
|
||||
/* Five-step transfer flow */
|
||||
.flow-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(520px, 1.1fr) minmax(390px, 0.9fr);
|
||||
align-items: start;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
.flow-visual {
|
||||
position: sticky;
|
||||
top: 115px;
|
||||
height: 660px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 27px;
|
||||
background:
|
||||
linear-gradient(rgba(112, 85, 124, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(112, 85, 124, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(145deg, #fdfbfe, #f4eff6);
|
||||
background-size: 28px 28px, 28px 28px, auto;
|
||||
box-shadow: inset 0 1px white, var(--shadow-sm);
|
||||
}
|
||||
|
||||
.flow-visual::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at center, transparent 20%, rgba(248, 244, 250, 0.65) 78%);
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.flow-visual-glow {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
left: 32%;
|
||||
width: 40%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
background: rgba(168, 85, 247, 0.18);
|
||||
filter: blur(55px);
|
||||
animation: flow-glow 7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.flow-track {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
inset: 4% 5% auto;
|
||||
width: 90%;
|
||||
height: 92%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.flow-track-shadow,
|
||||
.flow-track-base,
|
||||
.flow-track-energy {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.flow-track-shadow {
|
||||
stroke: rgba(168, 85, 247, 0.22);
|
||||
stroke-width: 34;
|
||||
filter: url(#flow-blur);
|
||||
}
|
||||
|
||||
.flow-track-base {
|
||||
stroke: #e4dae9;
|
||||
stroke-width: 18;
|
||||
}
|
||||
|
||||
.flow-track-energy {
|
||||
stroke: url(#flow-gradient);
|
||||
stroke-width: 6;
|
||||
stroke-dasharray: 3 17;
|
||||
animation: conduit-energy 2.1s linear infinite;
|
||||
}
|
||||
|
||||
.flow-moving-drop,
|
||||
.flow-moving-halo {
|
||||
offset-path: path("M132 141c216 14 240 88 153 159-89 73-43 157 81 160");
|
||||
offset-rotate: 0deg;
|
||||
animation: flow-drop 8s cubic-bezier(0.45, 0.08, 0.35, 0.98) infinite;
|
||||
}
|
||||
|
||||
.flow-moving-halo {
|
||||
animation-delay: -0.08s;
|
||||
}
|
||||
|
||||
.flow-gate {
|
||||
transform-origin: 241px 299px;
|
||||
animation: gate-breathe 3.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.flow-device {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
display: grid;
|
||||
width: 168px;
|
||||
height: 258px;
|
||||
place-items: center;
|
||||
border: 7px solid #d7cfdc;
|
||||
border-radius: 28px;
|
||||
background: #211d24;
|
||||
box-shadow: 0 24px 55px rgba(48, 31, 56, 0.18), inset 0 0 0 1px #ada2b3;
|
||||
}
|
||||
|
||||
.flow-device-sender {
|
||||
top: 46px;
|
||||
left: 31px;
|
||||
transform: rotate(-6deg);
|
||||
}
|
||||
|
||||
.flow-device-receiver {
|
||||
right: 25px;
|
||||
bottom: 32px;
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
|
||||
.flow-device-camera {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 50%;
|
||||
width: 54px;
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
background: #0c0a0e;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.flow-device-content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 25px 15px 15px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flow-mini-label {
|
||||
margin-bottom: 20px;
|
||||
color: #8c7e93;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 7px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.flow-file-stack {
|
||||
position: relative;
|
||||
width: 74px;
|
||||
height: 82px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.flow-file-stack span {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
width: 50px;
|
||||
height: 66px;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(216, 180, 254, 0.5);
|
||||
border-radius: 9px;
|
||||
background: #392e40;
|
||||
color: #d9a8fb;
|
||||
}
|
||||
|
||||
.flow-file-stack span:nth-child(1) { left: 0; transform: rotate(-10deg); }
|
||||
.flow-file-stack span:nth-child(2) { right: 0; transform: rotate(10deg); }
|
||||
.flow-file-stack span:nth-child(3) { left: 12px; background: #f7effc; color: var(--brand-600); }
|
||||
.flow-file-stack svg { width: 22px; }
|
||||
|
||||
.flow-device-content strong {
|
||||
font-family: var(--font-display);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.flow-device-content small {
|
||||
color: #928798;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.flow-complete-ring {
|
||||
display: grid;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin-bottom: 18px;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(216, 180, 254, 0.4);
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(168, 85, 247, 0.26), rgba(168, 85, 247, 0.05));
|
||||
color: #d59aff;
|
||||
animation: complete-ring 3.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.flow-complete-ring svg {
|
||||
width: 30px;
|
||||
stroke-width: 2.4;
|
||||
}
|
||||
|
||||
.flow-annotation {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #ded4e3;
|
||||
border-radius: 7px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
color: var(--brand-600);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 7px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.flow-annotation-one { top: 35%; right: 12%; transform: rotate(4deg); }
|
||||
.flow-annotation-two { bottom: 11%; left: 25%; transform: rotate(-4deg); }
|
||||
|
||||
.flow-steps {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 18px;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
transition: transform 200ms ease, border-color 200ms ease, box-shadow 200ms ease;
|
||||
}
|
||||
|
||||
.flow-step:hover {
|
||||
transform: translateX(6px);
|
||||
border-color: var(--brand-300);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.flow-step-icon {
|
||||
display: grid;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
background: var(--brand-100);
|
||||
color: var(--brand-600);
|
||||
}
|
||||
|
||||
.flow-step-icon svg { width: 21px; }
|
||||
|
||||
.flow-step-number {
|
||||
display: inline-block;
|
||||
margin-bottom: 2px;
|
||||
color: var(--brand-500);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.flow-step h3 {
|
||||
margin-bottom: 7px;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.flow-step p {
|
||||
margin-bottom: 0;
|
||||
color: var(--ink-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.58;
|
||||
}
|
||||
|
||||
108
docs/components/transfer-flow.tsx
Normal file
108
docs/components/transfer-flow.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Icon, type IconName } from "@/components/icons";
|
||||
|
||||
const steps: Array<{
|
||||
number: string;
|
||||
icon: IconName;
|
||||
title: string;
|
||||
text: string;
|
||||
}> = [
|
||||
{
|
||||
number: "01",
|
||||
icon: "folder",
|
||||
title: "Pick anything",
|
||||
text: "Choose one file, a whole batch, or a complete folder. Its structure stays intact.",
|
||||
},
|
||||
{
|
||||
number: "02",
|
||||
icon: "qr",
|
||||
title: "Pass a tiny invitation",
|
||||
text: "Share a QR code, tap an NFC tag, or send a portable .vnd invitation file.",
|
||||
},
|
||||
{
|
||||
number: "03",
|
||||
icon: "shield",
|
||||
title: "Let them knock",
|
||||
text: "Every new receiver asks first by default. You choose approve or refuse.",
|
||||
},
|
||||
{
|
||||
number: "04",
|
||||
icon: "verified",
|
||||
title: "Stream and verify",
|
||||
text: "Files move over an authenticated encrypted connection and are verified as they arrive.",
|
||||
},
|
||||
{
|
||||
number: "05",
|
||||
icon: "stop",
|
||||
title: "Close the door",
|
||||
text: "Follow each receiver, cancel in progress, or stop sharing to revoke the invitation.",
|
||||
},
|
||||
];
|
||||
|
||||
export function TransferFlow() {
|
||||
return (
|
||||
<div className="flow-layout">
|
||||
<div className="flow-visual" aria-hidden="true">
|
||||
<div className="flow-visual-glow" />
|
||||
<div className="flow-device flow-device-sender">
|
||||
<span className="flow-device-camera" />
|
||||
<div className="flow-device-content">
|
||||
<span className="flow-mini-label">SELECTED</span>
|
||||
<div className="flow-file-stack">
|
||||
<span><Icon name="file" /></span>
|
||||
<span><Icon name="file" /></span>
|
||||
<span><Icon name="file" /></span>
|
||||
</div>
|
||||
<strong>Project files</strong>
|
||||
<small>18 items · 248 MB</small>
|
||||
</div>
|
||||
</div>
|
||||
<svg className="flow-track" viewBox="0 0 480 600">
|
||||
<defs>
|
||||
<linearGradient id="flow-gradient" x1="90" y1="80" x2="401" y2="520" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#C778FF" />
|
||||
<stop offset="0.5" stopColor="#A855F7" />
|
||||
<stop offset="1" stopColor="#7628EA" />
|
||||
</linearGradient>
|
||||
<filter id="flow-blur" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="18" />
|
||||
</filter>
|
||||
</defs>
|
||||
<path className="flow-track-shadow" d="M132 141c216 14 240 88 153 159-89 73-43 157 81 160" />
|
||||
<path className="flow-track-base" d="M132 141c216 14 240 88 153 159-89 73-43 157 81 160" />
|
||||
<path className="flow-track-energy" d="M132 141c216 14 240 88 153 159-89 73-43 157 81 160" />
|
||||
<circle className="flow-moving-drop" cx="0" cy="0" r="11" fill="url(#flow-gradient)" />
|
||||
<circle className="flow-moving-halo" cx="0" cy="0" r="24" fill="#A855F7" opacity="0.18" filter="url(#flow-blur)" />
|
||||
<g className="flow-gate">
|
||||
<rect x="197" y="274" width="88" height="50" rx="17" fill="#FFFCFF" stroke="#D8B4FE" />
|
||||
<path d="m241 286 12 5v8.5c0 7.2-5.1 12.1-12 14.2-6.9-2.1-12-7-12-14.2V291Z" fill="url(#flow-gradient)" />
|
||||
<path d="m235.5 299 3.5 3.5 7.5-8" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
<div className="flow-device flow-device-receiver">
|
||||
<span className="flow-device-camera" />
|
||||
<div className="flow-device-content flow-received-content">
|
||||
<span className="flow-complete-ring"><Icon name="check" /></span>
|
||||
<strong>All received</strong>
|
||||
<small>18 items verified</small>
|
||||
</div>
|
||||
</div>
|
||||
<span className="flow-annotation flow-annotation-one">E2E</span>
|
||||
<span className="flow-annotation flow-annotation-two">VERIFIED</span>
|
||||
</div>
|
||||
<ol className="flow-steps">
|
||||
{steps.map((step) => (
|
||||
<li key={step.number} className="flow-step">
|
||||
<div className="flow-step-icon">
|
||||
<Icon name={step.icon} />
|
||||
</div>
|
||||
<div>
|
||||
<span className="flow-step-number">{step.number}</span>
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.text}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user