@protobase/ui 0.12.20 → 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.
Files changed (36) hide show
  1. package/dist/app/displays/cards-display.js +9 -140
  2. package/dist/app/displays/display-controls.js +3 -2
  3. package/dist/app/displays/list-displays.d.ts +1 -1
  4. package/dist/app/displays/list-displays.js +36 -3
  5. package/dist/app/displays/map-display.js +120 -0
  6. package/dist/app/displays/map-groups.js +55 -0
  7. package/dist/app/displays/record-card.js +171 -0
  8. package/dist/app/field-editor.js +1 -1
  9. package/dist/app/list-page.js +21 -9
  10. package/dist/app/map/record-map.js +2 -2
  11. package/dist/app/map/records-map.js +106 -0
  12. package/dist/app/named-icon.js +3 -2
  13. package/dist/app/pages/field-value.js +2 -2
  14. package/dist/app/record-page.js +1 -1
  15. package/dist/app-shell/app-shell.d.ts +1 -0
  16. package/dist/app-shell/back-target.js +12 -0
  17. package/dist/app-shell/breadcrumb.d.ts +1 -0
  18. package/dist/app-shell/breadcrumb.js +2 -1
  19. package/dist/app-shell/index.d.ts +1 -0
  20. package/dist/app-shell/top-bar.js +15 -2
  21. package/dist/package.js +1 -1
  22. package/dist/shell/assets/app-BYw-8qpE.js +66 -0
  23. package/dist/shell/assets/{index-CRZv7xHY.js → index-B8MhIaLr.js} +1 -1
  24. package/dist/shell/assets/index-CdSIk0BQ.css +2 -0
  25. package/dist/shell/assets/navigation-control-CGS0pamh.js +814 -0
  26. package/dist/shell/assets/record-map-9Y3IL_dg.js +1 -0
  27. package/dist/shell/assets/records-map-DXIWowJ3.js +1 -0
  28. package/dist/shell/assets/shared-protobase-ui-Bol6wL_S.js +1 -0
  29. package/dist/shell/index.html +4 -4
  30. package/dist/shell/protobase.shell.json +1 -1
  31. package/package.json +4 -4
  32. package/dist/shell/assets/app-D72h8BfW.js +0 -66
  33. package/dist/shell/assets/index-UCjnDdFX.css +0 -2
  34. package/dist/shell/assets/maplibre-gl-B8lfaWX9.js +0 -1
  35. package/dist/shell/assets/record-map-DJgpmB9O.js +0 -814
  36. package/dist/shell/assets/shared-protobase-ui-DxujbpUX.js +0 -1
@@ -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";
@@ -2,4 +2,5 @@ import "./bottom-bar.js";
2
2
  import "./nav.js";
3
3
  import "./profile-menu.js";
4
4
  import "./global-search.js";
5
+ import "./breadcrumb.js";
5
6
  import { ReactNode } from "react";
@@ -0,0 +1,12 @@
1
+ //#region src/app-shell/back-target.ts
2
+ /** Where the mobile back button goes: the crumb just before the current page, when it has a page of its own. */
3
+ const backTarget = (parts) => {
4
+ const previous = parts.at(-2);
5
+ if (typeof previous !== "object" || !previous.href || previous.loading) return void 0;
6
+ return {
7
+ label: previous.label,
8
+ href: previous.href
9
+ };
10
+ };
11
+ //#endregion
12
+ export { backTarget };
@@ -0,0 +1 @@
1
+ import "react";
@@ -3,6 +3,7 @@ import { Skeleton } from "../primitives/skeleton.js";
3
3
  import { ChevronRight } from "lucide-react";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/app-shell/breadcrumb.tsx
6
+ /** A plain left click, which the app follows itself; others open a new tab. */
6
7
  const inApp = (event) => event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
7
8
  /** The trail of where you are; the last crumb, the current page, shows on phones too. */
8
9
  const Breadcrumb = ({ parts, onNavigate }) => /* @__PURE__ */ jsx("nav", {
@@ -41,4 +42,4 @@ const Breadcrumb = ({ parts, onNavigate }) => /* @__PURE__ */ jsx("nav", {
41
42
  })
42
43
  });
43
44
  //#endregion
44
- export { Breadcrumb };
45
+ export { Breadcrumb, inApp };
@@ -3,6 +3,7 @@ import "./nav.js";
3
3
  import "./profile-menu.js";
4
4
  import { SidebarMode, sidebarModes } from "./sidebar-mode.js";
5
5
  import "./global-search.js";
6
+ import "./breadcrumb.js";
6
7
  import "./app-shell.js";
7
8
  import "./page-header.js";
8
9
  import "./sidebar.js";
@@ -1,13 +1,15 @@
1
1
  import { cn } from "../lib/cn.js";
2
- import { Breadcrumb } from "./breadcrumb.js";
2
+ import { backTarget } from "./back-target.js";
3
+ import { Breadcrumb, inApp } from "./breadcrumb.js";
3
4
  import { GlobalSearch } from "./global-search.js";
4
5
  import { useEffect, useRef, useState } from "react";
5
- import { Menu, Search, X } from "lucide-react";
6
+ import { ChevronLeft, Menu, Search, X } from "lucide-react";
6
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
8
  //#region src/app-shell/top-bar.tsx
8
9
  const iconButton = "inline-flex size-11 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted md:hidden";
9
10
  const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControls, search }) => {
10
11
  const [searchOpen, setSearchOpen] = useState(false);
12
+ const back = backTarget(breadcrumb);
11
13
  const desktopSearch = useRef(null);
12
14
  useEffect(() => {
13
15
  if (!search) return;
@@ -33,6 +35,17 @@ const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControl
33
35
  className: cn(iconButton, "-ml-0.5"),
34
36
  children: /* @__PURE__ */ jsx(Menu, { className: "size-5" })
35
37
  }),
38
+ back && /* @__PURE__ */ jsx("a", {
39
+ href: back.href,
40
+ "aria-label": `Back to ${back.label}`,
41
+ onClick: (event) => {
42
+ if (!onNavigate || !inApp(event)) return;
43
+ event.preventDefault();
44
+ onNavigate(back.href);
45
+ },
46
+ className: cn(iconButton, !onMenu && "-ml-0.5"),
47
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-5" })
48
+ }),
36
49
  /* @__PURE__ */ jsx(Breadcrumb, {
37
50
  parts: breadcrumb,
38
51
  onNavigate
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.12.20";
2
+ var version = "0.12.22";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };