/** * 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://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} `; } // ── 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"; } export function renderModuleLanding(opts: ModuleLandingOptions): string { const { module: mod, modules, theme = "dark" } = opts; const moduleListJSON = JSON.stringify(modules); // Modules with a standalone domain: embed it in a full-page iframe if (mod.standaloneDomain) { const embedUrl = `https://${escapeAttr(mod.standaloneDomain)}`; return ` ${escapeHtml(mod.name)} — rSpace
`; } // Modules without a standalone domain: simple generated landing page const demoUrl = `https://demo.rspace.online/${mod.id}`; return ` ${escapeHtml(mod.name)} — rSpace
${mod.icon}

${escapeHtml(mod.name)}

${escapeHtml(mod.description)}

← Back to rSpace
`; } 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; } } `; function escapeHtml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function escapeAttr(s: string): string { return s.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); }