From f5488eaa983eaef1a39ca585192aa9b7b450a2c4 Mon Sep 17 00:00:00 2001 From: hermes-explorigin Date: Sun, 30 Aug 2026 19:22:35 +0000 Subject: [PATCH] Live swipe feedback: content tracks finger with destination peek Replace the instant-jump swipe with a live drag on touch: the current tab content translates with the finger in real time (translateX driven by touchmove), a peek chip at the edge shows the destination tab, and on release the sheet animates to the next tab or snaps back. Horizontal vs vertical travel is decided after 12px so normal page scrolling is untouched; touch-action:pan-y keeps horizontal gestures JS-owned. Radar still pans its map instead of switching tabs. --- dist/index.html | 251 ++++++++++++++++++++++++++++++++++++++---------- src/App.svelte | 230 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 388 insertions(+), 93 deletions(-) diff --git a/dist/index.html b/dist/index.html index df8b7e2..ad7d694 100644 --- a/dist/index.html +++ b/dist/index.html @@ -7188,7 +7188,7 @@ var root_4$3 = /* @__PURE__ */ from_html(``); var root_6$2 = /* @__PURE__ */ from_html(`
`, 1); var root_7$1 = /* @__PURE__ */ from_html(`
Loading radar...
`); -var root_8 = /* @__PURE__ */ from_html(`
`); +var root_8$1 = /* @__PURE__ */ from_html(`
`); var root_9 = /* @__PURE__ */ from_html(`
⏳ Loading lightning...
`); var root_10 = /* @__PURE__ */ from_html(`
`); var root_11 = /* @__PURE__ */ from_html(`
⚡ No recent strikes nearby
`); @@ -7536,7 +7536,7 @@ function PrecipitationRadar($$anchor, $$props) { }); var node_7 = sibling(node_6, 2); var consequent_6 = ($$anchor) => { - var div_8 = root_8(); + var div_8 = root_8$1(); var span_2 = child(div_8); var text_1 = child(span_2); reset(span_2); @@ -8250,9 +8250,10 @@ var root_1 = /* @__PURE__ */ from_html(` `); var root_3 = /* @__PURE__ */ from_html(`
🌍

Welcome to WeatherLens

`); var root_4 = /* @__PURE__ */ from_html(`

Loading weather data...

`); -var root_5 = /* @__PURE__ */ from_html(`
`); -var root_6 = /* @__PURE__ */ from_html(`
`, 1); -var root_7 = /* @__PURE__ */ from_html(`

`); +var root_5 = /* @__PURE__ */ from_html(``); +var root_6 = /* @__PURE__ */ from_html(`
`); +var root_7 = /* @__PURE__ */ from_html(`
`, 1); +var root_8 = /* @__PURE__ */ from_html(`

`); function App($$anchor, $$props) { push($$props, true); let viewMode = /* @__PURE__ */ state(proxy(parseHash(typeof window !== "undefined" ? window.location.hash : ""))); @@ -8270,26 +8271,94 @@ function App($$anchor, $$props) { window.addEventListener("hashchange", onHash); return () => window.removeEventListener("hashchange", onHash); }); - const SWIPE_THRESHOLD = 60; - let touchAnchor = /* @__PURE__ */ state(null); + const SWIPE_THRESHOLD = 70; + const AXIS_THRESHOLD = 12; + const SWIPE_TIMEOUT = 160; + const TAB_LABELS = { + current: "Now", + hourly: "Hourly", + daily: "Daily", + detail: "More", + radar: "Radar" + }; + let contentEl = /* @__PURE__ */ state(null); + let swiping = /* @__PURE__ */ state(false); + let swipeAnchor = /* @__PURE__ */ state(null); + let swipeDX = /* @__PURE__ */ state(0); + let swipeLock = /* @__PURE__ */ state(null); + let settling = /* @__PURE__ */ state(false); + const clampIdx = (i) => Math.min(Math.max(i, 0), VALID_VIEWS.length - 1); + const peekView = /* @__PURE__ */ user_derived(() => get(swiping) && get(swipeLock) === "x" && VALID_VIEWS.includes(get(viewMode)) ? VALID_VIEWS[clampIdx(VALID_VIEWS.indexOf(get(viewMode)) + (get(swipeDX) < 0 ? 1 : -1))] : null); function onSwipeStart(e) { if (get(viewMode) === "radar" || !e.touches || !e.touches[0]) return; - set(touchAnchor, { - x: e.touches[0].clientX, - y: e.touches[0].clientY + const t = e.touches[0]; + set(swipeAnchor, { + x: t.clientX, + y: t.clientY }, true); + set(swipeDX, 0); + set(swipeLock, null); + set(settling, false); + set(swiping, true); } - function onSwipeEnd(e) { - if (get(viewMode) === "radar" || !get(touchAnchor) || !e.changedTouches || !e.changedTouches[0]) return; - const t = e.changedTouches[0]; - const dx = t.clientX - get(touchAnchor).x; - const dy = t.clientY - get(touchAnchor).y; - set(touchAnchor, null); - if (Math.abs(dx) < SWIPE_THRESHOLD || Math.abs(dx) < Math.abs(dy)) return; - const idx = VALID_VIEWS.indexOf(get(viewMode)); - const next = VALID_VIEWS[Math.min(Math.max(idx + (dx < 0 ? 1 : -1), 0), VALID_VIEWS.length - 1)]; - if (next !== get(viewMode)) setView(next); + function onSwipeMove(e) { + if (!get(swiping) || !get(swipeAnchor)) return; + const t = e.touches[0]; + const dx = t.clientX - get(swipeAnchor).x; + const dy = t.clientY - get(swipeAnchor).y; + if (!get(swipeLock)) { + if (Math.abs(dx) > AXIS_THRESHOLD && Math.abs(dx) > Math.abs(dy)) set(swipeLock, "x"); + else if (Math.abs(dy) > AXIS_THRESHOLD) set(swipeLock, "y"); + } + if (get(swipeLock) === "x") { + e.preventDefault(); + set(swipeDX, dx); + } else if (get(swipeLock) === "y") set(swipeDX, 0); } + function finishSwipe(e) { + if (!get(swiping)) return; + const t = e.changedTouches && e.changedTouches[0]; + const dx = get(swipeLock) === "x" ? t ? t.clientX - get(swipeAnchor).x : get(swipeDX) : get(swipeDX); + const dy = t ? t.clientY - get(swipeAnchor).y : 0; + if (get(swipeLock) === "x" && Math.abs(dx) >= SWIPE_THRESHOLD && Math.abs(dx) >= Math.abs(dy) && get(viewMode) !== "radar") { + const idx = VALID_VIEWS.indexOf(get(viewMode)); + const step = dx < 0 ? 1 : -1; + const next = VALID_VIEWS[clampIdx(idx + step)]; + if (next !== get(viewMode)) { + set(settling, true); + set(swipeDX, (get(contentEl) ? get(contentEl).clientWidth : 420) * step * -1); + setTimeout(() => { + setView(next); + set(swiping, false); + set(swipeDX, 0); + set(swipeLock, null); + set(swipeAnchor, null); + set(settling, false); + }, SWIPE_TIMEOUT); + return; + } + } + set(settling, true); + set(swipeDX, 0); + setTimeout(() => { + set(swiping, false); + set(swipeLock, null); + set(swipeAnchor, null); + set(settling, false); + }, SWIPE_TIMEOUT); + } + function cancelSwipe(e) { + if (!get(swiping)) return; + set(settling, true); + set(swipeDX, 0); + setTimeout(() => { + set(swiping, false); + set(swipeLock, null); + set(swipeAnchor, null); + set(settling, false); + }, SWIPE_TIMEOUT); + } + const slideStyle = /* @__PURE__ */ user_derived(() => get(swipeDX) ? `transform: translateX(${get(swipeDX)}px)` : ""); let initDone = /* @__PURE__ */ state(false); let geoError = /* @__PURE__ */ state(""); user_effect(() => { @@ -8361,7 +8430,7 @@ function App($$anchor, $$props) { const newest = updated[updated.length - 1]; if (newest) app$1.selectLocation(newest.id); } - var div = root_7(); + var div = root_8(); var div_1 = child(div); var button = child(div_1); var text = child(button, true); @@ -8441,8 +8510,8 @@ function App($$anchor, $$props) { if (app$1.loading && !app$1.forecastData) $$render(consequent_4); }); var node_8 = sibling(node_7, 2); - var consequent_10 = ($$anchor) => { - var fragment = root_6(); + var consequent_11 = ($$anchor) => { + var fragment = root_7(); var div_8 = first_child(fragment); var button_5 = child(div_8); let classes_1; @@ -8468,45 +8537,68 @@ function App($$anchor, $$props) { let classes_10; reset(div_9); var div_10 = sibling(div_9, 2); - var node_9 = child(div_10); + let classes_11; + var div_11 = child(div_10); + let classes_12; + var node_9 = child(div_11); var consequent_5 = ($$anchor) => { - var div_11 = root_5(); - CurrentWeather(child(div_11), {}); - reset(div_11); - append($$anchor, div_11); + var span_3 = root_5(); + let classes_13; + var text_5 = child(span_3, true); + reset(span_3); + template_effect(() => { + classes_13 = set_class(span_3, 1, "swipe-peek svelte-1n46o8q", null, classes_13, { + "from-left": get(swipeDX) >= 0, + "from-right": get(swipeDX) < 0 + }); + set_text(text_5, TAB_LABELS[get(peekView)]); + }); + append($$anchor, span_3); }; + if_block(node_9, ($$render) => { + if (get(peekView) && TAB_LABELS[get(peekView)]) $$render(consequent_5); + }); + var node_10 = sibling(node_9, 2); var consequent_6 = ($$anchor) => { - var div_12 = root_5(); - HourlyForecast(child(div_12), {}); + var div_12 = root_6(); + CurrentWeather(child(div_12), {}); reset(div_12); append($$anchor, div_12); }; var consequent_7 = ($$anchor) => { - var div_13 = root_5(); - DailyForecast(child(div_13), {}); + var div_13 = root_6(); + HourlyForecast(child(div_13), {}); reset(div_13); append($$anchor, div_13); }; var consequent_8 = ($$anchor) => { - var div_14 = root_5(); - WeatherDetail(child(div_14), {}); + var div_14 = root_6(); + DailyForecast(child(div_14), {}); reset(div_14); append($$anchor, div_14); }; var consequent_9 = ($$anchor) => { - var div_15 = root_5(); - PrecipitationRadar(child(div_15), {}); + var div_15 = root_6(); + WeatherDetail(child(div_15), {}); reset(div_15); append($$anchor, div_15); }; - if_block(node_9, ($$render) => { - if (get(viewMode) === "current") $$render(consequent_5); - else if (get(viewMode) === "hourly") $$render(consequent_6, 1); - else if (get(viewMode) === "daily") $$render(consequent_7, 2); - else if (get(viewMode) === "detail") $$render(consequent_8, 3); - else if (get(viewMode) === "radar") $$render(consequent_9, 4); + var consequent_10 = ($$anchor) => { + var div_16 = root_6(); + PrecipitationRadar(child(div_16), {}); + reset(div_16); + append($$anchor, div_16); + }; + if_block(node_10, ($$render) => { + if (get(viewMode) === "current") $$render(consequent_6); + else if (get(viewMode) === "hourly") $$render(consequent_7, 1); + else if (get(viewMode) === "daily") $$render(consequent_8, 2); + else if (get(viewMode) === "detail") $$render(consequent_9, 3); + else if (get(viewMode) === "radar") $$render(consequent_10, 4); }); + reset(div_11); reset(div_10); + bind_this(div_10, ($$value) => set(contentEl, $$value), () => get(contentEl)); template_effect(() => { classes_1 = set_class(button_5, 1, "nav-btn svelte-1n46o8q", null, classes_1, { active: get(viewMode) === "current" }); classes_2 = set_class(button_6, 1, "nav-btn svelte-1n46o8q", null, classes_2, { active: get(viewMode) === "hourly" }); @@ -8518,6 +8610,9 @@ function App($$anchor, $$props) { classes_8 = set_class(button_12, 1, "tab-btn svelte-1n46o8q", null, classes_8, { active: get(viewMode) === "daily" }); classes_9 = set_class(button_13, 1, "tab-btn svelte-1n46o8q", null, classes_9, { active: get(viewMode) === "detail" }); classes_10 = set_class(button_14, 1, "tab-btn svelte-1n46o8q", null, classes_10, { active: get(viewMode) === "radar" }); + classes_11 = set_class(div_10, 1, "content-area svelte-1n46o8q", null, classes_11, { swiping: get(swiping) }); + classes_12 = set_class(div_11, 1, "swipe-layer svelte-1n46o8q", null, classes_12, { settle: get(settling) }); + set_style(div_11, get(slideStyle)); }); delegated("click", button_5, () => setView("current")); delegated("click", button_6, () => setView("hourly")); @@ -8530,22 +8625,24 @@ function App($$anchor, $$props) { delegated("click", button_13, () => setView("detail")); delegated("click", button_14, () => setView("radar")); delegated("touchstart", div_10, onSwipeStart, void 0, true); - delegated("touchend", div_10, onSwipeEnd); + delegated("touchmove", div_10, onSwipeMove, void 0, true); + delegated("touchend", div_10, finishSwipe); + event("touchcancel", div_10, cancelSwipe); append($$anchor, fragment); }; if_block(node_8, ($$render) => { - if (app$1.forecastData && app$1.selectedLocation) $$render(consequent_10); + if (app$1.forecastData && app$1.selectedLocation) $$render(consequent_11); }); reset(main); - var node_15 = sibling(main, 2); - var consequent_11 = ($$anchor) => { + var node_16 = sibling(main, 2); + var consequent_12 = ($$anchor) => { SettingsDialog($$anchor, { onClose: () => app$1.showSettings = false }); }; - if_block(node_15, ($$render) => { - if (app$1.showSettings) $$render(consequent_11); + if_block(node_16, ($$render) => { + if (app$1.showSettings) $$render(consequent_12); }); - var node_16 = sibling(node_15, 2); - var consequent_12 = ($$anchor) => { + var node_17 = sibling(node_16, 2); + var consequent_13 = ($$anchor) => { AddLocationDialog($$anchor, { onClose: () => { app$1.showAddLocation = false; @@ -8554,8 +8651,8 @@ function App($$anchor, $$props) { onAdd: handleAddLocation }); }; - if_block(node_16, ($$render) => { - if (app$1.showAddLocation) $$render(consequent_12); + if_block(node_17, ($$render) => { + if (app$1.showAddLocation) $$render(consequent_13); }); reset(div); template_effect(() => { @@ -8572,11 +8669,12 @@ function App($$anchor, $$props) { delegate([ "click", "touchstart", + "touchmove", "touchend" ]); //#endregion //#region src/main.js -console.info({ commit_hash: "93e5660a5c76fd41088193f5ca3e3a35cd7478b1" }); +console.info({ commit_hash: "09cea275b7ac2fd46d2184d84524ee1c3ddcbe60" }); mount(App, { target: document.getElementById("app") }); //#endregion