Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-20 17:46:51 -07:00
commit 90b4426484
1 changed files with 93 additions and 16 deletions

View File

@ -137,6 +137,12 @@ class FolkCalendarView extends HTMLElement {
private lunarOverlayExpanded = false;
private _wheelTimer: ReturnType<typeof setTimeout> | null = null;
// Map resize state
private mapPanelWidth = 400;
private _isResizing = false;
private _resizeStartX = 0;
private _resizeStartWidth = 0;
// Transition state
private _pendingTransition: 'nav-left' | 'nav-right' | 'zoom-in' | 'zoom-out' | null = null;
private _ghostHtml: string | null = null;
@ -196,6 +202,11 @@ class FolkCalendarView extends HTMLElement {
this.boundKeyHandler = null;
}
if (this._wheelTimer) { clearTimeout(this._wheelTimer); this._wheelTimer = null; }
if (this._isResizing) {
this._isResizing = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
this._pointerCache = [];
this._panStartX = this._panStartY = this._initialPinchDist = null;
this._gestureMode = 'none';
@ -861,16 +872,17 @@ class FolkCalendarView extends HTMLElement {
${this.renderSources()}
<div class="main-layout ${isDocked ? "main-layout--docked" : ""}">
<div class="zoom-bar--top">${this.renderZoomController()}</div>
<div class="main-layout ${isDocked ? "main-layout--docked" : ""}" id="main-layout"${isDocked ? ` style="grid-template-columns: 1fr 6px ${this.mapPanelWidth}px"` : ""}>
<div class="calendar-pane" id="calendar-pane">
${this.renderCalendarContent()}
</div>
${isDocked ? `<div class="zoom-bar--middle">${this.renderZoomController()}</div>` : ""}
${isDocked ? `<div class="resize-handle" id="resize-handle"></div>` : ""}
${this.renderMapPanel()}
</div>
<div class="bottom-bar">
${!isDocked ? this.renderZoomController() : ""}
${this.renderLunarOverlay()}
<button class="bottom-bar__lunar-toggle ${this.showLunar ? "active" : ""}" id="toggle-lunar" title="Toggle lunar phases (l)">\u{1F319}</button>
</div>
@ -1883,6 +1895,62 @@ class FolkCalendarView extends HTMLElement {
$("map-fab")?.addEventListener("click", () => { this.mapPanelState = "docked"; this.render(); });
$("map-minimize")?.addEventListener("click", () => { this.mapPanelState = "minimized"; this.render(); });
// Resize handle drag
const resizeHandle = $("resize-handle");
const mainLayout = $("main-layout");
if (resizeHandle && mainLayout) {
const startResize = (startX: number) => {
this._isResizing = true;
this._resizeStartX = startX;
this._resizeStartWidth = this.mapPanelWidth;
resizeHandle.classList.add("active");
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
};
const doResize = (clientX: number) => {
if (!this._isResizing) return;
const delta = this._resizeStartX - clientX; // drag left = bigger map
const newWidth = Math.max(200, Math.min(800, this._resizeStartWidth + delta));
this.mapPanelWidth = newWidth;
(mainLayout as HTMLElement).style.gridTemplateColumns = `1fr 6px ${newWidth}px`;
};
const endResize = () => {
if (!this._isResizing) return;
this._isResizing = false;
resizeHandle.classList.remove("active");
document.body.style.cursor = "";
document.body.style.userSelect = "";
if (this.leafletMap) this.leafletMap.invalidateSize();
};
resizeHandle.addEventListener("mousedown", (e: Event) => {
(e as MouseEvent).preventDefault();
startResize((e as MouseEvent).clientX);
const onMouseMove = (em: MouseEvent) => doResize(em.clientX);
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
endResize();
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
resizeHandle.addEventListener("touchstart", (e: Event) => {
const te = e as TouchEvent;
te.preventDefault();
startResize(te.touches[0].clientX);
const onTouchMove = (em: Event) => doResize((em as TouchEvent).touches[0].clientX);
const onTouchEnd = () => {
document.removeEventListener("touchmove", onTouchMove);
document.removeEventListener("touchend", onTouchEnd);
endResize();
};
document.addEventListener("touchmove", onTouchMove);
document.addEventListener("touchend", onTouchEnd);
}, { passive: false });
}
// Zoom spectrum slider — click on track or tick labels
const zoomTrack = $("zoom-track");
if (zoomTrack) {
@ -2452,17 +2520,23 @@ class FolkCalendarView extends HTMLElement {
.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 .zoom-bar__track { min-width: 200px; }
.zoom-bar--top .zoom-bar__row { gap: 12px; }
.zoom-bar--top .zoom-bar__tick-label { font-size: 10px; }
.zoom-bar--top .zoom-bar__label-end { font-size: 11px; }
/* ── Main Layout ── */
.main-layout { position: relative; min-height: 400px; }
.main-layout--docked { display: grid; grid-template-columns: 1fr auto 400px; gap: 0; min-height: 500px; }
.main-layout--docked { display: grid; grid-template-columns: 1fr 6px 400px; gap: 0; min-height: 500px; }
.calendar-pane { overflow: auto; min-width: 0; touch-action: pan-y; user-select: none; }
.zoom-bar--middle { display: flex; flex-direction: column; justify-content: center; padding: 8px 2px; border-left: 1px solid var(--rs-border-subtle); border-right: 1px solid var(--rs-border-subtle); }
.zoom-bar--middle .zoom-bar { padding: 0; }
.zoom-bar--middle .zoom-bar__label-end { display: none; }
.zoom-bar--middle .zoom-bar__track { min-width: 100px; }
.zoom-bar--middle .zoom-bar__tick-label { font-size: 7px; }
.zoom-bar--middle .coupling-btn { padding: 2px 6px; font-size: 10px; }
.zoom-bar--middle .variant-indicator { display: none; }
/* ── Resize Handle ── */
.resize-handle { width: 6px; cursor: col-resize; background: transparent; position: relative; z-index: 10; transition: background 0.15s; }
.resize-handle:hover, .resize-handle.active { background: var(--rs-primary-hover); opacity: 0.5; }
.resize-handle::before { content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 2px; height: 32px; border-radius: 1px; background: var(--rs-border-strong); opacity: 0.4; transition: opacity 0.15s; }
.resize-handle:hover::before, .resize-handle.active::before { opacity: 0.8; }
/* ── Map Panel ── */
.map-panel { background: var(--rs-bg-surface-sunken); border: 1px solid var(--rs-border-strong); border-radius: 12px; overflow: hidden; display: flex; flex-direction: column; }
@ -2753,19 +2827,20 @@ class FolkCalendarView extends HTMLElement {
/* ── Tablet/Adaptive ── */
@media (max-width: 1024px) {
.main-layout--docked { grid-template-columns: 1fr auto 320px; }
.main-layout--docked { grid-template-columns: 1fr 6px 320px; }
}
@media (max-width: 900px) and (min-width: 769px) {
.main-layout--docked { grid-template-columns: 1fr; }
.zoom-bar--middle { display: none; }
.main-layout--docked { grid-template-columns: 1fr !important; }
.resize-handle { display: none; }
.ev-label { display: none; }
.day { min-height: 64px; }
.zoom-bar--top .zoom-bar__track { min-width: 140px; }
}
/* ── Mobile ── */
@media (max-width: 768px) {
:host { padding: 0.25rem; }
.main-layout--docked { grid-template-columns: 1fr; }
.zoom-bar--middle { display: none; }
.main-layout--docked { grid-template-columns: 1fr !important; }
.resize-handle { display: none; }
.year-grid { grid-template-columns: repeat(3, 1fr); }
.season-grid { grid-template-columns: 1fr; }
.day { min-height: 52px; padding: 4px; }
@ -2780,6 +2855,8 @@ 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 .zoom-bar__track { min-width: 120px; }
.zoom-bar__track { min-width: 120px; }
.zoom-bar__label-end { font-size: 9px; }
.zoom-bar__tick-label { font-size: 8px; }