feat(rcal): full-width spectrum bars + draggable map resize
Move temporal/spatial zoom bars to always-visible full-width position above the calendar+map area. Replace fixed 400px map panel with draggable resize handle (200-800px range). Responsive: handle hidden and layout collapses to single column at ≤900px. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
02b9feb760
commit
8ff3e83a12
|
|
@ -137,6 +137,12 @@ class FolkCalendarView extends HTMLElement {
|
||||||
private lunarOverlayExpanded = false;
|
private lunarOverlayExpanded = false;
|
||||||
private _wheelTimer: ReturnType<typeof setTimeout> | null = null;
|
private _wheelTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
// Map resize state
|
||||||
|
private mapPanelWidth = 400;
|
||||||
|
private _isResizing = false;
|
||||||
|
private _resizeStartX = 0;
|
||||||
|
private _resizeStartWidth = 0;
|
||||||
|
|
||||||
// Transition state
|
// Transition state
|
||||||
private _pendingTransition: 'nav-left' | 'nav-right' | 'zoom-in' | 'zoom-out' | null = null;
|
private _pendingTransition: 'nav-left' | 'nav-right' | 'zoom-in' | 'zoom-out' | null = null;
|
||||||
private _ghostHtml: string | null = null;
|
private _ghostHtml: string | null = null;
|
||||||
|
|
@ -196,6 +202,11 @@ class FolkCalendarView extends HTMLElement {
|
||||||
this.boundKeyHandler = null;
|
this.boundKeyHandler = null;
|
||||||
}
|
}
|
||||||
if (this._wheelTimer) { clearTimeout(this._wheelTimer); this._wheelTimer = 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._pointerCache = [];
|
||||||
this._panStartX = this._panStartY = this._initialPinchDist = null;
|
this._panStartX = this._panStartY = this._initialPinchDist = null;
|
||||||
this._gestureMode = 'none';
|
this._gestureMode = 'none';
|
||||||
|
|
@ -861,16 +872,17 @@ class FolkCalendarView extends HTMLElement {
|
||||||
|
|
||||||
${this.renderSources()}
|
${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">
|
<div class="calendar-pane" id="calendar-pane">
|
||||||
${this.renderCalendarContent()}
|
${this.renderCalendarContent()}
|
||||||
</div>
|
</div>
|
||||||
${isDocked ? `<div class="zoom-bar--middle">${this.renderZoomController()}</div>` : ""}
|
${isDocked ? `<div class="resize-handle" id="resize-handle"></div>` : ""}
|
||||||
${this.renderMapPanel()}
|
${this.renderMapPanel()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bottom-bar">
|
<div class="bottom-bar">
|
||||||
${!isDocked ? this.renderZoomController() : ""}
|
|
||||||
${this.renderLunarOverlay()}
|
${this.renderLunarOverlay()}
|
||||||
<button class="bottom-bar__lunar-toggle ${this.showLunar ? "active" : ""}" id="toggle-lunar" title="Toggle lunar phases (l)">\u{1F319}</button>
|
<button class="bottom-bar__lunar-toggle ${this.showLunar ? "active" : ""}" id="toggle-lunar" title="Toggle lunar phases (l)">\u{1F319}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1883,6 +1895,62 @@ class FolkCalendarView extends HTMLElement {
|
||||||
$("map-fab")?.addEventListener("click", () => { this.mapPanelState = "docked"; this.render(); });
|
$("map-fab")?.addEventListener("click", () => { this.mapPanelState = "docked"; this.render(); });
|
||||||
$("map-minimize")?.addEventListener("click", () => { this.mapPanelState = "minimized"; 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
|
// Zoom spectrum slider — click on track or tick labels
|
||||||
const zoomTrack = $("zoom-track");
|
const zoomTrack = $("zoom-track");
|
||||||
if (zoomTrack) {
|
if (zoomTrack) {
|
||||||
|
|
@ -2452,17 +2520,23 @@ class FolkCalendarView extends HTMLElement {
|
||||||
.src-badge:hover { filter: brightness(1.2); }
|
.src-badge:hover { filter: brightness(1.2); }
|
||||||
.src-badge.filtered { opacity: 0.3; text-decoration: line-through; }
|
.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 ── */
|
||||||
.main-layout { position: relative; min-height: 400px; }
|
.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; }
|
.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; }
|
/* ── Resize Handle ── */
|
||||||
.zoom-bar--middle .zoom-bar__label-end { display: none; }
|
.resize-handle { width: 6px; cursor: col-resize; background: transparent; position: relative; z-index: 10; transition: background 0.15s; }
|
||||||
.zoom-bar--middle .zoom-bar__track { min-width: 100px; }
|
.resize-handle:hover, .resize-handle.active { background: var(--rs-primary-hover); opacity: 0.5; }
|
||||||
.zoom-bar--middle .zoom-bar__tick-label { font-size: 7px; }
|
.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; }
|
||||||
.zoom-bar--middle .coupling-btn { padding: 2px 6px; font-size: 10px; }
|
.resize-handle:hover::before, .resize-handle.active::before { opacity: 0.8; }
|
||||||
.zoom-bar--middle .variant-indicator { display: none; }
|
|
||||||
|
|
||||||
/* ── Map Panel ── */
|
/* ── 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; }
|
.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 ── */
|
/* ── Tablet/Adaptive ── */
|
||||||
@media (max-width: 1024px) {
|
@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) {
|
@media (max-width: 900px) and (min-width: 769px) {
|
||||||
.main-layout--docked { grid-template-columns: 1fr; }
|
.main-layout--docked { grid-template-columns: 1fr !important; }
|
||||||
.zoom-bar--middle { display: none; }
|
.resize-handle { display: none; }
|
||||||
.ev-label { display: none; }
|
.ev-label { display: none; }
|
||||||
.day { min-height: 64px; }
|
.day { min-height: 64px; }
|
||||||
|
.zoom-bar--top .zoom-bar__track { min-width: 140px; }
|
||||||
}
|
}
|
||||||
/* ── Mobile ── */
|
/* ── Mobile ── */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
:host { padding: 0.25rem; }
|
:host { padding: 0.25rem; }
|
||||||
.main-layout--docked { grid-template-columns: 1fr; }
|
.main-layout--docked { grid-template-columns: 1fr !important; }
|
||||||
.zoom-bar--middle { display: none; }
|
.resize-handle { display: none; }
|
||||||
.year-grid { grid-template-columns: repeat(3, 1fr); }
|
.year-grid { grid-template-columns: repeat(3, 1fr); }
|
||||||
.season-grid { grid-template-columns: 1fr; }
|
.season-grid { grid-template-columns: 1fr; }
|
||||||
.day { min-height: 52px; padding: 4px; }
|
.day { min-height: 52px; padding: 4px; }
|
||||||
|
|
@ -2780,6 +2855,8 @@ class FolkCalendarView extends HTMLElement {
|
||||||
.wd { font-size: 10px; padding: 3px; }
|
.wd { font-size: 10px; padding: 3px; }
|
||||||
.season-cities, .week-event-meta, .tl-event-desc, .yv-country { display: none; }
|
.season-cities, .week-event-meta, .tl-event-desc, .yv-country { display: none; }
|
||||||
.week-view { font-size: 10px; }
|
.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__track { min-width: 120px; }
|
||||||
.zoom-bar__label-end { font-size: 9px; }
|
.zoom-bar__label-end { font-size: 9px; }
|
||||||
.zoom-bar__tick-label { font-size: 8px; }
|
.zoom-bar__tick-label { font-size: 8px; }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue