Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-04 18:31:36 -08:00
commit b86747d4e1
1 changed files with 114 additions and 54 deletions

View File

@ -549,14 +549,9 @@ class FolkCalendarView extends HTMLElement {
<button class="nav-btn" id="prev">\u2190</button> <button class="nav-btn" id="prev">\u2190</button>
<button class="nav-btn" id="today">Today</button> <button class="nav-btn" id="today">Today</button>
<span class="nav-title">${this.getViewLabel()}</span> <span class="nav-title">${this.getViewLabel()}</span>
<button class="nav-btn ${this.showLunar ? "active" : ""}" id="toggle-lunar">\u{1F319}</button>
<button class="nav-btn" id="next">\u2192</button> <button class="nav-btn" id="next">\u2192</button>
</div> </div>
${this.renderLunarOverlay()}
${this.renderZoomController()}
${this.renderSources()} ${this.renderSources()}
<div class="main-layout ${isDocked ? "main-layout--docked" : ""}"> <div class="main-layout ${isDocked ? "main-layout--docked" : ""}">
@ -566,16 +561,10 @@ class FolkCalendarView extends HTMLElement {
${this.renderMapPanel()} ${this.renderMapPanel()}
</div> </div>
<div class="kbd-hint"> <div class="bottom-bar">
<kbd>+</kbd>/<kbd>-</kbd> zoom &bull; ${this.renderZoomController()}
<kbd>\u2190</kbd>/<kbd>\u2192</kbd> nav &bull; ${this.renderLunarOverlay()}
<kbd>t</kbd> today &bull; <button class="bottom-bar__lunar-toggle ${this.showLunar ? "active" : ""}" id="toggle-lunar" title="Toggle lunar phases (l)">\u{1F319}</button>
<kbd>1-6</kbd> view &bull;
<kbd>v</kbd> variant &bull;
<kbd>m</kbd> map &bull;
<kbd>c</kbd> coupling &bull;
<kbd>l</kbd> lunar &bull;
<kbd>scroll</kbd> zoom
</div> </div>
${this.selectedEvent ? this.renderEventModal() : ""} ${this.selectedEvent ? this.renderEventModal() : ""}
@ -692,29 +681,36 @@ class FolkCalendarView extends HTMLElement {
{ idx: 4, label: "Month" }, { idx: 5, label: "Season" }, { idx: 4, label: "Month" }, { idx: 5, label: "Season" },
{ idx: 6, label: "Year" }, { idx: 7, label: "Years" }, { 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 spatialLabel = SPATIAL_LABELS[this.getEffectiveSpatialIndex()];
const maxVariant = VIEW_VARIANTS[this.viewMode] || 1; const maxVariant = VIEW_VARIANTS[this.viewMode] || 1;
// Position as percentage along the 5-step range (idx 27)
const pct = ((this.temporalGranularity - 2) / 5) * 100;
return `<div class="zoom-ctrl"> return `<div class="zoom-bar">
<button class="zoom-btn" id="zoom-in" ${!canIn ? "disabled" : ""} title="Zoom in (+)">+</button> <div class="zoom-bar__row">
<div class="zoom-track"> <span class="zoom-bar__label-end">Day</span>
${levels.map(l => `<div class="zoom-tick" data-zoom="${l.idx}"> <div class="zoom-bar__track" id="zoom-track">
<div class="zoom-tick-dot ${this.temporalGranularity === l.idx ? "active" : ""}"></div> <div class="zoom-bar__gradient"></div>
<div class="zoom-tick-label ${this.temporalGranularity === l.idx ? "active" : ""}">${l.label}</div> <div class="zoom-bar__thumb" id="zoom-thumb" style="left:${pct}%"></div>
</div>`).join("")} ${levels.map(l => {
const tickPct = ((l.idx - 2) / 5) * 100;
return `<div class="zoom-bar__tick" data-zoom="${l.idx}" style="left:${tickPct}%">
<div class="zoom-bar__tick-mark ${this.temporalGranularity === l.idx ? "active" : ""}"></div>
<div class="zoom-bar__tick-label ${this.temporalGranularity === l.idx ? "active" : ""}">${l.label}</div>
</div>`;
}).join("")}
</div>
<span class="zoom-bar__label-end">Years</span>
${maxVariant > 1 ? `<div class="variant-indicator" title="Press v to toggle variant">
${Array.from({length: maxVariant}, (_, i) =>
`<span class="variant-dot ${i === this.viewVariant ? "active" : ""}"></span>`
).join("")}
</div>` : ""}
<button class="coupling-btn ${this.zoomCoupled ? "coupled" : ""}" id="toggle-coupling"
title="${this.zoomCoupled ? "Unlink spatial zoom (c)" : "Link spatial zoom (c)"}">
${this.zoomCoupled ? "\u{1F517}" : "\u{1F513}"} ${spatialLabel}
</button>
</div> </div>
<button class="zoom-btn" id="zoom-out" ${!canOut ? "disabled" : ""} title="Zoom out (\u2212)">&#x2212;</button>
${maxVariant > 1 ? `<div class="variant-indicator" title="Press v to toggle variant">
${Array.from({length: maxVariant}, (_, i) =>
`<span class="variant-dot ${i === this.viewVariant ? "active" : ""}"></span>`
).join("")}
</div>` : ""}
<button class="coupling-btn ${this.zoomCoupled ? "coupled" : ""}" id="toggle-coupling"
title="${this.zoomCoupled ? "Unlink spatial zoom (c)" : "Link spatial zoom (c)"}">
${this.zoomCoupled ? "\u{1F517}" : "\u{1F513}"} ${spatialLabel}
</button>
</div>`; </div>`;
} }
@ -1471,11 +1467,67 @@ class FolkCalendarView extends HTMLElement {
this.render(); this.render();
}); });
// Zoom controller // Zoom spectrum slider — click on track or tick labels
$("zoom-in")?.addEventListener("click", () => this.zoomIn()); const zoomTrack = $("zoom-track");
$("zoom-out")?.addEventListener("click", () => this.zoomOut()); 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 => { $$("[data-zoom]").forEach(el => {
el.addEventListener("click", () => { el.addEventListener("click", (e) => {
e.stopPropagation();
this.setTemporalGranularity(parseInt((el as HTMLElement).dataset.zoom!)); 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-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; } .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 ── */ /* ── Bottom Bar ── */
.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 { 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 { 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: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; } .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.past { opacity: 0.4; }
.phase-chip-label { text-transform: capitalize; } .phase-chip-label { text-transform: capitalize; }
/* ── Zoom Controller ── */ /* ── Zoom Spectrum Bar ── */
.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-bar { padding: 6px 0; }
.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-bar__row { display: flex; align-items: center; gap: 8px; }
.zoom-btn:hover { border-color: var(--rs-primary-hover); color: var(--rs-text-primary); } .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-btn:disabled { opacity: 0.3; cursor: not-allowed; } .zoom-bar__track { position: relative; flex: 1; height: 28px; cursor: pointer; min-width: 180px; }
.zoom-btn:disabled:hover { border-color: var(--rs-border-strong); color: var(--rs-text-secondary); } .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-track { flex: 1; display: flex; align-items: center; gap: 0; min-width: 200px; } .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-tick { flex: 1; text-align: center; cursor: pointer; padding: 4px 0; } .zoom-bar__thumb:active { cursor: grabbing; transform: translateX(-50%) scale(1.15); transition: left 0s, transform 0.1s; }
.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-bar__tick { position: absolute; top: 0; transform: translateX(-50%); text-align: center; pointer-events: auto; cursor: pointer; z-index: 1; }
.zoom-tick-dot.active { border-color: var(--rs-primary-hover); background: var(--rs-primary); transform: scale(1.2); } .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-tick-label { font-size: 9px; color: var(--rs-text-muted); margin-top: 3px; transition: color 0.15s; } .zoom-bar__tick-mark.active { opacity: 0.8; }
.zoom-tick-label.active { color: #818cf8; font-weight: 600; } .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 { 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: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); } .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; } .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-track { min-width: 150px; } .zoom-bar__track { min-width: 120px; }
.zoom-ctrl { gap: 4px; padding: 6px 8px; } .zoom-bar__label-end { font-size: 9px; }
.zoom-bar__tick-label { font-size: 8px; }
.coupling-btn { font-size: 10px; padding: 3px 8px; } .coupling-btn { font-size: 10px; padding: 3px 8px; }
.lunar-summary { gap: 8px; flex-wrap: wrap; } .lunar-summary { gap: 8px; flex-wrap: wrap; }
.phase-chips { gap: 4px; } .phase-chips { gap: 4px; }