Merge branch 'dev'
This commit is contained in:
commit
58c4aaa4b9
|
|
@ -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.
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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." },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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." },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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." },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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." },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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." },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<WSData>({
|
|||
});
|
||||
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}`);
|
||||
|
|
|
|||
131
server/shell.ts
131
server/shell.ts
|
|
@ -5,7 +5,7 @@
|
|||
* switchers + identity, <main> 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
|
||||
? `<div class="rl-section">
|
||||
<div class="rl-container">
|
||||
<div class="rl-grid-3">
|
||||
${subPage.features.map(f => `<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">${f.icon}</div>
|
||||
<h3>${escapeHtml(f.title)}</h3>
|
||||
<p>${escapeHtml(f.text)}</p>
|
||||
</div>`).join("\n ")}
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const bodyContent = subPage.bodyHTML
|
||||
? subPage.bodyHTML()
|
||||
: `<div class="rl-hero">
|
||||
<span class="rl-tagline">${escapeHtml(subPage.tagline)}</span>
|
||||
<h1 class="rl-heading">${escapeHtml(subPage.title)}</h1>
|
||||
<p class="rl-subtext">${escapeHtml(subPage.description)}</p>
|
||||
<div class="rl-cta-row">
|
||||
<a href="${demoUrl}" class="rl-cta-primary" id="sp-primary">Try Demo</a>
|
||||
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
|
||||
</div>
|
||||
</div>
|
||||
${featuresGrid}
|
||||
<div class="rl-back">
|
||||
<a href="/${escapeAttr(mod.id)}">← Back to ${escapeHtml(mod.name)}</a>
|
||||
</div>`;
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>${subPage.icon}</text></svg>">
|
||||
<title>${escapeHtml(subPage.title)} — ${escapeHtml(mod.name)} | rSpace</title>
|
||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
||||
<link rel="stylesheet" href="/theme.css?v=1">
|
||||
<link rel="stylesheet" href="/shell.css?v=8">
|
||||
<style>${MODULE_LANDING_CSS}</style>
|
||||
<style>${RICH_LANDING_CSS}</style>
|
||||
<script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="rstack-header">
|
||||
<div class="rstack-header__left">
|
||||
<a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
|
||||
<rstack-app-switcher current="${escapeAttr(mod.id)}"></rstack-app-switcher>
|
||||
<a class="rstack-header__demo-btn" href="${demoUrl}">Try Demo</a>
|
||||
</div>
|
||||
<div class="rstack-header__center">
|
||||
<rstack-mi></rstack-mi>
|
||||
</div>
|
||||
<div class="rstack-header__right">
|
||||
<rstack-identity></rstack-identity>
|
||||
</div>
|
||||
</header>
|
||||
${bodyContent}
|
||||
<script type="module">
|
||||
import '/shell.js?v=8';
|
||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||
function _updateDemoBtn() {
|
||||
var btn = document.querySelector('.rstack-header__demo-btn');
|
||||
if (!btn) return;
|
||||
try {
|
||||
var raw = localStorage.getItem('encryptid_session');
|
||||
if (raw && JSON.parse(raw)?.accessToken) { btn.setAttribute('data-hide', ''); }
|
||||
else { btn.removeAttribute('data-hide'); }
|
||||
} catch(e) {}
|
||||
}
|
||||
_updateDemoBtn();
|
||||
document.addEventListener('auth-change', _updateDemoBtn);
|
||||
try {
|
||||
var raw = localStorage.getItem('encryptid_session');
|
||||
if (raw) {
|
||||
var session = JSON.parse(raw);
|
||||
if (session?.claims?.username) {
|
||||
var username = session.claims.username.toLowerCase();
|
||||
var primary = document.getElementById('sp-primary');
|
||||
if (primary) {
|
||||
primary.textContent = 'Open in My Space';
|
||||
primary.href = 'https://' + username + '.rspace.online/${escapeAttr(mod.id)}/${escapeAttr(subPage.path)}';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// ── Onboarding page (empty rApp state) ──
|
||||
|
||||
export interface OnboardingOptions {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,24 @@ export interface DocSchema<T = unknown> {
|
|||
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 })) } : {}),
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue