@protobase/ui 0.15.5 → 0.16.1
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/account/two-factor-setup.js +6 -5
- package/dist/app/app.js +1 -1
- package/dist/app/create/create-page.js +1 -0
- package/dist/app/create/create-record-form.js +1 -0
- package/dist/app/current-address.d.ts +5 -1
- package/dist/app/current-address.js +2 -1
- package/dist/app/displays/map-display.js +50 -34
- package/dist/app/displays/map-groups.js +12 -3
- package/dist/app/displays/measured-heights.js +23 -0
- package/dist/app/displays/record-card.js +29 -17
- package/dist/app/displays/virtual-cards.js +12 -14
- package/dist/app/embedded-list/embedded-display.js +7 -3
- package/dist/app/embedded-list/embedded-list.js +5 -4
- package/dist/app/field-editor.js +9 -1
- package/dist/app/list-page.js +5 -3
- package/dist/app/map/approximate-position.js +76 -0
- package/dist/app/map/area-features.js +80 -0
- package/dist/app/map/maplibre.js +11 -0
- package/dist/app/map/position-areas.js +78 -0
- package/dist/app/map/record-map.js +2 -7
- package/dist/app/map/record-position.js +5 -4
- package/dist/app/map/records-map.js +24 -7
- package/dist/app/map/theme-color.js +23 -0
- package/dist/app/nav-recent.js +1 -1
- package/dist/app/pages/blocks/card-frame.js +2 -2
- package/dist/app/pages/blocks/grid.js +10 -2
- package/dist/app/pages/blocks/link.js +1 -1
- package/dist/app/pages/blocks/record-row.js +51 -0
- package/dist/app/pages/blocks/stat.js +9 -5
- package/dist/app/pages/field-value.js +1 -1
- package/dist/app/pages/render-node.js +2 -0
- package/dist/app/record-page.js +5 -1
- package/dist/app/record-row/record-row-body.js +114 -0
- package/dist/app/record-row/record-row.js +93 -0
- package/dist/app/record-row/row-window.js +20 -0
- package/dist/app/record-row/saved-record-row.js +42 -0
- package/dist/app/record-row/use-row-scroll.js +51 -0
- package/dist/app/router.js +7 -8
- package/dist/app/saved/saved-records.js +1 -1
- package/dist/app/search/search-model.js +1 -1
- package/dist/app/searches/filter-chip-list.js +28 -0
- package/dist/app/searches/save-search-button.js +72 -8
- package/dist/app/searches/saved-search-rows.js +73 -0
- package/dist/app/searches/saved-search-section.js +91 -73
- package/dist/app/searches/saved-search.js +7 -3
- package/dist/app/searches/saved-searches.js +3 -5
- package/dist/app/searches/search-controls.js +13 -2
- package/dist/app/searches/search-diff.js +97 -0
- package/dist/app/searches/search-name-dialog.js +62 -0
- package/dist/app/searches/update-search-dialog.js +196 -0
- package/dist/app/searches/use-opened-search.js +15 -0
- package/dist/app/searches/use-save-search.js +68 -21
- package/dist/app/searches/use-search-row.js +56 -0
- package/dist/app/use-record-editor.js +5 -3
- package/dist/app-shell/app-shell.d.ts +1 -0
- package/dist/app-shell/app-shell.js +4 -3
- package/dist/app-shell/page-scroll.js +17 -1
- package/dist/app-shell/sidebar.d.ts +1 -0
- package/dist/app-shell/sidebar.js +4 -4
- package/dist/app-shell/top-bar.js +4 -4
- package/dist/auth/auth-gate.js +18 -9
- package/dist/auth/auth-provider.js +1 -0
- package/dist/auth/code-form.js +77 -0
- package/dist/auth/email-code-form.js +18 -77
- package/dist/auth/forgot-password-form.js +10 -10
- package/dist/auth/forgot-password-page.js +2 -2
- package/dist/auth/resend-code-button.js +29 -0
- package/dist/auth/sign-in-form.js +7 -8
- package/dist/auth/sign-in-page.js +3 -3
- package/dist/auth/sign-in-prompt.js +13 -9
- package/dist/auth/sign-up-code-form.js +22 -0
- package/dist/auth/sign-up-details-form.js +81 -111
- package/dist/auth/sign-up-page.js +17 -8
- package/dist/auth/two-factor-form.js +3 -3
- package/dist/auth/use-sign-in-address.js +21 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lib/flip-guard.js +43 -0
- package/dist/lib/page-scroll-lock.js +20 -0
- package/dist/lib/use-focus-trap.js +4 -4
- package/dist/organizations/invitation-page.js +7 -6
- package/dist/package.js +1 -1
- package/dist/primitives/dialog.js +3 -2
- package/dist/primitives/otp-input.d.ts +21 -0
- package/dist/primitives/otp-input.js +117 -0
- package/dist/primitives/otp-value.js +23 -0
- package/dist/primitives/responsive-dialog.d.ts +3 -1
- package/dist/primitives/responsive-dialog.js +2 -1
- package/dist/primitives/route-link.d.ts +1 -0
- package/dist/primitives/route-link.js +20 -0
- package/dist/primitives/textarea.js +36 -0
- package/dist/record-view/field.js +56 -53
- package/dist/shell/assets/app-sDEk9LFu.js +66 -0
- package/dist/shell/assets/index-D0AzKE47.css +2 -0
- package/dist/shell/assets/{index-DxmdRnr8.js → index-L7nXyA_7.js} +1 -1
- package/dist/shell/assets/maplibre-AOh9A0NP.js +814 -0
- package/dist/shell/assets/record-map-DbwHSPf3.js +1 -0
- package/dist/shell/assets/records-map-2U_CQ4RG.js +1 -0
- package/dist/shell/assets/shared-protobase-ui-BMcStVNP.js +1 -0
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/styles.css +15 -0
- package/dist/toasts/toast.js +22 -6
- package/package.json +4 -4
- package/dist/auth/code-input.js +0 -17
- package/dist/auth/sign-in-address.js +0 -47
- package/dist/shell/assets/app-BdEV1_xI.js +0 -66
- package/dist/shell/assets/index-ibaHR5wQ.css +0 -2
- package/dist/shell/assets/maplibre-gl-DFcSGTFJ.js +0 -1
- package/dist/shell/assets/record-map-CL9bBLic.js +0 -814
- package/dist/shell/assets/records-map-DgtD0cK5.js +0 -1
- package/dist/shell/assets/shared-protobase-ui-ibIDwOpE.js +0 -1
- /package/dist/shell/assets/{record-map-DBE96hFo.css → maplibre-DBE96hFo.css} +0 -0
|
@@ -3,7 +3,7 @@ import { Input } from "../primitives/input.js";
|
|
|
3
3
|
import { authMessage } from "../auth/auth-messages.js";
|
|
4
4
|
import { Button } from "../primitives/button.js";
|
|
5
5
|
import { BackupCodes } from "./backup-codes.js";
|
|
6
|
-
import {
|
|
6
|
+
import { OtpInput } from "../primitives/otp-input.js";
|
|
7
7
|
import { TotpQr, totpKey } from "./totp-qr.js";
|
|
8
8
|
import { useState } from "react";
|
|
9
9
|
import { AuthError } from "@protobase/client";
|
|
@@ -49,8 +49,8 @@ const TwoFactorSetup = ({ offerEmailedCodes, needsPassword, onStartApp, onConfir
|
|
|
49
49
|
setup: await onStartApp(given)
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
-
const confirm = (setup) => attempt(async () => {
|
|
53
|
-
await onConfirmApp(
|
|
52
|
+
const confirm = (setup, entered = code) => attempt(async () => {
|
|
53
|
+
await onConfirmApp(entered);
|
|
54
54
|
setStep({
|
|
55
55
|
kind: "backup-codes",
|
|
56
56
|
codes: setup.backupCodes
|
|
@@ -103,13 +103,14 @@ const TwoFactorSetup = ({ offerEmailedCodes, needsPassword, onStartApp, onConfir
|
|
|
103
103
|
}),
|
|
104
104
|
/* @__PURE__ */ jsxs("label", {
|
|
105
105
|
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
106
|
-
children: ["Code from the app", /* @__PURE__ */ jsx(
|
|
106
|
+
children: ["Code from the app", /* @__PURE__ */ jsx(OtpInput, {
|
|
107
107
|
name: "code",
|
|
108
108
|
required: true,
|
|
109
109
|
autoFocus: true,
|
|
110
110
|
value: code,
|
|
111
111
|
invalid: Boolean(error),
|
|
112
|
-
onChange:
|
|
112
|
+
onChange: setCode,
|
|
113
|
+
onComplete: (complete) => !busy && void confirm(step.setup, complete)
|
|
113
114
|
})]
|
|
114
115
|
}),
|
|
115
116
|
errorLine,
|
package/dist/app/app.js
CHANGED
|
@@ -175,7 +175,7 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
|
175
175
|
breadcrumb,
|
|
176
176
|
user: acting && userToShell(acting),
|
|
177
177
|
onSignOut: user && (() => void signOut()),
|
|
178
|
-
|
|
178
|
+
signIn: user ? void 0 : signIn.link(),
|
|
179
179
|
workspace: current?.organization?.name ?? workspace,
|
|
180
180
|
navGroups: groups,
|
|
181
181
|
bottomBar: bottomBarFromMeta(meta, basePath, place),
|
|
@@ -117,6 +117,7 @@ const LoadedCreate = ({ resource, values }) => {
|
|
|
117
117
|
help: view?.fields[name]?.help,
|
|
118
118
|
required: isRequired(field),
|
|
119
119
|
error: errors[name],
|
|
120
|
+
wide: Boolean(view?.fields[name]?.multiline),
|
|
120
121
|
editor: /* @__PURE__ */ jsx(FieldEditor, {
|
|
121
122
|
field,
|
|
122
123
|
hints: view?.fields[name],
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { AppAddress, FilterLayout, MapView, ResourceModel, ViewModel } from "@protobase/schema";
|
|
2
2
|
//#region src/app/current-address.d.ts
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* A list as the app shows it: every value there, the view's default where the address names none; `map` and
|
|
5
|
+
* `savedSearch`, the saved search it was opened from, have none.
|
|
6
|
+
*/
|
|
4
7
|
export type ListAddress = {
|
|
5
8
|
kind: 'list';
|
|
6
9
|
resource: string;
|
|
@@ -9,6 +12,7 @@ export type ListAddress = {
|
|
|
9
12
|
orderBy: string;
|
|
10
13
|
filterLayout: FilterLayout;
|
|
11
14
|
map?: MapView;
|
|
15
|
+
savedSearch?: string;
|
|
12
16
|
};
|
|
13
17
|
/** The address the app shows, as logical values. A record's key is in the text form the record API takes. */
|
|
14
18
|
export type CurrentAddress = ListAddress | {
|
|
@@ -54,7 +54,8 @@ const changedList = (read, change, defaults) => {
|
|
|
54
54
|
...differs("display") && { display: next.display },
|
|
55
55
|
...differs("orderBy") && { orderBy: next.orderBy },
|
|
56
56
|
...differs("filterLayout") && { filterLayout: next.filterLayout },
|
|
57
|
-
...next.map && { map: next.map }
|
|
57
|
+
...next.map && { map: next.map },
|
|
58
|
+
...next.savedSearch && { savedSearch: next.savedSearch }
|
|
58
59
|
};
|
|
59
60
|
};
|
|
60
61
|
//#endregion
|
|
@@ -11,7 +11,7 @@ import { boundsAround, worldArea } from "../map/map-area.js";
|
|
|
11
11
|
import { keptSelection, mapPoints, stepSelection } from "./map-groups.js";
|
|
12
12
|
import { useMapRecords } from "./use-map-records.js";
|
|
13
13
|
import { Suspense, lazy, useLayoutEffect, useRef, useState } from "react";
|
|
14
|
-
import { ChevronLeft, ChevronRight, X } from "lucide-react";
|
|
14
|
+
import { ChevronLeft, ChevronRight, CircleDashed, X } from "lucide-react";
|
|
15
15
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
16
16
|
//#region src/app/displays/map-display.tsx
|
|
17
17
|
const RecordsMap = lazy(() => import("../map/records-map.js").then((module) => ({ default: module.RecordsMap })));
|
|
@@ -22,7 +22,7 @@ const arrow = "flex size-8 items-center justify-center rounded-full border borde
|
|
|
22
22
|
* in view or the map is close, else a count for each part of it, which zooms in when chosen. The map asks again as it
|
|
23
23
|
* pans and zooms. Without a view of its own it first fits every matching record. Choosing a marker shows the card of
|
|
24
24
|
* its record at the map's bottom, with arrows through the records the marker stands for; closing it or choosing the map
|
|
25
|
-
* elsewhere clears it.
|
|
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
28
|
const style = useAdminMeta().maps?.style;
|
|
@@ -48,7 +48,13 @@ const MapDisplay = ({ model, view, display, filter, orderBy, fields, at, onMove,
|
|
|
48
48
|
const singular = (view?.names?.singular ?? humanize(model.name)).toLowerCase();
|
|
49
49
|
const selection = keptSelection(chosen, rows);
|
|
50
50
|
const row = selection && rows.find((candidate) => candidate.key === selection.keys[selection.index]);
|
|
51
|
-
const
|
|
51
|
+
const approximate = new Set(points.filter((point) => point.approximate).map((point) => point.key));
|
|
52
|
+
const markerLabel = (keys) => {
|
|
53
|
+
const near = keys.filter((key) => approximate.has(key)).length;
|
|
54
|
+
if (keys.length === 1) return near === 1 ? `Show the ${singular}, at an approximate position` : `Show the ${singular}`;
|
|
55
|
+
if (near === 0) return `Show ${keys.length} ${plural}`;
|
|
56
|
+
return near === keys.length ? `Show ${keys.length} ${plural}, at approximate positions` : `Show ${keys.length} ${plural}, ${near} at approximate positions`;
|
|
57
|
+
};
|
|
52
58
|
const bucketLabel = (count) => `Zoom in on ${count.toLocaleString("en-IE")} ${count === 1 ? singular : plural}`;
|
|
53
59
|
const overlay = useRef(null);
|
|
54
60
|
const [covered, setCovered] = useState(0);
|
|
@@ -112,46 +118,56 @@ const MapDisplay = ({ model, view, display, filter, orderBy, fields, at, onMove,
|
|
|
112
118
|
"aria-label": `Chosen ${singular}`,
|
|
113
119
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
114
120
|
className: "flex items-center justify-end gap-1.5",
|
|
115
|
-
children: [
|
|
116
|
-
/* @__PURE__ */
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
onClick: () => setChosen(stepSelection(selection, -1)),
|
|
121
|
-
children: /* @__PURE__ */ jsx(ChevronLeft, {
|
|
122
|
-
className: "size-4",
|
|
121
|
+
children: [
|
|
122
|
+
approximate.has(row.key) && /* @__PURE__ */ jsxs("span", {
|
|
123
|
+
className: "mr-auto flex items-center gap-1 rounded-full border border-border bg-background px-2 py-1 text-xs",
|
|
124
|
+
children: [/* @__PURE__ */ jsx(CircleDashed, {
|
|
125
|
+
className: "size-3.5",
|
|
123
126
|
"aria-hidden": true
|
|
124
|
-
})
|
|
125
|
-
}),
|
|
126
|
-
/* @__PURE__ */ jsxs("span", {
|
|
127
|
-
className: "rounded-full border border-border bg-background px-2 py-1 text-xs tabular-nums",
|
|
128
|
-
"aria-live": "polite",
|
|
129
|
-
children: [
|
|
130
|
-
selection.index + 1,
|
|
131
|
-
" / ",
|
|
132
|
-
selection.keys.length
|
|
133
|
-
]
|
|
127
|
+
}), "Approximate position"]
|
|
134
128
|
}),
|
|
129
|
+
selection.keys.length > 1 && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
130
|
+
/* @__PURE__ */ jsx("button", {
|
|
131
|
+
type: "button",
|
|
132
|
+
"aria-label": "Previous",
|
|
133
|
+
className: arrow,
|
|
134
|
+
onClick: () => setChosen(stepSelection(selection, -1)),
|
|
135
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, {
|
|
136
|
+
className: "size-4",
|
|
137
|
+
"aria-hidden": true
|
|
138
|
+
})
|
|
139
|
+
}),
|
|
140
|
+
/* @__PURE__ */ jsxs("span", {
|
|
141
|
+
className: "rounded-full border border-border bg-background px-2 py-1 text-xs tabular-nums",
|
|
142
|
+
"aria-live": "polite",
|
|
143
|
+
children: [
|
|
144
|
+
selection.index + 1,
|
|
145
|
+
" / ",
|
|
146
|
+
selection.keys.length
|
|
147
|
+
]
|
|
148
|
+
}),
|
|
149
|
+
/* @__PURE__ */ jsx("button", {
|
|
150
|
+
type: "button",
|
|
151
|
+
"aria-label": "Next",
|
|
152
|
+
className: arrow,
|
|
153
|
+
onClick: () => setChosen(stepSelection(selection, 1)),
|
|
154
|
+
children: /* @__PURE__ */ jsx(ChevronRight, {
|
|
155
|
+
className: "size-4",
|
|
156
|
+
"aria-hidden": true
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
] }),
|
|
135
160
|
/* @__PURE__ */ jsx("button", {
|
|
136
161
|
type: "button",
|
|
137
|
-
"aria-label": "
|
|
162
|
+
"aria-label": "Close",
|
|
138
163
|
className: arrow,
|
|
139
|
-
onClick: () => setChosen(
|
|
140
|
-
children: /* @__PURE__ */ jsx(
|
|
164
|
+
onClick: () => setChosen(void 0),
|
|
165
|
+
children: /* @__PURE__ */ jsx(X, {
|
|
141
166
|
className: "size-4",
|
|
142
167
|
"aria-hidden": true
|
|
143
168
|
})
|
|
144
169
|
})
|
|
145
|
-
]
|
|
146
|
-
type: "button",
|
|
147
|
-
"aria-label": "Close",
|
|
148
|
-
className: arrow,
|
|
149
|
-
onClick: () => setChosen(void 0),
|
|
150
|
-
children: /* @__PURE__ */ jsx(X, {
|
|
151
|
-
className: "size-4",
|
|
152
|
-
"aria-hidden": true
|
|
153
|
-
})
|
|
154
|
-
})]
|
|
170
|
+
]
|
|
155
171
|
}), /* @__PURE__ */ jsx("div", {
|
|
156
172
|
className: "shadow-lg",
|
|
157
173
|
children: /* @__PURE__ */ jsx(RecordCard, {
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { recordPosition } from "../map/record-position.js";
|
|
2
|
+
import { isApproximate, positionArea } from "../map/approximate-position.js";
|
|
2
3
|
//#region src/app/displays/map-groups.ts
|
|
3
|
-
/** The rows with a position, in their order; a row without one is not on the map. */
|
|
4
|
+
/** The rows with a position, in their order, approximate ones marked with their area; a row without one is not on the map. */
|
|
4
5
|
const mapPoints = (rows, fields) => rows.flatMap((row) => {
|
|
5
6
|
const position = recordPosition(row.record, fields);
|
|
6
|
-
|
|
7
|
+
if (!position) return [];
|
|
8
|
+
if (!isApproximate(row.record, fields)) return [{
|
|
7
9
|
key: row.key,
|
|
8
10
|
...position
|
|
9
|
-
}]
|
|
11
|
+
}];
|
|
12
|
+
const area = positionArea(row.record, fields, position);
|
|
13
|
+
return [{
|
|
14
|
+
key: row.key,
|
|
15
|
+
...position,
|
|
16
|
+
approximate: true,
|
|
17
|
+
...area && { area }
|
|
18
|
+
}];
|
|
10
19
|
});
|
|
11
20
|
/**
|
|
12
21
|
* The markers for the points as the map draws them: in order, each point joins the first marker within `radius` pixels
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/app/displays/measured-heights.ts
|
|
2
|
+
/** How many card widths the list remembers a measured height for. */
|
|
3
|
+
const remembered = 8;
|
|
4
|
+
/**
|
|
5
|
+
* `heights` with a card `height` tall measured at `width`, kept at the taller of it and the height measured there
|
|
6
|
+
* before, and `heights` itself when that changes nothing. A width the list comes back to, as with and without a
|
|
7
|
+
* scrollbar, keeps its height instead of falling back to a guess: a guess shorter than the cards shrinks the list, which
|
|
8
|
+
* can take the scrollbar away, widen the cards back, and measure them taller again, every frame.
|
|
9
|
+
*/
|
|
10
|
+
const withMeasuredHeight = (heights, width, height) => {
|
|
11
|
+
const current = heights.get(width);
|
|
12
|
+
if (current !== void 0 && current >= height) return heights;
|
|
13
|
+
const next = new Map(heights);
|
|
14
|
+
next.delete(width);
|
|
15
|
+
next.set(width, height);
|
|
16
|
+
for (const oldest of next.keys()) {
|
|
17
|
+
if (next.size <= remembered) break;
|
|
18
|
+
next.delete(oldest);
|
|
19
|
+
}
|
|
20
|
+
return next;
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { withMeasuredHeight };
|
|
@@ -32,7 +32,7 @@ const MetaValue = ({ icon, children }) => {
|
|
|
32
32
|
* value, joined by bullets; or, with a `card` component, what it draws of the row, the built-in content again when it
|
|
33
33
|
* is missing or throws. With a favorite button, a heart in the top right corner saves the record.
|
|
34
34
|
*/
|
|
35
|
-
const RecordCard = ({ model, view, display, row, swipe = true }) => {
|
|
35
|
+
const RecordCard = ({ model, view, display, row, swipe = true, mark }) => {
|
|
36
36
|
const { image, title, subtitle, amount, badge, meta, footer, flags } = cardFields(model, view, display);
|
|
37
37
|
const label = (name) => view?.fields[name]?.label ?? humanize(name);
|
|
38
38
|
const text = (name) => {
|
|
@@ -99,7 +99,7 @@ const RecordCard = ({ model, view, display, row, swipe = true }) => {
|
|
|
99
99
|
})]
|
|
100
100
|
}),
|
|
101
101
|
subtitle !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
102
|
-
className: "truncate text-muted-foreground",
|
|
102
|
+
className: view?.fields[subtitle]?.multiline ? "line-clamp-3 whitespace-pre-line break-words text-muted-foreground" : "truncate text-muted-foreground",
|
|
103
103
|
children: shownValue(subtitle)
|
|
104
104
|
}),
|
|
105
105
|
shownAmount !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
@@ -135,32 +135,44 @@ const RecordCard = ({ model, view, display, row, swipe = true }) => {
|
|
|
135
135
|
}), shownValue(name)] }, name))
|
|
136
136
|
})
|
|
137
137
|
] });
|
|
138
|
-
const card = (picture) => /* @__PURE__ */
|
|
138
|
+
const card = (picture) => /* @__PURE__ */ jsxs(AppLink, {
|
|
139
139
|
to: {
|
|
140
140
|
kind: "record",
|
|
141
141
|
resource: model.name,
|
|
142
142
|
key: row.key
|
|
143
143
|
},
|
|
144
144
|
className: "flex h-full flex-col gap-1.5 rounded-lg border border-border bg-background p-3 text-[13px] hover:border-border-strong hover:bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
|
|
145
|
-
children:
|
|
145
|
+
children: [mark && !pictured && /* @__PURE__ */ jsx(Badge, {
|
|
146
|
+
tone: "blue",
|
|
147
|
+
className: "self-start",
|
|
148
|
+
children: mark
|
|
149
|
+
}), Card ? /* @__PURE__ */ jsx(CustomBoundary, {
|
|
146
150
|
name: display.card,
|
|
147
151
|
fallback: builtIn(picture),
|
|
148
152
|
children: /* @__PURE__ */ jsx(Card, { row })
|
|
149
|
-
}) : builtIn(picture)
|
|
153
|
+
}) : builtIn(picture)]
|
|
150
154
|
});
|
|
151
155
|
return /* @__PURE__ */ jsxs("div", {
|
|
152
|
-
className: photos ? "group/card relative h-full" : favorite ? "relative h-full" : "h-full",
|
|
153
|
-
children: [
|
|
154
|
-
photos
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
156
|
+
className: photos ? "group/card relative h-full" : favorite || mark ? "relative h-full" : "h-full",
|
|
157
|
+
children: [
|
|
158
|
+
photos ? /* @__PURE__ */ jsx(CardCarousel, {
|
|
159
|
+
photos: row.photos ?? [],
|
|
160
|
+
cover,
|
|
161
|
+
swipe,
|
|
162
|
+
children: card
|
|
163
|
+
}, row.key) : card(/* @__PURE__ */ jsx(CardImage, { value: cover })),
|
|
164
|
+
mark && pictured && /* @__PURE__ */ jsx(Badge, {
|
|
165
|
+
tone: "blue",
|
|
166
|
+
className: "pointer-events-none absolute top-2 left-2 shadow-sm",
|
|
167
|
+
children: mark
|
|
168
|
+
}),
|
|
169
|
+
favorite && /* @__PURE__ */ jsx(FavoriteButton, {
|
|
170
|
+
favorite,
|
|
171
|
+
recordKey: row.key,
|
|
172
|
+
target: recordKey(model, row.record),
|
|
173
|
+
className: pictured ? void 0 : "right-1.5 top-1.5"
|
|
174
|
+
})
|
|
175
|
+
]
|
|
164
176
|
});
|
|
165
177
|
};
|
|
166
178
|
//#endregion
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Skeleton } from "../../primitives/skeleton.js";
|
|
2
2
|
import { usePagedRows } from "../../data-grid/use-paged-rows.js";
|
|
3
3
|
import { useVirtualRows } from "../../data-grid/use-virtual-rows.js";
|
|
4
|
+
import { createFlipGuard } from "../../lib/flip-guard.js";
|
|
4
5
|
import { anchoredTop, cardColumns, cardWidth, recordRange } from "./card-layout.js";
|
|
6
|
+
import { withMeasuredHeight } from "./measured-heights.js";
|
|
5
7
|
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
6
8
|
import { jsx } from "react/jsx-runtime";
|
|
7
9
|
//#region src/app/displays/virtual-cards.tsx
|
|
@@ -9,7 +11,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
9
11
|
* Cards in rows as wide as the list, scrolled like the table by the page or the nearest scrolling ancestor: the list is
|
|
10
12
|
* as tall as all its rows, only the rows in view are drawn, and the records are loaded in the table's pages, so a
|
|
11
13
|
* change of width keeps the loaded pages and the first record in view. Every card in a width is as tall as the tallest
|
|
12
|
-
* drawn so far.
|
|
14
|
+
* drawn so far at that width.
|
|
13
15
|
*/
|
|
14
16
|
const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, children }) => {
|
|
15
17
|
const { total } = source;
|
|
@@ -17,11 +19,8 @@ const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, c
|
|
|
17
19
|
const [width, setWidth] = useState(0);
|
|
18
20
|
const columns = cardColumns(width);
|
|
19
21
|
const itemWidth = cardWidth(width, columns);
|
|
20
|
-
const [measured, setMeasured] = useState(
|
|
21
|
-
|
|
22
|
-
height: 0
|
|
23
|
-
});
|
|
24
|
-
const itemHeight = Math.ceil(Math.max(minHeight(itemWidth), measured.width === itemWidth ? measured.height : 0));
|
|
22
|
+
const [measured, setMeasured] = useState(() => /* @__PURE__ */ new Map());
|
|
23
|
+
const itemHeight = Math.ceil(Math.max(minHeight(itemWidth), measured.get(itemWidth) ?? 0));
|
|
25
24
|
const rowHeight = itemHeight + 12;
|
|
26
25
|
const layout = {
|
|
27
26
|
columns,
|
|
@@ -33,7 +32,7 @@ const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, c
|
|
|
33
32
|
});
|
|
34
33
|
const changed = anchor.current.top > 0 && (anchor.current.layout.columns !== columns || anchor.current.layout.rowHeight !== rowHeight);
|
|
35
34
|
const settle = (top) => changed ? anchoredTop(anchor.current.top, anchor.current.layout, layout) : top;
|
|
36
|
-
const ready = itemWidth > 0 && measured.
|
|
35
|
+
const ready = itemWidth > 0 && measured.has(itemWidth);
|
|
37
36
|
const scroll = useVirtualRows({
|
|
38
37
|
total: Math.ceil(total / columns),
|
|
39
38
|
rowHeight,
|
|
@@ -56,23 +55,22 @@ const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, c
|
|
|
56
55
|
loadRows: source.loadRows,
|
|
57
56
|
latencyMs: 0
|
|
58
57
|
}, range.start, range.end);
|
|
58
|
+
const [guard] = useState(() => createFlipGuard());
|
|
59
59
|
useLayoutEffect(() => {
|
|
60
60
|
const body = scroll.ref.current;
|
|
61
61
|
if (!body) return;
|
|
62
|
-
const
|
|
62
|
+
const update = () => setWidth(guard(body.clientWidth, performance.now()));
|
|
63
|
+
const observer = new ResizeObserver(update);
|
|
63
64
|
observer.observe(body);
|
|
64
|
-
|
|
65
|
+
update();
|
|
65
66
|
return () => observer.disconnect();
|
|
66
|
-
}, []);
|
|
67
|
+
}, [guard]);
|
|
67
68
|
const widthRef = useRef(itemWidth);
|
|
68
69
|
widthRef.current = itemWidth;
|
|
69
70
|
const [observer] = useState(() => new ResizeObserver((entries) => {
|
|
70
71
|
const tallest = Math.max(...entries.map((entry) => entry.borderBoxSize[0]?.blockSize ?? entry.contentRect.height));
|
|
71
72
|
const at = widthRef.current;
|
|
72
|
-
setMeasured((current) => current
|
|
73
|
-
width: at,
|
|
74
|
-
height: Math.max(tallest, current.width === at ? current.height : 0)
|
|
75
|
-
});
|
|
73
|
+
setMeasured((current) => withMeasuredHeight(current, at, tallest));
|
|
76
74
|
}));
|
|
77
75
|
useLayoutEffect(() => () => observer.disconnect(), [observer]);
|
|
78
76
|
const observe = useCallback((item) => {
|
|
@@ -17,10 +17,14 @@ const embeddedDisplay = (view, xs, { picked, preferred }) => {
|
|
|
17
17
|
if (picked !== void 0 && names.includes(picked)) return picked;
|
|
18
18
|
return defaultDisplay(view, xs, preferred !== void 0 && names.includes(preferred) ? preferred : void 0);
|
|
19
19
|
};
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The resource's list with the embedded list's filter, order and display, leaving out what the list does by default, and
|
|
22
|
+
* the saved search whose results it shows, if any. `resource` names another list of the same records, such as the
|
|
23
|
+
* favorites that list a user's saved listings.
|
|
24
|
+
*/
|
|
25
|
+
const viewAllAddress = (model, view, xs, { resource = model.name, ...list }) => changedList({
|
|
22
26
|
kind: "list",
|
|
23
|
-
resource
|
|
27
|
+
resource
|
|
24
28
|
}, list, listDefaults(model, view, xs));
|
|
25
29
|
//#endregion
|
|
26
30
|
export { embeddedDisplay, embeddedDisplayKey, readPickedDisplay, viewAllAddress, writePickedDisplay };
|
|
@@ -36,7 +36,7 @@ const Loading = () => /* @__PURE__ */ jsx("div", {
|
|
|
36
36
|
* The first `limit` records of a list, drawn by the list page's own table (sized to its rows) or display, and a link
|
|
37
37
|
* to the whole list with the same filter, order and display. On a phone a cards display is one row that scrolls sideways.
|
|
38
38
|
*/
|
|
39
|
-
const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empty, row = false, viewAll }) => {
|
|
39
|
+
const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empty, row = false, viewAll, link = true }) => {
|
|
40
40
|
const { resources, views } = useAdminMeta();
|
|
41
41
|
const { fields: fieldTypes } = useProjectUi();
|
|
42
42
|
const { go } = useAppAddress();
|
|
@@ -74,11 +74,12 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
|
|
|
74
74
|
}, [loaded]);
|
|
75
75
|
const shown = shownDisplay(view, display);
|
|
76
76
|
const plural = view?.names?.plural ?? humanize(model.name);
|
|
77
|
-
const allLink = /* @__PURE__ */ jsxs(AppLink, {
|
|
77
|
+
const allLink = link && /* @__PURE__ */ jsxs(AppLink, {
|
|
78
78
|
to: viewAllAddress(model, view, xs, viewAll ? {
|
|
79
79
|
filter: viewAll.filter,
|
|
80
80
|
orderBy: defaultOrderBy(model, view),
|
|
81
|
-
display
|
|
81
|
+
display,
|
|
82
|
+
savedSearch: viewAll.savedSearch
|
|
82
83
|
} : {
|
|
83
84
|
filter,
|
|
84
85
|
orderBy,
|
|
@@ -104,7 +105,7 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
|
|
|
104
105
|
className: "text-[13px] text-muted-foreground",
|
|
105
106
|
children: empty ?? "Nothing here yet."
|
|
106
107
|
});
|
|
107
|
-
return viewAll ? /* @__PURE__ */ jsxs("div", {
|
|
108
|
+
return viewAll && link ? /* @__PURE__ */ jsxs("div", {
|
|
108
109
|
className: "flex flex-col gap-3",
|
|
109
110
|
children: [nothing, allLink]
|
|
110
111
|
}) : nothing;
|
package/dist/app/field-editor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { Input } from "../primitives/input.js";
|
|
3
3
|
import { Switch } from "../primitives/switch.js";
|
|
4
|
+
import { Textarea } from "../primitives/textarea.js";
|
|
4
5
|
import { displayValue } from "../live/display-value.js";
|
|
5
6
|
import { AppLink } from "./use-app-address.js";
|
|
6
7
|
import { RelationPicker } from "./relation-picker.js";
|
|
@@ -68,8 +69,9 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
|
|
|
68
69
|
}) : label
|
|
69
70
|
});
|
|
70
71
|
}
|
|
72
|
+
const multiline = Boolean(hints?.multiline) && field.type === "text";
|
|
71
73
|
if (!isEditable(field) || locked) return /* @__PURE__ */ jsx("div", {
|
|
72
|
-
className: cn("py-1.5 text-[13px] tabular-nums", field.type === "decimal" || field.type === "integer" ? "font-medium" : "text-muted-foreground"),
|
|
74
|
+
className: cn("py-1.5 text-[13px] tabular-nums", field.type === "decimal" || field.type === "integer" ? "font-medium" : "text-muted-foreground", multiline && "whitespace-pre-line break-words"),
|
|
73
75
|
children: displayValue(field, stored, hints)
|
|
74
76
|
});
|
|
75
77
|
if (field.type === "enum") return /* @__PURE__ */ jsxs("select", {
|
|
@@ -84,6 +86,12 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
|
|
|
84
86
|
children: enumLabel(option, hints?.valueLabels)
|
|
85
87
|
}, option))]
|
|
86
88
|
});
|
|
89
|
+
if (multiline) return /* @__PURE__ */ jsx(Textarea, {
|
|
90
|
+
invalid,
|
|
91
|
+
placeholder,
|
|
92
|
+
value: String(value),
|
|
93
|
+
onChange: (event) => onChange(event.target.value)
|
|
94
|
+
});
|
|
87
95
|
return /* @__PURE__ */ jsx(Input, {
|
|
88
96
|
invalid,
|
|
89
97
|
leading: hints?.prefix ? /* @__PURE__ */ jsx("span", {
|
package/dist/app/list-page.js
CHANGED
|
@@ -39,14 +39,14 @@ import { ListChart } from "./list-chart.js";
|
|
|
39
39
|
import { ListSearch } from "./list-search.js";
|
|
40
40
|
import { withFieldTypeCells } from "./field-types/field-type-cells.js";
|
|
41
41
|
import { SavedList } from "./saved/saved-list.js";
|
|
42
|
-
import { facetNames, filterChips
|
|
42
|
+
import { facetNames, filterChips } from "./searches/saved-search.js";
|
|
43
43
|
import { SaveSearchButton } from "./searches/save-search-button.js";
|
|
44
44
|
import { SavedSearches } from "./searches/saved-searches.js";
|
|
45
45
|
import { useMemo, useState } from "react";
|
|
46
46
|
import { Plus, SlidersHorizontal } from "lucide-react";
|
|
47
47
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
48
|
-
import { tableDisplay } from "@protobase/schema/internal";
|
|
49
48
|
import { createWhere } from "@protobase/schema";
|
|
49
|
+
import { tableDisplay } from "@protobase/schema/internal";
|
|
50
50
|
//#region src/app/list-page.tsx
|
|
51
51
|
const where = createWhere();
|
|
52
52
|
const searchText = (extras) => extras.find((clause) => clause.kind === "search");
|
|
@@ -264,9 +264,11 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
264
264
|
]
|
|
265
265
|
});
|
|
266
266
|
const saveSearch = view?.saveSearch && !scope && parsed.ok && filterText && /* @__PURE__ */ jsx(SaveSearchButton, {
|
|
267
|
+
model,
|
|
268
|
+
view,
|
|
267
269
|
search: view.saveSearch,
|
|
268
270
|
filter: filterText,
|
|
269
|
-
|
|
271
|
+
chips: filterChips(model, view, filterText, facetNames(config))
|
|
270
272
|
});
|
|
271
273
|
const displaySwitch = (menu) => /* @__PURE__ */ jsx(DisplaySwitch, {
|
|
272
274
|
view,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { numeric } from "./record-position.js";
|
|
2
|
+
//#region src/app/map/approximate-position.ts
|
|
3
|
+
const earthRadius = 6371008.8;
|
|
4
|
+
const circleCorners = 64;
|
|
5
|
+
/** Whether the record's position is approximate, as its field states; without one, or a value that says neither, it is not. */
|
|
6
|
+
const isApproximate = (record, fields) => {
|
|
7
|
+
if (fields.approximate !== void 0) return record[fields.approximate] === true;
|
|
8
|
+
if (fields.exact !== void 0) return record[fields.exact] === false;
|
|
9
|
+
return false;
|
|
10
|
+
};
|
|
11
|
+
/** The polygon of `metres` around `center`, its corners as far from it along the globe. */
|
|
12
|
+
const circleArea = (center, metres) => {
|
|
13
|
+
const distance = metres / earthRadius;
|
|
14
|
+
const latitude = center.latitude * Math.PI / 180;
|
|
15
|
+
const longitude = center.longitude * Math.PI / 180;
|
|
16
|
+
return {
|
|
17
|
+
type: "Polygon",
|
|
18
|
+
coordinates: [Array.from({ length: 65 }, (_, index) => {
|
|
19
|
+
const bearing = index % circleCorners / circleCorners * 2 * Math.PI;
|
|
20
|
+
const to = Math.asin(Math.sin(latitude) * Math.cos(distance) + Math.cos(latitude) * Math.sin(distance) * Math.cos(bearing));
|
|
21
|
+
return [(longitude + Math.atan2(Math.sin(bearing) * Math.sin(distance) * Math.cos(latitude), Math.cos(distance) - Math.sin(latitude) * Math.sin(to))) * 180 / Math.PI, to * 180 / Math.PI];
|
|
22
|
+
})]
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
const isPosition = (value) => Array.isArray(value) && value.length >= 2 && typeof value[0] === "number" && typeof value[1] === "number" && Math.abs(value[0]) <= 180 && Math.abs(value[1]) <= 90;
|
|
26
|
+
const isLine = (value, least) => Array.isArray(value) && value.length >= least && value.every(isPosition);
|
|
27
|
+
const isPolygon = (value) => Array.isArray(value) && value.length > 0 && value.every((ring) => isLine(ring, 4));
|
|
28
|
+
const isLines = (value) => Array.isArray(value) && value.length > 0 && value.every((line) => isLine(line, 2));
|
|
29
|
+
const parsed = (value) => {
|
|
30
|
+
if (typeof value !== "string") return value;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(value);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof SyntaxError) return void 0;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/** The GeoJSON polygon or line a value holds, as a geometry or a feature of one; none for anything else, such as a point or text that is no GeoJSON. */
|
|
39
|
+
const areaGeometry = (value) => {
|
|
40
|
+
const data = parsed(value);
|
|
41
|
+
if (typeof data !== "object" || data === null) return void 0;
|
|
42
|
+
const geometry = data.type === "Feature" ? data.geometry : data;
|
|
43
|
+
if (typeof geometry !== "object" || geometry === null) return void 0;
|
|
44
|
+
const { type, coordinates } = geometry;
|
|
45
|
+
if (type === "Polygon" && isPolygon(coordinates)) return {
|
|
46
|
+
type,
|
|
47
|
+
coordinates
|
|
48
|
+
};
|
|
49
|
+
if (type === "MultiPolygon" && Array.isArray(coordinates) && coordinates.length > 0 && coordinates.every(isPolygon)) return {
|
|
50
|
+
type,
|
|
51
|
+
coordinates
|
|
52
|
+
};
|
|
53
|
+
if (type === "LineString" && isLine(coordinates, 2)) return {
|
|
54
|
+
type,
|
|
55
|
+
coordinates
|
|
56
|
+
};
|
|
57
|
+
if (type === "MultiLineString" && isLines(coordinates)) return {
|
|
58
|
+
type,
|
|
59
|
+
coordinates
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* What the map draws around the record's approximate position: the area its field holds, else a circle of the radius
|
|
64
|
+
* its field holds, else one of the display's default radius. An exact position, or one whose record states none of
|
|
65
|
+
* them on a display without a default, has none.
|
|
66
|
+
*/
|
|
67
|
+
const positionArea = (record, fields, position) => {
|
|
68
|
+
if (!isApproximate(record, fields)) return void 0;
|
|
69
|
+
const area = fields.area === void 0 ? void 0 : areaGeometry(record[fields.area]);
|
|
70
|
+
if (area) return area;
|
|
71
|
+
const radius = fields.radius === void 0 ? void 0 : numeric(record[fields.radius]);
|
|
72
|
+
if (radius !== void 0 && Number.isFinite(radius) && radius > 0) return circleArea(position, radius);
|
|
73
|
+
return fields.defaultRadius === void 0 ? void 0 : circleArea(position, fields.defaultRadius);
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { areaGeometry, circleArea, isApproximate, positionArea };
|