@sjvair/map-sdk 0.0.0-stage → 0.1.0-beta.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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +487 -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 +42 -0
  12. package/dist/core/interactions.svelte.js +213 -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 +148 -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 +51 -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 +93 -0
  47. package/dist/plugins/points.svelte.js +473 -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 +501 -0
  96. package/dist/ui/FeatureList.svelte.d.ts +14 -0
  97. package/dist/ui/Legend.svelte +172 -0
  98. package/dist/ui/Legend.svelte.d.ts +13 -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 +66 -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 +31 -0
  115. package/dist/ui/feature-list.js +80 -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,66 @@
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 { levelChip } from "./level-chip.js";
7
+ import { formatValue } from "./value-format.js";
8
+
9
+ // The generic tooltip body under a feature's name: its value and a chip with its level. Shared
10
+ // by MapTooltip and by plugins that show it for features they don't list.
11
+
12
+ interface Props {
13
+ item: FeatureListItem;
14
+ messages: MessageProvider;
15
+ format: FormatProvider;
16
+ }
17
+
18
+ let { item, messages, format }: Props = $props();
19
+
20
+ // A level with an empty color shows its label on the neutral chip.
21
+ const level = $derived(item.level !== NO_LEVEL ? item.level : null);
22
+ const levelLabel = $derived(level ? level.label : messages("mapsdk_no_level"));
23
+ const chipColors = $derived.by(() => {
24
+ const chip = levelChip(level?.color);
25
+ return chip ? `background:${chip.background};color:${chip.text}` : "";
26
+ });
27
+ const valueText = $derived(
28
+ item.value == null
29
+ ? null
30
+ : typeof item.value === "number"
31
+ ? formatValue(item.value, item.precision, format)
32
+ : item.value
33
+ );
34
+ </script>
35
+
36
+ <div class="mapsdk-tooltip-item">
37
+ {#if valueText !== null}
38
+ <div class="mapsdk-tooltip-value">
39
+ {valueText}{#if item.units}&nbsp;{item.units}{/if}
40
+ </div>
41
+ {/if}
42
+ <span class="mapsdk-tooltip-chip text-xs" class:neutral={!chipColors} style={chipColors}>
43
+ {levelLabel}
44
+ </span>
45
+ </div>
46
+
47
+ <style>
48
+ .mapsdk-tooltip-item {
49
+ display: flex;
50
+ flex-direction: column;
51
+ align-items: flex-start;
52
+ gap: 0.25rem;
53
+ }
54
+
55
+ .mapsdk-tooltip-chip {
56
+ padding: 0.0625rem 0.5rem;
57
+ border: var(--swatch-outline-width, 1px) solid
58
+ var(--swatch-outline-color, color-mix(in srgb, var(--foreground) 75%, var(--background)));
59
+ border-radius: 999px;
60
+ }
61
+
62
+ .mapsdk-tooltip-chip.neutral {
63
+ background: var(--muted);
64
+ color: var(--muted-foreground);
65
+ }
66
+ </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,31 @@
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
+ /** A row's accessible name: name, value, units, and level label. */
31
+ export declare function rowLabel(item: FeatureListItem, messages: MessageProvider, format: FormatProvider): string;
@@ -0,0 +1,80 @@
1
+ import { NO_LEVEL } from "../data/scale.js";
2
+ import { formatValue } from "./value-format.js";
3
+ /**
4
+ * How long after Shift+F10 or the Menu key the browser's own keyboard `contextmenu` event (sent on
5
+ * keydown or keyup, depending on the platform) is treated as a duplicate of the one sent to the row.
6
+ */
7
+ export const MENU_EVENT_WINDOW_MS = 1000;
8
+ /**
9
+ * A valid, stable id fragment for a row, from its plugin and refKey: anything but letters and digits
10
+ * is escaped as `_<hex>_` (`_` and `-` included), so the `--` between the parts can only be the
11
+ * separator and different rows never share a fragment.
12
+ */
13
+ export function optionIdPart(pluginId, refKey) {
14
+ const escape = (text) => text.replace(/[^A-Za-z0-9]/g, (c) => `_${c.codePointAt(0).toString(16)}_`);
15
+ return `${escape(pluginId)}--${escape(refKey)}`;
16
+ }
17
+ /** Items whose point is inside `bounds`, edges included. No antimeridian handling (the Valley). */
18
+ export function inView(items, bounds) {
19
+ return items.filter(({ lngLat: [lng, lat] }) => {
20
+ return lng >= bounds.west && lng <= bounds.east && lat >= bounds.south && lat <= bounds.north;
21
+ });
22
+ }
23
+ /**
24
+ * The item's level, or null for `NO_LEVEL`. A level with an empty color is still shown, by its
25
+ * label on a neutral swatch.
26
+ */
27
+ export function shownLevel(item) {
28
+ return item.level !== NO_LEVEL ? item.level : null;
29
+ }
30
+ const collator = new Intl.Collator("en-US");
31
+ /** A value's sort weight; `null` sorts last. Category values sort by their level's tier. */
32
+ function weight(item) {
33
+ if (item.value === null || item.level === NO_LEVEL)
34
+ return null;
35
+ if (typeof item.value === "number")
36
+ return item.value;
37
+ return item.level.tier ?? null;
38
+ }
39
+ /** A sorted copy. `value`: highest first, null values and no level last; ties and `name` by name. */
40
+ export function sortItems(items, by) {
41
+ const byName = (a, b) => collator.compare(a.name, b.name);
42
+ if (by === "name")
43
+ return items.toSorted(byName);
44
+ const weighted = items.map((item) => ({ item, w: weight(item) }));
45
+ weighted.sort((a, b) => {
46
+ if (a.w === null || b.w === null) {
47
+ if (a.w !== b.w)
48
+ return a.w === null ? 1 : -1;
49
+ }
50
+ else if (a.w !== b.w) {
51
+ return b.w - a.w;
52
+ }
53
+ return byName(a.item, b.item);
54
+ });
55
+ return weighted.map(({ item }) => item);
56
+ }
57
+ /** The first index at or after `from` (wrapping) whose name starts with `query`, any case; -1 if none. */
58
+ export function typeaheadIndex(items, query, from) {
59
+ const q = query.toLocaleLowerCase("en-US");
60
+ const n = items.length;
61
+ for (let step = 0; step < n; step++) {
62
+ const i = (((from + step) % n) + n) % n;
63
+ if (items[i].name.toLocaleLowerCase("en-US").startsWith(q))
64
+ return i;
65
+ }
66
+ return -1;
67
+ }
68
+ /** A row's accessible name: name, value, units, and level label. */
69
+ export function rowLabel(item, messages, format) {
70
+ if (item.value === null)
71
+ return messages("mapsdk_list_row_no_value", { name: item.name });
72
+ const level = shownLevel(item);
73
+ const params = {
74
+ name: item.name,
75
+ value: typeof item.value === "number" ? formatValue(item.value, item.precision, format) : item.value,
76
+ units: item.units,
77
+ level: level ? level.label : messages("mapsdk_no_level")
78
+ };
79
+ return messages(item.units ? "mapsdk_list_row" : "mapsdk_list_row_no_units", params);
80
+ }
@@ -0,0 +1,41 @@
1
+ import type { LegendData } from "../core/plugin.js";
2
+ import type { FormatProvider } from "../data/format.js";
3
+ import type { DataStatus } from "../data/types.js";
4
+ export type LegendBar = {
5
+ kind: "gradient";
6
+ stops: Array<{
7
+ color: string;
8
+ label: string;
9
+ tick: string;
10
+ }>;
11
+ } | {
12
+ kind: "blocks";
13
+ blocks: Array<{
14
+ color: string;
15
+ opacity: number | null;
16
+ label: string;
17
+ tick: string;
18
+ }>;
19
+ };
20
+ export interface LegendSection {
21
+ pluginId: string;
22
+ title: string;
23
+ units: string;
24
+ bar: LegendBar;
25
+ }
26
+ export type LegendEntry = {
27
+ kind: "scale";
28
+ section: LegendSection;
29
+ } | {
30
+ kind: "status";
31
+ pluginId: string;
32
+ layer: string;
33
+ status: DataStatus;
34
+ };
35
+ /** One legend section from a plugin's legend data; null for status-only data or no levels. */
36
+ export declare function legendSection(data: LegendData, format: FormatProvider): LegendSection | null;
37
+ /**
38
+ * How a translucent fill looks over the page background (`background` is the computed
39
+ * `--background`, in a form color2k parses). An empty color or no opacity returns the color.
40
+ */
41
+ export declare function swatchOverBackground(color: string, opacity: number | null, background: string): string;
@@ -0,0 +1,40 @@
1
+ import { mix } from "color2k";
2
+ /** One legend section from a plugin's legend data; null for status-only data or no levels. */
3
+ export function legendSection(data, format) {
4
+ if (!("scale" in data))
5
+ return null;
6
+ const { scale } = data;
7
+ if (scale.levels.length === 0)
8
+ return null;
9
+ const bar = scale.kind === "range" && data.display !== "blocks"
10
+ ? {
11
+ kind: "gradient",
12
+ stops: scale.levels.map((level) => ({
13
+ color: level.color ?? "",
14
+ label: level.label,
15
+ tick: level.min === null ? "" : format.number(Math.floor(level.min))
16
+ }))
17
+ }
18
+ : {
19
+ kind: "blocks",
20
+ blocks: scale.levels.map((level) => ({
21
+ color: level.color ?? "",
22
+ opacity: level.opacity,
23
+ label: level.label,
24
+ // A range scale's lower bound, for screen readers; categories have none.
25
+ tick: scale.kind === "range" && level.min !== null
26
+ ? format.number(Math.floor(level.min))
27
+ : ""
28
+ }))
29
+ };
30
+ return { pluginId: data.pluginId, title: data.title, units: data.units, bar };
31
+ }
32
+ /**
33
+ * How a translucent fill looks over the page background (`background` is the computed
34
+ * `--background`, in a form color2k parses). An empty color or no opacity returns the color.
35
+ */
36
+ export function swatchOverBackground(color, opacity, background) {
37
+ if (!color || opacity === null)
38
+ return color;
39
+ return mix(background, color, opacity);
40
+ }
@@ -0,0 +1,11 @@
1
+ /** Colors for something filled with a level's color: the fill, a darker border, readable text. */
2
+ export interface LevelChipColors {
3
+ background: string;
4
+ border: string;
5
+ text: string;
6
+ }
7
+ /**
8
+ * The colors for a level color, or null when it is empty or unparseable (color2k throws there,
9
+ * as monitor-map's `darken("")` did), so callers fall back to their neutral look.
10
+ */
11
+ export declare function levelChip(color: string | null | undefined): LevelChipColors | null;
@@ -0,0 +1,15 @@
1
+ import { darken, readableColor } from "color2k";
2
+ /**
3
+ * The colors for a level color, or null when it is empty or unparseable (color2k throws there,
4
+ * as monitor-map's `darken("")` did), so callers fall back to their neutral look.
5
+ */
6
+ export function levelChip(color) {
7
+ if (!color)
8
+ return null;
9
+ try {
10
+ return { background: color, border: darken(color, 0.1), text: readableColor(color) };
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
@@ -0,0 +1,7 @@
1
+ import type { FormatProvider } from "../data/format.js";
2
+ /**
3
+ * A numeric value with exactly `precision` decimal places, or as `format.number` gives it when
4
+ * there is no precision. Every place that shows a value (list rows, tooltips) goes through this,
5
+ * so they all agree.
6
+ */
7
+ export declare function formatValue(value: number, precision: number | undefined, format: FormatProvider): string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A numeric value with exactly `precision` decimal places, or as `format.number` gives it when
3
+ * there is no precision. Every place that shows a value (list rows, tooltips) goes through this,
4
+ * so they all agree.
5
+ */
6
+ export function formatValue(value, precision, format) {
7
+ if (precision === undefined)
8
+ return format.number(value);
9
+ return format.number(value, {
10
+ minimumFractionDigits: precision,
11
+ maximumFractionDigits: precision
12
+ });
13
+ }
@@ -0,0 +1,11 @@
1
+ import type { FeatureListItem } from "../core/plugin.js";
2
+ import type { MessageProvider } from "../data/messages.js";
3
+ import type { Scale } from "../data/scale-types.js";
4
+ /**
5
+ * One line describing the listed features: the layer, then how many are at each level, in the
6
+ * scale's order, zero counts left out (items with no level to show come last). With no scale, or
7
+ * nothing counted, just the count and layer.
8
+ */
9
+ export declare function summaryText(layerLabel: string, items: FeatureListItem[], scale: Scale | null, messages: MessageProvider): string;
10
+ /** How long after the last camera move or selection change the live region is updated. */
11
+ export declare const SUMMARY_DEBOUNCE_MS = 750;
@@ -0,0 +1,33 @@
1
+ import { shownLevel } from "./feature-list.js";
2
+ const list = new Intl.ListFormat("en-US", { type: "unit" });
3
+ /**
4
+ * One line describing the listed features: the layer, then how many are at each level, in the
5
+ * scale's order, zero counts left out (items with no level to show come last). With no scale, or
6
+ * nothing counted, just the count and layer.
7
+ */
8
+ export function summaryText(layerLabel, items, scale, messages) {
9
+ const plain = () => messages("mapsdk_summary", { count: items.length, layer: layerLabel });
10
+ if (!scale || items.length === 0)
11
+ return plain();
12
+ const counts = new Map();
13
+ let none = 0;
14
+ for (const item of items) {
15
+ const level = shownLevel(item);
16
+ if (level && scale.levels.some((l) => l.key === level.key)) {
17
+ counts.set(level.key, (counts.get(level.key) ?? 0) + 1);
18
+ }
19
+ else {
20
+ none++;
21
+ }
22
+ }
23
+ const parts = scale.levels.flatMap((level) => {
24
+ const count = counts.get(level.key);
25
+ return count ? [messages("mapsdk_summary_level", { count, level: level.label })] : [];
26
+ });
27
+ if (none > 0) {
28
+ parts.push(messages("mapsdk_summary_level", { count: none, level: messages("mapsdk_no_level") }));
29
+ }
30
+ return messages("mapsdk_summary_with_levels", { layer: layerLabel, levels: list.format(parts) });
31
+ }
32
+ /** How long after the last camera move or selection change the live region is updated. */
33
+ export const SUMMARY_DEBOUNCE_MS = 750;