- ${levels.map(l => `
`).join("")}
+ return `
+
+
Day
+
+
+
+ ${levels.map(l => {
+ const tickPct = ((l.idx - 2) / 5) * 100;
+ return `
`;
+ }).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; }