diff --git a/modules/rcal/components/folk-calendar-view.ts b/modules/rcal/components/folk-calendar-view.ts index 055a333..b7b1a3d 100644 --- a/modules/rcal/components/folk-calendar-view.ts +++ b/modules/rcal/components/folk-calendar-view.ts @@ -549,14 +549,9 @@ class FolkCalendarView extends HTMLElement { ${this.getViewLabel()} - - ${this.renderLunarOverlay()} - - ${this.renderZoomController()} - ${this.renderSources()}
@@ -566,16 +561,10 @@ class FolkCalendarView extends HTMLElement { ${this.renderMapPanel()}
-
- +/- zoom • - \u2190/\u2192 nav • - t today • - 1-6 view • - v variant • - m map • - c coupling • - l lunar • - scroll zoom +
+ ${this.renderZoomController()} + ${this.renderLunarOverlay()} +
${this.selectedEvent ? this.renderEventModal() : ""} @@ -692,29 +681,36 @@ class FolkCalendarView extends HTMLElement { { idx: 4, label: "Month" }, { idx: 5, label: "Season" }, { idx: 6, label: "Year" }, { idx: 7, label: "Years" }, ]; - const canIn = this.temporalGranularity > 2; - const canOut = this.temporalGranularity < 7; const spatialLabel = SPATIAL_LABELS[this.getEffectiveSpatialIndex()]; const maxVariant = VIEW_VARIANTS[this.viewMode] || 1; + // Position as percentage along the 5-step range (idx 2–7) + const pct = ((this.temporalGranularity - 2) / 5) * 100; - return `
- -
- ${levels.map(l => `
-
-
${l.label}
-
`).join("")} + return `
+
+ Day +
+
+
+ ${levels.map(l => { + const tickPct = ((l.idx - 2) / 5) * 100; + return `
+
+
${l.label}
+
`; + }).join("")} +
+ Years + ${maxVariant > 1 ? `
+ ${Array.from({length: maxVariant}, (_, i) => + `` + ).join("")} +
` : ""} +
- - ${maxVariant > 1 ? `
- ${Array.from({length: maxVariant}, (_, i) => - `` - ).join("")} -
` : ""} -
`; } @@ -1471,11 +1467,67 @@ class FolkCalendarView extends HTMLElement { this.render(); }); - // Zoom controller - $("zoom-in")?.addEventListener("click", () => this.zoomIn()); - $("zoom-out")?.addEventListener("click", () => this.zoomOut()); + // Zoom spectrum slider — click on track or tick labels + const zoomTrack = $("zoom-track"); + if (zoomTrack) { + const pctToGranularity = (pct: number) => Math.round(pct * 5 / 100) + 2; + const trackClick = (e: MouseEvent) => { + const rect = zoomTrack.getBoundingClientRect(); + const pct = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100)); + this.setTemporalGranularity(pctToGranularity(pct)); + }; + zoomTrack.addEventListener("click", trackClick); + + // Drag on thumb + const thumb = $("zoom-thumb"); + if (thumb) { + const startDrag = (eDown: MouseEvent) => { + eDown.preventDefault(); + eDown.stopPropagation(); + const onMove = (eMove: MouseEvent) => { + const rect = zoomTrack.getBoundingClientRect(); + const pct = Math.max(0, Math.min(100, ((eMove.clientX - rect.left) / rect.width) * 100)); + (thumb as HTMLElement).style.left = `${pct}%`; + }; + const onUp = (eUp: MouseEvent) => { + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + const rect = zoomTrack.getBoundingClientRect(); + const pct = Math.max(0, Math.min(100, ((eUp.clientX - rect.left) / rect.width) * 100)); + this.setTemporalGranularity(pctToGranularity(pct)); + }; + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + }; + thumb.addEventListener("mousedown", startDrag); + + // Touch support + thumb.addEventListener("touchstart", (e: Event) => { + const te = e as TouchEvent; + te.preventDefault(); + const onTouchMove = (em: Event) => { + const tm = em as TouchEvent; + const rect = zoomTrack.getBoundingClientRect(); + const pct = Math.max(0, Math.min(100, ((tm.touches[0].clientX - rect.left) / rect.width) * 100)); + (thumb as HTMLElement).style.left = `${pct}%`; + }; + const onTouchEnd = (eu: Event) => { + document.removeEventListener("touchmove", onTouchMove); + document.removeEventListener("touchend", onTouchEnd); + const tu = eu as TouchEvent; + const rect = zoomTrack.getBoundingClientRect(); + const touch = tu.changedTouches[0]; + const pct = Math.max(0, Math.min(100, ((touch.clientX - rect.left) / rect.width) * 100)); + this.setTemporalGranularity(pctToGranularity(pct)); + }; + document.addEventListener("touchmove", onTouchMove); + document.addEventListener("touchend", onTouchEnd); + }, { passive: false }); + } + } $$("[data-zoom]").forEach(el => { - el.addEventListener("click", () => { + el.addEventListener("click", (e) => { + e.stopPropagation(); this.setTemporalGranularity(parseInt((el as HTMLElement).dataset.zoom!)); }); }); @@ -1751,8 +1803,14 @@ class FolkCalendarView extends HTMLElement { .nav-title { font-size: 15px; font-weight: 600; flex: 1; text-align: center; color: var(--rs-text-primary); } .nav-primary { padding: 6px 14px; border-radius: 6px; border: none; background: var(--rs-primary); color: #fff; font-weight: 600; cursor: pointer; font-size: 12px; } - /* ── Lunar Overlay ── */ - .lunar-overlay { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 8px; padding: 0; margin-bottom: 12px; overflow: hidden; } + /* ── Bottom Bar ── */ + .bottom-bar { margin-top: 12px; padding-top: 8px; border-top: 1px solid var(--rs-border-subtle); display: flex; flex-direction: column; gap: 6px; } + .bottom-bar__lunar-toggle { align-self: flex-start; padding: 4px 10px; border-radius: 12px; border: 1px solid var(--rs-border); background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 14px; transition: all 0.15s; } + .bottom-bar__lunar-toggle:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); } + .bottom-bar__lunar-toggle.active { border-color: var(--rs-primary-hover); color: var(--rs-primary-hover); } + + /* ── Lunar Overlay (bottom) ── */ + .lunar-overlay { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 8px; padding: 0; overflow: hidden; } .lunar-summary { display: flex; align-items: center; gap: 12px; padding: 8px 12px; cursor: pointer; user-select: none; transition: background 0.15s; } .lunar-summary:hover { background: var(--rs-bg-hover); } .lunar-summary-phase { font-size: 13px; font-weight: 600; color: var(--rs-text-primary); text-transform: capitalize; white-space: nowrap; } @@ -1765,18 +1823,19 @@ class FolkCalendarView extends HTMLElement { .phase-chip.past { opacity: 0.4; } .phase-chip-label { text-transform: capitalize; } - /* ── Zoom Controller ── */ - .zoom-ctrl { display: flex; align-items: center; gap: 6px; margin-bottom: 12px; padding: 8px 10px; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 8px; flex-wrap: wrap; } - .zoom-btn { width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--rs-border-strong); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } - .zoom-btn:hover { border-color: var(--rs-primary-hover); color: var(--rs-text-primary); } - .zoom-btn:disabled { opacity: 0.3; cursor: not-allowed; } - .zoom-btn:disabled:hover { border-color: var(--rs-border-strong); color: var(--rs-text-secondary); } - .zoom-track { flex: 1; display: flex; align-items: center; gap: 0; min-width: 200px; } - .zoom-tick { flex: 1; text-align: center; cursor: pointer; padding: 4px 0; } - .zoom-tick-dot { width: 12px; height: 12px; border-radius: 50%; border: 2px solid var(--rs-border-strong); background: var(--rs-bg-surface); margin: 0 auto; transition: all 0.15s; } - .zoom-tick-dot.active { border-color: var(--rs-primary-hover); background: var(--rs-primary); transform: scale(1.2); } - .zoom-tick-label { font-size: 9px; color: var(--rs-text-muted); margin-top: 3px; transition: color 0.15s; } - .zoom-tick-label.active { color: #818cf8; font-weight: 600; } + /* ── Zoom Spectrum Bar ── */ + .zoom-bar { padding: 6px 0; } + .zoom-bar__row { display: flex; align-items: center; gap: 8px; } + .zoom-bar__label-end { font-size: 10px; color: var(--rs-text-muted); font-weight: 500; white-space: nowrap; user-select: none; flex-shrink: 0; } + .zoom-bar__track { position: relative; flex: 1; height: 28px; cursor: pointer; min-width: 180px; } + .zoom-bar__gradient { position: absolute; top: 10px; left: 0; right: 0; height: 8px; border-radius: 4px; background: linear-gradient(to right, #818cf8, #6366f1, #4f46e5, #4338ca, #3730a3, #312e81); pointer-events: none; } + .zoom-bar__thumb { position: absolute; top: 4px; width: 20px; height: 20px; border-radius: 50%; background: #818cf8; border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.3); transform: translateX(-50%); cursor: grab; transition: left 0.15s ease; z-index: 2; } + .zoom-bar__thumb:active { cursor: grabbing; transform: translateX(-50%) scale(1.15); transition: left 0s, transform 0.1s; } + .zoom-bar__tick { position: absolute; top: 0; transform: translateX(-50%); text-align: center; pointer-events: auto; cursor: pointer; z-index: 1; } + .zoom-bar__tick-mark { width: 2px; height: 28px; margin: 0 auto; background: var(--rs-border, rgba(255,255,255,0.15)); opacity: 0.4; border-radius: 1px; transition: opacity 0.15s; } + .zoom-bar__tick-mark.active { opacity: 0.8; } + .zoom-bar__tick-label { font-size: 9px; color: var(--rs-text-muted); margin-top: 2px; transition: color 0.15s; white-space: nowrap; } + .zoom-bar__tick-label.active { color: #818cf8; font-weight: 600; } .coupling-btn { padding: 4px 10px; border-radius: 12px; border: 1px solid var(--rs-border-strong); background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 11px; transition: all 0.15s; white-space: nowrap; flex-shrink: 0; } .coupling-btn:hover { border-color: var(--rs-border-strong); color: var(--rs-text-secondary); } .coupling-btn.coupled { border-color: var(--rs-primary-hover); color: #818cf8; background: var(--rs-bg-active); } @@ -2040,8 +2099,9 @@ 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-track { min-width: 150px; } - .zoom-ctrl { gap: 4px; padding: 6px 8px; } + .zoom-bar__track { min-width: 120px; } + .zoom-bar__label-end { font-size: 9px; } + .zoom-bar__tick-label { font-size: 8px; } .coupling-btn { font-size: 10px; padding: 3px 8px; } .lunar-summary { gap: 8px; flex-wrap: wrap; } .phase-chips { gap: 4px; }