feat: strip demo pages to show just the interactive component

Demo pages now render the same clean shell as regular spaces — just the
<folk-*> component full-page, no marketing wrapper (hero, feature cards,
CTA). Descriptions belong on landing pages, not demos.

- Remove demo branch from 7 module route handlers (rcal, rcart, rfunds,
  rnotes, rtrips, rtube, rvote)
- Delete 7 demo.ts files (~1200 lines of dead markup)
- Remove renderDemoShell() and DEMO_PAGE_CSS from server/shell.ts
- Remove demoPage field from RSpaceModule interface
- Rename top rApp dropdown item from "rSpace" to "rStack"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-01 15:03:46 -08:00
parent 6bd23a6778
commit 3808b51a64
17 changed files with 10 additions and 1204 deletions

View File

@ -1,165 +0,0 @@
/**
* rCal demo page server-rendered HTML body.
*
* Embeds the full <folk-calendar-view space="demo"> component for
* real interactivity (month/week/day views, navigation, lunar overlay,
* source filtering, event modals, keyboard shortcuts) plus showcase
* sections explaining the rCal vision.
*/
const FEATURES = [
{
icon: "\u{1F50D}",
title: "Temporal Zoom",
desc: "Navigate seamlessly from geological eras down to individual minutes. The calendar adapts its grid density and label fidelity at every level.",
},
{
icon: "\u{1F30D}",
title: "Spatial Context",
desc: "Events are location-aware. Zoom the map and the calendar filters to show only events within the visible region.",
},
{
icon: "\u{1F319}",
title: "Lunar Cycles",
desc: "Overlay moon phases, tidal patterns, and seasonal markers. Useful for agriculture, ceremony, and natural rhythm tracking.",
},
{
icon: "\u{1F4C5}",
title: "Multi-Calendar",
desc: "Layer Gregorian, Islamic, Hebrew, Chinese, and custom community calendars. Cross-reference events across time systems.",
},
];
const INTEGRATIONS = [
{ icon: "\u{1F5FA}", name: "rTrips", desc: "Travel itineraries surface as calendar events with departure/arrival times and locations." },
{ icon: "\u{1F30D}", name: "rMaps", desc: "Events appear on the map. Zoom the map and the calendar filters to show only visible events." },
{ icon: "\u{1F465}", name: "rNetwork", desc: "See availability across your network. Coordinate meetings without back-and-forth." },
{ icon: "\u{1F4DD}", name: "rNotes", desc: "Link notes to calendar events. Meeting agendas, daily journals, and retrospective logs." },
{ icon: "\u{1F4B0}", name: "rFunds", desc: "Budget reviews, treasury flows, and governance votes appear on the calendar timeline." },
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Each space has its own calendar layer. Nest calendars across spaces for cross-community coordination." },
];
const ZOOM_LEVELS = [
"Era", "Century", "Decade", "Year", "Quarter",
"Month", "Week", "Day", "Hour", "Minute",
];
export function renderDemo(): string {
return `
<div class="rd-root" style="--rd-accent-from:#6366f1; --rd-accent-to:#a78bfa;">
<!-- Hero -->
<section class="rd-hero">
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(99,102,241,0.1);border:1px solid rgba(99,102,241,0.2);border-radius:9999px;font-size:0.875rem;color:#a5b4fc;font-weight:500;margin-bottom:1.5rem;">
Multi-Dimensional Calendar
</div>
<h1>rCal Demo</h1>
<p class="rd-subtitle">Temporal coordination with lunar cycles, spatial context, and multi-scale zoom</p>
<div class="rd-meta">
<span>\u{1F50D} Temporal Zoom</span>
<span style="color:#475569">|</span>
<span>\u{1F30D} Spatial Context</span>
<span style="color:#475569">|</span>
<span>\u{1F319} Lunar Cycles</span>
<span style="color:#475569">|</span>
<span>\u{1F4C5} Multi-Calendar</span>
</div>
</section>
<!-- Interactive Calendar -->
<section class="rd-section rd-section--narrow">
<div class="rd-card" style="padding:0;overflow:hidden;">
<folk-calendar-view space="demo"></folk-calendar-view>
</div>
<div style="text-align:center;padding:0.75rem 0;font-size:0.8rem;color:#64748b;">
<kbd style="background:#1e293b;padding:2px 6px;border-radius:4px;border:1px solid #334155;font-size:0.75rem;">1</kbd>
<kbd style="background:#1e293b;padding:2px 6px;border-radius:4px;border:1px solid #334155;font-size:0.75rem;">2</kbd>
<kbd style="background:#1e293b;padding:2px 6px;border-radius:4px;border:1px solid #334155;font-size:0.75rem;">3</kbd>
Day / Week / Month &nbsp;\u00B7&nbsp;
<kbd style="background:#1e293b;padding:2px 6px;border-radius:4px;border:1px solid #334155;font-size:0.75rem;">\u2190</kbd>
<kbd style="background:#1e293b;padding:2px 6px;border-radius:4px;border:1px solid #334155;font-size:0.75rem;">\u2192</kbd>
Navigate &nbsp;\u00B7&nbsp;
<kbd style="background:#1e293b;padding:2px 6px;border-radius:4px;border:1px solid #334155;font-size:0.75rem;">L</kbd>
Lunar &nbsp;\u00B7&nbsp;
<kbd style="background:#1e293b;padding:2px 6px;border-radius:4px;border:1px solid #334155;font-size:0.75rem;">T</kbd>
Today
</div>
</section>
<!-- Temporal Zoom Showcase -->
<section class="rd-section rd-section--narrow">
<div class="rd-card" style="padding:1.5rem;">
<h2 style="font-size:1.125rem;font-weight:600;color:#f1f5f9;margin:0 0 0.75rem;display:flex;align-items:center;gap:0.5rem;">
\u{1F50D} Temporal Zoom
</h2>
<p style="font-size:0.875rem;color:#94a3b8;margin:0 0 1rem;line-height:1.5;">
Navigate across 10 temporal granularities. The calendar adapts its grid at each level &mdash;
from geological eras to individual minutes.
</p>
<div style="display:flex;flex-wrap:wrap;gap:0.5rem;">
${ZOOM_LEVELS.map(
(level) => {
const isActive = level === "Month";
return `<div style="
padding:0.5rem 1rem;
border-radius:0.5rem;
font-size:0.8rem;
font-weight:500;
border:1px solid ${isActive ? "rgba(99,102,241,0.4)" : "rgba(51,65,85,0.4)"};
background:${isActive ? "rgba(99,102,241,0.15)" : "rgba(30,41,59,0.5)"};
color:${isActive ? "#818cf8" : "#64748b"};
${isActive ? "box-shadow:0 0 12px rgba(99,102,241,0.2);" : ""}
">${level}${isActive ? " \u25C0" : ""}</div>`;
},
).join("\n ")}
</div>
</div>
</section>
<!-- Core Concepts -->
<section class="rd-section">
<div class="rd-grid rd-grid--2">
${FEATURES.map(
(f) => `
<div class="rd-card" style="padding:1.5rem;">
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- Ecosystem Integrations -->
<section class="rd-section">
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
r* Ecosystem Integrations
</h2>
<div class="rd-grid rd-grid--3">
${INTEGRATIONS.map(
(i) => `
<div class="rd-card" style="padding:1.25rem;">
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- CTA -->
<section class="rd-section rd-section--narrow">
<div class="rd-cta">
<h2>Coordinate in Time & Space</h2>
<p>
rCal layers temporal zoom, spatial context, and lunar cycles into a single calendar.
Plan events that respect natural rhythms and local conditions.
</p>
<a href="/create-space" style="background:linear-gradient(135deg,#6366f1,#a78bfa);box-shadow:0 8px 24px rgba(99,102,241,0.25);">
Create Your Space
</a>
</div>
</section>
</div>`;
}

View File

@ -9,12 +9,11 @@ import { Hono } from "hono";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { sql } from "../../shared/db/pool";
import { renderShell, renderDemoShell } from "../../server/shell";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import { renderDemo } from "./demo";
const routes = new Hono();
@ -377,18 +376,6 @@ routes.get("/api/context/:tool", async (c) => {
// ── Page route ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
if (space === "demo") {
return c.html(renderDemoShell({
title: "rCal Demo — rSpace",
moduleId: "rcal",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
scripts: `<script type="module" src="/modules/rcal/folk-calendar-view.js?v=3"></script>`,
styles: `<link rel="stylesheet" href="/modules/rcal/cal.css?v=2">`,
}));
}
return c.html(renderShell({
title: `${space} — Calendar | rSpace`,
moduleId: "rcal",
@ -409,7 +396,6 @@ export const calModule: RSpaceModule = {
routes,
standaloneDomain: "rcal.online",
landingPage: renderLanding,
demoPage: renderDemo,
feeds: [
{
id: "events",

View File

@ -1,116 +0,0 @@
/**
* rCart demo page server-rendered HTML body.
*
* Embeds the full <folk-cart-shop space="demo"> component for
* real interactivity (catalog browsing, order tracking, filtering)
* plus showcase sections explaining the rCart vision.
*/
const FEATURES = [
{
icon: "\u{1F30D}",
title: "Cosmolocal Fulfillment",
desc: "Orders are matched to the nearest capable print shop or makerspace. Design global, manufacture local.",
},
{
icon: "\u{1F6D2}",
title: "Group Shopping",
desc: "Communities pool resources and split costs. Transparent funding progress for every item in the cart.",
},
{
icon: "\u{1F4B0}",
title: "Revenue Splits",
desc: "Every order automatically splits revenue between provider, creator, and community fund via rFunds flows.",
},
{
icon: "\u{1F4E6}",
title: "Order Tracking",
desc: "Follow orders from pending through production to delivery. Real-time status updates across the community.",
},
];
const INTEGRATIONS = [
{ icon: "\u{1F30A}", name: "rFunds", desc: "Revenue from orders flows through TBFF budget funnels with enoughness thresholds." },
{ icon: "\u{1F3A8}", name: "rDesign", desc: "Design artifacts become print-ready catalog entries with one click." },
{ icon: "\u{1F465}", name: "rNetwork", desc: "Provider registry matches orders to the closest maker in your network." },
{ icon: "\u{1F5FA}", name: "rMaps", desc: "See provider locations and delivery zones on the map." },
{ icon: "\u{1F4DD}", name: "rNotes", desc: "Link product specs, sizing guides, and design notes to catalog items." },
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Each space has its own shop. Nest catalogs across spaces for cross-community commerce." },
];
export function renderDemo(): string {
return `
<div class="rd-root" style="--rd-accent-from:#10b981; --rd-accent-to:#2dd4bf;">
<!-- Hero -->
<section class="rd-hero">
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(16,185,129,0.1);border:1px solid rgba(16,185,129,0.2);border-radius:9999px;font-size:0.875rem;color:#6ee7b7;font-weight:500;margin-bottom:1.5rem;">
Cosmolocal Print-on-Demand
</div>
<h1>rCart Demo</h1>
<p class="rd-subtitle">Group shopping with cosmolocal fulfillment, revenue splits, and transparent funding</p>
<div class="rd-meta">
<span>\u{1F6D2} Catalog & Orders</span>
<span style="color:#475569">|</span>
<span>\u{1F30D} Local Fulfillment</span>
<span style="color:#475569">|</span>
<span>\u{1F4B0} Revenue Splits</span>
<span style="color:#475569">|</span>
<span>\u{1F4E6} Order Tracking</span>
</div>
</section>
<!-- Interactive Shop -->
<section class="rd-section rd-section--narrow">
<div class="rd-card" style="padding:0;overflow:hidden;">
<folk-cart-shop space="demo"></folk-cart-shop>
</div>
</section>
<!-- Core Concepts -->
<section class="rd-section">
<div class="rd-grid rd-grid--2">
${FEATURES.map(
(f) => `
<div class="rd-card" style="padding:1.5rem;">
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- Ecosystem Integrations -->
<section class="rd-section">
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
r* Ecosystem Integrations
</h2>
<div class="rd-grid rd-grid--3">
${INTEGRATIONS.map(
(i) => `
<div class="rd-card" style="padding:1.25rem;">
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- CTA -->
<section class="rd-section rd-section--narrow">
<div class="rd-cta">
<h2>Ready to shop together?</h2>
<p>
rCart gives your community a shared catalog with cosmolocal fulfillment,
transparent funding, and automatic revenue splits.
</p>
<a href="/create-space" style="background:linear-gradient(135deg,#10b981,#059669);box-shadow:0 8px 24px rgba(16,185,129,0.25);">
Create Your First Cart
</a>
</div>
</section>
</div>`;
}

View File

@ -10,13 +10,12 @@ import { Hono } from "hono";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { sql } from "../../shared/db/pool";
import { renderShell, renderDemoShell } from "../../server/shell";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import { depositOrderRevenue } from "./flow";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import { renderDemo } from "./demo";
const routes = new Hono();
@ -443,20 +442,8 @@ routes.post("/api/fulfill/resolve", async (c) => {
// ── Page route: shop ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
if (space === "demo") {
return c.html(renderDemoShell({
title: "rCart Demo — rSpace",
moduleId: "rcart",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
scripts: `<script type="module" src="/modules/rcart/folk-cart-shop.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rcart/cart.css">`,
}));
}
return c.html(renderShell({
title: `Shop | rSpace`,
title: `${space} — Shop | rSpace`,
moduleId: "rcart",
spaceSlug: space,
modules: getModuleInfoList(),
@ -475,7 +462,6 @@ export const cartModule: RSpaceModule = {
routes,
standaloneDomain: "rcart.online",
landingPage: renderLanding,
demoPage: renderDemo,
feeds: [
{
id: "orders",

View File

@ -1,116 +0,0 @@
/**
* rFunds demo page server-rendered HTML body.
*
* Embeds the full <folk-funds-app space="demo" mode="demo"> component
* for real interactivity (flow listing, river visualization, TBFF diagrams)
* plus showcase sections explaining the rFunds vision.
*/
const FEATURES = [
{
icon: "\u{1F30A}",
title: "River Visualization",
desc: "Watch resources flow through animated Sankey rivers. Sources feed into funnels, funnels feed outcomes, and surplus overflows to where it's needed most.",
},
{
icon: "\u{1F4CA}",
title: "TBFF Flows",
desc: "Threshold-Based Funding Flows distribute resources based on enoughness. When a funnel is sufficient, surplus flows to the next highest-need area.",
},
{
icon: "\u{1F4B8}",
title: "Treasury Management",
desc: "Track deposits, withdrawals, and allocations across all community funnels. Transparent financial governance in real time.",
},
{
icon: "\u2696\uFE0F",
title: "Enoughness Layer",
desc: "Each funnel has a sufficiency threshold. Golden glow indicates a funded funnel. Resources keep flowing until everyone has enough.",
},
];
const INTEGRATIONS = [
{ icon: "\u{1F6D2}", name: "rCart", desc: "Order revenue flows through TBFF funnels with automatic creator/provider/community splits." },
{ icon: "\u{1F5F3}", name: "rVote", desc: "Governance votes determine funding priorities and threshold adjustments." },
{ icon: "\u{2708}\uFE0F", name: "rTrips", desc: "Group expenses feed into shared budget flows with per-person tracking." },
{ icon: "\u{1F4DD}", name: "rNotes", desc: "Attach budget rationales, meeting minutes, and audit logs to flows." },
{ icon: "\u{1F4C5}", name: "rCal", desc: "Budget reviews, treasury snapshots, and governance votes on the calendar timeline." },
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Each space has its own treasury. Nest flows across spaces for multi-community coordination." },
];
export function renderDemo(): string {
return `
<div class="rd-root" style="--rd-accent-from:#f59e0b; --rd-accent-to:#10b981;">
<!-- Hero -->
<section class="rd-hero">
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.2);border-radius:9999px;font-size:0.875rem;color:#fcd34d;font-weight:500;margin-bottom:1.5rem;">
Threshold-Based Funding Flows
</div>
<h1>rFunds Demo</h1>
<p class="rd-subtitle">Budget flows, river visualization, and treasury management with enoughness thresholds</p>
<div class="rd-meta">
<span>\u{1F30A} River Visualization</span>
<span style="color:#475569">|</span>
<span>\u{1F4CA} TBFF Flows</span>
<span style="color:#475569">|</span>
<span>\u{1F4B8} Treasury</span>
<span style="color:#475569">|</span>
<span>\u2696\uFE0F Enoughness</span>
</div>
</section>
<!-- Interactive Funds App -->
<section class="rd-section rd-section--narrow">
<div class="rd-card" style="padding:0;overflow:hidden;">
<folk-funds-app space="demo" mode="demo"></folk-funds-app>
</div>
</section>
<!-- Core Concepts -->
<section class="rd-section">
<div class="rd-grid rd-grid--2">
${FEATURES.map(
(f) => `
<div class="rd-card" style="padding:1.5rem;">
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- Ecosystem Integrations -->
<section class="rd-section">
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
r* Ecosystem Integrations
</h2>
<div class="rd-grid rd-grid--3">
${INTEGRATIONS.map(
(i) => `
<div class="rd-card" style="padding:1.25rem;">
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- CTA -->
<section class="rd-section rd-section--narrow">
<div class="rd-cta">
<h2>Design Your Funding Flows</h2>
<p>
rFunds lets your community design transparent budget flows with threshold-based
mechanisms, enoughness scoring, and animated river visualizations.
</p>
<a href="/create-space" style="background:linear-gradient(135deg,#f59e0b,#10b981);box-shadow:0 8px 24px rgba(245,158,11,0.25);">
Create Your Space
</a>
</div>
</section>
</div>`;
}

View File

@ -8,12 +8,11 @@ import { Hono } from "hono";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { sql } from "../../shared/db/pool";
import { renderShell, renderDemoShell } from "../../server/shell";
import { renderShell } from "../../server/shell";
import type { RSpaceModule } from "../../shared/module";
import { getModuleInfoList } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import { renderDemo } from "./demo";
const FLOW_SERVICE_URL = process.env.FLOW_SERVICE_URL || "http://payment-flow:3010";
@ -198,20 +197,8 @@ const fundsStyles = `<link rel="stylesheet" href="/modules/rfunds/funds.css">`;
// Landing page
routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "demo";
if (spaceSlug === "demo") {
return c.html(renderDemoShell({
title: "rFunds Demo — rSpace",
moduleId: "rfunds",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
scripts: `<script type="module" src="/modules/rfunds/folk-funds-app.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rfunds/funds.css">`,
}));
}
return c.html(renderShell({
title: `rFunds — TBFF Flow Funding | rSpace`,
title: `${spaceSlug} — Funds | rSpace`,
moduleId: "rfunds",
spaceSlug,
modules: getModuleInfoList(),
@ -260,7 +247,6 @@ export const fundsModule: RSpaceModule = {
description: "Budget flows, river visualization, and treasury management",
routes,
landingPage: renderLanding,
demoPage: renderDemo,
standaloneDomain: "rfunds.online",
feeds: [
{

View File

@ -1,116 +0,0 @@
/**
* rNotes demo page server-rendered HTML body.
*
* Embeds the full <folk-notes-app space="demo"> component for
* real interactivity (notebook browsing, note editing, search, tags)
* plus showcase sections explaining the rNotes vision.
*/
const FEATURES = [
{
icon: "\u{1F3A4}",
title: "Live Transcription",
desc: "Record and transcribe in real time. Stream audio via WebSocket or transcribe offline with Parakeet.js.",
},
{
icon: "\u270F\uFE0F",
title: "Rich Editing",
desc: "Headings, lists, code blocks, highlights, images, and file attachments in every note.",
},
{
icon: "\u{1F4D3}",
title: "Notebooks",
desc: "Organize notes into notebooks with sections. Nest as deep as you need for any project structure.",
},
{
icon: "\u{1F3F7}\uFE0F",
title: "Flexible Tags",
desc: "Cross-cutting tags let you find notes across all notebooks instantly. Filter and search by any combination.",
},
];
const INTEGRATIONS = [
{ icon: "\u{1F4C5}", name: "rCal", desc: "Link notes to calendar events. Meeting agendas, daily journals, and retrospective logs." },
{ icon: "\u{1F5FA}", name: "rMaps", desc: "Pin location-aware notes to places on the map. Field notes, venue reviews, site reports." },
{ icon: "\u{1F465}", name: "rNetwork", desc: "Collaborate on notes across your network with real-time Automerge sync." },
{ icon: "\u{1F3AC}", name: "rTube", desc: "Attach meeting notes, transcripts, and timestamps to video recordings." },
{ icon: "\u{1F5F3}", name: "rVote", desc: "Link governance proposals to supporting research notes and discussion threads." },
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Pin any note to the collaborative canvas. Each space has its own knowledge base." },
];
export function renderDemo(): string {
return `
<div class="rd-root" style="--rd-accent-from:#f59e0b; --rd-accent-to:#fb923c;">
<!-- Hero -->
<section class="rd-hero">
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.2);border-radius:9999px;font-size:0.875rem;color:#fbbf24;font-weight:500;margin-bottom:1.5rem;">
Collaborative Knowledge Base
</div>
<h1>rNotes Demo</h1>
<p class="rd-subtitle">Notebooks with rich-text notes, voice transcription, and real-time collaboration</p>
<div class="rd-meta">
<span>\u{1F3A4} Transcription</span>
<span style="color:#475569">|</span>
<span>\u270F\uFE0F Rich Editing</span>
<span style="color:#475569">|</span>
<span>\u{1F4D3} Notebooks</span>
<span style="color:#475569">|</span>
<span>\u{1F3F7}\uFE0F Tags & Search</span>
</div>
</section>
<!-- Interactive Notes App -->
<section class="rd-section rd-section--narrow">
<div class="rd-card" style="padding:0;overflow:hidden;">
<folk-notes-app space="demo"></folk-notes-app>
</div>
</section>
<!-- Core Concepts -->
<section class="rd-section">
<div class="rd-grid rd-grid--2">
${FEATURES.map(
(f) => `
<div class="rd-card" style="padding:1.5rem;">
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- Ecosystem Integrations -->
<section class="rd-section">
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
r* Ecosystem Integrations
</h2>
<div class="rd-grid rd-grid--3">
${INTEGRATIONS.map(
(i) => `
<div class="rd-card" style="padding:1.25rem;">
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- CTA -->
<section class="rd-section rd-section--narrow">
<div class="rd-cta">
<h2>Ready to capture everything?</h2>
<p>
rNotes gives your team a shared knowledge base with rich editing, flexible organization,
and deep integration with the r* ecosystem &mdash; all on a collaborative canvas.
</p>
<a href="/create-space" style="background:linear-gradient(135deg,#f59e0b,#f97316);box-shadow:0 8px 24px rgba(245,158,11,0.25);">
Start Taking Notes
</a>
</div>
</section>
</div>`;
}

View File

@ -9,12 +9,11 @@ import { Hono } from "hono";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { sql } from "../../shared/db/pool";
import { renderShell, renderDemoShell } from "../../server/shell";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import { renderDemo } from "./demo";
const routes = new Hono();
@ -363,18 +362,6 @@ routes.delete("/api/notes/:id", async (c) => {
// ── Page route ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
if (space === "demo") {
return c.html(renderDemoShell({
title: "rNotes Demo — rSpace",
moduleId: "rnotes",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
scripts: `<script type="module" src="/modules/rnotes/folk-notes-app.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rnotes/notes.css">`,
}));
}
return c.html(renderShell({
title: `${space} — Notes | rSpace`,
moduleId: "rnotes",
@ -394,7 +381,6 @@ export const notesModule: RSpaceModule = {
description: "Notebooks with rich-text notes, voice transcription, and collaboration",
routes,
landingPage: renderLanding,
demoPage: renderDemo,
standaloneDomain: "rnotes.online",
feeds: [
{

View File

@ -1,116 +0,0 @@
/**
* rTrips demo page server-rendered HTML body.
*
* Embeds the full <folk-trips-planner space="demo"> component for
* real interactivity (trip list, destinations, itinerary, bookings,
* expenses, packing lists) plus showcase sections explaining the rTrips vision.
*/
const FEATURES = [
{
icon: "\u{1F5FA}",
title: "Destinations",
desc: "Pin destinations on the map with arrival/departure dates, country info, and notes. Reorder your route with drag and drop.",
},
{
icon: "\u{1F4C5}",
title: "Itinerary",
desc: "Plan day-by-day activities grouped by date. Categories include hiking, dining, sightseeing, transit, and more.",
},
{
icon: "\u{1F4B0}",
title: "Expense Splitting",
desc: "Track group expenses with automatic per-person splits. See who paid what and who owes whom.",
},
{
icon: "\u{1F392}",
title: "Packing Lists",
desc: "Collaborative packing checklists organized by category. Check items off as you pack — synced in real time.",
},
];
const INTEGRATIONS = [
{ icon: "\u{1F5FA}", name: "rMaps", desc: "Destinations and routes appear on the interactive map with pins and driving directions." },
{ icon: "\u{1F4C5}", name: "rCal", desc: "Trip dates, activities, and bookings sync to the community calendar." },
{ icon: "\u{1F30A}", name: "rFunds", desc: "Group expenses feed into shared budget flows with threshold-based splits." },
{ icon: "\u{1F5F3}", name: "rVote", desc: "Vote on daily activities, restaurants, and route decisions as a group." },
{ icon: "\u{1F4DD}", name: "rNotes", desc: "Attach travel journals, packing tips, and logistics notes to the trip." },
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Each trip lives on its own canvas with maps, notes, polls, and expenses connected." },
];
export function renderDemo(): string {
return `
<div class="rd-root" style="--rd-accent-from:#14b8a6; --rd-accent-to:#06b6d4;">
<!-- Hero -->
<section class="rd-hero">
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(20,184,166,0.1);border:1px solid rgba(20,184,166,0.2);border-radius:9999px;font-size:0.875rem;color:#5eead4;font-weight:500;margin-bottom:1.5rem;">
Collaborative Trip Planner
</div>
<h1>rTrips Demo</h1>
<p class="rd-subtitle">Plan trips together with destinations, itinerary, bookings, expenses, and packing lists</p>
<div class="rd-meta">
<span>\u{1F5FA} Destinations</span>
<span style="color:#475569">|</span>
<span>\u{1F4C5} Itinerary</span>
<span style="color:#475569">|</span>
<span>\u{1F4B0} Expenses</span>
<span style="color:#475569">|</span>
<span>\u{1F392} Packing</span>
</div>
</section>
<!-- Interactive Trips Planner -->
<section class="rd-section rd-section--narrow">
<div class="rd-card" style="padding:0;overflow:hidden;">
<folk-trips-planner space="demo"></folk-trips-planner>
</div>
</section>
<!-- Core Concepts -->
<section class="rd-section">
<div class="rd-grid rd-grid--2">
${FEATURES.map(
(f) => `
<div class="rd-card" style="padding:1.5rem;">
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- Ecosystem Integrations -->
<section class="rd-section">
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
r* Ecosystem Integrations
</h2>
<div class="rd-grid rd-grid--3">
${INTEGRATIONS.map(
(i) => `
<div class="rd-card" style="padding:1.25rem;">
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- CTA -->
<section class="rd-section rd-section--narrow">
<div class="rd-cta">
<h2>Plan Your Next Adventure</h2>
<p>
rTrips gives your group everything you need &mdash; routes, schedules, polls,
shared expenses, and packing lists &mdash; all connected in one trip canvas.
</p>
<a href="/create-space" style="background:linear-gradient(135deg,#14b8a6,#06b6d4);box-shadow:0 8px 24px rgba(20,184,166,0.25);">
Start Planning
</a>
</div>
</section>
</div>`;
}

View File

@ -9,12 +9,11 @@ import { Hono } from "hono";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { sql } from "../../shared/db/pool";
import { renderShell, renderDemoShell } from "../../server/shell";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import { renderDemo } from "./demo";
const OSRM_URL = process.env.OSRM_URL || "http://osrm-backend:5000";
@ -255,18 +254,6 @@ routes.get("/routes", (c) => {
// ── Page route ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
if (space === "demo") {
return c.html(renderDemoShell({
title: "rTrips Demo — rSpace",
moduleId: "rtrips",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
scripts: `<script type="module" src="/modules/rtrips/folk-trips-planner.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rtrips/trips.css">`,
}));
}
return c.html(renderShell({
title: `${space} — Trips | rSpace`,
moduleId: "rtrips",
@ -286,7 +273,6 @@ export const tripsModule: RSpaceModule = {
description: "Collaborative trip planner with itinerary, bookings, and expense splitting",
routes,
landingPage: renderLanding,
demoPage: renderDemo,
standaloneDomain: "rtrips.online",
feeds: [
{

View File

@ -1,116 +0,0 @@
/**
* rTube demo page server-rendered HTML body.
*
* Embeds the full <folk-video-player space="demo"> component for
* real interactivity (video library, search, playback, live streaming)
* plus showcase sections explaining the rTube vision.
*/
const FEATURES = [
{
icon: "\u{1F3AC}",
title: "Video Library",
desc: "Browse, search, and play videos from your community's R2-backed storage. Supports MP4, WebM, MOV, and more.",
},
{
icon: "\u{1F4E1}",
title: "Live Streaming",
desc: "Broadcast live via RTMP from OBS Studio or any streaming software. Viewers watch in real-time with HLS playback.",
},
{
icon: "\u{1F4E4}",
title: "Easy Uploads",
desc: "Authenticated members upload videos directly. Files stream to Cloudflare R2 with automatic format detection.",
},
{
icon: "\u{1F517}",
title: "Direct Links",
desc: "Copy shareable links to any video. HTTP range requests enable efficient streaming and seeking.",
},
];
const INTEGRATIONS = [
{ icon: "\u{1F4DD}", name: "rNotes", desc: "Attach meeting notes, transcripts, and timestamps to video recordings." },
{ icon: "\u{1F4C5}", name: "rCal", desc: "Scheduled recordings and live streams appear on the community calendar." },
{ icon: "\u{1F465}", name: "rNetwork", desc: "Share videos across your network. Collaborative viewing and commenting." },
{ icon: "\u{1F4DA}", name: "rBooks", desc: "Embed video content in publications and educational materials." },
{ icon: "\u{1F5FA}", name: "rMaps", desc: "Geotagged videos appear on the map at their recording location." },
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Each space has its own video library. Pin videos to the collaborative canvas." },
];
export function renderDemo(): string {
return `
<div class="rd-root" style="--rd-accent-from:#ef4444; --rd-accent-to:#ec4899;">
<!-- Hero -->
<section class="rd-hero">
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.2);border-radius:9999px;font-size:0.875rem;color:#fca5a5;font-weight:500;margin-bottom:1.5rem;">
Community Video Hosting
</div>
<h1>rTube Demo</h1>
<p class="rd-subtitle">Video library, live streaming, and uploads powered by Cloudflare R2</p>
<div class="rd-meta">
<span>\u{1F3AC} Video Library</span>
<span style="color:#475569">|</span>
<span>\u{1F4E1} Live Streaming</span>
<span style="color:#475569">|</span>
<span>\u{1F4E4} Uploads</span>
<span style="color:#475569">|</span>
<span>\u{1F517} Direct Links</span>
</div>
</section>
<!-- Interactive Video Player -->
<section class="rd-section rd-section--narrow">
<div class="rd-card" style="padding:0;overflow:hidden;">
<folk-video-player space="demo"></folk-video-player>
</div>
</section>
<!-- Core Concepts -->
<section class="rd-section">
<div class="rd-grid rd-grid--2">
${FEATURES.map(
(f) => `
<div class="rd-card" style="padding:1.5rem;">
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- Ecosystem Integrations -->
<section class="rd-section">
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
r* Ecosystem Integrations
</h2>
<div class="rd-grid rd-grid--3">
${INTEGRATIONS.map(
(i) => `
<div class="rd-card" style="padding:1.25rem;">
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- CTA -->
<section class="rd-section rd-section--narrow">
<div class="rd-cta">
<h2>Host Your Video Library</h2>
<p>
rTube gives your community private video hosting with streaming,
uploads, and live broadcasting &mdash; all powered by Cloudflare R2.
</p>
<a href="/create-space" style="background:linear-gradient(135deg,#ef4444,#ec4899);box-shadow:0 8px 24px rgba(239,68,68,0.25);">
Create Your Space
</a>
</div>
</section>
</div>`;
}

View File

@ -6,12 +6,11 @@
*/
import { Hono } from "hono";
import { renderShell, renderDemoShell } from "../../server/shell";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import { renderDemo } from "./demo";
import { S3Client, ListObjectsV2Command, GetObjectCommand, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
const routes = new Hono();
@ -193,18 +192,6 @@ routes.get("/api/health", (c) => c.json({ ok: true }));
// ── Page route ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
if (space === "demo") {
return c.html(renderDemoShell({
title: "rTube Demo — rSpace",
moduleId: "rtube",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
scripts: `<script type="module" src="/modules/rtube/folk-video-player.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rtube/tube.css">`,
}));
}
return c.html(renderShell({
title: `${space} — Tube | rSpace`,
moduleId: "rtube",
@ -224,7 +211,6 @@ export const tubeModule: RSpaceModule = {
description: "Community video hosting & live streaming",
routes,
landingPage: renderLanding,
demoPage: renderDemo,
standaloneDomain: "rtube.online",
feeds: [
{

View File

@ -1,116 +0,0 @@
/**
* rVote demo page server-rendered HTML body.
*
* Embeds the full <folk-vote-dashboard space="demo"> component for
* real interactivity (browse spaces, create proposals, cast conviction
* votes, binary final votes) plus showcase sections explaining the rVote vision.
*/
const FEATURES = [
{
icon: "\u{1F4CA}",
title: "Conviction Voting",
desc: "Stake credits on proposals you support. Weight costs credits quadratically (weight\u00B2), preventing plutocratic capture while rewarding conviction.",
},
{
icon: "\u{1F3AF}",
title: "Promotion Threshold",
desc: "Proposals accumulate conviction score. When they hit the threshold, they auto-promote to a binary YES/NO/ABSTAIN final vote.",
},
{
icon: "\u23F3",
title: "Vote Decay",
desc: "Conviction decays after 30 days. This ensures ongoing relevance — stale votes fade, keeping governance dynamic and current.",
},
{
icon: "\u{1F3DB}\uFE0F",
title: "Governance Spaces",
desc: "Each community gets its own voting space with configurable thresholds, credit budgets, and voting periods.",
},
];
const INTEGRATIONS = [
{ icon: "\u{1F30A}", name: "rFunds", desc: "Passed proposals trigger funding flows. Vote on budget allocations and threshold adjustments." },
{ icon: "\u{1F4DD}", name: "rNotes", desc: "Link supporting research, discussion threads, and rationale documents to proposals." },
{ icon: "\u{1F4C5}", name: "rCal", desc: "Voting deadlines, governance meetings, and proposal review periods on the calendar." },
{ icon: "\u{1F465}", name: "rNetwork", desc: "Delegate voting power to trusted network members. Liquid democracy across communities." },
{ icon: "\u{1F6D2}", name: "rCart", desc: "Vote on merchandise decisions, provider selection, and catalog curation." },
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Each space has its own governance layer. Nest voting across spaces for multi-community decisions." },
];
export function renderDemo(): string {
return `
<div class="rd-root" style="--rd-accent-from:#f97316; --rd-accent-to:#fb923c;">
<!-- Hero -->
<section class="rd-hero">
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(249,115,22,0.1);border:1px solid rgba(249,115,22,0.2);border-radius:9999px;font-size:0.875rem;color:#fdba74;font-weight:500;margin-bottom:1.5rem;">
Conviction Voting Engine
</div>
<h1>rVote Demo</h1>
<p class="rd-subtitle">Credit-weighted conviction voting for collaborative governance decisions</p>
<div class="rd-meta">
<span>\u{1F4CA} Conviction Voting</span>
<span style="color:#475569">|</span>
<span>\u{1F3AF} Thresholds</span>
<span style="color:#475569">|</span>
<span>\u23F3 Vote Decay</span>
<span style="color:#475569">|</span>
<span>\u{1F3DB}\uFE0F Governance</span>
</div>
</section>
<!-- Interactive Vote Dashboard -->
<section class="rd-section rd-section--narrow">
<div class="rd-card" style="padding:0;overflow:hidden;">
<folk-vote-dashboard space="demo"></folk-vote-dashboard>
</div>
</section>
<!-- Core Concepts -->
<section class="rd-section">
<div class="rd-grid rd-grid--2">
${FEATURES.map(
(f) => `
<div class="rd-card" style="padding:1.5rem;">
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- Ecosystem Integrations -->
<section class="rd-section">
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
r* Ecosystem Integrations
</h2>
<div class="rd-grid rd-grid--3">
${INTEGRATIONS.map(
(i) => `
<div class="rd-card" style="padding:1.25rem;">
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- CTA -->
<section class="rd-section rd-section--narrow">
<div class="rd-cta">
<h2>Govern Together</h2>
<p>
rVote brings conviction-weighted governance to your community.
Proposals rise through collective conviction, not majority rule.
</p>
<a href="/create-space" style="background:linear-gradient(135deg,#f97316,#fb923c);box-shadow:0 8px 24px rgba(249,115,22,0.25);">
Create Your Space
</a>
</div>
</section>
</div>`;
}

View File

@ -9,9 +9,8 @@ import { Hono } from "hono";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { sql } from "../../shared/db/pool";
import { renderShell, renderDemoShell } from "../../server/shell";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import { renderDemo } from "./demo";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
@ -329,18 +328,6 @@ routes.post("/api/proposals/:id/final-vote", async (c) => {
// ── Page route ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
if (space === "demo") {
return c.html(renderDemoShell({
title: "rVote Demo — rSpace",
moduleId: "rvote",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
scripts: `<script type="module" src="/modules/rvote/folk-vote-dashboard.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rvote/vote.css">`,
}));
}
return c.html(renderShell({
title: `${space} — Vote | rSpace`,
moduleId: "rvote",
@ -361,7 +348,6 @@ export const voteModule: RSpaceModule = {
routes,
standaloneDomain: "rvote.online",
landingPage: renderLanding,
demoPage: renderDemo,
feeds: [
{
id: "proposals",

View File

@ -645,18 +645,6 @@ const WELCOME_CSS = `
`;
/**
* Render a demo page shell: standard shell + DEMO_PAGE_CSS + demo scripts.
* Used by modules that have a demoPage() renderer.
*/
export function renderDemoShell(opts: ShellOptions & { demoScripts?: string }): string {
return renderShell({
...opts,
styles: `${opts.styles || ""}\n<style>${DEMO_PAGE_CSS}</style>`,
scripts: `${opts.scripts || ""}\n${opts.demoScripts || ""}`,
});
}
// ── Module landing page (bare-domain rspace.online/{moduleId}) ──
export interface ModuleLandingOptions {
@ -982,227 +970,6 @@ export const RICH_LANDING_CSS = `
// ── Demo page CSS utilities (rd-* prefix, parallel to rl-* landing pages) ──
export const DEMO_PAGE_CSS = `
/* ── Demo page base ── */
.rd-root {
min-height: 100vh;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
color: white; padding-bottom: 2rem;
}
.rd-hero {
max-width: 48rem; margin: 0 auto; text-align: center;
padding: 3rem 1.5rem 2rem;
}
.rd-hero h1 {
font-size: 2.25rem; font-weight: 700; margin-bottom: 1rem;
background: linear-gradient(135deg, var(--rd-accent-from, #14b8a6), var(--rd-accent-to, #22d3ee));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
@media (min-width: 640px) { .rd-hero h1 { font-size: 3rem; } }
.rd-hero .rd-subtitle { font-size: 1.1rem; color: #cbd5e1; margin-bottom: 0.5rem; }
.rd-hero .rd-meta { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 1rem; font-size: 0.875rem; color: #94a3b8; margin-bottom: 1.5rem; }
.rd-hero .rd-meta span:not(:last-child)::after { content: ""; }
/* Avatars */
.rd-avatars { display: flex; align-items: center; justify-content: center; gap: 0.5rem; }
.rd-avatar {
width: 2.5rem; height: 2.5rem; border-radius: 9999px;
display: flex; align-items: center; justify-content: center;
font-size: 0.875rem; font-weight: 700; color: white;
box-shadow: 0 0 0 2px #1e293b;
}
.rd-avatars .rd-count { font-size: 0.875rem; color: #94a3b8; margin-left: 0.5rem; }
/* Cards */
.rd-card {
background: rgba(30,41,59,0.5); border-radius: 1rem;
border: 1px solid rgba(51,65,85,0.5); overflow: hidden;
}
.rd-card-header {
display: flex; align-items: center; justify-content: space-between;
padding: 0.75rem 1.25rem; border-bottom: 1px solid rgba(51,65,85,0.5);
}
.rd-card-header .rd-card-title { display: flex; align-items: center; gap: 0.5rem; font-weight: 600; font-size: 0.875rem; }
.rd-card-header .rd-card-title .rd-icon { font-size: 1.25rem; }
.rd-card-header .rd-open-link {
font-size: 0.75rem; padding: 0.375rem 0.75rem;
background: rgba(51,65,85,0.6); border-radius: 0.5rem;
color: #cbd5e1; text-decoration: none; transition: all 0.15s;
}
.rd-card-header .rd-open-link:hover { background: rgba(71,85,105,0.6); color: white; }
.rd-card-body { padding: 1.25rem; }
/* Live badge */
.rd-live {
display: inline-flex; align-items: center; gap: 0.375rem;
font-size: 0.75rem; color: #34d399;
}
.rd-live::before {
content: ""; width: 0.375rem; height: 0.375rem; border-radius: 9999px;
background: #34d399; animation: rd-pulse 2s ease-in-out infinite;
}
@keyframes rd-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
/* Status badge */
.rd-status {
display: inline-flex; align-items: center; gap: 0.375rem;
font-size: 0.75rem; padding: 0.25rem 0.625rem; border-radius: 9999px;
}
.rd-status--connected { color: #34d399; background: rgba(52,211,153,0.1); border: 1px solid rgba(52,211,153,0.2); }
.rd-status--disconnected { color: #f87171; background: rgba(248,113,113,0.1); border: 1px solid rgba(248,113,113,0.2); }
.rd-status::before {
content: ""; width: 0.5rem; height: 0.5rem; border-radius: 9999px;
}
.rd-status--connected::before { background: #34d399; }
.rd-status--disconnected::before { background: #f87171; }
/* Progress bar */
.rd-progress { height: 0.75rem; border-radius: 9999px; background: rgba(51,65,85,1); overflow: hidden; }
.rd-progress--sm { height: 0.375rem; }
.rd-progress--xs { height: 0.25rem; }
.rd-progress__fill {
height: 100%; border-radius: 9999px; transition: width 0.3s ease-out;
background: linear-gradient(90deg, var(--rd-accent-from, #14b8a6), var(--rd-accent-to, #2dd4bf));
}
.rd-progress__fill--emerald { background: #10b981; }
.rd-progress__fill--sky { background: #0ea5e9; }
.rd-progress__fill--amber { background: #f59e0b; }
.rd-progress__fill--rose { background: #f43f5e; }
.rd-progress__fill--orange { background: linear-gradient(90deg, #fb923c, #f97316); }
.rd-progress__fill--teal { background: linear-gradient(90deg, #14b8a6, #2dd4bf); }
.rd-progress__fill--cyan { background: #06b6d4; }
.rd-progress__fill--violet { background: #8b5cf6; }
/* Grid */
.rd-grid { display: grid; gap: 1rem; }
.rd-grid--2 { grid-template-columns: 1fr; }
.rd-grid--3 { grid-template-columns: 1fr; }
.rd-grid--4 { grid-template-columns: 1fr 1fr; }
@media (min-width: 640px) {
.rd-grid--2 { grid-template-columns: repeat(2, 1fr); }
.rd-grid--3 { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 768px) {
.rd-grid--3 { grid-template-columns: repeat(3, 1fr); }
.rd-grid--4 { grid-template-columns: repeat(4, 1fr); }
}
/* Section */
.rd-section { max-width: 72rem; margin: 0 auto; padding: 0 1.5rem 1.5rem; }
.rd-section--narrow { max-width: 64rem; }
/* Badge */
.rd-badge {
display: inline-block; font-size: 0.75rem; font-weight: 500;
padding: 0.125rem 0.625rem; border-radius: 9999px;
}
.rd-badge--emerald { background: rgba(16,185,129,0.2); color: #6ee7b7; }
.rd-badge--sky { background: rgba(14,165,233,0.2); color: #7dd3fc; }
.rd-badge--amber { background: rgba(245,158,11,0.2); color: #fcd34d; }
.rd-badge--rose { background: rgba(244,63,94,0.2); color: #fda4af; }
.rd-badge--orange { background: rgba(249,115,22,0.2); color: #fdba74; }
.rd-badge--teal { background: rgba(20,184,166,0.2); color: #5eead4; }
.rd-badge--slate { background: rgba(100,116,139,0.2); color: #94a3b8; }
/* Stat box */
.rd-stat { background: rgba(51,65,85,0.3); border-radius: 0.75rem; padding: 1rem; text-align: center; }
.rd-stat__value { font-size: 1.5rem; font-weight: 700; color: white; }
.rd-stat__label { font-size: 0.75rem; color: #94a3b8; margin-top: 0.25rem; }
.rd-stat__sub { font-size: 0.75rem; color: #64748b; margin-top: 0.125rem; }
/* Button */
.rd-btn {
display: inline-flex; align-items: center; gap: 0.5rem;
padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.875rem;
font-weight: 500; cursor: pointer; border: none; transition: all 0.15s;
}
.rd-btn--ghost { background: rgba(51,65,85,0.6); color: #cbd5e1; }
.rd-btn--ghost:hover { background: rgba(71,85,105,0.6); color: white; }
.rd-btn--ghost:disabled { opacity: 0.5; cursor: not-allowed; }
.rd-btn--primary { color: white; }
/* Divider row */
.rd-row {
display: flex; align-items: center; justify-content: space-between;
padding: 0.75rem 1.25rem; border-top: 1px solid rgba(51,65,85,0.5);
}
/* Item row (expense/cart list item) */
.rd-item {
display: flex; align-items: center; gap: 1rem;
padding: 0.75rem 1.25rem; transition: background 0.1s;
}
.rd-item:hover { background: rgba(51,65,85,0.2); }
.rd-item + .rd-item { border-top: 1px solid rgba(51,65,85,0.3); }
/* Checkbox */
.rd-checkbox {
width: 1.25rem; height: 1.25rem; border-radius: 0.25rem; flex-shrink: 0;
border: 2px solid #475569; display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.15s;
}
.rd-checkbox--checked { background: var(--rd-accent-from, #14b8a6); border-color: var(--rd-accent-from, #14b8a6); }
.rd-checkbox svg { width: 0.75rem; height: 0.75rem; color: white; }
.rd-checkbox:hover { border-color: #64748b; }
/* CTA section */
.rd-cta {
background: rgba(30,41,59,0.5); border-radius: 1rem;
border: 1px solid rgba(51,65,85,0.5); padding: 2.5rem;
text-align: center; margin-top: 1rem;
}
.rd-cta h2 { font-size: 1.875rem; font-weight: 700; margin-bottom: 0.75rem; }
.rd-cta p { color: #94a3b8; max-width: 32rem; margin: 0 auto 1.5rem; font-size: 0.875rem; }
.rd-cta a {
display: inline-block; padding: 0.875rem 2rem; border-radius: 0.75rem;
font-size: 1.1rem; font-weight: 500; color: white; text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
}
.rd-cta a:hover { transform: translateY(-2px); }
/* Text helpers */
.rd-text-muted { color: #94a3b8; }
.rd-text-dim { color: #64748b; }
.rd-text-sm { font-size: 0.875rem; }
.rd-text-xs { font-size: 0.75rem; }
.rd-text-center { text-align: center; }
.rd-font-bold { font-weight: 700; }
.rd-font-semibold { font-weight: 600; }
.rd-font-medium { font-weight: 500; }
.rd-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rd-line-through { text-decoration: line-through; }
.rd-hidden { display: none !important; }
/* Color helpers */
.rd-emerald { color: #34d399; }
.rd-rose { color: #fb7185; }
.rd-amber { color: #fbbf24; }
.rd-cyan { color: #22d3ee; }
.rd-teal { color: #2dd4bf; }
.rd-orange { color: #fb923c; }
.rd-sky { color: #38bdf8; }
.rd-violet { color: #a78bfa; }
/* BG helpers */
.rd-bg-emerald { background: #10b981; }
.rd-bg-cyan { background: #06b6d4; }
.rd-bg-violet { background: #8b5cf6; }
.rd-bg-amber { background: #f59e0b; }
.rd-bg-rose { background: #f43f5e; }
.rd-bg-teal { background: #14b8a6; }
.rd-bg-sky { background: #0ea5e9; }
.rd-bg-orange { background: #f97316; }
.rd-bg-slate { background: #64748b; }
/* Responsive */
@media (max-width: 640px) {
.rd-hero { padding: 2rem 1rem 1.5rem; }
.rd-hero h1 { font-size: 2rem; }
.rd-section { padding: 0 1rem 1rem; }
}
`;
export function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

View File

@ -153,7 +153,7 @@ export class RStackAppSwitcher extends HTMLElement {
<a class="rstack-header" href="https://rstack.online" target="_blank" rel="noopener">
<span class="rstack-badge">r*</span>
<div class="rstack-info">
<span class="rstack-title">rSpace</span>
<span class="rstack-title">rStack</span>
<span class="rstack-subtitle">Self-hosted community app suite</span>
</div>
</a>

View File

@ -54,8 +54,6 @@ export interface RSpaceModule {
url: string;
name: string;
};
/** Optional: render rich demo page body HTML (served when space === "demo") */
demoPage?: () => string;
}
/** Registry of all loaded modules */