feat: upgrade 6 module demos to use real interactive components

Replace static HTML mockups and WebSocket skeleton demos with the actual
self-contained web components (with built-in demo data) for rcart, rtube,
rfunds, rnotes, rtrips, and rvote — matching the rcal demo pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-01 14:10:52 -08:00
parent ef1d93d7e9
commit f7ecf50588
12 changed files with 505 additions and 1445 deletions

View File

@ -1,230 +1,112 @@
/**
* rCart demo page static community garden shopping cart.
* rCart demo page server-rendered HTML body.
*
* Renders a fully server-side demo with 8 cart items, funding progress bars,
* member activity, and summary stats. No WebSocket needed (all static data).
* Embeds the full <folk-cart-shop space="demo"> component for
* real interactivity (catalog browsing, order tracking, filtering)
* plus showcase sections explaining the rCart vision.
*/
/* ─── Mock Data ─────────────────────────────────────────────── */
const members = [
{ name: "Alice", color: "#10b981" },
{ name: "Bob", color: "#0ea5e9" },
{ name: "Carol", color: "#f59e0b" },
{ name: "Dave", color: "#8b5cf6" },
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.",
},
];
interface CartItem {
name: string;
price: number;
requestedBy: string;
funded: number;
status: "Funded" | "In Cart" | "Needs Funding";
}
const cartItems: CartItem[] = [
{ name: "Raised Garden Bed Kit (4x8 ft)", price: 89.99, requestedBy: "Alice", funded: 89.99, status: "Funded" },
{ name: "Organic Seed Variety Pack (30 types)", price: 34.5, requestedBy: "Carol", funded: 34.5, status: "Funded" },
{ name: "Premium Potting Soil (40 qt, 3-pack)", price: 47.99, requestedBy: "Bob", funded: 32.0, status: "In Cart" },
{ name: "Stainless Steel Garden Tool Set", price: 62.0, requestedBy: "Dave", funded: 62.0, status: "Funded" },
{ name: "Drip Irrigation Kit (100 ft)", price: 54.95, requestedBy: "Alice", funded: 20.0, status: "Needs Funding" },
{ name: "Compost Tumbler (45 gal)", price: 109.0, requestedBy: "Bob", funded: 109.0, status: "Funded" },
{ name: "Garden Kneeling Pad &amp; Gloves Set", price: 28.5, requestedBy: "Carol", funded: 12.0, status: "Needs Funding" },
{ name: "Solar-Powered Pest Repeller (4-pack)", price: 39.99, requestedBy: "Dave", funded: 39.99, status: "In Cart" },
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." },
];
/* ─── Helpers ──────────────────────────────────────────────── */
function getMemberColor(name: string): string {
return members.find((m) => m.name === name)?.color || "#64748b";
}
function statusBadgeClass(status: CartItem["status"]): string {
switch (status) {
case "Funded":
return "rd-badge--emerald";
case "In Cart":
return "rd-badge--sky";
case "Needs Funding":
return "rd-badge--amber";
}
}
function progressFillClass(pct: number): string {
if (pct >= 100) return "rd-progress__fill--emerald";
if (pct >= 50) return "rd-progress__fill--sky";
return "rd-progress__fill--amber";
}
/* ─── Render ─────────────────────────────────────────────── */
export function renderDemo(): string {
const totalCost = cartItems.reduce((sum, item) => sum + item.price, 0);
const totalFunded = cartItems.reduce((sum, item) => sum + item.funded, 0);
const perPerson = totalCost / members.length;
const fundedCount = cartItems.filter((i) => i.status === "Funded").length;
const overallPct = Math.round((totalFunded / totalCost) * 100);
const uniqueRequesters = new Set(cartItems.map((i) => i.requestedBy)).size;
return `
<div class="rd-root" style="--rd-accent-from: #10b981; --rd-accent-to: #2dd4bf;">
<div class="rd-root" style="--rd-accent-from:#10b981; --rd-accent-to:#2dd4bf;">
<!-- Hero -->
<!-- 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;">
Group Shopping, Together
Cosmolocal Print-on-Demand
</div>
<h1>See how rCart works</h1>
<p class="rd-subtitle">
A community garden project where neighbors pool resources to buy everything they need together.
</p>
<h1>rCart Demo</h1>
<p class="rd-subtitle">Group shopping with cosmolocal fulfillment, revenue splits, and transparent funding</p>
<div class="rd-meta">
<span>8 items</span>
<span>\u{1F6D2} Catalog & Orders</span>
<span style="color:#475569">|</span>
<span>$${totalCost.toFixed(2)} total</span>
<span>\u{1F30D} Local Fulfillment</span>
<span style="color:#475569">|</span>
<span>${fundedCount}/${cartItems.length} funded</span>
</div>
<div class="rd-avatars">
${members
.map(
(m) =>
`<div class="rd-avatar" style="background:${m.color}" title="${m.name}">${m.name[0]}</div>`,
)
.join("\n ")}
<span class="rd-count">${members.length} members</span>
<span>\u{1F4B0} Revenue Splits</span>
<span style="color:#475569">|</span>
<span>\u{1F4E6} Order Tracking</span>
</div>
</section>
<!-- Overall Funding Progress -->
<!-- Interactive Shop -->
<section class="rd-section rd-section--narrow">
<div class="rd-card" style="padding:1.5rem;margin-bottom:1.5rem;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
<div>
<h2 style="font-size:1.125rem;font-weight:600;color:#f1f5f9;margin:0 0 0.25rem;">Community Garden Project</h2>
<p class="rd-text-xs rd-text-muted" style="margin:0;">Shared cart for our neighborhood garden setup</p>
</div>
<div style="text-align:right">
<p style="font-size:1.5rem;font-weight:700;color:white;margin:0;">$${totalFunded.toFixed(2)}</p>
<p class="rd-text-xs rd-text-muted" style="margin:0;">of $${totalCost.toFixed(2)} funded</p>
</div>
</div>
<div class="rd-progress" style="margin-bottom:0.5rem;">
<div class="rd-progress__fill" style="width:${overallPct}%"></div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span class="rd-text-xs rd-text-muted">${overallPct}% funded</span>
<span class="rd-text-xs rd-text-muted">$${(totalCost - totalFunded).toFixed(2)} remaining</span>
</div>
</div>
<!-- Cart Items -->
<div class="rd-card" style="margin-bottom:1.5rem;">
<div class="rd-card-header">
<div class="rd-card-title"><span class="rd-icon">&#128722;</span> Cart Items</div>
<div style="display:flex;align-items:center;gap:0.75rem;font-size:0.75rem;color:#94a3b8;">
<span style="display:flex;align-items:center;gap:0.25rem;">
<span style="width:0.5rem;height:0.5rem;border-radius:9999px;background:#10b981;display:inline-block;"></span> Funded
</span>
<span style="display:flex;align-items:center;gap:0.25rem;">
<span style="width:0.5rem;height:0.5rem;border-radius:9999px;background:#0ea5e9;display:inline-block;"></span> In Cart
</span>
<span style="display:flex;align-items:center;gap:0.25rem;">
<span style="width:0.5rem;height:0.5rem;border-radius:9999px;background:#f59e0b;display:inline-block;"></span> Needs Funding
</span>
</div>
</div>
${cartItems
.map((item) => {
const pct = Math.round((item.funded / item.price) * 100);
const memberColor = getMemberColor(item.requestedBy);
return `
<div style="padding:1rem 1.25rem;${cartItems.indexOf(item) > 0 ? "border-top:1px solid rgba(51,65,85,0.3);" : ""}">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;margin-bottom:0.5rem;">
<div style="min-width:0;flex:1;">
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.25rem;">
<span class="rd-text-sm rd-font-medium" style="color:#e2e8f0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${item.name}</span>
<span class="rd-badge ${statusBadgeClass(item.status)}">${item.status}</span>
</div>
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;color:#94a3b8;">
<span style="display:flex;align-items:center;gap:0.375rem;">
<span style="width:1rem;height:1rem;background:${memberColor};border-radius:9999px;display:inline-flex;align-items:center;justify-content:center;font-size:0.625rem;font-weight:700;color:white;">${item.requestedBy[0]}</span>
${item.requestedBy}
</span>
<span style="color:#475569;">requested this</span>
</div>
</div>
<div style="text-align:right;flex-shrink:0;">
<p class="rd-text-sm rd-font-semibold" style="color:#e2e8f0;margin:0;">$${item.price.toFixed(2)}</p>
${item.status !== "Funded" ? `<p class="rd-text-xs" style="color:#64748b;margin:0;">$${item.funded.toFixed(2)} funded</p>` : ""}
</div>
</div>
<div class="rd-progress rd-progress--sm">
<div class="rd-progress__fill ${progressFillClass(pct)}" style="width:${pct}%"></div>
</div>
</div>`;
})
.join("")}
</div>
<!-- Summary Grid -->
<div class="rd-grid rd-grid--3" style="margin-bottom:1.5rem;">
<div class="rd-stat">
<p class="rd-text-xs rd-font-semibold rd-text-muted" style="text-transform:uppercase;letter-spacing:0.05em;margin:0 0 0.5rem;">Total Cost</p>
<p class="rd-stat__value">$${totalCost.toFixed(2)}</p>
<p class="rd-stat__sub">${cartItems.length} items across ${uniqueRequesters} requesters</p>
</div>
<div class="rd-stat">
<p class="rd-text-xs rd-font-semibold rd-text-muted" style="text-transform:uppercase;letter-spacing:0.05em;margin:0 0 0.5rem;">Amount Funded</p>
<p class="rd-stat__value" style="color:#34d399;">$${totalFunded.toFixed(2)}</p>
<p class="rd-stat__sub">${fundedCount} of ${cartItems.length} items fully funded</p>
</div>
<div class="rd-stat">
<p class="rd-text-xs rd-font-semibold rd-text-muted" style="text-transform:uppercase;letter-spacing:0.05em;margin:0 0 0.5rem;">Per-Person Split</p>
<p class="rd-stat__value" style="color:#2dd4bf;">$${perPerson.toFixed(2)}</p>
<p class="rd-stat__sub">split equally among ${members.length} members</p>
</div>
</div>
<!-- Member Activity -->
<div class="rd-card" style="padding:1.5rem;">
<h3 class="rd-text-sm rd-font-semibold" style="color:#cbd5e1;margin:0 0 1rem;">Member Activity</h3>
<div class="rd-grid rd-grid--2">
${members
.map((member) => {
const requested = cartItems.filter((i) => i.requestedBy === member.name);
const requestedTotal = requested.reduce((sum, i) => sum + i.price, 0);
return `
<div style="display:flex;align-items:center;gap:0.75rem;background:rgba(51,65,85,0.2);border-radius:0.75rem;padding:0.75rem;">
<div class="rd-avatar" style="background:${member.color};flex-shrink:0;width:2.5rem;height:2.5rem;font-size:0.875rem;">${member.name[0]}</div>
<div style="min-width:0;flex:1;">
<p class="rd-text-sm rd-font-medium" style="color:#e2e8f0;margin:0;">${member.name}</p>
<p class="rd-text-xs rd-text-muted" style="margin:0;">
${requested.length} item${requested.length !== 1 ? "s" : ""} requested
<span style="color:#475569;margin:0 0.375rem;">&middot;</span>
$${requestedTotal.toFixed(2)} total
</p>
</div>
<div style="text-align:right;flex-shrink:0;">
<p class="rd-text-sm rd-font-medium" style="color:#cbd5e1;margin:0;">$${perPerson.toFixed(2)}</p>
<p class="rd-text-xs" style="color:#64748b;margin:0;">share</p>
</div>
</div>`;
})
.join("")}
</div>
<div class="rd-card" style="padding:0;overflow:hidden;">
<folk-cart-shop space="demo"></folk-cart-shop>
</div>
</section>
<!-- CTA -->
<!-- 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>
Create a shared cart for your group, community, or team. Add items from any store,
split costs fairly, and check out together.
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);">
<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>

View File

@ -451,7 +451,7 @@ routes.get("/", (c) => {
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
scripts: `<script type="module" src="/modules/rcart/cart-demo.js"></script>`,
scripts: `<script type="module" src="/modules/rcart/folk-cart-shop.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rcart/cart.css">`,
}));
}

View File

@ -1,256 +1,116 @@
/**
* rFunds demo page group expense tracking for "Alpine Explorer 2026".
* rFunds demo page server-rendered HTML body.
*
* Renders server-side HTML skeleton with budget overview, expense list,
* balances, settlements, category breakdown, and per-person stats.
* The client-side funds-demo.ts hydrates via WebSocket (DemoSync).
* 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.
*/
/* ─── Constants ─────────────────────────────────────────────── */
const MEMBERS = [
{ name: "Maya", initial: "M", color: "#10b981", bgClass: "rd-bg-emerald" },
{ name: "Liam", initial: "L", color: "#06b6d4", bgClass: "rd-bg-cyan" },
{ name: "Priya", initial: "P", color: "#8b5cf6", bgClass: "rd-bg-violet" },
{ name: "Omar", initial: "O", color: "#f59e0b", bgClass: "rd-bg-amber" },
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 CATEGORIES = [
{ key: "transport", icon: "\u{1F682}", label: "Transport", colorClass: "rd-progress__fill--cyan", badgeClass: "rd-badge--sky", textClass: "rd-cyan" },
{ key: "accommodation", icon: "\u{1F3E8}", label: "Accommodation", colorClass: "rd-progress__fill--violet", badgeClass: "rd-badge--teal", textClass: "rd-violet" },
{ key: "activity", icon: "\u26F7", label: "Activities", colorClass: "rd-progress__fill--amber", badgeClass: "rd-badge--amber", textClass: "rd-amber" },
{ key: "food", icon: "\u{1F372}", label: "Food & Drink", colorClass: "rd-progress__fill--rose", badgeClass: "rd-badge--rose", textClass: "rd-rose" },
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." },
];
/* ─── Render ─────────────────────────────────────────────── */
export function renderDemo(): string {
return `
<div class="rd-root" style="--rd-accent-from: #f59e0b; --rd-accent-to: #10b981;">
<div class="rd-root" style="--rd-accent-from:#f59e0b; --rd-accent-to:#10b981;">
<!-- Hero -->
<!-- Hero -->
<section class="rd-hero">
<h1>Alpine Explorer 2026</h1>
<p class="rd-subtitle">Group Expenses</p>
<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{1F4C5} Jul 6-20, 2026</span>
<span>\u{1F30A} River Visualization</span>
<span style="color:#475569">|</span>
<span>\u{1F465} ${MEMBERS.length} travelers</span>
<span>\u{1F4CA} TBFF Flows</span>
<span style="color:#475569">|</span>
<span>\u{1F3D4} Chamonix \u2192 Zermatt \u2192 Dolomites</span>
</div>
<div class="rd-avatars">
${MEMBERS.map(
(m) =>
`<div class="rd-avatar ${m.bgClass}" title="${m.name}">${m.initial}</div>`,
).join("\n ")}
<span class="rd-count">${MEMBERS.length} members</span>
<span>\u{1F4B8} Treasury</span>
<span style="color:#475569">|</span>
<span>\u2696\uFE0F Enoughness</span>
</div>
</section>
<!-- Status bar -->
<div class="rd-section rd-section--narrow">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.5rem; flex-wrap:wrap; gap:0.75rem">
<div style="display:flex; align-items:center; gap:0.75rem; flex-wrap:wrap">
<span id="rd-conn-badge" class="rd-status rd-status--disconnected">Disconnected</span>
<span class="rd-badge rd-badge--amber" style="font-size:0.7rem">Live &mdash; synced across all r* demos</span>
</div>
<button id="rd-reset-btn" class="rd-btn rd-btn--ghost" disabled>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
Reset Demo
</button>
</div>
</div>
<!-- Loading state -->
<div class="rd-section rd-section--narrow">
<div id="rd-loading" class="rd-card" style="border:2px dashed rgba(100,116,139,0.4); display:none">
<div class="rd-card-body" style="padding:3rem; text-align:center">
<div style="width:2rem; height:2rem; margin:0 auto 0.75rem; border:3px solid rgba(245,158,11,0.2); border-top-color:#f59e0b; border-radius:50%; animation:rd-spin 0.8s linear infinite"></div>
<p class="rd-text-muted">Connecting to rSpace...</p>
</div>
</div>
<style>@keyframes rd-spin { to { transform: rotate(360deg); } }</style>
<!-- Empty state -->
<div id="rd-empty" class="rd-card" style="border:2px dashed rgba(100,116,139,0.4); display:none">
<div class="rd-card-body" style="padding:3rem; text-align:center">
<div style="font-size:2rem; margin-bottom:0.75rem">\u{1F4B0}</div>
<p class="rd-text-muted">No expense data found. Try resetting the demo.</p>
</div>
</div>
</div>
<!-- Budget Overview -->
<section id="rd-budget-section" class="rd-section rd-section--narrow" style="display:none">
<div class="rd-card" style="padding:1.5rem; margin-bottom:1.5rem;">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1rem;">
<h2 style="font-size:1.125rem; font-weight:600; color:#f1f5f9; margin:0; display:flex; align-items:center; gap:0.5rem;">
\u{1F4CA} Trip Budget
</h2>
<span class="rd-live">live</span>
</div>
<!-- Budget totals: 3-column stat grid -->
<div class="rd-grid rd-grid--3" style="margin-bottom:1rem;">
<div class="rd-stat">
<p class="rd-stat__value" id="rd-budget-total">\u20AC4,000</p>
<p class="rd-stat__label">Total Budget</p>
</div>
<div class="rd-stat">
<p class="rd-stat__value rd-emerald" id="rd-budget-spent">\u20AC0</p>
<p class="rd-stat__label">Spent</p>
</div>
<div class="rd-stat">
<p class="rd-stat__value rd-cyan" id="rd-budget-remaining">\u20AC4,000</p>
<p class="rd-stat__label" id="rd-budget-remaining-label">Remaining</p>
</div>
</div>
<!-- Progress bar -->
<div style="margin-bottom:1rem;">
<div style="display:flex; align-items:center; justify-content:space-between; font-size:0.75rem; color:#94a3b8; margin-bottom:0.375rem;">
<span id="rd-budget-pct-label">0% used</span>
<span id="rd-budget-left-label">\u20AC4,000 left</span>
</div>
<div class="rd-progress">
<div class="rd-progress__fill rd-progress__fill--emerald" id="rd-budget-bar" style="width:0%"></div>
</div>
</div>
<!-- Category breakdown -->
<div>
<h3 style="font-size:0.875rem; font-weight:600; color:#cbd5e1; margin:0 0 0.75rem;">Budget by Category</h3>
<div class="rd-grid rd-grid--2" id="rd-category-breakdown">
${CATEGORIES.map(
(cat) => `
<div class="rd-stat" style="padding:0.75rem;" data-category="${cat.key}">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:0.5rem;">
<span style="display:flex; align-items:center; gap:0.5rem; font-size:0.875rem; color:#e2e8f0;">
${cat.icon} ${cat.label}
</span>
<span class="rd-text-xs rd-text-muted" data-cat-amounts>\u20AC0 / \u20AC1,000</span>
</div>
<div class="rd-progress rd-progress--sm">
<div class="${cat.colorClass}" style="height:100%; border-radius:9999px; width:0%; transition:width 0.3s" data-cat-bar></div>
</div>
<p class="rd-text-xs rd-text-dim" style="margin:0.25rem 0 0;" data-cat-pct>0% used</p>
</div>`,
).join("")}
</div>
</div>
<!-- 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>
<!-- Expenses + Balances grid -->
<section id="rd-expenses-section" class="rd-section" style="display:none">
<div style="display:grid; grid-template-columns:1fr; gap:1rem;">
<!-- Expense list (left 2/3 on desktop) -->
<div class="rd-card" id="rd-expense-card" style="grid-column:1;">
<div class="rd-card-header">
<div class="rd-card-title"><span class="rd-icon">\u{1F4DD}</span> <span id="rd-expense-count">Expenses (0)</span></div>
<span class="rd-text-xs rd-text-muted">Click amount to edit</span>
</div>
<div id="rd-expense-list">
<!-- Populated by funds-demo.ts -->
</div>
<!-- Total row -->
<div style="display:flex; align-items:center; justify-content:space-between; padding:0.75rem 1.25rem; border-top:1px solid rgba(51,65,85,0.5); background:rgba(51,65,85,0.2);">
<span style="font-size:0.875rem; font-weight:600; color:#cbd5e1;">Total</span>
<span style="font-size:1.125rem; font-weight:700; color:white;" id="rd-expense-total">\u20AC0</span>
</div>
</div>
<!-- Balances (right 1/3 on desktop) -->
<div style="display:flex; flex-direction:column; gap:1rem;">
<div class="rd-card" id="rd-balances">
<div class="rd-card-header">
<div class="rd-card-title"><span class="rd-icon">\u2696\uFE0F</span> Balances</div>
</div>
<div class="rd-card-body" id="rd-balances-body">
<!-- Populated by funds-demo.ts -->
</div>
</div>
<div class="rd-card" id="rd-settlements">
<div class="rd-card-header">
<div class="rd-card-title"><span class="rd-icon">\u{1F4B8}</span> Settle Up</div>
</div>
<div class="rd-card-body" id="rd-settlements-body">
<!-- Populated by funds-demo.ts -->
</div>
</div>
</div>
<!-- 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>
<!-- Spending by Category -->
<section id="rd-spending-section" class="rd-section" style="display:none">
<div class="rd-card" style="padding:1.5rem;">
<h2 style="font-size:1.125rem; font-weight:600; margin:0 0 1rem; display:flex; align-items:center; gap:0.5rem;">
\u{1F4CA} Spending by Category
</h2>
<div class="rd-grid rd-grid--4" id="rd-spending-grid">
${CATEGORIES.map(
(cat) => `
<div class="rd-stat" data-spending-cat="${cat.key}">
<div style="font-size:1.5rem; margin-bottom:0.5rem;">${cat.icon}</div>
<p style="font-size:0.875rem; color:#cbd5e1; font-weight:500; margin:0;">${cat.label}</p>
<p style="font-size:1.125rem; font-weight:700; color:white; margin:0.25rem 0;" data-spending-amount>\u20AC0</p>
<div class="rd-progress rd-progress--xs" style="margin-bottom:0.25rem;">
<div class="${cat.colorClass}" style="height:100%; border-radius:9999px; width:0%; transition:width 0.3s" data-spending-bar></div>
</div>
<p class="rd-text-xs rd-text-dim" style="margin:0;" data-spending-pct>0% of total</p>
</div>`,
).join("")}
</div>
<!-- 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>
<!-- Per Person -->
<section id="rd-person-section" class="rd-section" style="display:none">
<div class="rd-card" style="padding:1.5rem;">
<h2 style="font-size:1.125rem; font-weight:600; margin:0 0 1rem; display:flex; align-items:center; gap:0.5rem;">
\u{1F464} Per Person
</h2>
<div class="rd-grid rd-grid--4" id="rd-person-grid">
${MEMBERS.map(
(m) => `
<div class="rd-stat" data-person="${m.name}">
<div class="rd-avatar ${m.bgClass}" style="width:3rem; height:3rem; font-size:1.125rem; margin:0 auto 0.5rem; box-shadow:0 0 0 2px #1e293b;">${m.initial}</div>
<p style="font-size:0.875rem; color:#cbd5e1; font-weight:500; margin:0;">${m.name}</p>
<p style="font-size:1.125rem; font-weight:700; color:white; margin:0.25rem 0;" data-person-paid>\u20AC0</p>
<p class="rd-text-xs rd-text-dim" style="margin:0;" data-person-pct>paid (0%)</p>
<p style="font-size:0.875rem; font-weight:600; margin:0.25rem 0 0;" data-person-balance>\u20AC0</p>
</div>`,
).join("")}
</div>
</div>
</section>
<!-- CTA -->
<!-- CTA -->
<section class="rd-section rd-section--narrow">
<div class="rd-cta">
<h2>Track Your Group Expenses</h2>
<h2>Design Your Funding Flows</h2>
<p>
rFunds makes it easy to manage shared costs, track budgets, and settle up.
Design custom funding flows with threshold-based mechanisms.
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);">
<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>
<style>
/* Responsive grid for expenses + balances on desktop */
@media (min-width: 768px) {
#rd-expenses-section > div {
grid-template-columns: 2fr 1fr;
}
}
</style>`;
</div>`;
}

