feat: refresh landing page with glow animation, SVG icons, interop diagram

Restore personality from old Next.js landing: animated hero glow, playful
"MySpace → (you)rSpace" copy, SVG feature cards with teal/indigo accents,
shield graphic for EncryptID, interoperability ASCII diagram, sharper
philosophy copy, ecosystem grid with r*.online domains, richer footer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-10 22:30:55 -04:00
parent 53c757e68e
commit 2f4258aa32
1 changed files with 375 additions and 42 deletions

View File

@ -17,10 +17,11 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
const ecosystemCards = modules
.map(
(m) => `
<a href="/${escapeAttr(m.id)}" class="rl-card rl-card--center" style="text-decoration:none;color:inherit">
<div class="rl-icon-box">${m.icon}</div>
<h3>${escapeHtml(m.name)}</h3>
<p>${escapeHtml(m.description)}</p>
<a href="/${escapeAttr(m.id)}" class="eco-card" style="text-decoration:none;color:inherit">
<div class="eco-card__icon">${m.icon}</div>
<h3 class="eco-card__name">${escapeHtml(m.name)}</h3>
${m.standaloneDomain ? `<span class="eco-card__domain">${escapeHtml(m.standaloneDomain)}</span>` : ""}
<p class="eco-card__desc">${escapeHtml(m.description)}</p>
</a>`,
)
.join("\n");
@ -76,22 +77,31 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
</header>
<!-- Hero -->
<div class="rl-hero">
<div class="rl-hero hero-glow-wrap">
<div class="hero-glow"></div>
<span class="rl-tagline">Community Platform</span>
<h1 class="rl-heading main-wordmark">(you)r<span class="main-wordmark__accent">Space</span></h1>
<p class="rl-subtitle">Remember back when the internet was cool?</p>
<p class="rl-subtitle">
Remember back when the internet was <span class="accent-cool">cool</span>?
</p>
<p class="rl-subtext" style="font-style:italic;opacity:0.85;margin-bottom:0.75rem">
We may not have <em>My</em>Space anymore, but we have <strong>(you)rSpace</strong>.
</p>
<p class="rl-subtext">
A collaborative, local-first platform with ${modules.length}+ interoperable tools.
Own your data, run your community, connect your world &mdash; no landlords required.
Build digital spaces to collaborate on improving your physical world.
Local-first, zero-knowledge privacy, outside the walls of big tech.
</p>
<div class="rl-cta-row">
<a href="${demoUrl}" class="rl-cta-primary" id="ml-primary">Try the Demo</a>
<a href="${demoUrl}" class="rl-cta-primary" id="ml-primary">Start (you)rSpace</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
<div class="main-badges">
<span class="rl-badge">Local-First</span>
<span class="badge-sep" aria-hidden="true">&#9733;</span>
<span class="rl-badge">Self-Hosted</span>
<span class="badge-sep" aria-hidden="true">&#9733;</span>
<span class="rl-badge">Open Source</span>
<span class="badge-sep" aria-hidden="true">&#9733;</span>
<span class="rl-badge">Offline-Ready</span>
</div>
</div>
@ -104,33 +114,45 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
Each tool works on its own, and they all work together.
</p>
<div class="rl-grid-3">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128176;</div>
<div class="feat-card feat-card--teal">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
</div>
<h3>Community Funds</h3>
<p>Budget rivers, revenue splits, and enoughness thresholds. Transparent finances with quadratic funding built in.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128172;</div>
<div class="feat-card feat-card--indigo">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<h3>Messaging &amp; Forum</h3>
<p>Real-time chat, threaded discussions, and async forums. All synced across devices with CRDT magic.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128193;</div>
<div class="feat-card feat-card--teal">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
</div>
<h3>Files &amp; Media</h3>
<p>Upload, organize, and share with memory cards. Public links, folder trees, and metadata that travels with content.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128272;</div>
<div class="feat-card feat-card--indigo">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<h3>Passkey Identity</h3>
<p>One passwordless login for the entire ecosystem. EncryptID uses WebAuthn &mdash; no passwords to leak, no accounts to hack.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128202;</div>
<div class="feat-card feat-card--teal">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg>
</div>
<h3>Dashboards &amp; Data</h3>
<p>Community analytics, voting results, spatial canvases. See everything at a glance and drill into what matters.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128737;</div>
<div class="feat-card feat-card--indigo">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<h3>Privacy by Design</h3>
<p>Your data lives on your device first. End-to-end encrypted sync, per-document keys, zero-knowledge architecture.</p>
</div>
@ -145,23 +167,31 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<div>
<span class="rl-tagline">EncryptID</span>
<h2 class="rl-heading">One Passkey for Everything</h2>
<p class="rl-subtext" style="margin-bottom:0.5rem;font-weight:500;color:var(--rs-text-primary)">
Secure by default, not by opt-in.
</p>
<p class="rl-subtext" style="margin-bottom:1.5rem">
Sign in once with your fingerprint or device PIN. Your passkey works
across every rApp &mdash; no passwords, no email verification loops,
no third-party auth providers.
across every rApp &mdash; no passwords, no email loops,
no third-party auth providers watching over your shoulder.
</p>
<ul class="rl-check-list">
<li><strong>WebAuthn passkeys</strong> &mdash; phishing-resistant by default</li>
<li><strong>WebAuthn passkeys</strong> &mdash; phishing-resistant, device-bound credentials</li>
<li><strong>Guardian recovery</strong> &mdash; 2-of-3 trusted contacts restore access</li>
<li><strong>Device linking</strong> &mdash; QR scan adds your phone or tablet</li>
<li><strong>One RP ID</strong> &mdash; shared across all r*.online domains</li>
<li><strong>Device linking</strong> &mdash; scan a QR to add your phone or tablet</li>
<li><strong>One RP ID</strong> &mdash; works across all r*.online domains</li>
</ul>
</div>
<div style="display:flex;align-items:center;justify-content:center">
<div class="rl-card" style="max-width:320px;text-align:center">
<div class="rl-icon-box" style="margin:0 auto 1rem;font-size:2rem">&#128274;</div>
<h3 style="margin-bottom:0.5rem">Passwordless Login</h3>
<p>Touch your fingerprint sensor, tap your security key, or use your device PIN. That&rsquo;s it.</p>
<div class="encryptid-visual">
<svg class="encryptid-shield" viewBox="0 0 120 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M60 8L10 30v40c0 32 22 55 50 62 28-7 50-30 50-62V30L60 8z" stroke="currentColor" stroke-width="3" fill="rgba(20,184,166,0.08)"/>
<path d="M60 40a14 14 0 1 0 0 28 14 14 0 0 0 0-28z" stroke="currentColor" stroke-width="2.5"/>
<circle cx="60" cy="54" r="4" fill="currentColor"/>
<path d="M48 80h24v8a12 12 0 0 1-24 0v-8z" stroke="currentColor" stroke-width="2.5" fill="rgba(20,184,166,0.12)"/>
<line x1="60" y1="80" x2="60" y2="92" stroke="currentColor" stroke-width="2"/>
</svg>
<p class="encryptid-visual__label">Touch. Tap. Done.</p>
</div>
</div>
</div>
@ -193,17 +223,52 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<p>Only changed bytes travel over the wire. Reconnect after days offline and catch up in seconds.</p>
</div>
</div>
<p class="offline-explain">
Under the hood: <strong>Automerge CRDTs</strong> model every document as a mergeable data structure.
<strong>IndexedDB</strong> persists the full state locally so you never lose work.
A <strong>Service Worker</strong> caches the app shell for instant loads &mdash; even without a network connection.
</p>
</div>
</section>
<!-- Interoperable by Design -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="max-width:820px;text-align:center">
<h2 class="rl-heading">Interoperable by Design</h2>
<p class="rl-subtext">
Every rApp reads and writes to the same sync layer.
No import/export rituals &mdash; data flows between tools automatically.
</p>
<div class="interop-diagram">
<pre class="interop-pre"><code> rDocs rCal rTasks rMaps rWallet ...
| | | | |
v v v v v
+--------------------------------------+
| Automerge Sync Layer |
| (encrypted, per-document keys) |
+--------------------------------------+
| | | | |
v v v v v
Your Your Your Your Your
Device Phone Laptop Server Backup</code></pre>
</div>
</div>
</section>
<!-- Philosophy -->
<section class="rl-section rl-section--alt">
<section class="rl-section">
<div class="rl-container" style="max-width:720px;text-align:center">
<h2 class="rl-heading">The Internet as It Was Always Meant to Be</h2>
<p class="rl-subtext">
<p class="rl-subtext" style="font-size:1.2rem;line-height:1.7">
No algorithms deciding what you see. No ads. No data harvesting.
Just tools that work for you, run by you, owned by you.
rSpace is infrastructure for communities who refuse to rent their digital commons.
</p>
<p class="rl-subtext">
rSpace is infrastructure for communities who refuse to rent their digital commons
from landlords who read the mail, count the footsteps, and sell the maps.
</p>
<p class="philosophy-punch">
Your space. Your community. Your rules.
</p>
<div class="rl-cta-row">
<a href="/about" class="rl-cta-secondary">Learn More</a>
@ -212,7 +277,7 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
</section>
<!-- Ecosystem Grid (dynamic) -->
<section class="rl-section">
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">${modules.length} rApps and Growing</h2>
<p class="rl-subtext" style="text-align:center">
@ -227,14 +292,25 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<!-- Footer -->
<footer class="main-footer">
<div class="rl-container" style="text-align:center">
<p style="color:#64748b;font-size:0.85rem;margin-bottom:0.5rem">
<div class="footer-quicklinks">
<a href="/rcal">rCal</a>
<a href="/rtasks">rTasks</a>
<a href="/rdocs">rDocs</a>
<a href="/rmaps">rMaps</a>
<a href="/rwallet">rWallet</a>
<a href="/rphotos">rPhotos</a>
<a href="/rflows">rFlows</a>
<a href="/rchoices">rChoices</a>
</div>
<p class="footer-tagline">Local-first &middot; Zero-knowledge &middot; Community-owned</p>
<div class="footer-links">
<a href="/about">About</a>
<a href="/create-space">Create a Space</a>
<a href="https://auth.rspace.online">EncryptID</a>
</div>
<p class="footer-tech">
Built with Bun, Hono, Automerge &amp; WebAuthn
</p>
<div style="display:flex;gap:1.5rem;justify-content:center;flex-wrap:wrap">
<a href="/about" style="color:#94a3b8;font-size:0.8rem;text-decoration:none">About</a>
<a href="/create-space" style="color:#94a3b8;font-size:0.8rem;text-decoration:none">Create a Space</a>
<a href="https://auth.rspace.online" style="color:#94a3b8;font-size:0.8rem;text-decoration:none">EncryptID</a>
</div>
</div>
</footer>
@ -432,7 +508,9 @@ const SPACE_DASHBOARD_CSS = `
`;
const MAIN_LANDING_CSS = `
/* Main landing page extras (on top of rl-* utilities) */
/* ── Main landing page extras (on top of rl-* utilities) ── */
/* Background + diagonal stripe overlay */
body {
background: linear-gradient(
170deg,
@ -445,8 +523,57 @@ body {
);
background-attachment: fixed;
}
body::before {
content: "";
position: fixed; inset: 0;
background: repeating-linear-gradient(
-45deg,
transparent,
transparent 60px,
rgba(20,184,166,0.015) 60px,
rgba(20,184,166,0.015) 61px
);
pointer-events: none;
z-index: 0;
}
[data-theme="light"] body {
background: linear-gradient(170deg, #f8fafc 0%, #f1f5f9 50%, #e2e8f0 100%);
}
[data-theme="light"] body::before {
background: repeating-linear-gradient(
-45deg,
transparent,
transparent 60px,
rgba(20,184,166,0.03) 60px,
rgba(20,184,166,0.03) 61px
);
}
/* ── Hero ── */
.hero-glow-wrap { position: relative; overflow: hidden; }
.hero-glow {
position: absolute;
top: 50%; left: 50%;
width: 600px; height: 600px;
transform: translate(-50%, -60%);
border-radius: 50%;
background: radial-gradient(circle, rgba(20,184,166,0.15) 0%, transparent 70%);
animation: hero-pulse 4s ease-in-out infinite;
pointer-events: none;
z-index: 0;
}
[data-theme="light"] .hero-glow {
background: radial-gradient(circle, rgba(20,184,166,0.1) 0%, transparent 70%);
}
@keyframes hero-pulse {
0%, 100% { opacity: 0.6; transform: translate(-50%, -60%) scale(1); }
50% { opacity: 1; transform: translate(-50%, -60%) scale(1.08); }
}
.rl-hero > * { position: relative; z-index: 1; }
.main-wordmark {
font-size: 3.5rem;
text-shadow: 0 0 40px rgba(20,184,166,0.2);
}
@media (min-width: 640px) { .main-wordmark { font-size: 4.5rem; } }
.main-wordmark__accent {
@ -454,12 +581,218 @@ body {
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.accent-cool {
color: var(--rs-accent);
font-weight: 700;
-webkit-text-fill-color: var(--rs-accent);
}
/* Badge bar with star separators */
.main-badges {
display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap;
align-items: center;
margin-top: 1.5rem;
}
.main-badges .rl-badge {
font-weight: 800;
letter-spacing: 0.04em;
}
.badge-sep {
color: var(--rs-accent);
font-size: 0.5rem;
opacity: 0.6;
}
/* ── Feature Cards (SVG icons, border glow) ── */
.feat-card {
background: var(--rs-card-bg);
border: 2px solid var(--rs-card-border);
border-radius: 1rem;
padding: 1.75rem;
text-align: center;
transition: border-color 0.25s, box-shadow 0.25s, transform 0.2s;
}
.feat-card:hover {
transform: translateY(-2px);
}
.feat-card--teal:hover {
border-color: rgba(20,184,166,0.5);
box-shadow: 0 0 20px rgba(20,184,166,0.12);
}
.feat-card--indigo:hover {
border-color: rgba(99,102,241,0.5);
box-shadow: 0 0 20px rgba(99,102,241,0.12);
}
.feat-card__icon {
width: 3rem; height: 3rem; border-radius: 0.75rem;
display: flex; align-items: center; justify-content: center;
margin: 0 auto 1rem;
font-size: 1.5rem;
}
.feat-card--teal .feat-card__icon {
background: rgba(20,184,166,0.12); color: #14b8a6;
}
.feat-card--indigo .feat-card__icon {
background: rgba(99,102,241,0.12); color: #6366f1;
}
.feat-card h3 {
font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.5rem;
}
.feat-card p {
font-size: 0.875rem; color: var(--rs-text-secondary); line-height: 1.6;
}
/* ── EncryptID Visual ── */
.encryptid-visual {
text-align: center;
}
.encryptid-shield {
width: 140px; height: 160px;
color: var(--rs-accent);
filter: drop-shadow(0 0 20px rgba(20,184,166,0.2));
}
.encryptid-visual__label {
font-size: 0.9rem; font-weight: 600;
color: var(--rs-text-secondary);
margin-top: 1rem;
}
/* ── Offline-First Explainer ── */
.offline-explain {
text-align: center;
font-size: 0.85rem;
color: var(--rs-text-muted);
line-height: 1.65;
max-width: 640px;
margin: 2rem auto 0;
border-top: 1px solid var(--rs-border-subtle);
padding-top: 1.5rem;
}
.offline-explain strong {
color: var(--rs-text-secondary);
font-weight: 600;
}
/* ── Interop Diagram ── */
.interop-diagram {
margin-top: 2rem;
}
.interop-pre {
display: inline-block;
text-align: left;
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
font-size: 0.78rem;
line-height: 1.6;
color: var(--rs-text-secondary);
background: var(--rs-card-bg);
border: 1px solid var(--rs-card-border);
border-radius: 0.75rem;
padding: 1.5rem 2rem;
overflow-x: auto;
max-width: 100%;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
}
[data-theme="light"] .interop-pre {
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
/* ── Philosophy Punch ── */
.philosophy-punch {
font-size: 1.3rem;
font-weight: 700;
background: var(--rs-gradient-brand);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
margin: 1.5rem 0 0;
letter-spacing: -0.01em;
}
/* ── Ecosystem Cards ── */
.eco-card {
background: var(--rs-card-bg);
border: 2px solid var(--rs-card-border);
border-radius: 1rem;
padding: 1.5rem;
text-align: center;
transition: border-color 0.25s, box-shadow 0.25s, transform 0.2s;
display: flex; flex-direction: column; align-items: center;
}
.eco-card:hover {
border-color: rgba(20,184,166,0.4);
box-shadow: 0 0 16px rgba(20,184,166,0.1);
transform: translateY(-2px);
}
.eco-card__icon {
font-size: 1.75rem;
margin-bottom: 0.5rem;
}
.eco-card__name {
font-size: 0.9rem; font-weight: 600;
color: var(--rs-text-primary);
margin: 0 0 0.15rem;
}
.eco-card__domain {
font-size: 0.65rem; font-weight: 600;
color: var(--rs-accent);
opacity: 0.75;
letter-spacing: 0.02em;
margin-bottom: 0.4rem;
display: block;
}
.eco-card__desc {
font-size: 0.78rem; color: var(--rs-text-secondary);
line-height: 1.5; margin: 0;
}
/* ── Footer ── */
.main-footer {
border-top: 1px solid var(--rs-border-subtle);
padding: 2.5rem 1.5rem;
padding: 2.5rem 1.5rem 2rem;
}
.footer-quicklinks {
display: flex; gap: 1.25rem; justify-content: center; flex-wrap: wrap;
margin-bottom: 1.25rem;
}
.footer-quicklinks a {
font-size: 0.75rem; font-weight: 600;
color: var(--rs-text-muted);
text-decoration: none;
transition: color 0.2s;
}
.footer-quicklinks a:hover { color: var(--rs-accent); }
.footer-tagline {
font-size: 0.8rem; font-weight: 700;
color: var(--rs-accent);
letter-spacing: 0.06em;
text-transform: uppercase;
margin: 0 0 1rem;
opacity: 0.7;
}
.footer-links {
display: flex; gap: 1.5rem; justify-content: center; flex-wrap: wrap;
margin-bottom: 1rem;
}
.footer-links a {
color: #94a3b8; font-size: 0.8rem; text-decoration: none;
transition: color 0.2s;
}
.footer-links a:hover { color: var(--rs-text-primary); }
.footer-tech {
color: #64748b; font-size: 0.75rem; margin: 0;
}
/* ── Retro Shadow Utility ── */
.retro-shadow {
box-shadow: 2px 2px 0 var(--rs-card-border);
}
/* ── Responsive ── */
@media (max-width: 480px) {
.main-wordmark { font-size: 2.75rem; }
.hero-glow { width: 350px; height: 350px; }
.interop-pre { font-size: 0.65rem; padding: 1rem; }
.philosophy-punch { font-size: 1.1rem; }
.footer-quicklinks { gap: 0.75rem; }
}
`;