From d61d6f3c5ba035ea38639794942ac2157862172a Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Thu, 16 Jul 2026 16:38:30 +0200 Subject: [PATCH 1/5] feat(docs): add animated product website --- docs/.gitignore | 10 + docs/README.md | 22 + docs/app/base.css | 325 ++ docs/app/footer.css | 103 + docs/app/home.css | 701 +++ docs/app/icon.svg | 10 + docs/app/layout.tsx | 71 + docs/app/page.tsx | 292 ++ docs/app/privacy/page.tsx | 381 ++ docs/app/privacy/privacy-document.css | 660 +++ docs/app/privacy/privacy-responsive.css | 156 + docs/app/responsive.css | 322 ++ docs/components/approval-demo.tsx | 86 + docs/components/approval-scene.css | 267 + docs/components/brand.tsx | 65 + docs/components/hero-illustration.tsx | 216 + docs/components/hero-scene.css | 247 + docs/components/icons.tsx | 140 + docs/components/illustration-motion.css | 276 + docs/components/invitation-scene.css | 405 ++ docs/components/invitation-showcase.tsx | 153 + docs/components/network-illustration.tsx | 51 + docs/components/network-scene.css | 190 + docs/components/platform-orbit.tsx | 25 + docs/components/platform-scene.css | 103 + docs/components/reveal.tsx | 52 + docs/components/site-footer.tsx | 47 + docs/components/site-header.tsx | 71 + docs/components/transfer-flow.css | 278 + docs/components/transfer-flow.tsx | 108 + docs/eslint.config.mjs | 9 + docs/next-env.d.ts | 6 + docs/next.config.ts | 8 + docs/package-lock.json | 6082 ++++++++++++++++++++++ docs/package.json | 30 + docs/public/brand-mark.svg | 21 + docs/public/site.webmanifest | 16 + docs/tsconfig.json | 41 + 38 files changed, 12046 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/README.md create mode 100644 docs/app/base.css create mode 100644 docs/app/footer.css create mode 100644 docs/app/home.css create mode 100644 docs/app/icon.svg create mode 100644 docs/app/layout.tsx create mode 100644 docs/app/page.tsx create mode 100644 docs/app/privacy/page.tsx create mode 100644 docs/app/privacy/privacy-document.css create mode 100644 docs/app/privacy/privacy-responsive.css create mode 100644 docs/app/responsive.css create mode 100644 docs/components/approval-demo.tsx create mode 100644 docs/components/approval-scene.css create mode 100644 docs/components/brand.tsx create mode 100644 docs/components/hero-illustration.tsx create mode 100644 docs/components/hero-scene.css create mode 100644 docs/components/icons.tsx create mode 100644 docs/components/illustration-motion.css create mode 100644 docs/components/invitation-scene.css create mode 100644 docs/components/invitation-showcase.tsx create mode 100644 docs/components/network-illustration.tsx create mode 100644 docs/components/network-scene.css create mode 100644 docs/components/platform-orbit.tsx create mode 100644 docs/components/platform-scene.css create mode 100644 docs/components/reveal.tsx create mode 100644 docs/components/site-footer.tsx create mode 100644 docs/components/site-header.tsx create mode 100644 docs/components/transfer-flow.css create mode 100644 docs/components/transfer-flow.tsx create mode 100644 docs/eslint.config.mjs create mode 100644 docs/next-env.d.ts create mode 100644 docs/next.config.ts create mode 100644 docs/package-lock.json create mode 100644 docs/package.json create mode 100644 docs/public/brand-mark.svg create mode 100644 docs/public/site.webmanifest create mode 100644 docs/tsconfig.json diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..d422c86 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,10 @@ +.next/ +out/ +.vercel/ +*.tsbuildinfo +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.env* +!.env.example diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..170405e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ +# VniDrop website + +The product website for VniDrop, built with Next.js and exported as a static site. + +## Local development + +```bash +npm install +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000). + +## Checks + +```bash +npm run lint +npm run typecheck +npm run build +``` + +The production build is written to `out/` and can be hosted by any static web server. diff --git a/docs/app/base.css b/docs/app/base.css new file mode 100644 index 0000000..f47fd3f --- /dev/null +++ b/docs/app/base.css @@ -0,0 +1,325 @@ +:root { + --brand-700: #6f27e9; + --brand-600: #842fee; + --brand-500: #a855f7; + --brand-400: #c084fc; + --brand-300: #d8b4fe; + --brand-200: #ead4ff; + --brand-100: #f5eaff; + --ink: #171419; + --ink-soft: #4f4854; + --ink-muted: #786f7d; + --paper: #fbfafc; + --paper-warm: #f7f4f8; + --surface: #ffffff; + --surface-soft: #f3eff5; + --line: #e5dfe8; + --line-strong: #d4cad8; + --dark: #17131a; + --dark-raised: #211a25; + --dark-line: #362c3c; + --success: #24a66b; + --warning: #f59e0b; + --font-sans: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-display: "Avenir Next", Avenir, "Segoe UI", ui-sans-serif, sans-serif; + --font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + --shadow-sm: 0 8px 30px rgba(41, 25, 48, 0.07); + --shadow-lg: 0 30px 90px rgba(49, 25, 60, 0.14); + --shell: 1220px; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: 92px; +} + +body { + margin: 0; + overflow-x: hidden; + background: var(--paper); + color: var(--ink); + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.6; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +::selection { + background: var(--brand-300); + color: #28132f; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input, +textarea, +select { + font: inherit; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +button { + color: inherit; +} + +svg { + display: block; +} + +h1, +h2, +h3, +p { + margin-top: 0; +} + +h1, +h2, +h3 { + font-family: var(--font-display); + text-wrap: balance; +} + +p { + text-wrap: pretty; +} + +:focus-visible { + outline: 3px solid rgba(168, 85, 247, 0.55); + outline-offset: 3px; +} + +.page-shell { + width: min(calc(100% - 48px), var(--shell)); + margin-inline: auto; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.skip-link { + position: fixed; + z-index: 1000; + top: 12px; + left: 12px; + transform: translateY(-140%); + padding: 10px 16px; + border-radius: 10px; + background: var(--ink); + color: white; + font-weight: 700; + transition: transform 180ms ease; +} + +.skip-link:focus { + transform: translateY(0); +} + +.site-header { + position: fixed; + z-index: 100; + top: 16px; + right: 0; + left: 0; + width: min(calc(100% - 32px), 1260px); + margin-inline: auto; + border: 1px solid rgba(220, 212, 224, 0.82); + border-radius: 18px; + background: rgba(251, 250, 252, 0.82); + box-shadow: 0 12px 40px rgba(42, 26, 49, 0.08); + backdrop-filter: blur(20px) saturate(1.4); + -webkit-backdrop-filter: blur(20px) saturate(1.4); +} + +.header-inner { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + height: 64px; + padding: 0 10px 0 17px; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + width: max-content; + font-family: var(--font-display); + font-size: 19px; + font-weight: 750; + letter-spacing: -0.04em; +} + +.brand-mark-shell { + display: grid; + width: 38px; + height: 38px; + place-items: center; + border: 1px solid #eadcf1; + border-radius: 11px; + background: white; + box-shadow: 0 5px 15px rgba(105, 42, 145, 0.08); +} + +.brand-mark-shell svg { + width: 27px; + height: 27px; +} + +.brand-name { + transform: translateY(-1px); +} + +.desktop-nav { + display: flex; + align-items: center; + gap: 3px; + padding: 4px; + border-radius: 13px; + background: rgba(239, 235, 241, 0.7); +} + +.desktop-nav a { + padding: 7px 14px; + border-radius: 10px; + color: var(--ink-soft); + font-size: 13px; + font-weight: 650; + transition: background 160ms ease, color 160ms ease; +} + +.desktop-nav a:hover { + background: white; + color: var(--ink); +} + +.header-github { + justify-self: end; +} + +.menu-button { + display: none; + width: 42px; + height: 42px; + place-items: center; + border: 0; + border-radius: 11px; + background: var(--surface-soft); + cursor: pointer; +} + +.menu-button svg { + width: 21px; +} + +.mobile-nav { + display: none; +} + +.button { + display: inline-flex; + min-height: 52px; + align-items: center; + justify-content: center; + gap: 10px; + padding: 0 21px; + border: 1px solid transparent; + border-radius: 14px; + font-size: 14px; + font-weight: 730; + line-height: 1; + cursor: pointer; + transition: transform 180ms ease, box-shadow 180ms ease, background 180ms ease, + border-color 180ms ease; +} + +.button:hover { + transform: translateY(-2px); +} + +.button svg { + width: 18px; + height: 18px; +} + +.button-small { + min-height: 44px; + padding-inline: 16px; + border-radius: 11px; + font-size: 13px; +} + +.button-primary { + background: linear-gradient(135deg, var(--brand-500), var(--brand-700)); + box-shadow: 0 12px 28px rgba(132, 47, 238, 0.24), inset 0 1px rgba(255, 255, 255, 0.35); + color: white; +} + +.button-primary:hover { + box-shadow: 0 16px 34px rgba(132, 47, 238, 0.3), inset 0 1px rgba(255, 255, 255, 0.35); +} + +.button-ghost { + border-color: var(--line); + background: rgba(255, 255, 255, 0.72); + color: var(--ink); +} + +.button-ghost:hover { + border-color: var(--line-strong); + background: white; + box-shadow: var(--shadow-sm); +} + +.button-dark { + background: var(--ink); + color: white; +} + +.button-light { + background: white; + color: #211925; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18); +} + +.button-dark-ghost { + border-color: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.06); + color: white; +} + +.motion-ready .reveal { + opacity: 0; + transform: translateY(28px); + transition: opacity 700ms cubic-bezier(0.2, 0.75, 0.25, 1), + transform 700ms cubic-bezier(0.2, 0.75, 0.25, 1); + transition-delay: var(--reveal-delay, 0ms); +} + +.motion-ready .reveal.is-visible { + opacity: 1; + transform: translateY(0); +} + diff --git a/docs/app/footer.css b/docs/app/footer.css new file mode 100644 index 0000000..e52838a --- /dev/null +++ b/docs/app/footer.css @@ -0,0 +1,103 @@ +.site-footer { + padding-top: 75px; + background: var(--paper); +} + +.footer-main { + display: grid; + grid-template-columns: 1fr auto; + gap: 60px; + padding-bottom: 70px; +} + +.footer-brand { + display: flex; + align-items: flex-start; + gap: 15px; +} + +.footer-mark { + display: grid; + width: 48px; + height: 48px; + flex: 0 0 auto; + place-items: center; + border: 1px solid var(--line); + border-radius: 14px; + background: white; +} + +.footer-mark svg { + width: 32px; + height: 32px; +} + +.footer-brand p { + margin-bottom: 0; + color: var(--ink-muted); + font-size: 13px; +} + +.footer-brand .footer-name { + margin-bottom: 3px; + color: var(--ink); + font-family: var(--font-display); + font-size: 21px; + font-weight: 750; + letter-spacing: -0.04em; +} + +.footer-links { + display: grid; + grid-template-columns: repeat(2, minmax(140px, 1fr)); + gap: 70px; +} + +.footer-links > div { + display: flex; + flex-direction: column; + gap: 9px; +} + +.footer-links .footer-label { + margin-bottom: 7px; + color: var(--ink-muted); + font-family: var(--font-mono); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.footer-links a { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--ink-soft); + font-size: 13px; + font-weight: 600; +} + +.footer-links a:hover { + color: var(--brand-600); +} + +.footer-links svg { + width: 13px; +} + +.footer-bottom { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 22px; + padding-bottom: 28px; + border-top: 1px solid var(--line); + color: var(--ink-muted); + font-size: 11px; +} + +.footer-bottom p { + margin: 0; +} + diff --git a/docs/app/home.css b/docs/app/home.css new file mode 100644 index 0000000..55d1bea --- /dev/null +++ b/docs/app/home.css @@ -0,0 +1,701 @@ +.hero-section { + position: relative; + min-height: 850px; + overflow: hidden; + padding: 150px 0 86px; + background: + radial-gradient(circle at 82% 18%, rgba(211, 165, 255, 0.24), transparent 28%), + radial-gradient(circle at 12% 76%, rgba(184, 108, 255, 0.12), transparent 25%), + linear-gradient(180deg, #fdfcfe 0%, #faf7fb 100%); +} + +.hero-section::before { + position: absolute; + inset: 0; + background-image: radial-gradient(rgba(93, 71, 102, 0.1) 0.7px, transparent 0.7px); + background-size: 19px 19px; + content: ""; + mask-image: linear-gradient(to bottom, black, transparent 82%); + opacity: 0.38; +} + +.hero-ambient { + position: absolute; + border: 1px solid rgba(168, 85, 247, 0.18); + border-radius: 999px; + pointer-events: none; +} + +.hero-ambient-one { + top: 120px; + right: -80px; + width: 420px; + height: 420px; + animation: ambient-drift 15s ease-in-out infinite alternate; +} + +.hero-ambient-two { + bottom: -190px; + left: -120px; + width: 400px; + height: 400px; + animation: ambient-drift 18s ease-in-out -5s infinite alternate-reverse; +} + +.hero-layout { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: minmax(0, 0.88fr) minmax(540px, 1.12fr); + align-items: center; + gap: 30px; +} + +.hero-copy { + position: relative; + z-index: 3; + padding-top: 12px; +} + +.eyebrow-badge { + display: inline-flex; + align-items: center; + gap: 9px; + margin-bottom: 25px; + padding: 7px 11px; + border: 1px solid #dfd4e4; + border-radius: 999px; + background: rgba(255, 255, 255, 0.72); + color: #665c6b; + font-family: var(--font-mono); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; +} + +.eyebrow-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--warning); + box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.13); +} + +.hero-copy h1 { + margin-bottom: 28px; + font-size: clamp(3.5rem, 5.1vw, 5.4rem); + font-weight: 760; + letter-spacing: -0.072em; + line-height: 0.98; +} + +.hero-copy h1 span { + background: linear-gradient(100deg, var(--brand-600), #ba62f8 58%, #7c2aef); + background-clip: text; + -webkit-background-clip: text; + color: transparent; +} + +.hero-lead { + max-width: 610px; + margin-bottom: 30px; + color: var(--ink-soft); + font-size: 18px; + line-height: 1.7; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 11px; +} + +.hero-platforms { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 14px; + margin-top: 35px; + color: var(--ink-muted); + font-size: 12px; +} + +.hero-platforms > span { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.hero-platforms ul { + display: flex; + flex-wrap: wrap; + gap: 7px; + margin: 0; + padding: 0; + list-style: none; +} + +.hero-platforms li { + padding: 4px 8px; + border: 1px solid var(--line); + border-radius: 7px; + background: rgba(255, 255, 255, 0.65); +} + +.hero-art-reveal { + min-width: 0; +} + +.trust-section { + position: relative; + z-index: 3; + margin-top: -32px; +} + +.trust-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + overflow: hidden; + border: 1px solid var(--line); + border-radius: 23px; + background: rgba(255, 255, 255, 0.94); + box-shadow: var(--shadow-lg); + backdrop-filter: blur(12px); +} + +.trust-item { + display: flex; + min-height: 124px; + align-items: center; + gap: 19px; + padding: 24px 28px; +} + +.trust-item + .trust-item { + border-left: 1px solid var(--line); +} + +.trust-number { + color: var(--brand-500); + font-family: var(--font-mono); + font-size: 11px; + font-weight: 800; +} + +.trust-item strong { + display: block; + margin-bottom: 3px; + font-family: var(--font-display); + font-size: 18px; + letter-spacing: -0.025em; +} + +.trust-item p { + margin: 0; + color: var(--ink-muted); + font-size: 13px; + line-height: 1.45; +} + +.word-stream { + overflow: hidden; + padding: 92px 0 28px; + color: #ebe5ee; + font-family: var(--font-display); + font-size: clamp(3.5rem, 7vw, 7.6rem); + font-weight: 800; + letter-spacing: -0.055em; + line-height: 1; + white-space: nowrap; +} + +.word-stream > div { + display: flex; + width: max-content; + align-items: center; + gap: 35px; + animation: word-stream 36s linear infinite; +} + +.word-stream i { + color: var(--brand-300); + font-size: 0.18em; + font-style: normal; +} + +.section { + position: relative; + padding: 132px 0; +} + +.section-heading { + margin-bottom: 64px; +} + +.section-heading h2, +.privacy-intro h2, +.network-copy h2, +.platforms-copy h2, +.final-cta h2, +.approval-copy h2 { + margin-bottom: 20px; + font-size: clamp(2.7rem, 4.4vw, 4.8rem); + font-weight: 740; + letter-spacing: -0.06em; + line-height: 1.02; +} + +.section-heading p, +.section-lead { + color: var(--ink-soft); + font-size: 17px; + line-height: 1.72; +} + +.section-heading-centered { + max-width: 810px; + margin-right: auto; + margin-left: auto; + text-align: center; +} + +.section-heading-centered p { + max-width: 620px; + margin-right: auto; + margin-bottom: 0; + margin-left: auto; +} + +.section-heading-split { + display: grid; + grid-template-columns: 1.1fr 0.72fr; + align-items: end; + gap: 90px; +} + +.section-heading-split h2 { + margin-bottom: 0; +} + +.section-heading-split p { + margin-bottom: 6px; +} + +.kicker { + display: inline-block; + margin-bottom: 18px; + color: var(--brand-600); + font-family: var(--font-mono); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.13em; +} + +.kicker-light { + color: #d89bff; +} + +.flow-section { + padding-top: 105px; +} + +.invitation-section { + overflow: hidden; + background: var(--paper-warm); +} + +.approval-section { + padding: 150px 0; +} + +.approval-layout { + display: grid; + grid-template-columns: minmax(0, 0.86fr) minmax(480px, 1.14fr); + align-items: center; + gap: 100px; +} + +.approval-copy h2 { + max-width: 620px; +} + +.approval-copy .section-lead { + max-width: 570px; + margin-bottom: 27px; +} + +.check-list { + display: grid; + gap: 13px; + margin: 0 0 25px; + padding: 0; + list-style: none; +} + +.check-list li { + display: flex; + align-items: center; + gap: 11px; + color: #3c3540; + font-size: 14px; + font-weight: 600; +} + +.check-list svg { + width: 20px; + height: 20px; + padding: 4px; + border-radius: 50%; + background: var(--brand-100); + color: var(--brand-600); + stroke-width: 2.5; +} + +.approval-note { + max-width: 555px; + margin: 0; + padding: 15px 17px; + border: 1px solid #eadff0; + border-radius: 12px; + background: #faf7fb; + color: var(--ink-muted); + font-size: 12px; + line-height: 1.55; +} + +.approval-demo-wrap { + position: relative; +} + +.demo-hint { + position: absolute; + right: 22px; + bottom: -45px; + display: inline-flex; + align-items: center; + gap: 7px; + color: var(--brand-600); + font-family: var(--font-mono); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.07em; + transform: rotate(-2deg); +} + +.demo-hint svg { + width: 15px; +} + +.privacy-principles-section { + position: relative; + overflow: hidden; + padding: 145px 0 135px; + background: + radial-gradient(circle at 90% 8%, rgba(168, 85, 247, 0.18), transparent 25%), + radial-gradient(circle at 5% 90%, rgba(124, 42, 239, 0.12), transparent 25%), + var(--dark); + color: white; +} + +.privacy-principles-section::after { + position: absolute; + inset: 0; + background-image: radial-gradient(rgba(255, 255, 255, 0.12) 0.7px, transparent 0.7px); + background-size: 22px 22px; + content: ""; + opacity: 0.13; + pointer-events: none; +} + +.privacy-principles-section .page-shell { + position: relative; + z-index: 1; +} + +.privacy-intro { + max-width: 890px; + margin-bottom: 65px; +} + +.privacy-intro h2 { + max-width: 840px; +} + +.privacy-intro > p { + max-width: 700px; + margin-bottom: 0; + color: #bdb4c2; + font-size: 18px; + line-height: 1.72; +} + +.privacy-point-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; +} + +.privacy-point { + min-height: 275px; + padding: 24px; + border: 1px solid var(--dark-line); + border-radius: 20px; + background: rgba(255, 255, 255, 0.035); + transition: border-color 200ms ease, transform 200ms ease, background 200ms ease; +} + +.privacy-point:hover { + transform: translateY(-5px); + border-color: rgba(192, 132, 252, 0.45); + background: rgba(255, 255, 255, 0.055); +} + +.privacy-point-top { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 43px; +} + +.privacy-point-top > span { + display: grid; + width: 44px; + height: 44px; + place-items: center; + border: 1px solid rgba(192, 132, 252, 0.28); + border-radius: 12px; + background: rgba(168, 85, 247, 0.1); + color: var(--brand-400); +} + +.privacy-point-top svg { + width: 22px; +} + +.privacy-point-top small { + color: #706775; + font-family: var(--font-mono); + font-size: 10px; +} + +.privacy-point h3 { + margin-bottom: 10px; + font-size: 19px; + letter-spacing: -0.025em; +} + +.privacy-point p { + margin-bottom: 0; + color: #a99fac; + font-size: 13px; + line-height: 1.65; +} + +.privacy-policy-link { + display: flex; + align-items: center; + justify-content: space-between; + gap: 30px; + margin-top: 38px; + padding-top: 27px; + border-top: 1px solid var(--dark-line); +} + +.privacy-policy-link p { + margin: 0; + color: #a99fac; + font-size: 13px; +} + +.privacy-policy-link a { + display: inline-flex; + align-items: center; + gap: 10px; + color: #dca7ff; + font-size: 14px; + font-weight: 750; +} + +.privacy-policy-link svg { + width: 18px; + transition: transform 180ms ease; +} + +.privacy-policy-link a:hover svg { + transform: translateX(4px); +} + +.network-section { + padding: 150px 0; + background: #0f0c11; + color: white; +} + +.network-layout { + display: grid; + grid-template-columns: minmax(0, 0.78fr) minmax(580px, 1.22fr); + align-items: center; + gap: 75px; +} + +.network-copy h2 { + font-size: clamp(2.6rem, 4vw, 4.2rem); +} + +.network-copy > p { + margin-bottom: 35px; + color: #aaa0ae; + font-size: 16px; + line-height: 1.75; +} + +.network-copy blockquote { + position: relative; + margin: 0; + padding: 20px 0 20px 25px; + border-left: 2px solid var(--brand-500); + color: #f0e9f3; + font-family: var(--font-display); + font-size: 20px; + font-weight: 630; + line-height: 1.45; +} + +.network-copy blockquote span { + position: absolute; + top: -14px; + left: 12px; + color: var(--brand-500); + font-family: Georgia, serif; + font-size: 44px; + opacity: 0.55; +} + +.platforms-section { + overflow: hidden; + padding: 155px 0; +} + +.platforms-layout { + display: grid; + grid-template-columns: minmax(0, 0.86fr) minmax(520px, 1.14fr); + align-items: center; + gap: 75px; +} + +.platforms-copy .section-lead { + max-width: 570px; + margin-bottom: 29px; +} + +.platform-feature-list { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.platform-feature-list span { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 11px; + border: 1px solid var(--line); + border-radius: 9px; + background: white; + color: var(--ink-soft); + font-size: 12px; + font-weight: 620; +} + +.platform-feature-list svg { + width: 16px; + color: var(--brand-600); +} + +.orbit-wrap { + min-width: 0; +} + +.final-cta-section { + padding: 0 0 40px; +} + +.final-cta { + position: relative; + overflow: hidden; + padding: 100px 50px; + border-radius: 31px; + background: + linear-gradient(125deg, rgba(255, 255, 255, 0.06), transparent 40%), + linear-gradient(135deg, #25172f, #171019 55%, #28143a); + color: white; + text-align: center; +} + +.final-cta::after { + position: absolute; + inset: 0; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: inherit; + content: ""; + pointer-events: none; +} + +.final-cta h2 { + position: relative; + z-index: 1; + max-width: 850px; + margin-right: auto; + margin-left: auto; + font-size: clamp(2.8rem, 5vw, 5.4rem); +} + +.final-cta > p { + position: relative; + z-index: 1; + max-width: 600px; + margin: 0 auto 31px; + color: #bfb4c4; + font-size: 16px; +} + +.final-cta > .kicker { + position: relative; + z-index: 1; +} + +.final-cta-actions { + position: relative; + z-index: 1; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; +} + +.final-cta-orb { + position: absolute; + border-radius: 50%; + background: var(--brand-500); + filter: blur(90px); + opacity: 0.18; +} + +.final-cta-orb-one { + top: -140px; + left: -70px; + width: 380px; + height: 380px; + animation: cta-orb 12s ease-in-out infinite alternate; +} + +.final-cta-orb-two { + right: -80px; + bottom: -200px; + width: 430px; + height: 430px; + animation: cta-orb 15s ease-in-out -4s infinite alternate-reverse; +} + diff --git a/docs/app/icon.svg b/docs/app/icon.svg new file mode 100644 index 0000000..eaf36b9 --- /dev/null +++ b/docs/app/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx new file mode 100644 index 0000000..1ce3158 --- /dev/null +++ b/docs/app/layout.tsx @@ -0,0 +1,71 @@ +import type { Metadata, Viewport } from "next"; +import type { ReactNode } from "react"; +import { SiteFooter } from "@/components/site-footer"; +import { SiteHeader } from "@/components/site-header"; +import "./base.css"; +import "./home.css"; +import "./footer.css"; +import "./privacy/privacy-document.css"; +import "../components/hero-scene.css"; +import "../components/transfer-flow.css"; +import "../components/invitation-scene.css"; +import "../components/approval-scene.css"; +import "../components/network-scene.css"; +import "../components/platform-scene.css"; +import "./responsive.css"; +import "./privacy/privacy-responsive.css"; +import "../components/illustration-motion.css"; + +export const metadata: Metadata = { + title: { + default: "VniDrop — Direct file transfer, on your terms", + template: "%s · VniDrop", + }, + description: + "Send files and folders directly across Android, iOS, macOS, Windows, and Linux—with approval by default and no hosted transfer copy.", + applicationName: "VniDrop", + manifest: "/site.webmanifest", + keywords: [ + "peer-to-peer file transfer", + "encrypted file sharing", + "cross-platform file transfer", + "open source", + ], + openGraph: { + type: "website", + siteName: "VniDrop", + title: "VniDrop — Direct file transfer, on your terms", + description: + "Move files from your device to theirs, with no account and no hosted transfer copy.", + }, + twitter: { + card: "summary", + title: "VniDrop — Direct file transfer, on your terms", + description: + "Move files from your device to theirs, with no account and no hosted transfer copy.", + }, +}; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#fbfafc" }, + { media: "(prefers-color-scheme: dark)", color: "#17131a" }, + ], +}; + +export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) { + return ( + + + + Skip to content + + + {children} + + + + ); +} diff --git a/docs/app/page.tsx b/docs/app/page.tsx new file mode 100644 index 0000000..98d936b --- /dev/null +++ b/docs/app/page.tsx @@ -0,0 +1,292 @@ +import Link from "next/link"; +import { ApprovalDemo } from "@/components/approval-demo"; +import { HeroIllustration } from "@/components/hero-illustration"; +import { Icon, type IconName } from "@/components/icons"; +import { InvitationShowcase } from "@/components/invitation-showcase"; +import { NetworkIllustration } from "@/components/network-illustration"; +import { PlatformOrbit } from "@/components/platform-orbit"; +import { Reveal } from "@/components/reveal"; +import { TransferFlow } from "@/components/transfer-flow"; + +const trustItems = [ + { value: "No account", detail: "Open the app and start sharing" }, + { value: "No hosted copy", detail: "Your transfer is not parked in our cloud" }, + { value: "No silent overwrite", detail: "Existing files stay untouched" }, +]; + +const privacyPoints: Array<{ + icon: IconName; + number: string; + title: string; + text: string; +}> = [ + { + icon: "lock", + number: "01", + title: "Encrypted in transit", + text: "Each connection is authenticated and end-to-end encrypted, including through a relay.", + }, + { + icon: "shield", + number: "02", + title: "Approval by default", + text: "An invitation helps devices meet. It is not automatic permission to download.", + }, + { + icon: "verified", + number: "03", + title: "Verified on arrival", + text: "Content addressing checks that incoming bytes match exactly what was offered.", + }, + { + icon: "stop", + number: "04", + title: "Revocable access", + text: "Stop a share at any time and the invitation can no longer be used.", + }, +]; + +export default function HomePage() { + return ( +
+
+
+ +
+
+ {trustItems.map((item, index) => ( + + 0{index + 1} +
+ {item.value} +

{item.detail}

+
+
+ ))} +
+
+ + + +
+
+ + ONE DROP, FIVE MOMENTS +

