${this.renderCalendarContent()}
@@ -882,6 +882,10 @@ class FolkCalendarView extends HTMLElement {
${this.renderMapPanel()}
+ ${this.renderSources()}
+
+ ${this.renderZoomController()}
+
${this.renderLunarOverlay()}
@@ -1068,6 +1072,7 @@ class FolkCalendarView extends HTMLElement {
private renderSources(): string {
if (this.sources.length === 0) return "";
return `
+
Calendar Legend
${this.sources.map(s => `${this.esc(s.name)}`).join("")}
@@ -1193,6 +1198,82 @@ class FolkCalendarView extends HTMLElement {
html += this.renderDayDetail(this.expandedDay, dayEvents);
}
+ // Multi-day event span bars
+ html += this.renderMultiDaySpans(year, month, firstDay, daysInMonth);
+
+ return html;
+ }
+
+ private renderMultiDaySpans(year: number, month: number, firstDay: number, daysInMonth: number): string {
+ const mm = String(month + 1).padStart(2, "0");
+ const monthStart = `${year}-${mm}-01`;
+ const monthEnd = `${year}-${mm}-${String(daysInMonth).padStart(2, "0")}`;
+
+ const multiDay = this.events.filter(e => {
+ if (!e.start_time || !e.end_time || this.filteredSources.has(e.source_name)) return false;
+ const sd = e.start_time.slice(0, 10);
+ const ed = e.end_time.slice(0, 10);
+ return ed > sd && ed >= monthStart && sd <= monthEnd;
+ });
+ if (multiDay.length === 0) return "";
+
+ const totalCells = firstDay + daysInMonth + ((7 - ((firstDay + daysInMonth) % 7)) % 7);
+ const totalRows = totalCells / 7;
+ let html = "";
+
+ // Helper: date string → cell index (clamped to grid)
+ const dateToCellIdx = (ds: string): number => {
+ const d = new Date(ds);
+ if (d.getFullYear() === year && d.getMonth() === month) return firstDay + d.getDate() - 1;
+ if (d < new Date(year, month, 1)) return 0;
+ return totalCells - 1;
+ };
+
+ for (let row = 0; row < totalRows; row++) {
+ const rowStart = row * 7;
+ const rowEnd = rowStart + 6;
+
+ const rowEvents: { ev: any; c0: number; c1: number }[] = [];
+ for (const e of multiDay) {
+ const sc = Math.max(dateToCellIdx(e.start_time.slice(0, 10)), rowStart);
+ const ec = Math.min(dateToCellIdx(e.end_time.slice(0, 10)), rowEnd);
+ if (sc > rowEnd || ec < rowStart) continue;
+ rowEvents.push({ ev: e, c0: sc - rowStart, c1: ec - rowStart });
+ }
+ if (rowEvents.length === 0) continue;
+
+ // Assign lanes (max 2 visible)
+ const MAX_LANES = 2;
+ const lanes: typeof rowEvents[] = [];
+ let overflow = 0;
+ for (const re of rowEvents) {
+ let placed = false;
+ for (let l = 0; l < MAX_LANES; l++) {
+ if (!lanes[l]) lanes[l] = [];
+ if (!lanes[l].some(x => re.c0 <= x.c1 && re.c1 >= x.c0)) {
+ lanes[l].push(re); placed = true; break;
+ }
+ }
+ if (!placed) overflow++;
+ }
+
+ const gridRow = row + 1;
+ for (let l = 0; l < lanes.length; l++) {
+ for (const re of lanes[l]) {
+ const color = re.ev.source_color || "#6366f1";
+ const realStart = dateToCellIdx(re.ev.start_time.slice(0, 10));
+ const realEnd = dateToCellIdx(re.ev.end_time.slice(0, 10));
+ const contL = realStart < rowStart;
+ const contR = realEnd > rowEnd;
+ const rL = contL ? "0" : "4px";
+ const rR = contR ? "0" : "4px";
+ html += `${this.esc(re.ev.title)}
`;
+ }
+ }
+ if (overflow > 0) {
+ html += `+${overflow} more
`;
+ }
+ }
return html;
}
@@ -2604,14 +2685,15 @@ class FolkCalendarView extends HTMLElement {
.zoom-bar__section--coupled .zoom-bar__spatial-track { opacity: 0.5; pointer-events: none; }
.zoom-bar__section--coupled .zoom-bar__spatial-thumb { cursor: default; }
- /* ── Sources ── */
- .sources { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
+ /* ── Sources / Calendar Legend ── */
+ .sources { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; align-items: center; }
+ .sources-heading { font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--rs-text-muted); margin-right: 4px; }
.src-badge { font-size: 10px; padding: 3px 8px; border-radius: 10px; border: 1px solid var(--rs-border-strong); cursor: pointer; transition: opacity 0.15s; user-select: none; }
.src-badge:hover { filter: brightness(1.2); }
.src-badge.filtered { opacity: 0.3; text-decoration: line-through; }
/* ── Zoom Bar Top ── */
- .zoom-bar--top { margin-bottom: 10px; padding: 8px 12px; background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 8px; }
+ .zoom-bar--top { margin-top: 10px; padding: 8px 12px; background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 8px; }
.zoom-bar--top .zoom-bar__track { min-width: 200px; }
.zoom-bar--top .zoom-bar__row { gap: 12px; }
.zoom-bar--top .zoom-bar__tick-label { font-size: 10px; }
@@ -2671,6 +2753,12 @@ class FolkCalendarView extends HTMLElement {
.dd-likelihood { font-size: 9px; color: var(--rs-warning); margin-left: 6px; }
.dot--tentative { border: 1px dashed; background: transparent !important; width: 6px; height: 6px; border-radius: 50%; display: inline-block; margin: 1px; }
+ /* ── Multi-day Span Bars ── */
+ .ev-span { height: 16px; font-size: 10px; line-height: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 4px; cursor: pointer; z-index: 2; align-self: start; pointer-events: auto; }
+ .ev-span:hover { filter: brightness(1.15); }
+ .ev-span-text { font-weight: 500; }
+ .ev-span-more { height: 14px; font-size: 9px; line-height: 14px; color: var(--rs-text-muted); z-index: 2; align-self: start; pointer-events: none; padding: 0 4px; }
+
/* ── Drop Target ── */
.day.drop-target { background: rgba(245,158,11,0.15); border: 2px dashed var(--rs-warning); }
@@ -2958,7 +3046,7 @@ class FolkCalendarView extends HTMLElement {
.wd { font-size: 10px; padding: 3px; }
.season-cities, .week-event-meta, .tl-event-desc, .yv-country { display: none; }
.week-view { font-size: 10px; }
- .zoom-bar--top { padding: 6px 8px; margin-bottom: 6px; }
+ .zoom-bar--top { padding: 6px 8px; margin-top: 6px; }
.zoom-bar--top .zoom-bar__track { min-width: 120px; }
.zoom-bar__track { min-width: 120px; }
.zoom-bar__label-end { font-size: 9px; }
diff --git a/shared/components/rstack-identity.ts b/shared/components/rstack-identity.ts
index ffca412..b15e029 100644
--- a/shared/components/rstack-identity.ts
+++ b/shared/components/rstack-identity.ts
@@ -431,7 +431,7 @@ export class RStackIdentity extends HTMLElement {
this.#render();
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
} else if (action === "my-account") {
- this.#showAccountModal();
+ this.showAccountModal();
} else if (action === "my-spaces") {
this.#showSpacesModal();
} else if (action === "my-wallets") {
@@ -488,6 +488,11 @@ export class RStackIdentity extends HTMLElement {
}
}
+ /** Public: check if user has an active session */
+ isSignedIn(): boolean {
+ return getSession() !== null;
+ }
+
/** Public method: show the auth modal programmatically */
showAuthModal(callbacks?: { onSuccess?: () => void; onCancel?: () => void }): void {
if (document.querySelector(".rstack-auth-overlay")) return;
@@ -695,7 +700,7 @@ export class RStackIdentity extends HTMLElement {
// ── Account modal (consolidated) ──
- #showAccountModal(): void {
+ showAccountModal(): void {
if (document.querySelector(".rstack-account-overlay")) return;
const overlay = document.createElement("div");
overlay.className = "rstack-account-overlay";
diff --git a/website/canvas.html b/website/canvas.html
index 560d955..7630acc 100644
--- a/website/canvas.html
+++ b/website/canvas.html
@@ -2983,10 +2983,25 @@
{ target: 'rstack-space-switcher', title: 'Spaces', msg: 'Each space is a self-contained community with its own data, members, and encryption. Switch between spaces or create a new one here.' },
{ target: 'rstack-identity', title: 'Passwordless Identity', msg: 'Sign in with passkeys \u2014 no passwords or seed phrases. Your identity is cryptographic and portable across all spaces.' },
{ target: '#canvas', title: 'The Canvas', msg: 'Drag to pan, scroll to zoom. Click any shape to interact with it. Connect shapes with arrows by dragging between ports. Everything syncs in real-time.' },
- { target: '#bottom-toolbar', title: 'Drawing Tools', msg: 'Select, draw, and erase on the canvas. Switch between tools here, or use keyboard shortcuts (V for select, P for pen, E for eraser).' },
+ { target: 'rstack-identity', title: 'Secure (You)rIdentity', msg: 'Set up your rIdentity to protect your account. Connect an email, add trusted guardians for social recovery, or link an external wallet — then click Get Started to begin.' },
];
var step = 0, tourEl = document.getElementById('rspace-tour'), backdrop = document.getElementById('rspace-tour-backdrop'), spotlight = document.getElementById('rspace-tour-spotlight'), tooltip = document.getElementById('rspace-tour-tooltip'), titleEl = document.getElementById('rspace-tour-title'), msgEl = document.getElementById('rspace-tour-msg'), stepEl = document.getElementById('rspace-tour-step'), prevBtn = document.getElementById('rspace-tour-prev'), nextBtn = document.getElementById('rspace-tour-next'), skipBtn = document.getElementById('rspace-tour-skip');
function endTour() { localStorage.setItem('rspace_tour_done', '1'); if (tourEl) tourEl.style.display = 'none'; }
+ function triggerIdentitySetup() {
+ var el = document.querySelector('rstack-identity');
+ if (!el) return;
+ if (el.isSignedIn && el.isSignedIn()) {
+ el.showAccountModal();
+ } else {
+ el.showAuthModal({
+ onSuccess: function() {
+ setTimeout(function() {
+ if (el.showAccountModal) el.showAccountModal();
+ }, 500);
+ }
+ });
+ }
+ }
function showStep() {
if (!tourEl || !backdrop || !spotlight || !tooltip) return;
var s = TOUR_STEPS[step], target = document.querySelector(s.target);
@@ -3000,14 +3015,14 @@
stepEl.textContent = (step + 1) + ' / ' + TOUR_STEPS.length; titleEl.textContent = s.title; msgEl.innerHTML = s.msg;
prevBtn.style.display = step > 0 ? '' : 'none'; nextBtn.textContent = step === TOUR_STEPS.length - 1 ? 'Get Started' : 'Next';
}
- if (nextBtn) nextBtn.addEventListener('click', function() { step++; if (step >= TOUR_STEPS.length) { endTour(); return; } showStep(); });
+ if (nextBtn) nextBtn.addEventListener('click', function() { step++; if (step >= TOUR_STEPS.length) { endTour(); triggerIdentitySetup(); return; } showStep(); });
if (prevBtn) prevBtn.addEventListener('click', function() { if (step > 0) { step--; showStep(); } });
if (skipBtn) skipBtn.addEventListener('click', endTour);
if (backdrop) backdrop.addEventListener('click', endTour);
document.addEventListener('keydown', function(e) {
if (!tourEl || tourEl.style.display === 'none') return;
if (e.key === 'Escape') { endTour(); e.preventDefault(); }
- if (e.key === 'ArrowRight' || e.key === 'Enter') { step++; if (step >= TOUR_STEPS.length) { endTour(); return; } showStep(); e.preventDefault(); }
+ if (e.key === 'ArrowRight' || e.key === 'Enter') { step++; if (step >= TOUR_STEPS.length) { endTour(); triggerIdentitySetup(); return; } showStep(); e.preventDefault(); }
if (e.key === 'ArrowLeft' && step > 0) { step--; showStep(); e.preventDefault(); }
});
setTimeout(function() { if (tourEl) { tourEl.style.display = ''; showStep(); } }, 800);