@protobase/ui 0.19.3 → 0.19.4

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.
@@ -25,7 +25,8 @@ const arrow = "flex size-8 items-center justify-center rounded-full border borde
25
25
  * elsewhere clears it. A marker and a card whose record's position is approximate say so.
26
26
  */
27
27
  const MapDisplay = ({ model, view, display, filter, orderBy, fields, at, onMove, footer = true, flush = false }) => {
28
- const style = useAdminMeta().maps?.style;
28
+ const maps = useAdminMeta().maps;
29
+ const style = maps?.style;
29
30
  const [chosen, setChosen] = useState();
30
31
  const [area, setArea] = useState();
31
32
  const settled = useDebounced(area, settleMs);
@@ -70,6 +71,7 @@ const MapDisplay = ({ model, view, display, filter, orderBy, fields, at, onMove,
70
71
  fallback: null,
71
72
  children: /* @__PURE__ */ jsx(RecordsMap, {
72
73
  style,
74
+ layers: maps?.layers,
73
75
  initial: at,
74
76
  points,
75
77
  buckets,
@@ -0,0 +1,11 @@
1
+ import { guarded } from "../../lib/guarded.js";
2
+ //#region src/app/map/map-layer-choice.ts
3
+ const mapLayerStorageKey = "protobase:map-layer";
4
+ /** The layer the browser last chose, if the app still offers it: else the style's own map, which is the default. */
5
+ const chosenMapLayer = (layers, id) => layers.find((layer) => layer.id === id);
6
+ /** The id of the layer chosen in this browser, if any; unavailable storage reads as the default map. */
7
+ const readMapLayer = (storage) => guarded(() => storage.getItem(mapLayerStorageKey), null);
8
+ /** Remembers a layer by its id, and the default map by forgetting the choice, so the default costs no storage. */
9
+ const writeMapLayer = (storage, id) => guarded(() => id === void 0 ? storage.removeItem(mapLayerStorageKey) : storage.setItem(mapLayerStorageKey, id), void 0);
10
+ //#endregion
11
+ export { chosenMapLayer, mapLayerStorageKey, readMapLayer, writeMapLayer };
@@ -0,0 +1,55 @@
1
+ import { Segmented } from "../../primitives/segmented.js";
2
+ import { chosenMapLayer, readMapLayer, writeMapLayer } from "./map-layer-choice.js";
3
+ import { useState } from "react";
4
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
+ import { Layer, Source, useMap } from "react-map-gl/maplibre";
6
+ //#region src/app/map/map-layer-switch.tsx
7
+ const styleValue = "";
8
+ /**
9
+ * Inside a map, a switch in its top left corner between the style's map and each of `layers`, and the chosen layer's
10
+ * tiles drawn over the style's, under `below` when the map has that layer, such as the areas around records. The
11
+ * layer's credit joins the map's attribution while it shows. Outside its bounds the style's map shows through. The
12
+ * choice is remembered in this browser, for every map. Nothing without layers.
13
+ */
14
+ const MapLayerSwitch = ({ layers, below }) => {
15
+ const [chosenId, setChosenId] = useState(() => readMapLayer(localStorage));
16
+ const { current: map } = useMap();
17
+ if (!layers || layers.length === 0) return null;
18
+ const chosen = chosenMapLayer(layers, chosenId);
19
+ const choose = (value) => {
20
+ const id = value === styleValue ? void 0 : value;
21
+ writeMapLayer(localStorage, id);
22
+ setChosenId(id ?? null);
23
+ };
24
+ const beforeId = below && map?.getLayer(below) ? below : void 0;
25
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [chosen && /* @__PURE__ */ jsx(Source, {
26
+ id: `map-layer-${chosen.id}`,
27
+ type: "raster",
28
+ tiles: [chosen.tiles],
29
+ tileSize: 256,
30
+ attribution: chosen.attribution,
31
+ maxzoom: chosen.maxzoom ?? 22,
32
+ ...chosen.bounds && { bounds: chosen.bounds },
33
+ children: /* @__PURE__ */ jsx(Layer, {
34
+ id: `map-layer-${chosen.id}`,
35
+ type: "raster",
36
+ ...beforeId && { beforeId }
37
+ })
38
+ }, chosen.id), /* @__PURE__ */ jsx("div", {
39
+ className: "absolute left-2.5 top-2.5 z-10 rounded-md shadow",
40
+ children: /* @__PURE__ */ jsx(Segmented, {
41
+ label: "Map layer",
42
+ value: chosen?.id ?? styleValue,
43
+ onChange: choose,
44
+ options: [{
45
+ value: styleValue,
46
+ label: "Map"
47
+ }, ...layers.map((layer) => ({
48
+ value: layer.id,
49
+ label: layer.label
50
+ }))]
51
+ })
52
+ })] });
53
+ };
54
+ //#endregion
55
+ export { MapLayerSwitch };
@@ -12,7 +12,8 @@ const defaultZoom = 15;
12
12
  * `/meta` names. Without a style, or for a record without a position, there is nothing to draw and no section.
13
13
  */
14
14
  const MapSection = ({ item, record }) => {
15
- const style = useAdminMeta().maps?.style;
15
+ const maps = useAdminMeta().maps;
16
+ const style = maps?.style;
16
17
  const position = recordPosition(record, item);
17
18
  if (!style || !position) return null;
18
19
  const map = /* @__PURE__ */ jsx("div", {
@@ -21,6 +22,7 @@ const MapSection = ({ item, record }) => {
21
22
  fallback: null,
22
23
  children: /* @__PURE__ */ jsx(RecordMap, {
23
24
  style,
25
+ layers: maps?.layers,
24
26
  position,
25
27
  zoom: item.zoom ?? defaultZoom
26
28
  }, `${position.latitude},${position.longitude}`)
@@ -2,6 +2,8 @@ import { areaFeatures, fadeIn } from "./area-features.js";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Layer, Source } from "react-map-gl/maplibre";
4
4
  //#region src/app/map/position-areas.tsx
5
+ /** The areas' lowest layer, which a layer of tiles goes under. */
6
+ const areaLayerId = "position-area-fills";
5
7
  const polygons = [
6
8
  "==",
7
9
  ["get", "kind"],
@@ -38,7 +40,7 @@ const PositionAreas = ({ points, selected, colors }) => {
38
40
  data,
39
41
  children: [
40
42
  /* @__PURE__ */ jsx(Layer, {
41
- id: "position-area-fills",
43
+ id: areaLayerId,
42
44
  type: "fill",
43
45
  filter: polygons,
44
46
  paint: {
@@ -75,4 +77,4 @@ const PositionAreas = ({ points, selected, colors }) => {
75
77
  });
76
78
  };
77
79
  //#endregion
78
- export { PositionAreas };
80
+ export { PositionAreas, areaLayerId };
@@ -1,3 +1,4 @@
1
+ import { MapLayerSwitch } from "./map-layer-switch.js";
1
2
  import { mapLib } from "./maplibre.js";
2
3
  import { MapPin } from "lucide-react";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -5,9 +6,9 @@ import { Map as Map$1, Marker, NavigationControl } from "react-map-gl/maplibre";
5
6
  //#region src/app/map/record-map.tsx
6
7
  /**
7
8
  * A map of `style` centred on `position` at `zoom`, with a pin on it. It pans and zooms with two fingers on a phone and
8
- * with Ctrl or Cmd and the wheel, so scrolling the page past it does not move it.
9
+ * with Ctrl or Cmd and the wheel, so scrolling the page past it does not move it. With `layers`, it switches to them.
9
10
  */
10
- const RecordMap = ({ style, position, zoom }) => /* @__PURE__ */ jsxs(Map$1, {
11
+ const RecordMap = ({ style, layers, position, zoom }) => /* @__PURE__ */ jsxs(Map$1, {
11
12
  mapLib,
12
13
  mapStyle: style,
13
14
  initialViewState: {
@@ -19,16 +20,20 @@ const RecordMap = ({ style, position, zoom }) => /* @__PURE__ */ jsxs(Map$1, {
19
20
  width: "100%",
20
21
  height: "100%"
21
22
  },
22
- children: [/* @__PURE__ */ jsx(NavigationControl, { showCompass: false }), /* @__PURE__ */ jsx(Marker, {
23
- latitude: position.latitude,
24
- longitude: position.longitude,
25
- anchor: "bottom",
26
- children: /* @__PURE__ */ jsx(MapPin, {
27
- "aria-hidden": true,
28
- className: "size-8 fill-primary text-primary-foreground drop-shadow",
29
- strokeWidth: 1.5
23
+ children: [
24
+ /* @__PURE__ */ jsx(NavigationControl, { showCompass: false }),
25
+ /* @__PURE__ */ jsx(MapLayerSwitch, { layers }),
26
+ /* @__PURE__ */ jsx(Marker, {
27
+ latitude: position.latitude,
28
+ longitude: position.longitude,
29
+ anchor: "bottom",
30
+ children: /* @__PURE__ */ jsx(MapPin, {
31
+ "aria-hidden": true,
32
+ className: "size-8 fill-primary text-primary-foreground drop-shadow",
33
+ strokeWidth: 1.5
34
+ })
30
35
  })
31
- })]
36
+ ]
32
37
  });
33
38
  //#endregion
34
39
  export { RecordMap };
@@ -1,7 +1,8 @@
1
1
  import { cn } from "../../lib/cn.js";
2
2
  import { groupPoints } from "../displays/map-groups.js";
3
+ import { MapLayerSwitch } from "./map-layer-switch.js";
3
4
  import { mapLib } from "./maplibre.js";
4
- import { PositionAreas } from "./position-areas.js";
5
+ import { PositionAreas, areaLayerId } from "./position-areas.js";
5
6
  import { themeColor } from "./theme-color.js";
6
7
  import { useEffect, useRef, useState } from "react";
7
8
  import { MapPin } from "lucide-react";
@@ -32,7 +33,7 @@ const corners = (bounds) => [[bounds.west, bounds.south], [bounds.east, bounds.n
32
33
  * marker selects its records; choosing the map elsewhere clears the selection. A chosen marker the covered bottom would
33
34
  * hide moves up into the open part of the map.
34
35
  */
35
- const RecordsMap = ({ style, initial, points, buckets, fit, onFitted, onMove, selected, covered, onSelect, onClear, markerLabel, bucketLabel }) => {
36
+ const RecordsMap = ({ style, layers, initial, points, buckets, fit, onFitted, onMove, selected, covered, onSelect, onClear, markerLabel, bucketLabel }) => {
36
37
  const ref = useRef(null);
37
38
  const [groups, setGroups] = useState([]);
38
39
  const [colors, setColors] = useState();
@@ -117,6 +118,10 @@ const RecordsMap = ({ style, initial, points, buckets, fit, onFitted, onMove, se
117
118
  onClick: onClear,
118
119
  children: [
119
120
  /* @__PURE__ */ jsx(NavigationControl, { showCompass: false }),
121
+ /* @__PURE__ */ jsx(MapLayerSwitch, {
122
+ layers,
123
+ below: areaLayerId
124
+ }),
120
125
  colors && /* @__PURE__ */ jsx(PositionAreas, {
121
126
  points,
122
127
  selected,
@@ -1,4 +1,4 @@
1
- import { PluginRef, ResourcePermissions, SourceRef } from "@protobase/client";
1
+ import { MapLayerModel, PluginRef, ResourcePermissions, SourceRef } from "@protobase/client";
2
2
  import { BottomBarModel, ResourceModel, SignInViewModel, UserMenuModel, ViewModel } from "@protobase/schema";
3
3
  import { PageModel } from "@protobase/layout";
4
4
  //#region src/data/meta-data.d.ts
@@ -27,9 +27,10 @@ export type MetaData = {
27
27
  dsn: string;
28
28
  environment?: string;
29
29
  };
30
- /** The map style the record page's maps are drawn with, when the app has one (see `MapSection`). */
30
+ /** The map style the app's maps are drawn with, and the layers they may switch to, when the app has one (see `MapSection`). */
31
31
  maps?: {
32
32
  style: string;
33
+ layers?: MapLayerModel[];
33
34
  };
34
35
  /** The hosts besides the app's own whose images are drawn with the page's referrer (see `FileImage`). */
35
36
  files?: {
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.19.3";
2
+ var version = "0.19.4";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };