/** * Main landing page for rspace.online/ * * Server-rendered using the same shell (header, CSS, theme) as all module * landing pages. Content is ported from the old static Next.js export and * adapted to use the shared rl-* utility classes. */ import type { ModuleInfo } from "../shared/module"; import { escapeHtml, escapeAttr, MODULE_LANDING_CSS, RICH_LANDING_CSS, versionAssetUrls, getSpaceShellMeta } from "./shell"; /** Category β†’ module IDs mapping for the tabbed showcase. */ const CATEGORY_GROUPS: Record = { plan: { label: "Plan", icon: "πŸ“‹", ids: ["rcal", "rtasks", "rschedule", "rtime"] }, create: { label: "Create", icon: "✏️", ids: ["rspace", "rdocs", "rnotes", "rdesign", "rsplat", "rpubs", "rsheets", "rbooks"] }, communicate: { label: "Communicate", icon: "πŸ’¬", ids: ["rchats", "rforum", "rinbox", "rmeets", "rsocials"] }, govern: { label: "Govern", icon: "βš–οΈ", ids: ["rchoices", "rvote", "rgov", "crowdsurf"] }, transact: { label: "Transact", icon: "πŸ’°", ids: ["rwallet", "rflows", "rexchange", "rauctions", "rcart", "rswag"] }, explore: { label: "Explore", icon: "πŸ—ΊοΈ", ids: ["rmaps", "rtrips", "rbnb", "rvnb"] }, data: { label: "Data", icon: "πŸ“Š", ids: ["rdata", "rfiles", "rphotos", "rtube"] }, identity: { label: "Identity", icon: "πŸͺͺ", ids: ["rnetwork"] }, ai: { label: "AI", icon: "πŸ€–", ids: ["ragents"] }, }; export function renderMainLanding(modules: ModuleInfo[]): string { const moduleListJSON = JSON.stringify(modules); const demoUrl = "https://demo.rspace.online/rspace"; // ── Build categorized app panels ── const allCategorized = new Set(Object.values(CATEGORY_GROUPS).flatMap(g => g.ids)); const uncategorized = modules.filter(m => !allCategorized.has(m.id)); const categoryKeys = Object.keys(CATEGORY_GROUPS); // Add "other" if there are uncategorized modules if (uncategorized.length > 0) { categoryKeys.push("other"); } function renderCard(m: ModuleInfo): string { return ` ${m.icon}
${escapeHtml(m.name)} ${m.standaloneDomain ? `${escapeHtml(m.standaloneDomain)}` : ""} ${escapeHtml(m.description)}
`; } const tabButtons = categoryKeys.map((key, i) => { const g = CATEGORY_GROUPS[key] || { label: "Other", icon: "πŸ“¦" }; return ``; }).join("\n "); const tabPanels = categoryKeys.map((key, i) => { const mods = key === "other" ? uncategorized : (CATEGORY_GROUPS[key]?.ids ?? []) .map(id => modules.find(m => m.id === id)) .filter((m): m is ModuleInfo => m != null); return `
${mods.map(renderCard).join("\n")}
`; }).join("\n "); // ── Footer category columns ── const footerColumns = Object.entries(CATEGORY_GROUPS).map(([, g]) => { const links = g.ids .map(id => modules.find(m => m.id === id)) .filter((m): m is ModuleInfo => m != null) .map(m => `${escapeHtml(m.name)}`) .join("\n "); return ``; }).join("\n "); return versionAssetUrls(` rSpace β€” Own Your Community Infrastructure
Local-first community platform

rSpace

One platform. ${modules.length} apps. All your community’s tools talking to each other.

📡 Offline-first 🔒 Encrypted 👥 Multiplayer 🛠 Open Source
${modules.length} composable apps
1 passkey for everything
0 data sold to anyone

One Platform. Every Tool Connected.

rApps share one sync layer. Data flows automatically — no import/export rituals.

📅 rCal 📋 rTasks 💬 rChats

Schedule a meeting → auto-generates a task → notifies your space.

☑ rChoices 💰 rWallet

Vote passes → budget allocation releases automatically.

🗺 rMaps 📝 rDocs

Pin a community location → shared doc created for that place.

⏱ rTime 📊 rData

Log commitments → analytics update in real-time.

${modules.length} rApps and Growing

Each app is independent and composable. Use one, use all, mix and match.

${tabButtons}
${tabPanels}

Built Offline-First

Your data lives on your device. Changes sync when you’re online, merge automatically when you’re not.

1

Local Persistence

Every document stored in encrypted IndexedDB on your device. Works without internet.

2

Auto-Merge CRDT

Automerge CRDTs resolve conflicts automatically. No “someone else is editing” lockouts.

3

Incremental Sync

Only changed bytes travel the wire. Reconnect after days offline and catch up in seconds.

EncryptID

One Passkey for Everything

Secure by default, not by opt-in.

Sign in once with your fingerprint or device PIN. Your passkey works across every rApp — no passwords, no email loops, no third-party auth providers watching over your shoulder.

  • WebAuthn passkeys — phishing-resistant, device-bound credentials
  • Guardian recovery — 2-of-3 trusted contacts restore access
  • Device linking — scan a QR to add your phone or tablet
  • One RP ID — works across all r*.online domains

Touch. Tap. Done.

Your community. Your rules. Your data.

No algorithms deciding what you see. No ads. No data harvesting. Just tools that work for you, run by you, owned by you.

`); } // ── Space Dashboard ── export function renderSpaceDashboard(space: string, modules: ModuleInfo[]): string { // Filter modules by space's enabledModules const enabledModules = getSpaceShellMeta(space).enabledModules; const visibleModules = enabledModules ? modules.filter(m => m.id === "rspace" || enabledModules.includes(m.id)) : modules; const moduleListJSON = JSON.stringify(visibleModules); const displayName = space === "demo" ? "Demo Space" : space; const subtitle = space === "demo" ? "Explore the rSpace ecosystem β€” click any rApp to try it live with sample data." : `${visibleModules.length} rApps available in this space.`; const appCards = visibleModules .map((m) => { return `
${m.icon}

${escapeHtml(m.name)}

${escapeHtml(m.description)}

`; }) .join("\n"); return versionAssetUrls(` ${escapeHtml(displayName)} | rSpace

${escapeHtml(displayName)}

${subtitle}

${appCards}
${space === "demo" ? ` ` : ""} `); } const SPACE_DASHBOARD_CSS = ` .sd-hero { text-align: center; padding: 100px 1.5rem 2rem; } .sd-hero__title { font-size: 2.25rem; font-weight: 700; margin: 0 0 0.5rem; background: var(--rs-gradient-brand); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .sd-hero__subtitle { font-size: 1.05rem; color: var(--rs-text-secondary); margin: 0; max-width: 520px; margin: 0 auto; line-height: 1.5; } .sd-container { max-width: 1100px; margin: 0 auto; padding: 1rem 1.5rem 3rem; } .sd-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 0.75rem; } .sd-card { display: flex; align-items: flex-start; gap: 1rem; padding: 1rem 1.25rem; background: var(--rs-card-bg); border: 1px solid var(--rs-card-border); border-radius: 0.75rem; text-decoration: none; color: inherit; transition: border-color 0.2s, background 0.2s, transform 0.15s; cursor: pointer; } .sd-card:hover { border-color: rgba(20,184,166,0.35); background: rgba(20,184,166,0.04); transform: translateY(-1px); } .sd-card__icon { font-size: 1.75rem; flex-shrink: 0; width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; } .sd-card__body { min-width: 0; } .sd-card__name { font-size: 0.9rem; font-weight: 600; color: var(--rs-text-primary); margin: 0 0 0.2rem; } .sd-card__desc { font-size: 0.78rem; color: var(--rs-text-muted); margin: 0; line-height: 1.45; } .sd-footer { text-align: center; padding: 2rem 1.5rem 3rem; border-top: 1px solid var(--rs-border-subtle); } .sd-footer p { color: var(--rs-text-muted); font-size: 0.9rem; margin: 0; } .sd-footer a { color: var(--rs-accent); text-decoration: none; font-weight: 600; } .sd-footer a:hover { text-decoration: underline; } @media (max-width: 480px) { .sd-grid { grid-template-columns: 1fr; } .sd-hero__title { font-size: 1.75rem; } .sd-hero { padding-top: 80px; } } `; const MAIN_LANDING_CSS = ` /* ══════════════════════════════════════════════════════════ Landing Page β€” lp-* prefix (no conflicts with rl-*) ══════════════════════════════════════════════════════════ */ /* ── Global background ── */ body { background: linear-gradient(170deg, #0a0f1e 0%, #0f172a 25%, #131b2e 45%, #0e1628 65%, #0d1424 85%, #080d19 100%); background-attachment: fixed; } [data-theme="light"] body { background: linear-gradient(170deg, #f8fafc 0%, #f1f5f9 50%, #e2e8f0 100%); } /* ── 1. Hero ── */ .lp-hero { position: relative; min-height: calc(100vh - 56px); display: flex; align-items: center; justify-content: center; overflow: hidden; padding: 3rem 1.5rem; } .lp-hero__content { position: relative; z-index: 2; text-align: center; max-width: 820px; } /* Animated orbs */ .lp-hero__orb { position: absolute; border-radius: 50%; pointer-events: none; z-index: 0; filter: blur(80px); } .lp-hero__orb--teal { width: 500px; height: 500px; top: 15%; left: 20%; background: radial-gradient(circle, rgba(20,184,166,0.25) 0%, transparent 70%); animation: lp-orb-drift 8s ease-in-out infinite; } .lp-hero__orb--indigo { width: 400px; height: 400px; bottom: 10%; right: 15%; background: radial-gradient(circle, rgba(99,102,241,0.2) 0%, transparent 70%); animation: lp-orb-drift 10s ease-in-out infinite reverse; } [data-theme="light"] .lp-hero__orb--teal { background: radial-gradient(circle, rgba(20,184,166,0.12) 0%, transparent 70%); } [data-theme="light"] .lp-hero__orb--indigo { background: radial-gradient(circle, rgba(99,102,241,0.1) 0%, transparent 70%); } @keyframes lp-orb-drift { 0%, 100% { transform: translate(0, 0) scale(1); } 33% { transform: translate(30px, -20px) scale(1.05); } 66% { transform: translate(-20px, 15px) scale(0.97); } } /* Grid overlay */ .lp-hero__grid { position: absolute; inset: 0; z-index: 1; background: linear-gradient(rgba(20,184,166,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(20,184,166,0.04) 1px, transparent 1px); background-size: 60px 60px; mask-image: radial-gradient(ellipse 60% 50% at 50% 50%, black 20%, transparent 100%); -webkit-mask-image: radial-gradient(ellipse 60% 50% at 50% 50%, black 20%, transparent 100%); } [data-theme="light"] .lp-hero__grid { background: linear-gradient(rgba(20,184,166,0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(20,184,166,0.06) 1px, transparent 1px); background-size: 60px 60px; } /* Wordmark */ .lp-wordmark { font-size: clamp(3.5rem, 8vw, 6rem); font-weight: 700; line-height: 1.05; margin: 0 0 1rem; letter-spacing: -0.02em; text-shadow: 0 0 60px rgba(20,184,166,0.15); } .lp-wordmark__r { font-weight: 400; color: var(--rs-text-primary); -webkit-text-fill-color: unset; } .lp-wordmark__space { background: var(--rs-gradient-brand); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .lp-hero__tagline { font-size: clamp(1.1rem, 2.5vw, 1.4rem); color: var(--rs-text-secondary); margin: 0 0 2rem; line-height: 1.5; } /* CTA buttons */ .lp-hero__ctas { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; margin-bottom: 2.5rem; } .lp-btn { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.85rem 2rem; border-radius: 0.6rem; font-size: 0.95rem; font-weight: 600; text-decoration: none; cursor: pointer; transition: transform 0.2s, box-shadow 0.25s; border: none; } .lp-btn:hover { transform: translateY(-2px); } .lp-btn--primary { background: var(--rs-gradient-cta); color: white; box-shadow: 0 0 20px rgba(20,184,166,0.2), 0 4px 12px rgba(20,184,166,0.15); animation: lp-glow-pulse 3s ease-in-out infinite; } .lp-btn--primary:hover { box-shadow: 0 0 30px rgba(20,184,166,0.35), 0 8px 20px rgba(20,184,166,0.2); } .lp-btn--ghost { background: transparent; color: var(--rs-text-secondary); border: 1px solid var(--rs-border); } .lp-btn--ghost:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); } @keyframes lp-glow-pulse { 0%, 100% { box-shadow: 0 0 20px rgba(20,184,166,0.2), 0 4px 12px rgba(20,184,166,0.15); } 50% { box-shadow: 0 0 30px rgba(20,184,166,0.3), 0 4px 16px rgba(20,184,166,0.2); } } /* Hero badges */ .lp-hero__badges { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; } .lp-badge { font-size: 0.72rem; font-weight: 600; color: var(--rs-text-secondary); background: rgba(20,184,166,0.06); border: 1px solid rgba(20,184,166,0.12); padding: 0.35rem 0.85rem; border-radius: 9999px; white-space: nowrap; } /* ── 2. Stats Bar ── */ .lp-stats { background: rgba(20,184,166,0.03); border-top: 1px solid rgba(20,184,166,0.08); border-bottom: 1px solid rgba(20,184,166,0.08); padding: 1.5rem 1.5rem; } [data-theme="light"] .lp-stats { background: rgba(20,184,166,0.04); } .lp-stats__inner { max-width: 900px; margin: 0 auto; display: flex; justify-content: center; gap: 3rem; flex-wrap: wrap; } .lp-stat { font-size: 0.9rem; color: var(--rs-text-secondary); text-align: center; white-space: nowrap; } .lp-stat__num { font-size: 1.75rem; font-weight: 800; background: var(--rs-gradient-brand); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; display: block; line-height: 1.2; } /* ── 3. Flow Stories ── */ .lp-section-heading { font-size: 2rem; font-weight: 700; text-align: center; margin-bottom: 0.75rem; background: var(--rs-gradient-brand); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; letter-spacing: -0.01em; } .lp-flows { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; margin-top: 1.5rem; } .lp-flow { background: var(--rs-card-bg); border: 1px solid var(--rs-card-border); border-left: 3px solid var(--rs-accent); border-radius: 0.75rem; padding: 1.25rem 1.5rem; transition: border-color 0.25s, box-shadow 0.25s, transform 0.2s; } .lp-flow:hover { border-color: rgba(20,184,166,0.4); border-left-color: var(--rs-accent); box-shadow: 0 0 16px rgba(20,184,166,0.1); transform: translateY(-2px); } .lp-flow__pills { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.6rem; } .lp-flow__pill { font-size: 0.85rem; 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.85rem; white-space: nowrap; } .lp-flow__arrow { color: var(--rs-accent); font-size: 1.1rem; opacity: 0.7; } .lp-flow__outcome { font-size: 0.85rem; color: var(--rs-text-secondary); line-height: 1.5; margin: 0; } /* ── 4. App Categories (tabbed) ── */ .lp-tabs { margin-top: 2rem; } .lp-tab-bar { display: flex; gap: 0.35rem; overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: 0.5rem; -webkit-overflow-scrolling: touch; scrollbar-width: thin; } .lp-tab-bar::-webkit-scrollbar { height: 3px; } .lp-tab-bar::-webkit-scrollbar-thumb { background: var(--rs-border); border-radius: 9999px; } .lp-tab { flex-shrink: 0; scroll-snap-align: start; font-size: 0.8rem; font-weight: 600; color: var(--rs-text-muted); background: transparent; border: 1px solid var(--rs-border-subtle); border-radius: 9999px; padding: 0.45rem 1rem; cursor: pointer; transition: all 0.2s; white-space: nowrap; font-family: inherit; } .lp-tab:hover { color: var(--rs-text-primary); border-color: var(--rs-border); } .lp-tab--active { color: white; background: var(--rs-accent); border-color: var(--rs-accent); } [data-theme="light"] .lp-tab--active { color: white; } .lp-tab-panel { display: none; } .lp-tab-panel--active { display: block; } .lp-tab-panels { margin-top: 1.25rem; } .lp-app-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 0.75rem; } .lp-app-card { display: flex; align-items: flex-start; gap: 0.75rem; padding: 1rem 1.25rem; background: var(--rs-card-bg); border: 1px solid var(--rs-card-border); border-radius: 0.75rem; text-decoration: none; color: inherit; transition: border-color 0.2s, background 0.2s, transform 0.15s; } .lp-app-card:hover { border-color: rgba(20,184,166,0.35); background: rgba(20,184,166,0.04); transform: translateY(-1px); } .lp-app-card__icon { font-size: 1.5rem; flex-shrink: 0; width: 2.25rem; height: 2.25rem; display: flex; align-items: center; justify-content: center; } .lp-app-card__body { min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; } .lp-app-card__name { font-size: 0.88rem; font-weight: 600; color: var(--rs-text-primary); } .lp-app-card__domain { font-size: 0.62rem; font-weight: 600; color: var(--rs-accent); opacity: 0.75; letter-spacing: 0.02em; } .lp-app-card__desc { font-size: 0.76rem; color: var(--rs-text-muted); line-height: 1.45; } /* ── 5. How It Works (steps with dotted line) ── */ .lp-steps { display: flex; align-items: flex-start; justify-content: center; gap: 0; flex-wrap: wrap; margin-top: 2rem; } .lp-step { display: flex; flex-direction: column; align-items: center; text-align: center; flex: 0 1 220px; padding: 0 1rem; } .lp-step__num { width: 2.75rem; height: 2.75rem; border-radius: 9999px; background: rgba(20,184,166,0.1); color: var(--rs-accent); display: flex; align-items: center; justify-content: center; font-size: 1rem; font-weight: 800; margin-bottom: 0.75rem; border: 2px solid rgba(20,184,166,0.2); } .lp-step h3 { font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin: 0 0 0.25rem; } .lp-step p { font-size: 0.82rem; color: var(--rs-text-secondary); line-height: 1.55; margin: 0; } .lp-steps__line { width: 40px; height: 2px; flex-shrink: 0; border-top: 2px dashed rgba(20,184,166,0.25); margin-top: 1.35rem; } /* ── 6. EncryptID Visual ── */ .lp-encryptid-visual { text-align: center; } .lp-encryptid-shield { width: 140px; height: 160px; color: var(--rs-accent); filter: drop-shadow(0 0 20px rgba(20,184,166,0.2)); } .lp-encryptid-visual__label { font-size: 0.9rem; font-weight: 600; color: var(--rs-text-secondary); margin-top: 1rem; } /* ── 7. Final CTA ── */ .lp-final-cta { padding: 5rem 1.5rem; border-top: 1px solid var(--rs-border-subtle); } .lp-final-cta__heading { font-size: clamp(1.5rem, 4vw, 2.25rem); font-weight: 700; margin: 0 0 1rem; background: var(--rs-gradient-brand); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; letter-spacing: -0.01em; } /* ── 8. Footer ── */ .lp-footer { border-top: 1px solid var(--rs-border-subtle); padding: 3rem 1.5rem 2rem; } .lp-footer__cols { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1.5rem 1rem; margin-bottom: 2.5rem; } .lp-footer-col h4 { font-size: 0.72rem; font-weight: 700; color: var(--rs-text-primary); text-transform: uppercase; letter-spacing: 0.06em; margin: 0 0 0.6rem; white-space: nowrap; } .lp-footer-col a { display: block; font-size: 0.75rem; color: var(--rs-text-muted); text-decoration: none; padding: 0.15rem 0; transition: color 0.2s; } .lp-footer-col a:hover { color: var(--rs-accent); } .lp-footer__bottom { border-top: 1px solid var(--rs-border-subtle); padding-top: 1.5rem; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 0.75rem; } .lp-footer__tagline { font-size: 0.8rem; font-weight: 700; color: var(--rs-accent); letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.7; } .lp-footer__links { display: flex; gap: 1.5rem; flex-wrap: wrap; justify-content: center; } .lp-footer__links a { color: var(--rs-text-muted); font-size: 0.8rem; text-decoration: none; transition: color 0.2s; } .lp-footer__links a:hover { color: var(--rs-text-primary); } .lp-footer__tech { color: var(--rs-text-muted); font-size: 0.72rem; opacity: 0.6; } /* ── Reduced Motion ── */ @media (prefers-reduced-motion: reduce) { .lp-hero__orb, .lp-btn--primary { animation: none !important; } .lp-flow, .lp-app-card, .lp-btn { transition: none !important; } } /* ── Responsive ── */ @media (max-width: 640px) { .lp-hero { padding: 2rem 1rem; min-height: calc(100vh - 56px); } .lp-hero__badges { gap: 0.5rem; } .lp-stats__inner { gap: 1.5rem; } .lp-steps { flex-direction: column; align-items: center; } .lp-steps__line { width: 2px; height: 24px; border-top: none; border-left: 2px dashed rgba(20,184,166,0.25); margin: 0.5rem 0; } .lp-step { flex-basis: auto; } .lp-flows { grid-template-columns: 1fr; } .lp-app-grid { grid-template-columns: 1fr; } .lp-footer__cols { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 380px) { .lp-footer__cols { grid-template-columns: repeat(2, 1fr); } } `;