A handoff that stays in your hands.

+

+ VniDrop keeps the transfer understandable from first selection to final verified file. +

+
+ + + +
+
+ +
+
+ +
+ MAKE THE INTRODUCTION +

One transfer. Three ways to meet.

+
+

+ The invitation is small enough to scan, tap, or send. Your files stay where they are + until a receiver connects and you allow the transfer. +

+
+ + + +
+
+ +
+
+ + CONTROL IS BUILT IN +

A link is not permission.

+

+ Every new receiver knocks by default. Approve people you recognize, refuse the rest, + and follow progress for each device. +

+
    +
  • See the receiver before anything moves
  • +
  • Cancel a transfer already in progress
  • +
  • Stop sharing to revoke future access
  • +
+

+ Need an open handoff? An optional “Anyone with this transfer” mode is available for + non-sensitive files. +

+
+ + + Try the controls + +
+
+ +
+
+ + PRIVACY BY DESIGN +

The file never becomes our file.

+

+ VniDrop coordinates a secure handoff. It does not turn your transfer into a hosted + download sitting on someone else’s storage. +

+
+
+ {privacyPoints.map((point, index) => ( + +
+ + {point.number} +
+

{point.title}

+

{point.text}

+
+ ))} +
+ +

Diagnostics are optional and exclude invitations, file paths, and transfer contents.

