@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.
- package/dist/app/displays/cards-display.js +9 -140
- package/dist/app/displays/display-controls.js +3 -2
- package/dist/app/displays/list-displays.d.ts +1 -1
- package/dist/app/displays/list-displays.js +36 -3
- package/dist/app/displays/map-display.js +120 -0
- package/dist/app/displays/map-groups.js +55 -0
- package/dist/app/displays/record-card.js +171 -0
- package/dist/app/field-editor.js +1 -1
- package/dist/app/list-page.js +21 -9
- package/dist/app/map/record-map.js +2 -2
- package/dist/app/map/records-map.js +106 -0
- package/dist/app/named-icon.js +3 -2
- package/dist/app/pages/field-value.js +2 -2
- package/dist/app/record-page.js +1 -1
- package/dist/app-shell/app-shell.d.ts +1 -0
- package/dist/app-shell/back-target.js +12 -0
- package/dist/app-shell/breadcrumb.d.ts +1 -0
- package/dist/app-shell/breadcrumb.js +2 -1
- package/dist/app-shell/index.d.ts +1 -0
- package/dist/app-shell/top-bar.js +15 -2
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-BYw-8qpE.js +66 -0
- package/dist/shell/assets/{index-CRZv7xHY.js → index-B8MhIaLr.js} +1 -1
- package/dist/shell/assets/index-CdSIk0BQ.css +2 -0
- package/dist/shell/assets/navigation-control-CGS0pamh.js +814 -0
- package/dist/shell/assets/record-map-9Y3IL_dg.js +1 -0
- package/dist/shell/assets/records-map-DXIWowJ3.js +1 -0
- package/dist/shell/assets/shared-protobase-ui-Bol6wL_S.js +1 -0
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-D72h8BfW.js +0 -66
- package/dist/shell/assets/index-UCjnDdFX.css +0 -2
- package/dist/shell/assets/maplibre-gl-B8lfaWX9.js +0 -1
- package/dist/shell/assets/record-map-DJgpmB9O.js +0 -814
- package/dist/shell/assets/shared-protobase-ui-DxujbpUX.js +0 -1
package/dist/app/list-page.js
CHANGED
|
@@ -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
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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 };
|
package/dist/app/named-icon.js
CHANGED
|
@@ -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";
|
package/dist/app/record-page.js
CHANGED
|
@@ -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";
|
|
@@ -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 };
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
-
import {
|
|
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