@sjvair/monitor-map 4.2.0 → 5.0.0-beta.0

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.
Files changed (265) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -126
  3. package/dist/lib/MonitorDetailPanel.svelte +23 -20
  4. package/dist/lib/MonitorMapLayout.svelte +147 -77
  5. package/dist/lib/abort.d.ts +16 -0
  6. package/dist/lib/abort.js +51 -0
  7. package/dist/lib/components/DataBox.svelte +4 -1
  8. package/dist/lib/components/ui/range-calendar/range-calendar.svelte.d.ts +1 -1
  9. package/dist/lib/config.svelte.d.ts +7 -0
  10. package/dist/lib/config.svelte.js +10 -0
  11. package/dist/lib/core.d.ts +2 -2
  12. package/dist/lib/core.js +2 -1
  13. package/dist/lib/data-chart/DataChart.svelte +54 -24
  14. package/dist/lib/data-chart/DateRange.d.ts +13 -4
  15. package/dist/lib/data-chart/DateRange.js +50 -14
  16. package/dist/lib/data-chart/chart-utils.js +5 -7
  17. package/dist/lib/data-chart/level-colors.d.ts +3 -4
  18. package/dist/lib/data-chart/level-colors.js +7 -4
  19. package/dist/lib/data-chart/pacific-time.d.ts +9 -0
  20. package/dist/lib/data-chart/pacific-time.js +25 -0
  21. package/dist/lib/data-chart/requests.d.ts +2 -1
  22. package/dist/lib/data-chart/requests.js +5 -2
  23. package/dist/lib/data-chart/tooltip.js +2 -2
  24. package/{src/lib/ev-stations/components/EvStationTooltip.svelte → dist/lib/ev-stations/EvPopup.svelte} +6 -5
  25. package/dist/lib/ev-stations/EvPopup.svelte.d.ts +7 -0
  26. package/dist/lib/ev-stations/ev-plugin.svelte.d.ts +14 -0
  27. package/dist/lib/ev-stations/ev-plugin.svelte.js +71 -0
  28. package/dist/lib/ev-stations/ev-station-icons.d.ts +1 -4
  29. package/dist/lib/ev-stations/ev-station-icons.js +1 -13
  30. package/dist/lib/ev-stations/ev-stations.d.ts +7 -0
  31. package/dist/lib/ev-stations/ev-stations.js +53 -0
  32. package/dist/lib/ev-stations/ev-store.svelte.d.ts +21 -0
  33. package/dist/lib/ev-stations/ev-store.svelte.js +83 -0
  34. package/dist/lib/ev-stations/station-popup.svelte.d.ts +14 -0
  35. package/dist/lib/ev-stations/station-popup.svelte.js +54 -0
  36. package/dist/lib/ev-stations/types.d.ts +0 -29
  37. package/dist/lib/ev-stations/zip-codes.d.ts +11 -0
  38. package/dist/lib/ev-stations/zip-codes.js +28 -16
  39. package/dist/lib/index.d.ts +8 -56
  40. package/dist/lib/index.js +7 -46
  41. package/dist/lib/legend/Legend.svelte +18 -19
  42. package/dist/lib/legend/Legend.svelte.d.ts +2 -2
  43. package/dist/lib/levels.d.ts +7 -0
  44. package/dist/lib/levels.js +19 -0
  45. package/dist/lib/map/MapShell.svelte +17 -24
  46. package/dist/lib/map/MapShell.svelte.d.ts +4 -4
  47. package/dist/lib/map/basemaps.d.ts +25 -0
  48. package/dist/lib/map/basemaps.js +66 -0
  49. package/dist/lib/map/live-map.svelte.d.ts +3 -0
  50. package/dist/lib/map/live-map.svelte.js +22 -0
  51. package/dist/lib/map/sync-plugins.svelte.d.ts +19 -0
  52. package/dist/lib/map/sync-plugins.svelte.js +52 -0
  53. package/dist/lib/monitors/components/MonitorSubscriptionManager.svelte +82 -82
  54. package/dist/lib/monitors/filtered-source.svelte.d.ts +9 -0
  55. package/dist/lib/monitors/filtered-source.svelte.js +28 -0
  56. package/dist/lib/monitors/monitor-details.svelte.js +11 -3
  57. package/dist/lib/monitors/monitors.svelte.d.ts +29 -26
  58. package/dist/lib/monitors/monitors.svelte.js +35 -126
  59. package/dist/lib/monitors/retry-load.d.ts +8 -0
  60. package/dist/lib/monitors/retry-load.js +11 -0
  61. package/dist/lib/monitors/subscriptions.d.ts +3 -0
  62. package/dist/lib/monitors/subscriptions.js +9 -0
  63. package/dist/lib/monitors/types.d.ts +1 -41
  64. package/dist/lib/options/map-option-defaults.d.ts +25 -36
  65. package/dist/lib/options/map-option-defaults.js +12 -30
  66. package/dist/lib/options/map-options.svelte.d.ts +13 -0
  67. package/dist/lib/options/map-options.svelte.js +26 -0
  68. package/dist/lib/options/menus/LayersMenu.svelte +8 -20
  69. package/dist/lib/options/menus/LayersMenu.svelte.d.ts +17 -2
  70. package/dist/lib/options/menus/MonitorsMenu.svelte +34 -18
  71. package/dist/lib/options/menus/OverlaysMenu.svelte +2 -2
  72. package/dist/lib/options/menus/PollutantMenu.svelte +4 -9
  73. package/dist/lib/options/menus/SettingsMenu.svelte +32 -25
  74. package/dist/lib/options/menus/SettingsMenu.svelte.d.ts +5 -16
  75. package/dist/lib/options/plugin-order.d.ts +8 -0
  76. package/dist/lib/options/plugin-order.js +36 -0
  77. package/dist/lib/options/pollutant-param.d.ts +7 -5
  78. package/dist/lib/options/pollutant-param.js +9 -5
  79. package/dist/lib/options/row-icons.d.ts +8 -0
  80. package/dist/lib/options/row-icons.js +15 -0
  81. package/dist/lib/options/rows/MarkerIcon.svelte +5 -5
  82. package/dist/lib/options/rows/MarkerIcon.svelte.d.ts +1 -2
  83. package/dist/lib/search/Search.svelte +20 -33
  84. package/dist/lib/search/Search.svelte.d.ts +5 -1
  85. package/dist/lib/search/search-pin.svelte.d.ts +11 -0
  86. package/dist/lib/search/search-pin.svelte.js +28 -0
  87. package/dist/lib/search/service.d.ts +7 -2
  88. package/dist/lib/search/service.js +17 -10
  89. package/dist/lib/stores.svelte.d.ts +8 -0
  90. package/dist/lib/stores.svelte.js +9 -0
  91. package/dist/lib/wind/wind-plugin.d.ts +11 -0
  92. package/dist/lib/wind/wind-plugin.js +25 -0
  93. package/package.json +12 -10
  94. package/src/lib/MonitorDetailPanel.svelte +23 -20
  95. package/src/lib/MonitorMapLayout.svelte +147 -77
  96. package/src/lib/abort.ts +59 -0
  97. package/src/lib/components/DataBox.svelte +4 -1
  98. package/src/lib/config.svelte.ts +16 -0
  99. package/src/lib/core.ts +2 -2
  100. package/src/lib/data-chart/DataChart.svelte +54 -24
  101. package/src/lib/data-chart/DateRange.ts +58 -15
  102. package/src/lib/data-chart/chart-utils.ts +6 -7
  103. package/src/lib/data-chart/level-colors.ts +8 -9
  104. package/src/lib/data-chart/pacific-time.ts +31 -0
  105. package/src/lib/data-chart/requests.ts +14 -7
  106. package/src/lib/data-chart/tooltip.ts +2 -2
  107. package/{dist/lib/ev-stations/components/EvStationTooltip.svelte → src/lib/ev-stations/EvPopup.svelte} +6 -5
  108. package/src/lib/ev-stations/ev-plugin.svelte.ts +86 -0
  109. package/src/lib/ev-stations/ev-station-icons.ts +1 -17
  110. package/src/lib/ev-stations/ev-stations.ts +72 -0
  111. package/src/lib/ev-stations/ev-store.svelte.ts +117 -0
  112. package/src/lib/ev-stations/station-popup.svelte.ts +64 -0
  113. package/src/lib/ev-stations/types.ts +0 -34
  114. package/src/lib/ev-stations/zip-codes.ts +29 -16
  115. package/src/lib/index.ts +8 -99
  116. package/src/lib/legend/Legend.svelte +18 -19
  117. package/src/lib/levels.ts +24 -0
  118. package/src/lib/map/MapShell.svelte +17 -24
  119. package/src/lib/map/basemaps.ts +76 -0
  120. package/src/lib/map/live-map.svelte.ts +24 -0
  121. package/src/lib/map/sync-plugins.svelte.ts +69 -0
  122. package/src/lib/monitors/components/MonitorSubscriptionManager.svelte +82 -82
  123. package/src/lib/monitors/filtered-source.svelte.ts +38 -0
  124. package/src/lib/monitors/monitor-details.svelte.ts +12 -3
  125. package/src/lib/monitors/monitors.svelte.ts +62 -153
  126. package/src/lib/monitors/retry-load.ts +14 -0
  127. package/src/lib/monitors/subscriptions.ts +17 -0
  128. package/src/lib/monitors/types.ts +1 -47
  129. package/src/lib/options/map-option-defaults.ts +30 -53
  130. package/src/lib/options/map-options.svelte.ts +30 -0
  131. package/src/lib/options/menus/LayersMenu.svelte +8 -20
  132. package/src/lib/options/menus/MonitorsMenu.svelte +34 -18
  133. package/src/lib/options/menus/OverlaysMenu.svelte +2 -2
  134. package/src/lib/options/menus/PollutantMenu.svelte +4 -9
  135. package/src/lib/options/menus/SettingsMenu.svelte +32 -25
  136. package/src/lib/options/plugin-order.ts +38 -0
  137. package/src/lib/options/pollutant-param.ts +14 -9
  138. package/src/lib/options/row-icons.ts +17 -0
  139. package/src/lib/options/rows/MarkerIcon.svelte +5 -5
  140. package/src/lib/search/Search.svelte +20 -33
  141. package/src/lib/search/search-pin.svelte.ts +35 -0
  142. package/src/lib/search/service.ts +34 -15
  143. package/src/lib/stores.svelte.ts +15 -0
  144. package/src/lib/wind/wind-plugin.ts +33 -0
  145. package/dist/lib/collocation-sites/CollocationTooltip.svelte +0 -98
  146. package/dist/lib/collocation-sites/CollocationTooltip.svelte.d.ts +0 -7
  147. package/dist/lib/collocation-sites/collocations-icon-manager.svelte.d.ts +0 -7
  148. package/dist/lib/collocation-sites/collocations-icon-manager.svelte.js +0 -18
  149. package/dist/lib/collocation-sites/collocations-map-integration.svelte.d.ts +0 -21
  150. package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +0 -113
  151. package/dist/lib/collocation-sites/collocations.svelte.d.ts +0 -9
  152. package/dist/lib/collocation-sites/collocations.svelte.js +0 -17
  153. package/dist/lib/collocation-sites/types.d.ts +0 -8
  154. package/dist/lib/collocation-sites/types.js +0 -1
  155. package/dist/lib/ev-stations/components/EvStationClusterTooltip.svelte +0 -16
  156. package/dist/lib/ev-stations/components/EvStationClusterTooltip.svelte.d.ts +0 -7
  157. package/dist/lib/ev-stations/components/EvStationTooltip.svelte.d.ts +0 -7
  158. package/dist/lib/ev-stations/ev-stations-cluster-renderer.d.ts +0 -19
  159. package/dist/lib/ev-stations/ev-stations-cluster-renderer.js +0 -131
  160. package/dist/lib/ev-stations/ev-stations-map-integration.svelte.d.ts +0 -31
  161. package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +0 -226
  162. package/dist/lib/ev-stations/ev-stations.svelte.d.ts +0 -9
  163. package/dist/lib/ev-stations/ev-stations.svelte.js +0 -55
  164. package/dist/lib/hms/hms-fire-icon-manager.d.ts +0 -13
  165. package/dist/lib/hms/hms-fire-icon-manager.js +0 -39
  166. package/dist/lib/hms/hms-fire-map-integration.svelte.d.ts +0 -23
  167. package/dist/lib/hms/hms-fire-map-integration.svelte.js +0 -94
  168. package/dist/lib/hms/hms-smoke-map-integration.svelte.d.ts +0 -39
  169. package/dist/lib/hms/hms-smoke-map-integration.svelte.js +0 -87
  170. package/dist/lib/hms/hms.svelte.d.ts +0 -19
  171. package/dist/lib/hms/hms.svelte.js +0 -227
  172. package/dist/lib/legend/LegendBar.svelte +0 -45
  173. package/dist/lib/legend/LegendBar.svelte.d.ts +0 -7
  174. package/dist/lib/legend/legend-data.d.ts +0 -40
  175. package/dist/lib/legend/legend-data.js +0 -65
  176. package/dist/lib/map/Map.svelte +0 -13
  177. package/dist/lib/map/Map.svelte.d.ts +0 -8
  178. package/dist/lib/map/basemap.d.ts +0 -14
  179. package/dist/lib/map/basemap.js +0 -10
  180. package/dist/lib/map/icons.d.ts +0 -7
  181. package/dist/lib/map/icons.js +0 -33
  182. package/dist/lib/map/integrations/base-layer-seperator.d.ts +0 -9
  183. package/dist/lib/map/integrations/base-layer-seperator.js +0 -11
  184. package/dist/lib/map/integrations/click-manager.d.ts +0 -14
  185. package/dist/lib/map/integrations/click-manager.js +0 -52
  186. package/dist/lib/map/integrations/integrations-manager.d.ts +0 -8
  187. package/dist/lib/map/integrations/integrations-manager.js +0 -20
  188. package/dist/lib/map/integrations/map-display-option.svelte.d.ts +0 -7
  189. package/dist/lib/map/integrations/map-display-option.svelte.js +0 -10
  190. package/dist/lib/map/integrations/map-geojson-integration.svelte.d.ts +0 -32
  191. package/dist/lib/map/integrations/map-geojson-integration.svelte.js +0 -60
  192. package/dist/lib/map/integrations/map-icon-manager.d.ts +0 -15
  193. package/dist/lib/map/integrations/map-icon-manager.js +0 -52
  194. package/dist/lib/map/integrations/map-integration.svelte.d.ts +0 -6
  195. package/dist/lib/map/integrations/map-integration.svelte.js +0 -2
  196. package/dist/lib/map/integrations/map-layer-integration.svelte.d.ts +0 -11
  197. package/dist/lib/map/integrations/map-layer-integration.svelte.js +0 -40
  198. package/dist/lib/map/integrations/tooltip.svelte.d.ts +0 -36
  199. package/dist/lib/map/integrations/tooltip.svelte.js +0 -131
  200. package/dist/lib/map/integrations/types.d.ts +0 -14
  201. package/dist/lib/map/integrations/types.js +0 -1
  202. package/dist/lib/map/map-manager.svelte.d.ts +0 -20
  203. package/dist/lib/map/map-manager.svelte.js +0 -63
  204. package/dist/lib/map/map-style-state.svelte.d.ts +0 -23
  205. package/dist/lib/map/map-style-state.svelte.js +0 -86
  206. package/dist/lib/map/utils.d.ts +0 -7
  207. package/dist/lib/map/utils.js +0 -36
  208. package/dist/lib/monitors/components/MonitorClusterTooltip.svelte +0 -112
  209. package/dist/lib/monitors/components/MonitorClusterTooltip.svelte.d.ts +0 -7
  210. package/dist/lib/monitors/components/MonitorTooltip.svelte +0 -61
  211. package/dist/lib/monitors/components/MonitorTooltip.svelte.d.ts +0 -7
  212. package/dist/lib/monitors/monitor-utils.d.ts +0 -4
  213. package/dist/lib/monitors/monitor-utils.js +0 -41
  214. package/dist/lib/monitors/monitors-cluster-renderer.d.ts +0 -33
  215. package/dist/lib/monitors/monitors-cluster-renderer.js +0 -221
  216. package/dist/lib/monitors/monitors-icon-manager.svelte.d.ts +0 -15
  217. package/dist/lib/monitors/monitors-icon-manager.svelte.js +0 -78
  218. package/dist/lib/monitors/monitors-map-integration.svelte.d.ts +0 -43
  219. package/dist/lib/monitors/monitors-map-integration.svelte.js +0 -301
  220. package/dist/lib/options/reset-map-options.d.ts +0 -2
  221. package/dist/lib/options/reset-map-options.js +0 -24
  222. package/dist/lib/reactivity.svelte.d.ts +0 -0
  223. package/dist/lib/reactivity.svelte.js +0 -83
  224. package/dist/lib/wind/wind.svelte.d.ts +0 -10
  225. package/dist/lib/wind/wind.svelte.js +0 -13
  226. package/src/lib/collocation-sites/CollocationTooltip.svelte +0 -98
  227. package/src/lib/collocation-sites/collocations-icon-manager.svelte.ts +0 -21
  228. package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +0 -136
  229. package/src/lib/collocation-sites/collocations.svelte.ts +0 -24
  230. package/src/lib/collocation-sites/types.ts +0 -10
  231. package/src/lib/ev-stations/components/EvStationClusterTooltip.svelte +0 -16
  232. package/src/lib/ev-stations/ev-stations-cluster-renderer.ts +0 -162
  233. package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +0 -248
  234. package/src/lib/ev-stations/ev-stations.svelte.ts +0 -79
  235. package/src/lib/hms/hms-fire-icon-manager.ts +0 -48
  236. package/src/lib/hms/hms-fire-map-integration.svelte.ts +0 -109
  237. package/src/lib/hms/hms-smoke-map-integration.svelte.ts +0 -105
  238. package/src/lib/hms/hms.svelte.ts +0 -264
  239. package/src/lib/legend/LegendBar.svelte +0 -45
  240. package/src/lib/legend/legend-data.ts +0 -101
  241. package/src/lib/map/Map.svelte +0 -13
  242. package/src/lib/map/basemap.ts +0 -24
  243. package/src/lib/map/icons.ts +0 -42
  244. package/src/lib/map/integrations/base-layer-seperator.ts +0 -17
  245. package/src/lib/map/integrations/click-manager.ts +0 -64
  246. package/src/lib/map/integrations/integrations-manager.ts +0 -25
  247. package/src/lib/map/integrations/map-display-option.svelte.ts +0 -13
  248. package/src/lib/map/integrations/map-geojson-integration.svelte.ts +0 -77
  249. package/src/lib/map/integrations/map-icon-manager.ts +0 -67
  250. package/src/lib/map/integrations/map-integration.svelte.ts +0 -8
  251. package/src/lib/map/integrations/map-layer-integration.svelte.ts +0 -49
  252. package/src/lib/map/integrations/tooltip.svelte.ts +0 -153
  253. package/src/lib/map/integrations/types.ts +0 -24
  254. package/src/lib/map/map-manager.svelte.ts +0 -83
  255. package/src/lib/map/map-style-state.svelte.ts +0 -94
  256. package/src/lib/map/utils.ts +0 -53
  257. package/src/lib/monitors/components/MonitorClusterTooltip.svelte +0 -112
  258. package/src/lib/monitors/components/MonitorTooltip.svelte +0 -61
  259. package/src/lib/monitors/monitor-utils.ts +0 -57
  260. package/src/lib/monitors/monitors-cluster-renderer.ts +0 -285
  261. package/src/lib/monitors/monitors-icon-manager.svelte.ts +0 -98
  262. package/src/lib/monitors/monitors-map-integration.svelte.ts +0 -318
  263. package/src/lib/options/reset-map-options.ts +0 -28
  264. package/src/lib/reactivity.svelte.ts +0 -82
  265. package/src/lib/wind/wind.svelte.ts +0 -20
