@protobase/ui 0.12.9 → 0.12.11
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/action-icon.js +3 -7
- package/dist/app/app.d.ts +4 -1
- package/dist/app/app.js +26 -18
- package/dist/app/create/create-page.js +32 -6
- package/dist/app/create/create-values.js +16 -3
- package/dist/app/current-address.d.ts +1 -0
- package/dist/app/displays/cards-display.js +58 -10
- package/dist/app/displays/list-displays.js +7 -3
- package/dist/app/field-editor.js +4 -0
- package/dist/app/named-icon.js +13 -0
- package/dist/app/pages/field-value.js +5 -5
- package/dist/app/pages/use-relation-labels.js +1 -1
- package/dist/app/record-page.js +4 -3
- package/dist/app/value-badge.js +14 -0
- package/dist/app-shell/app-shell.js +2 -1
- package/dist/app-shell/sidebar.js +12 -2
- package/dist/auth/auth-gate.js +17 -11
- package/dist/auth/auth-provider.js +10 -3
- package/dist/auth/sign-in-form.js +7 -1
- package/dist/auth/sign-in-page.js +2 -1
- package/dist/data-grid/cells.js +9 -6
- package/dist/lib/readable-text.js +23 -0
- package/dist/live/columns-from-model.js +3 -0
- package/dist/live/display-value.js +6 -3
- package/dist/live/use-filter-config.js +5 -3
- package/dist/live/value-color.js +8 -0
- package/dist/package.js +1 -1
- package/dist/primitives/badge.js +12 -3
- package/dist/roles/use-active-role.js +8 -3
- package/dist/shell/assets/app-B2rZN97s.js +65 -0
- package/dist/shell/assets/{index-CGPHR8-j.js → index-B7XSEKuk.js} +1 -1
- package/dist/shell/assets/index-otHceRXj.css +2 -0
- package/dist/shell/assets/{shared-protobase-ui-oolw3GIu.js → shared-protobase-ui-DhQ8wFhe.js} +1 -1
- 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-Ds2CYFLS.js +0 -65
- package/dist/shell/assets/index-BMIEmNDF.css +0 -2
|
@@ -19,6 +19,8 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
19
19
|
const [platformSignIn, setPlatformSignIn] = useState();
|
|
20
20
|
const [staffSignIn, setStaffSignIn] = useState();
|
|
21
21
|
const [organizations, setOrganizations] = useState();
|
|
22
|
+
const [anonymous, setAnonymous] = useState(false);
|
|
23
|
+
const signedOutState = useCallback(() => anonymous ? { kind: "visitor" } : { kind: "signed-out" }, [anonymous]);
|
|
22
24
|
const markSignedOut = useCallback(() => setState((current) => current.kind === "signed-in" ? { kind: "signed-out" } : current), []);
|
|
23
25
|
const signedOut = useRef(markSignedOut);
|
|
24
26
|
signedOut.current = markSignedOut;
|
|
@@ -49,9 +51,10 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
49
51
|
setPlatformSignIn(status.platformSignIn);
|
|
50
52
|
setStaffSignIn(status.staffSignIn);
|
|
51
53
|
setOrganizations(status.organizations);
|
|
54
|
+
setAnonymous(Boolean(status.anonymous));
|
|
52
55
|
if (status.needsAdmin) return setState({ kind: "needs-admin" });
|
|
53
56
|
const user = await session.session();
|
|
54
|
-
return user ? enter(user) : setState({ kind: "signed-out" });
|
|
57
|
+
return user ? enter(user) : setState({ kind: status.anonymous ? "visitor" : "signed-out" });
|
|
55
58
|
}).catch((error) => setState({
|
|
56
59
|
kind: "error",
|
|
57
60
|
message: error instanceof Error ? error.message : "The server did not answer"
|
|
@@ -73,6 +76,8 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
73
76
|
...organizations && { organizations },
|
|
74
77
|
markSignedOut,
|
|
75
78
|
recheck,
|
|
79
|
+
showSignIn: () => setState((current) => current.kind === "visitor" ? { kind: "signed-out" } : current),
|
|
80
|
+
...anonymous && { browse: () => setState((current) => current.kind === "signed-out" ? { kind: "visitor" } : current) },
|
|
76
81
|
signIn: async (email, password) => finish(await session.signIn(email, password)),
|
|
77
82
|
sendSignInCode: (email) => session.sendSignInCode(email),
|
|
78
83
|
signInWithCode: async (email, code) => finish(await session.signInWithCode(email, code)),
|
|
@@ -86,11 +91,11 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
86
91
|
},
|
|
87
92
|
stopStaffSession: async () => {
|
|
88
93
|
await session.staff.stop();
|
|
89
|
-
setState(
|
|
94
|
+
setState(signedOutState());
|
|
90
95
|
},
|
|
91
96
|
signOut: async () => {
|
|
92
97
|
await session.signOut();
|
|
93
|
-
setState(
|
|
98
|
+
setState(signedOutState());
|
|
94
99
|
}
|
|
95
100
|
}), [
|
|
96
101
|
state,
|
|
@@ -102,6 +107,8 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
102
107
|
platformSignIn,
|
|
103
108
|
staffSignIn,
|
|
104
109
|
organizations,
|
|
110
|
+
anonymous,
|
|
111
|
+
signedOutState,
|
|
105
112
|
markSignedOut,
|
|
106
113
|
recheck,
|
|
107
114
|
finish,
|
|
@@ -13,7 +13,7 @@ const Divider = () => /* @__PURE__ */ jsxs("div", {
|
|
|
13
13
|
/* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" })
|
|
14
14
|
]
|
|
15
15
|
});
|
|
16
|
-
const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["password"], onSendCode, onPasskey, passkeyDomains = [], onForgotPassword, providers = [], onContinueWith }) => {
|
|
16
|
+
const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["password"], onSendCode, onPasskey, passkeyDomains = [], onForgotPassword, providers = [], onContinueWith, onBrowse }) => {
|
|
17
17
|
const [email, setEmail] = useState("");
|
|
18
18
|
const [password, setPassword] = useState("");
|
|
19
19
|
const emailInput = useRef(null);
|
|
@@ -127,6 +127,12 @@ const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["pass
|
|
|
127
127
|
children: "Forgot password?"
|
|
128
128
|
})
|
|
129
129
|
]
|
|
130
|
+
}),
|
|
131
|
+
onBrowse && /* @__PURE__ */ jsx("button", {
|
|
132
|
+
type: "button",
|
|
133
|
+
onClick: onBrowse,
|
|
134
|
+
className: "mt-4 w-full text-center text-[13px] font-medium text-muted-foreground hover:text-foreground hover:underline",
|
|
135
|
+
children: "Continue without signing in"
|
|
130
136
|
})
|
|
131
137
|
]
|
|
132
138
|
});
|
|
@@ -5,7 +5,7 @@ import { useState } from "react";
|
|
|
5
5
|
import { AuthError } from "@protobase/client";
|
|
6
6
|
import { jsx } from "react/jsx-runtime";
|
|
7
7
|
//#region src/auth/sign-in-page.tsx
|
|
8
|
-
const SignInPage = ({ workspace, notice, onForgotPassword, onCodeSent, error: initialError }) => {
|
|
8
|
+
const SignInPage = ({ workspace, notice, onForgotPassword, onBrowse, onCodeSent, error: initialError }) => {
|
|
9
9
|
const { signIn, signInSocial, signInProviders, signInMethods, passkeyDomains, sendSignInCode, signInWithPasskey } = useAuth();
|
|
10
10
|
const [busy, setBusy] = useState(false);
|
|
11
11
|
const [error, setError] = useState(initialError);
|
|
@@ -27,6 +27,7 @@ const SignInPage = ({ workspace, notice, onForgotPassword, onCodeSent, error: in
|
|
|
27
27
|
notice,
|
|
28
28
|
methods: signInMethods,
|
|
29
29
|
onForgotPassword,
|
|
30
|
+
onBrowse,
|
|
30
31
|
providers: signInProviders,
|
|
31
32
|
onContinueWith: (provider) => void attempt(() => signInSocial(provider), signInMessage),
|
|
32
33
|
passkeyDomains,
|
package/dist/data-grid/cells.js
CHANGED
|
@@ -6,6 +6,7 @@ import { formatInt, formatPercent, formatSigned } from "../format/number.js";
|
|
|
6
6
|
import { formatDate, formatDateTime } from "../format/date.js";
|
|
7
7
|
import { fileShown, isPreviewable } from "../live/file-values.js";
|
|
8
8
|
import { userById } from "../mocks/users.js";
|
|
9
|
+
import { valueColor } from "../live/value-color.js";
|
|
9
10
|
import { Check, Paperclip } from "lucide-react";
|
|
10
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
12
|
import { enumLabel } from "@protobase/schema";
|
|
@@ -26,9 +27,9 @@ const renderCell = (spec, value) => {
|
|
|
26
27
|
className: "truncate font-mono text-xs text-muted-foreground",
|
|
27
28
|
children: String(value)
|
|
28
29
|
});
|
|
29
|
-
case "money": return /* @__PURE__ */
|
|
30
|
+
case "money": return /* @__PURE__ */ jsxs("span", {
|
|
30
31
|
className: "tabular-nums",
|
|
31
|
-
children: formatMoney(Number(value), spec.currency)
|
|
32
|
+
children: [formatMoney(Number(value), spec.currency), spec.suffix]
|
|
32
33
|
});
|
|
33
34
|
case "date": return /* @__PURE__ */ jsx("span", {
|
|
34
35
|
className: "tabular-nums text-muted-foreground",
|
|
@@ -38,13 +39,13 @@ const renderCell = (spec, value) => {
|
|
|
38
39
|
className: "tabular-nums text-muted-foreground",
|
|
39
40
|
children: formatDateTime(Number(value))
|
|
40
41
|
});
|
|
41
|
-
case "number": return /* @__PURE__ */
|
|
42
|
+
case "number": return /* @__PURE__ */ jsxs("span", {
|
|
42
43
|
className: "tabular-nums",
|
|
43
|
-
children: formatInt(Number(value))
|
|
44
|
+
children: [formatInt(Number(value)), spec.suffix]
|
|
44
45
|
});
|
|
45
|
-
case "signed": return /* @__PURE__ */
|
|
46
|
+
case "signed": return /* @__PURE__ */ jsxs("span", {
|
|
46
47
|
className: cn("tabular-nums", Number(value) < 0 ? "text-danger-text" : "text-success-text"),
|
|
47
|
-
children: formatSigned(Number(value))
|
|
48
|
+
children: [formatSigned(Number(value)), spec.suffix]
|
|
48
49
|
});
|
|
49
50
|
case "percent": return Number(value) === 0 ? muted("—") : /* @__PURE__ */ jsx("span", {
|
|
50
51
|
className: "tabular-nums",
|
|
@@ -52,6 +53,8 @@ const renderCell = (spec, value) => {
|
|
|
52
53
|
});
|
|
53
54
|
case "status": return /* @__PURE__ */ jsx(Badge, {
|
|
54
55
|
tone: spec.tones?.[String(value)] ?? "neutral",
|
|
56
|
+
color: valueColor(spec.colors, value),
|
|
57
|
+
shape: spec.shape,
|
|
55
58
|
children: enumLabel(String(value), spec.labels)
|
|
56
59
|
});
|
|
57
60
|
case "relation": return /* @__PURE__ */ jsx("span", {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/lib/readable-text.ts
|
|
2
|
+
const luminance = (hex) => {
|
|
3
|
+
const digits = hex.slice(1);
|
|
4
|
+
const full = digits.length === 3 ? [...digits].map((digit) => digit + digit).join("") : digits;
|
|
5
|
+
const [r, g, b] = [
|
|
6
|
+
0,
|
|
7
|
+
2,
|
|
8
|
+
4
|
|
9
|
+
].map((at) => {
|
|
10
|
+
const channel = Number.parseInt(full.slice(at, at + 2), 16) / 255;
|
|
11
|
+
return channel <= .03928 ? channel / 12.92 : ((channel + .055) / 1.055) ** 2.4;
|
|
12
|
+
});
|
|
13
|
+
return .2126 * r + .7152 * g + .0722 * b;
|
|
14
|
+
};
|
|
15
|
+
const light = "#ffffff";
|
|
16
|
+
const dark = "#111827";
|
|
17
|
+
/** The text colour, white or near black, that reads best on a hex background. */
|
|
18
|
+
const readableOn = (background) => {
|
|
19
|
+
const shade = luminance(background);
|
|
20
|
+
return 1.05 / (shade + .05) >= (shade + .05) / (luminance(dark) + .05) ? light : dark;
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { readableOn };
|
|
@@ -82,6 +82,9 @@ const columnsFromModel = (resource, view, highlight = []) => {
|
|
|
82
82
|
tones: Object.fromEntries((field.enumValues ?? []).map((value) => [value, toneFor(value)])),
|
|
83
83
|
labels
|
|
84
84
|
},
|
|
85
|
+
...field.type === "enum" && viewField?.valueColors && { colors: viewField.valueColors },
|
|
86
|
+
...field.type === "enum" && viewField?.badgeShape && { shape: viewField.badgeShape },
|
|
87
|
+
...viewField?.suffix !== void 0 && { suffix: viewField.suffix },
|
|
85
88
|
...kind === "money" && {
|
|
86
89
|
currency: "EUR",
|
|
87
90
|
value: (row) => row[name] == null ? null : currencyCents(row[name])
|
|
@@ -2,7 +2,8 @@ import { formatDate, formatDateTime } from "../format/date.js";
|
|
|
2
2
|
import { fileShown } from "./file-values.js";
|
|
3
3
|
import { enumLabel } from "@protobase/schema";
|
|
4
4
|
//#region src/live/display-value.ts
|
|
5
|
-
|
|
5
|
+
const affixed = (text, hints) => `${hints.prefix ?? ""}${text}${hints.suffix ?? ""}`;
|
|
6
|
+
/** A read-only rendering of a stored value, following the view's prefix, suffix, decimals, format and value label hints. */
|
|
6
7
|
const displayValue = (field, value, hints = {}) => {
|
|
7
8
|
if (value === null || value === void 0 || value === "") return "—";
|
|
8
9
|
switch (field.type) {
|
|
@@ -14,11 +15,13 @@ const displayValue = (field, value, hints = {}) => {
|
|
|
14
15
|
case "decimal": {
|
|
15
16
|
const text = hints.decimals === void 0 ? String(value) : Number(value).toFixed(hints.decimals);
|
|
16
17
|
if (hints.format === "percent") return `${Number(value)}%`;
|
|
17
|
-
return
|
|
18
|
+
return affixed(Number(text).toLocaleString("en-IE", {
|
|
18
19
|
minimumFractionDigits: hints.decimals ?? 0,
|
|
19
20
|
maximumFractionDigits: hints.decimals ?? 20
|
|
20
|
-
})
|
|
21
|
+
}), hints);
|
|
21
22
|
}
|
|
23
|
+
case "integer":
|
|
24
|
+
case "bigint": return affixed(String(value), hints);
|
|
22
25
|
default: return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
23
26
|
}
|
|
24
27
|
};
|
|
@@ -2,9 +2,9 @@ import { formatMoney } from "../format/money.js";
|
|
|
2
2
|
import { keys } from "../data/query-keys.js";
|
|
3
3
|
import { useClient } from "../data/api-provider.js";
|
|
4
4
|
import { humanize } from "./naming.js";
|
|
5
|
+
import { fetchLabels } from "./relation-labels.js";
|
|
5
6
|
import { stateToText } from "./filter-string.js";
|
|
6
7
|
import { useHistogram } from "../data/use-aggregates.js";
|
|
7
|
-
import { fetchLabels } from "./relation-labels.js";
|
|
8
8
|
import { widgetFields } from "./view-fields.js";
|
|
9
9
|
import { useQueries, useQueryClient } from "@tanstack/react-query";
|
|
10
10
|
import { enumLabel } from "@protobase/schema";
|
|
@@ -49,7 +49,9 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
|
49
49
|
range: void 0
|
|
50
50
|
}, state, extras);
|
|
51
51
|
const histogram = useHistogram(model.name, skeleton.range?.id ?? "", rangeFilter, 24, Boolean(skeleton.range));
|
|
52
|
-
const
|
|
52
|
+
const rangeHints = skeleton.range ? view?.fields[skeleton.range.id] : void 0;
|
|
53
|
+
const prefix = rangeHints?.prefix;
|
|
54
|
+
const suffix = rangeHints?.suffix ?? "";
|
|
53
55
|
return {
|
|
54
56
|
facets: skeleton.facets.map((group, index) => {
|
|
55
57
|
const field = model.fields[group.id];
|
|
@@ -71,7 +73,7 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
|
71
73
|
max: Math.ceil(histogram.data.max),
|
|
72
74
|
step: Math.max(1, Math.round((histogram.data.max - histogram.data.min) / 100)),
|
|
73
75
|
histogram: histogram.data.buckets.map((bucket) => bucket.count),
|
|
74
|
-
format: (value) => prefix === "€" ? formatMoney(value * 100).replace(".00", "") : String(value)
|
|
76
|
+
format: (value) => `${prefix === "€" ? formatMoney(value * 100).replace(".00", "") : String(value)}${suffix}`
|
|
75
77
|
} },
|
|
76
78
|
...skeleton.dateField && {
|
|
77
79
|
datePresets: skeleton.datePresets ?? true,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/live/value-color.ts
|
|
2
|
+
/** The colour a view gives a value's badge, if any. */
|
|
3
|
+
const valueColor = (colors, value) => {
|
|
4
|
+
const key = String(value);
|
|
5
|
+
return colors && Object.hasOwn(colors, key) ? colors[key] : void 0;
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
export { valueColor };
|
package/dist/package.js
CHANGED
package/dist/primitives/badge.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { readableOn } from "../lib/readable-text.js";
|
|
2
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
4
|
//#region src/primitives/badge.tsx
|
|
4
5
|
const tones = {
|
|
@@ -17,9 +18,17 @@ const dots = {
|
|
|
17
18
|
red: "bg-danger",
|
|
18
19
|
violet: "bg-violet"
|
|
19
20
|
};
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const arrow = "polygon(0 0, calc(100% - 0.625rem) 0, 100% 50%, calc(100% - 0.625rem) 100%, 0 100%)";
|
|
22
|
+
const Badge = ({ tone = "neutral", dot = true, color, shape, children, className }) => /* @__PURE__ */ jsxs("span", {
|
|
23
|
+
style: {
|
|
24
|
+
...color !== void 0 && {
|
|
25
|
+
backgroundColor: color,
|
|
26
|
+
color: readableOn(color)
|
|
27
|
+
},
|
|
28
|
+
...shape === "arrow" && { clipPath: arrow }
|
|
29
|
+
},
|
|
30
|
+
className: cn(shape === "arrow" ? "inline-flex h-5 items-center gap-1.5 rounded-l-sm pr-4 pl-2 text-xs font-semibold whitespace-nowrap" : "inline-flex h-5 items-center gap-1.5 rounded-full px-2 text-xs font-medium whitespace-nowrap", color === void 0 && tones[tone], className),
|
|
31
|
+
children: [dot && color === void 0 && shape === void 0 && /* @__PURE__ */ jsx("span", { className: cn("size-1.5 rounded-full", dots[tone]) }), children]
|
|
23
32
|
});
|
|
24
33
|
//#endregion
|
|
25
34
|
export { Badge };
|
|
@@ -5,17 +5,22 @@ import { useCallback, useEffect, useState } from "react";
|
|
|
5
5
|
import { AuthError } from "@protobase/client";
|
|
6
6
|
//#region src/roles/use-active-role.ts
|
|
7
7
|
const failureMessage = (failure, fallback) => failure instanceof AuthError ? authMessage(failure) : fallback;
|
|
8
|
-
/** The role the session acts with; a failed read or switch shows as a toast. */
|
|
9
|
-
const useActiveRole = (reopen) => {
|
|
8
|
+
/** The role the session acts with, read while `enabled` (someone is signed in); a failed read or switch shows as a toast. */
|
|
9
|
+
const useActiveRole = (reopen, enabled = true) => {
|
|
10
10
|
const { session } = useAuth();
|
|
11
11
|
const toast = useToast();
|
|
12
12
|
const [current, setCurrent] = useState();
|
|
13
13
|
useEffect(() => {
|
|
14
|
+
if (!enabled) return;
|
|
14
15
|
session.roles.current().then(setCurrent, (failure) => toast.show({
|
|
15
16
|
state: "error",
|
|
16
17
|
title: failureMessage(failure, "Could not read your roles")
|
|
17
18
|
}));
|
|
18
|
-
}, [
|
|
19
|
+
}, [
|
|
20
|
+
session,
|
|
21
|
+
toast,
|
|
22
|
+
enabled
|
|
23
|
+
]);
|
|
19
24
|
const switchTo = useCallback((name) => void session.roles.switchTo(name).then(reopen, (failure) => toast.show({
|
|
20
25
|
state: "error",
|
|
21
26
|
title: failureMessage(failure, "Could not switch the role")
|