diff --git a/dist/index.html b/dist/index.html
index d3127b4..1d62f07 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -229,6 +229,8 @@ function svelte_boundary_reset_onerror() {
var HYDRATION_ERROR = {};
var UNINITIALIZED = Symbol("uninitialized");
var NAMESPACE_HTML = "http://www.w3.org/1999/xhtml";
+var NAMESPACE_SVG = "http://www.w3.org/2000/svg";
+var NAMESPACE_MATHML = "http://www.w3.org/1998/Math/MathML";
/**
* Reading a derived belonging to a now-destroyed effect may result in stale values
*/
@@ -3497,6 +3499,24 @@ function from_html(content, flags) {
};
}
/**
+* Don't mark this as side-effect-free, hydration needs to walk all nodes
+* @param {any} value
+*/
+function text(value = "") {
+ if (!hydrating) {
+ var t = create_text(value + "");
+ assign_nodes(t, t);
+ return t;
+ }
+ var node = hydrate_node;
+ if (node.nodeType !== 3) {
+ node.before(node = create_text());
+ set_hydrate_node(node);
+ } else merge_text_nodes(node);
+ assign_nodes(node, node);
+ return node;
+}
+/**
* @returns {TemplateNode | DocumentFragment}
*/
function comment() {
@@ -4234,6 +4254,66 @@ function link(state, prev, next) {
if (next === null) state.effect.last = prev;
else next.prev = prev;
}
+/**
+* @param {Element | Text | Comment} node
+* @param {() => string | TrustedHTML} get_value
+* @param {boolean} [is_controlled]
+* @param {boolean} [svg]
+* @param {boolean} [mathml]
+* @param {boolean} [skip_warning]
+* @returns {void}
+*/
+function html(node, get_value, is_controlled = false, svg = false, mathml = false, skip_warning = false) {
+ var anchor = node;
+ /** @type {string | TrustedHTML} */
+ var value = "";
+ if (is_controlled) {
+ var parent_node = node;
+ if (hydrating) anchor = set_hydrate_node(/* @__PURE__ */ get_first_child(parent_node));
+ }
+ template_effect(() => {
+ var effect = active_effect;
+ if (value === (value = get_value() ?? "")) {
+ if (hydrating) hydrate_next();
+ return;
+ }
+ if (is_controlled && !hydrating) {
+ effect.nodes = null;
+ parent_node.innerHTML = value;
+ if (value !== "") assign_nodes(/* @__PURE__ */ get_first_child(parent_node), parent_node.lastChild);
+ return;
+ }
+ if (effect.nodes !== null) {
+ remove_effect_dom(effect.nodes.start, effect.nodes.end);
+ effect.nodes = null;
+ }
+ if (value === "") return;
+ if (hydrating) {
+ hydrate_node.data;
+ /** @type {TemplateNode | null} */
+ var next = hydrate_next();
+ var last = next;
+ while (next !== null && (next.nodeType !== 8 || next.data !== "")) {
+ last = next;
+ next = /* @__PURE__ */ get_next_sibling(next);
+ }
+ if (next === null) {
+ hydration_mismatch();
+ throw HYDRATION_ERROR;
+ }
+ assign_nodes(hydrate_node, last);
+ anchor = set_hydrate_node(next);
+ return;
+ }
+ var wrapper = create_element(svg ? "svg" : mathml ? "math" : "template", svg ? NAMESPACE_SVG : mathml ? NAMESPACE_MATHML : void 0);
+ wrapper.innerHTML = value;
+ /** @type {DocumentFragment | Element} */
+ var node = svg || mathml ? wrapper : /** @type {HTMLTemplateElement} */ wrapper.content;
+ assign_nodes(/* @__PURE__ */ get_first_child(node), node.lastChild);
+ if (svg || mathml) while (/* @__PURE__ */ get_first_child(node)) anchor.before(/* @__PURE__ */ get_first_child(node));
+ else anchor.before(node);
+ });
+}
//#endregion
//#region node_modules/svelte/src/internal/client/dom/elements/actions.js
/** @import { ActionPayload } from '#client' */
@@ -4849,11 +4929,11 @@ function replaceTraps(callback) {
}
function wrapFunction(func) {
if (getCursorAdvanceMethods().includes(func)) return function(...args) {
- func.apply(unwrap(this), args);
+ func.apply(unwrap$1(this), args);
return wrap(this.request);
};
return function(...args) {
- return wrap(func.apply(unwrap(this), args));
+ return wrap(func.apply(unwrap$1(this), args));
};
}
function transformCachableValue(value) {
@@ -4915,7 +4995,7 @@ async function* iterate(...args) {
cursor = cursor;
const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
- reverseTransformCache.set(proxiedCursor, unwrap(cursor));
+ reverseTransformCache.set(proxiedCursor, unwrap$1(cursor));
while (cursor) {
yield proxiedCursor;
cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
@@ -4929,7 +5009,7 @@ function isIteratorProp(target, prop) {
IDBCursor
]) || prop === "iterate" && instanceOfAny(target, [IDBIndex, IDBObjectStore]);
}
-var instanceOfAny, idbProxyableTypes, cursorAdvanceMethods, transactionDoneMap, transformCache, reverseTransformCache, idbProxyTraps, unwrap, readMethods, writeMethods, cachedMethods, advanceMethodProps, methodMap, advanceResults, ittrProxiedCursorToOriginalProxy, cursorIteratorTraps;
+var instanceOfAny, idbProxyableTypes, cursorAdvanceMethods, transactionDoneMap, transformCache, reverseTransformCache, idbProxyTraps, unwrap$1, readMethods, writeMethods, cachedMethods, advanceMethodProps, methodMap, advanceResults, ittrProxiedCursorToOriginalProxy, cursorIteratorTraps;
var init_build = __esmMin((() => {
instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
transactionDoneMap = /* @__PURE__ */ new WeakMap();
@@ -4952,7 +5032,7 @@ var init_build = __esmMin((() => {
return prop in target;
}
};
- unwrap = (value) => reverseTransformCache.get(value);
+ unwrap$1 = (value) => reverseTransformCache.get(value);
readMethods = [
"get",
"getKey",
@@ -5202,7 +5282,7 @@ async function fetchForecast(lat, lon, units = "metric") {
temperature_unit: tempUnit,
wind_speed_unit: windUnit
});
- const url = `${FORECAST_BASE}?${params.toString()}`;
+ const url = `${FORECAST_BASE$1}?${params.toString()}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Weather API error: ${res.status} ${res.statusText}`);
return res.json();
@@ -5232,9 +5312,9 @@ async function searchLocations(query) {
timezone: r.timezone || "UTC"
}));
}
-var FORECAST_BASE, GEOCODING_BASE;
+var FORECAST_BASE$1, GEOCODING_BASE;
var init_weather = __esmMin((() => {
- FORECAST_BASE = "https://api.open-meteo.com/v1/forecast";
+ FORECAST_BASE$1 = "https://api.open-meteo.com/v1/forecast";
GEOCODING_BASE = "https://geocoding-api.open-meteo.com/v1/search";
}));
//#endregion
@@ -6283,10 +6363,10 @@ function autofocus(node) {
//#endregion
//#region src/components/LocationSidebar.svelte
init_db();
-var root$10 = /* @__PURE__ */ from_html(`โณ`);
-var root_1$9 = /* @__PURE__ */ from_html(``);
-var root_2$9 = /* @__PURE__ */ from_html(`
`);
-var root_3$7 = /* @__PURE__ */ from_html(``);
+var root$11 = /* @__PURE__ */ from_html(`โณ`);
+var root_1$10 = /* @__PURE__ */ from_html(``);
+var root_2$10 = /* @__PURE__ */ from_html(``);
+var root_3$8 = /* @__PURE__ */ from_html(``);
var root_4$5 = /* @__PURE__ */ from_html(``);
var root_5$5 = /* @__PURE__ */ from_html(`No saved locations.
Enable location access or add one below.
`);
var root_6$3 = /* @__PURE__ */ from_html(``);
@@ -6348,16 +6428,16 @@ function LocationSidebar($$anchor, $$props) {
remove_input_defaults(input);
var node = sibling(input, 2);
var consequent = ($$anchor) => {
- append($$anchor, root$10());
+ append($$anchor, root$11());
};
if_block(node, ($$render) => {
if (app$1.geocodingLoading) $$render(consequent);
});
var node_1 = sibling(node, 2);
var consequent_1 = ($$anchor) => {
- var div_2 = root_2$9();
+ var div_2 = root_2$10();
each(div_2, 21, () => app$1.geocodingResults, index, ($$anchor, result) => {
- var button = root_1$9();
+ var button = root_1$10();
var span_1 = child(button);
var text = child(span_1, true);
reset(span_1);
@@ -6385,7 +6465,7 @@ function LocationSidebar($$anchor, $$props) {
var fragment = comment();
var node_3 = first_child(fragment);
var consequent_2 = ($$anchor) => {
- var div_3 = root_3$7();
+ var div_3 = root_3$8();
var button_1 = child(div_3);
let classes;
var span_3 = sibling(child(button_1), 2);
@@ -6459,12 +6539,12 @@ function LocationSidebar($$anchor, $$props) {
delegate(["input", "click"]);
//#endregion
//#region src/components/SourceTooltip.svelte
-var root$9 = /* @__PURE__ */ from_html(`โ `);
+var root$10 = /* @__PURE__ */ from_html(`โ `);
function SourceTooltip($$anchor, $$props) {
/** Small info badge whose tooltip explains the value came from the
* non-selected source (e.g. Open-Meteo while NWS is selected). */
let label = prop($$props, "label", 3, "");
- var span = root$9();
+ var span = root$10();
var span_1 = sibling(child(span), 2);
var text = child(span_1, true);
reset(span_1);
@@ -6477,10 +6557,10 @@ function SourceTooltip($$anchor, $$props) {
}
//#endregion
//#region src/components/CurrentWeather.svelte
-var root$8 = /* @__PURE__ */ from_html(` `);
-var root_1$8 = /* @__PURE__ */ from_html(`
`);
-var root_2$8 = /* @__PURE__ */ from_html(` `);
-var root_3$6 = /* @__PURE__ */ from_html(`๐ช๏ธ Gusts
`);
+var root$9 = /* @__PURE__ */ from_html(` `);
+var root_1$9 = /* @__PURE__ */ from_html(`
`);
+var root_2$9 = /* @__PURE__ */ from_html(` `);
+var root_3$7 = /* @__PURE__ */ from_html(`๐ช๏ธ Gusts
`);
var root_4$4 = /* @__PURE__ */ from_html(``);
var root_5$4 = /* @__PURE__ */ from_html(`
๐ง Humidity
๐จ Wind
๐ก๏ธ Pressure
โ๏ธ UV Index
`);
function CurrentWeather($$anchor, $$props) {
@@ -6546,7 +6626,7 @@ function CurrentWeather($$anchor, $$props) {
var text_3 = child(div_4);
var node_1 = sibling(text_3);
var consequent = ($$anchor) => {
- var span_2 = root$8();
+ var span_2 = root$9();
var text_4 = child(span_2);
reset(span_2);
template_effect(($0) => set_text(text_4, `ยท Feels like ${$0 ?? ""}${get(unit) ?? ""}`), [() => Math.round(get(current).apparent_temperature)]);
@@ -6559,7 +6639,7 @@ function CurrentWeather($$anchor, $$props) {
reset(div_4);
var node_2 = sibling(div_4, 2);
var consequent_1 = ($$anchor) => {
- var div_5 = root_1$8();
+ var div_5 = root_1$9();
var span_3 = child(div_5);
var text_5 = child(span_3);
reset(span_3);
@@ -6615,7 +6695,7 @@ function CurrentWeather($$anchor, $$props) {
reset(span_9);
var node_5 = sibling(span_9, 2);
var consequent_4 = ($$anchor) => {
- var span_10 = root_2$8();
+ var span_10 = root_2$9();
var text_12 = child(span_10, true);
reset(span_10);
template_effect(() => set_text(text_12, get(current).uv_index <= 2 ? "Low" : get(current).uv_index <= 5 ? "Moderate" : get(current).uv_index <= 7 ? "High" : "Very High"));
@@ -6627,7 +6707,7 @@ function CurrentWeather($$anchor, $$props) {
reset(div_10);
var node_6 = sibling(div_10, 2);
var consequent_5 = ($$anchor) => {
- var div_11 = root_3$6();
+ var div_11 = root_3$7();
var span_11 = sibling(child(div_11), 4);
var text_13 = child(span_11);
reset(span_11);
@@ -6691,10 +6771,10 @@ function CurrentWeather($$anchor, $$props) {
}
//#endregion
//#region src/components/HourlyForecast.svelte
-var root$7 = /* @__PURE__ */ from_html(` `, 1);
-var root_1$7 = /* @__PURE__ */ from_html(` --`, 1);
-var root_2$7 = /* @__PURE__ */ from_html(`
`);
-var root_3$5 = /* @__PURE__ */ from_html(``);
+var root$8 = /* @__PURE__ */ from_html(` `, 1);
+var root_1$8 = /* @__PURE__ */ from_html(` --`, 1);
+var root_2$8 = /* @__PURE__ */ from_html(`
`);
+var root_3$6 = /* @__PURE__ */ from_html(``);
function HourlyForecast($$anchor, $$props) {
push($$props, true);
const data = /* @__PURE__ */ user_derived(() => app$1.forecastData);
@@ -6729,11 +6809,11 @@ function HourlyForecast($$anchor, $$props) {
var fragment = comment();
var node = first_child(fragment);
var consequent_1 = ($$anchor) => {
- var div = root_3$5();
+ var div = root_3$6();
var div_1 = sibling(child(div), 2);
each(div_1, 21, () => get(next24Hours), index, ($$anchor, hour, i) => {
const info = /* @__PURE__ */ user_derived(() => getWeatherInfo(get(hour).code));
- var div_2 = root_2$7();
+ var div_2 = root_2$8();
let classes;
var span = child(div_2);
var text = child(span, true);
@@ -6746,7 +6826,7 @@ function HourlyForecast($$anchor, $$props) {
reset(span_2);
var node_1 = sibling(span_2, 2);
var consequent = ($$anchor) => {
- var fragment_1 = root$7();
+ var fragment_1 = root$8();
var div_3 = first_child(fragment_1);
var div_4 = child(div_3);
reset(div_3);
@@ -6760,7 +6840,7 @@ function HourlyForecast($$anchor, $$props) {
append($$anchor, fragment_1);
};
var alternate = ($$anchor) => {
- var fragment_2 = root_1$7();
+ var fragment_2 = root_1$8();
next(2);
append($$anchor, fragment_2);
};
@@ -6798,10 +6878,10 @@ function HourlyForecast($$anchor, $$props) {
}
//#endregion
//#region src/components/DailyForecast.svelte
-var root$6 = /* @__PURE__ */ from_html(``);
-var root_1$6 = /* @__PURE__ */ from_html(` `);
-var root_2$6 = /* @__PURE__ */ from_html(``);
-var root_3$4 = /* @__PURE__ */ from_html(``);
+var root$7 = /* @__PURE__ */ from_html(``);
+var root_1$7 = /* @__PURE__ */ from_html(` `);
+var root_2$7 = /* @__PURE__ */ from_html(``);
+var root_3$5 = /* @__PURE__ */ from_html(``);
function DailyForecast($$anchor, $$props) {
push($$props, true);
const data = /* @__PURE__ */ user_derived(() => app$1.forecastData);
@@ -6825,7 +6905,7 @@ function DailyForecast($$anchor, $$props) {
var fragment = comment();
var node = first_child(fragment);
var consequent_3 = ($$anchor) => {
- var div = root_3$4();
+ var div = root_3$5();
var div_1 = sibling(child(div), 2);
each(div_1, 21, () => get(daily).time, index, ($$anchor, day, i) => {
var fragment_1 = comment();
@@ -6836,7 +6916,7 @@ function DailyForecast($$anchor, $$props) {
const tempMax = /* @__PURE__ */ user_derived(() => Math.round(get(daily).temperature_2m_max[i]));
const tempMin = /* @__PURE__ */ user_derived(() => Math.round(get(daily).temperature_2m_min[i]));
const wind = /* @__PURE__ */ user_derived(() => get(daily).wind_speed_10m_max?.[i]);
- var div_2 = root_2$6();
+ var div_2 = root_2$7();
set_class(div_2, 1, "day-card card-glass svelte-ss3yj8", null, {}, { today: i === 0 });
var div_3 = child(div_2);
var span = child(div_3);
@@ -6865,7 +6945,7 @@ function DailyForecast($$anchor, $$props) {
reset(div_6);
var node_2 = sibling(div_6, 2);
var consequent = ($$anchor) => {
- var div_7 = root$6();
+ var div_7 = root$7();
var div_8 = sibling(child(div_7), 2);
var div_9 = child(div_8);
reset(div_8);
@@ -6884,7 +6964,7 @@ function DailyForecast($$anchor, $$props) {
});
var node_3 = sibling(node_2, 2);
var consequent_1 = ($$anchor) => {
- var span_7 = root_1$6();
+ var span_7 = root_1$7();
var text_7 = child(span_7);
reset(span_7);
template_effect(($0) => set_text(text_7, `๐จ ${$0 ?? ""} ${get(windUnit) ?? ""}`), [() => Math.round(get(wind))]);
@@ -6921,10 +7001,275 @@ function DailyForecast($$anchor, $$props) {
pop();
}
//#endregion
+//#region src/lib/api/moon.js
+/**
+* Moon phase helpers.
+*
+* Moon phase data is fetched from the Open-Meteo forecast API (daily
+* `moon_phase`), which returns the moon's position in its synodic cycle as a
+* fraction: 0 = new moon, 0.25 = first quarter, 0.5 = full moon,
+* 0.75 = last quarter, approaching 1.0 = next new moon. We fetch it
+* independently of the weather source (NWS or Open-Meteo) since NWS does not
+* provide moon data and Open-Meteo covers the globe without a key.
+*
+* The pure functions here are unit-testable; the fetch is thin.
+*/
+var FORECAST_BASE = "https://api.open-meteo.com/v1/forecast";
+var MOON_NAMES = [
+ "New Moon",
+ "Waxing Crescent",
+ "First Quarter",
+ "Waxing Gibbous",
+ "Full Moon",
+ "Waning Gibbous",
+ "Last Quarter",
+ "Waning Crescent"
+];
+/**
+* Fetch daily moon_phase values for a location.
+* @param {number} lat
+* @param {number} lon
+* @returns {Promise>} oldest โ newest
+*/
+async function fetchMoonPhases(lat, lon) {
+ const url = `${FORECAST_BASE}?${new URLSearchParams({
+ latitude: lat.toString(),
+ longitude: lon.toString(),
+ daily: "moon_phase",
+ timezone: "auto",
+ forecast_days: String(9)
+ }).toString()}`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Moon API error: ${res.status} ${res.statusText}`);
+ const data = await res.json();
+ const times = data.daily?.time || [];
+ const phases = data.daily?.moon_phase || [];
+ return times.map((date, i) => ({
+ date,
+ phase: phases[i]
+ }));
+}
+/**
+* Name of the moon phase for a synodic-cycle position (0 โ next new moon).
+* @param {number} phase 0..1 fraction of the lunar month
+* @returns {string}
+*/
+function moonName(phase) {
+ return MOON_NAMES[Math.floor(phase % 1 * 8) % 8];
+}
+/**
+* Fraction of the lunar disc illuminated, 0 (new) โ 1 (full).
+* @param {number} phase 0..1 synodic position (0=new, 0.5=full)
+* @returns {number} 0..1
+*/
+function illumination(phase) {
+ return (1 - Math.cos(2 * Math.PI * (phase % 1))) / 2;
+}
+/**
+* Convert a synodic position to an unwrapped, strictly increasing value so
+* the cycle wrapping (new moon) doesn't confuse "which boundary is next".
+* @param {number} phase 0..1
+* @param {number} wraps how many full cycles precede it
+* @returns {number}
+*/
+function unwrap(phase, wraps) {
+ return phase + wraps;
+}
+/**
+* Given consecutive daily samples, find the next major phase boundary (one of
+* new / first quarter / full / last quarter) strictly after the first sample:
+* which boundary it is, an interpolated crossing date, and days until it.
+*
+* @param {Array<{ date: string, phase: number }>} days oldest โ newest
+* @returns {{ name: string, date: string, daysUntil: number } | null}
+* null when no boundary is reached within the samples.
+*/
+function nextMoonPhase(days) {
+ if (!days || days.length < 2) return null;
+ const unwrapped = [];
+ let wraps = 0;
+ for (let i = 0; i < days.length; i++) {
+ if (i > 0 && days[i].phase < days[i - 1].phase) wraps++;
+ unwrapped.push(unwrap(days[i].phase, wraps));
+ }
+ const p0 = unwrapped[0];
+ const target = (Math.floor(p0 / .25) + 1) * .25;
+ for (let i = 0; i < days.length; i++) {
+ if (unwrapped[i] < target) continue;
+ const prev = unwrapped[Math.max(i - 1, 0)];
+ const next = unwrapped[i];
+ const prevDate = days[Math.max(i - 1, 0)].date;
+ const nextDate = days[i].date;
+ const span = next - prev;
+ const date = interpolateDate(prevDate, nextDate, span === 0 ? 0 : (target - prev) / span);
+ const daysUntil = Math.ceil(diffDays(days[0].date, date));
+ return {
+ name: moonName(target % 1),
+ date,
+ daysUntil
+ };
+ }
+ return null;
+}
+/** Interpolate a YYYY-MM-DD date between two dates by ratio [0,1]. */
+function interpolateDate(from, to, ratio) {
+ const t0 = Date.parse(`${from}T12:00:00`);
+ const ms = t0 + (Date.parse(`${to}T12:00:00`) - t0) * Math.max(0, Math.min(1, ratio));
+ return new Date(ms).toISOString().slice(0, 10);
+}
+/** Whole days from date `a` to date `b` (b >= a), at day granularity. */
+function diffDays(a, b) {
+ const ta = Date.parse(`${a}T12:00:00`);
+ const tb = Date.parse(`${b}T12:00:00`);
+ return Math.round((tb - ta) / 864e5);
+}
+//#endregion
+//#region src/components/MoonPhaseCard.svelte
+var root$6 = /* @__PURE__ */ from_html(``);
+var root_1$6 = /* @__PURE__ */ from_html(``);
+var root_2$6 = /* @__PURE__ */ from_html(``);
+var root_3$4 = /* @__PURE__ */ from_html(``);
+function MoonPhaseCard($$anchor, $$props) {
+ push($$props, true);
+ let _clipId = `moonclip-${Math.random().toString(36).slice(2, 7)}`;
+ let status = /* @__PURE__ */ state("loading");
+ let today = /* @__PURE__ */ state(null);
+ let info = /* @__PURE__ */ state(null);
+ let phaseName = /* @__PURE__ */ state("");
+ let illum = /* @__PURE__ */ state(0);
+ /**
+ * SVG moon: a lit disc with the unlit side carved by an offset dark circle.
+ * `phase` is the synodic fraction (0=new โฆ 0.5=full โฆ 1=new). The two-circle
+ * intersection produces correct crescent / quarter / gibbous shapes.
+ */
+ function moonSVG(phase, size = 96) {
+ const p = phase % 1;
+ const r = size / 2;
+ const k = illumination(p);
+ const off = 2 * r * k;
+ const cx = (p < .5 ? -1 : 1) * off;
+ return ``;
+ }
+ function formatDate(iso) {
+ if (!iso) return "--";
+ return (/* @__PURE__ */ new Date(`${iso}T12:00:00`)).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric"
+ });
+ }
+ async function load() {
+ const loc = app$1.selectedLocation;
+ if (!loc) return;
+ set(status, "loading");
+ try {
+ const phases = await fetchMoonPhases(loc.lat, loc.lon);
+ if (!phases.length) throw new Error("no moon data");
+ set(today, phases[0], true);
+ set(info, nextMoonPhase(phases), true);
+ set(phaseName, moonName(phases[0].phase), true);
+ set(illum, illumination(phases[0].phase), true);
+ set(status, "ready");
+ } catch (e) {
+ console.warn("Moon data unavailable:", e.message);
+ set(status, "error");
+ }
+ }
+ user_effect(() => {
+ app$1.selectedLocation;
+ if (app$1.selectedLocation) load();
+ });
+ var fragment = comment();
+ var node = first_child(fragment);
+ var consequent = ($$anchor) => {
+ append($$anchor, root$6());
+ };
+ var consequent_4 = ($$anchor) => {
+ var div_1 = root_2$6();
+ var div_2 = sibling(child(div_1), 2);
+ var div_3 = child(div_2);
+ html(div_3, () => moonSVG(get(today).phase), true);
+ reset(div_3);
+ var div_4 = sibling(div_3, 2);
+ var div_5 = child(div_4);
+ var text$1 = child(div_5, true);
+ reset(div_5);
+ var div_6 = sibling(div_5, 2);
+ var text_1 = child(div_6);
+ reset(div_6);
+ reset(div_4);
+ reset(div_2);
+ var node_1 = sibling(div_2, 2);
+ var consequent_3 = ($$anchor) => {
+ var div_7 = root_1$6();
+ var div_8 = sibling(child(div_7), 2);
+ var span = child(div_8);
+ var strong = child(span);
+ var text_2 = child(strong, true);
+ reset(strong);
+ var node_2 = sibling(strong, 2);
+ var consequent_1 = ($$anchor) => {
+ append($$anchor, text("is today"));
+ };
+ var consequent_2 = ($$anchor) => {
+ append($$anchor, text("in 1 day"));
+ };
+ var alternate = ($$anchor) => {
+ var text_5 = text();
+ template_effect(() => set_text(text_5, `in ${get(info).daysUntil ?? ""} days`));
+ append($$anchor, text_5);
+ };
+ if_block(node_2, ($$render) => {
+ if (get(info).daysUntil === 0) $$render(consequent_1);
+ else if (get(info).daysUntil === 1) $$render(consequent_2, 1);
+ else $$render(alternate, -1);
+ });
+ reset(span);
+ var span_1 = sibling(span, 2);
+ var text_6 = child(span_1, true);
+ reset(span_1);
+ reset(div_8);
+ reset(div_7);
+ template_effect(($0) => {
+ set_text(text_2, get(info).name);
+ set_text(text_6, $0);
+ }, [() => formatDate(get(info).date)]);
+ append($$anchor, div_7);
+ };
+ if_block(node_1, ($$render) => {
+ if (get(info)) $$render(consequent_3);
+ });
+ reset(div_1);
+ template_effect(($0) => {
+ set_text(text$1, get(phaseName));
+ set_text(text_1, `${$0 ?? ""}% illuminated`);
+ }, [() => Math.round(get(illum) * 100)]);
+ append($$anchor, div_1);
+ };
+ var alternate_1 = ($$anchor) => {
+ append($$anchor, root_3$4());
+ };
+ if_block(node, ($$render) => {
+ if (get(status) === "loading") $$render(consequent);
+ else if (get(status) === "ready" && get(today)) $$render(consequent_4, 1);
+ else $$render(alternate_1, -1);
+ });
+ append($$anchor, fragment);
+ pop();
+}
+//#endregion
//#region src/components/WeatherDetail.svelte
var root$5 = /* @__PURE__ */ from_html(` `);
var root_1$5 = /* @__PURE__ */ from_html(`24h Average
`);
-var root_2$5 = /* @__PURE__ */ from_html(`Weather Details
Speed
Direction
Gusts
Condition
Humidity
Pressure
Feels Like
Is Day
UV Index
Sunrise
Sunset
Max UV Today
Today's High
Today's Low
Precip Chance
Data source
Local time
Data updated
Elevation
`);
+var root_2$5 = /* @__PURE__ */ from_html(`Weather Details
Speed
Direction
Gusts
Condition
Humidity
Pressure
Feels Like
Is Day
UV Index
Sunrise
Sunset
Max UV Today
Today's High
Today's Low
Precip Chance
Data source
Local time
Data updated
Elevation
`);
function WeatherDetail($$anchor, $$props) {
push($$props, true);
const data = /* @__PURE__ */ user_derived(() => app$1.forecastData);
@@ -7097,70 +7442,73 @@ function WeatherDetail($$anchor, $$props) {
reset(div_15);
reset(div_14);
var div_20 = sibling(div_14, 2);
- var div_21 = sibling(child(div_20), 2);
- var div_22 = child(div_21);
- var span_13 = sibling(child(div_22), 2);
+ MoonPhaseCard(child(div_20), {});
+ reset(div_20);
+ var div_21 = sibling(div_20, 2);
+ var div_22 = sibling(child(div_21), 2);
+ var div_23 = child(div_22);
+ var span_13 = sibling(child(div_23), 2);
var text_13 = child(span_13, true);
reset(span_13);
- reset(div_22);
- var div_23 = sibling(div_22, 2);
- var span_14 = sibling(child(div_23), 2);
+ reset(div_23);
+ var div_24 = sibling(div_23, 2);
+ var span_14 = sibling(child(div_24), 2);
var text_14 = child(span_14, true);
reset(span_14);
- reset(div_23);
- var node_5 = sibling(div_23, 2);
+ reset(div_24);
+ var node_6 = sibling(div_24, 2);
var consequent_5 = ($$anchor) => {
const temps = /* @__PURE__ */ user_derived(() => get(hourly).temperature_2m || []);
const validTemps = /* @__PURE__ */ user_derived(() => get(temps).filter((t) => t != null));
var fragment_4 = comment();
- var node_6 = first_child(fragment_4);
+ var node_7 = first_child(fragment_4);
var consequent_4 = ($$anchor) => {
- var div_24 = root_1$5();
- var span_15 = sibling(child(div_24), 2);
+ var div_25 = root_1$5();
+ var span_15 = sibling(child(div_25), 2);
var text_15 = child(span_15);
reset(span_15);
- reset(div_24);
+ reset(div_25);
template_effect(($0) => set_text(text_15, `${$0 ?? ""}${get(unit) ?? ""}`), [() => Math.round(get(validTemps).reduce((a, b) => a + b, 0) / get(validTemps).length)]);
- append($$anchor, div_24);
+ append($$anchor, div_25);
};
- if_block(node_6, ($$render) => {
+ if_block(node_7, ($$render) => {
if (get(validTemps).length > 0) $$render(consequent_4);
});
append($$anchor, fragment_4);
};
- if_block(node_5, ($$render) => {
+ if_block(node_6, ($$render) => {
if (get(hourly)) $$render(consequent_5);
});
- var div_25 = sibling(node_5, 2);
- var span_16 = sibling(child(div_25), 2);
+ var div_26 = sibling(node_6, 2);
+ var span_16 = sibling(child(div_26), 2);
var text_16 = child(span_16);
reset(span_16);
- reset(div_25);
+ reset(div_26);
+ reset(div_22);
reset(div_21);
- reset(div_20);
reset(div_1);
- var div_26 = sibling(div_1, 2);
- var div_27 = child(div_26);
- var span_17 = sibling(child(div_27), 2);
+ var div_27 = sibling(div_1, 2);
+ var div_28 = child(div_27);
+ var span_17 = sibling(child(div_28), 2);
var text_17 = child(span_17, true);
reset(span_17);
- reset(div_27);
- var div_28 = sibling(div_27, 2);
- var span_18 = sibling(child(div_28), 2);
- var text_18 = child(span_18, true);
- reset(span_18);
reset(div_28);
var div_29 = sibling(div_28, 2);
- var span_19 = sibling(child(div_29), 2);
- var text_19 = child(span_19, true);
- reset(span_19);
+ var span_18 = sibling(child(div_29), 2);
+ var text_18 = child(span_18, true);
+ reset(span_18);
reset(div_29);
var div_30 = sibling(div_29, 2);
- var span_20 = sibling(child(div_30), 2);
+ var span_19 = sibling(child(div_30), 2);
+ var text_19 = child(span_19, true);
+ reset(span_19);
+ reset(div_30);
+ var div_31 = sibling(div_30, 2);
+ var span_20 = sibling(child(div_31), 2);
var text_20 = child(span_20, true);
reset(span_20);
- reset(div_30);
- reset(div_26);
+ reset(div_31);
+ reset(div_27);
reset(div);
template_effect(($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) => {
set_text(text, `${$0 ?? ""} ${get(windUnit) ?? ""}`);
@@ -8792,7 +9140,7 @@ delegate([
]);
//#endregion
//#region src/main.js
-console.info({ commit_hash: "897f49ae2209cb2213ea97a4a082bb4bdfa2889a" });
+console.info({ commit_hash: "2eb34a709b0f3e5322756e91e014abdaf754a526" });
registerServiceWorker();
mount(App, { target: document.getElementById("app") });
//#endregion
@@ -9841,6 +10189,85 @@ input::placeholder {
}
}
+ .moon-card.svelte-d2g8r6 {
+ padding: 16px;
+ }
+
+ .detail-header.svelte-d2g8r6 {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 12px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ .detail-icon.svelte-d2g8r6 { font-size: 1.1rem; }
+ .detail-label.svelte-d2g8r6 {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--color-text-secondary);
+ }
+
+ .moon-body.svelte-d2g8r6 {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ margin-bottom: 10px;
+ }
+
+ .moon-visual.svelte-d2g8r6 {
+ flex-shrink: 0;
+ filter: drop-shadow(0 0 6px rgba(232, 230, 223, 0.25));
+ }
+
+ .moon-info.svelte-d2g8r6 {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ }
+
+ .moon-title.svelte-d2g8r6 {
+ font-size: 1.05rem;
+ font-weight: 700;
+ color: var(--color-text);
+ }
+
+ .moon-sub.svelte-d2g8r6 {
+ font-size: 0.78rem;
+ color: var(--color-text-muted);
+ }
+
+ .moon-next.svelte-d2g8r6 {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding-top: 6px;
+ border-top: 1px solid var(--color-border);
+ }
+
+ .moon-next-text.svelte-d2g8r6 {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .moon-next-line.svelte-d2g8r6 {
+ font-size: 0.95rem;
+ color: var(--color-text);
+ }
+
+ .moon-next-date.svelte-d2g8r6 {
+ font-size: 0.78rem;
+ color: var(--color-text-muted);
+ }
+
+ .moon-loading.svelte-d2g8r6 {
+ font-size: 0.82rem;
+ color: var(--color-text-muted);
+ padding: 6px 0;
+ }
+
.detail-section.svelte-8lxt71 {
margin-bottom: 4px;
}
diff --git a/src/components/MoonPhaseCard.svelte b/src/components/MoonPhaseCard.svelte
new file mode 100644
index 0000000..001ddab
--- /dev/null
+++ b/src/components/MoonPhaseCard.svelte
@@ -0,0 +1,191 @@
+
+
+{#if status === 'loading'}
+
+
+
Loading moon phaseโฆ
+
+{:else if status === 'ready' && today}
+
+
+
+
+ {@html moonSVG(today.phase)}
+
+
+
{phaseName}
+
{Math.round(illum * 100)}% illuminated
+
+
+ {#if info}
+
+
๐
+
+
+ {info.name}
+ {#if info.daysUntil === 0}is today{:else if info.daysUntil === 1}in 1 day{:else}in {info.daysUntil} days{/if}
+
+ {formatDate(info.date)}
+
+
+ {/if}
+
+{:else}
+
+
+
Moon data unavailable
+
+{/if}
+
+
diff --git a/src/components/WeatherDetail.svelte b/src/components/WeatherDetail.svelte
index 96932de..b9416d1 100644
--- a/src/components/WeatherDetail.svelte
+++ b/src/components/WeatherDetail.svelte
@@ -2,6 +2,7 @@
import { app } from '../lib/stores/app.svelte.js'
import { getWeatherInfo } from '../lib/api/weather-codes.js'
import SourceTooltip from './SourceTooltip.svelte'
+ import MoonPhaseCard from './MoonPhaseCard.svelte'
const data = $derived(app.forecastData)
const current = $derived(data?.current)
@@ -143,6 +144,11 @@
+
+
+
+
+