/** * 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; /** Space visibility level (for client-side access gate) */ spaceVisibility?: string; } export function renderShell(opts: ShellOptions): string { const { title, moduleId, spaceSlug, spaceName, body, scripts = "", styles = "", modules, theme = "dark", head = "", spaceVisibility = "public_read", } = 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 ACCESS_GATE_CSS = ` #rspace-access-gate { position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; background: rgba(15, 23, 42, 0.95); backdrop-filter: blur(8px); } .access-gate__card { text-align: center; color: white; max-width: 400px; padding: 2rem; } .access-gate__icon { font-size: 3rem; margin-bottom: 1rem; } .access-gate__title { font-size: 1.5rem; margin: 0 0 0.5rem; background: linear-gradient(135deg, #06b6d4, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .access-gate__desc { color: #94a3b8; font-size: 0.95rem; line-height: 1.6; margin: 0 0 1.5rem; } .access-gate__btn { padding: 12px 32px; border-radius: 8px; border: none; font-size: 1rem; font-weight: 600; cursor: pointer; background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white; transition: opacity 0.15s, transform 0.15s; } .access-gate__btn:hover { opacity: 0.9; transform: translateY(-1px); } `; 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; } } `; /** * Render a demo page shell: standard shell + DEMO_PAGE_CSS + demo scripts. * Used by modules that have a demoPage() renderer. */ export function renderDemoShell(opts: ShellOptions & { demoScripts?: string }): string { return renderShell({ ...opts, styles: `${opts.styles || ""}\n`, scripts: `${opts.scripts || ""}\n${opts.demoScripts || ""}`, }); } // ── 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; } } `; // ── Demo page CSS utilities (rd-* prefix, parallel to rl-* landing pages) ── export const DEMO_PAGE_CSS = ` /* ── Demo page base ── */ .rd-root { min-height: 100vh; background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%); color: white; padding-bottom: 2rem; } .rd-hero { max-width: 48rem; margin: 0 auto; text-align: center; padding: 3rem 1.5rem 2rem; } .rd-hero h1 { font-size: 2.25rem; font-weight: 700; margin-bottom: 1rem; background: linear-gradient(135deg, var(--rd-accent-from, #14b8a6), var(--rd-accent-to, #22d3ee)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } @media (min-width: 640px) { .rd-hero h1 { font-size: 3rem; } } .rd-hero .rd-subtitle { font-size: 1.1rem; color: #cbd5e1; margin-bottom: 0.5rem; } .rd-hero .rd-meta { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 1rem; font-size: 0.875rem; color: #94a3b8; margin-bottom: 1.5rem; } .rd-hero .rd-meta span:not(:last-child)::after { content: ""; } /* Avatars */ .rd-avatars { display: flex; align-items: center; justify-content: center; gap: 0.5rem; } .rd-avatar { width: 2.5rem; height: 2.5rem; border-radius: 9999px; display: flex; align-items: center; justify-content: center; font-size: 0.875rem; font-weight: 700; color: white; box-shadow: 0 0 0 2px #1e293b; } .rd-avatars .rd-count { font-size: 0.875rem; color: #94a3b8; margin-left: 0.5rem; } /* Cards */ .rd-card { background: rgba(30,41,59,0.5); border-radius: 1rem; border: 1px solid rgba(51,65,85,0.5); overflow: hidden; } .rd-card-header { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1.25rem; border-bottom: 1px solid rgba(51,65,85,0.5); } .rd-card-header .rd-card-title { display: flex; align-items: center; gap: 0.5rem; font-weight: 600; font-size: 0.875rem; } .rd-card-header .rd-card-title .rd-icon { font-size: 1.25rem; } .rd-card-header .rd-open-link { font-size: 0.75rem; padding: 0.375rem 0.75rem; background: rgba(51,65,85,0.6); border-radius: 0.5rem; color: #cbd5e1; text-decoration: none; transition: all 0.15s; } .rd-card-header .rd-open-link:hover { background: rgba(71,85,105,0.6); color: white; } .rd-card-body { padding: 1.25rem; } /* Live badge */ .rd-live { display: inline-flex; align-items: center; gap: 0.375rem; font-size: 0.75rem; color: #34d399; } .rd-live::before { content: ""; width: 0.375rem; height: 0.375rem; border-radius: 9999px; background: #34d399; animation: rd-pulse 2s ease-in-out infinite; } @keyframes rd-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } /* Status badge */ .rd-status { display: inline-flex; align-items: center; gap: 0.375rem; font-size: 0.75rem; padding: 0.25rem 0.625rem; border-radius: 9999px; } .rd-status--connected { color: #34d399; background: rgba(52,211,153,0.1); border: 1px solid rgba(52,211,153,0.2); } .rd-status--disconnected { color: #f87171; background: rgba(248,113,113,0.1); border: 1px solid rgba(248,113,113,0.2); } .rd-status::before { content: ""; width: 0.5rem; height: 0.5rem; border-radius: 9999px; } .rd-status--connected::before { background: #34d399; } .rd-status--disconnected::before { background: #f87171; } /* Progress bar */ .rd-progress { height: 0.75rem; border-radius: 9999px; background: rgba(51,65,85,1); overflow: hidden; } .rd-progress--sm { height: 0.375rem; } .rd-progress--xs { height: 0.25rem; } .rd-progress__fill { height: 100%; border-radius: 9999px; transition: width 0.3s ease-out; background: linear-gradient(90deg, var(--rd-accent-from, #14b8a6), var(--rd-accent-to, #2dd4bf)); } .rd-progress__fill--emerald { background: #10b981; } .rd-progress__fill--sky { background: #0ea5e9; } .rd-progress__fill--amber { background: #f59e0b; } .rd-progress__fill--rose { background: #f43f5e; } .rd-progress__fill--orange { background: linear-gradient(90deg, #fb923c, #f97316); } .rd-progress__fill--teal { background: linear-gradient(90deg, #14b8a6, #2dd4bf); } .rd-progress__fill--cyan { background: #06b6d4; } .rd-progress__fill--violet { background: #8b5cf6; } /* Grid */ .rd-grid { display: grid; gap: 1rem; } .rd-grid--2 { grid-template-columns: 1fr; } .rd-grid--3 { grid-template-columns: 1fr; } .rd-grid--4 { grid-template-columns: 1fr 1fr; } @media (min-width: 640px) { .rd-grid--2 { grid-template-columns: repeat(2, 1fr); } .rd-grid--3 { grid-template-columns: repeat(3, 1fr); } } @media (min-width: 768px) { .rd-grid--3 { grid-template-columns: repeat(3, 1fr); } .rd-grid--4 { grid-template-columns: repeat(4, 1fr); } } /* Section */ .rd-section { max-width: 72rem; margin: 0 auto; padding: 0 1.5rem 1.5rem; } .rd-section--narrow { max-width: 64rem; } /* Badge */ .rd-badge { display: inline-block; font-size: 0.75rem; font-weight: 500; padding: 0.125rem 0.625rem; border-radius: 9999px; } .rd-badge--emerald { background: rgba(16,185,129,0.2); color: #6ee7b7; } .rd-badge--sky { background: rgba(14,165,233,0.2); color: #7dd3fc; } .rd-badge--amber { background: rgba(245,158,11,0.2); color: #fcd34d; } .rd-badge--rose { background: rgba(244,63,94,0.2); color: #fda4af; } .rd-badge--orange { background: rgba(249,115,22,0.2); color: #fdba74; } .rd-badge--teal { background: rgba(20,184,166,0.2); color: #5eead4; } .rd-badge--slate { background: rgba(100,116,139,0.2); color: #94a3b8; } /* Stat box */ .rd-stat { background: rgba(51,65,85,0.3); border-radius: 0.75rem; padding: 1rem; text-align: center; } .rd-stat__value { font-size: 1.5rem; font-weight: 700; color: white; } .rd-stat__label { font-size: 0.75rem; color: #94a3b8; margin-top: 0.25rem; } .rd-stat__sub { font-size: 0.75rem; color: #64748b; margin-top: 0.125rem; } /* Button */ .rd-btn { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.875rem; font-weight: 500; cursor: pointer; border: none; transition: all 0.15s; } .rd-btn--ghost { background: rgba(51,65,85,0.6); color: #cbd5e1; } .rd-btn--ghost:hover { background: rgba(71,85,105,0.6); color: white; } .rd-btn--ghost:disabled { opacity: 0.5; cursor: not-allowed; } .rd-btn--primary { color: white; } /* Divider row */ .rd-row { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1.25rem; border-top: 1px solid rgba(51,65,85,0.5); } /* Item row (expense/cart list item) */ .rd-item { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 1.25rem; transition: background 0.1s; } .rd-item:hover { background: rgba(51,65,85,0.2); } .rd-item + .rd-item { border-top: 1px solid rgba(51,65,85,0.3); } /* Checkbox */ .rd-checkbox { width: 1.25rem; height: 1.25rem; border-radius: 0.25rem; flex-shrink: 0; border: 2px solid #475569; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.15s; } .rd-checkbox--checked { background: var(--rd-accent-from, #14b8a6); border-color: var(--rd-accent-from, #14b8a6); } .rd-checkbox svg { width: 0.75rem; height: 0.75rem; color: white; } .rd-checkbox:hover { border-color: #64748b; } /* CTA section */ .rd-cta { background: rgba(30,41,59,0.5); border-radius: 1rem; border: 1px solid rgba(51,65,85,0.5); padding: 2.5rem; text-align: center; margin-top: 1rem; } .rd-cta h2 { font-size: 1.875rem; font-weight: 700; margin-bottom: 0.75rem; } .rd-cta p { color: #94a3b8; max-width: 32rem; margin: 0 auto 1.5rem; font-size: 0.875rem; } .rd-cta a { display: inline-block; padding: 0.875rem 2rem; border-radius: 0.75rem; font-size: 1.1rem; font-weight: 500; color: white; text-decoration: none; transition: transform 0.2s, box-shadow 0.2s; } .rd-cta a:hover { transform: translateY(-2px); } /* Text helpers */ .rd-text-muted { color: #94a3b8; } .rd-text-dim { color: #64748b; } .rd-text-sm { font-size: 0.875rem; } .rd-text-xs { font-size: 0.75rem; } .rd-text-center { text-align: center; } .rd-font-bold { font-weight: 700; } .rd-font-semibold { font-weight: 600; } .rd-font-medium { font-weight: 500; } .rd-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rd-line-through { text-decoration: line-through; } .rd-hidden { display: none !important; } /* Color helpers */ .rd-emerald { color: #34d399; } .rd-rose { color: #fb7185; } .rd-amber { color: #fbbf24; } .rd-cyan { color: #22d3ee; } .rd-teal { color: #2dd4bf; } .rd-orange { color: #fb923c; } .rd-sky { color: #38bdf8; } .rd-violet { color: #a78bfa; } /* BG helpers */ .rd-bg-emerald { background: #10b981; } .rd-bg-cyan { background: #06b6d4; } .rd-bg-violet { background: #8b5cf6; } .rd-bg-amber { background: #f59e0b; } .rd-bg-rose { background: #f43f5e; } .rd-bg-teal { background: #14b8a6; } .rd-bg-sky { background: #0ea5e9; } .rd-bg-orange { background: #f97316; } .rd-bg-slate { background: #64748b; } /* Responsive */ @media (max-width: 640px) { .rd-hero { padding: 2rem 1rem 1.5rem; } .rd-hero h1 { font-size: 2rem; } .rd-section { padding: 0 1rem 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, ">"); }