@sjvair/map-sdk 0.0.0-stage → 0.1.0-beta.2

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 (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +506 -2
  3. package/dist/core/basemap.svelte.d.ts +38 -0
  4. package/dist/core/basemap.svelte.js +163 -0
  5. package/dist/core/context.d.ts +5 -0
  6. package/dist/core/context.js +13 -0
  7. package/dist/core/create-map.svelte.d.ts +207 -0
  8. package/dist/core/create-map.svelte.js +376 -0
  9. package/dist/core/gl.d.ts +92 -0
  10. package/dist/core/gl.js +1 -0
  11. package/dist/core/interactions.svelte.d.ts +43 -0
  12. package/dist/core/interactions.svelte.js +230 -0
  13. package/dist/core/plugin-host.svelte.d.ts +46 -0
  14. package/dist/core/plugin-host.svelte.js +366 -0
  15. package/dist/core/plugin.d.ts +155 -0
  16. package/dist/core/plugin.js +1 -0
  17. package/dist/core/scoped-context.d.ts +7 -0
  18. package/dist/core/scoped-context.js +34 -0
  19. package/dist/data/format.d.ts +10 -0
  20. package/dist/data/format.js +24 -0
  21. package/dist/data/index.d.ts +9 -0
  22. package/dist/data/index.js +9 -0
  23. package/dist/data/messages/en.json +52 -0
  24. package/dist/data/messages.d.ts +10 -0
  25. package/dist/data/messages.js +17 -0
  26. package/dist/data/metadata.d.ts +22 -0
  27. package/dist/data/metadata.js +110 -0
  28. package/dist/data/refs.d.ts +18 -0
  29. package/dist/data/refs.js +61 -0
  30. package/dist/data/scale-types.d.ts +7 -0
  31. package/dist/data/scale-types.js +1 -0
  32. package/dist/data/scale.d.ts +13 -0
  33. package/dist/data/scale.js +27 -0
  34. package/dist/data/selection.d.ts +7 -0
  35. package/dist/data/selection.js +31 -0
  36. package/dist/data/static-source.d.ts +4 -0
  37. package/dist/data/static-source.js +3 -0
  38. package/dist/data/types.d.ts +69 -0
  39. package/dist/data/types.js +1 -0
  40. package/dist/index.d.ts +56 -0
  41. package/dist/index.js +41 -0
  42. package/dist/plugins/icons.d.ts +21 -0
  43. package/dist/plugins/icons.js +73 -0
  44. package/dist/plugins/points-clusters.d.ts +60 -0
  45. package/dist/plugins/points-clusters.js +93 -0
  46. package/dist/plugins/points.svelte.d.ts +100 -0
  47. package/dist/plugins/points.svelte.js +501 -0
  48. package/dist/plugins/scale-resolution.svelte.d.ts +27 -0
  49. package/dist/plugins/scale-resolution.svelte.js +111 -0
  50. package/dist/plugins/shapes.svelte.d.ts +29 -0
  51. package/dist/plugins/shapes.svelte.js +307 -0
  52. package/dist/plugins/shared.d.ts +6 -0
  53. package/dist/plugins/shared.js +18 -0
  54. package/dist/plugins/style.d.ts +57 -0
  55. package/dist/plugins/style.js +193 -0
  56. package/dist/presets/CollocationTooltip.svelte +132 -0
  57. package/dist/presets/CollocationTooltip.svelte.d.ts +16 -0
  58. package/dist/presets/MonitorClusterTooltip.svelte +149 -0
  59. package/dist/presets/MonitorClusterTooltip.svelte.d.ts +16 -0
  60. package/dist/presets/MonitorReadingBox.svelte +58 -0
  61. package/dist/presets/MonitorReadingBox.svelte.d.ts +12 -0
  62. package/dist/presets/MonitorTooltip.svelte +88 -0
  63. package/dist/presets/MonitorTooltip.svelte.d.ts +20 -0
  64. package/dist/presets/calheatscore.d.ts +24 -0
  65. package/dist/presets/calheatscore.js +22 -0
  66. package/dist/presets/collocation-sites.d.ts +18 -0
  67. package/dist/presets/collocation-sites.js +67 -0
  68. package/dist/presets/fire-groups.d.ts +26 -0
  69. package/dist/presets/fire-groups.js +51 -0
  70. package/dist/presets/forecast-zones.d.ts +25 -0
  71. package/dist/presets/forecast-zones.js +23 -0
  72. package/dist/presets/hms-fire.d.ts +26 -0
  73. package/dist/presets/hms-fire.js +87 -0
  74. package/dist/presets/hms-smoke.d.ts +19 -0
  75. package/dist/presets/hms-smoke.js +20 -0
  76. package/dist/presets/monitors.d.ts +48 -0
  77. package/dist/presets/monitors.js +243 -0
  78. package/dist/presets/reactive-props.svelte.d.ts +4 -0
  79. package/dist/presets/reactive-props.svelte.js +7 -0
  80. package/dist/presets/regions.d.ts +24 -0
  81. package/dist/presets/regions.js +22 -0
  82. package/dist/stores/abort.d.ts +16 -0
  83. package/dist/stores/abort.js +52 -0
  84. package/dist/stores/collocation-store.svelte.d.ts +25 -0
  85. package/dist/stores/collocation-store.svelte.js +72 -0
  86. package/dist/stores/hms-stores.svelte.d.ts +35 -0
  87. package/dist/stores/hms-stores.svelte.js +97 -0
  88. package/dist/stores/index.d.ts +4 -0
  89. package/dist/stores/index.js +4 -0
  90. package/dist/stores/load-once.svelte.d.ts +25 -0
  91. package/dist/stores/load-once.svelte.js +93 -0
  92. package/dist/stores/monitors-store.svelte.d.ts +85 -0
  93. package/dist/stores/monitors-store.svelte.js +340 -0
  94. package/dist/theme.css +34 -0
  95. package/dist/ui/FeatureList.svelte +505 -0
  96. package/dist/ui/FeatureList.svelte.d.ts +14 -0
  97. package/dist/ui/Legend.svelte +213 -0
  98. package/dist/ui/Legend.svelte.d.ts +22 -0
  99. package/dist/ui/LegendBar.svelte +161 -0
  100. package/dist/ui/LegendBar.svelte.d.ts +11 -0
  101. package/dist/ui/MapControls.svelte +135 -0
  102. package/dist/ui/MapControls.svelte.d.ts +11 -0
  103. package/dist/ui/MapTooltip.svelte +158 -0
  104. package/dist/ui/MapTooltip.svelte.d.ts +28 -0
  105. package/dist/ui/SelectionBar.svelte +123 -0
  106. package/dist/ui/SelectionBar.svelte.d.ts +10 -0
  107. package/dist/ui/TooltipItem.svelte +72 -0
  108. package/dist/ui/TooltipItem.svelte.d.ts +11 -0
  109. package/dist/ui/ViewSummary.svelte +97 -0
  110. package/dist/ui/ViewSummary.svelte.d.ts +7 -0
  111. package/dist/ui/controls.css +45 -0
  112. package/dist/ui/data-credits.svelte.d.ts +9 -0
  113. package/dist/ui/data-credits.svelte.js +52 -0
  114. package/dist/ui/feature-list.d.ts +38 -0
  115. package/dist/ui/feature-list.js +95 -0
  116. package/dist/ui/legend-data.d.ts +41 -0
  117. package/dist/ui/legend-data.js +40 -0
  118. package/dist/ui/level-chip.d.ts +11 -0
  119. package/dist/ui/level-chip.js +15 -0
  120. package/dist/ui/value-format.d.ts +7 -0
  121. package/dist/ui/value-format.js +13 -0
  122. package/dist/ui/view-summary.d.ts +11 -0
  123. package/dist/ui/view-summary.js +33 -0
  124. package/dist/view/MapView.svelte +373 -0
  125. package/dist/view/MapView.svelte.d.ts +43 -0
  126. package/dist/view/create-gl-map.d.ts +34 -0
  127. package/dist/view/create-gl-map.js +182 -0
  128. package/dist/view/list-placement.d.ts +21 -0
  129. package/dist/view/list-placement.js +13 -0
  130. package/package.json +103 -5
@@ -0,0 +1,158 @@
1
+ <script lang="ts">
2
+ import { untrack } from "svelte";
3
+ import type { MapContext } from "../core/create-map.svelte.js";
4
+ import type { GlMapLike } from "../core/gl.js";
5
+ import type { FeatureListItem, MapPlugin } from "../core/plugin.js";
6
+ import TooltipItem from "./TooltipItem.svelte";
7
+
8
+ interface Props {
9
+ map: MapContext;
10
+ /**
11
+ * Canvas pixel for a position, or `null` while there is no GL map. Reactive: it should
12
+ * read something that changes when the camera moves, so the tooltip follows the map.
13
+ */
14
+ project: (lngLat: [number, number]) => { x: number; y: number } | null;
15
+ /** The map container's size; the tooltip stays inside it. Unset: no clamping. */
16
+ width?: number;
17
+ height?: number;
18
+ /**
19
+ * `"pinned"` (default) anchors the tooltip to its feature; `"pointer"` shows it at the last
20
+ * pointer position and hides it on `gl`'s `zoomstart` until the next pointer move over a
21
+ * hit.
22
+ * A keyboard-focused list item is always pinned.
23
+ */
24
+ mode?: "pinned" | "pointer";
25
+ /** The GL map, for `zoomstart` in `"pointer"` mode. */
26
+ gl?: Pick<GlMapLike, "on" | "off"> | null;
27
+ }
28
+
29
+ let { map, project, width, height, mode = "pinned", gl = null }: Props = $props();
30
+
31
+ // Pointer mode only: set by a zoom, cleared by the next pointer move over a hit.
32
+ let zoomHidden = $state(false);
33
+ $effect(() => {
34
+ if (mode !== "pointer" || !gl) return;
35
+ const onZoomStart = () => (zoomHidden = true);
36
+ gl.on("zoomstart", onZoomStart);
37
+ return () => gl.off("zoomstart", onZoomStart);
38
+ });
39
+ $effect(() => {
40
+ // A new hovered object comes with every pointer move over a hit.
41
+ if (map.hovered) zoomHidden = false;
42
+ });
43
+
44
+ let tipWidth = $state(0);
45
+ let tipHeight = $state(0);
46
+ const GAP = 12;
47
+
48
+ // The keyboard-focused list item wins over the pointer.
49
+ const key = $derived(map.focusedKey ?? map.hovered?.hit.refKey ?? null);
50
+ // Primitives, so moving the pointer within one feature doesn't recompute the content.
51
+ const hitPluginId = $derived(map.focusedKey ? null : (map.hovered?.hit.pluginId ?? null));
52
+ const hitName = $derived(map.focusedKey ? null : (map.hovered?.hit.name ?? null));
53
+
54
+ // Reads only the shown feature's plugin (or all plugins for a focused key), and only while
55
+ // something is shown, so an idle map doesn't subscribe to any plugin's data.
56
+ const found = $derived.by((): { item: FeatureListItem; plugin: MapPlugin } | null => {
57
+ if (key === null) return null;
58
+ // The hovered hit's plugin, or the focused one; without either, the first plugin listing the key.
59
+ const pluginId = hitPluginId ?? map.focusedPluginId;
60
+ const plugins = pluginId ? map.plugins.filter((p) => p.id === pluginId) : map.plugins;
61
+ for (const plugin of plugins) {
62
+ const item = plugin.features?.().find((f) => f.refKey === key);
63
+ if (item) return { item, plugin };
64
+ }
65
+ return null;
66
+ });
67
+
68
+ const item = $derived(found?.item ?? null);
69
+ const name = $derived(item?.name ?? hitName);
70
+ // A found item anchors the tooltip at its point (stable while the pointer moves over it).
71
+ // In pointer mode a hovered hit is anchored at the pointer instead (a focused item stays
72
+ // pinned).
73
+ const anchor = $derived(
74
+ mode === "pointer" && !map.focusedKey
75
+ ? (map.hovered?.lngLat ?? null)
76
+ : (item?.lngLat ?? map.hovered?.lngLat ?? null)
77
+ );
78
+ const projected = $derived(anchor ? project(anchor) : null);
79
+ // Centered above the point; flipped below it near the top edge; inside the container.
80
+ const position = $derived.by(() => {
81
+ if (!projected) return null;
82
+ let { x, y } = projected;
83
+ if (width)
84
+ x = Math.min(Math.max(x, tipWidth / 2), Math.max(width - tipWidth / 2, tipWidth / 2));
85
+ let top = y - tipHeight - GAP;
86
+ if (top < 0) top = y + GAP;
87
+ if (height) top = Math.min(top, Math.max(height - tipHeight, 0));
88
+ return { left: x - tipWidth / 2, top: Math.max(top, 0) };
89
+ });
90
+ const custom = $derived.by(() => {
91
+ if (found) return found.plugin.tooltip?.(found.item);
92
+ // A hovered hit no plugin lists (e.g. a cluster bubble): its plugin's hover content.
93
+ if (key === null || hitPluginId === null) return undefined;
94
+ const plugin = map.plugins.find((p) => p.id === hitPluginId);
95
+ const hit = untrack(() => map.hovered?.hit);
96
+ return hit && plugin?.hoverTooltip?.(hit);
97
+ });
98
+
99
+ // The name header is left out when the plugin's own tooltip content shows the name.
100
+ const showName = $derived.by(() => {
101
+ if (!name) return false;
102
+ if (!custom) return true;
103
+ const plugin = found?.plugin ?? map.plugins.find((p) => p.id === hitPluginId);
104
+ return !plugin?.tooltipShowsName;
105
+ });
106
+ </script>
107
+
108
+ <!-- Only with something to show: a hit with no name, item, or content gets no empty box. -->
109
+ {#if (name || item || custom) && position && !(zoomHidden && !map.focusedKey)}
110
+ <div
111
+ class="mapsdk-tooltip text-sm"
112
+ role="tooltip"
113
+ bind:offsetWidth={tipWidth}
114
+ bind:offsetHeight={tipHeight}
115
+ style:left="{position.left}px"
116
+ style:top="{position.top}px"
117
+ >
118
+ {#if showName}
119
+ <div class="mapsdk-tooltip-name">{name}</div>
120
+ {/if}
121
+ {#if custom}
122
+ {@render custom()}
123
+ {:else if item}
124
+ <TooltipItem {item} messages={map.messages} format={map.format} />
125
+ {/if}
126
+ </div>
127
+ {/if}
128
+
129
+ <style>
130
+ .mapsdk-tooltip {
131
+ position: absolute;
132
+ z-index: 1;
133
+ display: flex;
134
+ flex-direction: column;
135
+ align-items: flex-start;
136
+ gap: 0.25rem;
137
+ max-width: 16rem;
138
+ box-sizing: border-box;
139
+ padding: 0.5rem 0.75rem;
140
+ border: 1px solid var(--card-border, var(--border));
141
+ border-radius: var(--radius-control, var(--radius));
142
+ background: var(--popover, var(--background));
143
+ color: var(--popover-foreground, var(--foreground));
144
+ box-shadow: var(--shadow-lifted, var(--shadow-card, none));
145
+ pointer-events: none;
146
+ }
147
+
148
+ /* The monitor, cluster, and collocation tooltips (today's ~420px cards) mark their content
149
+ `mapsdk-tooltip-wide`: wide enough for a reading box and name, never wider than the map. */
150
+ .mapsdk-tooltip:has(:global(.mapsdk-tooltip-wide)) {
151
+ width: max-content;
152
+ max-width: min(28rem, calc(100% - 1rem));
153
+ }
154
+
155
+ .mapsdk-tooltip-name {
156
+ font-weight: 600;
157
+ }
158
+ </style>
@@ -0,0 +1,28 @@
1
+ import type { MapContext } from "../core/create-map.svelte.js";
2
+ import type { GlMapLike } from "../core/gl.js";
3
+ interface Props {
4
+ map: MapContext;
5
+ /**
6
+ * Canvas pixel for a position, or `null` while there is no GL map. Reactive: it should
7
+ * read something that changes when the camera moves, so the tooltip follows the map.
8
+ */
9
+ project: (lngLat: [number, number]) => {
10
+ x: number;
11
+ y: number;
12
+ } | null;
13
+ /** The map container's size; the tooltip stays inside it. Unset: no clamping. */
14
+ width?: number;
15
+ height?: number;
16
+ /**
17
+ * `"pinned"` (default) anchors the tooltip to its feature; `"pointer"` shows it at the last
18
+ * pointer position and hides it on `gl`'s `zoomstart` until the next pointer move over a
19
+ * hit.
20
+ * A keyboard-focused list item is always pinned.
21
+ */
22
+ mode?: "pinned" | "pointer";
23
+ /** The GL map, for `zoomstart` in `"pointer"` mode. */
24
+ gl?: Pick<GlMapLike, "on" | "off"> | null;
25
+ }
26
+ declare const MapTooltip: import("svelte").Component<Props, {}, "">;
27
+ type MapTooltip = ReturnType<typeof MapTooltip>;
28
+ export default MapTooltip;
@@ -0,0 +1,123 @@
1
+ <script lang="ts">
2
+ import type { MapContext } from "../core/create-map.svelte.js";
3
+ import "./controls.css";
4
+
5
+ interface Props {
6
+ map: MapContext;
7
+ /** Leaves room at the right for a top-right List button, as the bar does at the left. */
8
+ clearRight?: boolean;
9
+ }
10
+
11
+ let { map, clearRight = false }: Props = $props();
12
+
13
+ let bar: HTMLElement | undefined = $state();
14
+
15
+ // Every id counts, whether or not it is in the data: a host may select features that load later.
16
+ const entries = $derived(
17
+ map.plugins.flatMap((plugin) => {
18
+ const count = map.getSelection(plugin.id)?.ids.length ?? 0;
19
+ return count > 0 ? [{ id: plugin.id, layer: plugin.label ?? plugin.id, count }] : [];
20
+ })
21
+ );
22
+
23
+ /**
24
+ * Clears one plugin's selection. The button is about to go (or its row is), so focus moves
25
+ * first: to another plugin's Clear button, else the List button, else the map region.
26
+ */
27
+ function clear(id: string, button: HTMLElement) {
28
+ const another = [...(bar?.querySelectorAll<HTMLElement>("button") ?? [])].find(
29
+ (b) => b !== button
30
+ );
31
+ const region = bar?.closest<HTMLElement>("[data-mapsdk-map]");
32
+ const target = another ?? region?.querySelector<HTMLElement>(".mapsdk-list-button") ?? region;
33
+ if (target === region && region && !region.hasAttribute("tabindex")) {
34
+ region.setAttribute("tabindex", "-1");
35
+ }
36
+ target?.focus();
37
+ map.clearSelection(id);
38
+ }
39
+ </script>
40
+
41
+ {#if entries.length > 0}
42
+ <div
43
+ bind:this={bar}
44
+ class={["mapsdk-selection text-sm", clearRight && "mapsdk-selection-clear-right"]}
45
+ role="group"
46
+ aria-label={map.messages("mapsdk_selection_label")}
47
+ >
48
+ {#each entries as entry (entry.id)}
49
+ {@const selected = map.messages("mapsdk_selected_count", { count: entry.count })}
50
+ <div class="mapsdk-selection-row">
51
+ <span>
52
+ {entries.length > 1
53
+ ? map.messages("mapsdk_selected_in_layer", { layer: entry.layer, selected })
54
+ : selected}
55
+ </span>
56
+ <button
57
+ type="button"
58
+ class="mapsdk-focus mapsdk-motion"
59
+ aria-label={map.messages("mapsdk_selection_clear_layer", { layer: entry.layer })}
60
+ onclick={(e) => clear(entry.id, e.currentTarget)}
61
+ >
62
+ {map.messages("mapsdk_selection_clear")}
63
+ </button>
64
+ </div>
65
+ {/each}
66
+ </div>
67
+ {/if}
68
+
69
+ <style>
70
+ .mapsdk-selection {
71
+ position: absolute;
72
+ top: 0.75rem;
73
+ /* Clear of the List button (left) and the zoom buttons (right); centered between them. */
74
+ right: 4rem;
75
+ left: 5rem;
76
+ z-index: 2;
77
+ display: flex;
78
+ flex-direction: column;
79
+ gap: 0.25rem;
80
+ box-sizing: border-box;
81
+ width: fit-content;
82
+ margin-inline: auto;
83
+ padding: 0.375rem 0.5rem 0.375rem 0.75rem;
84
+ border: 1px solid var(--card-border, var(--border));
85
+ border-radius: var(--radius-control, var(--radius));
86
+ background: var(--background);
87
+ color: var(--foreground);
88
+ box-shadow: var(--shadow-card, none);
89
+ pointer-events: auto;
90
+ }
91
+
92
+ .mapsdk-selection-clear-right {
93
+ right: 5rem;
94
+ }
95
+
96
+ .mapsdk-selection-row {
97
+ display: flex;
98
+ flex-wrap: wrap;
99
+ align-items: center;
100
+ justify-content: space-between;
101
+ gap: 0.25rem 0.75rem;
102
+ }
103
+
104
+ .mapsdk-selection-row span {
105
+ min-width: 0;
106
+ overflow-wrap: anywhere;
107
+ }
108
+
109
+ button {
110
+ flex: none;
111
+ padding: 0.25rem 0.625rem;
112
+ border: 1px solid var(--border);
113
+ border-radius: var(--radius-control, var(--radius));
114
+ background: var(--background);
115
+ color: var(--foreground);
116
+ font: inherit;
117
+ cursor: pointer;
118
+ }
119
+
120
+ button:hover {
121
+ background: var(--accent, var(--muted));
122
+ }
123
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { MapContext } from "../core/create-map.svelte.js";
2
+ import "./controls.css";
3
+ interface Props {
4
+ map: MapContext;
5
+ /** Leaves room at the right for a top-right List button, as the bar does at the left. */
6
+ clearRight?: boolean;
7
+ }
8
+ declare const SelectionBar: import("svelte").Component<Props, {}, "">;
9
+ type SelectionBar = ReturnType<typeof SelectionBar>;
10
+ export default SelectionBar;
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ import type { FormatProvider } from "../data/format.js";
3
+ import type { MessageProvider } from "../data/messages.js";
4
+ import type { FeatureListItem } from "../core/plugin.js";
5
+ import { NO_LEVEL } from "../data/scale.js";
6
+ import { levelText } from "./feature-list.js";
7
+ import { levelChip } from "./level-chip.js";
8
+ import { formatValue } from "./value-format.js";
9
+
10
+ // The generic tooltip body under a feature's name: its value and a chip with its level. Shared
11
+ // by MapTooltip and by plugins that show it for features they don't list.
12
+
13
+ interface Props {
14
+ item: FeatureListItem;
15
+ messages: MessageProvider;
16
+ format: FormatProvider;
17
+ }
18
+
19
+ let { item, messages, format }: Props = $props();
20
+
21
+ // A level with an empty color shows its label on the neutral chip.
22
+ const level = $derived(item.level !== NO_LEVEL ? item.level : null);
23
+ // The level's label, else the item's detail (plain text, no chip), else "No level".
24
+ const levelPlace = $derived(levelText(item, messages));
25
+ const chipColors = $derived.by(() => {
26
+ const chip = levelChip(level?.color);
27
+ return chip ? `background:${chip.background};color:${chip.text}` : "";
28
+ });
29
+ const valueText = $derived(
30
+ item.value == null
31
+ ? null
32
+ : typeof item.value === "number"
33
+ ? formatValue(item.value, item.precision, format)
34
+ : item.value
35
+ );
36
+ </script>
37
+
38
+ <div class="mapsdk-tooltip-item">
39
+ {#if valueText !== null}
40
+ <div class="mapsdk-tooltip-value">
41
+ {valueText}{#if item.units}&nbsp;{item.units}{/if}
42
+ </div>
43
+ {/if}
44
+ {#if levelPlace.kind === "detail"}
45
+ <span class="mapsdk-tooltip-detail text-xs">{levelPlace.text}</span>
46
+ {:else}
47
+ <span class="mapsdk-tooltip-chip text-xs" class:neutral={!chipColors} style={chipColors}>
48
+ {levelPlace.text}
49
+ </span>
50
+ {/if}
51
+ </div>
52
+
53
+ <style>
54
+ .mapsdk-tooltip-item {
55
+ display: flex;
56
+ flex-direction: column;
57
+ align-items: flex-start;
58
+ gap: 0.25rem;
59
+ }
60
+
61
+ .mapsdk-tooltip-chip {
62
+ padding: 0.0625rem 0.5rem;
63
+ border: var(--swatch-outline-width, 1px) solid
64
+ var(--swatch-outline-color, color-mix(in srgb, var(--foreground) 75%, var(--background)));
65
+ border-radius: 999px;
66
+ }
67
+
68
+ .mapsdk-tooltip-chip.neutral {
69
+ background: var(--muted);
70
+ color: var(--muted-foreground);
71
+ }
72
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { FormatProvider } from "../data/format.js";
2
+ import type { MessageProvider } from "../data/messages.js";
3
+ import type { FeatureListItem } from "../core/plugin.js";
4
+ interface Props {
5
+ item: FeatureListItem;
6
+ messages: MessageProvider;
7
+ format: FormatProvider;
8
+ }
9
+ declare const TooltipItem: import("svelte").Component<Props, {}, "">;
10
+ type TooltipItem = ReturnType<typeof TooltipItem>;
11
+ export default TooltipItem;
@@ -0,0 +1,97 @@
1
+ <script lang="ts">
2
+ import { untrack } from "svelte";
3
+ import type { MapContext } from "../core/create-map.svelte.js";
4
+ import { inView } from "./feature-list.js";
5
+ import { SUMMARY_DEBOUNCE_MS, summaryText } from "./view-summary.js";
6
+
7
+ let { map }: { map: MapContext } = $props();
8
+
9
+ interface Line {
10
+ /** Stable per plugin and kind, so an unchanged line keeps its element and isn't re-announced. */
11
+ key: string;
12
+ text: string;
13
+ }
14
+
15
+ // The map's one live region: what is in view per layer, and how many are selected. Spoken text
16
+ // only; the visible pieces are the legend, the list, and the selection bar. Lines are keyed and
17
+ // the region is not atomic, so only a line that changed is announced.
18
+ /** Each plugin's selection count as of the last update, to tell when one drops to zero. */
19
+ let previous: Record<string, number> = {};
20
+
21
+ // Selection counts of every plugin, so a change reschedules the update.
22
+ const selectionCounts = $derived(
23
+ map.plugins.map((p) => `${p.id}:${map.getSelection(p.id)?.ids.length ?? 0}`).join(",")
24
+ );
25
+
26
+ function compute(): Line[] {
27
+ const bounds = map.bounds;
28
+ const out: Line[] = [];
29
+ const counts: Record<string, number> = {};
30
+ for (const plugin of map.plugins) {
31
+ const layer = plugin.label ?? plugin.id;
32
+ if (bounds && plugin.features) {
33
+ const data = plugin.legend?.();
34
+ const scale = data && "scale" in data ? data.scale : null;
35
+ out.push({
36
+ key: `view:${plugin.id}`,
37
+ text: summaryText(layer, inView(plugin.features(), bounds), scale, map.messages)
38
+ });
39
+ }
40
+ const count = map.getSelection(plugin.id)?.ids.length ?? 0;
41
+ counts[plugin.id] = count;
42
+ // A drop to zero is said once; the line is gone at the next update.
43
+ const selected =
44
+ count > 0
45
+ ? map.messages("mapsdk_selected_count", { count })
46
+ : (previous[plugin.id] ?? 0) > 0
47
+ ? map.messages("mapsdk_selection_cleared")
48
+ : null;
49
+ if (selected !== null) {
50
+ out.push({
51
+ key: `selection:${plugin.id}`,
52
+ text: map.messages("mapsdk_selected_in_layer", { layer, selected })
53
+ });
54
+ }
55
+ }
56
+ previous = counts;
57
+ return out;
58
+ }
59
+
60
+ // What is there at mount (the host's view and selection) is the baseline, not news.
61
+ let lines = $state<Line[]>(untrack(compute));
62
+
63
+ let first = true;
64
+ $effect(() => {
65
+ void map.bounds;
66
+ void selectionCounts;
67
+ if (first) {
68
+ first = false;
69
+ return;
70
+ }
71
+ const timer = setTimeout(() => {
72
+ lines = untrack(compute);
73
+ }, SUMMARY_DEBOUNCE_MS);
74
+ return () => clearTimeout(timer);
75
+ });
76
+ </script>
77
+
78
+ <div class="mapsdk-summary" aria-live="polite" aria-atomic="false">
79
+ {#each lines as line (line.key)}
80
+ <p>{line.text}</p>
81
+ {/each}
82
+ </div>
83
+
84
+ <style>
85
+ .mapsdk-summary {
86
+ position: absolute;
87
+ width: 1px;
88
+ height: 1px;
89
+ overflow: hidden;
90
+ clip-path: inset(50%);
91
+ white-space: nowrap;
92
+ }
93
+
94
+ p {
95
+ margin: 0;
96
+ }
97
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { MapContext } from "../core/create-map.svelte.js";
2
+ type $$ComponentProps = {
3
+ map: MapContext;
4
+ };
5
+ declare const ViewSummary: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ViewSummary = ReturnType<typeof ViewSummary>;
7
+ export default ViewSummary;
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Shared by map-sdk's controls (plain CSS, not scoped: the classes are set by several components).
3
+ *
4
+ * Focus ring, only on :focus-visible: a 2px --ring outline with a --ring-edge halo, so the ring
5
+ * reads against any background. A host without --ring-edge gets the page background.
6
+ */
7
+ /* Doubled classes raise specificity above any component rule that sets box-shadow (a card
8
+ shadow, a selected row's ring), so the result never depends on bundle order. */
9
+ .mapsdk-focus.mapsdk-focus:focus-visible {
10
+ outline: 2px solid var(--ring);
11
+ outline-offset: 1px;
12
+ box-shadow: 0 0 0 4px var(--ring-edge, var(--background));
13
+ }
14
+
15
+ /* For controls and rows inside a container that clips: the ring is drawn inside. */
16
+ .mapsdk-focus-inset.mapsdk-focus-inset:focus-visible,
17
+ .mapsdk-focus-inset-active:focus-visible .mapsdk-active-option.mapsdk-active-option {
18
+ outline: 2px solid var(--ring);
19
+ outline-offset: -3px;
20
+ box-shadow: inset 0 0 0 4px var(--ring-edge, var(--background));
21
+ }
22
+
23
+ /* Elements only script focuses (tabindex="-1"): the outline alone. */
24
+ .mapsdk-focus-outline:focus-visible {
25
+ outline: 2px solid var(--ring);
26
+ outline-offset: -2px;
27
+ }
28
+
29
+ /* Motion goes through the host's tokens; the fallbacks apply when it defines none. */
30
+ .mapsdk-motion {
31
+ transition-property: background-color, border-color, color, opacity;
32
+ transition-duration: var(--motion-duration-fast, 120ms);
33
+ transition-timing-function: var(--motion-ease-standard, ease);
34
+ }
35
+
36
+ @media (prefers-reduced-motion: reduce) {
37
+ .mapsdk-motion {
38
+ transition-duration: 0s;
39
+ }
40
+ }
41
+
42
+ /* The engine's credits keep its colors; links get an underline so color isn't the only cue. */
43
+ .mapsdk-map .maplibregl-ctrl-attrib a {
44
+ text-decoration: underline;
45
+ }
@@ -0,0 +1,9 @@
1
+ import type { MapContext } from "../core/create-map.svelte.js";
2
+ /**
3
+ * The credit text of every dataset the map's plugins declare, each once, as it arrives from the
4
+ * metadata provider. Call during a component's initialization: the fetching runs in an effect
5
+ * and stops with the component.
6
+ */
7
+ export declare function createDataCredits(map: MapContext): {
8
+ readonly list: string[];
9
+ };
@@ -0,0 +1,52 @@
1
+ /* eslint-disable svelte/prefer-svelte-reactivity -- derived sets only dedupe; never mutated */
2
+ import { onDestroy, untrack } from "svelte";
3
+ /** An unavailable attribution is tried again on a later moveend, at most once a minute. */
4
+ const RETRY_MS = 60_000;
5
+ /**
6
+ * The credit text of every dataset the map's plugins declare, each once, as it arrives from the
7
+ * metadata provider. Call during a component's initialization: the fetching runs in an effect
8
+ * and stops with the component.
9
+ */
10
+ export function createDataCredits(map) {
11
+ const datasets = $derived([
12
+ ...new Set(map.plugins.flatMap((plugin) => (plugin.dataset ? [plugin.dataset] : [])))
13
+ ]);
14
+ let fetched = $state.raw({});
15
+ const list = $derived([...new Set(datasets.flatMap((d) => (fetched[d] ? [fetched[d]] : [])))]);
16
+ const controller = new AbortController();
17
+ onDestroy(() => controller.abort());
18
+ // Plain collections: bookkeeping, never rendered.
19
+ const inflight = new Set();
20
+ const failedAt = new Map();
21
+ $effect(() => {
22
+ void map.bounds;
23
+ const wanted = datasets;
24
+ untrack(() => {
25
+ for (const dataset of wanted) {
26
+ if (fetched[dataset] || inflight.has(dataset))
27
+ continue;
28
+ const failed = failedAt.get(dataset);
29
+ if (failed !== undefined && Date.now() - failed < RETRY_MS)
30
+ continue;
31
+ inflight.add(dataset);
32
+ map.metadata.attribution(dataset, controller.signal).then((text) => {
33
+ inflight.delete(dataset);
34
+ if (controller.signal.aborted)
35
+ return;
36
+ if (text === "unavailable")
37
+ failedAt.set(dataset, Date.now());
38
+ else
39
+ fetched = { ...fetched, [dataset]: text };
40
+ }, () => {
41
+ inflight.delete(dataset);
42
+ failedAt.set(dataset, Date.now());
43
+ });
44
+ }
45
+ });
46
+ });
47
+ return {
48
+ get list() {
49
+ return list;
50
+ }
51
+ };
52
+ }
@@ -0,0 +1,38 @@
1
+ import type { Bounds } from "../core/create-map.svelte.js";
2
+ import type { FeatureListItem } from "../core/plugin.js";
3
+ import type { FormatProvider } from "../data/format.js";
4
+ import type { MessageProvider } from "../data/messages.js";
5
+ import type { Level } from "../data/scale-types.js";
6
+ export type { Bounds };
7
+ export type SortBy = "name" | "value";
8
+ /**
9
+ * How long after Shift+F10 or the Menu key the browser's own keyboard `contextmenu` event (sent on
10
+ * keydown or keyup, depending on the platform) is treated as a duplicate of the one sent to the row.
11
+ */
12
+ export declare const MENU_EVENT_WINDOW_MS = 1000;
13
+ /**
14
+ * A valid, stable id fragment for a row, from its plugin and refKey: anything but letters and digits
15
+ * is escaped as `_<hex>_` (`_` and `-` included), so the `--` between the parts can only be the
16
+ * separator and different rows never share a fragment.
17
+ */
18
+ export declare function optionIdPart(pluginId: string, refKey: string): string;
19
+ /** Items whose point is inside `bounds`, edges included. No antimeridian handling (the Valley). */
20
+ export declare function inView(items: FeatureListItem[], bounds: Bounds): FeatureListItem[];
21
+ /**
22
+ * The item's level, or null for `NO_LEVEL`. A level with an empty color is still shown, by its
23
+ * label on a neutral swatch.
24
+ */
25
+ export declare function shownLevel(item: FeatureListItem): Level | null;
26
+ /** A sorted copy. `value`: highest first, null values and no level last; ties and `name` by name. */
27
+ export declare function sortItems(items: FeatureListItem[], by: SortBy): FeatureListItem[];
28
+ /** The first index at or after `from` (wrapping) whose name starts with `query`, any case; -1 if none. */
29
+ export declare function typeaheadIndex(items: FeatureListItem[], query: string, from: number): number;
30
+ /**
31
+ * What stands in an item's level place: its level's label, else its `detail`, else "No level".
32
+ */
33
+ export declare function levelText(item: FeatureListItem, messages: MessageProvider): {
34
+ kind: "level" | "detail" | "none";
35
+ text: string;
36
+ };
37
+ /** A row's accessible name: name, value, units, and level label (or detail). */
38
+ export declare function rowLabel(item: FeatureListItem, messages: MessageProvider, format: FormatProvider): string;