@protobase/ui 0.12.21 → 0.12.22

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.
@@ -27,6 +27,7 @@ import { Sheet } from "../primitives/sheet.js";
27
27
  import { useDeleteFlow } from "./delete/use-delete-flow.js";
28
28
  import { useRestore } from "./delete/use-restore.js";
29
29
  import { CardsDisplay } from "./displays/cards-display.js";
30
+ import { MapDisplay } from "./displays/map-display.js";
30
31
  import { CustomDisplay } from "./displays/custom-display.js";
31
32
  import { DisplaySwitch, SortMenu } from "./displays/display-controls.js";
32
33
  import { useDisplayRows } from "./displays/use-display-rows.js";
@@ -251,15 +252,26 @@ const LoadedList = ({ model, address }) => {
251
252
  pinKey: auth.kind === "signed-in" ? pinnedStorageKey(auth.user.id, resource) : void 0,
252
253
  className: "min-h-0 flex-1"
253
254
  }) : /* @__PURE__ */ jsx("div", { className: "flex-1 rounded-lg border bg-surface" });
254
- const shown = !display ? table : display.kind === "cards" ? /* @__PURE__ */ jsx(CardsDisplay, {
255
- model,
256
- view,
257
- display,
258
- list: displayRows
259
- }) : /* @__PURE__ */ jsx(CustomDisplay, {
260
- display,
261
- list: displayRows
262
- });
255
+ const drawDisplay = () => {
256
+ if (!display) return table;
257
+ if (display.kind === "cards") return /* @__PURE__ */ jsx(CardsDisplay, {
258
+ model,
259
+ view,
260
+ display,
261
+ list: displayRows
262
+ });
263
+ if (display.kind === "map") return /* @__PURE__ */ jsx(MapDisplay, {
264
+ model,
265
+ view,
266
+ display,
267
+ list: displayRows
268
+ });
269
+ return /* @__PURE__ */ jsx(CustomDisplay, {
270
+ display,
271
+ list: displayRows
272
+ });
273
+ };
274
+ const shown = drawDisplay();
263
275
  return /* @__PURE__ */ jsxs("div", {
264
276
  className: "flex min-h-0 flex-1 flex-col gap-3 px-5 py-4",
265
277
  children: [
@@ -4,7 +4,7 @@ import * as maplibre from "maplibre-gl";
4
4
  import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url";
5
5
  import "maplibre-gl/dist/maplibre-gl.css";
6
6
  import { Protocol } from "pmtiles";
7
- import { Map, Marker, NavigationControl } from "react-map-gl/maplibre";
7
+ import { Map as Map$1, Marker, NavigationControl } from "react-map-gl/maplibre";
8
8
  //#region src/app/map/record-map.tsx
9
9
  maplibre.setWorkerUrl(workerUrl);
10
10
  maplibre.addProtocol("pmtiles", new Protocol().tile);
@@ -12,7 +12,7 @@ maplibre.addProtocol("pmtiles", new Protocol().tile);
12
12
  * 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
13
13
  * with Ctrl or Cmd and the wheel, so scrolling the page past it does not move it.
14
14
  */
15
- const RecordMap = ({ style, position, zoom }) => /* @__PURE__ */ jsxs(Map, {
15
+ const RecordMap = ({ style, position, zoom }) => /* @__PURE__ */ jsxs(Map$1, {
16
16
  mapLib: maplibre,
17
17
  mapStyle: style,
18
18
  initialViewState: {
@@ -0,0 +1,106 @@
1
+ import { cn } from "../../lib/cn.js";
2
+ import { groupPoints, pointBounds } from "../displays/map-groups.js";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { MapPin } from "lucide-react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import * as maplibre from "maplibre-gl";
7
+ import { Map as Map$1, Marker, NavigationControl } from "react-map-gl/maplibre";
8
+ //#region src/app/map/records-map.tsx
9
+ const groupRadius = 28;
10
+ const fitOptions = {
11
+ padding: 48,
12
+ maxZoom: 15,
13
+ duration: 0
14
+ };
15
+ const markerHeight = 32;
16
+ /**
17
+ * A map of `style` with a marker for each group of records that lie close together at the zoom, showing how many it
18
+ * stands for. It fits the records when they change. Choosing a marker selects its records; choosing the map elsewhere
19
+ * clears the selection. A chosen marker the covered bottom would hide moves up into the open part of the map.
20
+ */
21
+ const RecordsMap = ({ style, points, selected, covered, onSelect, onClear, markerLabel }) => {
22
+ const ref = useRef(null);
23
+ const [groups, setGroups] = useState([]);
24
+ const byKey = new globalThis.Map(points.map((point) => [point.key, point]));
25
+ const pointKeys = points.map((point) => point.key).join(",");
26
+ const bounds = pointBounds(points);
27
+ const regroup = () => {
28
+ const map = ref.current?.getMap();
29
+ if (!map) return;
30
+ setGroups(groupPoints(points.map((point) => ({
31
+ key: point.key,
32
+ ...map.project([point.longitude, point.latitude])
33
+ })), groupRadius));
34
+ };
35
+ useEffect(() => {
36
+ const map = ref.current?.getMap();
37
+ if (!map) return;
38
+ if (bounds) map.fitBounds(bounds, fitOptions);
39
+ regroup();
40
+ }, [pointKeys]);
41
+ const chosenAt = groups.find((group) => selected !== void 0 && group.keys.some((key) => selected.includes(key)))?.at;
42
+ useEffect(() => {
43
+ const map = ref.current?.getMap();
44
+ const at = chosenAt === void 0 ? void 0 : byKey.get(chosenAt);
45
+ if (!map || !at || covered === 0) return;
46
+ const { y } = map.project([at.longitude, at.latitude]);
47
+ const open = map.getContainer().clientHeight - covered;
48
+ if (y > markerHeight && y < open) return;
49
+ map.easeTo({
50
+ center: [at.longitude, at.latitude],
51
+ offset: [0, (markerHeight - covered) / 2],
52
+ duration: 300
53
+ });
54
+ }, [chosenAt, covered]);
55
+ return /* @__PURE__ */ jsxs(Map$1, {
56
+ ref,
57
+ mapLib: maplibre,
58
+ mapStyle: style,
59
+ initialViewState: bounds ? {
60
+ bounds,
61
+ fitBoundsOptions: fitOptions
62
+ } : {
63
+ latitude: 0,
64
+ longitude: 0,
65
+ zoom: 1
66
+ },
67
+ style: {
68
+ width: "100%",
69
+ height: "100%"
70
+ },
71
+ onLoad: regroup,
72
+ onZoomEnd: regroup,
73
+ onClick: onClear,
74
+ children: [/* @__PURE__ */ jsx(NavigationControl, { showCompass: false }), groups.map((group) => {
75
+ const at = byKey.get(group.at);
76
+ if (!at) return null;
77
+ const chosen = selected !== void 0 && group.keys.some((key) => selected.includes(key));
78
+ return /* @__PURE__ */ jsx(Marker, {
79
+ latitude: at.latitude,
80
+ longitude: at.longitude,
81
+ anchor: "bottom",
82
+ style: { zIndex: chosen ? 1 : 0 },
83
+ children: /* @__PURE__ */ jsxs("button", {
84
+ type: "button",
85
+ "aria-label": markerLabel(group.keys),
86
+ "aria-pressed": chosen,
87
+ className: "relative block cursor-pointer",
88
+ onClick: (event) => {
89
+ event.stopPropagation();
90
+ onSelect(group.keys);
91
+ },
92
+ children: [/* @__PURE__ */ jsx(MapPin, {
93
+ "aria-hidden": true,
94
+ className: cn("size-8 drop-shadow", chosen ? "fill-foreground text-background" : "fill-primary text-primary-foreground"),
95
+ strokeWidth: 1.5
96
+ }), group.keys.length > 1 && /* @__PURE__ */ jsx("span", {
97
+ className: "absolute -right-2 -top-1 min-w-5 rounded-full border border-background bg-foreground px-1 text-center text-[11px] font-medium leading-4 text-background tabular-nums",
98
+ children: group.keys.length
99
+ })]
100
+ })
101
+ }, group.at);
102
+ })]
103
+ });
104
+ };
105
+ //#endregion
106
+ export { RecordsMap };
@@ -1,11 +1,12 @@
1
- import { FileText, RulerDimensionLine, Send, Wallet } from "lucide-react";
1
+ import { Bed, FileText, RulerDimensionLine, Send, Wallet } from "lucide-react";
2
2
  //#region src/app/named-icon.ts
3
3
  /** The icons an action's or a field's `icon` may name. */
4
4
  const byName = {
5
5
  wallet: Wallet,
6
6
  "file-text": FileText,
7
7
  send: Send,
8
- ruler: RulerDimensionLine
8
+ ruler: RulerDimensionLine,
9
+ bed: Bed
9
10
  };
10
11
  /** The icon a name stands for, or nothing for a name the app does not know. */
11
12
  const namedIcon = (name) => name !== void 0 && Object.hasOwn(byName, name) ? byName[name] : void 0;
@@ -2,10 +2,10 @@ import { cn } from "../../lib/cn.js";
2
2
  import { keys } from "../../data/query-keys.js";
3
3
  import { useClient } from "../../data/api-provider.js";
4
4
  import { fetchLabels } from "../../live/relation-labels.js";
5
- import { useAdminMeta } from "../meta-gate.js";
6
- import { AppLink } from "../use-app-address.js";
7
5
  import { fileShown } from "../../live/file-values.js";
8
6
  import { displayValue } from "../../live/display-value.js";
7
+ import { useAdminMeta } from "../meta-gate.js";
8
+ import { AppLink } from "../use-app-address.js";
9
9
  import { useShownWidget } from "../field-types/shown-widget.js";
10
10
  import { WidgetPart } from "../field-types/widget-part.js";
11
11
  import { ValueBadge } from "../value-badge.js";
@@ -7,10 +7,10 @@ import { useRecord } from "../data/use-record.js";
7
7
  import { recordTitle } from "../live/model-helpers.js";
8
8
  import { humanize } from "../live/naming.js";
9
9
  import { useProjectUi } from "./pages/project-ui.js";
10
+ import { displayValue } from "../live/display-value.js";
10
11
  import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
11
12
  import { useNavigationGuard, useRouter } from "./router.js";
12
13
  import { useAppAddress } from "./use-app-address.js";
13
- import { displayValue } from "../live/display-value.js";
14
14
  import { ErrorBanner } from "./error-banner.js";
15
15
  import { shownWidget } from "./field-types/shown-widget.js";
16
16
  import { WidgetPart } from "./field-types/widget-part.js";
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.12.21";
2
+ var version = "0.12.22";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };