@sjvair/map-sdk 0.1.0-beta.1 → 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.
- package/README.md +23 -4
- package/dist/core/interactions.svelte.d.ts +2 -1
- package/dist/core/interactions.svelte.js +20 -3
- package/dist/core/plugin.d.ts +9 -2
- package/dist/data/messages/en.json +1 -0
- package/dist/plugins/points.svelte.d.ts +9 -2
- package/dist/plugins/points.svelte.js +33 -5
- package/dist/ui/FeatureList.svelte +10 -6
- package/dist/ui/Legend.svelte +56 -15
- package/dist/ui/Legend.svelte.d.ts +9 -0
- package/dist/ui/TooltipItem.svelte +10 -4
- package/dist/ui/feature-list.d.ts +8 -1
- package/dist/ui/feature-list.js +20 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,7 +165,8 @@ in each level's color at the fill's opacity; choropleth only), or `"none"` (no o
|
|
|
165
165
|
`hmsSmokePlugin` uses `"level"`.
|
|
166
166
|
|
|
167
167
|
`zoomRange: { min?, max? }` on `pointsPlugin` limits every layer it draws (points, clusters,
|
|
168
|
-
selection) to zooms from `min` (inclusive) up to `max` (exclusive), switching live while
|
|
168
|
+
selection) to camera zooms from `min` (inclusive) up to `max` (exclusive), switching live while
|
|
169
|
+
zooming. Two plugins whose ranges meet hand over with no gap, on the globe too.
|
|
169
170
|
|
|
170
171
|
`hmsFirePlugin({ id?, source, hotspots?, layerLabel })` draws fires as flame icons sized and
|
|
171
172
|
colored by FRP. With `hotspots`, it switches by itself while zooming, as sjvair.com's map does:
|
|
@@ -180,7 +181,23 @@ each level's label under its segment; long labels wrap. `legendTitle: "layer" |
|
|
|
180
181
|
the legend section's title: `layerLabel` (default) or the entry type's label from the metadata
|
|
181
182
|
provider (`monitorsPlugin` uses the latter, so its section reads "PM2.5"; `layerLabel` still names
|
|
182
183
|
the layer in the list, selection bar and summary). `Legend` takes `order` (`"bottom-first"`,
|
|
183
|
-
default, or `"top-first"`)
|
|
184
|
+
default, or `"top-first"`) and `framed` (`true`, default, draws its own card; `false` draws no
|
|
185
|
+
border, background, padding or fixed width, so a host's own card frames it). Without a frame,
|
|
186
|
+
translucent swatches blend over the nearest ancestor with an opaque background (a see-through
|
|
187
|
+
card is skipped), else `--background`; that color is read on mount, when `framed` changes, and on
|
|
188
|
+
a theme change, but not when the card changes color through its own class or style.
|
|
189
|
+
|
|
190
|
+
```svelte
|
|
191
|
+
<div class="w-84 rounded-md bg-white shadow">
|
|
192
|
+
<h3>Legend</h3>
|
|
193
|
+
<Legend {map} framed={false} />
|
|
194
|
+
</div>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`detail?(props)` on `pointsPlugin` gives a feature with no level a short text (such as "Level 2
|
|
198
|
+
charger") shown where "No level" would be: in the feature list (plain text, no swatch), the row's
|
|
199
|
+
accessible name ("Fresno Station, Level 2 charger"), and the tooltip. A feature with a level
|
|
200
|
+
always shows its level. A plugin of your own sets the same `detail` on its `FeatureListItem`s.
|
|
184
201
|
|
|
185
202
|
`collocationSitesPlugin({ id?, source, layerLabel })` draws collocation sites as sjvair.com does:
|
|
186
203
|
each at its reference monitor, in that monitor's shape and level color (inactive gray, inside
|
|
@@ -329,8 +346,10 @@ time), and `movePlugin` moves it with the plugin's other layers.
|
|
|
329
346
|
|
|
330
347
|
The cursor is a pointer over any interactive feature or cluster bubble. Where several plugins'
|
|
331
348
|
features are under the pointer, the hover tooltip shows the one whose plugin has the highest
|
|
332
|
-
`hoverPriority` (default 0), the topmost on a tie
|
|
333
|
-
|
|
349
|
+
`hoverPriority` (default 0), the topmost on a tie. Over a cluster bubble, the bubble's tooltip
|
|
350
|
+
shows unless a feature beneath belongs to a plugin with a higher `hoverPriority` than the
|
|
351
|
+
bubble's (collocation sites under monitor clusters). Clicks always go to the topmost, so a click
|
|
352
|
+
on a bubble still zooms into it.
|
|
334
353
|
|
|
335
354
|
```ts
|
|
336
355
|
import type { CustomLayerInterface, MapPlugin, PluginContext } from "@sjvair/map-sdk";
|
|
@@ -10,7 +10,8 @@ export interface Hovered {
|
|
|
10
10
|
* Listens on the map itself (`click`, `mousemove`, `mouseout`) and queries only the registered
|
|
11
11
|
* layers, so registrations can come and go without re-binding listeners. Layers nobody registered
|
|
12
12
|
* are never hit. A blocking layer (e.g. a cluster bubble, which zooms instead) is never hit either,
|
|
13
|
-
*
|
|
13
|
+
* and when it is topmost at a point nothing beneath it is hit; only a feature whose plugin
|
|
14
|
+
* outranks it in `hoverPriority` is hovered through it.
|
|
14
15
|
*/
|
|
15
16
|
export declare class InteractionRouter {
|
|
16
17
|
#private;
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Listens on the map itself (`click`, `mousemove`, `mouseout`) and queries only the registered
|
|
5
5
|
* layers, so registrations can come and go without re-binding listeners. Layers nobody registered
|
|
6
6
|
* are never hit. A blocking layer (e.g. a cluster bubble, which zooms instead) is never hit either,
|
|
7
|
-
*
|
|
7
|
+
* and when it is topmost at a point nothing beneath it is hit; only a feature whose plugin
|
|
8
|
+
* outranks it in `hoverPriority` is hovered through it.
|
|
8
9
|
*/
|
|
9
10
|
export class InteractionRouter {
|
|
10
11
|
// Bookkeeping only; nothing reads these reactively.
|
|
@@ -120,9 +121,25 @@ export class InteractionRouter {
|
|
|
120
121
|
if (top?.blocking) {
|
|
121
122
|
const properties = features[0].properties ?? {};
|
|
122
123
|
const hit = top.toHit(properties);
|
|
124
|
+
// Clicks stay blocked, but the hover goes to the highest-priority feature beneath that
|
|
125
|
+
// outranks the blocking plugin.
|
|
126
|
+
let beneath = null;
|
|
127
|
+
let beneathPriority = top.priority;
|
|
128
|
+
for (const feature of features.slice(1)) {
|
|
129
|
+
const registration = byLayer.get(feature.layer.id);
|
|
130
|
+
if (!registration || registration.blocking)
|
|
131
|
+
continue;
|
|
132
|
+
if (registration.priority <= beneathPriority)
|
|
133
|
+
continue;
|
|
134
|
+
const below = registration.toHit(feature.properties ?? {});
|
|
135
|
+
if (!below)
|
|
136
|
+
continue;
|
|
137
|
+
beneath = { ...below, pluginId: registration.pluginId };
|
|
138
|
+
beneathPriority = registration.priority;
|
|
139
|
+
}
|
|
123
140
|
return {
|
|
124
141
|
hits: [],
|
|
125
|
-
hover:
|
|
142
|
+
hover: beneath,
|
|
126
143
|
blocked: true,
|
|
127
144
|
blocker: hit && { ...hit, pluginId: top.pluginId },
|
|
128
145
|
blockedBy: { registration: top, feature: { properties, geometry: features[0].geometry } }
|
|
@@ -194,7 +211,7 @@ export class InteractionRouter {
|
|
|
194
211
|
};
|
|
195
212
|
#onMove = (e) => {
|
|
196
213
|
const found = this.#lookup([e.point.x, e.point.y]);
|
|
197
|
-
const hit = found.blocked ? found.blocker : found.hover;
|
|
214
|
+
const hit = found.blocked ? (found.hover ?? found.blocker) : found.hover;
|
|
198
215
|
this.#hovered = hit ? { hit, lngLat: [e.lngLat.lng, e.lngLat.lat] } : null;
|
|
199
216
|
this.#setPointer(found.blocked || hit !== null);
|
|
200
217
|
};
|
package/dist/core/plugin.d.ts
CHANGED
|
@@ -48,6 +48,12 @@ export interface FeatureListItem {
|
|
|
48
48
|
precision?: number;
|
|
49
49
|
lngLat: [number, number];
|
|
50
50
|
selectable: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* A short text about a feature with no level (e.g. "Level 2 charger"), shown where its level
|
|
53
|
+
* would be: in the feature list, its row's accessible name, and the generic tooltip. Ignored
|
|
54
|
+
* when the item has a level.
|
|
55
|
+
*/
|
|
56
|
+
detail?: string;
|
|
51
57
|
}
|
|
52
58
|
export type LegendData = {
|
|
53
59
|
pluginId: string;
|
|
@@ -128,8 +134,9 @@ export interface MapPlugin {
|
|
|
128
134
|
selectable?: boolean;
|
|
129
135
|
/**
|
|
130
136
|
* Which plugin's feature the hover tooltip shows where several are under the pointer: the
|
|
131
|
-
* highest wins, the topmost on a tie. Default 0.
|
|
132
|
-
* beneath
|
|
137
|
+
* highest wins, the topmost on a tie. Default 0. Over a blocking layer (a cluster bubble), a
|
|
138
|
+
* feature beneath shows only if its plugin's priority is higher than the blocking plugin's.
|
|
139
|
+
* Clicks ignore this (topmost first), and a blocking layer on top still blocks them.
|
|
133
140
|
*/
|
|
134
141
|
hoverPriority?: number;
|
|
135
142
|
displayOptions?: Snippet;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"mapsdk_list_row": "{name}, {value} {units}, {level}",
|
|
9
9
|
"mapsdk_list_row_no_units": "{name}, {value}, {level}",
|
|
10
10
|
"mapsdk_list_row_no_value": "{name}, no reading",
|
|
11
|
+
"mapsdk_list_row_detail": "{name}, {detail}",
|
|
11
12
|
"mapsdk_list_empty": "Nothing on the map in this view",
|
|
12
13
|
"mapsdk_selected_count_one": "{count} selected",
|
|
13
14
|
"mapsdk_selected_count_other": "{count} selected",
|
|
@@ -60,8 +60,9 @@ export interface PointsOptions<P extends MapFeatureProperties> {
|
|
|
60
60
|
sortKey?(props: P): number;
|
|
61
61
|
/**
|
|
62
62
|
* Limits every layer the plugin draws (points, clusters, selection) to a zoom range: visible
|
|
63
|
-
* from `min` (inclusive) up to `max` (exclusive)
|
|
64
|
-
* zooming
|
|
63
|
+
* from `min` (inclusive) up to `max` (exclusive) of the camera zoom, switching live while
|
|
64
|
+
* zooming (layer visibility, not layer zoom ranges, so ranges that meet leave no gap on a
|
|
65
|
+
* globe). Default: all zooms.
|
|
65
66
|
*/
|
|
66
67
|
zoomRange?: {
|
|
67
68
|
min?: number;
|
|
@@ -75,6 +76,12 @@ export interface PointsOptions<P extends MapFeatureProperties> {
|
|
|
75
76
|
* it loads.
|
|
76
77
|
*/
|
|
77
78
|
legendTitle?: "layer" | "entry-type";
|
|
79
|
+
/**
|
|
80
|
+
* A short text for a feature with no level (e.g. "Level 2 charger"), shown in the feature list,
|
|
81
|
+
* its accessible name, and the generic tooltip where "No level" would be. Ignored for a
|
|
82
|
+
* feature with a level.
|
|
83
|
+
*/
|
|
84
|
+
detail?(props: P): string | undefined;
|
|
78
85
|
/** Whether `features()` lists this plugin's features. Default true. */
|
|
79
86
|
listed?: boolean;
|
|
80
87
|
/** Tooltip content for a feature, below its name; `props` are its source properties. */
|
|
@@ -93,6 +93,7 @@ export function pointsPlugin(options) {
|
|
|
93
93
|
if (!ref)
|
|
94
94
|
continue;
|
|
95
95
|
const [lng, lat] = geometry.coordinates;
|
|
96
|
+
const detail = options.detail?.(props)?.trim();
|
|
96
97
|
items.push({
|
|
97
98
|
refKey: props.refKey,
|
|
98
99
|
ref,
|
|
@@ -102,7 +103,8 @@ export function pointsPlugin(options) {
|
|
|
102
103
|
units: scale?.units ?? "",
|
|
103
104
|
precision,
|
|
104
105
|
lngLat: [lng, lat],
|
|
105
|
-
selectable: selectable && selectionId(ref) !== null
|
|
106
|
+
selectable: selectable && selectionId(ref) !== null,
|
|
107
|
+
...(detail ? { detail } : {})
|
|
106
108
|
});
|
|
107
109
|
}
|
|
108
110
|
return items;
|
|
@@ -209,15 +211,39 @@ export function pointsPlugin(options) {
|
|
|
209
211
|
}
|
|
210
212
|
return spec;
|
|
211
213
|
}
|
|
212
|
-
|
|
214
|
+
// The zoom range shows and hides the layers by the camera zoom, not with layer
|
|
215
|
+
// minzoom/maxzoom: on a globe MapLibre uses the next zoom's tiles a little before the camera
|
|
216
|
+
// gets there and leaves a layer out of tiles at or above its maxzoom, so two plugins whose
|
|
217
|
+
// ranges meet (fire groups below 10, hotspots from 10) would leave a gap where neither drew.
|
|
218
|
+
const { min, max } = options.zoomRange ?? {};
|
|
219
|
+
const ranged = min !== undefined || max !== undefined;
|
|
220
|
+
const inRange = () => {
|
|
221
|
+
const zoom = gl.getZoom();
|
|
222
|
+
return (min === undefined || zoom >= min) && (max === undefined || zoom < max);
|
|
223
|
+
};
|
|
224
|
+
let shown = ranged ? inRange() : true;
|
|
225
|
+
/** The spec, hidden while the camera is outside the plugin's zoom range. */
|
|
213
226
|
function zoomed(spec) {
|
|
214
|
-
|
|
227
|
+
if (!ranged)
|
|
228
|
+
return spec;
|
|
229
|
+
const layout = spec.layout;
|
|
215
230
|
return {
|
|
216
231
|
...spec,
|
|
217
|
-
...
|
|
218
|
-
...(max !== undefined ? { maxzoom: max } : {})
|
|
232
|
+
layout: { ...layout, visibility: shown ? "visible" : "none" }
|
|
219
233
|
};
|
|
220
234
|
}
|
|
235
|
+
const onZoom = () => {
|
|
236
|
+
if (dead || inRange() === shown)
|
|
237
|
+
return;
|
|
238
|
+
shown = !shown;
|
|
239
|
+
for (const layerId of layers) {
|
|
240
|
+
if (gl.getLayer(layerId)) {
|
|
241
|
+
gl.setLayoutProperty(layerId, "visibility", shown ? "visible" : "none");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
if (ranged)
|
|
246
|
+
gl.on("zoom", onZoom);
|
|
221
247
|
function removeLayers() {
|
|
222
248
|
for (const layerId of layers.toReversed()) {
|
|
223
249
|
if (gl.getLayer(layerId))
|
|
@@ -447,6 +473,8 @@ export function pointsPlugin(options) {
|
|
|
447
473
|
});
|
|
448
474
|
return () => {
|
|
449
475
|
dead = true;
|
|
476
|
+
if (ranged)
|
|
477
|
+
gl.off("zoom", onZoom);
|
|
450
478
|
providers = null;
|
|
451
479
|
live = null;
|
|
452
480
|
};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
inView,
|
|
10
10
|
optionIdPart,
|
|
11
11
|
rowLabel,
|
|
12
|
+
levelText,
|
|
12
13
|
shownLevel,
|
|
13
14
|
sortItems,
|
|
14
15
|
typeaheadIndex,
|
|
@@ -330,6 +331,7 @@
|
|
|
330
331
|
{#each windowRows as row, i (row.key)}
|
|
331
332
|
{@const index = range.start + i}
|
|
332
333
|
{@const level = shownLevel(row)}
|
|
334
|
+
{@const levelPlace = levelText(row, map.messages)}
|
|
333
335
|
{@const isOn = isSelected(row)}
|
|
334
336
|
<!-- Options aren't focused: the listbox holds focus and handles keys (aria-activedescendant). -->
|
|
335
337
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_interactive_supports_focus -->
|
|
@@ -360,12 +362,14 @@
|
|
|
360
362
|
{formatValue(row)}{#if row.units && row.value !== null} {row.units}{/if}
|
|
361
363
|
</span>
|
|
362
364
|
<span class="mapsdk-list-level text-xs">
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
365
|
+
{#if levelPlace.kind !== "detail"}
|
|
366
|
+
<span
|
|
367
|
+
class="mapsdk-list-swatch"
|
|
368
|
+
class:neutral={!level?.color}
|
|
369
|
+
style:background={level?.color || undefined}
|
|
370
|
+
></span>
|
|
371
|
+
{/if}
|
|
372
|
+
{levelPlace.text}
|
|
369
373
|
</span>
|
|
370
374
|
</div>
|
|
371
375
|
{/each}
|
package/dist/ui/Legend.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from "svelte";
|
|
2
|
+
import { onMount, untrack } from "svelte";
|
|
3
3
|
import "./controls.css";
|
|
4
4
|
import type { MapContext } from "../core/create-map.svelte.js";
|
|
5
5
|
import { watchTheme } from "../view/create-gl-map.js";
|
|
@@ -8,9 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
let {
|
|
10
10
|
map,
|
|
11
|
-
order = "bottom-first"
|
|
11
|
+
order = "bottom-first",
|
|
12
|
+
framed = true
|
|
12
13
|
}: {
|
|
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;
|
|
14
24
|
/**
|
|
15
25
|
* `"bottom-first"` (default) lists the bottom plugin's section first; `"top-first"`
|
|
16
26
|
* last.
|
|
@@ -43,30 +53,50 @@
|
|
|
43
53
|
)
|
|
44
54
|
);
|
|
45
55
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
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 } {
|
|
49
61
|
const ctx = document.createElement("canvas").getContext("2d");
|
|
50
|
-
if (!ctx) return css;
|
|
62
|
+
if (!ctx) return { rgb: css, opaque: true };
|
|
51
63
|
ctx.clearRect(0, 0, 1, 1);
|
|
52
64
|
ctx.fillStyle = css;
|
|
53
65
|
ctx.fillRect(0, 0, 1, 1);
|
|
54
|
-
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
55
|
-
return `rgb(${r}, ${g}, ${b})
|
|
66
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
67
|
+
return { rgb: `rgb(${r}, ${g}, ${b})`, opaque: a === 255 };
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return
|
|
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);
|
|
64
94
|
});
|
|
65
95
|
</script>
|
|
66
96
|
|
|
67
97
|
<section
|
|
68
98
|
bind:this={root}
|
|
69
|
-
class="mapsdk-legend"
|
|
99
|
+
class={["mapsdk-legend", !framed && "mapsdk-legend-frameless"]}
|
|
70
100
|
aria-label={map.messages("mapsdk_legend_label")}
|
|
71
101
|
hidden={entries.length === 0}
|
|
72
102
|
>
|
|
@@ -124,6 +154,17 @@
|
|
|
124
154
|
pointer-events: auto;
|
|
125
155
|
}
|
|
126
156
|
|
|
157
|
+
/* framed={false}: the host's card is the frame. */
|
|
158
|
+
.mapsdk-legend-frameless {
|
|
159
|
+
width: auto;
|
|
160
|
+
max-width: none;
|
|
161
|
+
padding: 0;
|
|
162
|
+
border: 0;
|
|
163
|
+
border-radius: 0;
|
|
164
|
+
background: none;
|
|
165
|
+
box-shadow: none;
|
|
166
|
+
}
|
|
167
|
+
|
|
127
168
|
.mapsdk-legend[hidden] {
|
|
128
169
|
display: none;
|
|
129
170
|
}
|
|
@@ -2,6 +2,15 @@ import "./controls.css";
|
|
|
2
2
|
import type { MapContext } from "../core/create-map.svelte.js";
|
|
3
3
|
type $$ComponentProps = {
|
|
4
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;
|
|
5
14
|
/**
|
|
6
15
|
* `"bottom-first"` (default) lists the bottom plugin's section first; `"top-first"`
|
|
7
16
|
* last.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { MessageProvider } from "../data/messages.js";
|
|
4
4
|
import type { FeatureListItem } from "../core/plugin.js";
|
|
5
5
|
import { NO_LEVEL } from "../data/scale.js";
|
|
6
|
+
import { levelText } from "./feature-list.js";
|
|
6
7
|
import { levelChip } from "./level-chip.js";
|
|
7
8
|
import { formatValue } from "./value-format.js";
|
|
8
9
|
|
|
@@ -19,7 +20,8 @@
|
|
|
19
20
|
|
|
20
21
|
// A level with an empty color shows its label on the neutral chip.
|
|
21
22
|
const level = $derived(item.level !== NO_LEVEL ? item.level : null);
|
|
22
|
-
|
|
23
|
+
// The level's label, else the item's detail (plain text, no chip), else "No level".
|
|
24
|
+
const levelPlace = $derived(levelText(item, messages));
|
|
23
25
|
const chipColors = $derived.by(() => {
|
|
24
26
|
const chip = levelChip(level?.color);
|
|
25
27
|
return chip ? `background:${chip.background};color:${chip.text}` : "";
|
|
@@ -39,9 +41,13 @@
|
|
|
39
41
|
{valueText}{#if item.units} {item.units}{/if}
|
|
40
42
|
</div>
|
|
41
43
|
{/if}
|
|
42
|
-
|
|
43
|
-
{
|
|
44
|
-
|
|
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}
|
|
45
51
|
</div>
|
|
46
52
|
|
|
47
53
|
<style>
|
|
@@ -27,5 +27,12 @@ export declare function shownLevel(item: FeatureListItem): Level | null;
|
|
|
27
27
|
export declare function sortItems(items: FeatureListItem[], by: SortBy): FeatureListItem[];
|
|
28
28
|
/** The first index at or after `from` (wrapping) whose name starts with `query`, any case; -1 if none. */
|
|
29
29
|
export declare function typeaheadIndex(items: FeatureListItem[], query: string, from: number): number;
|
|
30
|
-
/**
|
|
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). */
|
|
31
38
|
export declare function rowLabel(item: FeatureListItem, messages: MessageProvider, format: FormatProvider): string;
|
package/dist/ui/feature-list.js
CHANGED
|
@@ -65,16 +65,31 @@ export function typeaheadIndex(items, query, from) {
|
|
|
65
65
|
}
|
|
66
66
|
return -1;
|
|
67
67
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
/**
|
|
69
|
+
* What stands in an item's level place: its level's label, else its `detail`, else "No level".
|
|
70
|
+
*/
|
|
71
|
+
export function levelText(item, messages) {
|
|
72
72
|
const level = shownLevel(item);
|
|
73
|
+
if (level)
|
|
74
|
+
return { kind: "level", text: level.label };
|
|
75
|
+
const detail = item.detail?.trim();
|
|
76
|
+
if (detail)
|
|
77
|
+
return { kind: "detail", text: detail };
|
|
78
|
+
return { kind: "none", text: messages("mapsdk_no_level") };
|
|
79
|
+
}
|
|
80
|
+
/** A row's accessible name: name, value, units, and level label (or detail). */
|
|
81
|
+
export function rowLabel(item, messages, format) {
|
|
82
|
+
const level = levelText(item, messages);
|
|
83
|
+
if (item.value === null) {
|
|
84
|
+
return level.kind === "detail"
|
|
85
|
+
? messages("mapsdk_list_row_detail", { name: item.name, detail: level.text })
|
|
86
|
+
: messages("mapsdk_list_row_no_value", { name: item.name });
|
|
87
|
+
}
|
|
73
88
|
const params = {
|
|
74
89
|
name: item.name,
|
|
75
90
|
value: typeof item.value === "number" ? formatValue(item.value, item.precision, format) : item.value,
|
|
76
91
|
units: item.units,
|
|
77
|
-
level: level
|
|
92
|
+
level: level.text
|
|
78
93
|
};
|
|
79
94
|
return messages(item.units ? "mapsdk_list_row" : "mapsdk_list_row_no_units", params);
|
|
80
95
|
}
|