diff --git a/docker-compose.yml b/docker-compose.yml index 07561e0..9f38ff8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,21 +42,10 @@ services: - SMTP_HOST=${SMTP_HOST:-mailcowdockerized-postfix-mailcow-1} - SMTP_PORT=${SMTP_PORT:-587} - SMTP_USER=${SMTP_USER:-noreply@rmail.online} - - SMTP_PASS=${SMTP_PASS} - SITE_URL=https://rspace.online - RTASKS_REPO_BASE=/repos - - RTASKS_HMAC_SECRET=${RTASKS_HMAC_SECRET} - - RTASKS_API_KEY=${RTASKS_API_KEY} - SPLAT_NOTIFY_EMAIL=jeffemmett@gmail.com - TWENTY_API_URL=http://twenty-ch-server:3000 - - TWENTY_API_TOKEN=${TWENTY_API_TOKEN:-} - - TRANSAK_API_KEY=${TRANSAK_API_KEY:-} - - TRANSAK_API_KEY_STAGING=${TRANSAK_API_KEY_STAGING:-} - - TRANSAK_API_KEY_PRODUCTION=${TRANSAK_API_KEY_PRODUCTION:-} - - TRANSAK_SECRET=${TRANSAK_SECRET:-} - - TRANSAK_WEBHOOK_SECRET_STAGING=${TRANSAK_WEBHOOK_SECRET_STAGING:-} - - TRANSAK_WEBHOOK_SECRET_PRODUCTION=${TRANSAK_WEBHOOK_SECRET_PRODUCTION:-} - - TRANSAK_ENV=${TRANSAK_ENV:-STAGING} - OLLAMA_URL=http://ollama:11434 - INFISICAL_AI_CLIENT_ID=${INFISICAL_AI_CLIENT_ID} - INFISICAL_AI_CLIENT_SECRET=${INFISICAL_AI_CLIENT_SECRET} diff --git a/modules/rcal/components/folk-calendar-view.ts b/modules/rcal/components/folk-calendar-view.ts index 5b43373..c8f96e2 100644 --- a/modules/rcal/components/folk-calendar-view.ts +++ b/modules/rcal/components/folk-calendar-view.ts @@ -741,8 +741,12 @@ class FolkCalendarView extends HTMLElement { } private getEventsForDate(dateStr: string): any[] { - return this.events.filter(e => e.start_time && e.start_time.startsWith(dateStr) - && !this.filteredSources.has(e.source_name)); + return this.events.filter(e => { + if (!e.start_time || this.filteredSources.has(e.source_name)) return false; + const startDay = e.start_time.slice(0, 10); + const endDay = e.end_time ? e.end_time.slice(0, 10) : startDay; + return dateStr >= startDay && dateStr <= endDay; + }); } private getRemindersForDate(dateStr: string): any[] { @@ -870,10 +874,6 @@ class FolkCalendarView extends HTMLElement { - ${this.renderSources()} - -
${this.renderZoomController()}
-
${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);