@protobase/ui 0.46.8 → 0.46.10
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/change-email-dialog.js +1 -1
- package/dist/account/two-factor-setup.js +1 -1
- package/dist/app/confirm-sign-in-messages.js +22 -0
- package/dist/app/confirm-sign-in.js +88 -0
- package/dist/app/create/create-page.js +2 -2
- package/dist/app/create/create-values.js +17 -2
- package/dist/app/nav-from-meta.js +7 -3
- package/dist/app/sensitive-field.js +10 -2
- package/dist/app-shell/sidebar.js +1 -1
- package/dist/auth/auth-provider.js +3 -1
- package/dist/auth/code-form.js +1 -1
- package/dist/auth/two-factor-form.js +1 -1
- package/dist/auth/user-to-shell.js +4 -1
- package/dist/format/messages.js +2 -2
- package/dist/format/number.js +1 -1
- package/dist/forms/public-form-page.js +2 -2
- package/dist/index.js +1 -1
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-D_HKIf03.js +9 -0
- package/dist/shell/assets/export-worker-IJWS5-Wc.js +3 -0
- package/dist/shell/assets/{index-oppnv6lh.js → index-C6JUrzRh.js} +2 -2
- package/dist/shell/assets/{records-map-Ck8_EDiN.js → records-map-kVCCf9yO.js} +2 -2
- package/dist/shell/assets/shared-protobase-ui-t4TJnncm.js +2 -0
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/shell/protobase.sw.js +2 -2
- package/dist/shell-source-maps/assets/app-D_HKIf03.js.map +1 -0
- package/dist/shell-source-maps/assets/export-worker-IJWS5-Wc.js.map +1 -0
- package/dist/shell-source-maps/assets/{index-oppnv6lh.js.map → index-C6JUrzRh.js.map} +1 -1
- package/dist/shell-source-maps/assets/{records-map-Ck8_EDiN.js.map → records-map-kVCCf9yO.js.map} +1 -1
- package/dist/shell-source-maps/assets/{shared-protobase-ui-DWI6ok-_.js.map → shared-protobase-ui-t4TJnncm.js.map} +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-BxGBuUKh.js +0 -9
- package/dist/shell/assets/export-worker-ClACk00b.js +0 -3
- package/dist/shell/assets/shared-protobase-ui-DWI6ok-_.js +0 -2
- package/dist/shell-source-maps/assets/app-BxGBuUKh.js.map +0 -1
- package/dist/shell-source-maps/assets/export-worker-ClACk00b.js.map +0 -1
|
@@ -2,9 +2,9 @@ import { text } from "./messages.js";
|
|
|
2
2
|
import { Button } from "../primitives/button.js";
|
|
3
3
|
import { Dialog } from "../primitives/dialog.js";
|
|
4
4
|
import { Input } from "../primitives/input.js";
|
|
5
|
+
import { OtpInput } from "../primitives/otp-input.js";
|
|
5
6
|
import { SectionError } from "./account-section.js";
|
|
6
7
|
import { authMessage } from "../auth/auth-messages.js";
|
|
7
|
-
import { OtpInput } from "../primitives/otp-input.js";
|
|
8
8
|
import { useState } from "react";
|
|
9
9
|
import { AuthError } from "@protobase/client";
|
|
10
10
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -2,8 +2,8 @@ import { text } from "./messages.js";
|
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
import { Button } from "../primitives/button.js";
|
|
4
4
|
import { Input } from "../primitives/input.js";
|
|
5
|
-
import { authMessage } from "../auth/auth-messages.js";
|
|
6
5
|
import { OtpInput } from "../primitives/otp-input.js";
|
|
6
|
+
import { authMessage } from "../auth/auth-messages.js";
|
|
7
7
|
import { BackupCodes } from "./backup-codes.js";
|
|
8
8
|
import { TotpQr, totpKey } from "./totp-qr.js";
|
|
9
9
|
import { useState } from "react";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineMessages } from "../locale/messages.js";
|
|
2
|
+
//#region src/app/confirm-sign-in-messages.ts
|
|
3
|
+
const text = defineMessages({
|
|
4
|
+
en: {
|
|
5
|
+
confirmTitle: "Confirm it's you",
|
|
6
|
+
confirmWhy: (minutes) => `Showing this needs a passkey or your authenticator app in the last ${minutes} minutes.`,
|
|
7
|
+
usePasskey: "Use a passkey",
|
|
8
|
+
authenticatorCode: "Code from your authenticator app",
|
|
9
|
+
confirm: "Confirm",
|
|
10
|
+
nothingToConfirmWith: "Add a passkey or an authenticator app on your account page first."
|
|
11
|
+
},
|
|
12
|
+
nl: {
|
|
13
|
+
confirmTitle: "Bevestig dat jij het bent",
|
|
14
|
+
confirmWhy: (minutes) => `Dit tonen vraagt in de laatste ${minutes} minuten een passkey of je authenticator-app.`,
|
|
15
|
+
usePasskey: "Passkey gebruiken",
|
|
16
|
+
authenticatorCode: "Code uit je authenticator-app",
|
|
17
|
+
confirm: "Bevestigen",
|
|
18
|
+
nothingToConfirmWith: "Voeg eerst een passkey of een authenticator-app toe op je accountpagina."
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
//#endregion
|
|
22
|
+
export { text };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useOptionalAuth } from "../auth/auth-provider.js";
|
|
2
|
+
import { Button } from "../primitives/button.js";
|
|
3
|
+
import { OtpInput } from "../primitives/otp-input.js";
|
|
4
|
+
import { text } from "./confirm-sign-in-messages.js";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
8
|
+
//#region src/app/confirm-sign-in.tsx
|
|
9
|
+
/**
|
|
10
|
+
* Asks the person to confirm it's them, with a passkey or their authenticator app, after the server refused something
|
|
11
|
+
* that needs a recent strong sign-in (`strong-sign-in-required`). It offers only what the account has; a passkey signs
|
|
12
|
+
* the browser in anew, a code confirms the session it has.
|
|
13
|
+
*/
|
|
14
|
+
const ConfirmSignIn = ({ minutes, onConfirmed }) => {
|
|
15
|
+
const auth = useOptionalAuth();
|
|
16
|
+
const [code, setCode] = useState("");
|
|
17
|
+
const methods = useQuery({
|
|
18
|
+
queryKey: ["account-sign-in-methods"],
|
|
19
|
+
queryFn: () => auth.session.account.signInMethods(),
|
|
20
|
+
enabled: Boolean(auth)
|
|
21
|
+
});
|
|
22
|
+
const passkey = useMutation({
|
|
23
|
+
mutationFn: () => auth.signInWithPasskey(),
|
|
24
|
+
onSuccess: onConfirmed
|
|
25
|
+
});
|
|
26
|
+
const authenticator = useMutation({
|
|
27
|
+
mutationFn: (entered) => auth.verifyTwoFactor({
|
|
28
|
+
method: "totp",
|
|
29
|
+
code: entered
|
|
30
|
+
}),
|
|
31
|
+
onSuccess: onConfirmed
|
|
32
|
+
});
|
|
33
|
+
const account = methods.data?.account;
|
|
34
|
+
const error = passkey.error ?? authenticator.error ?? methods.error;
|
|
35
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
36
|
+
"aria-label": text("confirmTitle"),
|
|
37
|
+
className: "mt-2 flex flex-col gap-2 rounded-md border border-border bg-surface p-3",
|
|
38
|
+
children: [
|
|
39
|
+
/* @__PURE__ */ jsx("p", {
|
|
40
|
+
className: "font-medium",
|
|
41
|
+
children: text("confirmTitle")
|
|
42
|
+
}),
|
|
43
|
+
/* @__PURE__ */ jsx("p", {
|
|
44
|
+
className: "text-muted-foreground",
|
|
45
|
+
children: text("confirmWhy", minutes)
|
|
46
|
+
}),
|
|
47
|
+
account && account.passkeys > 0 && /* @__PURE__ */ jsx(Button, {
|
|
48
|
+
variant: "primary",
|
|
49
|
+
size: "sm",
|
|
50
|
+
className: "self-start",
|
|
51
|
+
loading: passkey.isPending,
|
|
52
|
+
onClick: () => passkey.mutate(),
|
|
53
|
+
children: text("usePasskey")
|
|
54
|
+
}),
|
|
55
|
+
account?.authenticatorApp && /* @__PURE__ */ jsxs("form", {
|
|
56
|
+
className: "flex flex-wrap items-end gap-2",
|
|
57
|
+
onSubmit: (event) => {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
authenticator.mutate(code.trim());
|
|
60
|
+
},
|
|
61
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
62
|
+
className: "flex flex-col gap-1.5",
|
|
63
|
+
children: [text("authenticatorCode"), /* @__PURE__ */ jsx(OtpInput, {
|
|
64
|
+
name: "code",
|
|
65
|
+
required: true,
|
|
66
|
+
value: code,
|
|
67
|
+
invalid: authenticator.isError,
|
|
68
|
+
onChange: setCode,
|
|
69
|
+
onComplete: (entered) => authenticator.mutate(entered)
|
|
70
|
+
})]
|
|
71
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
72
|
+
type: "submit",
|
|
73
|
+
size: "sm",
|
|
74
|
+
loading: authenticator.isPending,
|
|
75
|
+
children: text("confirm")
|
|
76
|
+
})]
|
|
77
|
+
}),
|
|
78
|
+
account && account.passkeys === 0 && !account.authenticatorApp && /* @__PURE__ */ jsx("p", { children: text("nothingToConfirmWith") }),
|
|
79
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
80
|
+
role: "alert",
|
|
81
|
+
className: "font-medium text-danger-text",
|
|
82
|
+
children: error.message
|
|
83
|
+
})
|
|
84
|
+
]
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
export { ConfirmSignIn };
|
|
@@ -6,7 +6,7 @@ import { text } from "../messages.js";
|
|
|
6
6
|
import { useNavigationGuard } from "../router.js";
|
|
7
7
|
import { useAppAddress } from "../use-app-address.js";
|
|
8
8
|
import { text as text$1 } from "./messages.js";
|
|
9
|
-
import { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isRequired } from "./create-values.js";
|
|
9
|
+
import { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isRequired, numberDefault } from "./create-values.js";
|
|
10
10
|
import { FieldEditor } from "../field-editor.js";
|
|
11
11
|
import { can } from "../permissions.js";
|
|
12
12
|
import { fetchLabels } from "../../live/relation-labels.js";
|
|
@@ -133,7 +133,7 @@ const LoadedCreate = ({ resource, values }) => {
|
|
|
133
133
|
value: isChosen(value) ? value.key : value ?? (field.type === "boolean" ? false : ""),
|
|
134
134
|
stored: void 0,
|
|
135
135
|
relationLabel: isChosen(value) ? value.label : void 0,
|
|
136
|
-
placeholder: hasDatabaseDefault(field) ? text$1("setByDatabase") :
|
|
136
|
+
placeholder: hasDatabaseDefault(field) ? text$1("setByDatabase") : numberDefault(field),
|
|
137
137
|
relationPicker: target ? {
|
|
138
138
|
target,
|
|
139
139
|
view: meta.views[target.name]
|
|
@@ -59,17 +59,32 @@ const givenValue = (field, value) => {
|
|
|
59
59
|
if (field.type === "boolean") return value === "true";
|
|
60
60
|
return value;
|
|
61
61
|
};
|
|
62
|
+
const numeric = /* @__PURE__ */ new Set([
|
|
63
|
+
"integer",
|
|
64
|
+
"bigint",
|
|
65
|
+
"decimal"
|
|
66
|
+
]);
|
|
67
|
+
/**
|
|
68
|
+
* A number field's literal default, which its empty input shows as a placeholder rather than as its value: typing into a
|
|
69
|
+
* prefilled `0` would add to it, and the create applies the default to an empty field.
|
|
70
|
+
*/
|
|
71
|
+
const numberDefault = (field) => {
|
|
72
|
+
if (!numeric.has(field.type) || !field.default || !("value" in field.default)) return void 0;
|
|
73
|
+
const value = field.default.value;
|
|
74
|
+
return value === null || value === void 0 ? void 0 : String(value);
|
|
75
|
+
};
|
|
62
76
|
/**
|
|
63
77
|
* The form's starting values: the values the address gives (`/notes/new?listing_id=42`), for the fields the form has,
|
|
64
|
-
* else literal defaults from `/meta
|
|
78
|
+
* else literal defaults from `/meta`, but for numbers. Database defaults stay empty so the database fills them.
|
|
65
79
|
*/
|
|
66
80
|
const initialDraft = (model, sections, given = {}) => Object.fromEntries(sections.flatMap((section) => section.fields.flatMap((name) => {
|
|
67
81
|
const field = model.fields[name];
|
|
68
82
|
if (Object.hasOwn(given, name)) return [[name, givenValue(field, given[name])]];
|
|
69
83
|
const fallback = field.default;
|
|
84
|
+
if (numberDefault(field) !== void 0) return [];
|
|
70
85
|
if (!fallback || !("value" in fallback) || fallback.value === null || fallback.value === void 0) return [];
|
|
71
86
|
return [[name, typeof fallback.value === "boolean" ? fallback.value : String(fallback.value)]];
|
|
72
87
|
})));
|
|
73
88
|
const hasDatabaseDefault = (field) => Boolean(field.default && "db" in field.default);
|
|
74
89
|
//#endregion
|
|
75
|
-
export { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isCreatable, isRequired, keyValue };
|
|
90
|
+
export { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isCreatable, isRequired, keyValue, numberDefault };
|
|
@@ -16,8 +16,13 @@ const groupLabels = {
|
|
|
16
16
|
core: "admin",
|
|
17
17
|
hr: "hr"
|
|
18
18
|
};
|
|
19
|
+
/** The group of a schema's resources: none for the default `public` schema, whose name would read as "visible to anyone". */
|
|
20
|
+
const schemaGroup = (schema = "public") => {
|
|
21
|
+
if (schema === "public") return "";
|
|
22
|
+
return groupLabels[schema] ? text(groupLabels[schema]) : humanize(schema);
|
|
23
|
+
};
|
|
19
24
|
/**
|
|
20
|
-
* One sidebar group per `nav.group` (default: the database schema, and "Pages" for pages); `nav.hidden` and a place in
|
|
25
|
+
* One sidebar group per `nav.group` (default: the database schema, none for `public`, and "Pages" for pages); `nav.hidden` and a place in
|
|
21
26
|
* the user menu leave an entry out, `nav.order` sorts within a group, and `recent` (from `useNavRecent`) makes an entry
|
|
22
27
|
* collapsible. Groups with pages come first, so a dashboard leads the sidebar. The resources of a source that cannot be
|
|
23
28
|
* read are listed under its label, each with the problem. A visitor also gets the resources whose views ask them to sign
|
|
@@ -50,8 +55,7 @@ const navFromMeta = (meta, basePath = "", recent = {}) => {
|
|
|
50
55
|
for (const model of Object.values(meta.resources)) {
|
|
51
56
|
const view = meta.views[model.name];
|
|
52
57
|
if (!view || inMenu.has(model.name)) continue;
|
|
53
|
-
const
|
|
54
|
-
const defaultGroup = view.nav?.group ?? (groupLabels[schema] ? text(groupLabels[schema]) : humanize(schema));
|
|
58
|
+
const defaultGroup = view.nav?.group ?? schemaGroup(model.table.schema);
|
|
55
59
|
const add = (groupName, item, order) => {
|
|
56
60
|
const group = groups.get(groupName) ?? {
|
|
57
61
|
label: groupName,
|
|
@@ -6,15 +6,19 @@ import { displayValue } from "../live/display-value.js";
|
|
|
6
6
|
import { text } from "./messages.js";
|
|
7
7
|
import { toDraft } from "../live/field-values.js";
|
|
8
8
|
import { ErrorBanner } from "./error-banner.js";
|
|
9
|
+
import { ConfirmSignIn } from "./confirm-sign-in.js";
|
|
9
10
|
import { useState } from "react";
|
|
10
11
|
import { Eye, EyeOff } from "lucide-react";
|
|
12
|
+
import { isApiError } from "@protobase/client";
|
|
11
13
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
14
|
import { useMutation } from "@tanstack/react-query";
|
|
13
15
|
//#region src/app/sensitive-field.tsx
|
|
14
16
|
const mask = "••••••••••";
|
|
17
|
+
const strongSignInMinutes = (error) => isApiError(error) && error.slug === "strong-sign-in-required" ? error.problem.minutes ?? 15 : void 0;
|
|
15
18
|
/**
|
|
16
19
|
* A value hidden like a password. Showing a stored one asks the server for it, once per page visit; a write-only one has
|
|
17
|
-
* none to show, so its eye only unmasks what was typed.
|
|
20
|
+
* none to show, so its eye only unmasks what was typed. When the server wants a recent strong sign-in first, the field
|
|
21
|
+
* asks the person to confirm it's them, then shows the value.
|
|
18
22
|
*/
|
|
19
23
|
const SensitiveField = ({ field, hints, value, reveal, locked, invalid, placeholder, onChange }) => {
|
|
20
24
|
const [shown, setShown] = useState(false);
|
|
@@ -38,7 +42,11 @@ const SensitiveField = ({ field, hints, value, reveal, locked, invalid, placehol
|
|
|
38
42
|
onClick: toggle,
|
|
39
43
|
children: !stored.isPending && (shown ? /* @__PURE__ */ jsx(EyeOff, { className: "size-3.5" }) : /* @__PURE__ */ jsx(Eye, { className: "size-3.5" }))
|
|
40
44
|
});
|
|
41
|
-
const
|
|
45
|
+
const minutes = strongSignInMinutes(stored.error);
|
|
46
|
+
const error = minutes !== void 0 ? /* @__PURE__ */ jsx(ConfirmSignIn, {
|
|
47
|
+
minutes,
|
|
48
|
+
onConfirmed: () => stored.mutate()
|
|
49
|
+
}) : stored.error && /* @__PURE__ */ jsx("div", {
|
|
42
50
|
className: "mt-2",
|
|
43
51
|
children: /* @__PURE__ */ jsx(ErrorBanner, {
|
|
44
52
|
title: text("couldNotShow", label),
|
|
@@ -34,7 +34,7 @@ const Sidebar = ({ mode, activeItem, user, workspace, appName, appIcon, forceExp
|
|
|
34
34
|
className: cn("flex-1 overflow-y-auto overflow-x-hidden pb-2", mode === "icon-label" ? "px-2" : "px-2"),
|
|
35
35
|
children: groups.map((group, index) => /* @__PURE__ */ jsxs("div", {
|
|
36
36
|
className: cn("flex flex-col gap-0.5", index > 0 && "mt-3"),
|
|
37
|
-
children: [inline ? /* @__PURE__ */ jsx("div", {
|
|
37
|
+
children: [inline ? group.label && /* @__PURE__ */ jsx("div", {
|
|
38
38
|
className: cn("px-3 pb-1 pt-1 text-xs font-medium uppercase tracking-wide text-faint-foreground", expanding && "h-4 whitespace-nowrap py-0 opacity-0 transition-opacity group-hover/side:opacity-100 group-focus-within/side:opacity-100", expanded && "opacity-100"),
|
|
39
39
|
children: group.label
|
|
40
40
|
}) : index > 0 && /* @__PURE__ */ jsx("div", { className: "mx-auto mb-1 h-px w-6 bg-border" }), group.items.map((item) => item.recent && isTextMode(mode) ? /* @__PURE__ */ jsx(SidebarGroup, {
|
|
@@ -12,6 +12,8 @@ const useAuth = () => {
|
|
|
12
12
|
if (!api) throw new Error("useAuth must be used inside AuthProvider");
|
|
13
13
|
return api;
|
|
14
14
|
};
|
|
15
|
+
/** The auth state and steps inside `AuthProvider`, `null` outside it, for a part that also works without one. */
|
|
16
|
+
const useOptionalAuth = () => useContext(AuthContext);
|
|
15
17
|
/**
|
|
16
18
|
* Finds out whether the server has a login and who is signed in, and keeps that state for the app. Without a `session`,
|
|
17
19
|
* it makes one on `origin`, the page's own by default; in the iOS or Android app, with its `nativeBridge`, the session
|
|
@@ -216,4 +218,4 @@ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
|
|
|
216
218
|
});
|
|
217
219
|
};
|
|
218
220
|
//#endregion
|
|
219
|
-
export { AuthProvider, useAuth };
|
|
221
|
+
export { AuthProvider, useAuth, useOptionalAuth };
|
package/dist/auth/code-form.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { text } from "./messages.js";
|
|
2
2
|
import { Button } from "../primitives/button.js";
|
|
3
|
-
import { AuthLayout } from "./auth-layout.js";
|
|
4
3
|
import { OtpInput } from "../primitives/otp-input.js";
|
|
4
|
+
import { AuthLayout } from "./auth-layout.js";
|
|
5
5
|
import { ResendCodeButton } from "./resend-code-button.js";
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { text } from "./messages.js";
|
|
2
2
|
import { Button } from "../primitives/button.js";
|
|
3
3
|
import { Input } from "../primitives/input.js";
|
|
4
|
+
import { OtpInput } from "../primitives/otp-input.js";
|
|
4
5
|
import { Checkbox } from "../primitives/checkbox.js";
|
|
5
6
|
import { AuthLayout } from "./auth-layout.js";
|
|
6
|
-
import { OtpInput } from "../primitives/otp-input.js";
|
|
7
7
|
import { useState } from "react";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/auth/two-factor-form.tsx
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { text } from "./messages.js";
|
|
2
|
+
import { capitalise } from "../live/naming.js";
|
|
2
3
|
//#region src/auth/user-to-shell.ts
|
|
3
4
|
/** The hue of someone's avatar, the same for the same id. */
|
|
4
5
|
const hueOf = (text) => [...text].reduce((sum, char) => (sum * 31 + char.charCodeAt(0)) % 360, 7);
|
|
@@ -7,6 +8,8 @@ const initialsOf = (name) => name.split(/[\s@.]+/).filter(Boolean).slice(0, 2).m
|
|
|
7
8
|
const rolesOf = (user) => (user.role ?? "user").split(",").map((role) => role.trim());
|
|
8
9
|
/** The user holds the `admin` role, among others perhaps. */
|
|
9
10
|
const isAdmin = (user) => rolesOf(user).includes("admin");
|
|
11
|
+
/** A role's name as words: `full_access` becomes "Full access". */
|
|
12
|
+
const roleLabel = (role) => capitalise(role.replace(/[_-]+/g, " ").trim());
|
|
10
13
|
/** What the sidebar's profile menu shows for a signed-in user. */
|
|
11
14
|
const userToShell = (user) => {
|
|
12
15
|
const roles = rolesOf(user);
|
|
@@ -16,7 +19,7 @@ const userToShell = (user) => {
|
|
|
16
19
|
email: user.email,
|
|
17
20
|
initials: initialsOf(user.name || user.email) || "?",
|
|
18
21
|
hue: hueOf(user.id || user.email),
|
|
19
|
-
role: admin ? text("admin") : (roles[0] ?? "user")
|
|
22
|
+
role: admin ? text("admin") : roleLabel(roles[0] ?? "user"),
|
|
20
23
|
roleTone: admin ? "violet" : "blue"
|
|
21
24
|
};
|
|
22
25
|
};
|
package/dist/format/messages.js
CHANGED
|
@@ -2,7 +2,7 @@ import { defineMessages } from "../locale/messages.js";
|
|
|
2
2
|
//#region src/format/messages.ts
|
|
3
3
|
const text = defineMessages({
|
|
4
4
|
en: {
|
|
5
|
-
rows: (count) => `${count} rows`,
|
|
5
|
+
rows: (count, one) => `${count} ${one ? "row" : "rows"}`,
|
|
6
6
|
aboutRows: (count) => `about ${count} rows`,
|
|
7
7
|
secondsAgo: (count) => `${count} s ago`,
|
|
8
8
|
minutesAgo: (count) => `${count} min ago`,
|
|
@@ -11,7 +11,7 @@ const text = defineMessages({
|
|
|
11
11
|
justNow: "just now"
|
|
12
12
|
},
|
|
13
13
|
nl: {
|
|
14
|
-
rows: (count) => `${count} rijen`,
|
|
14
|
+
rows: (count, one) => `${count} ${one ? "rij" : "rijen"}`,
|
|
15
15
|
aboutRows: (count) => `ongeveer ${count} rijen`,
|
|
16
16
|
secondsAgo: (count) => `${count} s geleden`,
|
|
17
17
|
minutesAgo: (count) => `${count} min geleden`,
|
package/dist/format/number.js
CHANGED
|
@@ -6,7 +6,7 @@ const formatInt = (value) => grouped().format(value);
|
|
|
6
6
|
const formatSigned = (value) => value > 0 ? `+${grouped().format(value)}` : grouped().format(value).replace("-", "−");
|
|
7
7
|
const formatPercent = (value) => `${value}%`;
|
|
8
8
|
const formatApproxRows = (count) => {
|
|
9
|
-
if (count < 1e4) return text("rows", formatInt(count));
|
|
9
|
+
if (count < 1e4) return text("rows", formatInt(count), count === 1);
|
|
10
10
|
const magnitude = 10 ** (Math.floor(Math.log10(count)) - 3);
|
|
11
11
|
return text("aboutRows", formatInt(Math.round(count / magnitude) * magnitude));
|
|
12
12
|
};
|
|
@@ -6,7 +6,7 @@ import { Notice } from "../app/meta-gate.js";
|
|
|
6
6
|
import { Button } from "../primitives/button.js";
|
|
7
7
|
import { Field } from "../record-view/field.js";
|
|
8
8
|
import { humanize } from "../live/naming.js";
|
|
9
|
-
import { createBody, hasDatabaseDefault, initialDraft, isRequired } from "../app/create/create-values.js";
|
|
9
|
+
import { createBody, hasDatabaseDefault, initialDraft, isRequired, numberDefault } from "../app/create/create-values.js";
|
|
10
10
|
import { ErrorBanner } from "../app/error-banner.js";
|
|
11
11
|
import { FieldEditor } from "../app/field-editor.js";
|
|
12
12
|
import { fieldErrors, fieldMessage } from "../app/field-errors.js";
|
|
@@ -123,7 +123,7 @@ const LoadedForm = ({ form }) => {
|
|
|
123
123
|
hints,
|
|
124
124
|
value: draft[name] ?? (field.type === "boolean" ? false : ""),
|
|
125
125
|
stored: void 0,
|
|
126
|
-
placeholder: hasDatabaseDefault(field) ? text$1("setByDatabase") :
|
|
126
|
+
placeholder: hasDatabaseDefault(field) ? text$1("setByDatabase") : numberDefault(field),
|
|
127
127
|
invalid: Boolean(problem),
|
|
128
128
|
onChange: (next) => change(name, next)
|
|
129
129
|
})
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { AppLink, useAppAddress } from "./app/use-app-address.js";
|
|
|
12
12
|
import { Segmented } from "./primitives/segmented.js";
|
|
13
13
|
import { Textarea } from "./primitives/textarea.js";
|
|
14
14
|
import { useList } from "./data/use-list.js";
|
|
15
|
+
import { OtpInput } from "./primitives/otp-input.js";
|
|
15
16
|
import { definePlugin, defineUi } from "./app/pages/project-ui.js";
|
|
16
17
|
import { Markdown } from "./markdown/markdown.js";
|
|
17
18
|
import { ActionInputError } from "./app/pages/action-inputs/input-errors.js";
|
|
@@ -22,7 +23,6 @@ import { SignInPrompt } from "./auth/sign-in-prompt.js";
|
|
|
22
23
|
import { useListDisplay } from "./app/displays/display-context.js";
|
|
23
24
|
import { MenuSelect } from "./primitives/menu-select.js";
|
|
24
25
|
import { AuthLayout } from "./auth/auth-layout.js";
|
|
25
|
-
import { OtpInput } from "./primitives/otp-input.js";
|
|
26
26
|
import { allowDelayMs, isFramed, useArmedAfterFocus, useGuardedPress } from "./connect/interaction-guard.js";
|
|
27
27
|
import { ResendCodeButton } from "./auth/resend-code-button.js";
|
|
28
28
|
import { CodeForm } from "./auth/code-form.js";
|
package/dist/package.js
CHANGED