diff --git a/dist/index.html b/dist/index.html
index 485fd63..cb12139 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -8208,6 +8208,42 @@ function AddLocationDialog($$anchor, $$props) {
}
delegate(["click", "input"]);
//#endregion
+//#region src/lib/router.js
+/**
+* Lightweight hash router for the app's tab views.
+*
+* Tabs are routable via the URL hash so a user can share/copy a URL for a
+* specific tab (e.g. #/radar) and use browser back/forward between tabs.
+* There is no nested route structure — just a single top-level view segment.
+*/
+var VALID_VIEWS = [
+ "current",
+ "hourly",
+ "daily",
+ "detail",
+ "radar"
+];
+var DEFAULT_VIEW = "current";
+/**
+* Parse a location hash into a view id, falling back to the default view.
+* Accepts '#/hourly', 'hourly', '#hourly', '', '#/unknown', etc.
+*
+* @param {string} [hash] - window.location.hash
+* @returns {string} one of VALID_VIEWS
+*/
+function parseHash(hash = "") {
+ const seg = String(hash).replace(/^#\/?/, "").split("/")[0];
+ return VALID_VIEWS.includes(seg) ? seg : DEFAULT_VIEW;
+}
+/**
+* Convert a view id into its URL hash representation.
+* @param {string} view - one of VALID_VIEWS
+* @returns {string} e.g. '#/radar'
+*/
+function viewToHash(view) {
+ return `#/${view}`;
+}
+//#endregion
//#region src/App.svelte
var root = /* @__PURE__ */ from_html(``);
var root_1 = /* @__PURE__ */ from_html(`📍 Current`);
@@ -8219,7 +8255,21 @@ var root_6 = /* @__PURE__ */ from_html(`
<
var root_7 = /* @__PURE__ */ from_html(`
`);
function App($$anchor, $$props) {
push($$props, true);
- let viewMode = /* @__PURE__ */ state("current");
+ let viewMode = /* @__PURE__ */ state(proxy(parseHash(typeof window !== "undefined" ? window.location.hash : "")));
+ /** Switch the active tab and reflect it in the URL hash. */
+ function setView(v) {
+ set(viewMode, v, true);
+ if (typeof window !== "undefined") window.location.hash = viewToHash(v);
+ }
+ user_effect(() => {
+ if (typeof window === "undefined") return;
+ const onHash = () => {
+ const v = parseHash(window.location.hash);
+ if (v !== get(viewMode)) set(viewMode, v, true);
+ };
+ window.addEventListener("hashchange", onHash);
+ return () => window.removeEventListener("hashchange", onHash);
+ });
let initDone = /* @__PURE__ */ state(false);
let geoError = /* @__PURE__ */ state("");
user_effect(() => {
@@ -8449,16 +8499,16 @@ function App($$anchor, $$props) {
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" });
});
- delegated("click", button_5, () => set(viewMode, "current"));
- delegated("click", button_6, () => set(viewMode, "hourly"));
- delegated("click", button_7, () => set(viewMode, "daily"));
- delegated("click", button_8, () => set(viewMode, "detail"));
- delegated("click", button_9, () => set(viewMode, "radar"));
- delegated("click", button_10, () => set(viewMode, "current"));
- delegated("click", button_11, () => set(viewMode, "hourly"));
- delegated("click", button_12, () => set(viewMode, "daily"));
- delegated("click", button_13, () => set(viewMode, "detail"));
- delegated("click", button_14, () => set(viewMode, "radar"));
+ delegated("click", button_5, () => setView("current"));
+ delegated("click", button_6, () => setView("hourly"));
+ delegated("click", button_7, () => setView("daily"));
+ delegated("click", button_8, () => setView("detail"));
+ delegated("click", button_9, () => setView("radar"));
+ delegated("click", button_10, () => setView("current"));
+ delegated("click", button_11, () => setView("hourly"));
+ delegated("click", button_12, () => setView("daily"));
+ delegated("click", button_13, () => setView("detail"));
+ delegated("click", button_14, () => setView("radar"));
append($$anchor, fragment);
};
if_block(node_8, ($$render) => {
diff --git a/src/App.svelte b/src/App.svelte
index e25e7b7..a7dbfb0 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -10,8 +10,32 @@
import NotificationBell from './components/NotificationBell.svelte'
import SettingsDialog from './components/SettingsDialog.svelte'
import AddLocationDialog from './components/AddLocationDialog.svelte'
+ import { parseHash, viewToHash } from './lib/router.js'
+
+ // Hash-routable tab: initialize from the URL hash so a shared link like
+ // #/radar opens straight to that tab.
+ let viewMode = $state(parseHash(typeof window !== 'undefined' ? window.location.hash : ''))
+
+ /** Switch the active tab and reflect it in the URL hash. */
+ function setView(v) {
+ viewMode = v
+ if (typeof window !== 'undefined') {
+ window.location.hash = viewToHash(v)
+ }
+ }
+
+ // Keep viewMode in sync with the hash — covers browser back/forward and a
+ // manually edited URL. Guarded so we never re-enter on the same value.
+ $effect(() => {
+ if (typeof window === 'undefined') return
+ const onHash = () => {
+ const v = parseHash(window.location.hash)
+ if (v !== viewMode) viewMode = v
+ }
+ window.addEventListener('hashchange', onHash)
+ return () => window.removeEventListener('hashchange', onHash)
+ })
- let viewMode = $state('current') // 'current' | 'hourly' | 'daily' | 'detail' | 'radar'
let initDone = $state(false)
let geoError = $state('')
@@ -186,37 +210,37 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/src/lib/router.js b/src/lib/router.js
new file mode 100644
index 0000000..4191ccd
--- /dev/null
+++ b/src/lib/router.js
@@ -0,0 +1,32 @@
+/**
+ * Lightweight hash router for the app's tab views.
+ *
+ * Tabs are routable via the URL hash so a user can share/copy a URL for a
+ * specific tab (e.g. #/radar) and use browser back/forward between tabs.
+ * There is no nested route structure — just a single top-level view segment.
+ */
+
+export const VALID_VIEWS = ['current', 'hourly', 'daily', 'detail', 'radar']
+export const DEFAULT_VIEW = 'current'
+
+/**
+ * Parse a location hash into a view id, falling back to the default view.
+ * Accepts '#/hourly', 'hourly', '#hourly', '', '#/unknown', etc.
+ *
+ * @param {string} [hash] - window.location.hash
+ * @returns {string} one of VALID_VIEWS
+ */
+export function parseHash(hash = '') {
+ const clean = String(hash).replace(/^#\/?/, '')
+ const seg = clean.split('/')[0]
+ return VALID_VIEWS.includes(seg) ? seg : DEFAULT_VIEW
+}
+
+/**
+ * Convert a view id into its URL hash representation.
+ * @param {string} view - one of VALID_VIEWS
+ * @returns {string} e.g. '#/radar'
+ */
+export function viewToHash(view) {
+ return `#/${view}`
+}
diff --git a/tests/lib/router.test.js b/tests/lib/router.test.js
new file mode 100644
index 0000000..3a6c2aa
--- /dev/null
+++ b/tests/lib/router.test.js
@@ -0,0 +1,34 @@
+import { describe, it, expect } from 'vitest'
+import { parseHash, viewToHash, VALID_VIEWS, DEFAULT_VIEW } from '../../src/lib/router.js'
+
+describe('parseHash', () => {
+ it('parses a standard hash route', () => {
+ expect(parseHash('#/hourly')).toBe('hourly')
+ expect(parseHash('#/radar')).toBe('radar')
+ })
+
+ it('handles a bare segment without a leading slash', () => {
+ expect(parseHash('daily')).toBe('daily')
+ expect(parseHash('#daily')).toBe('daily')
+ })
+
+ it('falls back to the default view for empty or unknown hashes', () => {
+ expect(parseHash('')).toBe(DEFAULT_VIEW)
+ expect(parseHash('#')).toBe(DEFAULT_VIEW)
+ expect(parseHash('#/nope')).toBe(DEFAULT_VIEW)
+ expect(parseHash('#/current')).toBe('current')
+ })
+
+ it('ignores a trailing path after the view segment', () => {
+ expect(parseHash('#/detail/sub')).toBe('detail')
+ })
+})
+
+describe('viewToHash', () => {
+ it('round-trips a valid view', () => {
+ for (const v of VALID_VIEWS) {
+ expect(viewToHash(v)).toBe(`#/${v}`)
+ expect(parseHash(viewToHash(v))).toBe(v)
+ }
+ })
+})