package/dist/lib/index.js CHANGED
@@ -15,9 +15,11 @@
15
15
  * ```svelte
16
16
  * <!-- app root -->
17
17
  * <script>
18
- * import { provideMonitorMapRouter } from "@sjvair/monitor-map";
18
+ * import { configureMonitorMap, provideMonitorMapRouter } from "@sjvair/monitor-map";
19
19
  * import { route, navigate, p, isActive, Router } from "./router";
20
20
  *
21
+ * // Once at startup, before the map or the EV layer is used. The library reads no keys itself.
22
+ * configureMonitorMap({ maptilerKey, nrelKey });
21
23
  * provideMonitorMapRouter({ route, navigate, p, isActive, basePath: "/my-map" });
22
24
  * </script>
23
25
  *
@@ -25,53 +27,12 @@
25
27
  * ```
26
28
  *
27
29
  * A host that must stay light at startup imports from the subpath entries instead:
28
- * `provideMonitorMapRouter`/`monitorsManager` from "@sjvair/monitor-map/core" (no map), and
30
+ * `configureMonitorMap`/`provideMonitorMapRouter`/`monitorsManager` from
31
+ * "@sjvair/monitor-map/core" (no map), and
29
32
  * `monitorMapRoutes` from "@sjvair/monitor-map/routes" (same tree, lazy layout and routes).
30
33
  * `monitorMapRoutes` from this entry stays eager.
31
34
  */
32
35
  export { monitorMapRoutes } from "./routes";
36
+ export { configureMonitorMap, monitorMapConfig } from "./config.svelte.js";
33
37
  export { ROUTER_KEY, provideMonitorMapRouter, useMonitorMapRouter } from "./router-context";
34
- // State managers
35
- export { monitorsManager } from "./monitors/monitors.svelte";
36
- export { collocationSitesManager } from "./collocation-sites/collocations.svelte";
37
- export { evStationsManager } from "./ev-stations/ev-stations.svelte";
38
- export { hmsManager } from "./hms/hms.svelte";
39
- export { mapManager, DefaultMapStyle, initializeMap } from "./map/map-manager.svelte.js";
40
- // Map integrations
41
- export { monitorsMapIntegration, MonitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
42
- export { collocationSitesMapIntegration } from "./collocation-sites/collocations-map-integration.svelte";
43
- export { evStationsMapIntegration } from "./ev-stations/ev-stations-map-integration.svelte";
44
- export { windMapIntegration } from "./wind/wind.svelte";
45
- export { baseLayerSeperator } from "./map/integrations/base-layer-seperator";
46
- export { hmsFireMapIntegration } from "./hms/hms-fire-map-integration.svelte";
47
- export { hmsSmokeMapIntegration } from "./hms/hms-smoke-map-integration.svelte";
48
- // Components
49
- export { default as Map } from "./map/Map.svelte";
50
- export { default as MapShell } from "./map/MapShell.svelte";
51
- export { default as LoadScreen, loadScreenState, enable as enableLoadScreen, disable as disableLoadScreen } from "./LoadScreen.svelte";
52
- // Options toolbar / panel (compose these inside MapShell's `menu` snippet)
53
- export { default as OptionsBar } from "./options/OptionsBar.svelte";
54
- export { default as OptionsGroup } from "./options/OptionsGroup.svelte";
55
- export { default as OptionsMenu } from "./options/OptionsMenu.svelte";
56
- export { default as CheckboxRow } from "./options/rows/CheckboxRow.svelte";
57
- export { default as RadioRow } from "./options/rows/RadioRow.svelte";
58
- export { default as SubmenuRow } from "./options/rows/SubmenuRow.svelte";
59
- export { default as GroupRow } from "./options/rows/GroupRow.svelte";
60
- export { default as MarkerIcon } from "./options/rows/MarkerIcon.svelte";
61
- export { default as PollutantMenu } from "./options/menus/PollutantMenu.svelte";
62
- export { default as MonitorsMenu } from "./options/menus/MonitorsMenu.svelte";
63
- export { default as LayersMenu } from "./options/menus/LayersMenu.svelte";
64
- export { default as OverlaysMenu } from "./options/menus/OverlaysMenu.svelte";
65
- export { default as SettingsMenu } from "./options/menus/SettingsMenu.svelte";
66
- export { resetMapOptions } from "./options/reset-map-options";
67
- export { mapStyleState } from "./map/map-style-state.svelte.js";
68
- // Legend
69
- export { default as Legend } from "./legend/Legend.svelte";
70
- export { default as LegendBar } from "./legend/LegendBar.svelte";
71
- export { pollutantLegendSection, fireSmokeLegendSections } from "./legend/legend-data";
72
- export { FIRE_LEGEND_CATEGORIES } from "./hms/hms-fire-icon-manager";
73
- export { SMOKE_LEGEND } from "./hms/hms-smoke-map-integration.svelte";
74
- // Integration base classes (extend these to add custom map features)
75
- export { MapIntegration } from "./map/integrations/map-integration.svelte";
76
- export { MapLayerIntegration } from "./map/integrations/map-layer-integration.svelte";
77
- export { MapIconLayerIntegration as MapGeoJSONIntegration } from "./map/integrations/map-geojson-integration.svelte";
38
+ export { monitorsManager } from "./monitors/monitors.svelte.js";
@@ -1,15 +1,25 @@
1
1
  <script lang="ts">
2
2
  import { ChevronDownIcon, InfoIcon } from "@lucide/svelte";
3
- import LegendBar from "./LegendBar.svelte";
4
- import type { LegendSection } from "./legend-data";
3
+ import { Legend, legendSection, type MapContext } from "@sjvair/map-sdk";
5
4
 
6
- let { sections }: { sections: Array<LegendSection> } = $props();
5
+ let { map }: { map: MapContext } = $props();
7
6
 
8
7
  // Independent of which sections are active, so toggling a layer doesn't reset it.
9
8
  let collapsed = $state(false);
9
+
10
+ // What map-sdk's Legend would list: a scale with levels, or a status line for a plugin that is
11
+ // loading or failing. The card is hidden while there is none.
12
+ const hasSections = $derived(
13
+ map.plugins.some((plugin) => {
14
+ const data = plugin.legend?.();
15
+ if (!data) return false;
16
+ if ("status" in data) return data.status.state !== "ready";
17
+ return legendSection(data, map.format) !== null;
18
+ })
19
+ );
10
20
  </script>
11
21
 
12
- {#if sections.length}
22
+ {#if hasSections}
13
23
  <div class="pointer-events-auto w-84 max-w-[calc(100vw-2rem)] rounded-md bg-white shadow">
14
24
  <button
15
25
  type="button"
@@ -21,20 +31,9 @@
21
31
  <span class="flex-1 text-left">Legend</span>
22
32
  <ChevronDownIcon size={18} class="transition-transform {collapsed ? '-rotate-90' : ''}" />
23
33
  </button>
24
- {#if !collapsed}
25
- <div class="flex flex-col gap-3 border-t border-gray-200 px-4 py-3">
26
- {#each sections as section (section.id)}
27
- <div>
28
- <p class="mb-2 text-center text-sm font-semibold">
29
- {section.title}
30
- {#if section.unit}
31
- <span class="text-xs font-normal text-gray-500">({section.unit})</span>
32
- {/if}
33
- </p>
34
- <LegendBar bar={section.bar} />
35
- </div>
36
- {/each}
37
- </div>
38
- {/if}
34
+ <!-- Kept mounted (hidden) when collapsed so the legend doesn't re-read the theme on expand. -->
35
+ <div class="border-t border-gray-200 px-4 py-3 {collapsed ? 'hidden' : ''}">
36
+ <Legend {map} order="top-first" framed={false} />
37
+ </div>
39
38
  </div>
40
39
  {/if}
@@ -1,6 +1,6 @@
1
- import type { LegendSection } from "./legend-data";
1
+ import { Legend, type MapContext } from "@sjvair/map-sdk";
2
2
  type $$ComponentProps = {
3
- sections: Array<LegendSection>;
3
+ map: MapContext;
4
4
  };
5
5
  declare const Legend: import("svelte").Component<$$ComponentProps, {}, "">;
6
6
  type Legend = ReturnType<typeof Legend>;
@@ -0,0 +1,7 @@
1
+ import { type Level, type Scale } from "@sjvair/map-sdk/data";
2
+ /** The color for a reading no level covers, as map-sdk draws an inactive monitor. */
3
+ export declare const NEUTRAL_LEVEL_COLOR = "#969696";
4
+ /** The scale level a reading falls in, or null when there is no scale or no level covers it. */
5
+ export declare function readingLevel(value: string | number | null | undefined, scale: Scale | null): Level | null;
6
+ /** A reading as the panel shows it: cut off to a whole number, or "–" when not a number. */
7
+ export declare function readingDisplay(value: string | number | null | undefined): string;
@@ -0,0 +1,19 @@
1
+ import { levelFor, NO_LEVEL } from "@sjvair/map-sdk/data";
2
+ /** The color for a reading no level covers, as map-sdk draws an inactive monitor. */
3
+ export const NEUTRAL_LEVEL_COLOR = "#969696";
4
+ /** The scale level a reading falls in, or null when there is no scale or no level covers it. */
5
+ export function readingLevel(value, scale) {
6
+ if (!scale)
7
+ return null;
8
+ const level = levelFor(value, scale);
9
+ return level === NO_LEVEL ? null : level;
10
+ }
11
+ /** A reading as the panel shows it: cut off to a whole number, or "–" when not a number. */
12
+ export function readingDisplay(value) {
13
+ // Number("") and Number(null) are 0, not a reading.
14
+ if (value === null || value === undefined || (typeof value === "string" && !value.trim())) {
15
+ return "–";
16
+ }
17
+ const number = Math.trunc(Number(value));
18
+ return Number.isFinite(number) ? number.toString() : "–";
19
+ }
@@ -1,13 +1,13 @@
1
1
  <script lang="ts" module>
2
2
  import type { Snippet } from "svelte";
3
- import type { SomeMapIntegration } from "./integrations/types";
3
+ import type { MapContext } from "@sjvair/map-sdk";
4
4
 
5
5
  export interface MapShellProps {
6
- /** Map integrations to register, e.g. [monitorsMapIntegration] */
7
- integrations: Array<SomeMapIntegration>;
6
+ /** The map to show, from `createMap()` (the monitor map uses `createLiveMap()`). */
7
+ map: MapContext;
8
8
  /**
9
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
10
+ * load. The shell hides the load screen once the map is ready AND this
11
11
  * is true.
12
12
  */
13
13
  ready: boolean;
@@ -56,13 +56,12 @@
56
56
 
57
57
  <script lang="ts">
58
58
  import LoadScreen, { disable as disableLoadScreen } from "../LoadScreen.svelte";
59
- import Map from "./Map.svelte";
59
+ import { MapView } from "@sjvair/map-sdk";
60
60
  import OptionsBar from "../options/OptionsBar.svelte";
61
- import { mapManager } from "./map-manager.svelte.js";
62
61
  import { useMonitorMapRouter } from "../router-context";
63
62
 
64
63
  let {
65
- integrations,
64
+ map,
66
65
  ready,
67
66
  loadError,
68
67
  panelOpen,
@@ -76,28 +75,14 @@
76
75
  }: MapShellProps = $props();
77
76
 
78
77
  const basePath = providedBasePath ?? (routerEscapeHatch ? useMonitorMapRouter().basePath : "");
79
- const TRANSITION_MS = 300;
80
78
 
81
79
  $effect(() => {
82
- // mapManager.map is only ever assigned once its own initial "load" has
83
- // already fired (see initializeMap in map-manager.svelte.ts), so by the time this
84
- // effect sees a non-null map, it's already safe to reveal — waiting for a
85
- // further "idle" event here is unreliable since one may never fire again.
86
- if (mapManager.map && ready) {
80
+ // `map.ready` is map-sdk's signal that the style loaded and the map went idle.
81
+ if (map.ready && ready) {
87
82
  disableLoadScreen();
88
83
  }
89
84
  });
90
85
 
91
- $effect(() => {
92
- if (panelOpen) {
93
- // Container width snapped to open; resize after layout settles
94
- requestAnimationFrame(() => mapManager.map?.resize());
95
- } else {
96
- // Container width snaps back after the transform transition ends
97
- setTimeout(() => mapManager.map?.resize(), TRANSITION_MS);
98
- }
99
- });
100
-
101
86
  /**
102
87
  * HACK: Fix for escaping sv-router and allowing navigation to other pages,
103
88
  * as well as navigating back
@@ -131,7 +116,15 @@
131
116
  <div class="relative flex h-full w-full flex-col md:flex-row">
132
117
  <LoadScreen error={loadError} />
133
118
  <div class="relative flex-1 overflow-hidden">
134
- <Map {integrations} />
119
+ <MapView
120
+ {map}
121
+ label="Air quality monitor map"
122
+ controls="engine"
123
+ featureList={{ position: "top-right", reveal: "focus" }}
124
+ selectionBar={false}
125
+ tooltip="pointer"
126
+ class="h-full w-full"
127
+ />
135
128
  {@render overlays?.()}
136
129
  {#if menu || search}
137
130
  <OptionsBar {search}>{@render menu?.()}</OptionsBar>
@@ -1,11 +1,11 @@
1
1
  import type { Snippet } from "svelte";
2
- import type { SomeMapIntegration } from "./integrations/types";
2
+ import type { MapContext } from "@sjvair/map-sdk";
3
3
  export interface MapShellProps {
4
- /** Map integrations to register, e.g. [monitorsMapIntegration] */
5
- integrations: Array<SomeMapIntegration>;
4
+ /** The map to show, from `createMap()` (the monitor map uses `createLiveMap()`). */
5
+ map: MapContext;
6
6
  /**
7
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
8
+ * load. The shell hides the load screen once the map is ready AND this
9
9
  * is true.
10
10
  */
11
11
  ready: boolean;
@@ -0,0 +1,25 @@
1
+ import type { BasemapConfig } from "@sjvair/map-sdk";
2
+ /** The basemap the map opens on, and Reset selects: Streets v4's default variant. */
3
+ export declare function defaultBasemapId(): string;
4
+ /**
5
+ * The monitor map's basemaps: every MapTiler style's variants (style URLs, so map-sdk's fallback
6
+ * can match a failed style by URL), opening on Streets v4, with no layers removed.
7
+ */
8
+ export declare function monitorMapBasemaps(): BasemapConfig;
9
+ export interface BasemapChoice {
10
+ name: string;
11
+ variants: Array<{
12
+ id: string;
13
+ type: string;
14
+ name: string;
15
+ }>;
16
+ }
17
+ /** The Basemap select's options (one per style name) and each one's Theme radios. */
18
+ export declare function basemapChoices(): BasemapChoice[];
19
+ /** The choice that has a variant with this id. */
20
+ export declare function choiceForId(id: string): BasemapChoice | undefined;
21
+ /**
22
+ * The variant to show after switching to the basemap named `toBaseName`: the same variant type
23
+ * (e.g. "DARK") as `currentId` when the new basemap offers it, otherwise its default.
24
+ */
25
+ export declare function switchBasemap(currentId: string, toBaseName: string): string;
@@ -0,0 +1,66 @@
1
+ import { MapStyle } from "@maptiler/sdk";
2
+ /** Every MapTiler style at its highest `_V<n>` version, sorted by name. */
3
+ const BASEMAP_STYLES = (() => {
4
+ const latest = new Map();
5
+ for (const style of Object.values(MapStyle)) {
6
+ const id = style.getId();
7
+ const match = id.match(/^(.+?)_V(\d+)$/i);
8
+ const baseName = match ? match[1] : id;
9
+ const version = match ? parseInt(match[2], 10) : 0;
10
+ const existing = latest.get(baseName);
11
+ if (!existing || version > existing.version) {
12
+ latest.set(baseName, { version, style });
13
+ }
14
+ }
15
+ return Array.from(latest.values())
16
+ .map((e) => e.style)
17
+ .sort((a, b) => a.getName().localeCompare(b.getName()));
18
+ })();
19
+ /** The basemap the map opens on, and Reset selects: Streets v4's default variant. */
20
+ export function defaultBasemapId() {
21
+ return MapStyle.STREETS_V4.getDefaultVariant().getId();
22
+ }
23
+ /**
24
+ * The monitor map's basemaps: every MapTiler style's variants (style URLs, so map-sdk's fallback
25
+ * can match a failed style by URL), opening on Streets v4, with no layers removed.
26
+ */
27
+ export function monitorMapBasemaps() {
28
+ const seen = new Set();
29
+ const styles = [];
30
+ for (const style of BASEMAP_STYLES) {
31
+ for (const variant of style.getVariants()) {
32
+ const id = variant.getId();
33
+ if (seen.has(id))
34
+ continue;
35
+ seen.add(id);
36
+ styles.push({ id, style: variant.getExpandedStyleURL() });
37
+ }
38
+ }
39
+ return { styles, defaultId: defaultBasemapId(), excludedLayers: [] };
40
+ }
41
+ /** The Basemap select's options (one per style name) and each one's Theme radios. */
42
+ export function basemapChoices() {
43
+ return BASEMAP_STYLES.map((style) => ({
44
+ name: style.getName(),
45
+ variants: style
46
+ .getVariants()
47
+ .map((variant) => ({ id: variant.getId(), type: variant.getType(), name: variant.getName() }))
48
+ }));
49
+ }
50
+ /** The choice that has a variant with this id. */
51
+ export function choiceForId(id) {
52
+ return basemapChoices().find((choice) => choice.variants.some((v) => v.id === id));
53
+ }
54
+ /**
55
+ * The variant to show after switching to the basemap named `toBaseName`: the same variant type
56
+ * (e.g. "DARK") as `currentId` when the new basemap offers it, otherwise its default.
57
+ */
58
+ export function switchBasemap(currentId, toBaseName) {
59
+ const target = BASEMAP_STYLES.find((style) => style.getName() === toBaseName);
60
+ if (!target)
61
+ return currentId;
62
+ const type = choiceForId(currentId)?.variants.find((v) => v.id === currentId)?.type;
63
+ if (type && target.hasVariant(type))
64
+ return target.getVariant(type).getId();
65
+ return target.getDefaultVariant().getId();
66
+ }
@@ -0,0 +1,3 @@
1
+ import { type MapContext } from "@sjvair/map-sdk";
2
+ /** The monitor map's map context: a globe over space on Streets v4, with every basemap to choose. */
3
+ export declare function createLiveMap(): MapContext;
@@ -0,0 +1,22 @@
1
+ import { createMap } from "@sjvair/map-sdk";
2
+ import { monitorMapBasemaps } from "./basemaps";
3
+ import { monitorMapConfig } from "../config.svelte.js";
4
+ /** The San Joaquin Valley view the map opens on. */
5
+ const INITIAL_VIEW = {
6
+ center: [-119.7987626619462, 36.76272050981146],
7
+ zoom: 7,
8
+ bearing: 0,
9
+ pitch: 0
10
+ };
11
+ /** The monitor map's map context: a globe over space on Streets v4, with every basemap to choose. */
12
+ export function createLiveMap() {
13
+ return createMap({
14
+ config: {
15
+ maptilerKey: monitorMapConfig().maptilerKey,
16
+ initialView: INITIAL_VIEW,
17
+ projection: "globe",
18
+ space: { preset: "milkyway" },
19
+ basemaps: monitorMapBasemaps()
20
+ }
21
+ });
22
+ }
@@ -0,0 +1,19 @@
1
+ import type { MapContext, MapPlugin, PluginOptions } from "@sjvair/map-sdk";
2
+ import { type PluginKey } from "../options/plugin-order";
3
+ /** A plugin, or a plugin with the add options it needs (e.g. its selection mode). */
4
+ export type PluginEntry = MapPlugin | {
5
+ plugin: MapPlugin;
6
+ options?: Omit<PluginOptions, "before">;
7
+ };
8
+ /**
9
+ * Builds a fresh plugin for each key (plugin objects are single-use). Called untracked. A key
10
+ * with no factory is never added, and never removed either: another `syncPlugins` call may own it.
11
+ */
12
+ export type PluginFactories = Partial<Record<PluginKey, () => PluginEntry>>;
13
+ /**
14
+ * Keeps `map`'s plugins in step with `wanted()`, in `DRAW_ORDER`: adds a wanted plugin below the
15
+ * first present plugin above it and removes one that is no longer wanted. A change to anything
16
+ * `deps()` reads removes and re-adds every present plugin of `factories` (e.g. `clustered`,
17
+ * which a plugin only takes when it is built). Call it during component setup.
18
+ */
19
+ export declare function syncPlugins(map: MapContext, wanted: () => Set<PluginKey>, factories: PluginFactories, deps?: () => unknown): void;
@@ -0,0 +1,52 @@
1
+ import { untrack } from "svelte";
2
+ import { beforeId, DRAW_ORDER } from "../options/plugin-order";
3
+ function entryOf(entry) {
4
+ return "plugin" in entry ? entry : { plugin: entry };
5
+ }
6
+ /**
7
+ * Keeps `map`'s plugins in step with `wanted()`, in `DRAW_ORDER`: adds a wanted plugin below the
8
+ * first present plugin above it and removes one that is no longer wanted. A change to anything
9
+ * `deps()` reads removes and re-adds every present plugin of `factories` (e.g. `clustered`,
10
+ * which a plugin only takes when it is built). Call it during component setup.
11
+ */
12
+ export function syncPlugins(map, wanted, factories, deps) {
13
+ const keys = DRAW_ORDER.filter((key) => factories[key]);
14
+ // Bumped when a dep changes; separate from `wanted` so an option change that leaves the
15
+ // wanted set as it is never rebuilds anything.
16
+ let generation = $state(0);
17
+ let built = 0;
18
+ if (deps) {
19
+ let first = true;
20
+ $effect(() => {
21
+ deps();
22
+ if (first)
23
+ first = false;
24
+ else
25
+ untrack(() => generation++);
26
+ });
27
+ }
28
+ $effect(() => {
29
+ const want = wanted();
30
+ const current = generation;
31
+ untrack(() => {
32
+ const rebuild = current !== built;
33
+ built = current;
34
+ // A scratch copy of the ids for this pass; nothing reads it reactively.
35
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
36
+ const present = new Set(map.plugins.map((p) => p.id));
37
+ for (const key of keys) {
38
+ if (present.has(key) && (rebuild || !want.has(key))) {
39
+ map.removePlugin(key);
40
+ present.delete(key);
41
+ }
42
+ }
43
+ for (const key of keys) {
44
+ if (!want.has(key) || present.has(key))
45
+ continue;
46
+ const { plugin, options } = entryOf(factories[key]());
47
+ map.addPlugin(plugin, { ...options, before: beforeId(key, present) });
48
+ present.add(key);
49
+ }
50
+ });
51
+ });
52
+ }
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { BellCheckIcon, BellOffIcon, ChevronDownIcon } from "@lucide/svelte";
3
- import { getSubscriptions, subscribe, unsubscribe } from "@sjvair/sdk/account";
4
- import { monitorsManager } from "../..";
3
+ import { subscribe, unsubscribe } from "@sjvair/sdk/account";
4
+ import LoadError from "../../LoadError.svelte";
5
+ import { monitorsManager } from "../monitors.svelte.js";
6
+ import { getApiToken, loadSubscriptionLevel } from "../subscriptions";
5
7
 
6
8
  interface MonitorSubscriptionManagerProps {
7
9
  monitorId: string;
@@ -21,38 +23,38 @@
21
23
  let saving = $state(false);
22
24
  let subscribedLevel: string | null = $state(null);
23
25
 
26
+ // The alert rows, in this order, from the PM2.5 scale; a key the scale lacks is skipped.
24
27
  const alertLevels = $derived.by(() => {
25
- if (!monitorsManager.meta) return [];
26
- try {
27
- return monitorsManager.meta
28
- .entryType("pm25")
29
- .asIter.levels!.filter((level) =>
30
- ALERT_LEVEL_NAMES.includes(level.name as (typeof ALERT_LEVEL_NAMES)[number])
31
- );
32
- } catch {
33
- return [];
34
- }
28
+ const levels = monitorsManager.meta?.entryType("pm25").scale?.levels ?? [];
29
+ return ALERT_LEVEL_NAMES.flatMap((key) => levels.find((level) => level.key === key) ?? []);
35
30
  });
36
31
 
32
+ let loadError = $state(false);
33
+ let attempt = $state(0);
34
+
37
35
  $effect(() => {
38
- void monitorId;
36
+ const id = monitorId;
37
+ void attempt;
39
38
  open = false;
40
39
  subscribedLevel = null;
41
- loadSubscription();
42
- });
43
-
44
- async function loadSubscription() {
45
- const id = monitorId;
40
+ loadError = false;
41
+ const controller = new AbortController();
42
+ const { signal } = controller;
46
43
  loading = true;
47
- try {
48
- const subscriptions = await getSubscriptions(getApiToken());
49
- if (id !== monitorId) return;
50
- const match = subscriptions.find((s) => s.monitor === id);
51
- subscribedLevel = match?.level ?? null;
52
- } finally {
53
- if (id === monitorId) loading = false;
54
- }
55
- }
44
+ loadSubscriptionLevel(id, { signal })
45
+ .then((level) => {
46
+ if (!signal.aborted) subscribedLevel = level;
47
+ })
48
+ .catch((error) => {
49
+ if (signal.aborted) return;
50
+ console.error("Could not load subscription status", error);
51
+ loadError = true;
52
+ })
53
+ .finally(() => {
54
+ if (!signal.aborted) loading = false;
55
+ });
56
+ return () => controller.abort();
57
+ });
56
58
 
57
59
  async function handleLevelClick(level: string | null) {
58
60
  if (saving || !level) return;
@@ -75,61 +77,59 @@
75
77
  }
76
78
  open = false;
77
79
  }
78
-
79
- function getApiToken(): string {
80
- return (
81
- (globalThis as unknown as Record<string, Record<string, string>>)["USER"]?.api_token ?? ""
82
- );
83
- }
84
80
  </script>
85
81
 
86
- <div class="relative select-none">
87
- <button
88
- class={[
89
- "flex w-full cursor-pointer items-center justify-center gap-2 rounded border px-3 py-1.5 text-sm font-semibold text-white",
90
- subscribedLevel
91
- ? "border-green-700 bg-green-600 hover:bg-green-700"
92
- : "border-blue-600 bg-blue-500 hover:bg-blue-600"
93
- ]}
94
- disabled={loading}
95
- onclick={() => (open = !open)}
96
- >
97
- {subscribedLevel ? "Manage Subscription" : "Subscribe to Alerts"}
98
- <ChevronDownIcon
99
- size={16}
100
- class={["transition-transform duration-300", open ? "rotate-180" : ""]}
101
- />
102
- </button>
82
+ {#if loadError}
83
+ <LoadError message="Couldn't load your alert settings." onretry={() => attempt++} />
84
+ {:else}
85
+ <div class="relative select-none">
86
+ <button
87
+ class={[
88
+ "flex w-full cursor-pointer items-center justify-center gap-2 rounded border px-3 py-1.5 text-sm font-semibold text-white",
89
+ subscribedLevel
90
+ ? "border-green-700 bg-green-600 hover:bg-green-700"
91
+ : "border-blue-600 bg-blue-500 hover:bg-blue-600"
92
+ ]}
93
+ disabled={loading}
94
+ onclick={() => (open = !open)}
95
+ >
96
+ {subscribedLevel ? "Manage Subscription" : "Subscribe to Alerts"}
97
+ <ChevronDownIcon
98
+ size={16}
99
+ class={["transition-transform duration-300", open ? "rotate-180" : ""]}
100
+ />
101
+ </button>
103
102
 
104
- {#if open}
105
- <button aria-label="Close" class="fixed inset-0 z-10" onclick={() => (open = false)}></button>
106
- <div class="absolute top-full left-0 z-20 mt-1 w-full overflow-hidden rounded shadow-lg">
107
- {#if subscribedLevel}
108
- <button
109
- class="flex w-full cursor-pointer items-center justify-between rounded-t border border-black/30 px-4 py-2 text-sm hover:bg-[#F5F5F3]"
110
- onclick={() => handleLevelClick(subscribedLevel)}
111
- >
112
- Unsubscribe
113
- <BellOffIcon size={14} />
114
- </button>
115
- {/if}
116
- {#each alertLevels as level (level.name)}
117
- {@const isSubscribed = level.name === subscribedLevel}
118
- <button
119
- class={[
120
- "flex w-full items-center justify-between px-4 py-2 text-sm text-white last:rounded-b",
121
- !isSubscribed ? "cursor-pointer first:rounded-t hover:brightness-90" : ""
122
- ]}
123
- style="background-color: {level.color}"
124
- disabled={saving}
125
- onclick={!isSubscribed ? () => handleLevelClick(level.name) : null}
126
- >
127
- <span>{level.label}</span>
128
- {#if isSubscribed}
129
- <BellCheckIcon size={14} />
130
- {/if}
131
- </button>
132
- {/each}
133
- </div>
134
- {/if}
135
- </div>
103
+ {#if open}
104
+ <button aria-label="Close" class="fixed inset-0 z-10" onclick={() => (open = false)}></button>
105
+ <div class="absolute top-full left-0 z-20 mt-1 w-full overflow-hidden rounded shadow-lg">
106
+ {#if subscribedLevel}
107
+ <button
108
+ class="flex w-full cursor-pointer items-center justify-between rounded-t border border-black/30 px-4 py-2 text-sm hover:bg-[#F5F5F3]"
109
+ onclick={() => handleLevelClick(subscribedLevel)}
110
+ >
111
+ Unsubscribe
112
+ <BellOffIcon size={14} />
113
+ </button>
114
+ {/if}
115
+ {#each alertLevels as level (level.key)}
116
+ {@const isSubscribed = level.key === subscribedLevel}
117
+ <button
118
+ class={[
119
+ "flex w-full items-center justify-between px-4 py-2 text-sm text-white last:rounded-b",
120
+ !isSubscribed ? "cursor-pointer first:rounded-t hover:brightness-90" : ""
121
+ ]}
122
+ style="background-color: {level.color}"
123
+ disabled={saving}
124
+ onclick={!isSubscribed ? () => handleLevelClick(level.key) : null}
125
+ >
126
+ <span>{level.label}</span>
127
+ {#if isSubscribed}
128
+ <BellCheckIcon size={14} />
129
+ {/if}
130
+ </button>
131
+ {/each}
132
+ </div>
133
+ {/if}
134
+ </div>
135
+ {/if}