diff --git a/modules/rbooks/mod.ts b/modules/rbooks/mod.ts index d4ed66a..0477800 100644 --- a/modules/rbooks/mod.ts +++ b/modules/rbooks/mod.ts @@ -455,6 +455,20 @@ export const booksModule: RSpaceModule = { { path: "collections", name: "Collections", icon: "๐Ÿ“‘", description: "Curated book collections" }, ], + subPageInfos: [ + { + path: "read", + title: "Flipbook Reader", + icon: "๐Ÿ“–", + tagline: "rBooks Tool", + description: "Read PDFs in a beautiful flipbook layout with page-turn animations. Bookmark pages, adjust display, and read collaboratively.", + features: [ + { icon: "๐Ÿ“„", title: "Flipbook View", text: "Page-turning animations bring PDFs to life in a book-like reading experience." }, + { icon: "๐Ÿ”–", title: "Bookmarks & Notes", text: "Mark your place and add annotations that sync across devices." }, + { icon: "๐Ÿ‘ฅ", title: "Shared Library", text: "Upload PDFs to your community library for everyone to discover and read." }, + ], + }, + ], async onSpaceCreate(ctx: SpaceLifecycleContext) { // Books are global, not space-scoped (for now). No-op. }, diff --git a/modules/rflows/mod.ts b/modules/rflows/mod.ts index c265fbf..af3d67e 100644 --- a/modules/rflows/mod.ts +++ b/modules/rflows/mod.ts @@ -355,4 +355,18 @@ export const flowsModule: RSpaceModule = { { path: "budgets", name: "Budgets", icon: "๐Ÿ’ฐ", description: "Budget allocations and funnels" }, { path: "flows", name: "Flows", icon: "๐ŸŒŠ", description: "Revenue and resource flow visualizations" }, ], + subPageInfos: [ + { + path: "flow", + title: "Flow Viewer", + icon: "๐ŸŒŠ", + tagline: "rFlows Tool", + description: "Visualize a single budget flow โ€” deposits, withdrawals, funnel allocations, and real-time balance. Drill into transactions and manage outcomes.", + features: [ + { icon: "๐Ÿ“ˆ", title: "River Visualization", text: "See funds flow through funnels and outcomes as an animated river diagram." }, + { icon: "๐Ÿ’ธ", title: "Deposits & Withdrawals", text: "Track every transaction with full history and on-chain verification." }, + { icon: "๐ŸŽฏ", title: "Outcome Tracking", text: "Define funding outcomes and monitor how capital reaches its destination." }, + ], + }, + ], }; diff --git a/modules/rinbox/mod.ts b/modules/rinbox/mod.ts index a2686c4..55f1ed8 100644 --- a/modules/rinbox/mod.ts +++ b/modules/rinbox/mod.ts @@ -1081,4 +1081,18 @@ export const inboxModule: RSpaceModule = { }, ], acceptsFeeds: ["data"], + subPageInfos: [ + { + path: "about", + title: "About rInbox", + icon: "๐Ÿ“ฎ", + tagline: "rInbox", + description: "Collaborative email for communities โ€” shared mailboxes with multisig approval, threaded discussions, and team workflows.", + features: [ + { icon: "๐Ÿ“ฌ", title: "Shared Mailboxes", text: "Create shared inboxes that multiple team members can read and respond from." }, + { icon: "โœ…", title: "Multisig Approval", text: "Require multiple approvals before sending sensitive emails on behalf of the group." }, + { icon: "๐Ÿ’ฌ", title: "Internal Comments", text: "Discuss emails privately with your team before crafting a response." }, + ], + }, + ], }; diff --git a/modules/rnetwork/mod.ts b/modules/rnetwork/mod.ts index fd70a5b..f9472e1 100644 --- a/modules/rnetwork/mod.ts +++ b/modules/rnetwork/mod.ts @@ -343,4 +343,18 @@ export const networkModule: RSpaceModule = { { path: "connections", name: "Connections", icon: "๐Ÿค", description: "Community member connections" }, { path: "groups", name: "Groups", icon: "๐Ÿ‘ฅ", description: "Relationship groups and circles" }, ], + subPageInfos: [ + { + path: "crm", + title: "Community CRM", + icon: "๐Ÿ“‡", + tagline: "rNetwork Tool", + description: "Full-featured CRM for community relationship management โ€” contacts, companies, deals, and pipelines powered by Twenty CRM.", + features: [ + { icon: "๐Ÿ‘ค", title: "Contact Management", text: "Track people, organizations, and their roles in your community." }, + { icon: "๐Ÿ”—", title: "Relationship Graph", text: "Visualize how members connect and identify key connectors." }, + { icon: "๐Ÿ“Š", title: "Pipeline Tracking", text: "Manage opportunities and partnerships through customizable stages." }, + ], + }, + ], }; diff --git a/modules/rphotos/mod.ts b/modules/rphotos/mod.ts index d95495f..8a59542 100644 --- a/modules/rphotos/mod.ts +++ b/modules/rphotos/mod.ts @@ -161,4 +161,18 @@ export const photosModule: RSpaceModule = { { path: "albums", name: "Albums", icon: "๐Ÿ“ธ", description: "Photo albums and collections" }, { path: "galleries", name: "Galleries", icon: "๐Ÿ–ผ๏ธ", description: "Public photo galleries" }, ], + subPageInfos: [ + { + path: "album", + title: "Photo Album", + icon: "๐Ÿ–ผ๏ธ", + tagline: "rPhotos Tool", + description: "Browse a curated photo album with lightbox viewing, metadata display, and easy sharing. Powered by Immich.", + features: [ + { icon: "๐Ÿ”", title: "Lightbox Viewer", text: "Full-screen photo viewing with EXIF data, zoom, and slideshow mode." }, + { icon: "๐Ÿ“ค", title: "Easy Sharing", text: "Share individual photos or entire albums with a single link." }, + { icon: "๐Ÿท๏ธ", title: "Smart Tags", text: "AI-powered face recognition and object tagging for easy discovery." }, + ], + }, + ], }; diff --git a/modules/rsocials/mod.ts b/modules/rsocials/mod.ts index dcd8d5e..52da061 100644 --- a/modules/rsocials/mod.ts +++ b/modules/rsocials/mod.ts @@ -894,6 +894,7 @@ function renderThreadBuilderPage(space: string, threadData?: ThreadData | null): // โ”€โ”€ Draft management โ”€โ”€ async function saveDraft() { + if (window.__rspaceSaveGate && !window.__rspaceSaveGate(saveDraft)) return; const tweets = textarea.value.split(/\\n---\\n/).map(t => t.trim()).filter(Boolean); if (!tweets.length) return; @@ -1807,4 +1808,37 @@ export const socialsModule: RSpaceModule = { { path: "campaigns", name: "Campaigns", icon: "๐Ÿ“ข", description: "Social media campaigns" }, { path: "posts", name: "Posts", icon: "๐Ÿ“ฑ", description: "Social feed posts across platforms" }, ], + subPageInfos: [ + { + path: "thread", + title: "Thread Builder", + icon: "๐Ÿงต", + tagline: "rSocials Tool", + description: "Compose, preview, and schedule tweet threads with a live card-by-card preview. Save drafts, generate share images, and publish when ready.", + features: [ + { icon: "โœ๏ธ", title: "Live Preview", text: "See your thread as tweet cards in real time as you type, with character counts and thread numbering." }, + { icon: "๐Ÿ’พ", title: "Save & Edit Drafts", text: "Save thread drafts to your space, revisit and refine them before publishing." }, + { icon: "๐Ÿ–ผ๏ธ", title: "Share Images", text: "Auto-generate a branded share image of your thread for cross-posting." }, + ], + }, + { + path: "campaign", + title: "Campaign Manager", + icon: "๐Ÿ“ข", + tagline: "rSocials Tool", + description: "Plan and track multi-platform social media campaigns with scheduling, analytics, and team collaboration.", + features: [ + { icon: "๐Ÿ“…", title: "Schedule Posts", text: "Queue posts across platforms with a visual calendar timeline." }, + { icon: "๐Ÿ“Š", title: "Track Performance", text: "Monitor engagement metrics and campaign reach in one dashboard." }, + { icon: "๐Ÿ‘ฅ", title: "Team Workflow", text: "Draft, review, and approve posts collaboratively before publishing." }, + ], + }, + { + path: "threads", + title: "Thread Gallery", + icon: "๐Ÿ“‹", + tagline: "rSocials Tool", + description: "Browse all saved thread drafts in your community. Find inspiration, remix threads, or pick up where you left off.", + }, + ], }; diff --git a/modules/rsplat/mod.ts b/modules/rsplat/mod.ts index b071a9f..e4284af 100644 --- a/modules/rsplat/mod.ts +++ b/modules/rsplat/mod.ts @@ -733,6 +733,20 @@ export const splatModule: RSpaceModule = { outputPaths: [ { path: "drawings", name: "Drawings", icon: "๐Ÿ”ฎ", description: "3D Gaussian splat drawings" }, ], + subPageInfos: [ + { + path: "view", + title: "Splat Viewer", + icon: "๐Ÿ”ฎ", + tagline: "rSplat Tool", + description: "Explore 3D Gaussian splat captures in an interactive WebGL viewer. Orbit, zoom, and inspect photorealistic 3D scenes.", + features: [ + { icon: "๐Ÿ–ฑ๏ธ", title: "Interactive 3D", text: "Orbit, pan, and zoom through photorealistic 3D captures in your browser." }, + { icon: "๐Ÿ“ท", title: "Multi-Angle Capture", text: "View scenes reconstructed from hundreds of photos using Gaussian splatting." }, + { icon: "๐Ÿ“ค", title: "Upload & Share", text: "Upload .ply or .splat files and share interactive 3D views with anyone." }, + ], + }, + ], async onInit(ctx) { _syncServer = ctx.syncServer; console.log("[Splat] Automerge document store ready"); diff --git a/modules/rtrips/mod.ts b/modules/rtrips/mod.ts index 3deec1d..6403757 100644 --- a/modules/rtrips/mod.ts +++ b/modules/rtrips/mod.ts @@ -554,4 +554,18 @@ export const tripsModule: RSpaceModule = { outputPaths: [ { path: "itineraries", name: "Itineraries", icon: "๐Ÿ—“๏ธ", description: "Trip itineraries with bookings and activities" }, ], + subPageInfos: [ + { + path: "routes", + title: "Route Planner", + icon: "๐Ÿ—บ๏ธ", + tagline: "rTrips Tool", + description: "Plan multi-stop routes with distance and duration estimates. Optimize waypoints and export routes for navigation.", + features: [ + { icon: "๐Ÿ“", title: "Multi-Stop Planning", text: "Add waypoints, reorder stops, and calculate the optimal route." }, + { icon: "โฑ๏ธ", title: "Time & Distance", text: "See real-time estimates for travel time and distances between stops." }, + { icon: "๐Ÿ—บ๏ธ", title: "Map Visualization", text: "View your full route on an interactive map with turn-by-turn preview." }, + ], + }, + ], }; diff --git a/server/index.ts b/server/index.ts index 0195e6c..7126a88 100644 --- a/server/index.ts +++ b/server/index.ts @@ -71,7 +71,7 @@ import { designModule } from "../modules/rdesign/mod"; import { scheduleModule } from "../modules/rschedule/mod"; import { spaces, createSpace, resolveCallerRole, roleAtLeast } from "./spaces"; import type { SpaceRoleString } from "./spaces"; -import { renderShell, renderModuleLanding, renderOnboarding } from "./shell"; +import { renderShell, renderModuleLanding, renderSubPageInfo, renderOnboarding } from "./shell"; import { renderOutputListPage } from "./output-list"; import { renderMainLanding, renderSpaceDashboard } from "./landing"; import { fetchLandingPage } from "./landing-proxy"; @@ -1911,7 +1911,29 @@ const server = Bun.serve({ }); return new Response(html, { headers: { "Content-Type": "text/html" } }); } - // rspace.online/{moduleId}/sub-path โ†’ rewrite to demo space internally + // rspace.online/{moduleId}/sub-path + const secondSegment = pathSegments[1]?.toLowerCase(); + + // 1. API routes always pass through to demo + if (secondSegment === "api") { + const normalizedPath = "/" + [firstSegment, ...pathSegments.slice(1)].join("/"); + const rewrittenPath = `/demo${normalizedPath}`; + const rewrittenUrl = new URL(rewrittenPath + url.search, `http://localhost:${PORT}`); + return app.fetch(new Request(rewrittenUrl, req)); + } + + // 2. If sub-path matches a subPageInfo โ†’ render info page + const subPageInfo = mod.subPageInfos?.find(sp => sp.path === secondSegment); + if (subPageInfo) { + const html = renderSubPageInfo({ + subPage: subPageInfo, + module: mod, + modules: getModuleInfoList(), + }); + return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } }); + } + + // 3. Fallback: rewrite to demo space (backward compat) const normalizedPath = "/" + [firstSegment, ...pathSegments.slice(1)].join("/"); const rewrittenPath = `/demo${normalizedPath}`; const rewrittenUrl = new URL(rewrittenPath + url.search, `http://localhost:${PORT}`); diff --git a/server/shell.ts b/server/shell.ts index dd7a8a5..50fdf94 100644 --- a/server/shell.ts +++ b/server/shell.ts @@ -5,7 +5,7 @@ * switchers + identity,
with module content, shell script + styles. */ -import type { ModuleInfo } from "../shared/module"; +import type { ModuleInfo, SubPageInfo } from "../shared/module"; import { getDocumentData } from "./community-store"; /** Extract enabledModules and encryption status from a loaded space. */ @@ -201,6 +201,27 @@ export function renderShell(opts: ShellOptions): string { if (el) el.style.display = 'none'; }; + // โ”€โ”€ Save-gate: prompt sign-in on write when unauthenticated โ”€โ”€ + window.__rspaceSaveGate = function(callback) { + try { + var raw = localStorage.getItem('encryptid_session'); + if (raw) { + var session = JSON.parse(raw); + if (session && session.accessToken) return true; // authenticated + } + } catch(e) {} + // Not authenticated โ€” show modal + var identity = document.querySelector('rstack-identity'); + if (identity && identity.showAuthModal) { + identity.showAuthModal({ + title: 'Sign in to save', + message: 'Sign in with EncryptID to save your work to your own rSpace.', + onSuccess: function() { if (typeof callback === 'function') callback(); } + }); + } + return false; + }; + // โ”€โ”€ Private space access gate โ”€โ”€ // If the space is private and no session exists, show a sign-in gate (function() { @@ -1043,6 +1064,114 @@ export const RICH_LANDING_CSS = ` } `; +// โ”€โ”€ Sub-page info page (bare-domain rspace.online/{moduleId}/{subPage}) โ”€โ”€ + +export interface SubPageInfoOptions { + /** The sub-page info to render */ + subPage: SubPageInfo; + /** The parent module info */ + module: ModuleInfo; + /** All available modules (for app switcher) */ + modules: ModuleInfo[]; +} + +export function renderSubPageInfo(opts: SubPageInfoOptions): string { + const { subPage, module: mod, modules } = opts; + const moduleListJSON = JSON.stringify(modules); + const demoUrl = `https://demo.rspace.online/${mod.id}/${subPage.path}`; + + const featuresGrid = subPage.features?.length + ? `
+
+
+ ${subPage.features.map(f => `
+
${f.icon}
+

${escapeHtml(f.title)}

+

${escapeHtml(f.text)}

+
`).join("\n ")} +
+
+
` + : ""; + + const bodyContent = subPage.bodyHTML + ? subPage.bodyHTML() + : `
+ ${escapeHtml(subPage.tagline)} +

${escapeHtml(subPage.title)}

+

${escapeHtml(subPage.description)}

+ +
+ ${featuresGrid} + `; + + return ` + + + + + + ${escapeHtml(subPage.title)} โ€” ${escapeHtml(mod.name)} | rSpace + + + + + + + + +
+
+ + + Try Demo +
+
+ +
+
+ +
+
+ ${bodyContent} + + +`; +} + // โ”€โ”€ Onboarding page (empty rApp state) โ”€โ”€ export interface OnboardingOptions { diff --git a/shared/module.ts b/shared/module.ts index 2128f85..7069bc6 100644 --- a/shared/module.ts +++ b/shared/module.ts @@ -34,6 +34,24 @@ export interface DocSchema { init: () => T; } +/** Info for a sub-page shown on bare-domain instead of the functional app. */ +export interface SubPageInfo { + /** URL segment (e.g. "thread", "flow", "crm") */ + path: string; + /** Display title (e.g. "Thread Builder") */ + title: string; + /** Emoji icon */ + icon: string; + /** Short tagline shown as a pill above the title */ + tagline: string; + /** 1-2 sentence description */ + description: string; + /** Optional feature cards for a grid section */ + features?: Array<{ icon: string; title: string; text: string }>; + /** Optional: fully custom body HTML (replaces generic template) */ + bodyHTML?: () => string; +} + /** A browsable content type that a module produces. */ export interface OutputPath { /** URL segment: "notebooks" */ @@ -100,6 +118,8 @@ export interface RSpaceModule { outputPaths?: OutputPath[]; /** Optional: render rich landing page body HTML */ landingPage?: () => string; + /** Info pages for sub-paths on bare domain (replaces demo rewrite with marketing page) */ + subPageInfos?: SubPageInfo[]; /** Optional: external app to embed via iframe when ?view=app */ externalApp?: { url: string; @@ -146,6 +166,7 @@ export interface ModuleInfo { name: string; }; outputPaths?: OutputPath[]; + subPageInfos?: Array<{ path: string; title: string }>; } export function getModuleInfoList(): ModuleInfo[] { @@ -163,5 +184,6 @@ export function getModuleInfoList(): ModuleInfo[] { ...(m.landingPage ? { hasLandingPage: true } : {}), ...(m.externalApp ? { externalApp: m.externalApp } : {}), ...(m.outputPaths ? { outputPaths: m.outputPaths } : {}), + ...(m.subPageInfos ? { subPageInfos: m.subPageInfos.map(s => ({ path: s.path, title: s.title })) } : {}), })); }