mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
Merge pull request #17 from vnidrop/feat/docs
feat(docs): add VniDrop product website
This commit is contained in:
47
.github/workflows/docs.yml
vendored
Normal file
47
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Docs website
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/**"
|
||||
- ".github/workflows/docs.yml"
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "docs/**"
|
||||
- ".github/workflows/docs.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docs-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: docs/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Type-check
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,3 +22,4 @@ target/
|
||||
|
||||
# Local design export scratch
|
||||
output/
|
||||
.screenshots
|
||||
|
||||
10
docs/.gitignore
vendored
Normal file
10
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.next/
|
||||
out/
|
||||
.vercel/
|
||||
*.tsbuildinfo
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
.env*
|
||||
!.env.example
|
||||
26
docs/README.md
Normal file
26
docs/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 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.
|
||||
|
||||
Set `NEXT_PUBLIC_SITE_URL` to the canonical production origin when building for deployment so
|
||||
Open Graph and Twitter image URLs resolve to the public site. Vercel deployment URLs are detected
|
||||
automatically.
|
||||
179
docs/app/base.css
Normal file
179
docs/app/base.css
Normal file
@@ -0,0 +1,179 @@
|
||||
:root {
|
||||
--brand-700: #6f27e9;
|
||||
--brand-600: #842fee;
|
||||
--brand-500: #a855f7;
|
||||
--brand-400: #c084fc;
|
||||
--brand-300: #d8b4fe;
|
||||
--ink: #171419;
|
||||
--ink-soft: #4f4854;
|
||||
--ink-muted: #786f7d;
|
||||
--paper: #fbfafc;
|
||||
--paper-warm: #f7f4f8;
|
||||
--surface: #ffffff;
|
||||
--line: #e5dfe8;
|
||||
--line-strong: #d4cad8;
|
||||
--dark-line: #362c3c;
|
||||
--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;
|
||||
--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);
|
||||
}
|
||||
|
||||
.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-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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
66
docs/app/footer.css
Normal file
66
docs/app/footer.css
Normal file
@@ -0,0 +1,66 @@
|
||||
.site-footer {
|
||||
padding-top: 42px;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 40px;
|
||||
padding-bottom: 38px;
|
||||
}
|
||||
|
||||
.footer-identity {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--ink);
|
||||
font-family: var(--font-display);
|
||||
font-size: 18px;
|
||||
font-weight: 750;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.footer-identity img {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--ink-soft);
|
||||
font-size: 12px;
|
||||
font-weight: 620;
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
color: var(--brand-600);
|
||||
}
|
||||
|
||||
.footer-links svg {
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 26px;
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--ink-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.footer-bottom p {
|
||||
margin: 0;
|
||||
}
|
||||
358
docs/app/home.css
Normal file
358
docs/app/home.css
Normal file
@@ -0,0 +1,358 @@
|
||||
.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: 590px;
|
||||
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 {
|
||||
display: grid;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
place-items: center;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
box-shadow: 0 7px 18px rgba(55, 35, 63, 0.055);
|
||||
}
|
||||
|
||||
.hero-platforms .platform-glyph {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.platform-glyph[data-platform="android"] { color: #2fbf71; }
|
||||
.platform-glyph[data-platform="ios"] { color: #201c23; }
|
||||
.platform-glyph[data-platform="macos"] { color: #1687f8; }
|
||||
.platform-glyph[data-platform="windows"] { color: #008de5; }
|
||||
|
||||
.hero-art-reveal {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-app-preview {
|
||||
position: relative;
|
||||
width: min(112%, 850px);
|
||||
margin: 0 0 0 -3%;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.hero-app-preview::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 14% 8% 8% 10%;
|
||||
border-radius: 48%;
|
||||
background: rgba(168, 85, 247, 0.2);
|
||||
filter: blur(70px);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.hero-app-preview-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 28px 42px rgba(36, 24, 42, 0.2));
|
||||
}
|
||||
|
||||
.simple-section {
|
||||
padding: 124px 0;
|
||||
}
|
||||
|
||||
.simple-heading {
|
||||
max-width: 760px;
|
||||
margin-bottom: 58px;
|
||||
}
|
||||
|
||||
.kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 17px;
|
||||
color: var(--brand-600);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.13em;
|
||||
}
|
||||
|
||||
.kicker-light {
|
||||
color: #d89bff;
|
||||
}
|
||||
|
||||
.simple-heading h2,
|
||||
.trust-summary-copy h2 {
|
||||
margin-bottom: 18px;
|
||||
font-size: clamp(2.6rem, 4.2vw, 4.6rem);
|
||||
font-weight: 740;
|
||||
letter-spacing: -0.06em;
|
||||
line-height: 1.02;
|
||||
}
|
||||
|
||||
.simple-heading p {
|
||||
max-width: 650px;
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 17px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.simple-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
border-top: 1px solid var(--line);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.simple-step {
|
||||
min-height: 235px;
|
||||
padding: 30px 30px 36px;
|
||||
}
|
||||
|
||||
.simple-step + .simple-step {
|
||||
border-left: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.simple-step-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 46px;
|
||||
}
|
||||
|
||||
.simple-step-top span {
|
||||
color: var(--brand-600);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.simple-step-top svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--brand-500);
|
||||
}
|
||||
|
||||
.simple-step h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 22px;
|
||||
letter-spacing: -0.035em;
|
||||
}
|
||||
|
||||
.simple-step p {
|
||||
max-width: 310px;
|
||||
margin: 0;
|
||||
color: var(--ink-muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.trust-summary-section {
|
||||
padding: 112px 0;
|
||||
background: #171319;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.trust-summary {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr);
|
||||
align-items: start;
|
||||
gap: 100px;
|
||||
}
|
||||
|
||||
.trust-summary-copy {
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.trust-summary-copy p {
|
||||
margin-bottom: 28px;
|
||||
color: #b7adb9;
|
||||
font-size: 16px;
|
||||
line-height: 1.72;
|
||||
}
|
||||
|
||||
.simple-text-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
color: #ddaaff;
|
||||
font-size: 14px;
|
||||
font-weight: 720;
|
||||
}
|
||||
|
||||
.simple-text-link svg {
|
||||
width: 17px;
|
||||
}
|
||||
|
||||
.simple-text-link:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.trust-summary-list {
|
||||
border-top: 1px solid var(--dark-line);
|
||||
}
|
||||
|
||||
.trust-summary-item {
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr;
|
||||
gap: 18px;
|
||||
padding: 24px 0;
|
||||
border-bottom: 1px solid var(--dark-line);
|
||||
}
|
||||
|
||||
.trust-summary-item > span {
|
||||
display: grid;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(192, 132, 252, 0.24);
|
||||
border-radius: 12px;
|
||||
color: var(--brand-400);
|
||||
}
|
||||
|
||||
.trust-summary-item svg {
|
||||
width: 21px;
|
||||
}
|
||||
|
||||
.trust-summary-item h3 {
|
||||
margin-bottom: 6px;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.trust-summary-item p {
|
||||
max-width: 530px;
|
||||
margin: 0;
|
||||
color: #a99fac;
|
||||
font-size: 13px;
|
||||
line-height: 1.62;
|
||||
}
|
||||
29
docs/app/icon.svg
Normal file
29
docs/app/icon.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="40" y="40" width="944" height="944" rx="210" fill="#FFFFFF" stroke="#E9E7F0" stroke-width="8"/>
|
||||
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="url(#paint0_linear_11_12)"/>
|
||||
<path d="M520.4 431.72C495.8 464.52 443.32 530.12 420.36 577.68C390.84 636.72 403.96 699.04 446.6 731.84C487.6 758.08 549.92 758.08 592.56 730.2C633.56 700.68 646.68 636.72 620.44 577.68C597.48 530.12 546.64 464.52 520.4 431.72Z" fill="url(#paint1_linear_11_12)"/>
|
||||
<mask id="mask0_11_12" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="176" y="148" width="671" height="732">
|
||||
<path d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_11_12)">
|
||||
<path d="M688 148H782C832 148 842 171.8 847.48 210V303.68L688 148Z" fill="url(#paint2_linear_11_12)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_11_12" x1="176" y1="148" x2="904.706" y2="816.252" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#A855F7"/>
|
||||
<stop offset="0.48" stop-color="#9D4DF4"/>
|
||||
<stop offset="1" stop-color="#7C2AEF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_11_12" x1="404.439" y1="431.72" x2="707.314" y2="649.286" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#A855F7"/>
|
||||
<stop offset="0.48" stop-color="#9D4DF4"/>
|
||||
<stop offset="1" stop-color="#7C2AEF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_11_12" x1="683.48" y1="144.6" x2="793.636" y2="306.833" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2DDFF"/>
|
||||
<stop offset="1" stop-color="#C084FC"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
90
docs/app/layout.tsx
Normal file
90
docs/app/layout.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
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 "../components/header.css";
|
||||
import "./home.css";
|
||||
import "./footer.css";
|
||||
import "./privacy/privacy-document.css";
|
||||
import "./responsive.css";
|
||||
import "./privacy/privacy-responsive.css";
|
||||
|
||||
const configuredSiteUrl =
|
||||
process.env.NEXT_PUBLIC_SITE_URL ??
|
||||
process.env.VERCEL_PROJECT_PRODUCTION_URL ??
|
||||
process.env.VERCEL_URL ??
|
||||
"http://localhost:3000";
|
||||
|
||||
const metadataBase = new URL(
|
||||
configuredSiteUrl.startsWith("http") ? configuredSiteUrl : `https://${configuredSiteUrl}`,
|
||||
);
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase,
|
||||
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.",
|
||||
images: [
|
||||
{
|
||||
url: "/og.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "VniDrop — Your files, a straight line between devices.",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "VniDrop — Direct file transfer, on your terms",
|
||||
description:
|
||||
"Move files from your device to theirs, with no account and no hosted transfer copy.",
|
||||
images: [
|
||||
{
|
||||
url: "/og.png",
|
||||
alt: "VniDrop — Your files, a straight line between devices.",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
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 (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<a className="skip-link" href="#main-content">
|
||||
Skip to content
|
||||
</a>
|
||||
<SiteHeader />
|
||||
{children}
|
||||
<SiteFooter />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
424
docs/app/og.png/route.tsx
Normal file
424
docs/app/og.png/route.tsx
Normal file
@@ -0,0 +1,424 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
const imageSize = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
};
|
||||
|
||||
function BrandMark() {
|
||||
return (
|
||||
<svg width="54" height="54" viewBox="0 0 1024 1024" fill="none">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z"
|
||||
fill="url(#brand-gradient)"
|
||||
/>
|
||||
<path
|
||||
d="M520.4 431.72C495.8 464.52 443.32 530.12 420.36 577.68C390.84 636.72 403.96 699.04 446.6 731.84C487.6 758.08 549.92 758.08 592.56 730.2C633.56 700.68 646.68 636.72 620.44 577.68C597.48 530.12 546.64 464.52 520.4 431.72Z"
|
||||
fill="url(#drop-gradient)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="brand-gradient" x1="176" y1="148" x2="905" y2="816">
|
||||
<stop stopColor="#C084FC" />
|
||||
<stop offset="0.52" stopColor="#A855F7" />
|
||||
<stop offset="1" stopColor="#7C2AEF" />
|
||||
</linearGradient>
|
||||
<linearGradient id="drop-gradient" x1="404" y1="432" x2="707" y2="649">
|
||||
<stop stopColor="#E9D5FF" />
|
||||
<stop offset="1" stopColor="#A855F7" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function FileGlyph() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M6 3h8l4 4v14H6z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.7"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M14 3v5h4M9 13h6M9 17h4" stroke="currentColor" strokeWidth="1.7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckGlyph() {
|
||||
return (
|
||||
<svg width="27" height="27" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="m6.5 12.5 3.4 3.4 7.8-8"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LockGlyph() {
|
||||
return (
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="5" y="10" width="14" height="11" rx="3" stroke="currentColor" strokeWidth="1.8" />
|
||||
<path d="M8 10V7a4 4 0 0 1 8 0v3M12 14v3" stroke="currentColor" strokeWidth="1.8" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function TransferIllustration() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 86,
|
||||
right: 72,
|
||||
width: 470,
|
||||
height: 474,
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 24,
|
||||
left: 48,
|
||||
width: 400,
|
||||
height: 400,
|
||||
borderRadius: 999,
|
||||
display: "flex",
|
||||
background:
|
||||
"radial-gradient(circle, rgba(168, 85, 247, 0.24) 0%, rgba(124, 42, 239, 0.09) 44%, rgba(13, 10, 16, 0) 72%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "#A99EAD",
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 2.1,
|
||||
}}
|
||||
>
|
||||
DIRECT · ENCRYPTED
|
||||
</div>
|
||||
|
||||
<svg
|
||||
width="470"
|
||||
height="474"
|
||||
viewBox="0 0 470 474"
|
||||
fill="none"
|
||||
style={{ position: "absolute", inset: 0 }}
|
||||
>
|
||||
<path
|
||||
d="M264 229C305 233 310 300 353 313"
|
||||
stroke="url(#route-gradient)"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="8 9"
|
||||
/>
|
||||
<circle cx="264" cy="229" r="6" fill="#D8B4FE" />
|
||||
<circle cx="353" cy="313" r="6" fill="#A855F7" />
|
||||
<defs>
|
||||
<linearGradient id="route-gradient" x1="264" y1="229" x2="353" y2="313">
|
||||
<stop stopColor="#D8B4FE" />
|
||||
<stop offset="1" stopColor="#7C2AEF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 79,
|
||||
left: 1,
|
||||
width: 286,
|
||||
height: 184,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
border: "2px solid #44384A",
|
||||
borderRadius: 18,
|
||||
background: "#18131D",
|
||||
boxShadow: "0 24px 70px rgba(0, 0, 0, 0.38)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 34,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "0 14px",
|
||||
borderBottom: "1px solid #382E3D",
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 7, height: 7, borderRadius: 99, background: "#685B6E", display: "flex" }} />
|
||||
<div style={{ width: 7, height: 7, borderRadius: 99, background: "#685B6E", display: "flex", marginLeft: 7 }} />
|
||||
<div style={{ width: 7, height: 7, borderRadius: 99, background: "#A855F7", display: "flex", marginLeft: 7 }} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: "20px 21px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
width: 42,
|
||||
height: 42,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
border: "1px solid #4C3A57",
|
||||
borderRadius: 11,
|
||||
background: "#241A2B",
|
||||
color: "#C084FC",
|
||||
}}
|
||||
>
|
||||
<FileGlyph />
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", marginLeft: 13 }}>
|
||||
<div style={{ color: "#F8F4FA", fontSize: 16, fontWeight: 650 }}>Project files</div>
|
||||
<div style={{ color: "#8E8292", fontSize: 12, marginTop: 3 }}>12 items · 284 MB</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 5,
|
||||
display: "flex",
|
||||
marginTop: 21,
|
||||
borderRadius: 99,
|
||||
background: "#342A39",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "72%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
borderRadius: 99,
|
||||
background: "linear-gradient(90deg, #C084FC, #7C2AEF)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 10 }}>
|
||||
<div style={{ color: "#A99EAD", fontSize: 11 }}>Sending directly</div>
|
||||
<div style={{ color: "#D8B4FE", fontSize: 11, fontWeight: 700 }}>72%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 263,
|
||||
left: 112,
|
||||
width: 62,
|
||||
height: 8,
|
||||
display: "flex",
|
||||
borderRadius: 99,
|
||||
background: "#44384A",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 270,
|
||||
left: 87,
|
||||
width: 112,
|
||||
height: 8,
|
||||
display: "flex",
|
||||
borderRadius: 99,
|
||||
background: "#29212E",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 226,
|
||||
left: 286,
|
||||
width: 50,
|
||||
height: 50,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
border: "1px solid #6A477B",
|
||||
borderRadius: 15,
|
||||
background: "#211627",
|
||||
color: "#D8B4FE",
|
||||
boxShadow: "0 12px 32px rgba(124, 42, 239, 0.3)",
|
||||
}}
|
||||
>
|
||||
<LockGlyph />
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 238,
|
||||
right: 6,
|
||||
width: 127,
|
||||
height: 230,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
border: "2px solid #51415A",
|
||||
borderRadius: 30,
|
||||
background: "#18131D",
|
||||
boxShadow: "0 24px 70px rgba(0, 0, 0, 0.42)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 5,
|
||||
display: "flex",
|
||||
marginTop: 10,
|
||||
borderRadius: 99,
|
||||
background: "#45374C",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: 58,
|
||||
height: 58,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 38,
|
||||
border: "1px solid #6F4A82",
|
||||
borderRadius: 99,
|
||||
background: "#27182E",
|
||||
color: "#D8B4FE",
|
||||
}}
|
||||
>
|
||||
<CheckGlyph />
|
||||
</div>
|
||||
<div style={{ color: "#F8F4FA", fontSize: 15, fontWeight: 700, marginTop: 15 }}>Received</div>
|
||||
<div style={{ color: "#8E8292", fontSize: 10, marginTop: 5 }}>Verified on arrival</div>
|
||||
<div
|
||||
style={{
|
||||
width: 46,
|
||||
height: 4,
|
||||
display: "flex",
|
||||
marginTop: 32,
|
||||
borderRadius: 99,
|
||||
background: "#45374C",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GET() {
|
||||
return new ImageResponse(
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
background: "#0D0A10",
|
||||
color: "#FAF7FC",
|
||||
fontFamily: "sans-serif",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
background:
|
||||
"radial-gradient(circle at 84% 48%, rgba(168, 85, 247, 0.14), transparent 34%), radial-gradient(circle at 16% 100%, rgba(124, 42, 239, 0.09), transparent 30%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ position: "absolute", top: 64, bottom: 64, left: 64, display: "flex", borderLeft: "1px dashed #3A3040" }} />
|
||||
<div style={{ position: "absolute", top: 64, bottom: 64, right: 64, display: "flex", borderRight: "1px dashed #3A3040" }} />
|
||||
<div style={{ position: "absolute", top: 64, right: 0, left: 0, display: "flex", borderTop: "1px solid #3A3040" }} />
|
||||
<div style={{ position: "absolute", right: 0, bottom: 64, left: 0, display: "flex", borderBottom: "1px solid #3A3040" }} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 89,
|
||||
left: 96,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<BrandMark />
|
||||
<div style={{ display: "flex", alignItems: "baseline", marginLeft: 12 }}>
|
||||
<div style={{ fontSize: 28, fontWeight: 750, letterSpacing: -1.1 }}>VniDrop</div>
|
||||
<div
|
||||
style={{
|
||||
marginLeft: 18,
|
||||
color: "#8E8292",
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 1.8,
|
||||
}}
|
||||
>
|
||||
OPEN SOURCE · LOCAL P2P
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 185,
|
||||
left: 96,
|
||||
width: 625,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
fontSize: 66,
|
||||
fontWeight: 750,
|
||||
lineHeight: 0.99,
|
||||
letterSpacing: -3.4,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>Your files.</div>
|
||||
<div style={{ display: "flex", color: "#C084FC", marginTop: 3 }}>A straight line</div>
|
||||
<div style={{ display: "flex", marginTop: 3 }}>between devices.</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 101,
|
||||
left: 96,
|
||||
display: "flex",
|
||||
color: "#AAA0AE",
|
||||
fontSize: 25,
|
||||
fontWeight: 450,
|
||||
letterSpacing: -0.5,
|
||||
}}
|
||||
>
|
||||
Send files directly. Stay in control.
|
||||
</div>
|
||||
|
||||
<TransferIllustration />
|
||||
</div>,
|
||||
imageSize,
|
||||
);
|
||||
}
|
||||
169
docs/app/page.tsx
Normal file
169
docs/app/page.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Icon, type IconName } from "@/components/icons";
|
||||
import { PlatformIcon, supportedPlatforms } from "@/components/platform-icon";
|
||||
import { Reveal } from "@/components/reveal";
|
||||
|
||||
const steps: Array<{
|
||||
icon: IconName;
|
||||
number: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}> = [
|
||||
{
|
||||
icon: "folder",
|
||||
number: "01",
|
||||
title: "Choose what to send",
|
||||
text: "Pick files, a batch, or a complete folder. VniDrop preserves the folder structure.",
|
||||
},
|
||||
{
|
||||
icon: "qr",
|
||||
number: "02",
|
||||
title: "Share an invitation",
|
||||
text: "Introduce the devices with a QR code, NFC tap, or portable .vnd invitation.",
|
||||
},
|
||||
{
|
||||
icon: "shield",
|
||||
number: "03",
|
||||
title: "Approve and transfer",
|
||||
text: "The receiver asks first. Once approved, the files move over an authenticated encrypted connection.",
|
||||
},
|
||||
];
|
||||
|
||||
const trustItems: Array<{
|
||||
icon: IconName;
|
||||
title: string;
|
||||
text: string;
|
||||
}> = [
|
||||
{
|
||||
icon: "devices",
|
||||
title: "Direct when possible",
|
||||
text: "Devices connect directly when they can. An encrypted relay forwards traffic when they cannot.",
|
||||
},
|
||||
{
|
||||
icon: "lock",
|
||||
title: "No hosted copy",
|
||||
text: "A relay is a route, not storage. Your transfer is never turned into a cloud download.",
|
||||
},
|
||||
{
|
||||
icon: "verified",
|
||||
title: "Verified on arrival",
|
||||
text: "Content addressing checks that the received bytes match exactly what you sent.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main id="main-content">
|
||||
<section className="hero-section">
|
||||
<div className="hero-ambient hero-ambient-one" aria-hidden="true" />
|
||||
<div className="hero-ambient hero-ambient-two" aria-hidden="true" />
|
||||
<div className="page-shell hero-layout">
|
||||
<div className="hero-copy">
|
||||
<Reveal>
|
||||
<div className="eyebrow-badge">
|
||||
<span className="eyebrow-dot" />
|
||||
OPEN SOURCE · EARLY DEVELOPMENT
|
||||
</div>
|
||||
<h1>
|
||||
Your files.
|
||||
<br />
|
||||
<span>A straight line</span>
|
||||
<br />
|
||||
between devices.
|
||||
</h1>
|
||||
<p className="hero-lead">
|
||||
Send files and folders across your devices—direct when possible, private by design,
|
||||
and always under your control.
|
||||
</p>
|
||||
<div className="hero-actions">
|
||||
<a className="button button-primary" href="#how-it-works">
|
||||
See how it works
|
||||
<Icon name="arrow" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="hero-platforms" aria-label="Supported platforms">
|
||||
<span>Available across</span>
|
||||
<ul>
|
||||
{supportedPlatforms.map((platform) => (
|
||||
<li key={platform} aria-label={platform} title={platform}>
|
||||
<PlatformIcon className="platform-glyph" platform={platform} />
|
||||
<span className="sr-only">{platform}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<Reveal className="hero-art-reveal" delay={120}>
|
||||
<figure className="hero-app-preview">
|
||||
<Image
|
||||
className="hero-app-preview-image"
|
||||
src="/App.png"
|
||||
width={2338}
|
||||
height={1873}
|
||||
sizes="(max-width: 920px) calc(100vw - 32px), 56vw"
|
||||
alt="VniDrop running on desktop and iPhone, showing transfer review, receiver permissions, and invitation options."
|
||||
priority
|
||||
unoptimized
|
||||
/>
|
||||
</figure>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="how-it-works" className="simple-section simple-flow-section">
|
||||
<div className="page-shell">
|
||||
<Reveal className="simple-heading">
|
||||
<span className="kicker">HOW IT WORKS</span>
|
||||
<h2>Three steps. No account.</h2>
|
||||
<p>
|
||||
Choose the files, introduce the devices, and approve the handoff. VniDrop handles the
|
||||
secure route.
|
||||
</p>
|
||||
</Reveal>
|
||||
<div className="simple-steps">
|
||||
{steps.map((step) => (
|
||||
<article key={step.number} className="simple-step">
|
||||
<div className="simple-step-top">
|
||||
<span>{step.number}</span>
|
||||
<Icon name={step.icon} />
|
||||
</div>
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.text}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="privacy" className="trust-summary-section">
|
||||
<div className="page-shell trust-summary">
|
||||
<Reveal className="trust-summary-copy">
|
||||
<span className="kicker kicker-light">PRIVACY BY DESIGN</span>
|
||||
<h2>What VniDrop does—and doesn’t do.</h2>
|
||||
<p>
|
||||
The transfer happens between devices. These are the details that matter when you
|
||||
decide what to share and who can receive it.
|
||||
</p>
|
||||
<Link className="simple-text-link" href="/privacy/">
|
||||
Read the privacy policy
|
||||
<Icon name="arrow" />
|
||||
</Link>
|
||||
</Reveal>
|
||||
<div className="trust-summary-list">
|
||||
{trustItems.map((item) => (
|
||||
<article key={item.title} className="trust-summary-item">
|
||||
<span><Icon name={item.icon} /></span>
|
||||
<div>
|
||||
<h3>{item.title}</h3>
|
||||
<p>{item.text}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
388
docs/app/privacy/page.tsx
Normal file
388
docs/app/privacy/page.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Privacy policy",
|
||||
description:
|
||||
"How VniDrop handles transfers, local app data, optional diagnostics, bug reports, and website visits.",
|
||||
};
|
||||
|
||||
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 (
|
||||
<main id="main-content" className="privacy-page">
|
||||
<section className="privacy-hero">
|
||||
<div className="page-shell privacy-hero-inner">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>
|
||||
This policy explains what moves between devices, what stays local, and what is sent
|
||||
only when you choose to share diagnostics or a bug report.
|
||||
</p>
|
||||
<p className="privacy-meta">Effective July 16, 2026 · Version 1.0</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="privacy-document-section">
|
||||
<div className="page-shell privacy-document-layout">
|
||||
<aside className="privacy-toc">
|
||||
<p>On this page</p>
|
||||
<nav aria-label="Privacy policy sections">
|
||||
<ol>
|
||||
{sections.map(([id, label]) => (
|
||||
<li key={id}>
|
||||
<a href={`#${id}`}>{label}</a>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<article className="privacy-document">
|
||||
<div className="privacy-callout">
|
||||
<strong>The short version</strong>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section id="scope" className="policy-section">
|
||||
<h2>Scope and who “VniDrop” means</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="transfers" className="policy-section">
|
||||
<h2>What happens during a transfer</h2>
|
||||
<h3>File contents</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<h3>Invitations and transfer metadata</h3>
|
||||
<p>
|
||||
A QR code, NFC tag, or <code>.vnd</code> 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.
|
||||
</p>
|
||||
<h3>What peers and relays can see</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<div className="policy-note">
|
||||
<p>
|
||||
Approval is required by default. If the sender selects “Anyone with this transfer,”
|
||||
anyone holding the invitation may receive the files until sharing stops.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="local-data" className="policy-section">
|
||||
<h2>Information kept on your device</h2>
|
||||
<p>VniDrop stores the information needed to operate the app locally, including:</p>
|
||||
<ul>
|
||||
<li>device identity and networking keys used to establish secure connections;</li>
|
||||
<li>active shares, transfer history, receiver requests, progress, and status;</li>
|
||||
<li>app preferences, including access and diagnostics choices;</li>
|
||||
<li>download destinations and locally managed transfer data; and</li>
|
||||
<li>an anonymous installation identifier used only for diagnostics correlation.</li>
|
||||
</ul>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="diagnostics" className="policy-section">
|
||||
<h2>Optional diagnostics and bug reports</h2>
|
||||
<h3>Automatic product diagnostics</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<h3>User-submitted bug reports</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<h3>Data deliberately excluded</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="website" className="policy-section">
|
||||
<h2>The VniDrop website</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="permissions" className="policy-section">
|
||||
<h2>Device permissions</h2>
|
||||
<dl className="permission-list">
|
||||
<div>
|
||||
<dt>Files & folders</dt>
|
||||
<dd>Choose what to send and where received files are saved.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Camera / scanner</dt>
|
||||
<dd>Scan a QR invitation when you choose that receive method.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>NFC</dt>
|
||||
<dd>Read or write an invitation through a compatible NFC tag.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Network & notifications</dt>
|
||||
<dd>Connect peers and alert you to background receiver requests.</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="providers" className="policy-section">
|
||||
<h2>Infrastructure and external services</h2>
|
||||
<dl className="provider-list">
|
||||
<div>
|
||||
<dt>Iroh / public relay operators</dt>
|
||||
<dd>
|
||||
Device discovery, connection establishment, and encrypted relay fallback.
|
||||
Relays process connection metadata but cannot decrypt transfer contents.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Cloudflare</dt>
|
||||
<dd>
|
||||
The project’s diagnostics design uses Cloudflare Workers, D1, and R2.
|
||||
Cloudflare also processes source IPs for request delivery and abuse controls.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Google Play services</dt>
|
||||
<dd>
|
||||
May provide the QR code scanner on supported Android devices when you choose to
|
||||
scan an invitation.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>GitHub</dt>
|
||||
<dd>
|
||||
Hosts the source repository, issue tracker, and external pages linked from this
|
||||
site. GitHub’s own privacy terms apply after you follow those links.
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p className="provider-links">
|
||||
Provider policies:{" "}
|
||||
<a
|
||||
href="https://services.iroh.computer/legal/privacy"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Iroh
|
||||
</a>
|
||||
,{" "}
|
||||
<a
|
||||
href="https://www.cloudflare.com/policies/privacy/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Cloudflare
|
||||
</a>
|
||||
,{" "}
|
||||
<a href="https://policies.google.com/privacy" target="_blank" rel="noreferrer">
|
||||
Google
|
||||
</a>
|
||||
, and{" "}
|
||||
<a
|
||||
href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="retention" className="policy-section">
|
||||
<h2>Retention and deletion</h2>
|
||||
<div className="retention-table-wrap">
|
||||
<table className="retention-table">
|
||||
<caption className="sr-only">Data retention periods</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Data</th>
|
||||
<th scope="col">Typical retention</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Transfer history and settings</th>
|
||||
<td>Until you delete them, clear app data, or uninstall</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Pending local crash reports</th>
|
||||
<td>Up to 30 days and 20 reports; deleted when diagnostics is disabled</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Server diagnostics and bug reports</th>
|
||||
<td>The current project configuration is 90 days, with scheduled deletion</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Downloaded files</th>
|
||||
<td>Until you delete them through your operating system</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="choices" className="policy-section">
|
||||
<h2>Your choices and rights</h2>
|
||||
<ul>
|
||||
<li>Enable or disable “Share diagnostics” in VniDrop settings.</li>
|
||||
<li>
|
||||
Submit a bug report only when you choose, omit contact information, and exclude
|
||||
logs.
|
||||
</li>
|
||||
<li>Approve or refuse each receiver, cancel a transfer, or stop sharing.</li>
|
||||
<li>
|
||||
Delete individual transfer history or clear completed, failed, and cancelled
|
||||
receive history.
|
||||
</li>
|
||||
<li>
|
||||
Delete downloaded files using your operating system, or clear all app data by
|
||||
uninstalling or resetting the app.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="security" className="policy-section">
|
||||
<h2>Security</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
Please report a suspected vulnerability through the private process in the{" "}
|
||||
<a
|
||||
href="https://github.com/vnidrop/vnidrop/blob/master/SECURITY.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
VniDrop security policy
|
||||
</a>
|
||||
, not in a public issue.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="changes" className="policy-section">
|
||||
<h2>Changes to this policy</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="contact" className="policy-section policy-contact">
|
||||
<h2>Contact</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<a
|
||||
className="privacy-contact-link"
|
||||
href="https://github.com/vnidrop/vnidrop/issues/new"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Contact the maintainers
|
||||
</a>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
288
docs/app/privacy/privacy-document.css
Normal file
288
docs/app/privacy/privacy-document.css
Normal file
@@ -0,0 +1,288 @@
|
||||
.privacy-page {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.privacy-hero {
|
||||
padding: 158px 0 72px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.privacy-hero-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.privacy-hero h1 {
|
||||
margin-bottom: 22px;
|
||||
font-size: clamp(3.25rem, 5.2vw, 4.5rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.055em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.privacy-hero-inner > p:not(.privacy-meta) {
|
||||
max-width: 640px;
|
||||
margin-bottom: 24px;
|
||||
color: var(--ink-soft);
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.privacy-meta {
|
||||
margin: 0;
|
||||
color: var(--ink-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.privacy-document-section {
|
||||
padding: 80px 0 144px;
|
||||
}
|
||||
|
||||
.privacy-document-layout {
|
||||
display: grid;
|
||||
grid-template-areas: "article toc";
|
||||
grid-template-columns: minmax(0, 720px) 220px;
|
||||
justify-content: center;
|
||||
gap: 92px;
|
||||
}
|
||||
|
||||
.privacy-document {
|
||||
grid-area: article;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.privacy-toc {
|
||||
position: sticky;
|
||||
top: 108px;
|
||||
grid-area: toc;
|
||||
align-self: start;
|
||||
padding-left: 24px;
|
||||
border-left: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.privacy-toc > p {
|
||||
margin-bottom: 14px;
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.privacy-toc ol {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.privacy-toc a {
|
||||
display: block;
|
||||
padding: 5px 0;
|
||||
color: var(--ink-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
transition: color 160ms ease;
|
||||
}
|
||||
|
||||
.privacy-toc a:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.privacy-callout {
|
||||
margin-bottom: 64px;
|
||||
padding: 0 0 40px 20px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
border-left: 2px solid var(--ink);
|
||||
}
|
||||
|
||||
.privacy-callout strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.privacy-callout p {
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.policy-section {
|
||||
scroll-margin-top: 104px;
|
||||
padding-bottom: 56px;
|
||||
}
|
||||
|
||||
.policy-section + .policy-section {
|
||||
padding-top: 56px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.policy-section h2 {
|
||||
margin-bottom: 24px;
|
||||
font-size: clamp(1.75rem, 2.7vw, 2rem);
|
||||
font-weight: 680;
|
||||
letter-spacing: -0.035em;
|
||||
line-height: 1.22;
|
||||
}
|
||||
|
||||
.policy-section h3 {
|
||||
margin: 32px 0 10px;
|
||||
font-size: 19px;
|
||||
font-weight: 680;
|
||||
letter-spacing: -0.018em;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.policy-section p,
|
||||
.policy-section li,
|
||||
.permission-list dd,
|
||||
.provider-list dd {
|
||||
color: var(--ink-soft);
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.policy-section p {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.policy-section ul {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin: 18px 0 24px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.policy-section li::marker {
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.policy-section code {
|
||||
padding: 2px 5px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
background: var(--paper-warm);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.policy-section a,
|
||||
.privacy-contact-link {
|
||||
color: var(--brand-700);
|
||||
font-weight: 620;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(111, 39, 233, 0.34);
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.policy-section a:hover,
|
||||
.privacy-contact-link:hover {
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
.policy-note {
|
||||
margin-top: 28px;
|
||||
padding: 18px 20px;
|
||||
border-left: 2px solid var(--line-strong);
|
||||
background: var(--paper-warm);
|
||||
}
|
||||
|
||||
.policy-note p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.permission-list,
|
||||
.provider-list {
|
||||
margin: 28px 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.permission-list > div,
|
||||
.provider-list > div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(150px, 0.42fr) 1fr;
|
||||
gap: 24px;
|
||||
padding: 17px 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.permission-list dt,
|
||||
.provider-list dt {
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
font-weight: 680;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.permission-list dd,
|
||||
.provider-list dd {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.provider-links {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.retention-table-wrap {
|
||||
overflow-x: auto;
|
||||
margin: 28px 0;
|
||||
border-top: 1px solid var(--line);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.retention-table {
|
||||
width: 100%;
|
||||
min-width: 600px;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.retention-table th,
|
||||
.retention-table td {
|
||||
padding: 15px 16px 15px 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
color: var(--ink-soft);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.retention-table th:first-child {
|
||||
width: 38%;
|
||||
}
|
||||
|
||||
.retention-table thead th {
|
||||
color: var(--ink-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.retention-table tbody th {
|
||||
color: var(--ink);
|
||||
font-weight: 620;
|
||||
}
|
||||
|
||||
.policy-contact {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.policy-contact p {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.privacy-contact-link::after {
|
||||
margin-left: 5px;
|
||||
content: "↗";
|
||||
}
|
||||
89
docs/app/privacy/privacy-responsive.css
Normal file
89
docs/app/privacy/privacy-responsive.css
Normal file
@@ -0,0 +1,89 @@
|
||||
@media (max-width: 940px) {
|
||||
.privacy-document-layout {
|
||||
grid-template-areas:
|
||||
"toc"
|
||||
"article";
|
||||
grid-template-columns: minmax(0, 720px);
|
||||
gap: 56px;
|
||||
}
|
||||
|
||||
.privacy-toc {
|
||||
position: static;
|
||||
padding: 0 0 28px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.privacy-toc ol {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
column-gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.privacy-hero {
|
||||
padding: 122px 0 54px;
|
||||
}
|
||||
|
||||
.privacy-hero h1 {
|
||||
margin-bottom: 18px;
|
||||
font-size: clamp(2.55rem, 12vw, 3.4rem);
|
||||
}
|
||||
|
||||
.privacy-hero-inner > p:not(.privacy-meta) {
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.privacy-meta {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.privacy-document-section {
|
||||
padding: 56px 0 96px;
|
||||
}
|
||||
|
||||
.privacy-document-layout {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.privacy-toc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.privacy-callout {
|
||||
margin-bottom: 48px;
|
||||
padding: 0 0 32px 16px;
|
||||
}
|
||||
|
||||
.privacy-callout p,
|
||||
.policy-section p,
|
||||
.policy-section li,
|
||||
.permission-list dd,
|
||||
.provider-list dd {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.policy-section {
|
||||
padding-bottom: 44px;
|
||||
}
|
||||
|
||||
.policy-section + .policy-section {
|
||||
padding-top: 44px;
|
||||
}
|
||||
|
||||
.policy-section h2 {
|
||||
font-size: 1.65rem;
|
||||
}
|
||||
|
||||
.policy-section h3 {
|
||||
margin-top: 28px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.permission-list > div,
|
||||
.provider-list > div {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
191
docs/app/responsive.css
Normal file
191
docs/app/responsive.css
Normal file
@@ -0,0 +1,191 @@
|
||||
@keyframes ambient-drift {
|
||||
from { transform: translate3d(-10px, -8px, 0) rotate(0deg); }
|
||||
to { transform: translate3d(24px, 18px, 0) rotate(8deg); }
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
.trust-summary {
|
||||
gap: 65px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.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;
|
||||
}
|
||||
|
||||
.hero-app-preview {
|
||||
width: min(100%, 780px);
|
||||
margin: 12px auto 0;
|
||||
}
|
||||
|
||||
.simple-step {
|
||||
padding-right: 22px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.trust-summary {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 50px;
|
||||
}
|
||||
|
||||
.trust-summary-copy {
|
||||
max-width: 680px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.page-shell {
|
||||
width: min(calc(100% - 32px), var(--shell));
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.hero-app-preview {
|
||||
width: calc(100% + 24px);
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.simple-section,
|
||||
.trust-summary-section {
|
||||
padding: 88px 0;
|
||||
}
|
||||
|
||||
.simple-heading {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.simple-heading h2,
|
||||
.trust-summary-copy h2 {
|
||||
font-size: clamp(2.35rem, 12vw, 3.5rem);
|
||||
}
|
||||
|
||||
.simple-heading p,
|
||||
.trust-summary-copy p {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.simple-steps {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.simple-step {
|
||||
min-height: 0;
|
||||
padding: 28px 0 32px;
|
||||
}
|
||||
|
||||
.simple-step + .simple-step {
|
||||
border-top: 1px solid var(--line);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.simple-step-top {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.trust-summary-item {
|
||||
grid-template-columns: 40px 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.trust-summary-item > span {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.footer-inner,
|
||||
.footer-bottom {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
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;
|
||||
}
|
||||
}
|
||||
33
docs/components/brand.tsx
Normal file
33
docs/components/brand.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
type BrandAssetProps = {
|
||||
className?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export function BrandMark({ className, title }: BrandAssetProps) {
|
||||
return (
|
||||
<Image
|
||||
className={className}
|
||||
src="/brand-mark.svg"
|
||||
width={1024}
|
||||
height={1024}
|
||||
alt={title ?? ""}
|
||||
aria-hidden={title ? undefined : true}
|
||||
unoptimized
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Brand() {
|
||||
return (
|
||||
<Link className="brand" href="/" aria-label="VniDrop home">
|
||||
<span className="brand-primary">
|
||||
<BrandMark className="brand-mark-image" />
|
||||
<span className="brand-name">VniDrop</span>
|
||||
</span>
|
||||
<span className="brand-tagline">Send files directly. Stay in control.</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
151
docs/components/header.css
Normal file
151
docs/components/header.css
Normal file
@@ -0,0 +1,151 @@
|
||||
.site-header {
|
||||
position: fixed;
|
||||
z-index: 220;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 72px;
|
||||
border-bottom: 1px solid rgba(224, 217, 228, 0.76);
|
||||
background: rgba(251, 250, 252, 0.88);
|
||||
box-shadow: 0 8px 30px rgba(42, 26, 49, 0.035);
|
||||
backdrop-filter: blur(20px) saturate(1.3);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.3);
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
display: flex;
|
||||
height: 72px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.brand-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
height: 47px;
|
||||
}
|
||||
|
||||
.brand-mark-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 7px 14px rgba(126, 42, 189, 0.14));
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
color: var(--ink);
|
||||
font-family: var(--font-display);
|
||||
font-size: 19px;
|
||||
font-weight: 780;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
margin-top: -4px;
|
||||
margin-left: 4px;
|
||||
color: #756c79;
|
||||
font-size: 9px;
|
||||
font-weight: 580;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.01em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-github-button {
|
||||
display: inline-flex;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #28232b;
|
||||
border-radius: 10px;
|
||||
background: var(--ink);
|
||||
box-shadow: 0 8px 22px rgba(23, 20, 25, 0.13), inset 0 1px rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 680;
|
||||
line-height: 1;
|
||||
animation: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.header-github-button:hover {
|
||||
border-color: #6f6475;
|
||||
background: #302a34;
|
||||
box-shadow: 0 11px 28px rgba(23, 20, 25, 0.2), inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.header-github-button:focus-visible {
|
||||
outline: 3px solid rgba(168, 85, 247, 0.28);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.header-github-button svg {
|
||||
width: 17px;
|
||||
height: 16px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.site-header,
|
||||
.header-inner {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.brand-mark-image {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.brand-primary {
|
||||
height: 43px;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
margin-top: -3px;
|
||||
margin-left: 3px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.header-github-button {
|
||||
min-height: 38px;
|
||||
gap: 7px;
|
||||
padding-inline: 12px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.brand-mark-image {
|
||||
width: 41px;
|
||||
height: 41px;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-size: 7.5px;
|
||||
}
|
||||
|
||||
.header-github-button {
|
||||
min-height: 36px;
|
||||
padding-inline: 10px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
85
docs/components/icons.tsx
Normal file
85
docs/components/icons.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export type IconName =
|
||||
| "arrow"
|
||||
| "devices"
|
||||
| "folder"
|
||||
| "github"
|
||||
| "lock"
|
||||
| "qr"
|
||||
| "shield"
|
||||
| "verified";
|
||||
|
||||
type IconProps = SVGProps<SVGSVGElement> & {
|
||||
name: IconName;
|
||||
};
|
||||
|
||||
export function Icon({ name, ...props }: IconProps) {
|
||||
const paths: Record<IconName, React.ReactNode> = {
|
||||
arrow: (
|
||||
<>
|
||||
<path d="M5 12h14" />
|
||||
<path d="m14 7 5 5-5 5" />
|
||||
</>
|
||||
),
|
||||
devices: (
|
||||
<>
|
||||
<rect x="3" y="5" width="13" height="10" rx="2" />
|
||||
<path d="M7 19h5M9.5 15v4" />
|
||||
<rect x="17" y="8" width="4" height="10" rx="1" />
|
||||
</>
|
||||
),
|
||||
folder: (
|
||||
<path d="M3 7.5A2.5 2.5 0 0 1 5.5 5H10l2 2h6.5A2.5 2.5 0 0 1 21 9.5v7a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 16.5z" />
|
||||
),
|
||||
github: (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.5 2.22168C5.23312 2.22168 2.58496 4.87398 2.58496 8.14677C2.58496 10.7642 4.27962 12.9853 6.63026 13.7684C6.92601 13.8228 7.03366 13.6401 7.03366 13.4827C7.03366 13.3425 7.02893 12.9693 7.02597 12.4754C5.38041 12.8333 5.0332 11.681 5.0332 11.681C4.76465 10.996 4.37663 10.8139 4.37663 10.8139C3.83954 10.4471 4.41744 10.4542 4.41744 10.4542C5.01072 10.4956 5.32303 11.0647 5.32303 11.0647C5.85065 11.9697 6.70774 11.7082 7.04431 11.5568C7.09873 11.1741 7.25134 10.9132 7.42051 10.7654C6.10737 10.6157 4.72621 10.107 4.72621 7.83683C4.72621 7.19031 4.95689 6.66092 5.33486 6.24686C5.27394 6.09721 5.07105 5.49447 5.39283 4.67938C5.39283 4.67938 5.88969 4.51967 7.01947 5.28626C7.502 5.15466 7.99985 5.08763 8.5 5.08692C9.00278 5.08929 9.50851 5.15495 9.98113 5.28626C11.1103 4.51967 11.606 4.67879 11.606 4.67879C11.9289 5.49447 11.7255 6.09721 11.6651 6.24686C12.0437 6.66092 12.2732 7.19031 12.2732 7.83683C12.2732 10.1129 10.8897 10.6139 9.5724 10.7606C9.78475 10.9434 9.97344 11.3048 9.97344 11.8579C9.97344 12.6493 9.96634 13.2887 9.96634 13.4827C9.96634 13.6413 10.0728 13.8258 10.3733 13.7678C11.5512 13.3728 12.5751 12.6175 13.3003 11.6089C14.0256 10.6002 14.4155 9.38912 14.415 8.14677C14.415 4.87398 11.7663 2.22168 8.5 2.22168Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
),
|
||||
lock: (
|
||||
<>
|
||||
<rect x="5" y="10" width="14" height="11" rx="3" />
|
||||
<path d="M8 10V7a4 4 0 0 1 8 0v3M12 14v3" />
|
||||
</>
|
||||
),
|
||||
qr: (
|
||||
<>
|
||||
<rect x="3" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1" />
|
||||
<path d="M14 14h3v3h-3zM18 18h3v3h-3zM18 13h3M13 19h3v2" />
|
||||
</>
|
||||
),
|
||||
shield: (
|
||||
<>
|
||||
<path d="M12 3 4.5 6v5.5c0 4.8 3.2 8.1 7.5 9.5 4.3-1.4 7.5-4.7 7.5-9.5V6z" />
|
||||
<path d="m8.5 12 2.2 2.2 4.8-5" />
|
||||
</>
|
||||
),
|
||||
verified: (
|
||||
<>
|
||||
<path d="m12 3 2 2.1 2.9-.1.1 2.9 2 2.1-2 2.1-.1 2.9-2.9-.1-2 2.1-2-2.1-2.9.1-.1-2.9-2-2.1 2-2.1.1-2.9 2.9.1z" />
|
||||
<path d="m9 10 2 2 4-4" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={name === "github" ? "0 0 17 16" : "0 0 24 24"}
|
||||
fill="none"
|
||||
stroke={name === "github" ? "none" : "currentColor"}
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
{paths[name]}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
90
docs/components/platform-icon.tsx
Normal file
90
docs/components/platform-icon.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export const supportedPlatforms = ["Android", "iOS", "macOS", "Windows", "Linux"] as const;
|
||||
|
||||
export type PlatformName = (typeof supportedPlatforms)[number];
|
||||
|
||||
type PlatformIconProps = Omit<SVGProps<SVGSVGElement>, "children"> & {
|
||||
platform: PlatformName;
|
||||
};
|
||||
|
||||
function AndroidIcon() {
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
d="M7.25 8.15h9.5a1.5 1.5 0 0 1 1.5 1.5v6.1a1.5 1.5 0 0 1-1.5 1.5h-9.5a1.5 1.5 0 0 1-1.5-1.5v-6.1a1.5 1.5 0 0 1 1.5-1.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M8.15 8.1a3.95 3.95 0 0 1 7.7 0" fill="currentColor" />
|
||||
<path d="m8.35 4.45-1.2-1.7M15.65 4.45l1.2-1.7" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" />
|
||||
<path d="M4.2 10.05v5.15M19.8 10.05v5.15M8.5 17v3.05M15.5 17v3.05" stroke="currentColor" strokeWidth="2.1" strokeLinecap="round" />
|
||||
<circle cx="9.5" cy="6.65" r="0.62" fill="white" />
|
||||
<circle cx="14.5" cy="6.65" r="0.62" fill="white" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AppleIcon() {
|
||||
return (
|
||||
<path
|
||||
d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.32.07 2.24.76 3.01.82 1.15-.23 2.25-.89 3.44-.8 1.43.12 2.51.68 3.22 1.7-2.96 1.78-2.26 5.68.46 6.77-.54 1.42-1.24 2.83-2.13 4.48ZM12.03 7.25c-.15-2.13 1.59-3.89 3.58-4.06.27 2.46-2.23 4.3-3.58 4.06Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MacIcon() {
|
||||
return (
|
||||
<>
|
||||
<rect x="2.5" y="2.5" width="19" height="19" rx="5.5" fill="currentColor" />
|
||||
<path d="M12 2.5v19" stroke="white" strokeOpacity="0.72" />
|
||||
<path d="M8 9.15h.01M16 9.15h.01" stroke="white" strokeWidth="1.9" strokeLinecap="round" />
|
||||
<path d="M7.1 15.2c2.7 1.45 7.1 1.45 9.8 0" stroke="white" strokeWidth="1.35" strokeLinecap="round" />
|
||||
<path d="m13.55 6.25-2.45 8.3" stroke="white" strokeWidth="1.1" strokeLinecap="round" strokeOpacity="0.82" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function WindowsIcon() {
|
||||
return (
|
||||
<path
|
||||
d="M3 5.15 11 4v7H3V5.15Zm9-1.3L21 2.5V11h-9V3.85ZM3 12h8v7l-8-1.15V12Zm9 0h9v8.5l-9-1.35V12Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LinuxIcon() {
|
||||
return (
|
||||
<>
|
||||
<ellipse cx="12" cy="12.6" rx="5.25" ry="7.55" fill="#242027" />
|
||||
<ellipse cx="12" cy="14.55" rx="3.45" ry="4.7" fill="#f8f7f9" />
|
||||
<ellipse cx="9.95" cy="8.45" rx="1.35" ry="1.8" fill="white" />
|
||||
<ellipse cx="14.05" cy="8.45" rx="1.35" ry="1.8" fill="white" />
|
||||
<circle cx="10.35" cy="8.65" r="0.55" fill="#242027" />
|
||||
<circle cx="13.65" cy="8.65" r="0.55" fill="#242027" />
|
||||
<path d="m9.8 10.15 2.2-1 2.2 1-2.2 2.1-2.2-2.1Z" fill="#f5b82e" />
|
||||
<path d="M6.15 18.45c1.2-1 2.45-1.3 3.75-.75-.7 1.6-2.35 2.45-4.9 2.2.2-.6.58-1.08 1.15-1.45ZM17.85 18.45c-1.2-1-2.45-1.3-3.75-.75.7 1.6 2.35 2.45 4.9 2.2-.2-.6-.58-1.08-1.15-1.45Z" fill="#f5b82e" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function PlatformIcon({ platform, className, ...props }: PlatformIconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
data-platform={platform.toLowerCase()}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
{platform === "Android" && <AndroidIcon />}
|
||||
{platform === "iOS" && <AppleIcon />}
|
||||
{platform === "macOS" && <MacIcon />}
|
||||
{platform === "Windows" && <WindowsIcon />}
|
||||
{platform === "Linux" && <LinuxIcon />}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
52
docs/components/reveal.tsx
Normal file
52
docs/components/reveal.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
type RevealProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
export function Reveal({ children, className = "", delay = 0 }: RevealProps) {
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.classList.add("motion-ready");
|
||||
const target = targetRef.current;
|
||||
if (!target) return;
|
||||
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
if (reduceMotion || !("IntersectionObserver" in window)) {
|
||||
target.classList.add("is-visible");
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
entry.target.classList.add("is-visible");
|
||||
observer.unobserve(entry.target);
|
||||
});
|
||||
},
|
||||
{ rootMargin: "0px 0px -10%", threshold: 0.08 },
|
||||
);
|
||||
|
||||
observer.observe(target);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={targetRef}
|
||||
className={`reveal ${className}`.trim()}
|
||||
data-reveal
|
||||
style={{ "--reveal-delay": `${delay}ms` } as CSSProperties}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
docs/components/site-footer.tsx
Normal file
33
docs/components/site-footer.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import Link from "next/link";
|
||||
import { BrandMark } from "@/components/brand";
|
||||
import { Icon } from "@/components/icons";
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="site-footer">
|
||||
<div className="footer-inner page-shell">
|
||||
<div className="footer-identity">
|
||||
<BrandMark />
|
||||
<span>VniDrop</span>
|
||||
</div>
|
||||
<nav className="footer-links" aria-label="Footer navigation">
|
||||
<a href="https://github.com/vnidrop/vnidrop" target="_blank" rel="noreferrer">
|
||||
<Icon name="github" /> GitHub
|
||||
</a>
|
||||
<Link href="/privacy/">Privacy</Link>
|
||||
<a
|
||||
href="https://github.com/vnidrop/vnidrop/blob/master/LICENSE"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Apache 2.0
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="footer-bottom page-shell">
|
||||
<p>© 2026 VniDrop contributors.</p>
|
||||
<p>Open source · Early development</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
22
docs/components/site-header.tsx
Normal file
22
docs/components/site-header.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import Link from "next/link";
|
||||
import { Brand } from "@/components/brand";
|
||||
import { Icon } from "@/components/icons";
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="site-header">
|
||||
<div className="header-inner page-shell">
|
||||
<Brand />
|
||||
<Link
|
||||
className="header-github-button"
|
||||
href="https://github.com/vnidrop/vnidrop"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Icon name="github" />
|
||||
View on GitHub
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
9
docs/eslint.config.mjs
Normal file
9
docs/eslint.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTypeScript from "eslint-config-next/typescript";
|
||||
|
||||
export default defineConfig([
|
||||
...nextVitals,
|
||||
...nextTypeScript,
|
||||
globalIgnores([".next/**", "out/**", "next-env.d.ts"]),
|
||||
]);
|
||||
6
docs/next-env.d.ts
vendored
Normal file
6
docs/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
8
docs/next.config.ts
Normal file
8
docs/next.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
trailingSlash: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6082
docs/package-lock.json
generated
Normal file
6082
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
docs/package.json
Normal file
30
docs/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "vnidrop-site",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=22.12"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.2.10",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "26.1.1",
|
||||
"@types/react": "19.2.17",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"eslint": "9.39.5",
|
||||
"eslint-config-next": "16.2.10",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"overrides": {
|
||||
"postcss": "8.5.19"
|
||||
}
|
||||
}
|
||||
BIN
docs/public/App.png
Normal file
BIN
docs/public/App.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
26
docs/public/brand-mark.svg
Normal file
26
docs/public/brand-mark.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="url(#paint0_linear_11_12)"/>
|
||||
<path d="M520.4 431.72C495.8 464.52 443.32 530.12 420.36 577.68C390.84 636.72 403.96 699.04 446.6 731.84C487.6 758.08 549.92 758.08 592.56 730.2C633.56 700.68 646.68 636.72 620.44 577.68C597.48 530.12 546.64 464.52 520.4 431.72Z" fill="url(#paint1_linear_11_12)"/>
|
||||
<mask id="mask0_11_12" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="176" y="148" width="671" height="732">
|
||||
<path d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_11_12)">
|
||||
<path d="M688 148H782C832 148 842 171.8 847.48 210V303.68L688 148Z" fill="url(#paint2_linear_11_12)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_11_12" x1="176" y1="148" x2="904.706" y2="816.252" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#A855F7"/>
|
||||
<stop offset="0.48" stop-color="#9D4DF4"/>
|
||||
<stop offset="1" stop-color="#7C2AEF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_11_12" x1="404.439" y1="431.72" x2="707.314" y2="649.286" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#A855F7"/>
|
||||
<stop offset="0.48" stop-color="#9D4DF4"/>
|
||||
<stop offset="1" stop-color="#7C2AEF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_11_12" x1="683.48" y1="144.6" x2="793.636" y2="306.833" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2DDFF"/>
|
||||
<stop offset="1" stop-color="#C084FC"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
16
docs/public/site.webmanifest
Normal file
16
docs/public/site.webmanifest
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "VniDrop",
|
||||
"short_name": "VniDrop",
|
||||
"description": "Direct, private file transfer across your devices.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#fbfafc",
|
||||
"theme_color": "#a855f7",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
docs/tsconfig.json
Normal file
41
docs/tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user