@protobase/ui 0.15.5 → 0.16.0
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/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 +2 -2
- package/dist/app/displays/virtual-cards.js +12 -14
- package/dist/app/field-editor.js +9 -1
- package/dist/app/list-page.js +1 -1
- 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/stat.js +9 -5
- package/dist/app/pages/field-value.js +1 -1
- package/dist/app/record-page.js +5 -1
- 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/use-save-search.js +4 -3
- 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/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/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-DJAZP1yB.js +66 -0
- package/dist/shell/assets/index-C5pYkUfv.css +2 -0
- package/dist/shell/assets/{index-DxmdRnr8.js → index-LBhMBiK4.js} +1 -1
- package/dist/shell/assets/maplibre-Dy0ZTnFZ.js +814 -0
- package/dist/shell/assets/record-map-BiWSbqqT.js +1 -0
- package/dist/shell/assets/records-map-DabYc3ad.js +1 -0
- package/dist/shell/assets/shared-protobase-ui-BURelnPG.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
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
//#region src/app/map/record-position.ts
|
|
2
|
-
|
|
2
|
+
/** A number field's value: it comes as a number, or as text for a decimal or bigint; anything else, or an empty value, is none. */
|
|
3
|
+
const numeric = (value) => {
|
|
3
4
|
if (typeof value === "number") return value;
|
|
4
5
|
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
5
6
|
return Number(value);
|
|
6
7
|
};
|
|
7
8
|
/** The record's position from its two fields, when both hold degrees on the globe; none otherwise, so no map is drawn. */
|
|
8
9
|
const recordPosition = (record, fields) => {
|
|
9
|
-
const latitude =
|
|
10
|
-
const longitude =
|
|
10
|
+
const latitude = numeric(record[fields.latitude]);
|
|
11
|
+
const longitude = numeric(record[fields.longitude]);
|
|
11
12
|
if (latitude === void 0 || longitude === void 0) return void 0;
|
|
12
13
|
if (!(Math.abs(latitude) <= 90 && Math.abs(longitude) <= 180)) return void 0;
|
|
13
14
|
return {
|
|
@@ -16,4 +17,4 @@ const recordPosition = (record, fields) => {
|
|
|
16
17
|
};
|
|
17
18
|
};
|
|
18
19
|
//#endregion
|
|
19
|
-
export { recordPosition };
|
|
20
|
+
export { numeric, recordPosition };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { cn } from "../../lib/cn.js";
|
|
2
2
|
import { groupPoints } from "../displays/map-groups.js";
|
|
3
|
+
import { mapLib } from "./maplibre.js";
|
|
4
|
+
import { PositionAreas } from "./position-areas.js";
|
|
5
|
+
import { themeColor } from "./theme-color.js";
|
|
3
6
|
import { useEffect, useRef, useState } from "react";
|
|
4
7
|
import { MapPin } from "lucide-react";
|
|
5
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import * as maplibre from "maplibre-gl";
|
|
7
9
|
import { Map as Map$1, Marker, NavigationControl } from "react-map-gl/maplibre";
|
|
8
10
|
//#region src/app/map/records-map.tsx
|
|
9
11
|
const groupRadius = 28;
|
|
@@ -25,13 +27,15 @@ const compact = new Intl.NumberFormat("en-IE", {
|
|
|
25
27
|
const corners = (bounds) => [[bounds.west, bounds.south], [bounds.east, bounds.north]];
|
|
26
28
|
/**
|
|
27
29
|
* A map of `style` with a marker for each group of records that lie close together at the zoom, showing how many it
|
|
28
|
-
* stands for, and a circle with the count of each bucket, which zooms in on its records when chosen.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
30
|
+
* stands for, and a circle with the count of each bucket, which zooms in on its records when chosen. A marker whose
|
|
31
|
+
* records' positions are all approximate is drawn hollow with a dashed edge, over the areas around them. Choosing a
|
|
32
|
+
* marker selects its records; choosing the map elsewhere clears the selection. A chosen marker the covered bottom would
|
|
33
|
+
* hide moves up into the open part of the map.
|
|
31
34
|
*/
|
|
32
35
|
const RecordsMap = ({ style, initial, points, buckets, fit, onFitted, onMove, selected, covered, onSelect, onClear, markerLabel, bucketLabel }) => {
|
|
33
36
|
const ref = useRef(null);
|
|
34
37
|
const [groups, setGroups] = useState([]);
|
|
38
|
+
const [colors, setColors] = useState();
|
|
35
39
|
const byKey = new globalThis.Map(points.map((point) => [point.key, point]));
|
|
36
40
|
const pointKeys = points.map((point) => point.key).join(",");
|
|
37
41
|
const fitKey = fit && JSON.stringify(fit);
|
|
@@ -87,7 +91,7 @@ const RecordsMap = ({ style, initial, points, buckets, fit, onFitted, onMove, se
|
|
|
87
91
|
}, [chosenAt, covered]);
|
|
88
92
|
return /* @__PURE__ */ jsxs(Map$1, {
|
|
89
93
|
ref,
|
|
90
|
-
mapLib
|
|
94
|
+
mapLib,
|
|
91
95
|
mapStyle: style,
|
|
92
96
|
initialViewState: initial ?? {
|
|
93
97
|
latitude: 0,
|
|
@@ -99,6 +103,11 @@ const RecordsMap = ({ style, initial, points, buckets, fit, onFitted, onMove, se
|
|
|
99
103
|
height: "100%"
|
|
100
104
|
},
|
|
101
105
|
onLoad: () => {
|
|
106
|
+
const container = ref.current.getMap().getContainer();
|
|
107
|
+
setColors({
|
|
108
|
+
area: themeColor(container, "--primary"),
|
|
109
|
+
chosen: themeColor(container, "--foreground")
|
|
110
|
+
});
|
|
102
111
|
regroup();
|
|
103
112
|
if (fit) fitNow();
|
|
104
113
|
else report();
|
|
@@ -108,6 +117,11 @@ const RecordsMap = ({ style, initial, points, buckets, fit, onFitted, onMove, se
|
|
|
108
117
|
onClick: onClear,
|
|
109
118
|
children: [
|
|
110
119
|
/* @__PURE__ */ jsx(NavigationControl, { showCompass: false }),
|
|
120
|
+
colors && /* @__PURE__ */ jsx(PositionAreas, {
|
|
121
|
+
points,
|
|
122
|
+
selected,
|
|
123
|
+
colors
|
|
124
|
+
}),
|
|
111
125
|
buckets.map((bucket) => /* @__PURE__ */ jsx(Marker, {
|
|
112
126
|
latitude: bucket.latitude,
|
|
113
127
|
longitude: bucket.longitude,
|
|
@@ -127,6 +141,7 @@ const RecordsMap = ({ style, initial, points, buckets, fit, onFitted, onMove, se
|
|
|
127
141
|
const at = byKey.get(group.at);
|
|
128
142
|
if (!at) return null;
|
|
129
143
|
const chosen = selected !== void 0 && group.keys.some((key) => selected.includes(key));
|
|
144
|
+
const approximate = group.keys.every((key) => byKey.get(key)?.approximate);
|
|
130
145
|
return /* @__PURE__ */ jsx(Marker, {
|
|
131
146
|
latitude: at.latitude,
|
|
132
147
|
longitude: at.longitude,
|
|
@@ -143,8 +158,10 @@ const RecordsMap = ({ style, initial, points, buckets, fit, onFitted, onMove, se
|
|
|
143
158
|
},
|
|
144
159
|
children: [/* @__PURE__ */ jsx(MapPin, {
|
|
145
160
|
"aria-hidden": true,
|
|
146
|
-
|
|
147
|
-
|
|
161
|
+
"data-approximate": approximate || void 0,
|
|
162
|
+
className: cn("size-8 drop-shadow", approximate ? chosen ? "fill-background text-foreground" : "fill-background text-primary" : chosen ? "fill-foreground text-background" : "fill-primary text-primary-foreground"),
|
|
163
|
+
strokeWidth: approximate ? 2 : 1.5,
|
|
164
|
+
strokeDasharray: approximate ? "3 2" : void 0
|
|
148
165
|
}), group.keys.length > 1 && /* @__PURE__ */ jsx("span", {
|
|
149
166
|
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",
|
|
150
167
|
children: group.keys.length
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/app/map/theme-color.ts
|
|
2
|
+
/**
|
|
3
|
+
* The colour a theme token such as `--primary` holds where `element` is, as `rgba()`, which MapLibre's paint reads
|
|
4
|
+
* whatever the theme writes it in: the browser resolves the token, and a pixel painted with it gives its channels.
|
|
5
|
+
*/
|
|
6
|
+
const themeColor = (element, token) => {
|
|
7
|
+
const probe = document.createElement("span");
|
|
8
|
+
probe.style.color = `var(${token})`;
|
|
9
|
+
element.append(probe);
|
|
10
|
+
const resolved = getComputedStyle(probe).color;
|
|
11
|
+
probe.remove();
|
|
12
|
+
const canvas = document.createElement("canvas");
|
|
13
|
+
canvas.width = 1;
|
|
14
|
+
canvas.height = 1;
|
|
15
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
16
|
+
if (!context) return resolved;
|
|
17
|
+
context.fillStyle = resolved;
|
|
18
|
+
context.fillRect(0, 0, 1, 1);
|
|
19
|
+
const [red, green, blue, alpha] = context.getImageData(0, 0, 1, 1).data;
|
|
20
|
+
return `rgba(${red}, ${green}, ${blue}, ${alpha / 255})`;
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { themeColor };
|
package/dist/app/nav-recent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { recordKey, recordTitle, recordTitleField } from "../live/model-helpers.js";
|
|
2
2
|
import { defaultOrderBy } from "./list-order.js";
|
|
3
|
-
import { defaultRecentLimit, encodeKey } from "@protobase/schema/internal";
|
|
4
3
|
import { appPath, enumLabel } from "@protobase/schema";
|
|
4
|
+
import { defaultRecentLimit, encodeKey } from "@protobase/schema/internal";
|
|
5
5
|
//#region src/app/nav-recent.ts
|
|
6
6
|
/** The list request behind a sidebar group: its filter, order (the list's sort when unset), size, and only the fields it shows. */
|
|
7
7
|
const recentRequest = (model, view, recent) => ({
|
|
@@ -2,7 +2,7 @@ import { cn } from "../../../lib/cn.js";
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
//#region src/app/pages/blocks/card-frame.tsx
|
|
4
4
|
/** The card every block that shows a box uses: title, description and header actions over the body. */
|
|
5
|
-
const CardFrame = ({ title, description, actions, className, children }) => /* @__PURE__ */ jsxs("section", {
|
|
5
|
+
const CardFrame = ({ title, description, actions, className, bodyClassName, children }) => /* @__PURE__ */ jsxs("section", {
|
|
6
6
|
className: cn("flex min-w-0 flex-col rounded-lg border border-border bg-background", className),
|
|
7
7
|
"aria-label": title,
|
|
8
8
|
children: [(title || description || actions) && /* @__PURE__ */ jsxs("header", {
|
|
@@ -21,7 +21,7 @@ const CardFrame = ({ title, description, actions, className, children }) => /* @
|
|
|
21
21
|
children: actions
|
|
22
22
|
})]
|
|
23
23
|
}), /* @__PURE__ */ jsx("div", {
|
|
24
|
-
className: "min-w-0 flex-1 p-4 text-[13px]",
|
|
24
|
+
className: cn("min-w-0 flex-1 p-4 text-[13px]", bodyClassName),
|
|
25
25
|
children
|
|
26
26
|
})]
|
|
27
27
|
});
|
|
@@ -8,18 +8,26 @@ const columns = {
|
|
|
8
8
|
3: "md:grid-cols-2 xl:grid-cols-3",
|
|
9
9
|
4: "sm:grid-cols-2 xl:grid-cols-4"
|
|
10
10
|
};
|
|
11
|
+
const statColumns = {
|
|
12
|
+
1: "",
|
|
13
|
+
2: "grid-cols-2",
|
|
14
|
+
3: "grid-cols-2 xl:grid-cols-3",
|
|
15
|
+
4: "grid-cols-2 xl:grid-cols-4"
|
|
16
|
+
};
|
|
11
17
|
const spans = {
|
|
12
18
|
1: "",
|
|
13
19
|
2: "md:col-span-2",
|
|
14
20
|
3: "md:col-span-2 xl:col-span-3",
|
|
15
21
|
4: "sm:col-span-2 md:col-span-2 xl:col-span-4"
|
|
16
22
|
};
|
|
17
|
-
|
|
23
|
+
const isStat = (child) => typeof child === "object" && !child.custom && child.type === "Stat";
|
|
24
|
+
/** Columns that collapse to one on a phone, or two when it holds only `Stat`s; a child's `span` makes it wider. */
|
|
18
25
|
const GridBlock = ({ node, children }) => {
|
|
19
26
|
const { columns: count = 2 } = propsOf(node);
|
|
20
27
|
const childNodes = node.children;
|
|
28
|
+
const stats = childNodes.length > 0 && childNodes.every(isStat);
|
|
21
29
|
return /* @__PURE__ */ jsx("div", {
|
|
22
|
-
className: cn("grid grid-cols-1 gap-4", columns[count]),
|
|
30
|
+
className: cn("grid grid-cols-1 gap-4", stats ? statColumns[count] : columns[count]),
|
|
23
31
|
children: children.map((child, index) => {
|
|
24
32
|
const element = childNodes[index];
|
|
25
33
|
const span = typeof element === "object" ? element.props.span : void 0;
|
|
@@ -2,8 +2,8 @@ import { AppLink } from "../../use-app-address.js";
|
|
|
2
2
|
import { useLayoutRecord } from "../record-scope.js";
|
|
3
3
|
import { propsOf } from "./block-props.js";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
|
-
import { fillAddress } from "@protobase/schema/internal";
|
|
6
5
|
import { appPath } from "@protobase/schema";
|
|
6
|
+
import { fillAddress } from "@protobase/schema/internal";
|
|
7
7
|
//#region src/app/pages/blocks/link.tsx
|
|
8
8
|
const style = "font-medium text-primary-text hover:underline";
|
|
9
9
|
/**
|
|
@@ -23,7 +23,7 @@ const Count = ({ model, filter }) => {
|
|
|
23
23
|
resource: model.name,
|
|
24
24
|
filter
|
|
25
25
|
},
|
|
26
|
-
className: `${big} hover:text-primary-text`,
|
|
26
|
+
className: `${big} hover:text-primary-text after:absolute after:inset-0 after:content-['']`,
|
|
27
27
|
children: count.data.toLocaleString("en-IE")
|
|
28
28
|
});
|
|
29
29
|
};
|
|
@@ -41,20 +41,24 @@ const RecordValue = ({ field, format }) => {
|
|
|
41
41
|
})
|
|
42
42
|
});
|
|
43
43
|
};
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* One number or value with a label: fixed, a count of matching records (linking to them from anywhere on the card), or
|
|
46
|
+
* a field of the record. A label that wraps pushes nothing out of line: the value sits at the card's bottom.
|
|
47
|
+
*/
|
|
45
48
|
const StatBlock = ({ node }) => {
|
|
46
49
|
const { label, value, resource, filter, field, format, description } = propsOf(node);
|
|
47
50
|
const { resources } = useAdminMeta();
|
|
48
51
|
const model = resource === void 0 ? void 0 : resources[resource];
|
|
49
52
|
return /* @__PURE__ */ jsxs(CardFrame, {
|
|
50
|
-
className: "h-full",
|
|
53
|
+
className: "relative h-full",
|
|
54
|
+
bodyClassName: "flex flex-col",
|
|
51
55
|
children: [
|
|
52
56
|
/* @__PURE__ */ jsx("p", {
|
|
53
|
-
className: "text-[13px] text-muted-foreground",
|
|
57
|
+
className: "text-[13px] text-muted-foreground break-words",
|
|
54
58
|
children: label
|
|
55
59
|
}),
|
|
56
60
|
/* @__PURE__ */ jsxs("div", {
|
|
57
|
-
className: "mt-1",
|
|
61
|
+
className: "mt-auto pt-1",
|
|
58
62
|
children: [
|
|
59
63
|
value !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
60
64
|
className: big,
|
|
@@ -77,7 +77,7 @@ const BuiltInValue = ({ model, field, value, format, relationLabel }) => {
|
|
|
77
77
|
children: text
|
|
78
78
|
});
|
|
79
79
|
return /* @__PURE__ */ jsx("span", {
|
|
80
|
-
className: cn(hints.format === "code" && "font-mono text-xs", (field.type === "decimal" || field.type === "integer") && "tabular-nums"),
|
|
80
|
+
className: cn(hints.format === "code" && "font-mono text-xs", (field.type === "decimal" || field.type === "integer") && "tabular-nums", hints.multiline && "whitespace-pre-line break-words"),
|
|
81
81
|
children: text
|
|
82
82
|
});
|
|
83
83
|
};
|
package/dist/app/record-page.js
CHANGED
|
@@ -144,6 +144,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
144
144
|
label: hint(name)?.label ?? humanize(name),
|
|
145
145
|
help: hint(name)?.help,
|
|
146
146
|
error: editor.errors[name],
|
|
147
|
+
wide: Boolean(hint(name)?.multiline),
|
|
147
148
|
conflict: conflict === void 0 ? void 0 : {
|
|
148
149
|
user: {
|
|
149
150
|
name: "Someone else",
|
|
@@ -181,7 +182,10 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
181
182
|
value: stored.record[name],
|
|
182
183
|
hints: hint(name),
|
|
183
184
|
children: text
|
|
184
|
-
}) : /* @__PURE__ */ jsx("strong", {
|
|
185
|
+
}) : /* @__PURE__ */ jsx("strong", {
|
|
186
|
+
className: hint(name)?.multiline ? "whitespace-pre-line break-words" : void 0,
|
|
187
|
+
children: text
|
|
188
|
+
});
|
|
185
189
|
const shown = shownWidget(fieldTypes, field, hint(name));
|
|
186
190
|
return [{
|
|
187
191
|
label: hint(name)?.label ?? humanize(name),
|
package/dist/app/router.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RouteLink } from "../primitives/route-link.js";
|
|
1
2
|
import { Button } from "../primitives/button.js";
|
|
2
3
|
import { Dialog } from "../primitives/dialog.js";
|
|
3
4
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -112,15 +113,13 @@ const Router = ({ basePath = "", initialUrl, children }) => {
|
|
|
112
113
|
})]
|
|
113
114
|
});
|
|
114
115
|
};
|
|
115
|
-
|
|
116
|
+
/** A link to a path below the base path, as `appPath` writes it, that the router follows without reloading the page. */
|
|
117
|
+
const Link = ({ to, replace, ...rest }) => {
|
|
116
118
|
const { navigate, basePath } = useRouter();
|
|
117
|
-
return /* @__PURE__ */ jsx(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.button !== 0) return;
|
|
122
|
-
event.preventDefault();
|
|
123
|
-
navigate(to);
|
|
119
|
+
return /* @__PURE__ */ jsx(RouteLink, {
|
|
120
|
+
to: {
|
|
121
|
+
href: `${basePath}${to}`,
|
|
122
|
+
onFollow: () => navigate(to, { replace })
|
|
124
123
|
},
|
|
125
124
|
...rest
|
|
126
125
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { recordKey } from "../../live/model-helpers.js";
|
|
2
|
-
import { encodeKey } from "@protobase/schema/internal";
|
|
3
2
|
import { createWhere } from "@protobase/schema";
|
|
3
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
4
4
|
//#region src/app/saved/saved-records.ts
|
|
5
5
|
const where = createWhere();
|
|
6
6
|
const relationValue = (value) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { recordKey, recordTitleField } from "../../live/model-helpers.js";
|
|
2
2
|
import { humanize } from "../../live/naming.js";
|
|
3
|
-
import { encodeKey, matchedSearchField } from "@protobase/schema/internal";
|
|
4
3
|
import { appPath, createWhere, enumLabel, printFilter } from "@protobase/schema";
|
|
4
|
+
import { encodeKey, matchedSearchField } from "@protobase/schema/internal";
|
|
5
5
|
//#region src/app/search/search-model.ts
|
|
6
6
|
const where = createWhere();
|
|
7
7
|
const searchTargets = (meta) => Object.values(meta.resources).flatMap((model) => {
|
|
@@ -20,7 +20,7 @@ const useSaveSearch = (search, filter, name) => {
|
|
|
20
20
|
const client = useClient();
|
|
21
21
|
const toast = useToast();
|
|
22
22
|
const refresh = useRefreshResource();
|
|
23
|
-
const {
|
|
23
|
+
const { href, open } = useAppAddress();
|
|
24
24
|
const { resource } = search;
|
|
25
25
|
const model = meta.resources[resource];
|
|
26
26
|
const of = meta.views[resource]?.searchesOf;
|
|
@@ -44,10 +44,11 @@ const useSaveSearch = (search, filter, name) => {
|
|
|
44
44
|
title: "Search saved",
|
|
45
45
|
action: {
|
|
46
46
|
label: `Open ${plural.toLowerCase()}`,
|
|
47
|
-
|
|
47
|
+
href: href({
|
|
48
48
|
kind: "list",
|
|
49
49
|
resource
|
|
50
|
-
})
|
|
50
|
+
}),
|
|
51
|
+
onNavigate: open
|
|
51
52
|
}
|
|
52
53
|
});
|
|
53
54
|
},
|
|
@@ -27,7 +27,8 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
|
|
|
27
27
|
const [linesConflict, setLinesConflict] = useState(false);
|
|
28
28
|
const [conflicts, setConflicts] = useState({});
|
|
29
29
|
const [errors, setErrors] = useState({});
|
|
30
|
-
const
|
|
30
|
+
const changed = Object.entries(draft).filter(([name, entry]) => differs(model.fields[name], entry, stored.record[name]));
|
|
31
|
+
const fieldsDirty = changed.length > 0;
|
|
31
32
|
const dirty = fieldsDirty || Boolean(lines?.dirty);
|
|
32
33
|
useEffect(() => {
|
|
33
34
|
if (restored?.order) lines?.setOrder(restored.order);
|
|
@@ -52,7 +53,8 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
|
|
|
52
53
|
const value = (name) => name in draft ? draft[name] : ["boolean", "file"].includes(model.fields[name].type) ? stored.record[name] : toDraft(stored.record[name]);
|
|
53
54
|
const change = (name, next) => {
|
|
54
55
|
setErrors((current) => without(current, name));
|
|
55
|
-
|
|
56
|
+
const typed = typeof next === "string" ? next !== toDraft(stored.record[name]) : differs(model.fields[name], next, stored.record[name]);
|
|
57
|
+
setDraft((current) => typed ? {
|
|
56
58
|
...current,
|
|
57
59
|
[name]: next
|
|
58
60
|
} : without(current, name));
|
|
@@ -71,7 +73,7 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
|
|
|
71
73
|
setDraft(keep);
|
|
72
74
|
setConflicts(theirs);
|
|
73
75
|
};
|
|
74
|
-
const patchOf = () => Object.fromEntries(
|
|
76
|
+
const patchOf = () => Object.fromEntries(changed.map(([name, entry]) => [name, toPatchValue(model.fields[name], entry)]));
|
|
75
77
|
const fieldErrors = (error) => isApiError(error) ? Object.fromEntries(error.errors.flatMap((entry) => entry.field ? [[entry.field, entry.message]] : [])) : {};
|
|
76
78
|
const saveRecordOnly = async () => {
|
|
77
79
|
try {
|
|
@@ -2,14 +2,14 @@ import { cn } from "../lib/cn.js";
|
|
|
2
2
|
import { useIsDesktop } from "../lib/use-media-query.js";
|
|
3
3
|
import { BottomBar } from "./bottom-bar.js";
|
|
4
4
|
import { Drawer } from "./drawer.js";
|
|
5
|
-
import { PageScrollContext, usePageInsets } from "./page-scroll.js";
|
|
5
|
+
import { PageScrollContext, usePageGutter, usePageInsets } from "./page-scroll.js";
|
|
6
6
|
import { Sidebar } from "./sidebar.js";
|
|
7
7
|
import { TopBar } from "./top-bar.js";
|
|
8
8
|
import { useEffect, useRef, useState } from "react";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
//#region src/app-shell/app-shell.tsx
|
|
11
11
|
const drawerId = "app-drawer";
|
|
12
|
-
const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, actions, rightPanel, forceExpanded, profileMenuOpen, navGroups, userMenu, bottomBar, onNavigate, onSignOut,
|
|
12
|
+
const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, actions, rightPanel, forceExpanded, profileMenuOpen, navGroups, userMenu, bottomBar, onNavigate, onSignOut, signIn, organizations, roles, theme, search, pageScroll: scrollsByDefault = false, className, children }) => {
|
|
13
13
|
const desktop = useIsDesktop();
|
|
14
14
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
15
15
|
useEffect(() => {
|
|
@@ -21,6 +21,7 @@ const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, action
|
|
|
21
21
|
const topBarRef = useRef(null);
|
|
22
22
|
const bottomBarRef = useRef(null);
|
|
23
23
|
usePageInsets(pageScroll, topBarRef, bottomBarRef, Boolean(tabs));
|
|
24
|
+
usePageGutter(pageScroll);
|
|
24
25
|
const sidebar = (mode, sticky = false) => /* @__PURE__ */ jsx(Sidebar, {
|
|
25
26
|
mode,
|
|
26
27
|
sticky,
|
|
@@ -33,7 +34,7 @@ const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, action
|
|
|
33
34
|
userMenu,
|
|
34
35
|
onNavigate,
|
|
35
36
|
onSignOut,
|
|
36
|
-
|
|
37
|
+
signIn,
|
|
37
38
|
organizations,
|
|
38
39
|
roles,
|
|
39
40
|
theme
|
|
@@ -16,6 +16,22 @@ const usePageScroll = (on) => {
|
|
|
16
16
|
return () => set(void 0);
|
|
17
17
|
}, [set, on]);
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* While the page scrolls, keeps room for its scrollbar whether or not the page overflows, where scrollbars take room
|
|
21
|
+
* (Windows, Linux, or a Mac set to always show them). Otherwise a page whose height follows its width, such as the
|
|
22
|
+
* cards, can be just too tall with the width without a scrollbar and short enough with the width beside one: the
|
|
23
|
+
* scrollbar narrows the cards, they get shorter, the page fits, the scrollbar goes, they widen again, every frame.
|
|
24
|
+
*/
|
|
25
|
+
const usePageGutter = (on) => {
|
|
26
|
+
useLayoutEffect(() => {
|
|
27
|
+
if (!on) return;
|
|
28
|
+
const root = document.documentElement;
|
|
29
|
+
root.style.setProperty("scrollbar-gutter", "stable");
|
|
30
|
+
return () => {
|
|
31
|
+
root.style.removeProperty("scrollbar-gutter");
|
|
32
|
+
};
|
|
33
|
+
}, [on]);
|
|
34
|
+
};
|
|
19
35
|
const properties = [
|
|
20
36
|
"--pb-sticky-top",
|
|
21
37
|
"--pb-sticky-bottom",
|
|
@@ -54,4 +70,4 @@ const usePageInsets = (on, top, bottom, bottomShown) => {
|
|
|
54
70
|
]);
|
|
55
71
|
};
|
|
56
72
|
//#endregion
|
|
57
|
-
export { PageScrollContext, usePageInsets, usePageScroll };
|
|
73
|
+
export { PageScrollContext, usePageGutter, usePageInsets, usePageScroll };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { RouteLink } from "../primitives/route-link.js";
|
|
2
3
|
import { Logo } from "./logo.js";
|
|
3
4
|
import { navGroups } from "./nav.js";
|
|
4
5
|
import { ThemeSwitch } from "./theme-switch.js";
|
|
@@ -9,7 +10,7 @@ import { SidebarGroup } from "./sidebar-group.js";
|
|
|
9
10
|
import { LogIn } from "lucide-react";
|
|
10
11
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
11
12
|
//#region src/app-shell/sidebar.tsx
|
|
12
|
-
const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut,
|
|
13
|
+
const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut, signIn, organizations, roles, theme, sticky }) => {
|
|
13
14
|
const expanding = mode === "icon-expand";
|
|
14
15
|
const expanded = expanding && forceExpanded;
|
|
15
16
|
const inline = isTextMode(mode) || expanding;
|
|
@@ -62,9 +63,8 @@ const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenu
|
|
|
62
63
|
}) : /* @__PURE__ */ jsxs(Fragment, { children: [theme && inline && /* @__PURE__ */ jsx(ThemeSwitch, {
|
|
63
64
|
theme,
|
|
64
65
|
className: "flex min-h-11 w-full items-center gap-2.5 whitespace-nowrap p-1.5 text-[13px] text-muted-foreground md:min-h-0"
|
|
65
|
-
}),
|
|
66
|
-
|
|
67
|
-
onClick: onSignIn,
|
|
66
|
+
}), signIn && /* @__PURE__ */ jsxs(RouteLink, {
|
|
67
|
+
to: signIn,
|
|
68
68
|
"aria-label": "Sign in",
|
|
69
69
|
className: "flex min-h-11 w-full items-center gap-2.5 rounded-md p-1.5 text-left text-[13px] font-medium text-muted-foreground hover:bg-muted hover:text-foreground md:min-h-0",
|
|
70
70
|
children: [/* @__PURE__ */ jsx(LogIn, { className: "size-4 shrink-0" }), inline && /* @__PURE__ */ jsx("span", {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
-
import { backTarget } from "./back-target.js";
|
|
3
2
|
import { Breadcrumb, inApp } from "./breadcrumb.js";
|
|
3
|
+
import { backTarget } from "./back-target.js";
|
|
4
4
|
import { GlobalSearch } from "./global-search.js";
|
|
5
5
|
import { useEffect, useRef, useState } from "react";
|
|
6
6
|
import { ChevronLeft, Menu, Search, X } from "lucide-react";
|
|
@@ -25,7 +25,7 @@ const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControl
|
|
|
25
25
|
}, [search]);
|
|
26
26
|
return /* @__PURE__ */ jsxs("header", {
|
|
27
27
|
ref,
|
|
28
|
-
className: cn(sticky ? "sticky top-0 z-30" : "relative", "flex h-12 shrink-0 items-center gap-1 border-b bg-background
|
|
28
|
+
className: cn(sticky ? "sticky top-0 z-30" : "relative", "flex h-12 shrink-0 items-center gap-1 border-b bg-background pl-4 pr-2 md:gap-4 md:px-5"),
|
|
29
29
|
children: [
|
|
30
30
|
onMenu && /* @__PURE__ */ jsx("button", {
|
|
31
31
|
type: "button",
|
|
@@ -33,7 +33,7 @@ const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControl
|
|
|
33
33
|
"aria-expanded": Boolean(menuOpen),
|
|
34
34
|
"aria-controls": menuControls,
|
|
35
35
|
onClick: onMenu,
|
|
36
|
-
className: cn(iconButton, "-ml-
|
|
36
|
+
className: cn(iconButton, "-ml-3"),
|
|
37
37
|
children: /* @__PURE__ */ jsx(Menu, { className: "size-5" })
|
|
38
38
|
}),
|
|
39
39
|
back && /* @__PURE__ */ jsx("a", {
|
|
@@ -44,7 +44,7 @@ const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControl
|
|
|
44
44
|
event.preventDefault();
|
|
45
45
|
onNavigate(back.href);
|
|
46
46
|
},
|
|
47
|
-
className: cn(iconButton, !onMenu && "-ml-
|
|
47
|
+
className: cn(iconButton, !onMenu && "-ml-3"),
|
|
48
48
|
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-5" })
|
|
49
49
|
}),
|
|
50
50
|
/* @__PURE__ */ jsx(Breadcrumb, {
|
package/dist/auth/auth-gate.js
CHANGED
|
@@ -11,7 +11,6 @@ import { ForgotPasswordPage } from "./forgot-password-page.js";
|
|
|
11
11
|
import { ProviderSignInPage } from "./provider-sign-in-page.js";
|
|
12
12
|
import { ResetPasswordPage } from "./reset-password-page.js";
|
|
13
13
|
import { SetupRequiredPage } from "./setup-required-page.js";
|
|
14
|
-
import { signInPath } from "./sign-in-address.js";
|
|
15
14
|
import { SignInPage } from "./sign-in-page.js";
|
|
16
15
|
import { SignUpPage } from "./sign-up-page.js";
|
|
17
16
|
import { readStaffLink, withoutStaffLink } from "./staff-link.js";
|
|
@@ -22,6 +21,7 @@ import { readInvitationLink, withoutInvitationLink } from "../organizations/invi
|
|
|
22
21
|
import { InvitationPage } from "../organizations/invitation-page.js";
|
|
23
22
|
import { useCallback, useEffect, useState } from "react";
|
|
24
23
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
24
|
+
import { signInPath } from "@protobase/schema";
|
|
25
25
|
//#region src/auth/auth-gate.tsx
|
|
26
26
|
const firstPage = () => {
|
|
27
27
|
const href = window.location.href;
|
|
@@ -67,10 +67,11 @@ const Redirect = ({ to }) => {
|
|
|
67
67
|
const AuthGate = ({ workspace, children }) => {
|
|
68
68
|
const { state, recheck, passwordReset, signOut, signInProviders, canSignUp, browse } = useAuth();
|
|
69
69
|
const { path, params, basePath, navigate } = useRouter();
|
|
70
|
-
const { address, next, open, leave } = useSignInAddress();
|
|
70
|
+
const { address, next, open, link, leave } = useSignInAddress();
|
|
71
71
|
const [page, setPage] = useState(firstPage);
|
|
72
72
|
const [codeEmail, setCodeEmail] = useState();
|
|
73
73
|
const [signUpEmail, setSignUpEmail] = useState();
|
|
74
|
+
const [signUpTicket, setSignUpTicket] = useState();
|
|
74
75
|
const href = new URL(`${basePath}${path}${params.size > 0 ? `?${params}` : ""}`, window.location.origin).href;
|
|
75
76
|
const forget = useCallback((without) => {
|
|
76
77
|
const below = without(href).slice(basePath.length);
|
|
@@ -87,6 +88,7 @@ const AuthGate = ({ workspace, children }) => {
|
|
|
87
88
|
if (page.kind === "provider-sign-in" || page.kind === "sign-in" && (page.notice || page.error || page.providerError)) setPage({ kind: "sign-in" });
|
|
88
89
|
setCodeEmail(void 0);
|
|
89
90
|
setSignUpEmail(void 0);
|
|
91
|
+
setSignUpTicket(void 0);
|
|
90
92
|
}, [
|
|
91
93
|
entered,
|
|
92
94
|
page,
|
|
@@ -161,7 +163,7 @@ const AuthGate = ({ workspace, children }) => {
|
|
|
161
163
|
forget(withoutInvitationLink);
|
|
162
164
|
setPage({ kind: "sign-in" });
|
|
163
165
|
},
|
|
164
|
-
|
|
166
|
+
signIn: link()
|
|
165
167
|
});
|
|
166
168
|
if (state.kind === "signed-in") return children;
|
|
167
169
|
if (state.kind === "two-factor") return /* @__PURE__ */ jsx(TwoFactorPage, {
|
|
@@ -184,25 +186,32 @@ const AuthGate = ({ workspace, children }) => {
|
|
|
184
186
|
"forgot-password": passwordReset,
|
|
185
187
|
"email-code": codeEmail !== void 0,
|
|
186
188
|
"sign-up": canSignUp,
|
|
187
|
-
"sign-up-code": canSignUp && signUpEmail !== void 0
|
|
189
|
+
"sign-up-code": canSignUp && signUpEmail !== void 0,
|
|
190
|
+
"sign-up-details": canSignUp && signUpTicket !== void 0
|
|
188
191
|
}[address.step] ? address.step : "sign-in";
|
|
189
192
|
const backTo = (target) => () => open(target, { replace: true });
|
|
190
193
|
const onBrowse = browse && (() => {
|
|
191
194
|
browse();
|
|
192
195
|
leave();
|
|
193
196
|
});
|
|
194
|
-
if (step === "forgot-password") return /* @__PURE__ */ jsx(ForgotPasswordPage, {
|
|
197
|
+
if (step === "forgot-password") return /* @__PURE__ */ jsx(ForgotPasswordPage, { back: link("sign-in", { replace: true }) });
|
|
195
198
|
if (step === "email-code") return /* @__PURE__ */ jsx(EmailCodePage, {
|
|
196
199
|
email: codeEmail,
|
|
197
200
|
onBack: backTo("sign-in")
|
|
198
201
|
});
|
|
199
|
-
if (step === "sign-up" || step === "sign-up-code") return /* @__PURE__ */ jsx(SignUpPage, {
|
|
202
|
+
if (step === "sign-up" || step === "sign-up-code" || step === "sign-up-details") return /* @__PURE__ */ jsx(SignUpPage, {
|
|
200
203
|
workspace,
|
|
201
|
-
email: step === "sign-up
|
|
204
|
+
email: step === "sign-up" ? void 0 : signUpEmail,
|
|
205
|
+
ticket: step === "sign-up-details" ? signUpTicket : void 0,
|
|
202
206
|
onCodeSent: (email) => {
|
|
203
207
|
setSignUpEmail(email);
|
|
208
|
+
setSignUpTicket(void 0);
|
|
204
209
|
open("sign-up-code");
|
|
205
210
|
},
|
|
211
|
+
onVerified: (ticket) => {
|
|
212
|
+
setSignUpTicket(ticket);
|
|
213
|
+
open("sign-up-details", { replace: true });
|
|
214
|
+
},
|
|
206
215
|
onBack: backTo("sign-up"),
|
|
207
216
|
onSignIn: () => open("sign-in"),
|
|
208
217
|
onBrowse
|
|
@@ -213,12 +222,12 @@ const AuthGate = ({ workspace, children }) => {
|
|
|
213
222
|
workspace,
|
|
214
223
|
notice: signIn.notice,
|
|
215
224
|
error,
|
|
216
|
-
|
|
225
|
+
forgotPassword: passwordReset ? link("forgot-password") : void 0,
|
|
217
226
|
onCodeSent: (email) => {
|
|
218
227
|
setCodeEmail(email);
|
|
219
228
|
open("email-code");
|
|
220
229
|
},
|
|
221
|
-
|
|
230
|
+
signUp: canSignUp ? link("sign-up") : void 0,
|
|
222
231
|
onBrowse
|
|
223
232
|
}, error);
|
|
224
233
|
};
|
|
@@ -86,6 +86,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
86
86
|
sendSignInCode: (email) => session.sendSignInCode(email),
|
|
87
87
|
signInWithCode: async (email, code) => finish(await session.signInWithCode(email, code)),
|
|
88
88
|
sendSignUpCode: (email) => session.sendSignUpCode(email),
|
|
89
|
+
verifySignUpCode: (email, code) => session.verifySignUpCode(email, code),
|
|
89
90
|
signUp: async (input) => enter(await session.signUp(input)),
|
|
90
91
|
signInWithPasskey: async (domain) => enter(await session.signInWithPasskey(domain)),
|
|
91
92
|
verifyTwoFactor: async (input) => enter(await session.verifyTwoFactor(input)),
|