From 1cd82256807a34802076950acc5c9ac7d3489f2e Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 10 Mar 2026 17:07:16 -0700 Subject: [PATCH] feat(rcal): zoom bar relocation, likelihood feature, rich demo data, remove floating map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move zoom bars between calendar and map panes when docked (3-column grid layout) - Add likelihood/pencil-in feature: tentative events render with dashed borders, lower opacity, percentage badges across all 7 view paths + modal + map markers - Expand demo data from ~47 to 105 events spanning months -1 to +8 with periodic (yoga, standups), episodic (workshop series, book club), and tentative (25-80% likelihood) events - Add Community and Health source categories - Remove floating map option — map is now docked or minimized only - Decouple zoom bars by default (spatialGranularity=Country, zoomCoupled=false) - Preserve effective spatial index when decoupling via 'c' key or button - Extend lunar computation range to month+9 Co-Authored-By: Claude Opus 4.6 --- modules/rcal/components/folk-calendar-view.ts | 227 +++++++++++++----- modules/rcal/schemas.ts | 1 + 2 files changed, 173 insertions(+), 55 deletions(-) diff --git a/modules/rcal/components/folk-calendar-view.ts b/modules/rcal/components/folk-calendar-view.ts index 0608dcd..fbdd2c8 100644 --- a/modules/rcal/components/folk-calendar-view.ts +++ b/modules/rcal/components/folk-calendar-view.ts @@ -126,11 +126,11 @@ class FolkCalendarView extends HTMLElement { // Spatio-temporal state private temporalGranularity = 4; // MONTH - private spatialGranularity: number | null = null; // null = auto-coupled - private zoomCoupled = true; + private spatialGranularity: number | null = 3; // Country (independent default) + private zoomCoupled = false; // Map panel state (replaces old tab system) - private mapPanelState: "minimized" | "floating" | "docked" = "docked"; + private mapPanelState: "minimized" | "docked" = "docked"; private currentTileLayer: any = null; private lunarOverlayExpanded = false; private _wheelTimer: ReturnType | null = null; @@ -267,15 +267,21 @@ class FolkCalendarView extends HTMLElement { case "+": case "=": e.preventDefault(); this.zoomIn(); break; case "-": case "_": e.preventDefault(); this.zoomOut(); break; case "m": case "M": - // Cycle: floating → docked → minimized → floating - if (this.mapPanelState === "floating") this.mapPanelState = "docked"; - else if (this.mapPanelState === "docked") this.mapPanelState = "minimized"; - else this.mapPanelState = "floating"; + // Toggle: docked ↔ minimized + this.mapPanelState = this.mapPanelState === "docked" ? "minimized" : "docked"; this.render(); break; case "c": case "C": - this.zoomCoupled = !this.zoomCoupled; - if (this.zoomCoupled) { this.spatialGranularity = null; this.syncMapToSpatial(); } + if (!this.zoomCoupled) { + // Coupling: let spatial follow temporal + this.zoomCoupled = true; + this.spatialGranularity = null; + this.syncMapToSpatial(); + } else { + // Decoupling: preserve current effective spatial index + this.spatialGranularity = this.getEffectiveSpatialIndex(); + this.zoomCoupled = false; + } this.render(); break; } @@ -406,6 +412,8 @@ class FolkCalendarView extends HTMLElement { { name: "Travel", color: "#f97316" }, { name: "Personal", color: "#10b981" }, { name: "Conferences", color: "#8b5cf6" }, + { name: "Community", color: "#ec4899" }, + { name: "Health", color: "#06b6d4" }, ]; const rel = (monthDelta: number, day: number, hour: number, min: number) => @@ -414,7 +422,7 @@ class FolkCalendarView extends HTMLElement { const demoEvents: { start: Date; durationMin: number; title: string; source: number; desc: string; location: string | null; virtual: boolean; - lat?: number; lng?: number; breadcrumb?: string; + lat?: number; lng?: number; breadcrumb?: string; likelihood?: number; }[] = [ // ── LAST MONTH ── { start: rel(-1, 18, 10, 0), durationMin: 90, title: "Sprint 22 Review", source: 0, desc: "Sprint review with Berlin engineering team", location: "Factory Berlin", virtual: false, lat: 52.5030, lng: 13.3345, breadcrumb: "Earth > Europe > Germany > Berlin" }, @@ -422,11 +430,13 @@ class FolkCalendarView extends HTMLElement { { start: rel(-1, 20, 9, 0), durationMin: 480, title: "EthBerlin Hackathon Day 1", source: 3, desc: "Web3 hackathon at Factory Kreuzberg", location: "Factory G\u00f6rlitzer Park", virtual: false, lat: 52.4934, lng: 13.4278, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, { start: rel(-1, 21, 9, 0), durationMin: 480, title: "EthBerlin Hackathon Day 2", source: 3, desc: "Judging and demos", location: "Factory G\u00f6rlitzer Park", virtual: false, lat: 52.4934, lng: 13.4278, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, { start: rel(-1, 22, 18, 0), durationMin: 120, title: "Birthday Dinner \u2014 Marco", source: 2, desc: "Italian dinner at La Focacceria", location: "La Focacceria, Kreuzberg", virtual: false, lat: 52.4940, lng: 13.4100, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, - { start: rel(-1, 25, 7, 30), durationMin: 75, title: "Morning Yoga", source: 2, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, lat: 52.4960, lng: 13.4088, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(-1, 23, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, lat: 52.4960, lng: 13.4088, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(-1, 25, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, lat: 52.4960, lng: 13.4088, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, { start: rel(-1, 26, 16, 0), durationMin: 60, title: "Client Call NYC", source: 0, desc: "Q4 API integration sync", location: null, virtual: true }, + { start: rel(-1, 27, 19, 0), durationMin: 90, title: "Climate Action Meetup", source: 4, desc: "Monthly community meetup on local climate projects", location: "Regenbogenfabrik", virtual: false, lat: 52.4945, lng: 13.4290, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, { start: rel(-1, 28, 8, 0), durationMin: 300, title: "Weekend Hike \u2014 Spreewald", source: 2, desc: "Kayak + hike in biosphere reserve", location: "L\u00fcbbenau, Spreewald", virtual: false, lat: 51.8644, lng: 13.7669, breadcrumb: "Earth > Europe > Germany > Brandenburg > Spreewald" }, - // ── THIS MONTH ── + // ── THIS MONTH — Periodic: MWF standups ── { start: rel(0, 1, 9, 0), durationMin: 30, title: "Team Standup", source: 0, desc: "Daily sync \u2014 Berlin engineering team", location: "Factory Berlin", virtual: false, lat: 52.5030, lng: 13.3345, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 1, 14, 0), durationMin: 60, title: "Code Review Session", source: 0, desc: "Review PRs from the weekend batch", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 2, 9, 0), durationMin: 30, title: "Team Standup", source: 0, desc: "Daily sync", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, @@ -435,42 +445,109 @@ class FolkCalendarView extends HTMLElement { { start: rel(0, 3, 15, 0), durationMin: 90, title: "Architecture Deep-Dive", source: 0, desc: "CRDT merge strategy for offline-first mobile", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 4, 10, 0), durationMin: 90, title: "Product Review", source: 0, desc: "Quarterly product roadmap review", location: "Factory Berlin, Room 3", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 4, 12, 30), durationMin: 60, title: "Lunch with Alex", source: 2, desc: "Catch up over Vietnamese food", location: "District Mot, Berlin", virtual: false, lat: 52.5244, lng: 13.4023, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" }, + { start: rel(0, 4, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, { start: rel(0, 5, 10, 0), durationMin: 120, title: "Sprint Planning", source: 0, desc: "Plan sprint 24 \u2014 local-first sync", location: "Factory Berlin, Room 1", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, + { start: rel(0, 6, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, { start: rel(0, 7, 8, 0), durationMin: 300, title: "Weekend Hike \u2014 Grunewald", source: 2, desc: "Forest loop trail, ~14 km", location: "S-Bahn Grunewald", virtual: false, lat: 52.4730, lng: 13.2260, breadcrumb: "Earth > Europe > Germany > Berlin > Grunewald" }, + { start: rel(0, 8, 9, 0), durationMin: 30, title: "Team Standup", source: 0, desc: "Weekly sync", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 8, 16, 0), durationMin: 60, title: "Client Call NYC", source: 0, desc: "Sync with NYC partner team on API integration", location: null, virtual: true }, - { start: rel(0, 9, 7, 30), durationMin: 75, title: "Morning Yoga", source: 2, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(0, 9, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(0, 10, 9, 0), durationMin: 30, title: "Team Standup", source: 0, desc: "Daily sync", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 10, 11, 0), durationMin: 45, title: "1:1 with Manager", source: 0, desc: "Monthly check-in", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 10, 15, 30), durationMin: 60, title: "Deploy Prep", source: 0, desc: "Pre-release checklist and staging verification", location: null, virtual: true }, + { start: rel(0, 11, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(0, 11, 18, 0), durationMin: 120, title: "Workshop: Intro to CRDTs (1/4)", source: 4, desc: "Community workshop series on conflict-free replicated data types", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" }, { start: rel(0, 12, 7, 15), durationMin: 390, title: "Train Berlin \u2192 Amsterdam", source: 1, desc: "ICE 643 Berlin Hbf \u2192 Amsterdam Centraal", location: "Berlin Hauptbahnhof", virtual: false, lat: 52.5251, lng: 13.3694, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 12, 14, 30), durationMin: 30, title: "Hotel Check-in", source: 1, desc: "Hotel V Nesplein, Amsterdam", location: "Hotel V Nesplein", virtual: false, lat: 52.3667, lng: 4.8945, breadcrumb: "Earth > Europe > Netherlands > Amsterdam" }, { start: rel(0, 13, 10, 0), durationMin: 180, title: "Partner Meeting", source: 0, desc: "On-site with Amsterdam design team", location: "WeWork Weteringschans", virtual: false, lat: 52.3603, lng: 4.8880, breadcrumb: "Earth > Europe > Netherlands > Amsterdam" }, { start: rel(0, 13, 15, 0), durationMin: 120, title: "Canal District Walk", source: 2, desc: "Afternoon along Prinsengracht and Jordaan", location: "Prinsengracht, Amsterdam", virtual: false, lat: 52.3738, lng: 4.8820, breadcrumb: "Earth > Europe > Netherlands > Amsterdam" }, { start: rel(0, 14, 9, 30), durationMin: 390, title: "Return Train Amsterdam \u2192 Berlin", source: 1, desc: "ICE 148 Amsterdam \u2192 Berlin", location: "Amsterdam Centraal", virtual: false, lat: 52.3791, lng: 4.9003, breadcrumb: "Earth > Europe > Netherlands > Amsterdam" }, + { start: rel(0, 15, 9, 0), durationMin: 30, title: "Team Standup", source: 0, desc: "Post-travel sync", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 15, 19, 30), durationMin: 120, title: "Dinner with Friends", source: 2, desc: "Birthday dinner for Mia", location: "Il Casolare, Kreuzberg", virtual: false, lat: 52.4900, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, - { start: rel(0, 16, 7, 30), durationMin: 75, title: "Morning Yoga", source: 2, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(0, 16, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(0, 16, 14, 0), durationMin: 45, title: "Physiotherapy", source: 5, desc: "Shoulder rehab session", location: "Praxis Neuk\u00f6lln", virtual: false, lat: 52.4812, lng: 13.4350, breadcrumb: "Earth > Europe > Germany > Berlin > Neuk\u00f6lln" }, { start: rel(0, 17, 10, 0), durationMin: 60, title: "Sprint Retro", source: 0, desc: "Sprint 23 retrospective", location: "Factory Berlin, Room 2", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 17, 14, 0), durationMin: 120, title: "Release Deploy", source: 0, desc: "Push v2.4.0 to production", location: null, virtual: true }, + { start: rel(0, 18, 9, 0), durationMin: 30, title: "Team Standup", source: 0, desc: "Daily sync", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 18, 14, 0), durationMin: 90, title: "Demo Day", source: 0, desc: "Sprint 23 showcase for stakeholders", location: "Factory Berlin, Main Hall", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, - { start: rel(0, 19, 9, 0), durationMin: 45, title: "Dentist", source: 2, desc: "Regular checkup, Dr. Weber", location: "Torstr. 140, Berlin", virtual: false, lat: 52.5308, lng: 13.3970, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" }, - { start: rel(0, 20, 19, 0), durationMin: 90, title: "Book Club", source: 2, desc: '"The Mushroom at the End of the World"', location: "Shakespeare & Sons, Berlin", virtual: false, lat: 52.4925, lng: 13.4310, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, - { start: rel(0, 21, 14, 0), durationMin: 60, title: "c-base Open Tuesday", source: 2, desc: "Weekly open hackerspace session", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" }, + { start: rel(0, 18, 18, 0), durationMin: 120, title: "Workshop: CRDTs (2/4)", source: 4, desc: "Hands-on: Counters, sets, and registers", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" }, + { start: rel(0, 19, 9, 0), durationMin: 45, title: "Dentist", source: 5, desc: "Regular checkup, Dr. Weber", location: "Torstr. 140, Berlin", virtual: false, lat: 52.5308, lng: 13.3970, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" }, + { start: rel(0, 20, 19, 0), durationMin: 90, title: "Book Club", source: 4, desc: '"The Mushroom at the End of the World"', location: "Shakespeare & Sons, Berlin", virtual: false, lat: 52.4925, lng: 13.4310, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(0, 21, 14, 0), durationMin: 60, title: "c-base Open Tuesday", source: 4, desc: "Weekly open hackerspace session", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" }, { start: rel(0, 22, 6, 45), durationMin: 195, title: "Flight \u2192 Lisbon", source: 1, desc: "TAP TP 571 BER \u2192 LIS", location: "BER Airport", virtual: false, lat: 52.3667, lng: 13.5033, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(0, 23, 9, 0), durationMin: 540, title: "Web Summit Day 1", source: 3, desc: "Opening keynotes, startup pavilion", location: "Altice Arena, Lisbon", virtual: false, lat: 38.7685, lng: -9.0943, breadcrumb: "Earth > Europe > Portugal > Lisbon" }, { start: rel(0, 24, 9, 0), durationMin: 540, title: "Web Summit Day 2", source: 3, desc: "Panel: Local-First Software", location: "Altice Arena, Lisbon", virtual: false, lat: 38.7685, lng: -9.0943, breadcrumb: "Earth > Europe > Portugal > Lisbon" }, { start: rel(0, 25, 10, 0), durationMin: 360, title: "Lisbon City Tour", source: 2, desc: "Alfama, Tram 28, Past\u00e9is de Bel\u00e9m", location: "Alfama, Lisbon", virtual: false, lat: 38.7118, lng: -9.1300, breadcrumb: "Earth > Europe > Portugal > Lisbon" }, { start: rel(0, 25, 19, 30), durationMin: 195, title: "Flight \u2192 Berlin", source: 1, desc: "TAP TP 572 LIS \u2192 BER", location: "Lisbon Airport", virtual: false, lat: 38.7756, lng: -9.1354, breadcrumb: "Earth > Europe > Portugal > Lisbon" }, - { start: rel(0, 26, 18, 0), durationMin: 180, title: "Hackathon \u2014 c-base", source: 2, desc: "Local-first data sync hackathon", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" }, + { start: rel(0, 25, 18, 0), durationMin: 120, title: "Workshop: CRDTs (3/4)", source: 4, desc: "Merging trees and documents", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" }, + { start: rel(0, 26, 18, 0), durationMin: 180, title: "Hackathon \u2014 c-base", source: 4, desc: "Local-first data sync hackathon", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" }, { start: rel(0, 27, 9, 0), durationMin: 30, title: "Team Standup", source: 0, desc: "Post-travel sync", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, + { start: rel(0, 27, 19, 0), durationMin: 90, title: "Climate Action Meetup", source: 4, desc: "Monthly: urban food systems", location: "Regenbogenfabrik", virtual: false, lat: 52.4945, lng: 13.4290, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, { start: rel(0, 28, 15, 0), durationMin: 90, title: "Architecture Review", source: 0, desc: "Review local-first sync architecture", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, - // ── NEXT MONTH ── + // ── NEXT MONTH (+1) ── + { start: rel(1, 1, 18, 0), durationMin: 120, title: "Workshop: CRDTs (4/4)", source: 4, desc: "Final session: production patterns", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" }, { start: rel(1, 2, 10, 0), durationMin: 120, title: "Sprint 25 Planning", source: 0, desc: "Plan next sprint", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(1, 3, 6, 0), durationMin: 180, title: "Flight \u2192 Barcelona", source: 1, desc: "VY 1862 BER \u2192 BCN", location: "BER Airport", virtual: false, lat: 52.3667, lng: 13.5033, breadcrumb: "Earth > Europe > Germany > Berlin" }, { start: rel(1, 3, 14, 0), durationMin: 240, title: "Team Retreat Day 1", source: 0, desc: "Strategy offsite at Barcel\u00f3 Sants", location: "Barcel\u00f3 Sants, Barcelona", virtual: false, lat: 41.3795, lng: 2.1405, breadcrumb: "Earth > Europe > Spain > Barcelona" }, { start: rel(1, 4, 9, 0), durationMin: 480, title: "Team Retreat Day 2", source: 0, desc: "Workshops + Sagrada Familia visit", location: "Barcelona", virtual: false, lat: 41.4036, lng: 2.1744, breadcrumb: "Earth > Europe > Spain > Barcelona" }, { start: rel(1, 5, 15, 0), durationMin: 180, title: "Flight \u2192 Berlin", source: 1, desc: "VY 1863 BCN \u2192 BER", location: "BCN Airport", virtual: false, lat: 41.2974, lng: 2.0833, breadcrumb: "Earth > Europe > Spain > Barcelona" }, + { start: rel(1, 6, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(1, 8, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, { start: rel(1, 8, 19, 0), durationMin: 120, title: "Berlin Philharmonic", source: 2, desc: "Brahms Symphony No. 4", location: "Berliner Philharmonie", virtual: false, lat: 52.5103, lng: 13.3699, breadcrumb: "Earth > Europe > Germany > Berlin > Tiergarten" }, + { start: rel(1, 10, 19, 0), durationMin: 90, title: "Book Club", source: 4, desc: '"Braiding Sweetgrass"', location: "Shakespeare & Sons, Berlin", virtual: false, lat: 52.4925, lng: 13.4310, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, { start: rel(1, 12, 10, 0), durationMin: 120, title: "Brussels Workshop", source: 3, desc: "EU Digital Commons Working Group", location: "Brussels", virtual: false, lat: 50.8503, lng: 4.3517, breadcrumb: "Earth > Europe > Belgium > Brussels" }, + { start: rel(1, 15, 9, 0), durationMin: 30, title: "Team Standup", source: 0, desc: "Weekly sync", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, + { start: rel(1, 20, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(1, 22, 14, 0), durationMin: 45, title: "Physiotherapy", source: 5, desc: "Shoulder rehab follow-up", location: "Praxis Neuk\u00f6lln", virtual: false, lat: 52.4812, lng: 13.4350, breadcrumb: "Earth > Europe > Germany > Berlin > Neuk\u00f6lln" }, + { start: rel(1, 25, 19, 0), durationMin: 90, title: "Climate Action Meetup", source: 4, desc: "Monthly: renewable energy co-ops", location: "Regenbogenfabrik", virtual: false, lat: 52.4945, lng: 13.4290, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + + // ── MONTH +2 ── + { start: rel(2, 3, 9, 0), durationMin: 30, title: "Team Standup", source: 0, desc: "Weekly sync", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, + { start: rel(2, 5, 10, 0), durationMin: 120, title: "Sprint Planning", source: 0, desc: "Sprint 26", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" }, + { start: rel(2, 8, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(2, 10, 19, 0), durationMin: 90, title: "Book Club", source: 4, desc: '"Ministry for the Future"', location: "Shakespeare & Sons, Berlin", virtual: false, lat: 52.4925, lng: 13.4310, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(2, 14, 9, 0), durationMin: 480, title: "DWeb Camp Day 1", source: 3, desc: "Decentralized web camp", location: "Camp Navarro, CA", virtual: false, lat: 39.1766, lng: -123.6335, breadcrumb: "Earth > Americas > USA > California" }, + { start: rel(2, 15, 9, 0), durationMin: 480, title: "DWeb Camp Day 2", source: 3, desc: "Workshops and lightning talks", location: "Camp Navarro, CA", virtual: false, lat: 39.1766, lng: -123.6335, breadcrumb: "Earth > Americas > USA > California" }, + { start: rel(2, 20, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + { start: rel(2, 25, 19, 0), durationMin: 90, title: "Climate Action Meetup", source: 4, desc: "Monthly: water commons", location: "Regenbogenfabrik", virtual: false, lat: 52.4945, lng: 13.4290, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" }, + + // ── MONTH +3 — tentative events begin ── + { start: rel(3, 5, 10, 0), durationMin: 120, title: "Sprint Planning", source: 0, desc: "Sprint 27", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin", likelihood: 80 }, + { start: rel(3, 8, 9, 0), durationMin: 540, title: "IPFS Camp", source: 3, desc: "Potential IPFS community gathering", location: "Lisbon", virtual: false, lat: 38.7223, lng: -9.1393, breadcrumb: "Earth > Europe > Portugal > Lisbon", likelihood: 50 }, + { start: rel(3, 9, 9, 0), durationMin: 540, title: "IPFS Camp Day 2", source: 3, desc: "Workshops track", location: "Lisbon", virtual: false, lat: 38.7223, lng: -9.1393, breadcrumb: "Earth > Europe > Portugal > Lisbon", likelihood: 50 }, + { start: rel(3, 12, 19, 0), durationMin: 90, title: "Book Club", source: 4, desc: "TBD", location: "Shakespeare & Sons", virtual: false, lat: 52.4925, lng: 13.4310, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg", likelihood: 70 }, + { start: rel(3, 15, 14, 0), durationMin: 45, title: "Physiotherapy", source: 5, desc: "Check-in session", location: "Praxis Neuk\u00f6lln", virtual: false, lat: 52.4812, lng: 13.4350, breadcrumb: "Earth > Europe > Germany > Berlin > Neuk\u00f6lln", likelihood: 75 }, + { start: rel(3, 20, 10, 0), durationMin: 480, title: "Summer Retreat \u2014 Alps", source: 2, desc: "Hiking & writing retreat in Bavarian Alps", location: "Garmisch-Partenkirchen", virtual: false, lat: 47.4921, lng: 11.0958, breadcrumb: "Earth > Europe > Germany > Bavaria", likelihood: 60 }, + { start: rel(3, 21, 10, 0), durationMin: 480, title: "Summer Retreat Day 2", source: 2, desc: "Zugspitze summit attempt", location: "Garmisch-Partenkirchen", virtual: false, lat: 47.4921, lng: 11.0958, breadcrumb: "Earth > Europe > Germany > Bavaria", likelihood: 60 }, + + // ── MONTH +4 ── + { start: rel(4, 2, 10, 0), durationMin: 120, title: "Sprint Planning", source: 0, desc: "Sprint 28", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin", likelihood: 75 }, + { start: rel(4, 6, 9, 0), durationMin: 480, title: "EthCC Paris", source: 3, desc: "Ethereum community conference", location: "Palais Brongniart, Paris", virtual: false, lat: 48.8696, lng: 2.3413, breadcrumb: "Earth > Europe > France > Paris", likelihood: 65 }, + { start: rel(4, 7, 9, 0), durationMin: 480, title: "EthCC Day 2", source: 3, desc: "Panel on decentralized identity", location: "Palais Brongniart, Paris", virtual: false, lat: 48.8696, lng: 2.3413, breadcrumb: "Earth > Europe > France > Paris", likelihood: 65 }, + { start: rel(4, 15, 19, 0), durationMin: 90, title: "Climate Action Meetup", source: 4, desc: "Monthly: transport decarbonization", location: "Regenbogenfabrik", virtual: false, lat: 52.4945, lng: 13.4290, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg", likelihood: 70 }, + { start: rel(4, 20, 19, 0), durationMin: 90, title: "Book Club", source: 4, desc: "TBD", location: "Shakespeare & Sons", virtual: false, lat: 52.4925, lng: 13.4310, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg", likelihood: 60 }, + + // ── MONTH +5 ── + { start: rel(5, 5, 10, 0), durationMin: 120, title: "Sprint Planning", source: 0, desc: "Sprint 29", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin", likelihood: 60 }, + { start: rel(5, 10, 10, 0), durationMin: 480, title: "Team Offsite \u2014 Tenerife", source: 0, desc: "Winter offsite planning", location: "Tenerife", virtual: false, lat: 28.2916, lng: -16.6291, breadcrumb: "Earth > Europe > Spain > Canary Islands", likelihood: 40 }, + { start: rel(5, 11, 10, 0), durationMin: 480, title: "Team Offsite Day 2", source: 0, desc: "Workshops + hike", location: "Tenerife", virtual: false, lat: 28.2916, lng: -16.6291, breadcrumb: "Earth > Europe > Spain > Canary Islands", likelihood: 40 }, + { start: rel(5, 18, 19, 0), durationMin: 90, title: "Book Club", source: 4, desc: "TBD", location: "Shakespeare & Sons", virtual: false, lat: 52.4925, lng: 13.4310, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg", likelihood: 55 }, + + // ── MONTH +6 ── + { start: rel(6, 3, 9, 0), durationMin: 480, title: "RheinMain Salon", source: 3, desc: "Regional tech meetup", location: "Frankfurt", virtual: false, lat: 50.1109, lng: 8.6821, breadcrumb: "Earth > Europe > Germany > Hessen > Frankfurt", likelihood: 45 }, + { start: rel(6, 12, 10, 0), durationMin: 480, title: "DevConnect Istanbul", source: 3, desc: "Ethereum developer conference", location: "Istanbul", virtual: false, lat: 41.0082, lng: 28.9784, breadcrumb: "Earth > Europe > Turkey > Istanbul", likelihood: 35 }, + { start: rel(6, 13, 10, 0), durationMin: 480, title: "DevConnect Day 2", source: 3, desc: "Identity & privacy track", location: "Istanbul", virtual: false, lat: 41.0082, lng: 28.9784, breadcrumb: "Earth > Europe > Turkey > Istanbul", likelihood: 35 }, + { start: rel(6, 20, 19, 0), durationMin: 90, title: "Climate Action Meetup", source: 4, desc: "Monthly TBD", location: "Regenbogenfabrik", virtual: false, lat: 52.4945, lng: 13.4290, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg", likelihood: 50 }, + + // ── MONTH +7 ── + { start: rel(7, 5, 10, 0), durationMin: 120, title: "Sprint Planning", source: 0, desc: "Sprint 30", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin", likelihood: 40 }, + { start: rel(7, 15, 10, 0), durationMin: 480, title: "36C3 Day 1", source: 3, desc: "Chaos Communication Congress", location: "CCH, Hamburg", virtual: false, lat: 53.5631, lng: 9.9864, breadcrumb: "Earth > Europe > Germany > Hamburg", likelihood: 30 }, + { start: rel(7, 16, 10, 0), durationMin: 480, title: "36C3 Day 2", source: 3, desc: "Assembly: local-first computing", location: "CCH, Hamburg", virtual: false, lat: 53.5631, lng: 9.9864, breadcrumb: "Earth > Europe > Germany > Hamburg", likelihood: 30 }, + + // ── MONTH +8 ── + { start: rel(8, 8, 10, 0), durationMin: 480, title: "FOSDEM", source: 3, desc: "Free & Open Source Developers European Meeting", location: "ULB, Brussels", virtual: false, lat: 50.8120, lng: 4.3817, breadcrumb: "Earth > Europe > Belgium > Brussels", likelihood: 25 }, + { start: rel(8, 9, 10, 0), durationMin: 480, title: "FOSDEM Day 2", source: 3, desc: "Decentralization devroom", location: "ULB, Brussels", virtual: false, lat: 50.8120, lng: 4.3817, breadcrumb: "Earth > Europe > Belgium > Brussels", likelihood: 25 }, ]; this.events = demoEvents.map((e, i) => { @@ -491,14 +568,15 @@ class FolkCalendarView extends HTMLElement { virtual_url: e.virtual ? "#" : undefined, latitude: e.lat, longitude: e.lng, + likelihood: e.likelihood ?? null, }; }); this.sources = sources; - // Compute lunar phases for visible range (±6 months for season/year views) + // Compute lunar phases for visible range (month-6 through month+9) const lunar: Record = {}; - for (let m = month - 6; m <= month + 6; m++) { + for (let m = month - 6; m <= month + 9; m++) { const actualYear = m < 0 ? year - 1 : (m > 11 ? year + 1 : year); const actualMonth = ((m % 12) + 12) % 12; const dim = new Date(actualYear, actualMonth + 1, 0).getDate(); @@ -593,6 +671,25 @@ class FolkCalendarView extends HTMLElement { && !this.filteredSources.has(e.source_name)); } + /** Returns visual style overrides for tentative (likelihood < 100) events. */ + private getEventStyles(ev: any): { bgColor: string; borderStyle: string; opacity: number; isTentative: boolean; likelihoodLabel: string } { + const baseColor = ev.source_color || "#6366f1"; + const likelihood: number | null = ev.likelihood ?? null; + if (likelihood === null || likelihood >= 100) { + return { bgColor: `${baseColor}20`, borderStyle: "solid", opacity: 1, isTentative: false, likelihoodLabel: "" }; + } + // Scale opacity from 0.5 (0%) to 1.0 (100%) + const alpha = Math.round(0.5 + (likelihood / 100) * 0.5); + const hexAlpha = Math.round((0.08 + (likelihood / 100) * 0.12) * 255).toString(16).padStart(2, "0"); + return { + bgColor: `${baseColor}${hexAlpha}`, + borderStyle: "dashed", + opacity: 0.5 + (likelihood / 100) * 0.5, + isTentative: true, + likelihoodLabel: `${likelihood}%`, + }; + } + private getSpatialLabel(breadcrumb: string | null | undefined, level: number): string { if (!breadcrumb) return ""; const parts = breadcrumb.split(" > ").map(s => s.trim()); @@ -695,11 +792,12 @@ class FolkCalendarView extends HTMLElement {
${this.renderCalendarContent()}
+ ${isDocked ? `
${this.renderZoomController()}
` : ""} ${this.renderMapPanel()}
- ${this.renderZoomController()} + ${!isDocked ? this.renderZoomController() : ""} ${this.renderLunarOverlay()}
@@ -897,17 +995,13 @@ class FolkCalendarView extends HTMLElement { return ``; } - const mode = this.mapPanelState; const spatialLabel = SPATIAL_LABELS[this.getEffectiveSpatialIndex()]; - return `
+ return `
\u{1F5FA} ${spatialLabel}
- ${mode === "floating" - ? `` - : ``} - +
@@ -970,15 +1064,22 @@ class FolkCalendarView extends HTMLElement {
${dayEvents.length > 0 ? `
- ${dayEvents.slice(0, 5).map(e => ``).join("")} + ${dayEvents.slice(0, 5).map(e => { + const es = this.getEventStyles(e); + return es.isTentative + ? `` + : ``; + }).join("")} ${dayEvents.length > 5 ? `+${dayEvents.length - 5}` : ""}
${dayEvents.slice(0, 2).map(e => { const evColor = e.source_color || "#6366f1"; + const es = this.getEventStyles(e); const city = this.getSpatialLabel(e.location_breadcrumb, 5); const cityHtml = city ? `${this.esc(city)}` : ""; const virtualHtml = e.is_virtual ? `\u{1F4BB}` : ""; - return `
${e.rToolSource === "rSchedule" ? '🔔' : ""}${virtualHtml}${this.formatTime(e.start_time)}${this.esc(e.title)}${cityHtml}
`; + const likelihoodHtml = es.isTentative ? `${es.likelihoodLabel}` : ""; + return `
${e.rToolSource === "rSchedule" ? '🔔' : ""}${virtualHtml}${this.formatTime(e.start_time)}${this.esc(e.title)}${likelihoodHtml}${cityHtml}
`; }).join("")} ` : ""}
`; @@ -1079,11 +1180,11 @@ class FolkCalendarView extends HTMLElement { const duration = Math.max(endMin - startMin, 30); const leftPx = ((startMin - START_HOUR * 60) / 60) * HOUR_WIDTH; const widthPx = Math.max((duration / 60) * HOUR_WIDTH, 60); - const bgColor = ev.source_color ? `${ev.source_color}24` : "#6366f124"; + const es = this.getEventStyles(ev); const virtualBadge = ev.is_virtual ? `\u{1F4BB}` : ""; eventsHtml += `
+ left:${leftPx}px;width:${widthPx}px;background:${es.bgColor};border-top:3px ${es.borderStyle} ${ev.source_color || "#6366f1"};opacity:${es.opacity}">
${virtualBadge}${this.esc(ev.title)}
${this.formatTime(ev.start_time)} \u2013 ${this.formatTime(ev.end_time)}
${ev.location_name ? `
${this.esc(ev.location_name)}
` : ""} @@ -1364,15 +1465,16 @@ class FolkCalendarView extends HTMLElement { const duration = Math.max(endMin - startMin, 30); const topPx = ((startMin - START_HOUR * 60) / 60) * HOUR_HEIGHT; const heightPx = Math.max((duration / 60) * HOUR_HEIGHT, 24); - const bgColor = ev.source_color ? `${ev.source_color}20` : "#6366f120"; + const es = this.getEventStyles(ev); const showDesc = heightPx >= 60 && ev.description; const descPreview = showDesc ? (ev.description.length > 60 ? ev.description.slice(0, 57) + "..." : ev.description) : ""; const neighborhood = this.getSpatialLabel(ev.location_breadcrumb, 6); const virtualBadge = ev.is_virtual ? `\u{1F4BB} ${this.esc(ev.virtual_platform || 'Virtual')}` : ""; + const likelihoodBadge = es.isTentative ? `${es.likelihoodLabel}` : ""; eventsHtml += `
-
${this.esc(ev.title)}
+ top:${topPx}px;height:${heightPx}px;background:${es.bgColor};border-left-color:${ev.source_color || "#6366f1"};border-left-style:${es.borderStyle};opacity:${es.opacity}"> +
${this.esc(ev.title)}${likelihoodBadge}
${this.formatTime(ev.start_time)} \u2013 ${this.formatTime(ev.end_time)}${virtualBadge}
${ev.location_name ? `
${this.esc(ev.location_name)}
` : ""} ${neighborhood ? `${this.esc(neighborhood)}` : ""} @@ -1453,7 +1555,7 @@ class FolkCalendarView extends HTMLElement { const duration = Math.max(endMin - startMin, 20); const topPx = ((startMin - START_HOUR * 60) / 60) * HOUR_HEIGHT; const heightPx = Math.max((duration / 60) * HOUR_HEIGHT, 18); - const bgColor = ev.source_color ? `${ev.source_color}28` : "#6366f128"; + const es = this.getEventStyles(ev); const colLeft = `calc(44px + ${i} * ((100% - 44px) / 7) + 2px)`; const colWidth = `calc((100% - 44px) / 7 - 4px)`; const showMeta = heightPx >= 36; @@ -1462,7 +1564,7 @@ class FolkCalendarView extends HTMLElement { const virtualBadge = ev.is_virtual ? `\u{1F4BB}` : ""; eventsOverlay += `
+ background:${es.bgColor};border-left-color:${ev.source_color || "#6366f1"};border-left-style:${es.borderStyle};opacity:${es.opacity}">
${virtualBadge}${this.esc(ev.title)}
${showMeta ? `
${timeStr}${locName ? `${locName}` : ""}
` : ""}
`; @@ -1503,10 +1605,12 @@ class FolkCalendarView extends HTMLElement { dayEvents.sort((a, b) => a.start_time.localeCompare(b.start_time)).map(e => { const ddDesc = e.description ? (e.description.length > 80 ? e.description.slice(0, 77) + "..." : e.description) : ""; const srcTag = e.source_name ? `${this.esc(e.source_name)}` : ""; - return `
-
+ const es = this.getEventStyles(e); + const likelihoodBadge = es.isTentative ? `${es.likelihoodLabel}` : ""; + return `
+
-
${this.esc(e.title)}${srcTag}
+
${this.esc(e.title)}${likelihoodBadge}${srcTag}
${this.formatTime(e.start_time)}${e.end_time ? ` \u2013 ${this.formatTime(e.end_time)}` : ""}${e.location_name ? ` \u00B7 ${this.esc(e.location_name)}` : ""}${e.is_virtual ? " \u00B7 Virtual" : ""}
${ddDesc ? `
${this.esc(ddDesc)}
` : ""}
@@ -1519,11 +1623,13 @@ class FolkCalendarView extends HTMLElement { private renderEventModal(): string { const e = this.selectedEvent; + const es = this.getEventStyles(e); return `