+ + Read the full privacy policy + + +
+
+
+ +
+
+ + DIRECT WHEN POSSIBLE +

The shortest safe path, chosen automatically.

+

+ VniDrop tries to connect devices directly—even across home routers and mobile + networks. If that path is not available, a relay forwards the same encrypted + connection without becoming a file store. +

+
+ + The relay can move the envelope. It cannot open it. +
+
+ + + +
+
+ +
+
+ + ONE FAMILIAR HANDOFF +

At home on every screen.

+

+ Android, iOS, macOS, Windows, and Linux share one clear transfer experience while each + platform keeps control of its own file picker and save destination. +

+
+ Cross-platform by design + Native file destinations + Open-source core +
+
+ + + +
+
+ +
+
+ +
+
+ ); +} diff --git a/docs/app/privacy/page.tsx b/docs/app/privacy/page.tsx new file mode 100644 index 0000000..298a4d3 --- /dev/null +++ b/docs/app/privacy/page.tsx @@ -0,0 +1,381 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { BrandMark } from "@/components/brand"; +import { Icon, type IconName } from "@/components/icons"; +import { Reveal } from "@/components/reveal"; + +export const metadata: Metadata = { + title: "Privacy policy", + description: + "How VniDrop handles transfers, local app data, optional diagnostics, bug reports, and website visits.", +}; + +const summaries: Array<{ icon: IconName; label: string; value: string }> = [ + { icon: "devices", label: "Transfers", value: "Device to device" }, + { icon: "globe", label: "Accounts", value: "None" }, + { icon: "shield", label: "Diagnostics", value: "Optional" }, + { icon: "folder", label: "Hosted file copy", value: "None" }, +]; + +const sections = [ + ["scope", "Scope"], + ["transfers", "Transfers"], + ["local-data", "Local data"], + ["diagnostics", "Diagnostics"], + ["website", "Website"], + ["permissions", "Permissions"], + ["providers", "Service providers"], + ["retention", "Retention"], + ["choices", "Your choices"], + ["security", "Security"], + ["changes", "Changes"], + ["contact", "Contact"], +]; + +export default function PrivacyPage() { + return ( +
+
+
+ +
+
+ {summaries.map((summary, index) => ( + + + {summary.label} + {summary.value} + + ))} +
+
+ +
+
+ + +
+
+ +
+ The short version +

+ VniDrop has no user accounts and does not upload your transfer to a VniDrop file + store. Files travel over an authenticated, end-to-end encrypted connection. + Product diagnostics are opt-in; a bug report is sent only when you submit one. +

+
+
+ +
+ 01 +

Scope and who “VniDrop” means

+

+ This policy covers the official VniDrop website, the VniDrop applications for + Android, iOS, macOS, Windows, and Linux, and the diagnostics service configured by + the official project. In this policy, “VniDrop,” “we,” and “us” refer to the + maintainers of the official VniDrop project and the official builds they distribute. +

+

+ VniDrop is open-source software. A build distributed or operated by someone else + may use different networking infrastructure, diagnostics settings, or website + hosting. That distributor is responsible for explaining its own practices. +

+
+ +
+ 02 +

What happens during a transfer

+

File contents

+

+ The sender chooses files or folders on their device. VniDrop streams those bytes to + an approved receiver and does not first upload them to a VniDrop-hosted storage + bucket. The receiver saves the files to a destination they choose. Relayed traffic + remains end-to-end encrypted. +

+

Invitations and transfer metadata

+

+ A QR code, NFC tag, or .vnd file contains a transfer invitation. The + invitation includes connection and content identifiers plus transfer metadata such + as the transfer name, optional sender name, creation time, file count, and total + size. It is a capability: anyone who receives it may be able to request the transfer + while the share is active. Treat it like a private access link. +

+

What peers and relays can see

+

+ A receive request can disclose the receiver’s chosen display or device name, + application version, and a technical endpoint identifier to the sender. A direct + connection exposes the peers’ IP addresses to one another. When a public relay is + used, its operator can observe connection metadata such as source and destination IP + addresses, connection time, and the amount of relayed data, but cannot read the + encrypted transfer contents. +

+
+ +

+ Approval is required by default. If the sender selects “Anyone with this transfer,” + anyone holding the invitation may receive the files until sharing stops. +

+
+
+ +
+ 03 +

Information kept on your device

+

VniDrop stores the information needed to operate the app locally, including:

+
    +
  • device identity and networking keys used to establish secure connections;
  • +
  • active shares, transfer history, receiver requests, progress, and status;
  • +
  • app preferences, including access and diagnostics choices;
  • +
  • download destinations and locally managed transfer data; and
  • +
  • an anonymous installation identifier used only for diagnostics correlation.
  • +
+

+ This information remains until you remove the relevant history, stop or delete a + share, clear the app’s data, or uninstall the app, subject to operating-system file + behavior. Removing VniDrop history does not delete a file you already downloaded; + delete that file through your operating system if you no longer want it. +

+
+ +
+ 04 +

Optional diagnostics and bug reports

+

Automatic product diagnostics

+

+ When an official build includes diagnostics, automatic usage events and crash + reports are disabled until you enable “Share diagnostics.” If enabled, VniDrop may + send an anonymous installation ID, app version, platform, sparse event names and + properties, crash type and message, a redacted stack trace, timestamps, and recent + in-app breadcrumbs. You can turn this off at any time; doing so also removes pending + local crash reports. +

+

User-submitted bug reports

+

+ A bug report is separate from the diagnostics toggle and is sent only when you press + submit. It can contain what you say happened, what you expected, reproduction steps, + an optional contact email, app and platform versions, an anonymous installation ID, + device name and model, operating system, network and battery information, recent + breadcrumbs, and optional recent logs. You can exclude logs before submitting. +

+

Data deliberately excluded

+

+ Automatic diagnostics are designed to exclude transfer contents, invitations, and + file paths. Before diagnostic text or optional logs are sent, VniDrop applies rules + intended to redact invitation tokens, endpoint identifiers, absolute paths, file and + content URIs, and platform document identifiers. No redaction system is perfect, so + review anything you type into a bug report and avoid including secrets. +

+
+ +
+ 05 +

The VniDrop website

+

+ This website is a static product site. It does not provide an account, contact form, + advertising, behavioral analytics, marketing pixels, or non-essential cookies. It + does not ask the browser for access to your files, camera, contacts, location, or + nearby devices. +

+

+ The hosting and security infrastructure may process routine request information—such + as IP address, time, requested page, referrer, and browser user agent—to deliver the + site, maintain reliability, and prevent abuse. The live hosting provider must be + identified in this policy before public deployment if it differs from the providers + described below. +

+
+ +
+ 06 +

Device permissions

+
+
Files & folders

Choose what to send and where received files are saved.

+
Camera / scanner

Scan a QR invitation when you choose that receive method.

+
NFC

Read or write an invitation through a compatible NFC tag.

+
Network & notifications

Connect peers and alert you to background receiver requests.

+
+

+ VniDrop requests a platform permission only for the related feature. On Android, QR + scanning may be provided through Google Play services Code Scanner. Platform-level + permission prompts and service-provider terms also apply. +

+
+ +
+ 07 +

Infrastructure and external services

+
+
+ I +
Iroh / public relay operators

Device discovery, connection establishment, and encrypted relay fallback. Relays process connection metadata but cannot decrypt transfer contents.

+
+
+ C +
Cloudflare

The project’s diagnostics design uses Cloudflare Workers, D1, and R2. Cloudflare also processes source IPs for request delivery and abuse controls.

+
+
+ G +
Google Play services

May provide the QR code scanner on supported Android devices when you choose to scan an invitation.

+
+
+ GH +
GitHub

Hosts the source repository, issue tracker, and external pages linked from this site. GitHub’s own privacy terms apply after you follow those links.

+
+
+

+ Provider policies: {" "} + Iroh, {" "} + Cloudflare, {" "} + Google, and {" "} + GitHub. +

+
+ +
+ 08 +

Retention and deletion

+
+
+ DataTypical retention +
+
+ Transfer history and settingsUntil you delete them, clear app data, or uninstall +
+
+ Pending local crash reportsUp to 30 days and 20 reports; deleted when diagnostics is disabled +
+
+ Server diagnostics and bug reportsThe current project configuration is 90 days, with scheduled deletion +
+
+ Downloaded filesUntil you delete them through your operating system +
+
+

+ Operational backups, provider logs, and deletion backlogs may persist briefly beyond + the stated period where necessary for security, integrity, or legal obligations. If + the production diagnostics retention configuration changes, this policy should be + updated to match it. +

+
+ +
+ 09 +

Your choices and rights

+
    +
  • Enable or disable “Share diagnostics” in VniDrop settings.
  • +
  • Submit a bug report only when you choose, omit contact information, and exclude logs.
  • +
  • Approve or refuse each receiver, cancel a transfer, or stop sharing.
  • +
  • Delete individual transfer history or clear completed, failed, and cancelled receive history.
  • +
  • Delete downloaded files using your operating system, or clear all app data by uninstalling or resetting the app.
  • +
+

+ Depending on where you live, privacy law may provide rights to access, correct, + delete, restrict, or object to processing of personal information. Because VniDrop + has no account and automatic diagnostics use an anonymous installation ID, we may + not be able to connect a server record to you without additional information. Use + the contact method below and provide only what is needed to locate your submission. +

+
+ +
+ 10 +

Security

+

+ VniDrop uses authenticated end-to-end encrypted connections, content verification, + deny-by-default share access, bounded diagnostics payloads, redaction, and safe file + publishing that avoids silently replacing an existing file. No system can guarantee + absolute security. Keep invitations private, verify receiver names, keep your device + updated, and stop sharing when a transfer is finished. +

+

+ Please report a suspected vulnerability through the private process in the {" "} + VniDrop security policy, not in a public issue. +

+
+ +
+ 11 +

Changes to this policy

+

+ VniDrop is in early development. Features and data practices may change. When this + policy changes, we will update the effective date and version at the top of the page + and publish the revised text with the project. Material changes should be called out + in release notes or the application where practical. +

+
+ +
+ 12 +

Contact

+

+ VniDrop is currently maintained as an open-source project and does not yet publish a + dedicated privacy email or postal address. For a privacy question or request, open a + request in the project issue tracker. Do not put an invitation, file content, + credentials, or other sensitive information in a public issue. +

+ + Contact the maintainers + + +
+
+
+
+
+ ); +} diff --git a/docs/app/privacy/privacy-document.css b/docs/app/privacy/privacy-document.css new file mode 100644 index 0000000..c722f99 --- /dev/null +++ b/docs/app/privacy/privacy-document.css @@ -0,0 +1,660 @@ +.privacy-page { + background: #fcfbfd; +} + +.privacy-hero { + position: relative; + overflow: hidden; + padding: 165px 0 105px; + background: + radial-gradient(circle at 80% 20%, rgba(192, 132, 252, 0.2), transparent 30%), + linear-gradient(180deg, #fdfcfe, #f7f3f9); +} + +.privacy-hero-grid { + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(114, 84, 128, 0.055) 1px, transparent 1px), + linear-gradient(90deg, rgba(114, 84, 128, 0.055) 1px, transparent 1px); + background-size: 32px 32px; + mask-image: linear-gradient(to bottom, black, transparent 90%); +} + +.privacy-hero-layout { + position: relative; + display: grid; + grid-template-columns: minmax(0, 0.92fr) minmax(430px, 0.72fr); + align-items: center; + gap: 95px; +} + +.privacy-back { + display: flex; + width: max-content; + align-items: center; + gap: 8px; + margin-bottom: 42px; + color: var(--ink-muted); + font-size: 12px; + font-weight: 650; +} + +.privacy-hero-copy > .kicker { + display: block; + width: max-content; +} + +.privacy-back svg { + width: 16px; + transform: rotate(180deg); + transition: transform 180ms ease; +} + +.privacy-back:hover { + color: var(--brand-600); +} + +.privacy-back:hover svg { + transform: rotate(180deg) translateX(4px); +} + +.privacy-hero-copy h1 { + margin: 0 0 25px; + font-size: clamp(3.7rem, 6.2vw, 6.5rem); + font-weight: 760; + letter-spacing: -0.075em; + line-height: 0.98; +} + +.privacy-hero-copy > p { + max-width: 680px; + margin-bottom: 28px; + color: var(--ink-soft); + font-size: 18px; + line-height: 1.72; +} + +.privacy-meta { + display: flex; + align-items: center; + gap: 12px; + color: var(--ink-muted); + font-family: var(--font-mono); + font-size: 9px; + font-weight: 700; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.privacy-meta i { + width: 3px; + height: 3px; + border-radius: 50%; + background: var(--brand-500); +} + +.privacy-hero-art { + position: relative; + min-height: 435px; +} + +.privacy-art-orbit { + position: absolute; + top: 50%; + left: 50%; + border: 1px solid rgba(168, 85, 247, 0.18); + border-radius: 50%; + transform: translate(-50%, -50%); +} + +.privacy-art-orbit::before, +.privacy-art-orbit::after { + position: absolute; + border-radius: 50%; + background: var(--brand-400); + content: ""; +} + +.privacy-art-orbit-one { + width: 420px; + height: 420px; + animation: privacy-orbit 28s linear infinite; +} + +.privacy-art-orbit-one::before { + top: 11%; + left: 16%; + width: 7px; + height: 7px; + box-shadow: 0 0 0 7px rgba(168, 85, 247, 0.09); +} + +.privacy-art-orbit-one::after { + right: 8%; + bottom: 25%; + width: 5px; + height: 5px; +} + +.privacy-art-orbit-two { + width: 295px; + height: 295px; + border-style: dashed; + animation: privacy-orbit 21s linear infinite reverse; +} + +.privacy-art-orbit-two::before { + top: 48%; + left: -4px; + width: 7px; + height: 7px; +} + +.privacy-art-orbit-two::after { + top: 4%; + right: 22%; + width: 5px; + height: 5px; +} + +.privacy-mark-card { + position: absolute; + z-index: 2; + top: 50%; + left: 50%; + display: grid; + width: 180px; + height: 180px; + place-items: center; + border: 1px solid #dfd2e6; + border-radius: 45px; + background: rgba(255, 255, 255, 0.91); + box-shadow: 0 35px 90px rgba(65, 30, 80, 0.17); + transform: translate(-50%, -50%) rotate(-3deg); + backdrop-filter: blur(14px); +} + +.privacy-mark-card > svg { + position: relative; + z-index: 2; + width: 112px; + height: 112px; +} + +.privacy-mark-glow { + position: absolute; + inset: -35px; + border-radius: 50%; + background: rgba(168, 85, 247, 0.2); + filter: blur(42px); + animation: privacy-glow 5s ease-in-out infinite; +} + +.privacy-mark-lock { + position: absolute; + right: -12px; + bottom: -10px; + display: grid; + width: 49px; + height: 49px; + place-items: center; + border: 4px solid #f8f4fa; + border-radius: 15px; + background: var(--ink); + box-shadow: 0 12px 30px rgba(42, 25, 50, 0.2); + color: white; + transform: rotate(3deg); +} + +.privacy-mark-lock svg { + width: 21px; +} + +.privacy-data-pill { + position: absolute; + z-index: 4; + display: inline-flex; + align-items: center; + gap: 7px; + padding: 8px 11px; + border: 1px solid #dfd3e4; + border-radius: 10px; + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 12px 35px rgba(58, 33, 69, 0.11); + color: var(--brand-600); + font-family: var(--font-mono); + font-size: 8px; + font-weight: 800; + letter-spacing: 0.07em; + backdrop-filter: blur(10px); + animation: privacy-pill 5s ease-in-out infinite; +} + +.privacy-data-pill svg { + width: 15px; +} + +.privacy-data-pill-one { + top: 11%; + left: 4%; + transform: rotate(-5deg); +} + +.privacy-data-pill-two { + top: 29%; + right: -2%; + transform: rotate(4deg); + animation-delay: -2s; +} + +.privacy-data-pill-three { + bottom: 6%; + left: 7%; + transform: rotate(3deg); + animation-delay: -3.5s; +} + +.privacy-summary { + position: relative; + z-index: 4; + margin-top: -37px; +} + +.privacy-summary-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + overflow: hidden; + border: 1px solid var(--line); + border-radius: 22px; + background: rgba(255, 255, 255, 0.95); + box-shadow: var(--shadow-lg); + backdrop-filter: blur(12px); +} + +.privacy-summary-card { + display: grid; + min-height: 151px; + align-content: center; + padding: 25px 27px; +} + +.privacy-summary-card + .privacy-summary-card { + border-left: 1px solid var(--line); +} + +.privacy-summary-card > span { + display: grid; + width: 37px; + height: 37px; + margin-bottom: 17px; + place-items: center; + border-radius: 10px; + background: var(--brand-100); + color: var(--brand-600); +} + +.privacy-summary-card svg { + width: 19px; +} + +.privacy-summary-card small { + margin-bottom: 1px; + color: var(--ink-muted); + font-family: var(--font-mono); + font-size: 8px; + font-weight: 700; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.privacy-summary-card strong { + font-family: var(--font-display); + font-size: 16px; + letter-spacing: -0.02em; +} + +.privacy-document-section { + padding: 135px 0 155px; +} + +.privacy-document-layout { + display: grid; + grid-template-columns: 210px minmax(0, 780px); + justify-content: center; + gap: 105px; +} + +.privacy-toc { + position: sticky; + top: 112px; + align-self: start; +} + +.privacy-toc > p { + margin-bottom: 17px; + color: var(--ink); + font-family: var(--font-mono); + font-size: 9px; + font-weight: 800; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.privacy-toc nav { + display: flex; + flex-direction: column; +} + +.privacy-toc a { + display: grid; + grid-template-columns: 26px 1fr; + align-items: center; + gap: 6px; + padding: 7px 0; + color: #79707e; + font-size: 11px; + font-weight: 600; + transition: color 160ms ease, transform 160ms ease; +} + +.privacy-toc a:hover { + color: var(--brand-600); + transform: translateX(3px); +} + +.privacy-toc a span { + color: #b5aabb; + font-family: var(--font-mono); + font-size: 8px; +} + +.privacy-document { + min-width: 0; +} + +.privacy-callout { + display: grid; + grid-template-columns: auto 1fr; + gap: 17px; + margin-bottom: 75px; + padding: 25px; + border: 1px solid #dfd0e7; + border-radius: 17px; + background: linear-gradient(145deg, #fbf6ff, #f7f1fa); +} + +.privacy-callout > svg { + width: 24px; + color: var(--brand-600); +} + +.privacy-callout strong { + display: block; + margin-bottom: 5px; + font-family: var(--font-display); + font-size: 14px; +} + +.privacy-callout p { + margin: 0; + color: var(--ink-soft); + font-size: 12px; + line-height: 1.7; +} + +.policy-section { + position: relative; + scroll-margin-top: 110px; + padding: 0 0 72px; +} + +.policy-section + .policy-section { + padding-top: 3px; + border-top: 1px solid var(--line); +} + +.policy-number { + display: inline-flex; + margin-top: 0; + margin-bottom: 15px; + color: var(--brand-600); + font-family: var(--font-mono); + font-size: 9px; + font-weight: 800; + letter-spacing: 0.08em; +} + +.policy-section + .policy-section .policy-number { + margin-top: 65px; +} + +.policy-section h2 { + margin-bottom: 27px; + font-size: clamp(2rem, 3.3vw, 3.15rem); + font-weight: 720; + letter-spacing: -0.052em; + line-height: 1.08; +} + +.policy-section h3 { + margin: 34px 0 10px; + font-size: 16px; + font-weight: 720; + letter-spacing: -0.018em; +} + +.policy-section p, +.policy-section li { + color: #514956; + font-size: 14px; + line-height: 1.82; +} + +.policy-section p { + margin-bottom: 19px; +} + +.policy-section ul { + display: grid; + gap: 8px; + margin: 17px 0 22px; + padding-left: 22px; +} + +.policy-section li::marker { + color: var(--brand-500); +} + +.policy-section code { + padding: 2px 6px; + border: 1px solid #e7dceb; + border-radius: 5px; + background: #f8f3fa; + color: var(--brand-700); + font-family: var(--font-mono); + font-size: 0.86em; +} + +.policy-section a:not(.button) { + color: var(--brand-700); + font-weight: 650; + text-decoration: underline; + text-decoration-color: rgba(132, 47, 238, 0.28); + text-underline-offset: 3px; +} + +.policy-note { + display: grid; + grid-template-columns: auto 1fr; + gap: 13px; + margin-top: 28px; + padding: 18px; + border: 1px solid #e2d6e8; + border-radius: 13px; + background: #faf7fb; +} + +.policy-note svg { + width: 21px; + color: var(--brand-600); +} + +.policy-note p { + margin: 0; + color: #5e5264; + font-size: 12px; + line-height: 1.65; +} + +.permission-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin: 30px 0; +} + +.permission-grid > div { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + gap: 5px 12px; + padding: 17px; + border: 1px solid var(--line); + border-radius: 13px; + background: white; +} + +.permission-grid svg { + width: 21px; + grid-row: 1 / 3; + color: var(--brand-600); +} + +.permission-grid strong { + font-size: 11px; +} + +.permission-grid p { + margin: 0; + color: var(--ink-muted); + font-size: 9px; + line-height: 1.5; +} + +.provider-list { + display: grid; + gap: 9px; + margin: 28px 0; +} + +.provider-list > div { + display: grid; + grid-template-columns: auto 1fr; + gap: 15px; + padding: 17px; + border: 1px solid var(--line); + border-radius: 13px; + background: white; +} + +.provider-list > div > span { + display: grid; + width: 38px; + height: 38px; + place-items: center; + border-radius: 10px; + background: var(--brand-100); + color: var(--brand-700); + font-family: var(--font-mono); + font-size: 9px; + font-weight: 850; +} + +.provider-list strong { + display: block; + margin-bottom: 3px; + font-size: 11px; +} + +.provider-list p { + margin: 0; + color: var(--ink-muted); + font-size: 10px; + line-height: 1.55; +} + +.provider-links { + font-size: 11px !important; +} + +.retention-table { + overflow: hidden; + margin: 28px 0; + border: 1px solid var(--line); + border-radius: 14px; + background: white; +} + +.retention-row { + display: grid; + grid-template-columns: minmax(190px, 0.72fr) 1.28fr; +} + +.retention-row + .retention-row { + border-top: 1px solid var(--line); +} + +.retention-row span { + padding: 14px 16px; + color: var(--ink-soft); + font-size: 10px; + line-height: 1.55; +} + +.retention-row span + span { + border-left: 1px solid var(--line); + color: var(--ink-muted); +} + +.retention-head { + background: #f4eff6; +} + +.retention-head span { + color: var(--ink) !important; + font-family: var(--font-mono); + font-size: 8px; + font-weight: 800; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.policy-contact { + overflow: hidden; + padding: 54px !important; + border: 1px solid #dfd0e7 !important; + border-radius: 21px; + background: + radial-gradient(circle at 100% 0%, rgba(168, 85, 247, 0.14), transparent 35%), + linear-gradient(145deg, #fbf7fe, #f6f0f9); +} + +.policy-contact .policy-number { + margin-top: 0 !important; +} + +.policy-contact h2 { + margin-bottom: 16px; +} + +.policy-contact p { + max-width: 620px; + margin-bottom: 26px; +} + +.policy-contact .button { + color: white; + text-decoration: none; +} + diff --git a/docs/app/privacy/privacy-responsive.css b/docs/app/privacy/privacy-responsive.css new file mode 100644 index 0000000..151a56c --- /dev/null +++ b/docs/app/privacy/privacy-responsive.css @@ -0,0 +1,156 @@ +@keyframes privacy-orbit { + from { transform: translate(-50%, -50%) rotate(0); } + to { transform: translate(-50%, -50%) rotate(360deg); } +} + +@keyframes privacy-glow { + 0%, 100% { opacity: 0.5; transform: scale(0.84); } + 50% { opacity: 1; transform: scale(1.14); } +} + +@keyframes privacy-pill { + 0%, 100% { margin-top: 0; } + 50% { margin-top: -8px; } +} + +@media (max-width: 1050px) { + .privacy-hero-layout { + grid-template-columns: minmax(0, 1fr) 390px; + gap: 45px; + } + + .privacy-hero-copy h1 { + font-size: clamp(3.6rem, 6.8vw, 5.5rem); + } + + .privacy-document-layout { + grid-template-columns: 180px minmax(0, 720px); + gap: 60px; + } +} + +@media (max-width: 820px) { + .privacy-hero { + padding-top: 135px; + } + + .privacy-hero-layout { + grid-template-columns: 1fr; + } + + .privacy-hero-copy { + max-width: 690px; + } + + .privacy-hero-art { + width: min(100%, 520px); + margin: 0 auto; + } + + .privacy-summary-grid { + grid-template-columns: 1fr 1fr; + } + + .privacy-summary-card:nth-child(3) { + border-top: 1px solid var(--line); + border-left: 0; + } + + .privacy-summary-card:nth-child(4) { + border-top: 1px solid var(--line); + } + + .privacy-document-layout { + grid-template-columns: 1fr; + } + + .privacy-toc { + position: relative; + top: auto; + padding-bottom: 25px; + border-bottom: 1px solid var(--line); + } + + .privacy-toc nav { + display: grid; + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 560px) { + .privacy-hero { + padding: 120px 0 90px; + } + + .privacy-back { + margin-bottom: 32px; + } + + .privacy-hero-copy h1 { + font-size: clamp(3rem, 15vw, 4.5rem); + } + + .privacy-hero-copy > p { + font-size: 15px; + } + + .privacy-hero-art { + min-height: 360px; + transform: scale(0.86); + } + + .privacy-summary-grid { + grid-template-columns: 1fr; + } + + .privacy-summary-card { + min-height: 120px; + } + + .privacy-summary-card + .privacy-summary-card { + border-top: 1px solid var(--line); + border-left: 0; + } + + .privacy-document-section { + padding: 95px 0 105px; + } + + .privacy-toc nav { + grid-template-columns: 1fr 1fr; + } + + .privacy-callout { + padding: 20px; + } + + .policy-section h2 { + font-size: 2.25rem; + } + + .policy-section p, + .policy-section li { + font-size: 13px; + } + + .permission-grid { + grid-template-columns: 1fr; + } + + .retention-row { + grid-template-columns: 1fr; + } + + .retention-row span + span { + padding-top: 0; + border-left: 0; + } + + .retention-head { + display: none; + } + + .policy-contact { + padding: 35px 24px !important; + } +} diff --git a/docs/app/responsive.css b/docs/app/responsive.css new file mode 100644 index 0000000..95b1ca1 --- /dev/null +++ b/docs/app/responsive.css @@ -0,0 +1,322 @@ +@keyframes ambient-drift { + from { transform: translate3d(-10px, -8px, 0) rotate(0deg); } + to { transform: translate3d(24px, 18px, 0) rotate(8deg); } +} + +@keyframes word-stream { + to { transform: translateX(-50%); } +} + +@keyframes cta-orb { + from { transform: translate3d(-20px, -10px, 0) scale(0.9); } + to { transform: translate3d(40px, 25px, 0) scale(1.12); } +} + +@media (max-width: 1120px) { + .hero-layout { + grid-template-columns: minmax(0, 0.88fr) minmax(470px, 1.12fr); + } + + .hero-copy h1 { + font-size: clamp(3.2rem, 5.4vw, 4.5rem); + } + + .approval-layout, + .network-layout, + .platforms-layout { + gap: 55px; + } + + .privacy-point-grid { + grid-template-columns: repeat(2, 1fr); + } + + .privacy-point { + min-height: 230px; + } +} + +@media (max-width: 920px) { + .desktop-nav, + .header-github { + display: none; + } + + .header-inner { + grid-template-columns: 1fr auto; + } + + .menu-button { + display: grid; + } + + .mobile-nav { + display: grid; + max-height: 0; + overflow: hidden; + padding: 0 12px; + opacity: 0; + visibility: hidden; + transition: max-height 250ms ease, padding 250ms ease, opacity 180ms ease, visibility 250ms; + } + + .mobile-nav.is-open { + max-height: 280px; + padding: 4px 12px 12px; + opacity: 1; + visibility: visible; + } + + .mobile-nav a { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 10px; + border-top: 1px solid var(--line); + color: var(--ink-soft); + font-size: 13px; + font-weight: 650; + } + + .mobile-nav svg { + width: 17px; + } + + .hero-section { + padding-top: 128px; + } + + .hero-layout { + grid-template-columns: 1fr; + gap: 35px; + } + + .hero-copy { + max-width: 720px; + margin: 0 auto; + text-align: center; + } + + .hero-lead { + margin-right: auto; + margin-left: auto; + } + + .hero-actions, + .hero-platforms { + justify-content: center; + } + + .trust-grid { + grid-template-columns: 1fr; + } + + .trust-item { + min-height: 102px; + } + + .trust-item + .trust-item { + border-top: 1px solid var(--line); + border-left: 0; + } + + .section-heading-split { + grid-template-columns: 1fr; + gap: 20px; + } + + .section-heading-split p { + max-width: 640px; + } + + .approval-layout, + .network-layout, + .platforms-layout { + grid-template-columns: 1fr; + } + + .approval-copy, + .network-copy, + .platforms-copy { + max-width: 690px; + } + + .network-layout { + gap: 75px; + } + + .platforms-layout { + gap: 65px; + } +} + +@media (max-width: 680px) { + .page-shell { + width: min(calc(100% - 32px), var(--shell)); + } + + .site-header { + top: 10px; + width: calc(100% - 20px); + border-radius: 15px; + } + + .header-inner { + height: 58px; + padding-left: 11px; + } + + .brand-name { + font-size: 17px; + } + + .brand-mark-shell { + width: 36px; + height: 36px; + } + + .hero-section { + min-height: auto; + padding: 115px 0 72px; + } + + .hero-copy h1 { + font-size: clamp(2.75rem, 14vw, 4.25rem); + letter-spacing: -0.065em; + } + + .hero-lead { + font-size: 16px; + } + + .hero-actions { + display: grid; + } + + .hero-actions .button { + width: 100%; + } + + .hero-platforms { + display: grid; + justify-items: center; + } + + .hero-platforms ul { + justify-content: center; + } + + .trust-section { + margin-top: -18px; + } + + .trust-grid { + border-radius: 18px; + } + + .trust-item { + padding: 20px; + } + + .word-stream { + padding-top: 70px; + font-size: 3.6rem; + } + + .section { + padding: 95px 0; + } + + .section-heading { + margin-bottom: 42px; + } + + .section-heading h2, + .privacy-intro h2, + .network-copy h2, + .platforms-copy h2, + .final-cta h2, + .approval-copy h2 { + font-size: clamp(2.35rem, 12vw, 3.5rem); + } + + .section-heading p, + .section-lead { + font-size: 15px; + } + + .approval-section, + .platforms-section, + .network-section, + .privacy-principles-section { + padding: 100px 0; + } + + .approval-layout { + gap: 55px; + } + + .privacy-point-grid { + grid-template-columns: 1fr; + } + + .privacy-point { + min-height: 0; + } + + .privacy-point-top { + margin-bottom: 30px; + } + + .privacy-policy-link { + align-items: flex-start; + flex-direction: column; + } + + .final-cta-section { + padding-bottom: 24px; + } + + .final-cta { + padding: 76px 22px; + border-radius: 23px; + } + + .final-cta-actions { + display: grid; + } + + .footer-main { + grid-template-columns: 1fr; + } + + .footer-links { + gap: 35px; + } + + .footer-bottom { + align-items: flex-start; + flex-direction: column; + gap: 5px; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + + .motion-ready .reveal { + opacity: 1; + transform: none; + } +} diff --git a/docs/components/approval-demo.tsx b/docs/components/approval-demo.tsx new file mode 100644 index 0000000..74ffb18 --- /dev/null +++ b/docs/components/approval-demo.tsx @@ -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("pending"); + + return ( +
+
+ + + + Receiver requests +
+
+
+
+ + +
+
+ + {state === "pending" ? "NEW REQUEST" : state === "approved" ? "IN PROGRESS" : "REQUEST CLOSED"} + +

+ {state === "pending" && "Maya’s phone wants to receive"} + {state === "approved" && "Sending securely to Maya"} + {state === "refused" && "Request refused"} +

+
+
+ +
+ + + + + Summer photos + 42 files · 1.8 GB + + {state === "approved" && 68%} +
+ + {state === "pending" && ( +
+ + +
+ )} + + {state === "approved" && ( +
+
+ +
+
+ Direct connection + +
+
+ )} + + {state === "refused" && ( +
+ +

This device was not given access.

+ +
+ )} +
+
+ + Approval is required by default +
+
+ ); +} diff --git a/docs/components/approval-scene.css b/docs/components/approval-scene.css new file mode 100644 index 0000000..2edd2e0 --- /dev/null +++ b/docs/components/approval-scene.css @@ -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); } + diff --git a/docs/components/brand.tsx b/docs/components/brand.tsx new file mode 100644 index 0000000..ffcb2ad --- /dev/null +++ b/docs/components/brand.tsx @@ -0,0 +1,65 @@ +import Link from "next/link"; + +type BrandMarkProps = { + className?: string; + title?: string; +}; + +export function BrandMark({ className, title }: BrandMarkProps) { + return ( + + + + + + + + + + + + + + + + + ); +} + +export function Brand({ compact = false }: { compact?: boolean }) { + return ( + + + + + {!compact && VniDrop} + + ); +} diff --git a/docs/components/hero-illustration.tsx b/docs/components/hero-illustration.tsx new file mode 100644 index 0000000..37fbfd1 --- /dev/null +++ b/docs/components/hero-illustration.tsx @@ -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(null); + + const updateParallax = (event: PointerEvent) => { + 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 ( +
+
+ ); +} diff --git a/docs/components/hero-scene.css b/docs/components/hero-scene.css new file mode 100644 index 0000000..d46f06a --- /dev/null +++ b/docs/components/hero-scene.css @@ -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; +} + diff --git a/docs/components/icons.tsx b/docs/components/icons.tsx new file mode 100644 index 0000000..905d4c1 --- /dev/null +++ b/docs/components/icons.tsx @@ -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 & { + name: IconName; +}; + +export function Icon({ name, ...props }: IconProps) { + const paths: Record = { + arrow: ( + <> + + + + ), + check: , + chevron: , + close: ( + <> + + + + ), + code: ( + <> + + + + + ), + devices: ( + <> + + + + + ), + file: ( + <> + + + + ), + folder: ( + + ), + github: ( + + ), + globe: ( + <> + + + + ), + lock: ( + <> + + + + ), + menu: ( + <> + + + ), + nfc: ( + <> + + + + + ), + qr: ( + <> + + + + + + ), + shield: ( + <> + + + + ), + spark: ( + + ), + stop: , + verified: ( + <> + + + + ), + x: ( + <> + + + + ), + }; + + return ( + + ); +} diff --git a/docs/components/illustration-motion.css b/docs/components/illustration-motion.css new file mode 100644 index 0000000..0f80c17 --- /dev/null +++ b/docs/components/illustration-motion.css @@ -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%; } +} diff --git a/docs/components/invitation-scene.css b/docs/components/invitation-scene.css new file mode 100644 index 0000000..e12aa8b --- /dev/null +++ b/docs/components/invitation-scene.css @@ -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; } + diff --git a/docs/components/invitation-showcase.tsx b/docs/components/invitation-showcase.tsx new file mode 100644 index 0000000..312b902 --- /dev/null +++ b/docs/components/invitation-showcase.tsx @@ -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 ( +
+
+ {Array.from({ length: 81 }, (_, index) => ( + + ))} +
+ + + + + +
+ ); +} + +function NfcArt() { + return ( +
+
+ +
+
+ + + + +
+
+ + VniDrop +
+
+ ); +} + +function FileArt() { + return ( +
+ + +
+ + + Weekend trip + VniDrop invitation + .vnd +
+
+ +
+
+ ); +} + +export function InvitationShowcase() { + const [active, setActive] = useState("qr"); + const current = methods.find((method) => method.id === active) ?? methods[0]; + + return ( +
+
+ {methods.map((method) => ( + + ))} +
+
+
+ {current.eyebrow} +

{current.title}

+

{current.text}

+
+ + + The invitation is a private access link. + Only share it with people you intend to receive the transfer. + +
+
+ +
+
+ ); +} diff --git a/docs/components/network-illustration.tsx b/docs/components/network-illustration.tsx new file mode 100644 index 0000000..5251913 --- /dev/null +++ b/docs/components/network-illustration.tsx @@ -0,0 +1,51 @@ +import { Icon } from "@/components/icons"; + +export function NetworkIllustration() { + return ( +