View File

@ -206,8 +206,7 @@ routes.get("/", (c) => {
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
demoScripts: `<script type="module" src="/lib/demo-sync.js"></script>
<script type="module" src="/modules/rfunds/funds-demo.js"></script>`,
scripts: `<script type="module" src="/modules/rfunds/folk-funds-app.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rfunds/funds.css">`,
}));
}

View File

@ -1,214 +1,104 @@
/**
* rNotes demo page server-rendered HTML body.
*
* Returns the static HTML skeleton for the interactive notes demo.
* The client-side notes-demo.ts populates note cards, packing list,
* sidebar, and notebook header via WebSocket snapshots.
* 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">
<div class="rd-root" style="--rd-accent-from:#f59e0b; --rd-accent-to:#fb923c;">
<!-- Hero -->
<!-- Hero -->
<section class="rd-hero">
<div style="display:inline-flex;align-items:center;gap:0.5rem;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;">
<span id="rd-hero-dot" style="width:0.5rem;height:0.5rem;border-radius:9999px;background:#f59e0b;animation:rd-pulse 2s ease-in-out infinite;"></span>
<span id="rd-hero-label">Interactive Demo</span>
<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>See how rNotes works</h1>
<p class="rd-subtitle">A collaborative knowledge base for your team</p>
<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>Live transcription</span>
<span>Audio &amp; video</span>
<span>Organized notebooks</span>
<span>Canvas sync</span>
<span>Real-time collaboration</span>
</div>
<div class="rd-avatars" id="rd-avatars">
<div class="rd-avatar" style="background:#14b8a6" title="...">...</div>
<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>
<!-- Context bar + Reset -->
<!-- Interactive Notes App -->
<section class="rd-section rd-section--narrow">
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:1rem;margin-bottom:1.5rem;">
<p style="text-align:center;font-size:0.875rem;color:#94a3b8;max-width:40rem;margin:0;">
This demo shows a <span style="color:#e2e8f0;font-weight:500">Trip Planning Notebook</span> scenario
with notes, a packing list, tags, and canvas sync &mdash; all powered by the
<span style="color:#e2e8f0;font-weight:500">r* ecosystem</span> with live data from
<span style="color:#e2e8f0;font-weight:500">rSpace</span>.
</p>
<button id="rd-reset-btn" class="rd-btn rd-btn--ghost" disabled>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
Reset Demo
</button>
<div class="rd-card" style="padding:0;overflow:hidden;">
<folk-notes-app space="demo"></folk-notes-app>
</div>
</section>
<!-- Notebook header card -->
<section class="rd-section rd-section--narrow">
<div class="rd-card" id="rd-notebook-header" style="margin-bottom:1.5rem;">
<div class="rd-card-header">
<div class="rd-card-title">
<span class="rd-icon" style="font-size:1.25rem;">&#128211;</span>
<span id="rd-nb-title">Loading...</span>
<span id="rd-nb-count" class="rd-text-xs rd-text-muted" style="margin-left:0.5rem;"></span>
</div>
<a href="https://rnotes.online" target="_blank" rel="noopener noreferrer" class="rd-card-header rd-open-link">Open in rNotes</a>
</div>
<div class="rd-card-body" style="padding:0.75rem 1.25rem;">
<p id="rd-nb-desc" class="rd-text-sm rd-text-muted" style="margin:0;">Loading notebook data...</p>
</div>
</div>
<!-- Main layout: sidebar + content -->
<div style="display:grid;grid-template-columns:1fr;gap:1.5rem;" class="rd-notes-layout">
<!-- Sidebar -->
<div id="rd-sidebar">
<div class="rd-card" style="overflow:hidden;">
<!-- Sidebar header -->
<div style="padding:0.75rem 1rem;border-bottom:1px solid rgba(51,65,85,0.5);display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:0.75rem;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:0.05em;">Notebook</span>
<span id="rd-sb-note-count" style="font-size:0.75rem;color:#64748b;">0 notes</span>
</div>
<!-- Active notebook tree -->
<div style="padding:0.5rem;">
<div style="margin-bottom:0.25rem;">
<div style="display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-radius:0.5rem;font-size:0.875rem;background:rgba(245,158,11,0.1);color:#fcd34d;">
<span>&#128211;</span>
<span id="rd-sb-nb-title" style="font-weight:500;">Loading...</span>
</div>
<div style="margin-left:1rem;margin-top:0.125rem;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:0.375rem 0.75rem;border-radius:0.375rem;font-size:0.75rem;background:rgba(51,65,85,0.4);color:white;">
<span>Notes</span>
<span id="rd-sb-notes-num" style="color:#475569">0</span>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:0.375rem 0.75rem;border-radius:0.375rem;font-size:0.75rem;color:#64748b;margin-top:0.125rem;cursor:pointer;" onmouseover="this.style.background='rgba(51,65,85,0.2)';this.style.color='#cbd5e1'" onmouseout="this.style.background='';this.style.color='#64748b'">
<span>Packing List</span>
<span style="color:#475569">1</span>
</div>
</div>
</div>
</div>
<!-- Quick info links -->
<div style="padding:0.75rem 1rem;border-top:1px solid rgba(51,65,85,0.5);display:flex;flex-direction:column;gap:0.5rem;">
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;color:#64748b;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<span>Search notes...</span>
</div>
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;color:#64748b;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
<span>Browse tags</span>
</div>
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;color:#64748b;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span>Recent edits</span>
</div>
</div>
</div>
</div>
<!-- Notes + Packing list -->
<div>
<!-- Notes section -->
<div id="rd-notes-section">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;">
<div style="display:flex;align-items:center;gap:0.5rem;">
<h2 style="font-size:0.875rem;font-weight:600;color:#cbd5e1;margin:0;">Notes</h2>
<span id="rd-notes-count" class="rd-text-xs rd-text-muted">0 notes</span>
</div>
<span class="rd-text-xs rd-text-muted">Sort: Recently edited</span>
</div>
<!-- Loading skeleton -->
<div id="rd-loading" style="display:flex;flex-direction:column;gap:1rem;">
${[1, 2, 3]
.map(
() => `
<div class="rd-card" style="padding:1rem;">
<div style="height:1rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:66%;margin-bottom:0.75rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:0.75rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:100%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:0.75rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:80%;margin-bottom:0.75rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="display:flex;gap:0.5rem;">
<div style="height:1.25rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:4rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:1.25rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
</div>
</div>`,
)
.join("")}
</div>
<style>
@keyframes rd-skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
</style>
<!-- Note cards container (populated by notes-demo.ts) -->
<div id="rd-notes-container" style="display:flex;flex-direction:column;gap:1rem;"></div>
<!-- Empty state -->
<div id="rd-notes-empty" class="rd-card" style="display:none;padding:2rem;text-align:center;">
<p class="rd-text-muted rd-text-sm">No notes found. Try resetting the demo.</p>
</div>
</div>
<!-- Packing list section -->
<div id="rd-packing-section" style="margin-top:1.5rem;display:none;">
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:1rem;">
<h2 style="font-size:0.875rem;font-weight:600;color:#cbd5e1;margin:0;">Packing List</h2>
</div>
<div id="rd-packing-container"></div>
</div>
</div>
</div>
</section>
<!-- Features showcase -->
<section class="rd-section" style="margin-top:2rem;">
<h2 style="font-size:1.5rem;font-weight:700;color:white;text-align:center;margin-bottom:2rem;">Everything you need to capture knowledge</h2>
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:1rem;" class="rd-features-grid">
${[
{
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>`,
title: "Live Transcription",
desc: "Record and transcribe in real time. Stream audio via WebSocket or transcribe offline with Parakeet.js.",
},
{
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
title: "Rich Editing",
desc: "Headings, lists, code blocks, highlights, images, and file attachments in every note.",
},
{
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>`,
title: "Notebooks",
desc: "Organize notes into notebooks with sections. Nest as deep as you need.",
},
{
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>`,
title: "Flexible Tags",
desc: "Cross-cutting tags let you find notes across all notebooks instantly.",
},
{
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
title: "Canvas Sync",
desc: "Pin any note to your rSpace canvas for visual collaboration with your team.",
},
]
.map(
(f) => `
<div class="rd-card" style="padding:1.25rem;">
<div style="width:2.5rem;height:2.5rem;background:rgba(245,158,11,0.1);border-radius:0.5rem;display:flex;align-items:center;justify-content:center;margin-bottom:0.75rem;">
${f.icon}
</div>
<h3 style="font-size:0.875rem;font-weight:600;color:white;margin:0 0 0.25rem;">${f.title}</h3>
<p style="font-size:0.75rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
<!-- 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("")}
).join("")}
</div>
</section>
<!-- CTA -->
<!-- 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>
@ -222,139 +112,5 @@ export function renderDemo(): string {
</div>
</section>
</div>
<style>
/* ── Notes-specific layout ── */
@media (min-width: 1024px) {
.rd-notes-layout {
grid-template-columns: 16rem 1fr !important;
}
}
@media (min-width: 640px) {
.rd-features-grid {
grid-template-columns: repeat(3, 1fr) !important;
}
}
@media (min-width: 1024px) {
.rd-features-grid {
grid-template-columns: repeat(5, 1fr) !important;
}
}
/* Note card styles */
.rd-note-card {
cursor: pointer;
transition: border-color 0.15s;
}
.rd-note-card:hover {
border-color: rgba(100,116,139,0.6);
}
.rd-note-card--expanded {
cursor: default;
border-color: rgba(245,158,11,0.3) !important;
box-shadow: 0 0 0 1px rgba(245,158,11,0.15);
}
/* Synced badge */
.rd-synced-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-size: 0.75rem;
padding: 0.25rem 0.625rem;
background: rgba(20,184,166,0.1);
border: 1px solid rgba(20,184,166,0.2);
color: #2dd4bf;
border-radius: 9999px;
white-space: nowrap;
flex-shrink: 0;
}
/* Markdown rendered content */
.rd-md h3 { font-size: 1.125rem; font-weight: 700; color: white; margin: 1rem 0 0.5rem; }
.rd-md h4 { font-size: 1rem; font-weight: 600; color: #e2e8f0; margin: 1rem 0 0.5rem; }
.rd-md h5 { font-size: 0.875rem; font-weight: 600; color: #cbd5e1; margin: 0.75rem 0 0.25rem; }
.rd-md p { font-size: 0.875rem; color: #cbd5e1; margin: 0.25rem 0; line-height: 1.6; }
.rd-md strong { color: white; font-weight: 500; }
.rd-md em { color: #cbd5e1; font-style: italic; }
.rd-md code {
color: #fcd34d;
background: rgba(30,41,59,1);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-family: monospace;
}
.rd-md .rd-md-quote {
background: rgba(245,158,11,0.1);
border-left: 2px solid #f59e0b;
padding: 0.5rem 1rem;
border-radius: 0 0.5rem 0.5rem 0;
margin: 0.5rem 0;
}
.rd-md .rd-md-quote p { color: #fcd34d; }
.rd-md ul, .rd-md ol { margin: 0.5rem 0; padding: 0; list-style: none; }
.rd-md ul li, .rd-md ol li {
display: flex; align-items: flex-start; gap: 0.5rem;
font-size: 0.875rem; color: #cbd5e1; padding: 0.125rem 0;
}
.rd-md ul li::before { content: "\\2022"; color: #f59e0b; margin-top: 0.1rem; flex-shrink: 0; }
.rd-md ol li .rd-md-num { color: #f59e0b; font-weight: 500; min-width: 1.2em; text-align: right; flex-shrink: 0; }
.rd-md .rd-md-codeblock {
background: rgba(2,6,23,1);
border-radius: 0.5rem;
border: 1px solid rgba(51,65,85,0.5);
overflow: hidden;
margin: 0.5rem 0;
}
.rd-md .rd-md-codeblock-lang {
display: flex; align-items: center; justify-content: space-between;
padding: 0.5rem 1rem;
background: rgba(30,41,59,0.5);
border-bottom: 1px solid rgba(51,65,85,0.5);
font-size: 0.75rem; color: #94a3b8; font-family: monospace;
}
.rd-md .rd-md-codeblock pre {
padding: 0.75rem 1rem;
font-size: 0.75rem; color: #cbd5e1;
font-family: monospace;
overflow-x: auto;
line-height: 1.5;
margin: 0;
}
/* Tag pill */
.rd-note-tag {
display: inline-block;
font-size: 0.75rem;
padding: 0.125rem 0.5rem;
background: rgba(51,65,85,0.5);
color: #94a3b8;
border-radius: 0.375rem;
border: 1px solid rgba(51,65,85,0.3);
}
/* Packing list checkbox */
.rd-pack-check {
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-pack-check--checked {
background: #f59e0b; border-color: #f59e0b;
}
.rd-pack-check:hover { border-color: #64748b; }
/* Packing item label */
.rd-pack-item {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
cursor: pointer;
transition: background 0.1s;
}
.rd-pack-item:hover { background: rgba(51,65,85,0.3); }
</style>`;
</div>`;
}

View File

@ -371,8 +371,7 @@ routes.get("/", (c) => {
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
demoScripts: `<script type="module" src="/lib/demo-sync.js"></script>
<script type="module" src="/modules/rnotes/notes-demo.js"></script>`,
scripts: `<script type="module" src="/modules/rnotes/folk-notes-app.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rnotes/notes.css">`,
}));
}

View File

@ -1,252 +1,110 @@
/**
* rTrips demo page server-rendered HTML body.
*
* "Alpine Explorer 2026" dashboard with 6 cards powered by the rStack:
* Maps (SVG), Notes (packing checklist), Calendar (grid),
* Polls (bars), Funds (expenses), Cart (gear progress).
*
* Client-side trips-demo.ts populates all cards via WebSocket snapshots.
* 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">
<div class="rd-root" style="--rd-accent-from:#14b8a6; --rd-accent-to:#06b6d4;">
<!-- Trip Header -->
<!-- Hero -->
<section class="rd-hero">
<div style="display:inline-flex;align-items:center;gap:0.5rem;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;">
<span id="rd-hero-dot" style="width:0.5rem;height:0.5rem;border-radius:9999px;background:#14b8a6;animation:rd-pulse 2s ease-in-out infinite;"></span>
<span id="rd-hero-label">Interactive Demo</span>
<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 id="rd-trip-title" style="background:linear-gradient(135deg,#5eead4,#67e8f9,#93c5fd);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;">Alpine Explorer 2026</h1>
<p class="rd-subtitle" id="rd-trip-route">Chamonix Zermatt Dolomites</p>
<div class="rd-meta" id="rd-trip-meta">
<span>📅 Jul 620, 2026</span>
<span>💶 ~4,000 budget</span>
<span>🏔 3 countries</span>
</div>
<div class="rd-avatars" id="rd-avatars">
<div class="rd-avatar" style="background:#64748b" title="Loading"></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>
<!-- Context bar -->
<!-- Interactive Trips Planner -->
<section class="rd-section rd-section--narrow">
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:1rem;margin-bottom:1.5rem;">
<p style="text-align:center;font-size:0.875rem;color:#94a3b8;max-width:40rem;margin:0;">
Every trip is powered by the <span style="color:#e2e8f0;font-weight:500">rStack</span>
a suite of collaborative tools that handle routes, notes, schedules, voting, expenses,
and shared purchases. Each card below shows live data with a link to the full tool.
</p>
<button id="rd-reset-btn" class="rd-btn rd-btn--ghost" disabled>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
Reset Demo
</button>
<div class="rd-card" style="padding:0;overflow:hidden;">
<folk-trips-planner space="demo"></folk-trips-planner>
</div>
</section>
<!-- 6-Card Dashboard Grid -->
<!-- Core Concepts -->
<section class="rd-section">
<div class="rd-trips-grid">
<!-- 1. Route Map (span 2) -->
<div class="rd-card rd-trips-card rd-trips-card--span2" id="rd-card-maps">
<div class="rd-trips-card-header">
<div style="display:flex;align-items:center;gap:0.5rem;">
<span style="font-size:1.125rem;">🗺</span>
<span style="font-weight:600;font-size:0.875rem;">Route Map</span>
<span class="rd-trips-live" id="rd-maps-live" style="display:none;">
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
live
</span>
</div>
<a href="https://rmaps.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rMaps </a>
</div>
<div class="rd-trips-card-body">
<div style="position:relative;width:100%;border-radius:0.75rem;background:rgba(15,23,42,0.6);overflow:hidden;">
<svg id="rd-map-svg" viewBox="0 0 800 300" style="width:100%;height:auto;display:block;" xmlns="http://www.w3.org/2000/svg">
<!-- Mountain silhouettes -->
<path d="M0 280 L60 200 L100 240 L160 160 L200 210 L260 130 L320 180 L380 100 L420 150 L480 80 L540 140 L600 110 L660 160 L720 120 L800 180 L800 300 L0 300 Z" fill="rgba(30,41,59,0.8)"/>
<path d="M0 280 L80 220 L140 250 L200 190 L280 230 L340 170 L400 200 L460 150 L520 190 L580 160 L640 200 L700 170 L800 220 L800 300 L0 300 Z" fill="rgba(51,65,85,0.6)"/>
<path d="M370 100 L380 100 L390 108 L375 105 Z" fill="rgba(255,255,255,0.4)"/>
<path d="M470 80 L480 80 L492 90 L476 86 Z" fill="rgba(255,255,255,0.5)"/>
<path d="M590 110 L600 110 L612 120 L598 116 Z" fill="rgba(255,255,255,0.4)"/>
<!-- Route line (default) -->
<path id="rd-route-path" d="M160 180 C250 160, 350 200, 430 150 C510 100, 560 160, 650 140" fill="none" stroke="rgba(94,234,212,0.7)" stroke-width="3" stroke-dasharray="10 6"/>
<!-- Default destination pins -->
<g id="rd-map-pins">
<g><circle cx="160" cy="180" r="8" fill="#14b8a6" stroke="#0d9488" stroke-width="2"/><text x="160" y="210" text-anchor="middle" fill="#94a3b8" font-size="12" font-weight="600">Chamonix</text><text x="160" y="224" text-anchor="middle" fill="#64748b" font-size="10">Jul 610</text></g>
<g><circle cx="430" cy="150" r="8" fill="#06b6d4" stroke="#0891b2" stroke-width="2"/><text x="430" y="180" text-anchor="middle" fill="#94a3b8" font-size="12" font-weight="600">Zermatt</text><text x="430" y="194" text-anchor="middle" fill="#64748b" font-size="10">Jul 1014</text></g>
<g><circle cx="650" cy="140" r="8" fill="#8b5cf6" stroke="#7c3aed" stroke-width="2"/><text x="650" y="170" text-anchor="middle" fill="#94a3b8" font-size="12" font-weight="600">Dolomites</text><text x="650" y="184" text-anchor="middle" fill="#64748b" font-size="10">Jul 1420</text></g>
</g>
<!-- Activity icons -->
<text x="280" y="168" font-size="16">🥾</text>
<text x="350" y="188" font-size="16">🧗</text>
<text x="500" y="128" font-size="16">🚵</text>
<text x="560" y="148" font-size="16">🪂</text>
<text x="620" y="158" font-size="16">🛶</text>
</svg>
<div style="position:absolute;bottom:0.75rem;left:0.75rem;display:flex;gap:0.75rem;font-size:0.75rem;color:#94a3b8;">
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#14b8a6;"></span> France</span>
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#06b6d4;"></span> Switzerland</span>
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#8b5cf6;"></span> Italy</span>
</div>
</div>
</div>
</div>
<!-- 2. Trip Notes (packing checklist) -->
<div class="rd-card rd-trips-card" id="rd-card-notes">
<div class="rd-trips-card-header">
<div style="display:flex;align-items:center;gap:0.5rem;">
<span style="font-size:1.125rem;">📝</span>
<span style="font-weight:600;font-size:0.875rem;">Trip Notes</span>
<span class="rd-trips-live" id="rd-notes-live" style="display:none;">
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
live
</span>
</div>
<a href="https://rnotes.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rNotes </a>
</div>
<div class="rd-trips-card-body" id="rd-notes-body">
<div>
<h4 class="rd-trips-sub-heading">Packing Checklist</h4>
<div id="rd-packing-list" class="rd-trips-skeleton">
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:75%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:50%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:66%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:33%;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
</div>
</div>
<div style="margin-top:1rem;">
<h4 class="rd-trips-sub-heading">Trip Rules</h4>
<ol style="margin:0;padding:0 0 0 1.25rem;font-size:0.8125rem;color:#cbd5e1;line-height:1.75;">
<li>Majority vote on daily activities</li>
<li>Shared expenses split equally</li>
<li>Quiet hours after 10pm in huts</li>
<li>Everyone carries their own pack</li>
</ol>
</div>
</div>
</div>
<!-- 3. Group Calendar (span 2) -->
<div class="rd-card rd-trips-card rd-trips-card--span2" id="rd-card-cal">
<div class="rd-trips-card-header">
<div style="display:flex;align-items:center;gap:0.5rem;">
<span style="font-size:1.125rem;">📅</span>
<span style="font-weight:600;font-size:0.875rem;">Group Calendar</span>
<span class="rd-trips-live" id="rd-cal-live" style="display:none;">
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
live
</span>
</div>
<a href="https://rcal.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rCal </a>
</div>
<div class="rd-trips-card-body">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
<h4 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0;">July 2026</h4>
<span id="rd-cal-days" style="font-size:0.75rem;color:#94a3b8;">15 days</span>
</div>
<div id="rd-cal-grid"></div>
<div style="display:flex;flex-wrap:wrap;gap:0.75rem;margin-top:0.75rem;font-size:0.75rem;color:#94a3b8;">
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#14b8a6;"></span> Travel</span>
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#10b981;"></span> Hiking</span>
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#f59e0b;"></span> Adventure</span>
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#8b5cf6;"></span> Culture</span>
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#64748b;"></span> Rest</span>
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#06b6d4;"></span> Transit</span>
</div>
</div>
</div>
<!-- 4. Group Polls -->
<div class="rd-card rd-trips-card" id="rd-card-polls">
<div class="rd-trips-card-header">
<div style="display:flex;align-items:center;gap:0.5rem;">
<span style="font-size:1.125rem;">🗳</span>
<span style="font-weight:600;font-size:0.875rem;">Group Polls</span>
<span class="rd-trips-live" id="rd-polls-live" style="display:none;">
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
live
</span>
</div>
<a href="https://rvote.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rVote </a>
</div>
<div class="rd-trips-card-body" id="rd-polls-body">
<div class="rd-trips-skeleton">
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:75%;margin-bottom:0.75rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:0.5rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:100%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:0.5rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:66%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:0.5rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:33%;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
</div>
</div>
</div>
<!-- 5. Group Expenses -->
<div class="rd-card rd-trips-card" id="rd-card-funds">
<div class="rd-trips-card-header">
<div style="display:flex;align-items:center;gap:0.5rem;">
<span style="font-size:1.125rem;">💰</span>
<span style="font-weight:600;font-size:0.875rem;">Group Expenses</span>
<span class="rd-trips-live" id="rd-funds-live" style="display:none;">
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
live
</span>
</div>
<a href="https://rfunds.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rFunds </a>
</div>
<div class="rd-trips-card-body" id="rd-funds-body">
<div style="text-align:center;padding:0.5rem 0;">
<p id="rd-funds-total" style="font-size:1.5rem;font-weight:700;color:white;margin:0;">...</p>
<p style="font-size:0.75rem;color:#94a3b8;margin:0.25rem 0 0;">Total group spending</p>
</div>
<div class="rd-trips-skeleton" id="rd-funds-skeleton">
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:75%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:50%;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
</div>
<div id="rd-funds-expenses" style="display:none;"></div>
<div id="rd-funds-balances" style="display:none;"></div>
</div>
</div>
<!-- 6. Shared Gear (span 2) -->
<div class="rd-card rd-trips-card rd-trips-card--span2" id="rd-card-cart">
<div class="rd-trips-card-header">
<div style="display:flex;align-items:center;gap:0.5rem;">
<span style="font-size:1.125rem;">🛒</span>
<span style="font-weight:600;font-size:0.875rem;">Shared Gear</span>
<span class="rd-trips-live" id="rd-cart-live" style="display:none;">
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
live
</span>
</div>
<a href="https://rcart.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rCart </a>
</div>
<div class="rd-trips-card-body" id="rd-cart-body">
<div class="rd-trips-skeleton" id="rd-cart-skeleton">
<div style="height:0.5rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:100%;margin-bottom:0.75rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;">
<div style="height:3rem;background:rgba(51,65,85,0.3);border-radius:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:3rem;background:rgba(51,65,85,0.3);border-radius:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:3rem;background:rgba(51,65,85,0.3);border-radius:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
<div style="height:3rem;background:rgba(51,65,85,0.3);border-radius:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
</div>
</div>
<div id="rd-cart-content" style="display:none;"></div>
</div>
</div>
<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>
<!-- CTA -->
<!-- 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 Own Group Adventure</h2>
<h2>Plan Your Next Adventure</h2>
<p>
The rStack gives your group everything you need &mdash; routes, schedules, polls,
shared expenses, and gear lists &mdash; all connected in one trip canvas.
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
@ -254,161 +112,5 @@ export function renderDemo(): string {
</div>
</section>
</div>
<style>
@keyframes rd-skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.rd-trips-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.rd-trips-grid {
grid-template-columns: repeat(3, 1fr);
}
.rd-trips-card--span2 {
grid-column: span 2;
}
}
.rd-trips-card {
overflow: hidden;
}
.rd-trips-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-trips-card-body {
padding: 1.25rem;
}
.rd-trips-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;
white-space: nowrap;
}
.rd-trips-open-link:hover {
background: rgba(71,85,105,0.6);
color: white;
}
.rd-trips-live {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: #34d399;
}
.rd-trips-sub-heading {
font-size: 0.6875rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0 0 0.5rem;
}
.rd-trips-skeleton {
animation: rd-skeleton-pulse 1.5s ease-in-out infinite;
}
/* Packing item */
.rd-trips-pack-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8125rem;
padding: 0.25rem 0;
cursor: pointer;
}
.rd-trips-pack-item:hover { opacity: 0.85; }
.rd-trips-pack-check {
width: 1rem; height: 1rem; border-radius: 0.25rem; flex-shrink: 0;
border: 2px solid #475569;
display: flex; align-items: center; justify-content: center;
transition: all 0.15s;
}
.rd-trips-pack-check--checked {
background: #14b8a6; border-color: #14b8a6;
}
/* Calendar grid */
.rd-cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.25rem;
}
.rd-cal-day-name {
text-align: center;
font-size: 0.6875rem;
color: #64748b;
font-weight: 500;
padding: 0.25rem 0;
}
.rd-cal-cell {
min-height: 3.5rem;
border-radius: 0.5rem;
padding: 0.25rem;
font-size: 0.75rem;
}
.rd-cal-cell--trip {
background: rgba(51,65,85,0.4);
border: 1px solid rgba(71,85,105,0.4);
}
.rd-cal-cell--empty {
background: rgba(30,41,59,0.3);
}
.rd-cal-cell-num { display: block; margin-bottom: 0.125rem; }
.rd-cal-cell-num--trip { color: #e2e8f0; font-weight: 500; }
.rd-cal-cell-num--off { color: #475569; }
.rd-cal-event {
display: block;
border-radius: 0.25rem;
padding: 0 0.25rem;
color: white;
font-size: 9px;
margin-top: 0.125rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Poll bar */
.rd-trips-poll-bar-bg {
height: 0.5rem;
background: rgba(51,65,85,0.7);
border-radius: 9999px;
overflow: hidden;
}
.rd-trips-poll-bar {
height: 100%;
border-radius: 9999px;
transition: width 0.3s;
}
/* Cart item */
.rd-trips-cart-item {
background: rgba(51,65,85,0.3);
border-radius: 0.5rem;
padding: 0.75rem;
}
.rd-trips-cart-bar-bg {
height: 0.375rem;
background: rgba(51,65,85,0.6);
border-radius: 9999px;
overflow: hidden;
}
.rd-trips-cart-bar {
height: 100%;
border-radius: 9999px;
transition: width 0.3s;
}
</style>`;
</div>`;
}

View File

@ -263,7 +263,7 @@ routes.get("/", (c) => {
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
scripts: `<script type="module" src="/modules/rtrips/trips-demo.js"></script>`,
scripts: `<script type="module" src="/modules/rtrips/folk-trips-planner.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rtrips/trips.css">`,
}));
}

View File

@ -1,301 +1,116 @@
/**
* rTube demo page server-rendered HTML body.
*
* Video library with sidebar, search, player area, download/copy-link buttons.
* Client-side tube-demo.ts handles selection, filtering, and playback.
* 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.
*/
/* ─── Seed Data ─────────────────────────────────────────────── */
interface DemoVideo {
name: string;
size: number;
}
const DEMO_VIDEOS: DemoVideo[] = [
{ name: "lac-blanc-test-footage.mp4", size: 245_000_000 },
{ name: "chamonix-arrival-timelapse.mp4", size: 128_000_000 },
{ name: "matterhorn-sunset-4k.mp4", size: 512_000_000 },
{ name: "paragliding-zermatt.webm", size: 89_000_000 },
{ name: "glacier-paradise-walk.mp4", size: 340_000_000 },
{ name: "tre-cime-circuit-gopro.mp4", size: 420_000_000 },
{ name: "dolomites-drone-footage.mkv", size: 780_000_000 },
{ name: "group-dinner-recap.mp4", size: 67_000_000 },
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.",
},
];
/* ─── Helpers ──────────────────────────────────────────────── */
function formatSize(bytes: number): string {
if (!bytes) return "";
const units = ["B", "KB", "MB", "GB"];
let i = 0;
let b = bytes;
while (b >= 1024 && i < units.length - 1) {
b /= 1024;
i++;
}
return `${b.toFixed(1)} ${units[i]}`;
}
function getIcon(filename: string): string {
const ext = filename.split(".").pop()?.toLowerCase() || "";
if (["mp4", "webm", "mov"].includes(ext)) return "\u{1F3AC}";
if (["mkv", "avi"].includes(ext)) return "\u26A0\uFE0F";
return "\u{1F4C4}";
}
function isPlayable(filename: string): boolean {
const ext = filename.split(".").pop()?.toLowerCase() || "";
return ["mp4", "webm", "mov", "ogg", "m4v"].includes(ext);
}
/* ─── Render ─────────────────────────────────────────────── */
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 {
const videoListHTML = DEMO_VIDEOS.map(
(v) => `
<li class="rd-tube-item${!isPlayable(v.name) ? " rd-tube-item--unplayable" : ""}"
data-video="${v.name}" role="button" tabindex="0">
<span class="rd-tube-item__icon">${getIcon(v.name)}</span>
<span class="rd-tube-item__name" title="${v.name}">${v.name}</span>
<span class="rd-tube-item__size">${formatSize(v.size)}</span>
</li>`,
).join("\n");
return `
<div class="rd-root" style="--rd-accent-from:#ef4444; --rd-accent-to:#ec4899">
<div class="rd-root" style="--rd-accent-from:#ef4444; --rd-accent-to:#ec4899;">
<!-- Hero -->
<!-- Hero -->
<section class="rd-hero">
<h1>Video Library</h1>
<p class="rd-subtitle">Browse, preview, and download videos from the Alpine Explorer expedition</p>
<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} ${DEMO_VIDEOS.length} videos</span>
<span>\u{1F3AC} Video Library</span>
<span style="color:#475569">|</span>
<span>\u{1F4BE} ${formatSize(DEMO_VIDEOS.reduce((sum, v) => sum + v.size, 0))} total</span>
<span>\u{1F4E1} Live Streaming</span>
<span style="color:#475569">|</span>
<span>\u{1F3D4} Alpine Explorer 2026</span>
<span>\u{1F4E4} Uploads</span>
<span style="color:#475569">|</span>
<span>\u{1F517} Direct Links</span>
</div>
</section>
<!-- Two-column layout -->
<div class="rd-section">
<div class="rd-tube-layout">
<!-- Sidebar -->
<aside class="rd-tube-sidebar" id="rd-video-sidebar">
<input type="text" id="rd-video-search" class="rd-tube-search"
placeholder="Search videos..." autocomplete="off" />
<h2 class="rd-tube-sidebar__heading">Library</h2>
<ul class="rd-tube-list" id="rd-video-list">
${videoListHTML}
</ul>
<p class="rd-tube-empty" id="rd-video-empty" style="display:none">No videos found</p>
</aside>
<!-- Player area -->
<div class="rd-tube-player-wrap" id="rd-video-player-area">
<div class="rd-tube-player" id="rd-player-container">
<p class="rd-tube-placeholder" id="rd-player-placeholder">Select a video to play</p>
</div>
<!-- Info bar (hidden until a video is selected) -->
<div class="rd-tube-info" id="rd-video-info" style="display:none">
<p class="rd-tube-info__name" id="rd-video-name"></p>
<div class="rd-tube-info__actions">
<a id="rd-download-btn" href="#" download class="rd-btn rd-btn--ghost rd-btn--sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Download
</a>
<button id="rd-copy-link-btn" class="rd-btn rd-btn--ghost rd-btn--sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
Copy Link
</button>
</div>
</div>
</div>
<!-- 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>
</div>
</section>
<!-- CTA -->
<!-- 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 a private video hosting solution with streaming,
uploads, and live broadcasting all powered by Cloudflare R2.
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);">
<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>
<style>
/* ── Tube demo layout ── */
.rd-tube-layout {
display: grid;
grid-template-columns: 300px 1fr;
gap: 1.5rem;
align-items: start;
}
@media (max-width: 768px) {
.rd-tube-layout {
grid-template-columns: 1fr;
}
}
/* ── Sidebar ── */
.rd-tube-sidebar {
background: rgba(30, 41, 59, 0.5);
border: 1px solid rgba(51, 65, 85, 0.5);
border-radius: 1rem;
padding: 1rem;
max-height: 80vh;
overflow-y: auto;
}
.rd-tube-search {
width: 100%;
padding: 0.625rem 0.875rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #334155;
border-radius: 0.5rem;
color: #f1f5f9;
font-size: 0.875rem;
margin-bottom: 0.75rem;
outline: none;
transition: border-color 0.15s;
box-sizing: border-box;
}
.rd-tube-search::placeholder { color: #64748b; }
.rd-tube-search:focus { border-color: #ef4444; }
.rd-tube-sidebar__heading {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
margin: 0 0 0.5rem;
font-weight: 600;
}
/* ── Video list ── */
.rd-tube-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.rd-tube-item {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.5rem 0.625rem;
border-radius: 0.5rem;
cursor: pointer;
transition: background 0.15s;
font-size: 0.875rem;
border-left: 2px solid transparent;
}
.rd-tube-item:hover { background: rgba(51, 65, 85, 0.5); }
.rd-tube-item--active {
background: rgba(239, 68, 68, 0.15);
border-left-color: #ef4444;
}
.rd-tube-item--unplayable { opacity: 0.6; }
.rd-tube-item__icon { flex-shrink: 0; font-size: 1rem; }
.rd-tube-item__name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #e2e8f0;
}
.rd-tube-item__size {
font-size: 0.7rem;
color: #475569;
flex-shrink: 0;
}
.rd-tube-empty {
color: #64748b;
font-size: 0.875rem;
padding: 1rem;
text-align: center;
}
/* ── Player area ── */
.rd-tube-player-wrap { min-width: 0; }
.rd-tube-player {
background: #000;
border-radius: 1rem;
overflow: hidden;
aspect-ratio: 16 / 9;
display: flex;
align-items: center;
justify-content: center;
}
.rd-tube-player video {
width: 100%;
height: 100%;
object-fit: contain;
}
.rd-tube-placeholder {
color: #475569;
font-size: 1.125rem;
margin: 0;
}
.rd-tube-warning {
text-align: center;
padding: 2rem;
color: #e2e8f0;
}
.rd-tube-warning__icon { font-size: 2.5rem; margin-bottom: 0.75rem; }
.rd-tube-warning__ext { font-weight: 700; }
.rd-tube-warning__hint {
font-size: 0.875rem;
color: #64748b;
margin-top: 0.5rem;
}
/* ── Info bar ── */
.rd-tube-info {
margin-top: 0.75rem;
background: rgba(30, 41, 59, 0.5);
border: 1px solid rgba(51, 65, 85, 0.5);
border-radius: 0.75rem;
padding: 0.875rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem;
}
.rd-tube-info__name {
font-weight: 500;
color: #f1f5f9;
margin: 0;
font-size: 0.9375rem;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rd-tube-info__actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
/* ── Button size variant ── */
.rd-btn--sm {
font-size: 0.8125rem;
padding: 0.375rem 0.75rem;
gap: 0.375rem;
}
</style>`;
</div>`;
}

View File

@ -201,7 +201,7 @@ routes.get("/", (c) => {
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
scripts: `<script type="module" src="/modules/rtube/tube-demo.js"></script>`,
scripts: `<script type="module" src="/modules/rtube/folk-video-player.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rtube/tube.css">`,
}));
}

View File

@ -1,68 +1,116 @@
/**
* rVote demo page server-rendered HTML body.
*
* Returns the static HTML skeleton for the interactive poll demo.
* The client-side vote-demo.ts populates #rd-polls-container via WebSocket.
* 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">
<div class="rd-root" style="--rd-accent-from:#f97316; --rd-accent-to:#fb923c;">
<!-- Hero -->
<div class="rd-hero">
<!-- 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">Interactive polls synced in real-time across all r* demos</p>
<p class="rd-subtitle">Credit-weighted conviction voting for collaborative governance decisions</p>
<div class="rd-meta">
<span>Vote on options with +/- buttons</span>
<span>Changes sync instantly via WebSocket</span>
<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>
</div>
</section>
<!-- Connection bar -->
<div class="rd-section rd-section--narrow">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.5rem; flex-wrap:wrap; gap:0.75rem">
<div style="display:flex; align-items:center; gap:0.75rem; flex-wrap:wrap">
<span id="rd-conn-badge" class="rd-status rd-status--disconnected">Disconnected</span>
<span class="rd-badge rd-badge--orange" style="font-size:0.7rem">Live &mdash; synced across all r* demos</span>
</div>
<button id="rd-reset-btn" class="rd-btn rd-btn--ghost" disabled>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
Reset Demo
</button>
<!-- 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>
<!-- Loading state (shown until first snapshot) -->
<div id="rd-loading" class="rd-card" style="border:2px dashed rgba(100,116,139,0.4); display:none">
<div class="rd-card-body" style="padding:3rem; text-align:center">
<div style="width:2rem; height:2rem; margin:0 auto 0.75rem; border:3px solid rgba(249,115,22,0.2); border-top-color:#f97316; border-radius:50%; animation:rd-spin 0.8s linear infinite"></div>
<p class="rd-text-muted">Connecting to rSpace...</p>
</div>
<!-- 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>
<style>@keyframes rd-spin { to { transform: rotate(360deg); } }</style>
</section>
<!-- Empty state (no polls after connection) -->
<div id="rd-empty" class="rd-card" style="border:2px dashed rgba(100,116,139,0.4); display:none">
<div class="rd-card-body" style="padding:3rem; text-align:center">
<div style="font-size:2rem; margin-bottom:0.75rem">🗳</div>
<p class="rd-text-muted">No polls found. Try resetting the demo.</p>
</div>
<!-- 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>
<!-- Poll cards container (populated by vote-demo.ts) -->
<div id="rd-polls-container" style="display:flex; flex-direction:column; gap:1rem"></div>
</div>
<!-- CTA -->
<div class="rd-section rd-section--narrow">
<!-- CTA -->
<section class="rd-section rd-section--narrow">
<div class="rd-cta">
<h2>Build with rVote</h2>
<p>Run conviction-weighted polls and governance decisions in your own community space.</p>
<a href="/create-space" style="background:linear-gradient(135deg,#f97316,#fb923c); box-shadow:0 4px 12px rgba(249,115,22,0.3)">
<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>
</div>
</section>
</div>`;
}

View File

@ -337,8 +337,7 @@ routes.get("/", (c) => {
modules: getModuleInfoList(),
theme: "dark",
body: renderDemo(),
demoScripts: `<script type="module" src="/lib/demo-sync.js"></script>
<script type="module" src="/modules/rvote/vote-demo.js"></script>`,
scripts: `<script type="module" src="/modules/rvote/folk-vote-dashboard.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rvote/vote.css">`,
}));
}