@sjvair/monitor-map 3.2.2 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,10 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
- import { onDestroy } from "svelte";
4
- import LoadScreen, { disable as disableLoadScreen } from "./LoadScreen.svelte";
5
- import Map from "./map/Map.svelte";
6
- import Menu from "./map/Menu.svelte";
3
+ import { onDestroy, untrack } from "svelte";
4
+ import MapShell from "./map/MapShell.svelte";
7
5
  import MonitorsDisplayOptions from "./monitors/components/MonitorsDisplayOptions.svelte";
8
6
  import MapLayersDisplayOptions from "./components/MapLayersDisplayOptions.svelte";
9
7
  import MapStyleDisplayOptions from "./map/MapStyleDisplayOptions.svelte";
10
- import { mapManager } from "./map/map.svelte";
11
8
  import { monitorsManager } from "./monitors/monitors.svelte";
12
9
  import { monitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
13
10
  import { windMapIntegration } from "./wind/wind.svelte";
@@ -33,8 +30,6 @@
33
30
 
34
31
  const { route, navigate, basePath } = useMonitorMapRouter();
35
32
 
36
- const TRANSITION_MS = 300;
37
-
38
33
  const integrations: Array<SomeMapIntegration> = [
39
34
  baseLayerSeperator,
40
35
  collocationSitesMapIntegration,
@@ -59,10 +54,15 @@
59
54
  // applies a later "?pollutant=" change (e.g. navigating in from elsewhere) to an already-running manager.
60
55
  $effect(() => {
61
56
  const urlPollutant = route.search.pollutant;
57
+ // Read monitorsManager.pollutant via untrack: this effect must only react to the URL
58
+ // changing (e.g. navigating in from elsewhere), not to monitorsManager.pollutant itself.
59
+ // Tracking it here would make a UI-driven pollutant change (e.g. the display-options
60
+ // toggle) re-trigger this effect before the manager->URL effect below can sync the URL,
61
+ // so this effect would see the still-stale URL and immediately revert the user's change.
62
62
  if (
63
63
  monitorsManager.initialized &&
64
64
  (urlPollutant === "pm25" || urlPollutant === "o3") &&
65
- monitorsManager.pollutant !== urlPollutant
65
+ untrack(() => monitorsManager.pollutant) !== urlPollutant
66
66
  ) {
67
67
  monitorsManager.pollutant = urlPollutant;
68
68
  }
@@ -76,22 +76,6 @@
76
76
  }
77
77
  });
78
78
 
79
- $effect(() => {
80
- if (mapManager.map && monitorsManager.initialized) {
81
- mapManager.map.once("idle", () => disableLoadScreen());
82
- }
83
- });
84
-
85
- $effect(() => {
86
- if (panelOpen) {
87
- // Container width snapped to open; resize after layout settles
88
- requestAnimationFrame(() => mapManager.map?.resize());
89
- } else {
90
- // Container width snaps back after the transform transition ends
91
- setTimeout(() => mapManager.map?.resize(), TRANSITION_MS);
92
- }
93
- });
94
-
95
79
  // Clear selected icon scale when the detail panel closes
96
80
  $effect(() => {
97
81
  if (panelOpen) return;
@@ -101,75 +85,28 @@
101
85
  onDestroy(() => {
102
86
  monitorsManager.autoUpdate.stop();
103
87
  });
104
-
105
- /**
106
- * HACK: Fix for escaping sv-router and allowing navigation to other pages,
107
- * as well as navigating back
108
- */
109
- const rootPath = basePath || "/";
110
- const knownRoutes = [`${basePath}/monitor/`];
111
-
112
- window.addEventListener("pageshow", (e) => {
113
- if (e.persisted) window.location.reload();
114
- });
115
-
116
- document.addEventListener(
117
- "click",
118
- (e) => {
119
- const anchor = e
120
- .composedPath()
121
- .find((el) => el instanceof HTMLAnchorElement) as HTMLAnchorElement;
122
- if (!anchor) return;
123
- const { pathname } = new URL(anchor.href);
124
- const isKnown = pathname === rootPath || knownRoutes.some((r) => pathname.startsWith(r));
125
- if (!isKnown) {
126
- e.stopImmediatePropagation();
127
- window.location.href = anchor.href;
128
- }
129
- },
130
- { capture: true }
131
- );
132
88
  </script>
133
89
 
134
- <!--
135
- <div class="shell" class:panel-open={panelOpen}>
136
- -->
137
- <div class="relative flex h-full w-full flex-col md:flex-row">
138
- <LoadScreen />
139
- <div class="relative flex-1 overflow-hidden">
140
- <Map {integrations} />
90
+ <MapShell
91
+ {integrations}
92
+ ready={monitorsManager.initialized}
93
+ {panelOpen}
94
+ knownRoutes={[`${basePath}/monitor/`]}
95
+ {basePath}
96
+ >
97
+ {#snippet menu()}
98
+ <MonitorsDisplayOptions />
99
+ <EvStationsDisplayOptions />
100
+ <MapLayersDisplayOptions />
101
+ <MapStyleDisplayOptions />
102
+ {/snippet}
103
+ {#snippet overlays()}
141
104
  <div class="pointer-events-none absolute bottom-0 left-0 z-10">
142
105
  <MapLegend />
143
106
  </div>
144
- <div class="absolute top-4 left-4 z-10">
145
- <div class="absolute z-10">
146
- <Menu>
147
- <MonitorsDisplayOptions />
148
- <EvStationsDisplayOptions />
149
- <MapLayersDisplayOptions />
150
- <MapStyleDisplayOptions />
151
- </Menu>
152
- </div>
153
- <div class="absolute left-16">
154
- <Search />
155
- </div>
156
- </div>
157
- </div>
158
- <div
159
- class={[
160
- "panel-containr w-full shrink-0 overflow-hidden",
161
- panelOpen ? "h-1/2 md:h-full md:w-1/3" : "md:w-0"
162
- ]}
163
- >
164
- <div
165
- class={[
166
- "panel-contet h-full duration-300 ease-in-out",
167
- panelOpen
168
- ? "translate-y-0 md:translate-x-0"
169
- : "translate-y-full md:translate-x-full md:translate-y-0"
170
- ]}
171
- >
172
- {@render children()}
107
+ <div class="absolute top-4 left-20 z-10">
108
+ <Search />
173
109
  </div>
174
- </div>
175
- </div>
110
+ {/snippet}
111
+ {@render children()}
112
+ </MapShell>
@@ -44,6 +44,7 @@ export { baseLayerSeperator } from "./map/integrations/base-layer-seperator";
44
44
  export { hmsFireMapIntegration } from "./hms/hms-fire-map-integration.svelte";
45
45
  export { hmsSmokeMapIntegration } from "./hms/hms-smoke-map-integration.svelte";
46
46
  export { default as Map } from "./map/Map.svelte";
47
+ export { default as MapShell, type MapShellProps } from "./map/MapShell.svelte";
47
48
  export { default as Menu } from "./map/Menu.svelte";
48
49
  export { default as LoadScreen, loadScreenState, enable as enableLoadScreen, disable as disableLoadScreen } from "./LoadScreen.svelte";
49
50
  export type { LoadScreenState } from "./LoadScreen.svelte";
package/dist/lib/index.js CHANGED
@@ -42,6 +42,7 @@ export { hmsFireMapIntegration } from "./hms/hms-fire-map-integration.svelte";
42
42
  export { hmsSmokeMapIntegration } from "./hms/hms-smoke-map-integration.svelte";
43
43
  // Components
44
44
  export { default as Map } from "./map/Map.svelte";
45
+ export { default as MapShell } from "./map/MapShell.svelte";
45
46
  export { default as Menu } from "./map/Menu.svelte";
46
47
  export { default as LoadScreen, loadScreenState, enable as enableLoadScreen, disable as disableLoadScreen } from "./LoadScreen.svelte";
47
48
  export { default as MonitorsDisplayOptions } from "./monitors/components/MonitorsDisplayOptions.svelte";
@@ -0,0 +1,150 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { SomeMapIntegration } from "./integrations/types";
4
+
5
+ export interface MapShellProps {
6
+ /** Map integrations to register, e.g. [monitorsMapIntegration] */
7
+ integrations: Array<SomeMapIntegration>;
8
+ /**
9
+ * True once the caller's own data managers have finished their initial
10
+ * load. The shell hides the load screen once the map is idle AND this
11
+ * is true.
12
+ */
13
+ ready: boolean;
14
+ /** True when the routed detail panel (`children`) should be visible */
15
+ panelOpen: boolean;
16
+ /**
17
+ * Path prefixes that are part of this map's own routed panel (e.g.
18
+ * [`${basePath}/monitor/`]) — internal navigation to these should not
19
+ * trigger the escape-hatch full-page navigation below.
20
+ */
21
+ knownRoutes?: Array<string>;
22
+ /** Rendered inside the display-options menu toggle */
23
+ menu?: Snippet;
24
+ /** Freeform absolutely-positioned content over the map (legend, search, etc.) */
25
+ overlays?: Snippet;
26
+ /** The routed detail panel content */
27
+ children?: Snippet;
28
+ /**
29
+ * Base path to use for the sv-router escape-hatch's "known" root route.
30
+ * If provided, used directly instead of reading `basePath` from
31
+ * `useMonitorMapRouter()`. Only relevant when `routerEscapeHatch` is
32
+ * enabled.
33
+ */
34
+ basePath?: string;
35
+ /**
36
+ * Whether to install the sv-router escape-hatch (`pageshow`/`click`
37
+ * listeners that force a full-page navigation for links outside this
38
+ * map's own routes). Defaults to `true`, preserving the existing
39
+ * full-page-map behavior. When `true` and no `basePath` prop is given,
40
+ * `MapShell` falls back to reading `basePath` from
41
+ * `useMonitorMapRouter()`. Set to `false` to embed `MapShell` inside a
42
+ * host app with its own routing — this also skips calling
43
+ * `useMonitorMapRouter()` entirely, so no `provideMonitorMapRouter(...)`
44
+ * ancestor is required.
45
+ */
46
+ routerEscapeHatch?: boolean;
47
+ }
48
+ </script>
49
+
50
+ <script lang="ts">
51
+ import LoadScreen, { disable as disableLoadScreen } from "../LoadScreen.svelte";
52
+ import Map from "./Map.svelte";
53
+ import Menu from "./Menu.svelte";
54
+ import { mapManager } from "./map.svelte";
55
+ import { useMonitorMapRouter } from "../router-context";
56
+
57
+ let {
58
+ integrations,
59
+ ready,
60
+ panelOpen,
61
+ knownRoutes = [],
62
+ menu,
63
+ overlays,
64
+ children,
65
+ basePath: providedBasePath,
66
+ routerEscapeHatch = true
67
+ }: MapShellProps = $props();
68
+
69
+ const basePath = providedBasePath ?? (routerEscapeHatch ? useMonitorMapRouter().basePath : "");
70
+ const TRANSITION_MS = 300;
71
+
72
+ $effect(() => {
73
+ // mapManager.map is only ever assigned once its own initial "load" has
74
+ // already fired (see initializeMap in map.svelte.ts), so by the time this
75
+ // effect sees a non-null map, it's already safe to reveal — waiting for a
76
+ // further "idle" event here is unreliable since one may never fire again.
77
+ if (mapManager.map && ready) {
78
+ disableLoadScreen();
79
+ }
80
+ });
81
+
82
+ $effect(() => {
83
+ if (panelOpen) {
84
+ // Container width snapped to open; resize after layout settles
85
+ requestAnimationFrame(() => mapManager.map?.resize());
86
+ } else {
87
+ // Container width snaps back after the transform transition ends
88
+ setTimeout(() => mapManager.map?.resize(), TRANSITION_MS);
89
+ }
90
+ });
91
+
92
+ /**
93
+ * HACK: Fix for escaping sv-router and allowing navigation to other pages,
94
+ * as well as navigating back
95
+ */
96
+ if (routerEscapeHatch) {
97
+ const rootPath = basePath || "/";
98
+
99
+ window.addEventListener("pageshow", (e) => {
100
+ if (e.persisted) window.location.reload();
101
+ });
102
+
103
+ document.addEventListener(
104
+ "click",
105
+ (e) => {
106
+ const anchor = e
107
+ .composedPath()
108
+ .find((el) => el instanceof HTMLAnchorElement) as HTMLAnchorElement;
109
+ if (!anchor) return;
110
+ const { pathname } = new URL(anchor.href);
111
+ const isKnown = pathname === rootPath || knownRoutes.some((r) => pathname.startsWith(r));
112
+ if (!isKnown) {
113
+ e.stopImmediatePropagation();
114
+ window.location.href = anchor.href;
115
+ }
116
+ },
117
+ { capture: true }
118
+ );
119
+ }
120
+ </script>
121
+
122
+ <div class="relative flex h-full w-full flex-col md:flex-row">
123
+ <LoadScreen />
124
+ <div class="relative flex-1 overflow-hidden">
125
+ <Map {integrations} />
126
+ {@render overlays?.()}
127
+ {#if menu}
128
+ <div class="absolute top-4 left-4 z-10">
129
+ <Menu>{@render menu()}</Menu>
130
+ </div>
131
+ {/if}
132
+ </div>
133
+ <div
134
+ class={[
135
+ "panel-containr w-full shrink-0 overflow-hidden",
136
+ panelOpen ? "h-1/2 md:h-full md:w-1/3" : "md:w-0"
137
+ ]}
138
+ >
139
+ <div
140
+ class={[
141
+ "panel-contet h-full duration-300 ease-in-out",
142
+ panelOpen
143
+ ? "translate-y-0 md:translate-x-0"
144
+ : "translate-y-full md:translate-x-full md:translate-y-0"
145
+ ]}
146
+ >
147
+ {@render children?.()}
148
+ </div>
149
+ </div>
150
+ </div>
@@ -0,0 +1,48 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { SomeMapIntegration } from "./integrations/types";
3
+ export interface MapShellProps {
4
+ /** Map integrations to register, e.g. [monitorsMapIntegration] */
5
+ integrations: Array<SomeMapIntegration>;
6
+ /**
7
+ * True once the caller's own data managers have finished their initial
8
+ * load. The shell hides the load screen once the map is idle AND this
9
+ * is true.
10
+ */
11
+ ready: boolean;
12
+ /** True when the routed detail panel (`children`) should be visible */
13
+ panelOpen: boolean;
14
+ /**
15
+ * Path prefixes that are part of this map's own routed panel (e.g.
16
+ * [`${basePath}/monitor/`]) — internal navigation to these should not
17
+ * trigger the escape-hatch full-page navigation below.
18
+ */
19
+ knownRoutes?: Array<string>;
20
+ /** Rendered inside the display-options menu toggle */
21
+ menu?: Snippet;
22
+ /** Freeform absolutely-positioned content over the map (legend, search, etc.) */
23
+ overlays?: Snippet;
24
+ /** The routed detail panel content */
25
+ children?: Snippet;
26
+ /**
27
+ * Base path to use for the sv-router escape-hatch's "known" root route.
28
+ * If provided, used directly instead of reading `basePath` from
29
+ * `useMonitorMapRouter()`. Only relevant when `routerEscapeHatch` is
30
+ * enabled.
31
+ */
32
+ basePath?: string;
33
+ /**
34
+ * Whether to install the sv-router escape-hatch (`pageshow`/`click`
35
+ * listeners that force a full-page navigation for links outside this
36
+ * map's own routes). Defaults to `true`, preserving the existing
37
+ * full-page-map behavior. When `true` and no `basePath` prop is given,
38
+ * `MapShell` falls back to reading `basePath` from
39
+ * `useMonitorMapRouter()`. Set to `false` to embed `MapShell` inside a
40
+ * host app with its own routing — this also skips calling
41
+ * `useMonitorMapRouter()` entirely, so no `provideMonitorMapRouter(...)`
42
+ * ancestor is required.
43
+ */
44
+ routerEscapeHatch?: boolean;
45
+ }
46
+ declare const MapShell: import("svelte").Component<MapShellProps, {}, "">;
47
+ type MapShell = ReturnType<typeof MapShell>;
48
+ export default MapShell;
@@ -42,7 +42,7 @@ export const initializeMap = (container) => {
42
42
  preset: "milkyway"
43
43
  }
44
44
  });
45
- map.once("idle", () => {
45
+ map.once("load", () => {
46
46
  mapManager.map = map;
47
47
  });
48
48
  return () => map.remove();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sjvair/monitor-map",
3
3
  "description": "SJVAir's interactive monitor map",
4
- "version": "3.2.2",
4
+ "version": "3.3.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SJVAir/monitor-map"
@@ -1,13 +1,10 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
- import { onDestroy } from "svelte";
4
- import LoadScreen, { disable as disableLoadScreen } from "$lib/LoadScreen.svelte";
5
- import Map from "$lib/map/Map.svelte";
6
- import Menu from "$lib/map/Menu.svelte";
3
+ import { onDestroy, untrack } from "svelte";
4
+ import MapShell from "$lib/map/MapShell.svelte";
7
5
  import MonitorsDisplayOptions from "$lib/monitors/components/MonitorsDisplayOptions.svelte";
8
6
  import MapLayersDisplayOptions from "$lib/components/MapLayersDisplayOptions.svelte";
9
7
  import MapStyleDisplayOptions from "$lib/map/MapStyleDisplayOptions.svelte";
10
- import { mapManager } from "$lib/map/map.svelte";
11
8
  import { monitorsManager } from "$lib/monitors/monitors.svelte";
12
9
  import { monitorsMapIntegration } from "$lib/monitors/monitors-map-integration.svelte";
13
10
  import { windMapIntegration } from "$lib/wind/wind.svelte";
@@ -33,8 +30,6 @@
33
30
 
34
31
  const { route, navigate, basePath } = useMonitorMapRouter();
35
32
 
36
- const TRANSITION_MS = 300;
37
-
38
33
  const integrations: Array<SomeMapIntegration> = [
39
34
  baseLayerSeperator,
40
35
  collocationSitesMapIntegration,
@@ -59,10 +54,15 @@
59
54
  // applies a later "?pollutant=" change (e.g. navigating in from elsewhere) to an already-running manager.
60
55
  $effect(() => {
61
56
  const urlPollutant = route.search.pollutant;
57
+ // Read monitorsManager.pollutant via untrack: this effect must only react to the URL
58
+ // changing (e.g. navigating in from elsewhere), not to monitorsManager.pollutant itself.
59
+ // Tracking it here would make a UI-driven pollutant change (e.g. the display-options
60
+ // toggle) re-trigger this effect before the manager->URL effect below can sync the URL,
61
+ // so this effect would see the still-stale URL and immediately revert the user's change.
62
62
  if (
63
63
  monitorsManager.initialized &&
64
64
  (urlPollutant === "pm25" || urlPollutant === "o3") &&
65
- monitorsManager.pollutant !== urlPollutant
65
+ untrack(() => monitorsManager.pollutant) !== urlPollutant
66
66
  ) {
67
67
  monitorsManager.pollutant = urlPollutant;
68
68
  }
@@ -76,22 +76,6 @@
76
76
  }
77
77
  });
78
78
 
79
- $effect(() => {
80
- if (mapManager.map && monitorsManager.initialized) {
81
- mapManager.map.once("idle", () => disableLoadScreen());
82
- }
83
- });
84
-
85
- $effect(() => {
86
- if (panelOpen) {
87
- // Container width snapped to open; resize after layout settles
88
- requestAnimationFrame(() => mapManager.map?.resize());
89
- } else {
90
- // Container width snaps back after the transform transition ends
91
- setTimeout(() => mapManager.map?.resize(), TRANSITION_MS);
92
- }
93
- });
94
-
95
79
  // Clear selected icon scale when the detail panel closes
96
80
  $effect(() => {
97
81
  if (panelOpen) return;
@@ -101,75 +85,28 @@
101
85
  onDestroy(() => {
102
86
  monitorsManager.autoUpdate.stop();
103
87
  });
104
-
105
- /**
106
- * HACK: Fix for escaping sv-router and allowing navigation to other pages,
107
- * as well as navigating back
108
- */
109
- const rootPath = basePath || "/";
110
- const knownRoutes = [`${basePath}/monitor/`];
111
-
112
- window.addEventListener("pageshow", (e) => {
113
- if (e.persisted) window.location.reload();
114
- });
115
-
116
- document.addEventListener(
117
- "click",
118
- (e) => {
119
- const anchor = e
120
- .composedPath()
121
- .find((el) => el instanceof HTMLAnchorElement) as HTMLAnchorElement;
122
- if (!anchor) return;
123
- const { pathname } = new URL(anchor.href);
124
- const isKnown = pathname === rootPath || knownRoutes.some((r) => pathname.startsWith(r));
125
- if (!isKnown) {
126
- e.stopImmediatePropagation();
127
- window.location.href = anchor.href;
128
- }
129
- },
130
- { capture: true }
131
- );
132
88
  </script>
133
89
 
134
- <!--
135
- <div class="shell" class:panel-open={panelOpen}>
136
- -->
137
- <div class="relative flex h-full w-full flex-col md:flex-row">
138
- <LoadScreen />
139
- <div class="relative flex-1 overflow-hidden">
140
- <Map {integrations} />
90
+ <MapShell
91
+ {integrations}
92
+ ready={monitorsManager.initialized}
93
+ {panelOpen}
94
+ knownRoutes={[`${basePath}/monitor/`]}
95
+ {basePath}
96
+ >
97
+ {#snippet menu()}
98
+ <MonitorsDisplayOptions />
99
+ <EvStationsDisplayOptions />
100
+ <MapLayersDisplayOptions />
101
+ <MapStyleDisplayOptions />
102
+ {/snippet}
103
+ {#snippet overlays()}
141
104
  <div class="pointer-events-none absolute bottom-0 left-0 z-10">
142
105
  <MapLegend />
143
106
  </div>
144
- <div class="absolute top-4 left-4 z-10">
145
- <div class="absolute z-10">
146
- <Menu>
147
- <MonitorsDisplayOptions />
148
- <EvStationsDisplayOptions />
149
- <MapLayersDisplayOptions />
150
- <MapStyleDisplayOptions />
151
- </Menu>
152
- </div>
153
- <div class="absolute left-16">
154
- <Search />
155
- </div>
156
- </div>
157
- </div>
158
- <div
159
- class={[
160
- "panel-containr w-full shrink-0 overflow-hidden",
161
- panelOpen ? "h-1/2 md:h-full md:w-1/3" : "md:w-0"
162
- ]}
163
- >
164
- <div
165
- class={[
166
- "panel-contet h-full duration-300 ease-in-out",
167
- panelOpen
168
- ? "translate-y-0 md:translate-x-0"
169
- : "translate-y-full md:translate-x-full md:translate-y-0"
170
- ]}
171
- >
172
- {@render children()}
107
+ <div class="absolute top-4 left-20 z-10">
108
+ <Search />
173
109
  </div>
174
- </div>
175
- </div>
110
+ {/snippet}
111
+ {@render children()}
112
+ </MapShell>
package/src/lib/index.ts CHANGED
@@ -55,6 +55,7 @@ export { hmsSmokeMapIntegration } from "./hms/hms-smoke-map-integration.svelte";
55
55
 
56
56
  // Components
57
57
  export { default as Map } from "./map/Map.svelte";
58
+ export { default as MapShell, type MapShellProps } from "./map/MapShell.svelte";
58
59
  export { default as Menu } from "./map/Menu.svelte";
59
60
  export {
60
61
  default as LoadScreen,
@@ -0,0 +1,150 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { SomeMapIntegration } from "$lib/map/integrations/types";
4
+
5
+ export interface MapShellProps {
6
+ /** Map integrations to register, e.g. [monitorsMapIntegration] */
7
+ integrations: Array<SomeMapIntegration>;
8
+ /**
9
+ * True once the caller's own data managers have finished their initial
10
+ * load. The shell hides the load screen once the map is idle AND this
11
+ * is true.
12
+ */
13
+ ready: boolean;
14
+ /** True when the routed detail panel (`children`) should be visible */
15
+ panelOpen: boolean;
16
+ /**
17
+ * Path prefixes that are part of this map's own routed panel (e.g.
18
+ * [`${basePath}/monitor/`]) — internal navigation to these should not
19
+ * trigger the escape-hatch full-page navigation below.
20
+ */
21
+ knownRoutes?: Array<string>;
22
+ /** Rendered inside the display-options menu toggle */
23
+ menu?: Snippet;
24
+ /** Freeform absolutely-positioned content over the map (legend, search, etc.) */
25
+ overlays?: Snippet;
26
+ /** The routed detail panel content */
27
+ children?: Snippet;
28
+ /**
29
+ * Base path to use for the sv-router escape-hatch's "known" root route.
30
+ * If provided, used directly instead of reading `basePath` from
31
+ * `useMonitorMapRouter()`. Only relevant when `routerEscapeHatch` is
32
+ * enabled.
33
+ */
34
+ basePath?: string;
35
+ /**
36
+ * Whether to install the sv-router escape-hatch (`pageshow`/`click`
37
+ * listeners that force a full-page navigation for links outside this
38
+ * map's own routes). Defaults to `true`, preserving the existing
39
+ * full-page-map behavior. When `true` and no `basePath` prop is given,
40
+ * `MapShell` falls back to reading `basePath` from
41
+ * `useMonitorMapRouter()`. Set to `false` to embed `MapShell` inside a
42
+ * host app with its own routing — this also skips calling
43
+ * `useMonitorMapRouter()` entirely, so no `provideMonitorMapRouter(...)`
44
+ * ancestor is required.
45
+ */
46
+ routerEscapeHatch?: boolean;
47
+ }
48
+ </script>
49
+
50
+ <script lang="ts">
51
+ import LoadScreen, { disable as disableLoadScreen } from "$lib/LoadScreen.svelte";
52
+ import Map from "$lib/map/Map.svelte";
53
+ import Menu from "$lib/map/Menu.svelte";
54
+ import { mapManager } from "$lib/map/map.svelte";
55
+ import { useMonitorMapRouter } from "$lib/router-context";
56
+
57
+ let {
58
+ integrations,
59
+ ready,
60
+ panelOpen,
61
+ knownRoutes = [],
62
+ menu,
63
+ overlays,
64
+ children,
65
+ basePath: providedBasePath,
66
+ routerEscapeHatch = true
67
+ }: MapShellProps = $props();
68
+
69
+ const basePath = providedBasePath ?? (routerEscapeHatch ? useMonitorMapRouter().basePath : "");
70
+ const TRANSITION_MS = 300;
71
+
72
+ $effect(() => {
73
+ // mapManager.map is only ever assigned once its own initial "load" has
74
+ // already fired (see initializeMap in map.svelte.ts), so by the time this
75
+ // effect sees a non-null map, it's already safe to reveal — waiting for a
76
+ // further "idle" event here is unreliable since one may never fire again.
77
+ if (mapManager.map && ready) {
78
+ disableLoadScreen();
79
+ }
80
+ });
81
+
82
+ $effect(() => {
83
+ if (panelOpen) {
84
+ // Container width snapped to open; resize after layout settles
85
+ requestAnimationFrame(() => mapManager.map?.resize());
86
+ } else {
87
+ // Container width snaps back after the transform transition ends
88
+ setTimeout(() => mapManager.map?.resize(), TRANSITION_MS);
89
+ }
90
+ });
91
+
92
+ /**
93
+ * HACK: Fix for escaping sv-router and allowing navigation to other pages,
94
+ * as well as navigating back
95
+ */
96
+ if (routerEscapeHatch) {
97
+ const rootPath = basePath || "/";
98
+
99
+ window.addEventListener("pageshow", (e) => {
100
+ if (e.persisted) window.location.reload();
101
+ });
102
+
103
+ document.addEventListener(
104
+ "click",
105
+ (e) => {
106
+ const anchor = e
107
+ .composedPath()
108
+ .find((el) => el instanceof HTMLAnchorElement) as HTMLAnchorElement;
109
+ if (!anchor) return;
110
+ const { pathname } = new URL(anchor.href);
111
+ const isKnown = pathname === rootPath || knownRoutes.some((r) => pathname.startsWith(r));
112
+ if (!isKnown) {
113
+ e.stopImmediatePropagation();
114
+ window.location.href = anchor.href;
115
+ }
116
+ },
117
+ { capture: true }
118
+ );
119
+ }
120
+ </script>
121
+
122
+ <div class="relative flex h-full w-full flex-col md:flex-row">
123
+ <LoadScreen />
124
+ <div class="relative flex-1 overflow-hidden">
125
+ <Map {integrations} />
126
+ {@render overlays?.()}
127
+ {#if menu}
128
+ <div class="absolute top-4 left-4 z-10">
129
+ <Menu>{@render menu()}</Menu>
130
+ </div>
131
+ {/if}
132
+ </div>
133
+ <div
134
+ class={[
135
+ "panel-containr w-full shrink-0 overflow-hidden",
136
+ panelOpen ? "h-1/2 md:h-full md:w-1/3" : "md:w-0"
137
+ ]}
138
+ >
139
+ <div
140
+ class={[
141
+ "panel-contet h-full duration-300 ease-in-out",
142
+ panelOpen
143
+ ? "translate-y-0 md:translate-x-0"
144
+ : "translate-y-full md:translate-x-full md:translate-y-0"
145
+ ]}
146
+ >
147
+ {@render children?.()}
148
+ </div>
149
+ </div>
150
+ </div>
@@ -61,7 +61,7 @@ export const initializeMap: Attachment<HTMLDivElement> = (container: string | HT
61
61
  }
62
62
  });
63
63
 
64
- map.once("idle", () => {
64
+ map.once("load", () => {
65
65
  mapManager.map = map;
66
66
  });
67
67