Merge branch 'dev'
CI/CD / deploy (push) Successful in 3m39s Details

This commit is contained in:
Jeff Emmett 2026-04-12 16:19:10 -04:00
commit 01ffe5fef2
2 changed files with 130 additions and 214 deletions

View File

@ -3566,7 +3566,11 @@ app.get("/:space", (c) => {
if (token) {
return c.redirect(`/${space}/rspace`, 302);
}
// Demo space: show dashboard; all others: redirect to main landing
if (space === "demo") {
return c.html(renderSpaceDashboard(space, getModuleInfoList()));
}
return c.redirect("/", 302);
});
// ── WebSocket types ──
@ -3981,10 +3985,14 @@ const server = Bun.serve<WSData>({
if (token) {
return Response.redirect(`${proto}//${url.host}/rspace`, 302);
}
// Demo space: show dashboard; all others: redirect to main landing
if (subdomain === "demo") {
return new Response(renderSpaceDashboard(subdomain, getModuleInfoList()), {
headers: { "Content-Type": "text/html" },
});
}
return Response.redirect(`${proto}//rspace.online/`, 302);
}
// Global routes pass through without subdomain prefix
if (

View File

@ -71,7 +71,6 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<rstack-mi></rstack-mi>
</div>
<div class="rstack-header__right">
<a class="rstack-header__demo-btn" href="${demoUrl}">Try Demo</a>
<rstack-identity></rstack-identity>
</div>
</header>
@ -79,82 +78,112 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<!-- Hero -->
<div class="rl-hero hero-glow-wrap">
<div class="hero-glow"></div>
<span class="rl-tagline">Community Platform</span>
<span class="rl-tagline">The rStack of rApps</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 <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>.
One platform. Every tool your community needs. All talking to each other.
</p>
<p class="rl-subtext">
Build digital spaces to collaborate on improving your physical world.
Local-first, zero-knowledge privacy, outside the walls of big tech.
Data flows between apps automatically &mdash; no import/export rituals.
Local-first, encrypted, and yours.
</p>
<div class="rl-cta-row">
<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>
<a href="#ecosystem" class="rl-cta-secondary">Explore the rApps</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="rl-badge">Multiplayer</span>
<span class="badge-sep" aria-hidden="true">&#9733;</span>
<span class="rl-badge">Encrypted</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>
<!-- Collaboration Features -->
<!-- Interop Flow Cards -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Everything Your Community Needs</h2>
<h2 class="rl-heading" style="text-align:center">One rStack. Every Tool Connected.</h2>
<p class="rl-subtext" style="text-align:center">
Each tool works on its own, and they all work together.
rApps talk to each other through one shared sync layer. Here&rsquo;s what that looks like.
</p>
<div class="flow-cards">
<div class="flow-card">
<div class="flow-card__pills">
<span class="flow-pill">&#128197; rCal</span>
<span class="flow-arrow" aria-hidden="true">&rarr;</span>
<span class="flow-pill">&#128203; rTasks</span>
<span class="flow-arrow" aria-hidden="true">&rarr;</span>
<span class="flow-pill">&#128172; rChats</span>
</div>
<p class="flow-card__desc">Schedule a meeting, auto-generates a task, notifies your space.</p>
</div>
<div class="flow-card">
<div class="flow-card__pills">
<span class="flow-pill">&#9745; rChoices</span>
<span class="flow-arrow" aria-hidden="true">&rarr;</span>
<span class="flow-pill">&#128176; rWallet</span>
</div>
<p class="flow-card__desc">Vote passes, budget allocation releases automatically.</p>
</div>
<div class="flow-card">
<div class="flow-card__pills">
<span class="flow-pill">&#128506; rMaps</span>
<span class="flow-arrow" aria-hidden="true">&rarr;</span>
<span class="flow-pill">&#128221; rDocs</span>
</div>
<p class="flow-card__desc">Pin a community location, shared doc created for that place.</p>
</div>
<div class="flow-card">
<div class="flow-card__pills">
<span class="flow-pill">&#9201; rTime</span>
<span class="flow-arrow" aria-hidden="true">&rarr;</span>
<span class="flow-pill">&#128202; rData</span>
</div>
<p class="flow-card__desc">Log commitments, analytics update in real-time.</p>
</div>
</div>
</div>
</section>
<!-- Ecosystem Grid (dynamic) promoted to section 2 -->
<section id="ecosystem" 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">
Each app is independent and composable. Use one, use all, mix and match.
</p>
<div class="rl-grid-4">
${ecosystemCards}
</div>
</div>
</section>
<!-- Offline-First -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Offline-First, Always</h2>
<p class="rl-subtext" style="text-align:center">
Your data lives on your device. Changes sync when you&rsquo;re online,
merge automatically when you&rsquo;re not.
</p>
<div class="rl-grid-3">
<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 class="rl-step">
<div class="rl-step__num">1</div>
<h3>Local Persistence</h3>
<p>Every document is stored in encrypted IndexedDB on your device. Works without internet.</p>
</div>
<h3>Community Funds</h3>
<p>Budget rivers, revenue splits, and enoughness thresholds. Transparent finances with quadratic funding built in.</p>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Auto-Merge CRDT</h3>
<p>Automerge CRDTs resolve conflicts automatically. No &ldquo;someone else is editing&rdquo; lockouts.</p>
</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="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="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="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="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 class="rl-step">
<div class="rl-step__num">3</div>
<h3>Incremental Sync</h3>
<p>Only changed bytes travel over the wire. Reconnect after days offline and catch up in seconds.</p>
</div>
</div>
</div>
@ -198,75 +227,14 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
</div>
</section>
<!-- Offline-First -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Offline-First, Always</h2>
<p class="rl-subtext" style="text-align:center">
Your data lives on your device. Changes sync when you&rsquo;re online,
merge automatically when you&rsquo;re not.
</p>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Local Persistence</h3>
<p>Every document is stored in encrypted IndexedDB on your device. Works without internet.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Auto-Merge CRDT</h3>
<p>Automerge CRDTs resolve conflicts automatically. No &ldquo;someone else is editing&rdquo; lockouts.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Incremental Sync</h3>
<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">
<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>
<h2 class="rl-heading">Built for Communities, Not Corporations</h2>
<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.
</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>
@ -276,19 +244,6 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
</div>
</section>
<!-- Ecosystem Grid (dynamic) -->
<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">
Each app is independent and composable. Use one, use all, mix and match.
</p>
<div class="rl-grid-4">
${ecosystemCards}
</div>
</div>
</section>
<!-- Footer -->
<footer class="main-footer">
<div class="rl-container" style="text-align:center">
@ -318,14 +273,12 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
import '/shell.js';
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
// Logged-in users: hide header demo btn, swap hero CTA + space switcher
// Logged-in users: swap hero CTA + space switcher
try {
var raw = localStorage.getItem('encryptid_session');
var sw = document.querySelector('rstack-space-switcher');
if (raw) {
var session = JSON.parse(raw);
var hdrBtn = document.querySelector('.rstack-header__demo-btn');
if (hdrBtn) hdrBtn.setAttribute('data-hide', '');
if (session?.claims?.username) {
var username = session.claims.username.toLowerCase();
var primary = document.getElementById('ml-primary');
@ -550,6 +503,7 @@ body::before {
}
/* ── Hero ── */
.rl-hero { padding-top: 2rem; }
.hero-glow-wrap { position: relative; overflow: hidden; }
.hero-glow {
position: absolute;
@ -582,12 +536,6 @@ body::before {
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;
@ -604,43 +552,47 @@ body::before {
opacity: 0.6;
}
/* ── Feature Cards (SVG icons, border glow) ── */
.feat-card {
/* ── Flow Cards (interop demos) ── */
.flow-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.flow-card {
background: var(--rs-card-bg);
border: 2px solid var(--rs-card-border);
border-radius: 1rem;
padding: 1.75rem;
text-align: center;
padding: 1.5rem;
transition: border-color 0.25s, box-shadow 0.25s, transform 0.2s;
}
.feat-card:hover {
.flow-card:hover {
border-color: rgba(20,184,166,0.4);
box-shadow: 0 0 16px rgba(20,184,166,0.1);
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);
.flow-card__pills {
display: flex; align-items: center; gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 0.75rem;
}
.feat-card--indigo:hover {
border-color: rgba(99,102,241,0.5);
box-shadow: 0 0 20px rgba(99,102,241,0.12);
.flow-pill {
font-size: 0.8rem; font-weight: 600;
color: var(--rs-text-primary);
background: rgba(20,184,166,0.08);
border: 1px solid rgba(20,184,166,0.2);
border-radius: 2rem;
padding: 0.3rem 0.75rem;
white-space: nowrap;
}
.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;
.flow-arrow {
color: var(--rs-accent);
font-size: 1rem;
opacity: 0.7;
}
.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;
.flow-card__desc {
font-size: 0.85rem; color: var(--rs-text-secondary);
line-height: 1.5; margin: 0;
}
/* ── EncryptID Visual ── */
@ -658,45 +610,6 @@ body::before {
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;
@ -782,16 +695,11 @@ body::before {
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; }
.flow-cards { grid-template-columns: 1fr; }
.philosophy-punch { font-size: 1.1rem; }
.footer-quicklinks { gap: 0.75rem; }
}