feat: bare-domain sub-pages show info/marketing pages + save-gate
Bare-domain URLs like rspace.online/rsocials/thread now render an info
page with CTAs instead of silently serving the functional app. The
functional app only appears inside a {space} context (e.g.
demo.rspace.online/rsocials/thread). API routes still pass through.
- Add SubPageInfo interface to shared/module.ts
- Add renderSubPageInfo() renderer to server/shell.ts
- Modify bare-domain routing: api/ passthrough → info page → demo fallback
- Add subPageInfos to 8 modules (rsocials, rflows, rnetwork, rtrips,
rbooks, rphotos, rinbox, rsplat)
- Add window.__rspaceSaveGate() auth prompt on write operations
- Wire save-gate into rsocials Thread Builder save handler
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1d0e82294b
commit
59bdd741be
|
|
@ -455,6 +455,20 @@ export const booksModule: RSpaceModule = {
|
||||||
{ path: "collections", name: "Collections", icon: "📑", description: "Curated book collections" },
|
{ 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) {
|
async onSpaceCreate(ctx: SpaceLifecycleContext) {
|
||||||
// Books are global, not space-scoped (for now). No-op.
|
// 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: "budgets", name: "Budgets", icon: "💰", description: "Budget allocations and funnels" },
|
||||||
{ path: "flows", name: "Flows", icon: "🌊", description: "Revenue and resource flow visualizations" },
|
{ 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"],
|
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: "connections", name: "Connections", icon: "🤝", description: "Community member connections" },
|
||||||
{ path: "groups", name: "Groups", icon: "👥", description: "Relationship groups and circles" },
|
{ 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: "albums", name: "Albums", icon: "📸", description: "Photo albums and collections" },
|
||||||
{ path: "galleries", name: "Galleries", icon: "🖼️", description: "Public photo galleries" },
|
{ 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 ──
|
// ── Draft management ──
|
||||||
async function saveDraft() {
|
async function saveDraft() {
|
||||||
|
if (window.__rspaceSaveGate && !window.__rspaceSaveGate(saveDraft)) return;
|
||||||
const tweets = textarea.value.split(/\\n---\\n/).map(t => t.trim()).filter(Boolean);
|
const tweets = textarea.value.split(/\\n---\\n/).map(t => t.trim()).filter(Boolean);
|
||||||
if (!tweets.length) return;
|
if (!tweets.length) return;
|
||||||
|
|
||||||
|
|
@ -1807,4 +1808,37 @@ export const socialsModule: RSpaceModule = {
|
||||||
{ path: "campaigns", name: "Campaigns", icon: "📢", description: "Social media campaigns" },
|
{ path: "campaigns", name: "Campaigns", icon: "📢", description: "Social media campaigns" },
|
||||||
{ path: "posts", name: "Posts", icon: "📱", description: "Social feed posts across platforms" },
|
{ 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: [
|
outputPaths: [
|
||||||
{ path: "drawings", name: "Drawings", icon: "🔮", description: "3D Gaussian splat drawings" },
|
{ 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) {
|
async onInit(ctx) {
|
||||||
_syncServer = ctx.syncServer;
|
_syncServer = ctx.syncServer;
|
||||||
console.log("[Splat] Automerge document store ready");
|
console.log("[Splat] Automerge document store ready");
|
||||||
|
|
|
||||||
|
|
@ -554,4 +554,18 @@ export const tripsModule: RSpaceModule = {
|
||||||
outputPaths: [
|
outputPaths: [
|
||||||
{ path: "itineraries", name: "Itineraries", icon: "🗓️", description: "Trip itineraries with bookings and activities" },
|
{ 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 { scheduleModule } from "../modules/rschedule/mod";
|
||||||
import { spaces, createSpace, resolveCallerRole, roleAtLeast } from "./spaces";
|
import { spaces, createSpace, resolveCallerRole, roleAtLeast } from "./spaces";
|
||||||
import type { SpaceRoleString } 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 { renderOutputListPage } from "./output-list";
|
||||||
import { renderMainLanding, renderSpaceDashboard } from "./landing";
|
import { renderMainLanding, renderSpaceDashboard } from "./landing";
|
||||||
import { fetchLandingPage } from "./landing-proxy";
|
import { fetchLandingPage } from "./landing-proxy";
|
||||||
|
|
@ -1911,7 +1911,29 @@ const server = Bun.serve<WSData>({
|
||||||
});
|
});
|
||||||
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
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 normalizedPath = "/" + [firstSegment, ...pathSegments.slice(1)].join("/");
|
||||||
const rewrittenPath = `/demo${normalizedPath}`;
|
const rewrittenPath = `/demo${normalizedPath}`;
|
||||||
const rewrittenUrl = new URL(rewrittenPath + url.search, `http://localhost:${PORT}`);
|
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.
|
* 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";
|
import { getDocumentData } from "./community-store";
|
||||||
|
|
||||||
/** Extract enabledModules and encryption status from a loaded space. */
|
/** 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';
|
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 ──
|
// ── Private space access gate ──
|
||||||
// If the space is private and no session exists, show a sign-in gate
|
// If the space is private and no session exists, show a sign-in gate
|
||||||
(function() {
|
(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) ──
|
// ── Onboarding page (empty rApp state) ──
|
||||||
|
|
||||||
export interface OnboardingOptions {
|
export interface OnboardingOptions {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,24 @@ export interface DocSchema<T = unknown> {
|
||||||
init: () => T;
|
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. */
|
/** A browsable content type that a module produces. */
|
||||||
export interface OutputPath {
|
export interface OutputPath {
|
||||||
/** URL segment: "notebooks" */
|
/** URL segment: "notebooks" */
|
||||||
|
|
@ -100,6 +118,8 @@ export interface RSpaceModule {
|
||||||
outputPaths?: OutputPath[];
|
outputPaths?: OutputPath[];
|
||||||
/** Optional: render rich landing page body HTML */
|
/** Optional: render rich landing page body HTML */
|
||||||
landingPage?: () => string;
|
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 */
|
/** Optional: external app to embed via iframe when ?view=app */
|
||||||
externalApp?: {
|
externalApp?: {
|
||||||
url: string;
|
url: string;
|
||||||
|
|
@ -146,6 +166,7 @@ export interface ModuleInfo {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
outputPaths?: OutputPath[];
|
outputPaths?: OutputPath[];
|
||||||
|
subPageInfos?: Array<{ path: string; title: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getModuleInfoList(): ModuleInfo[] {
|
export function getModuleInfoList(): ModuleInfo[] {
|
||||||
|
|
@ -163,5 +184,6 @@ export function getModuleInfoList(): ModuleInfo[] {
|
||||||
...(m.landingPage ? { hasLandingPage: true } : {}),
|
...(m.landingPage ? { hasLandingPage: true } : {}),
|
||||||
...(m.externalApp ? { externalApp: m.externalApp } : {}),
|
...(m.externalApp ? { externalApp: m.externalApp } : {}),
|
||||||
...(m.outputPaths ? { outputPaths: m.outputPaths } : {}),
|
...(m.outputPaths ? { outputPaths: m.outputPaths } : {}),
|
||||||
|
...(m.subPageInfos ? { subPageInfos: m.subPageInfos.map(s => ({ path: s.path, title: s.title })) } : {}),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue