/** * Shell HTML renderer. * * Wraps module content in the shared rSpace layout: header with app/space * switchers + identity,
with module content, shell script + styles. */ import type { ModuleInfo } from "../shared/module"; export interface ShellOptions { /** Page */ title: string; /** Current module ID (highlighted in app switcher) */ moduleId: string; /** Current space slug */ spaceSlug: string; /** Space display name */ spaceName?: string; /** Module HTML content to inject into <main> */ body: string; /** Additional <script type="module"> tags for module-specific JS */ scripts?: string; /** Additional <link>/<style> tags for module-specific CSS */ styles?: string; /** List of available modules (for app switcher) */ modules: ModuleInfo[]; /** Theme for the header: 'dark' or 'light' */ theme?: "dark" | "light"; /** Extra <head> content (meta tags, preloads, etc.) */ head?: string; } export function renderShell(opts: ShellOptions): string { const { title, moduleId, spaceSlug, spaceName, body, scripts = "", styles = "", modules, theme = "dark", head = "", } = opts; const moduleListJSON = JSON.stringify(modules); const shellDemoUrl = `https://demo.rspace.online/${escapeAttr(moduleId)}`; 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'>🌌</text></svg>"> <title>${escapeHtml(title)} ${styles} ${head}
${body}
${renderWelcomeOverlay()} ${scripts} `; } // ── External app iframe shell ── export interface ExternalAppShellOptions { /** Page */ title: string; /** Current module ID */ moduleId: string; /** Current space slug */ spaceSlug: string; /** Space display name */ spaceName?: string; /** List of available modules */ modules: ModuleInfo[]; /** External app URL to embed */ appUrl: string; /** External app display name */ appName: string; /** Theme */ theme?: "dark" | "light"; } export function renderExternalAppShell(opts: ExternalAppShellOptions): string { const { title, moduleId, spaceSlug, spaceName, modules, appUrl, appName, theme = "dark", } = opts; const moduleListJSON = JSON.stringify(modules); const demoUrl = `?view=demo`; 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'>🌌</text></svg>"> <title>${escapeHtml(title)}
Loading ${escapeHtml(appName)}…
Open in new tab ↗
`; } // ── Welcome overlay (quarter-screen popup for first-time visitors on demo) ── function renderWelcomeOverlay(): string { return ` `; } const WELCOME_CSS = ` .rspace-welcome { position: fixed; bottom: 20px; right: 20px; z-index: 10000; display: none; align-items: flex-end; justify-content: flex-end; } .rspace-welcome__popup { position: relative; width: min(380px, 44vw); max-height: 50vh; background: #1e293b; border: 1px solid rgba(255,255,255,0.12); border-radius: 16px; padding: 24px 24px 18px; box-shadow: 0 20px 60px rgba(0,0,0,0.5); color: #e2e8f0; overflow-y: auto; animation: rspace-welcome-in 0.3s ease-out; } @keyframes rspace-welcome-in { from { opacity: 0; transform: translateY(20px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } .rspace-welcome__close { position: absolute; top: 10px; right: 12px; background: none; border: none; color: #64748b; font-size: 1.4rem; cursor: pointer; line-height: 1; padding: 4px; border-radius: 4px; } .rspace-welcome__close:hover { color: #e2e8f0; background: rgba(255,255,255,0.08); } .rspace-welcome__title { font-size: 1.35rem; margin: 0 0 8px; background: linear-gradient(135deg, #14b8a6, #22d3ee); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .rspace-welcome__text { font-size: 0.85rem; color: #94a3b8; margin: 0 0 14px; line-height: 1.55; } .rspace-welcome__text strong { color: #e2e8f0; } .rspace-welcome__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; margin-bottom: 14px; font-size: 0.8rem; color: #cbd5e1; } .rspace-welcome__grid span { padding: 3px 0; } .rspace-welcome__actions { display: flex; gap: 8px; margin-bottom: 12px; } .rspace-welcome__btn { padding: 8px 16px; border-radius: 8px; font-size: 0.82rem; font-weight: 600; text-decoration: none; cursor: pointer; border: none; transition: transform 0.15s, box-shadow 0.15s; } .rspace-welcome__btn:hover { transform: translateY(-1px); } .rspace-welcome__btn--primary { background: linear-gradient(135deg, #14b8a6, #0d9488); color: white; box-shadow: 0 2px 8px rgba(20,184,166,0.3); } .rspace-welcome__btn--secondary { background: rgba(255,255,255,0.08); color: #94a3b8; } .rspace-welcome__btn--secondary:hover { color: #e2e8f0; } .rspace-welcome__footer { display: flex; align-items: center; gap: 6px; } .rspace-welcome__link { font-size: 0.72rem; color: #64748b; text-decoration: none; transition: color 0.15s; } .rspace-welcome__link:hover { color: #c4b5fd; } .rspace-welcome__dot { color: #475569; font-size: 0.6rem; } @media (max-width: 600px) { .rspace-welcome { bottom: 12px; right: 12px; left: 12px; } .rspace-welcome__popup { width: 100%; max-width: none; } } `; // ── Module landing page (bare-domain rspace.online/{moduleId}) ── export interface ModuleLandingOptions { /** The module to render a landing page for */ module: ModuleInfo; /** All available modules (for app switcher) */ modules: ModuleInfo[]; /** Theme */ theme?: "dark" | "light"; /** Rich body HTML from a module's landing.ts (replaces generic hero) */ bodyHTML?: string; } export function renderModuleLanding(opts: ModuleLandingOptions): string { const { module: mod, modules, theme = "dark", bodyHTML } = opts; const moduleListJSON = JSON.stringify(modules); const demoUrl = `https://demo.rspace.online/${mod.id}`; const cssBlock = bodyHTML ? `\n ` : ``; const bodyContent = bodyHTML ? bodyHTML : `
${mod.icon}

${escapeHtml(mod.name)}

${escapeHtml(mod.description)}

← Back to rSpace
`; return ` ${escapeHtml(mod.name)} — rSpace ${cssBlock}
${bodyContent} `; } export const MODULE_LANDING_CSS = ` body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); color: white; min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding-top: 56px; } .ml-hero { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: calc(80vh - 56px); width: 100%; } .ml-container { text-align: center; max-width: 560px; padding: 40px 20px; } .ml-icon { font-size: 4rem; display: block; margin-bottom: 1rem; } .ml-name { font-size: 2.5rem; margin-bottom: 0.75rem; background: linear-gradient(135deg, #14b8a6, #22d3ee); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .ml-desc { font-size: 1.15rem; color: #94a3b8; margin-bottom: 2.5rem; line-height: 1.6; } .ml-ctas { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; } .ml-cta-primary { display: inline-block; padding: 14px 32px; border-radius: 8px; background: linear-gradient(135deg, #14b8a6, #0d9488); color: white; font-size: 1rem; font-weight: 600; text-decoration: none; transition: transform 0.2s, box-shadow 0.2s; } .ml-cta-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(20,184,166,0.3); } .ml-cta-secondary { display: inline-block; padding: 14px 32px; border-radius: 8px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.2); color: #94a3b8; font-size: 1rem; font-weight: 600; text-decoration: none; transition: transform 0.2s, border-color 0.2s, color 0.2s; } .ml-cta-secondary:hover { transform: translateY(-2px); border-color: rgba(255,255,255,0.4); color: white; } .ml-back { padding: 2rem 0 3rem; text-align: center; } .ml-back a { font-size: 0.85rem; color: #64748b; text-decoration: none; transition: color 0.2s; } .ml-back a:hover { color: #e2e8f0; } @media (max-width: 600px) { .ml-name { font-size: 2rem; } .ml-icon { font-size: 3rem; } } `; export const RICH_LANDING_CSS = ` /* ── Rich Landing Page Utilities ── */ .rl-section { border-top: 1px solid rgba(255,255,255,0.06); padding: 4rem 1.5rem; } .rl-section--alt { background: rgba(255,255,255,0.015); } .rl-container { max-width: 1100px; margin: 0 auto; } .rl-hero { text-align: center; padding: 5rem 1.5rem 3rem; max-width: 820px; margin: 0 auto; } .rl-tagline { display: inline-block; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: #14b8a6; background: rgba(20,184,166,0.1); border: 1px solid rgba(20,184,166,0.2); padding: 0.35rem 1rem; border-radius: 9999px; margin-bottom: 1.5rem; } .rl-heading { font-size: 2rem; font-weight: 700; line-height: 1.15; margin-bottom: 0.75rem; letter-spacing: -0.01em; background: linear-gradient(135deg, #14b8a6, #22d3ee); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .rl-hero .rl-heading { font-size: 2.5rem; } @media (min-width: 640px) { .rl-hero .rl-heading { font-size: 3rem; } } .rl-subtitle { font-size: 1.25rem; font-weight: 500; color: #cbd5e1; margin-bottom: 1rem; letter-spacing: -0.005em; } .rl-hero .rl-subtitle { font-size: 1.35rem; } @media (min-width: 640px) { .rl-hero .rl-subtitle { font-size: 1.5rem; } } .rl-subtext { font-size: 1.05rem; color: #94a3b8; line-height: 1.65; max-width: 640px; margin: 0 auto 2rem; } .rl-hero .rl-subtext { font-size: 1.15rem; } /* Grids */ .rl-grid-2 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; } .rl-grid-3 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; } .rl-grid-4 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; } @media (min-width: 640px) { .rl-grid-2 { grid-template-columns: repeat(2, 1fr); } .rl-grid-3 { grid-template-columns: repeat(3, 1fr); } .rl-grid-4 { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 1024px) { .rl-grid-4 { grid-template-columns: repeat(4, 1fr); } } /* Card */ .rl-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 1rem; padding: 1.75rem; transition: border-color 0.2s; } .rl-card:hover { border-color: rgba(20,184,166,0.3); } .rl-card h3 { font-size: 0.95rem; font-weight: 600; color: #e2e8f0; margin-bottom: 0.5rem; } .rl-card p { font-size: 0.875rem; color: #94a3b8; line-height: 1.6; } .rl-card--center { text-align: center; } /* Step circles */ .rl-step { display: flex; flex-direction: column; align-items: center; text-align: center; } .rl-step__num { width: 2.5rem; height: 2.5rem; border-radius: 9999px; background: rgba(20,184,166,0.1); color: #14b8a6; display: flex; align-items: center; justify-content: center; font-size: 0.8rem; font-weight: 700; margin-bottom: 0.75rem; } .rl-step h3 { font-size: 0.95rem; font-weight: 600; color: #e2e8f0; margin-bottom: 0.25rem; } .rl-step p { font-size: 0.82rem; color: #94a3b8; line-height: 1.55; } /* CTA row */ .rl-cta-row { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; margin-top: 2rem; } .rl-cta-primary { display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem; background: linear-gradient(135deg, #14b8a6, #0d9488); color: white; font-size: 0.95rem; font-weight: 600; text-decoration: none; transition: transform 0.2s, box-shadow 0.2s; } .rl-cta-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(20,184,166,0.3); } .rl-cta-secondary { display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.15); color: #94a3b8; font-size: 0.95rem; font-weight: 600; text-decoration: none; transition: transform 0.2s, border-color 0.2s, color 0.2s; } .rl-cta-secondary:hover { transform: translateY(-2px); border-color: rgba(255,255,255,0.35); color: white; } /* Check list */ .rl-check-list { list-style: none; padding: 0; margin: 0; } .rl-check-list li { display: flex; align-items: flex-start; gap: 0.5rem; font-size: 0.875rem; color: #94a3b8; line-height: 1.55; padding: 0.35rem 0; } .rl-check-list li::before { content: "✓"; color: #14b8a6; font-weight: 700; flex-shrink: 0; margin-top: 0.05em; } .rl-check-list li strong { color: #e2e8f0; font-weight: 600; } /* Badge */ .rl-badge { display: inline-block; font-size: 0.65rem; font-weight: 700; color: white; background: #14b8a6; padding: 0.15rem 0.5rem; border-radius: 9999px; } /* Divider */ .rl-divider { display: flex; align-items: center; gap: 0.75rem; margin: 1.5rem 0; } .rl-divider::before, .rl-divider::after { content: ""; flex: 1; height: 1px; background: rgba(255,255,255,0.06); } .rl-divider span { font-size: 0.75rem; color: #64748b; white-space: nowrap; } /* Icon box */ .rl-icon-box { width: 3rem; height: 3rem; border-radius: 0.75rem; background: rgba(20,184,166,0.12); color: #14b8a6; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; margin-bottom: 1rem; } .rl-card--center .rl-icon-box { margin: 0 auto 1rem; } /* Integration (2-col with icon) */ .rl-integration { display: flex; align-items: flex-start; gap: 1rem; background: rgba(20,184,166,0.04); border: 1px solid rgba(20,184,166,0.15); border-radius: 1rem; padding: 1.5rem; } .rl-integration h3 { font-size: 0.95rem; font-weight: 600; color: #e2e8f0; margin-bottom: 0.35rem; } .rl-integration p { font-size: 0.85rem; color: #94a3b8; line-height: 1.55; } /* Back link */ .rl-back { padding: 2rem 0 3rem; text-align: center; } .rl-back a { font-size: 0.85rem; color: #64748b; text-decoration: none; transition: color 0.2s; } .rl-back a:hover { color: #e2e8f0; } /* Progress bar */ .rl-progress { height: 0.5rem; border-radius: 9999px; background: rgba(255,255,255,0.06); overflow: hidden; } .rl-progress__fill { height: 100%; border-radius: 9999px; background: #14b8a6; } /* Tier row */ .rl-tier { display: flex; gap: 0.5rem; margin: 1rem 0; } .rl-tier__item { flex: 1; text-align: center; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.06); padding: 0.5rem; font-size: 0.75rem; } .rl-tier__item--active { border-color: rgba(20,184,166,0.4); background: rgba(20,184,166,0.05); color: #14b8a6; } .rl-tier__item--active strong { color: #14b8a6; } /* Temporal zoom bar */ .rl-zoom-bar { display: flex; flex-direction: column; gap: 0.5rem; } .rl-zoom-bar__row { display: flex; align-items: center; gap: 0.75rem; } .rl-zoom-bar__label { font-size: 0.7rem; color: #64748b; width: 1.2rem; text-align: right; font-family: monospace; } .rl-zoom-bar__bar { height: 1.5rem; border-radius: 0.375rem; background: rgba(99,102,241,0.15); display: flex; align-items: center; padding: 0 0.75rem; } .rl-zoom-bar__name { font-size: 0.75rem; font-weight: 600; color: #e2e8f0; white-space: nowrap; } .rl-zoom-bar__span { font-size: 0.6rem; color: #64748b; margin-left: auto; white-space: nowrap; } /* Responsive helpers */ @media (max-width: 600px) { .rl-hero { padding: 3rem 1rem 2rem; } .rl-hero .rl-heading { font-size: 2rem; } .rl-section { padding: 2.5rem 1rem; } } `; export function escapeHtml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } export function escapeAttr(s: string): string { return s.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); }