@sjvair/map-sdk 0.0.0-stage → 0.1.0-beta.10
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.
- package/LICENSE +21 -0
- package/README.md +713 -2
- package/dist/core/basemap.svelte.d.ts +38 -0
- package/dist/core/basemap.svelte.js +163 -0
- package/dist/core/context.d.ts +5 -0
- package/dist/core/context.js +13 -0
- package/dist/core/create-map.svelte.d.ts +274 -0
- package/dist/core/create-map.svelte.js +591 -0
- package/dist/core/gl.d.ts +97 -0
- package/dist/core/gl.js +1 -0
- package/dist/core/interactions.svelte.d.ts +50 -0
- package/dist/core/interactions.svelte.js +330 -0
- package/dist/core/plugin-host.svelte.d.ts +71 -0
- package/dist/core/plugin-host.svelte.js +445 -0
- package/dist/core/plugin.d.ts +190 -0
- package/dist/core/plugin.js +1 -0
- package/dist/core/scoped-context.d.ts +7 -0
- package/dist/core/scoped-context.js +34 -0
- package/dist/core/shape-view.d.ts +27 -0
- package/dist/core/shape-view.js +323 -0
- package/dist/data/format.d.ts +10 -0
- package/dist/data/format.js +37 -0
- package/dist/data/index.d.ts +9 -0
- package/dist/data/index.js +9 -0
- package/dist/data/messages/en.json +61 -0
- package/dist/data/messages.d.ts +10 -0
- package/dist/data/messages.js +17 -0
- package/dist/data/metadata.d.ts +22 -0
- package/dist/data/metadata.js +110 -0
- package/dist/data/refs.d.ts +18 -0
- package/dist/data/refs.js +61 -0
- package/dist/data/scale-types.d.ts +7 -0
- package/dist/data/scale-types.js +1 -0
- package/dist/data/scale.d.ts +13 -0
- package/dist/data/scale.js +27 -0
- package/dist/data/selection.d.ts +13 -0
- package/dist/data/selection.js +52 -0
- package/dist/data/static-source.d.ts +4 -0
- package/dist/data/static-source.js +3 -0
- package/dist/data/types.d.ts +74 -0
- package/dist/data/types.js +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +42 -0
- package/dist/plugins/icons.d.ts +21 -0
- package/dist/plugins/icons.js +73 -0
- package/dist/plugins/points-clusters.d.ts +60 -0
- package/dist/plugins/points-clusters.js +93 -0
- package/dist/plugins/points.svelte.d.ts +105 -0
- package/dist/plugins/points.svelte.js +502 -0
- package/dist/plugins/scale-resolution.svelte.d.ts +27 -0
- package/dist/plugins/scale-resolution.svelte.js +111 -0
- package/dist/plugins/shapes.svelte.d.ts +34 -0
- package/dist/plugins/shapes.svelte.js +325 -0
- package/dist/plugins/shared.d.ts +6 -0
- package/dist/plugins/shared.js +18 -0
- package/dist/plugins/style.d.ts +57 -0
- package/dist/plugins/style.js +193 -0
- package/dist/presets/CollocationTooltip.svelte +132 -0
- package/dist/presets/CollocationTooltip.svelte.d.ts +16 -0
- package/dist/presets/FireTooltip.svelte +70 -0
- package/dist/presets/FireTooltip.svelte.d.ts +13 -0
- package/dist/presets/MonitorClusterTooltip.svelte +168 -0
- package/dist/presets/MonitorClusterTooltip.svelte.d.ts +16 -0
- package/dist/presets/MonitorReadingBox.svelte +58 -0
- package/dist/presets/MonitorReadingBox.svelte.d.ts +12 -0
- package/dist/presets/MonitorTooltip.svelte +88 -0
- package/dist/presets/MonitorTooltip.svelte.d.ts +20 -0
- package/dist/presets/calheatscore.d.ts +24 -0
- package/dist/presets/calheatscore.js +22 -0
- package/dist/presets/cluster-area.d.ts +13 -0
- package/dist/presets/cluster-area.js +34 -0
- package/dist/presets/collocation-sites.d.ts +18 -0
- package/dist/presets/collocation-sites.js +67 -0
- package/dist/presets/fire-groups.d.ts +26 -0
- package/dist/presets/fire-groups.js +51 -0
- package/dist/presets/forecast-zones.d.ts +25 -0
- package/dist/presets/forecast-zones.js +23 -0
- package/dist/presets/hms-fire.d.ts +30 -0
- package/dist/presets/hms-fire.js +86 -0
- package/dist/presets/hms-smoke.d.ts +20 -0
- package/dist/presets/hms-smoke.js +22 -0
- package/dist/presets/monitors.d.ts +48 -0
- package/dist/presets/monitors.js +243 -0
- package/dist/presets/reactive-props.svelte.d.ts +4 -0
- package/dist/presets/reactive-props.svelte.js +7 -0
- package/dist/presets/regions.d.ts +24 -0
- package/dist/presets/regions.js +22 -0
- package/dist/stores/abort.d.ts +16 -0
- package/dist/stores/abort.js +52 -0
- package/dist/stores/collocation-store.svelte.d.ts +25 -0
- package/dist/stores/collocation-store.svelte.js +72 -0
- package/dist/stores/hms-stores.svelte.d.ts +38 -0
- package/dist/stores/hms-stores.svelte.js +97 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +4 -0
- package/dist/stores/load-once.svelte.d.ts +25 -0
- package/dist/stores/load-once.svelte.js +93 -0
- package/dist/stores/monitors-store.svelte.d.ts +85 -0
- package/dist/stores/monitors-store.svelte.js +340 -0
- package/dist/theme.css +34 -0
- package/dist/ui/FeatureList.svelte +501 -0
- package/dist/ui/FeatureList.svelte.d.ts +14 -0
- package/dist/ui/Legend.svelte +214 -0
- package/dist/ui/Legend.svelte.d.ts +22 -0
- package/dist/ui/LegendBar.svelte +161 -0
- package/dist/ui/LegendBar.svelte.d.ts +11 -0
- package/dist/ui/MapControls.svelte +135 -0
- package/dist/ui/MapControls.svelte.d.ts +11 -0
- package/dist/ui/MapTooltip.svelte +397 -0
- package/dist/ui/MapTooltip.svelte.d.ts +37 -0
- package/dist/ui/PickNotice.svelte +56 -0
- package/dist/ui/PickNotice.svelte.d.ts +7 -0
- package/dist/ui/SelectionBar.svelte +123 -0
- package/dist/ui/SelectionBar.svelte.d.ts +10 -0
- package/dist/ui/TooltipItem.svelte +66 -0
- package/dist/ui/TooltipItem.svelte.d.ts +11 -0
- package/dist/ui/ViewSummary.svelte +97 -0
- package/dist/ui/ViewSummary.svelte.d.ts +7 -0
- package/dist/ui/card.d.ts +29 -0
- package/dist/ui/card.js +1 -0
- package/dist/ui/controls.css +45 -0
- package/dist/ui/data-credits.svelte.d.ts +9 -0
- package/dist/ui/data-credits.svelte.js +52 -0
- package/dist/ui/feature-list.d.ts +46 -0
- package/dist/ui/feature-list.js +115 -0
- package/dist/ui/legend-data.d.ts +41 -0
- package/dist/ui/legend-data.js +40 -0
- package/dist/ui/level-chip.d.ts +11 -0
- package/dist/ui/level-chip.js +15 -0
- package/dist/ui/value-format.d.ts +8 -0
- package/dist/ui/value-format.js +20 -0
- package/dist/ui/view-summary.d.ts +11 -0
- package/dist/ui/view-summary.js +33 -0
- package/dist/view/MapView.svelte +443 -0
- package/dist/view/MapView.svelte.d.ts +51 -0
- package/dist/view/create-gl-map.d.ts +34 -0
- package/dist/view/create-gl-map.js +186 -0
- package/dist/view/list-placement.d.ts +21 -0
- package/dist/view/list-placement.js +13 -0
- package/package.json +104 -5
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, untrack } from "svelte";
|
|
3
|
+
import "./controls.css";
|
|
4
|
+
import type { MapContext } from "../core/create-map.svelte.js";
|
|
5
|
+
import { watchTheme } from "../view/create-gl-map.js";
|
|
6
|
+
import LegendBar from "./LegendBar.svelte";
|
|
7
|
+
import { legendSection, type LegendEntry } from "./legend-data.js";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
map,
|
|
11
|
+
order = "bottom-first",
|
|
12
|
+
framed = true
|
|
13
|
+
}: {
|
|
14
|
+
map: MapContext;
|
|
15
|
+
/**
|
|
16
|
+
* `true` (default) draws the legend's own card. `false` draws no frame (no border,
|
|
17
|
+
* background, padding, or fixed width) so a host's card frames it. Translucent swatches
|
|
18
|
+
* then blend over the nearest ancestor with an opaque background (a see-through card is
|
|
19
|
+
* skipped), else the `--background` token. The color is read on mount, when `framed`
|
|
20
|
+
* changes, and on a theme change; a card that changes color through its own class or
|
|
21
|
+
* style isn't re-read.
|
|
22
|
+
*/
|
|
23
|
+
framed?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* `"bottom-first"` (default) lists the bottom plugin's section first; `"top-first"`
|
|
26
|
+
* last.
|
|
27
|
+
*/
|
|
28
|
+
order?: "bottom-first" | "top-first";
|
|
29
|
+
} = $props();
|
|
30
|
+
|
|
31
|
+
let root: HTMLElement | undefined = $state();
|
|
32
|
+
// The `--background` token as rgb, for mixing translucent swatches over it. Read as soon as the
|
|
33
|
+
// element exists (the bars wait for it), so no color is assumed before.
|
|
34
|
+
let background = $state<string | null>(null);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A section per plugin with a scale, and a status line per plugin still loading or failing, in
|
|
38
|
+
* `order`.
|
|
39
|
+
*/
|
|
40
|
+
const entries = $derived(
|
|
41
|
+
(order === "top-first" ? [...map.plugins].reverse() : map.plugins).flatMap(
|
|
42
|
+
(plugin): LegendEntry[] => {
|
|
43
|
+
const data = plugin.legend?.();
|
|
44
|
+
if (!data) return [];
|
|
45
|
+
if ("status" in data) {
|
|
46
|
+
if (data.status.state === "ready") return [];
|
|
47
|
+
const layer = plugin.label ?? plugin.id;
|
|
48
|
+
return [{ kind: "status", pluginId: data.pluginId, layer, status: data.status }];
|
|
49
|
+
}
|
|
50
|
+
const section = legendSection(data, map.format);
|
|
51
|
+
return section ? [{ kind: "scale", section }] : [];
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A computed color as rgb and whether it's opaque; computed colors can be oklch() or color(),
|
|
58
|
+
* and a 1x1 canvas turns any of them into rgb.
|
|
59
|
+
*/
|
|
60
|
+
function toRgb(css: string): { rgb: string; opaque: boolean } {
|
|
61
|
+
const ctx = document.createElement("canvas").getContext("2d");
|
|
62
|
+
if (!ctx) return { rgb: css, opaque: true };
|
|
63
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
64
|
+
ctx.fillStyle = css;
|
|
65
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
66
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
67
|
+
return { rgb: `rgb(${r}, ${g}, ${b})`, opaque: a === 255 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What translucent swatches blend over: the legend's own card, or without a frame the nearest
|
|
72
|
+
* ancestor with an opaque background (the host's card), else the `--background` token.
|
|
73
|
+
*/
|
|
74
|
+
function resolveBackground(el: HTMLElement, frameless: boolean): string {
|
|
75
|
+
if (!frameless) return toRgb(getComputedStyle(el).backgroundColor).rgb;
|
|
76
|
+
for (let at = el.parentElement; at; at = at.parentElement) {
|
|
77
|
+
const color = toRgb(getComputedStyle(at).backgroundColor);
|
|
78
|
+
if (color.opaque) return color.rgb;
|
|
79
|
+
}
|
|
80
|
+
// A computed custom property has its var() references already substituted.
|
|
81
|
+
return toRgb(getComputedStyle(el).getPropertyValue("--background").trim()).rgb;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function update() {
|
|
85
|
+
if (root) background = resolveBackground(root, !framed);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onMount(() => watchTheme(root!, update));
|
|
89
|
+
|
|
90
|
+
// On mount, and again when `framed` changes.
|
|
91
|
+
$effect(() => {
|
|
92
|
+
void framed;
|
|
93
|
+
untrack(update);
|
|
94
|
+
});
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<section
|
|
98
|
+
bind:this={root}
|
|
99
|
+
class={["mapsdk-legend", !framed && "mapsdk-legend-frameless"]}
|
|
100
|
+
aria-label={map.messages("mapsdk_legend_label")}
|
|
101
|
+
hidden={entries.length === 0}
|
|
102
|
+
>
|
|
103
|
+
{#each entries as entry (entry.kind === "scale" ? entry.section.pluginId : entry.pluginId)}
|
|
104
|
+
{#if entry.kind === "scale"}
|
|
105
|
+
{@const section = entry.section}
|
|
106
|
+
<div class="mapsdk-legend-section">
|
|
107
|
+
<h3 class="text-sm">
|
|
108
|
+
{section.title}
|
|
109
|
+
{#if section.units}
|
|
110
|
+
<span class="text-xs">({section.units})</span>
|
|
111
|
+
{/if}
|
|
112
|
+
</h3>
|
|
113
|
+
{#if background !== null}
|
|
114
|
+
<LegendBar bar={section.bar} {background} messages={map.messages} />
|
|
115
|
+
{/if}
|
|
116
|
+
</div>
|
|
117
|
+
{:else}
|
|
118
|
+
{@const status = entry.status}
|
|
119
|
+
<div class="mapsdk-legend-status text-sm" role="status">
|
|
120
|
+
{#if status.state === "loading"}
|
|
121
|
+
<h3 class="text-sm">{entry.layer}</h3>
|
|
122
|
+
<p>{map.messages("mapsdk_loading")}</p>
|
|
123
|
+
{:else}
|
|
124
|
+
<p>{map.messages("mapsdk_unavailable", { layer: entry.layer })}</p>
|
|
125
|
+
{#if status.state === "error"}
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
class="mapsdk-retry mapsdk-focus mapsdk-motion text-sm"
|
|
129
|
+
onclick={() => status.retry()}
|
|
130
|
+
>
|
|
131
|
+
{map.messages("mapsdk_error_retry")}
|
|
132
|
+
</button>
|
|
133
|
+
{/if}
|
|
134
|
+
{/if}
|
|
135
|
+
</div>
|
|
136
|
+
{/if}
|
|
137
|
+
{/each}
|
|
138
|
+
</section>
|
|
139
|
+
|
|
140
|
+
<style>
|
|
141
|
+
.mapsdk-legend {
|
|
142
|
+
display: flex;
|
|
143
|
+
flex-direction: column;
|
|
144
|
+
gap: 0.75rem;
|
|
145
|
+
width: 21rem;
|
|
146
|
+
/* The map's width inside a MapView (set there), else the window's. */
|
|
147
|
+
max-width: calc(var(--mapsdk-map-width, 100vw) - 2rem);
|
|
148
|
+
box-sizing: border-box;
|
|
149
|
+
padding: 0.75rem 1rem;
|
|
150
|
+
border: 1px solid var(--card-border, var(--border));
|
|
151
|
+
border-radius: var(--radius-control, var(--radius));
|
|
152
|
+
background: var(--background);
|
|
153
|
+
color: var(--foreground);
|
|
154
|
+
box-shadow: var(--shadow-card, none);
|
|
155
|
+
pointer-events: auto;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* framed={false}: the host's card is the frame. */
|
|
159
|
+
.mapsdk-legend-frameless {
|
|
160
|
+
width: auto;
|
|
161
|
+
max-width: none;
|
|
162
|
+
padding: 0;
|
|
163
|
+
border: 0;
|
|
164
|
+
border-radius: 0;
|
|
165
|
+
background: none;
|
|
166
|
+
box-shadow: none;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.mapsdk-legend[hidden] {
|
|
170
|
+
display: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
h3 {
|
|
174
|
+
margin: 0 0 0.5rem;
|
|
175
|
+
text-align: center;
|
|
176
|
+
font-weight: 600;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.mapsdk-legend-status {
|
|
180
|
+
display: flex;
|
|
181
|
+
flex-direction: column;
|
|
182
|
+
align-items: center;
|
|
183
|
+
gap: 0.5rem;
|
|
184
|
+
text-align: center;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.mapsdk-legend-status h3 {
|
|
188
|
+
margin: 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.mapsdk-legend-status p {
|
|
192
|
+
margin: 0;
|
|
193
|
+
color: var(--muted-foreground);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.mapsdk-retry {
|
|
197
|
+
padding: 0.25rem 0.75rem;
|
|
198
|
+
border: 1px solid var(--border);
|
|
199
|
+
border-radius: var(--radius-control, var(--radius));
|
|
200
|
+
background: var(--background);
|
|
201
|
+
color: var(--foreground);
|
|
202
|
+
font: inherit;
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.mapsdk-retry:hover {
|
|
207
|
+
background: var(--accent, var(--muted));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
h3 span {
|
|
211
|
+
font-weight: 400;
|
|
212
|
+
color: var(--muted-foreground);
|
|
213
|
+
}
|
|
214
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import "./controls.css";
|
|
2
|
+
import type { MapContext } from "../core/create-map.svelte.js";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
map: MapContext;
|
|
5
|
+
/**
|
|
6
|
+
* `true` (default) draws the legend's own card. `false` draws no frame (no border,
|
|
7
|
+
* background, padding, or fixed width) so a host's card frames it. Translucent swatches
|
|
8
|
+
* then blend over the nearest ancestor with an opaque background (a see-through card is
|
|
9
|
+
* skipped), else the `--background` token. The color is read on mount, when `framed`
|
|
10
|
+
* changes, and on a theme change; a card that changes color through its own class or
|
|
11
|
+
* style isn't re-read.
|
|
12
|
+
*/
|
|
13
|
+
framed?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* `"bottom-first"` (default) lists the bottom plugin's section first; `"top-first"`
|
|
16
|
+
* last.
|
|
17
|
+
*/
|
|
18
|
+
order?: "bottom-first" | "top-first";
|
|
19
|
+
};
|
|
20
|
+
declare const Legend: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
21
|
+
type Legend = ReturnType<typeof Legend>;
|
|
22
|
+
export default Legend;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { MessageProvider } from "../data/messages.js";
|
|
3
|
+
import { swatchOverBackground, type LegendBar } from "./legend-data.js";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
bar: LegendBar;
|
|
7
|
+
/** The page background as a color color2k can parse. */
|
|
8
|
+
background: string;
|
|
9
|
+
messages: MessageProvider;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { bar, background, messages }: Props = $props();
|
|
13
|
+
|
|
14
|
+
// An empty color (a level with none) gets a neutral swatch rather than nothing.
|
|
15
|
+
const NEUTRAL = "var(--muted-foreground)";
|
|
16
|
+
|
|
17
|
+
const items = $derived(
|
|
18
|
+
bar.kind === "gradient"
|
|
19
|
+
? bar.stops.map((stop) => ({
|
|
20
|
+
color: stop.color || NEUTRAL,
|
|
21
|
+
label: stop.label,
|
|
22
|
+
// The ticks are aria-hidden, so their numbers reach screen readers here.
|
|
23
|
+
threshold: stop.tick ? messages("mapsdk_legend_threshold", { value: stop.tick }) : ""
|
|
24
|
+
}))
|
|
25
|
+
: bar.blocks.map((block) => ({
|
|
26
|
+
color: block.color
|
|
27
|
+
? swatchOverBackground(block.color, block.opacity, background)
|
|
28
|
+
: NEUTRAL,
|
|
29
|
+
label: block.label,
|
|
30
|
+
threshold: block.tick ? messages("mapsdk_legend_threshold", { value: block.tick }) : ""
|
|
31
|
+
}))
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const gradient = $derived(
|
|
35
|
+
bar.kind === "gradient" && bar.stops.length > 0
|
|
36
|
+
? bar.stops.length === 1
|
|
37
|
+
? items[0].color
|
|
38
|
+
: `linear-gradient(to right, ${items.map((item) => item.color).join(", ")})`
|
|
39
|
+
: null
|
|
40
|
+
);
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
{#if bar.kind === "gradient" && gradient}
|
|
44
|
+
<div class="mapsdk-legend-gradient" style:background={gradient} aria-hidden="true"></div>
|
|
45
|
+
<div class="mapsdk-legend-ticks text-xs" aria-hidden="true">
|
|
46
|
+
{#each bar.stops as stop, i (i)}
|
|
47
|
+
<span>{stop.tick}</span>
|
|
48
|
+
{/each}
|
|
49
|
+
</div>
|
|
50
|
+
{/if}
|
|
51
|
+
{#if bar.kind === "blocks"}
|
|
52
|
+
<!-- One contiguous bar, each level's label under its segment (a list for screen readers). -->
|
|
53
|
+
<div class="mapsdk-legend-blocks" aria-hidden="true">
|
|
54
|
+
{#each items as item, i (i)}
|
|
55
|
+
<span class="mapsdk-legend-block" style:background={item.color}></span>
|
|
56
|
+
{/each}
|
|
57
|
+
</div>
|
|
58
|
+
<ul class="mapsdk-legend-block-labels text-xs" style:--blocks={items.length}>
|
|
59
|
+
{#each items as item, i (i)}
|
|
60
|
+
<li>
|
|
61
|
+
{item.label}
|
|
62
|
+
{#if item.threshold}<span class="mapsdk-legend-threshold">{item.threshold}</span>{/if}
|
|
63
|
+
</li>
|
|
64
|
+
{/each}
|
|
65
|
+
</ul>
|
|
66
|
+
{:else}
|
|
67
|
+
<ul class="mapsdk-legend-items text-xs">
|
|
68
|
+
{#each items as item, i (i)}
|
|
69
|
+
<li>
|
|
70
|
+
<span class="mapsdk-legend-swatch" style:background={item.color}></span>
|
|
71
|
+
<span>{item.label}</span>
|
|
72
|
+
{#if item.threshold}<span class="mapsdk-legend-threshold">{item.threshold}</span>{/if}
|
|
73
|
+
</li>
|
|
74
|
+
{/each}
|
|
75
|
+
</ul>
|
|
76
|
+
{/if}
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.mapsdk-legend-gradient {
|
|
80
|
+
height: 0.75rem;
|
|
81
|
+
border-radius: 0.25rem;
|
|
82
|
+
outline: var(--swatch-outline-width, 1px) solid
|
|
83
|
+
var(--swatch-outline-color, color-mix(in srgb, var(--foreground) 75%, var(--background)));
|
|
84
|
+
outline-offset: 1px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.mapsdk-legend-blocks {
|
|
88
|
+
display: flex;
|
|
89
|
+
height: 0.75rem;
|
|
90
|
+
border-radius: 0.25rem;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
outline: var(--swatch-outline-width, 1px) solid
|
|
93
|
+
var(--swatch-outline-color, color-mix(in srgb, var(--foreground) 75%, var(--background)));
|
|
94
|
+
outline-offset: 1px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.mapsdk-legend-block {
|
|
98
|
+
flex: 1 1 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Equal columns under the equal segments; long labels wrap inside their column. */
|
|
102
|
+
.mapsdk-legend-block-labels {
|
|
103
|
+
display: grid;
|
|
104
|
+
grid-template-columns: repeat(var(--blocks), minmax(0, 1fr));
|
|
105
|
+
margin: 0.375rem 0 0;
|
|
106
|
+
padding: 0;
|
|
107
|
+
list-style: none;
|
|
108
|
+
text-align: center;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.mapsdk-legend-block-labels li {
|
|
112
|
+
position: relative;
|
|
113
|
+
padding: 0 0.125rem;
|
|
114
|
+
overflow-wrap: anywhere;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.mapsdk-legend-ticks {
|
|
118
|
+
display: flex;
|
|
119
|
+
justify-content: space-between;
|
|
120
|
+
margin-top: 0.375rem;
|
|
121
|
+
font-variant-numeric: tabular-nums;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.mapsdk-legend-items {
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-wrap: wrap;
|
|
127
|
+
gap: 0.25rem 0.75rem;
|
|
128
|
+
margin: 0.5rem 0 0;
|
|
129
|
+
padding: 0;
|
|
130
|
+
list-style: none;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Visually hidden, read after the label. */
|
|
134
|
+
.mapsdk-legend-threshold {
|
|
135
|
+
position: absolute;
|
|
136
|
+
width: 1px;
|
|
137
|
+
height: 1px;
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
clip-path: inset(50%);
|
|
140
|
+
white-space: nowrap;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.mapsdk-legend-items li {
|
|
144
|
+
position: relative;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: 0.375rem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* The outline sits 1px off the swatch, over the background, so it needs 3:1 against the
|
|
151
|
+
background only; the gap keeps a swatch near the outline's own color readable. */
|
|
152
|
+
.mapsdk-legend-swatch {
|
|
153
|
+
width: 0.875rem;
|
|
154
|
+
height: 0.875rem;
|
|
155
|
+
flex: none;
|
|
156
|
+
border-radius: 0.125rem;
|
|
157
|
+
outline: var(--swatch-outline-width, 1px) solid
|
|
158
|
+
var(--swatch-outline-color, color-mix(in srgb, var(--foreground) 75%, var(--background)));
|
|
159
|
+
outline-offset: 1px;
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MessageProvider } from "../data/messages.js";
|
|
2
|
+
import { type LegendBar } from "./legend-data.js";
|
|
3
|
+
interface Props {
|
|
4
|
+
bar: LegendBar;
|
|
5
|
+
/** The page background as a color color2k can parse. */
|
|
6
|
+
background: string;
|
|
7
|
+
messages: MessageProvider;
|
|
8
|
+
}
|
|
9
|
+
declare const LegendBar: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type LegendBar = ReturnType<typeof LegendBar>;
|
|
11
|
+
export default LegendBar;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import "./controls.css";
|
|
3
|
+
import type { MapContext } from "../core/create-map.svelte.js";
|
|
4
|
+
import type { GlMapLike } from "../core/gl.js";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
map: MapContext;
|
|
8
|
+
/** The live GL map; the zoom buttons do nothing while it is `null`. */
|
|
9
|
+
gl: GlMapLike | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { map, gl }: Props = $props();
|
|
13
|
+
|
|
14
|
+
function zoomBy(delta: number) {
|
|
15
|
+
if (!gl) return;
|
|
16
|
+
const reduced = globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
17
|
+
gl.easeTo({ zoom: gl.getZoom() + delta, ...(reduced && { duration: 0 }) });
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<div class="mapsdk-zoom" role="group" aria-label={map.messages("mapsdk_controls_label")}>
|
|
22
|
+
<button type="button" class="mapsdk-focus mapsdk-motion" disabled={!gl} onclick={() => zoomBy(1)}>
|
|
23
|
+
<span class="mapsdk-zoom-icon" aria-hidden="true">+</span>
|
|
24
|
+
<span class="mapsdk-sr">{map.messages("mapsdk_zoom_in")}</span>
|
|
25
|
+
<span class="mapsdk-zoom-tip mapsdk-motion text-xs" aria-hidden="true"
|
|
26
|
+
>{map.messages("mapsdk_zoom_in")}</span
|
|
27
|
+
>
|
|
28
|
+
</button>
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
class="mapsdk-focus mapsdk-motion"
|
|
32
|
+
disabled={!gl}
|
|
33
|
+
onclick={() => zoomBy(-1)}
|
|
34
|
+
>
|
|
35
|
+
<span class="mapsdk-zoom-icon" aria-hidden="true">−</span>
|
|
36
|
+
<span class="mapsdk-sr">{map.messages("mapsdk_zoom_out")}</span>
|
|
37
|
+
<span class="mapsdk-zoom-tip mapsdk-motion text-xs" aria-hidden="true"
|
|
38
|
+
>{map.messages("mapsdk_zoom_out")}</span
|
|
39
|
+
>
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
.mapsdk-zoom {
|
|
45
|
+
position: absolute;
|
|
46
|
+
top: 0.75rem;
|
|
47
|
+
right: 0.75rem;
|
|
48
|
+
z-index: 2;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
border: 1px solid var(--card-border, var(--border));
|
|
52
|
+
border-radius: var(--radius-control, var(--radius));
|
|
53
|
+
background: var(--background);
|
|
54
|
+
box-shadow: var(--shadow-card, none);
|
|
55
|
+
pointer-events: auto;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.mapsdk-zoom button {
|
|
59
|
+
width: 2.25rem;
|
|
60
|
+
height: 2.25rem;
|
|
61
|
+
padding: 0;
|
|
62
|
+
border: 0;
|
|
63
|
+
background: transparent;
|
|
64
|
+
color: var(--foreground);
|
|
65
|
+
font: inherit;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.mapsdk-zoom button + button {
|
|
70
|
+
border-top: 1px solid var(--card-border, var(--border));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.mapsdk-zoom button:disabled {
|
|
74
|
+
color: var(--muted-foreground);
|
|
75
|
+
cursor: default;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.mapsdk-zoom button {
|
|
79
|
+
position: relative;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* The hover fill follows the group's rounded corners. */
|
|
83
|
+
.mapsdk-zoom button:first-child {
|
|
84
|
+
border-radius: var(--radius-control, var(--radius)) var(--radius-control, var(--radius)) 0 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.mapsdk-zoom button:last-child {
|
|
88
|
+
border-radius: 0 0 var(--radius-control, var(--radius)) var(--radius-control, var(--radius));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.mapsdk-zoom button:hover:not(:disabled) {
|
|
92
|
+
background: var(--accent, var(--muted));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Above the next button, so the ring isn't cut by it. */
|
|
96
|
+
.mapsdk-zoom button:focus-visible {
|
|
97
|
+
z-index: 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* The visible name: on hover and focus, left of the button; the screen-reader text stays. */
|
|
101
|
+
.mapsdk-zoom-tip {
|
|
102
|
+
position: absolute;
|
|
103
|
+
top: 50%;
|
|
104
|
+
right: calc(100% + 0.5rem);
|
|
105
|
+
padding: 0.125rem 0.5rem;
|
|
106
|
+
border: 1px solid var(--card-border, var(--border));
|
|
107
|
+
border-radius: var(--radius-control, var(--radius));
|
|
108
|
+
background: var(--popover, var(--background));
|
|
109
|
+
color: var(--popover-foreground, var(--foreground));
|
|
110
|
+
box-shadow: var(--shadow-lifted, var(--shadow-card, none));
|
|
111
|
+
white-space: nowrap;
|
|
112
|
+
opacity: 0;
|
|
113
|
+
pointer-events: none;
|
|
114
|
+
transform: translateY(-50%);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.mapsdk-zoom button:hover .mapsdk-zoom-tip,
|
|
118
|
+
.mapsdk-zoom button:focus-visible .mapsdk-zoom-tip {
|
|
119
|
+
opacity: 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.mapsdk-zoom-icon {
|
|
123
|
+
font-size: 1.25rem;
|
|
124
|
+
line-height: 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.mapsdk-sr {
|
|
128
|
+
position: absolute;
|
|
129
|
+
width: 1px;
|
|
130
|
+
height: 1px;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
clip-path: inset(50%);
|
|
133
|
+
white-space: nowrap;
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "./controls.css";
|
|
2
|
+
import type { MapContext } from "../core/create-map.svelte.js";
|
|
3
|
+
import type { GlMapLike } from "../core/gl.js";
|
|
4
|
+
interface Props {
|
|
5
|
+
map: MapContext;
|
|
6
|
+
/** The live GL map; the zoom buttons do nothing while it is `null`. */
|
|
7
|
+
gl: GlMapLike | null;
|
|
8
|
+
}
|
|
9
|
+
declare const MapControls: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type MapControls = ReturnType<typeof MapControls>;
|
|
11
|
+
export default MapControls;
|