rspace-online/modules/rcal/demo.ts

345 lines
12 KiB
TypeScript

/**
* rCal demo page — server-rendered HTML body.
*
* Static July 2026 calendar grid with Alpine Explorer trip events,
* tab switching (Temporal/Spatial/Lunar/Context), zoom panel,
* and feature cards. Entirely local state, no WebSocket.
*/
/* ─── Event Data ──────────────────────────────────────────── */
interface CalEvent {
day: number;
emoji: string;
label: string;
color: string;
bg: string;
}
const TRIP_EVENTS: CalEvent[] = [
{ day: 6, emoji: "\u2708\uFE0F", label: "Arrive Chamonix", color: "#2dd4bf", bg: "rgba(20,184,166,0.15)" },
{ day: 7, emoji: "\u{1F97E}", label: "Lac Blanc Hike", color: "#34d399", bg: "rgba(16,185,129,0.15)" },
{ day: 8, emoji: "\u{1F9D7}", label: "Aiguille du Midi", color: "#fbbf24", bg: "rgba(245,158,11,0.15)" },
{ day: 9, emoji: "\u{1F97E}", label: "Mer de Glace", color: "#34d399", bg: "rgba(16,185,129,0.15)" },
{ day: 10, emoji: "\u{1F682}", label: "Train to Zermatt", color: "#22d3ee", bg: "rgba(6,182,212,0.15)" },
{ day: 11, emoji: "\u{1F97E}", label: "Five Lakes Walk", color: "#34d399", bg: "rgba(16,185,129,0.15)" },
{ day: 12, emoji: "\u26F7", label: "Glacier Paradise", color: "#fbbf24", bg: "rgba(245,158,11,0.15)" },
{ day: 13, emoji: "\u{1F3DB}", label: "Alpine Museum", color: "#a78bfa", bg: "rgba(139,92,246,0.15)" },
{ day: 14, emoji: "\u{1F68C}", label: "Bus to Dolomites", color: "#22d3ee", bg: "rgba(6,182,212,0.15)" },
{ day: 15, emoji: "\u{1F97E}", label: "Tre Cime Circuit", color: "#34d399", bg: "rgba(16,185,129,0.15)" },
{ day: 16, emoji: "\u{1FA82}", label: "Paragliding", color: "#fbbf24", bg: "rgba(245,158,11,0.15)" },
{ day: 17, emoji: "\u{1F6F6}", label: "Lago di Braies", color: "#fbbf24", bg: "rgba(245,158,11,0.15)" },
{ day: 18, emoji: "\u{1F97E}", label: "Seceda Ridge", color: "#34d399", bg: "rgba(16,185,129,0.15)" },
{ day: 19, emoji: "\u{1F4F8}", label: "Rest & Photos", color: "#94a3b8", bg: "rgba(100,116,139,0.15)" },
{ day: 20, emoji: "\u2708\uFE0F", label: "Depart", color: "#2dd4bf", bg: "rgba(20,184,166,0.15)" },
];
const TABS = ["Temporal", "Spatial", "Lunar", "Context"];
const ZOOM_LEVELS = [
"Era", "Century", "Decade", "Year", "Quarter",
"Month", "Week", "Day", "Hour", "Minute",
];
const FEATURES = [
{
icon: "\u{1F50D}",
title: "Temporal Zoom",
desc: "Navigate seamlessly from geological eras down to individual minutes. The calendar adapts its grid density and label fidelity at every level.",
},
{
icon: "\u{1F30D}",
title: "Spatial Context",
desc: "Events are location-aware. Zoom the map and the calendar filters to show only events within the visible region.",
},
{
icon: "\u{1F319}",
title: "Lunar Cycles",
desc: "Overlay moon phases, tidal patterns, and seasonal markers. Useful for agriculture, ceremony, and natural rhythm tracking.",
},
{
icon: "\u{1F4C5}",
title: "Multi-Calendar",
desc: "Layer Gregorian, Islamic, Hebrew, Chinese, and custom community calendars. Cross-reference events across time systems.",
},
];
const LEGEND = [
{ color: "#2dd4bf", label: "Travel" },
{ color: "#34d399", label: "Hike" },
{ color: "#fbbf24", label: "Adventure" },
{ color: "#22d3ee", label: "Transit" },
{ color: "#a78bfa", label: "Culture" },
{ color: "#94a3b8", label: "Rest" },
];
/* ─── Helpers ─────────────────────────────────────────────── */
function eventForDay(day: number): CalEvent | undefined {
return TRIP_EVENTS.find((e) => e.day === day);
}
function isTripDay(day: number): boolean {
return day >= 6 && day <= 20;
}
/* ─── Render ──────────────────────────────────────────────── */
export function renderDemo(): string {
// July 2026: starts on Wednesday (offset 2 for Mon-based grid), 31 days
const firstDayOffset = 2; // Mon=0, Tue=1, Wed=2
const totalDays = 31;
const dayNames = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
// Build calendar cells
const calendarCells: string[] = [];
// Empty offset cells
for (let i = 0; i < firstDayOffset; i++) {
calendarCells.push(`<div class="rcal-cell rcal-cell--empty"></div>`);
}
// Day cells
for (let d = 1; d <= totalDays; d++) {
const ev = eventForDay(d);
const trip = isTripDay(d);
const todayClass = d === 15 ? " rcal-cell--today" : "";
const tripClass = trip ? " rcal-cell--trip" : "";
let pill = "";
if (ev) {
pill = `<div class="rcal-pill" style="background:${ev.bg};color:${ev.color};border:1px solid ${ev.color}22;">
<span class="rcal-pill__emoji">${ev.emoji}</span>
<span class="rcal-pill__label">${ev.label}</span>
</div>`;
}
calendarCells.push(`<div class="rcal-cell${tripClass}${todayClass}">
<span class="rcal-cell__num${trip ? " rcal-cell__num--trip" : ""}">${d}</span>
${pill}
</div>`);
}
return `
<div class="rd-root" style="--rd-accent-from:#6366f1; --rd-accent-to:#a78bfa;">
<!-- ── Hero ── -->
<section class="rd-hero">
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(99,102,241,0.1);border:1px solid rgba(99,102,241,0.2);border-radius:9999px;font-size:0.875rem;color:#a5b4fc;font-weight:500;margin-bottom:1.5rem;">
Multi-Dimensional Calendar
</div>
<h1>rCal Demo</h1>
<p class="rd-subtitle">Multi-dimensional calendar with temporal zoom</p>
<div class="rd-meta">
<span>\u{1F50D} Temporal Zoom</span>
<span style="color:#475569">|</span>
<span>\u{1F30D} Spatial Context</span>
<span style="color:#475569">|</span>
<span>\u{1F319} Lunar Cycles</span>
<span style="color:#475569">|</span>
<span>\u{1F4C5} Multi-Calendar</span>
</div>
</section>
<!-- ── Calendar Section ── -->
<section class="rd-section rd-section--narrow">
<!-- Header bar -->
<div class="rd-card" style="margin-bottom:0;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid rgba(51,65,85,0.3);flex-wrap:wrap;gap:0.75rem;">
<h2 style="font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0;display:flex;align-items:center;gap:0.5rem;">
\u{1F4C5} July 2026
</h2>
<div style="display:flex;gap:0.25rem;" id="rcal-tabs">
${TABS.map(
(tab, i) => `<button data-cal-tab="${tab.toLowerCase()}" style="
padding:0.375rem 0.875rem;
border-radius:0.5rem;
font-size:0.8rem;
font-weight:500;
border:none;
cursor:pointer;
transition:all 0.15s;
${i === 0 ? "background:rgba(99,102,241,0.15);color:#818cf8;" : "background:transparent;color:#94a3b8;"}
">${tab}</button>`,
).join("\n ")}
</div>
</div>
<!-- Day header row -->
<div class="rcal-grid rcal-grid--header">
${dayNames.map((d) => `<div class="rcal-day-header">${d}</div>`).join("\n ")}
</div>
<!-- Calendar grid -->
<div class="rcal-grid">
${calendarCells.join("\n ")}
</div>
<!-- Legend -->
<div style="display:flex;align-items:center;gap:1rem;padding:0.75rem 1.25rem;border-top:1px solid rgba(51,65,85,0.3);flex-wrap:wrap;">
<span style="font-size:0.75rem;color:#64748b;font-weight:500;">Legend:</span>
${LEGEND.map(
(l) => `<span style="display:flex;align-items:center;gap:0.375rem;font-size:0.75rem;color:#94a3b8;">
<span style="width:0.5rem;height:0.5rem;border-radius:9999px;background:${l.color};display:inline-block;"></span>
${l.label}
</span>`,
).join("\n ")}
</div>
</div>
</section>
<!-- ── Zoom Panel ── -->
<section class="rd-section rd-section--narrow">
<div class="rd-card" style="padding:1.5rem;">
<h2 style="font-size:1.125rem;font-weight:600;color:#f1f5f9;margin:0 0 1rem;display:flex;align-items:center;gap:0.5rem;">
\u{1F50D} Temporal Zoom
</h2>
<p style="font-size:0.875rem;color:#94a3b8;margin:0 0 1rem;">
Navigate across temporal granularities. The calendar grid adapts at each zoom level.
</p>
<div style="display:flex;flex-wrap:wrap;gap:0.5rem;">
${ZOOM_LEVELS.map(
(level) => {
const isActive = level === "Month";
return `<div style="
padding:0.5rem 1rem;
border-radius:0.5rem;
font-size:0.8rem;
font-weight:500;
border:1px solid ${isActive ? "rgba(99,102,241,0.4)" : "rgba(51,65,85,0.4)"};
background:${isActive ? "rgba(99,102,241,0.15)" : "rgba(30,41,59,0.5)"};
color:${isActive ? "#818cf8" : "#64748b"};
${isActive ? "box-shadow:0 0 12px rgba(99,102,241,0.2);" : ""}
">${level}${isActive ? " \u25C0" : ""}</div>`;
},
).join("\n ")}
</div>
</div>
</section>
<!-- ── Features ── -->
<section class="rd-section">
<div class="rd-grid rd-grid--2">
${FEATURES.map(
(f) => `
<div class="rd-card" style="padding:1.5rem;">
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
</div>`,
).join("")}
</div>
</section>
<!-- ── CTA ── -->
<section class="rd-section rd-section--narrow">
<div class="rd-cta">
<h2>Coordinate in Time & Space</h2>
<p>
rCal layers temporal zoom, spatial context, and lunar cycles into a single calendar.
Plan events that respect natural rhythms and local conditions.
</p>
<a href="/create-space" style="background:linear-gradient(135deg,#6366f1,#a78bfa);box-shadow:0 8px 24px rgba(99,102,241,0.25);">
Create Your Space
</a>
</div>
</section>
</div>
<style>
/* ── rCal demo grid ── */
.rcal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: rgba(51,65,85,0.2);
padding: 0 1px 1px;
}
.rcal-grid--header {
gap: 0;
padding: 0;
background: transparent;
border-bottom: 1px solid rgba(51,65,85,0.3);
}
.rcal-day-header {
padding: 0.5rem 0;
text-align: center;
font-size: 0.75rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.rcal-cell {
min-height: 3.5rem;
padding: 0.375rem;
background: rgba(15,23,42,0.6);
display: flex;
flex-direction: column;
gap: 0.25rem;
transition: background 0.15s;
}
.rcal-cell:hover {
background: rgba(30,41,59,0.8);
}
.rcal-cell--empty {
background: rgba(15,23,42,0.3);
}
.rcal-cell--trip {
background: rgba(99,102,241,0.04);
}
.rcal-cell--today {
outline: 2px solid rgba(99,102,241,0.5);
outline-offset: -2px;
background: rgba(99,102,241,0.08);
}
.rcal-cell__num {
font-size: 0.75rem;
font-weight: 500;
color: #64748b;
line-height: 1;
}
.rcal-cell__num--trip {
color: #a5b4fc;
font-weight: 600;
}
.rcal-pill {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.625rem;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
}
.rcal-pill__emoji {
flex-shrink: 0;
font-size: 0.7rem;
}
.rcal-pill__label {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
}
/* Responsive: stack pill text on small screens */
@media (max-width: 640px) {
.rcal-cell {
min-height: 2.75rem;
padding: 0.25rem;
}
.rcal-pill__label {
display: none;
}
.rcal-pill {
justify-content: center;
padding: 0.125rem;
}
}
</style>`;
}