@protobase/ui 0.3.4 → 0.4.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.
Files changed (91) hide show
  1. package/dist/account/account-pages.js +20 -0
  2. package/dist/account/account-section.js +31 -0
  3. package/dist/account/account-security-page.js +59 -0
  4. package/dist/account/backup-codes.js +44 -0
  5. package/dist/account/passkeys-section.js +141 -0
  6. package/dist/account/password-dialog.js +58 -0
  7. package/dist/account/sign-in-policy-form.js +108 -0
  8. package/dist/account/sign-in-policy-labels.js +76 -0
  9. package/dist/account/sign-in-policy-page.js +72 -0
  10. package/dist/account/staff-sign-in-log.js +62 -0
  11. package/dist/account/totp-qr.js +37 -0
  12. package/dist/account/two-factor-section.js +123 -0
  13. package/dist/account/two-factor-setup.js +188 -0
  14. package/dist/activity-chart/activity-chart.js +1 -1
  15. package/dist/activity-chart/activity-tooltip.js +1 -1
  16. package/dist/app/app.d.ts +3 -1
  17. package/dist/app/app.js +50 -18
  18. package/dist/app/assistant-shell.js +1 -1
  19. package/dist/app/create/create-page.js +1 -1
  20. package/dist/app/delete/use-delete-flow.js +1 -1
  21. package/dist/app/error-banner.js +1 -1
  22. package/dist/app/list-page.js +1 -1
  23. package/dist/app/list-search.js +1 -1
  24. package/dist/app/meta-gate.js +1 -1
  25. package/dist/app/pages/blocks/modal-form.js +3 -3
  26. package/dist/app/pages/record-table.js +1 -1
  27. package/dist/app/record-page.js +5 -5
  28. package/dist/app/runtime/runtime-status.js +25 -0
  29. package/dist/app/runtime/runtime-update.js +77 -0
  30. package/dist/app/runtime/use-runtime-status.js +59 -0
  31. package/dist/app/sensitive-field.js +1 -1
  32. package/dist/app/staff-banner.js +67 -0
  33. package/dist/app/use-record-editor.js +1 -1
  34. package/dist/app-shell/breadcrumb.js +1 -1
  35. package/dist/app-shell/global-search.js +1 -1
  36. package/dist/app-shell/profile-menu.js +1 -1
  37. package/dist/app-shell/sidebar-group.js +1 -1
  38. package/dist/app-shell/top-bar.js +1 -1
  39. package/dist/assistant-dock/assistant-dock.js +1 -1
  40. package/dist/auth/auth-gate.js +42 -4
  41. package/dist/auth/auth-messages.js +48 -0
  42. package/dist/auth/auth-provider.js +40 -14
  43. package/dist/auth/code-input.js +17 -0
  44. package/dist/auth/email-code-form.js +81 -0
  45. package/dist/auth/email-code-page.js +40 -0
  46. package/dist/auth/first-run-page.js +1 -1
  47. package/dist/auth/forgot-password-page.js +1 -1
  48. package/dist/auth/reset-password-page.js +1 -1
  49. package/dist/auth/setup-required-form.js +43 -0
  50. package/dist/auth/setup-required-page.js +51 -0
  51. package/dist/auth/sign-in-form.js +53 -20
  52. package/dist/auth/sign-in-page.js +14 -13
  53. package/dist/auth/staff-link.js +30 -0
  54. package/dist/auth/staff-sign-in-form.js +93 -0
  55. package/dist/auth/staff-sign-in-page.js +41 -0
  56. package/dist/auth/two-factor-form.js +137 -0
  57. package/dist/auth/two-factor-page.js +39 -0
  58. package/dist/auth/user-to-shell.js +5 -2
  59. package/dist/chat/composer.js +1 -1
  60. package/dist/data/api-provider.d.ts +1 -1
  61. package/dist/data/api-provider.js +1 -1
  62. package/dist/data/meta-data.d.ts +4 -0
  63. package/dist/data/use-aggregates.js +1 -1
  64. package/dist/data/use-list.js +1 -1
  65. package/dist/data/use-meta.js +2 -1
  66. package/dist/data/use-update-record.js +1 -1
  67. package/dist/data-grid/cells.js +3 -3
  68. package/dist/data-grid/grid-header.js +2 -2
  69. package/dist/data-grid/grid-row.js +1 -1
  70. package/dist/data-grid/header-menu.js +1 -1
  71. package/dist/data-grid/row-menu.js +1 -1
  72. package/dist/diff-view/diff-view.js +1 -1
  73. package/dist/filter-panel/active-filter-chips.js +1 -1
  74. package/dist/filter-panel/facet-group.js +1 -1
  75. package/dist/filter-panel/filter-menu.js +1 -1
  76. package/dist/inline-child-table/inline-child-table.js +1 -1
  77. package/dist/inline-child-table/line-menu.js +1 -1
  78. package/dist/inline-child-table/line-row.js +2 -2
  79. package/dist/inline-child-table/totals-footer.js +1 -1
  80. package/dist/lib/use-visual-viewport.js +27 -0
  81. package/dist/live/use-filter-config.js +1 -1
  82. package/dist/primitives/checkbox.js +1 -1
  83. package/dist/primitives/sheet.js +1 -1
  84. package/dist/primitives/spinner.js +1 -1
  85. package/dist/record-view/field.js +1 -1
  86. package/dist/record-view/record-sidebar.js +1 -1
  87. package/dist/side-panel/side-panel.d.ts +5 -1
  88. package/dist/side-panel/side-panel.js +49 -35
  89. package/dist/step-list/step-list.js +1 -1
  90. package/dist/toasts/toast.js +1 -1
  91. package/package.json +10 -8
@@ -0,0 +1,37 @@
1
+ import { useMemo } from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { encode } from "uqr";
4
+ //#region src/account/totp-qr.tsx
5
+ /** The `otpauth://` URI as a QR code for an authenticator app to scan: one square per dark module, with a quiet border. */
6
+ const TotpQr = ({ uri, className }) => {
7
+ const { size, path } = useMemo(() => {
8
+ const qr = encode(uri, {
9
+ ecc: "M",
10
+ border: 2
11
+ });
12
+ const squares = qr.data.flatMap((row, y) => row.flatMap((dark, x) => dark ? [`M${x} ${y}h1v1h-1z`] : []));
13
+ return {
14
+ size: qr.size,
15
+ path: squares.join("")
16
+ };
17
+ }, [uri]);
18
+ return /* @__PURE__ */ jsxs("svg", {
19
+ viewBox: `0 0 ${size} ${size}`,
20
+ role: "img",
21
+ "aria-label": "QR code for your authenticator app",
22
+ shapeRendering: "crispEdges",
23
+ className,
24
+ children: [/* @__PURE__ */ jsx("rect", {
25
+ width: size,
26
+ height: size,
27
+ fill: "#fff"
28
+ }), /* @__PURE__ */ jsx("path", {
29
+ d: path,
30
+ fill: "#000"
31
+ })]
32
+ });
33
+ };
34
+ /** The secret in an `otpauth://` URI, in groups of four, for typing into an app that cannot scan. */
35
+ const totpKey = (uri) => (new URL(uri).searchParams.get("secret") ?? "").replace(/(.{4})(?=.)/g, "$1 ");
36
+ //#endregion
37
+ export { TotpQr, totpKey };
@@ -0,0 +1,123 @@
1
+ import { Badge } from "../primitives/badge.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { Dialog } from "../primitives/dialog.js";
4
+ import { AccountSection } from "./account-section.js";
5
+ import { BackupCodes } from "./backup-codes.js";
6
+ import { PasswordDialog } from "./password-dialog.js";
7
+ import { TwoFactorSetup } from "./two-factor-setup.js";
8
+ import { useState } from "react";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+ //#region src/account/two-factor-section.tsx
11
+ const offDescriptions = {
12
+ allowed: "Ask for a second step after your password or an emailed sign-in code: a code from an authenticator app, or one emailed to you. A passkey needs no second step.",
13
+ required: "This app requires two-factor authentication for every account. Turn it on to keep using the app.",
14
+ forbidden: "Your admin turned two-factor authentication off for this app."
15
+ };
16
+ const TwoFactorSection = (props) => {
17
+ const { rule, enabled, authenticatorApp, mail, needsPassword, onTurnOff, onNewBackupCodes, onChanged } = props;
18
+ const [open, setOpen] = useState();
19
+ const close = () => setOpen(void 0);
20
+ const status = enabled ? /* @__PURE__ */ jsx(Badge, {
21
+ tone: "green",
22
+ children: "On"
23
+ }) : rule === "required" ? /* @__PURE__ */ jsx(Badge, {
24
+ tone: "amber",
25
+ children: "Required"
26
+ }) : /* @__PURE__ */ jsx(Badge, { children: "Off" });
27
+ const description = !enabled ? offDescriptions[rule] : authenticatorApp ? `Signing in with a password or an emailed sign-in code also asks for a code from your authenticator app${mail ? ", or one emailed to you" : ""}. Backup codes work when you do not have the app.` : "Signing in with a password or an emailed sign-in code also asks for a code emailed to you.";
28
+ if (open?.kind === "setup") return /* @__PURE__ */ jsx(AccountSection, {
29
+ title: "Two-factor authentication",
30
+ status,
31
+ description,
32
+ children: /* @__PURE__ */ jsx("div", {
33
+ className: "max-w-sm",
34
+ children: /* @__PURE__ */ jsx(TwoFactorSetup, {
35
+ offerEmailedCodes: mail && !open.appOnly,
36
+ needsPassword,
37
+ onStartApp: props.onStartApp,
38
+ onConfirmApp: props.onConfirmApp,
39
+ onEmailedCodes: props.onEmailedCodes,
40
+ onCancel: close,
41
+ onDone: () => {
42
+ close();
43
+ onChanged();
44
+ }
45
+ })
46
+ })
47
+ });
48
+ return /* @__PURE__ */ jsxs(AccountSection, {
49
+ title: "Two-factor authentication",
50
+ status,
51
+ description,
52
+ children: [
53
+ /* @__PURE__ */ jsxs("div", {
54
+ className: "flex flex-wrap gap-2",
55
+ children: [
56
+ !enabled && rule !== "forbidden" && /* @__PURE__ */ jsx(Button, {
57
+ variant: "primary",
58
+ onClick: () => setOpen({
59
+ kind: "setup",
60
+ appOnly: false
61
+ }),
62
+ children: "Turn on"
63
+ }),
64
+ enabled && !authenticatorApp && rule !== "forbidden" && /* @__PURE__ */ jsx(Button, {
65
+ onClick: () => setOpen({
66
+ kind: "setup",
67
+ appOnly: true
68
+ }),
69
+ children: "Add an authenticator app"
70
+ }),
71
+ enabled && authenticatorApp && /* @__PURE__ */ jsx(Button, {
72
+ onClick: () => setOpen({ kind: "new-codes" }),
73
+ children: "New backup codes"
74
+ }),
75
+ enabled && /* @__PURE__ */ jsx(Button, {
76
+ variant: "danger",
77
+ disabled: rule === "required",
78
+ title: rule === "required" ? "This app requires two-factor authentication." : void 0,
79
+ onClick: () => setOpen({ kind: "turn-off" }),
80
+ children: "Turn off"
81
+ })
82
+ ]
83
+ }),
84
+ open?.kind === "turn-off" && /* @__PURE__ */ jsx(PasswordDialog, {
85
+ title: "Turn off two-factor authentication?",
86
+ description: "Signing in then needs only your password or an emailed code. Your backup codes stop working.",
87
+ confirmLabel: "Turn off",
88
+ danger: true,
89
+ needsPassword,
90
+ onClose: close,
91
+ onConfirm: async (password) => {
92
+ await onTurnOff(password);
93
+ close();
94
+ onChanged();
95
+ }
96
+ }),
97
+ open?.kind === "new-codes" && /* @__PURE__ */ jsx(PasswordDialog, {
98
+ title: "Make new backup codes?",
99
+ description: "Your current backup codes stop working, and you get ten new ones.",
100
+ confirmLabel: "Make new codes",
101
+ needsPassword,
102
+ onClose: close,
103
+ onConfirm: async (password) => setOpen({
104
+ kind: "codes",
105
+ codes: await onNewBackupCodes(password)
106
+ })
107
+ }),
108
+ open?.kind === "codes" && /* @__PURE__ */ jsx(Dialog, {
109
+ title: "Your new backup codes",
110
+ description: "Keep them somewhere safe: each one signs you in once if you lose your phone. They are not shown again.",
111
+ onClose: close,
112
+ actions: /* @__PURE__ */ jsx(Button, {
113
+ variant: "primary",
114
+ onClick: close,
115
+ children: "I saved my backup codes"
116
+ }),
117
+ children: /* @__PURE__ */ jsx(BackupCodes, { codes: open.codes })
118
+ })
119
+ ]
120
+ });
121
+ };
122
+ //#endregion
123
+ export { TwoFactorSection };
@@ -0,0 +1,188 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { Input } from "../primitives/input.js";
3
+ import { authMessage } from "../auth/auth-messages.js";
4
+ import { Button } from "../primitives/button.js";
5
+ import { BackupCodes } from "./backup-codes.js";
6
+ import { CodeInput } from "../auth/code-input.js";
7
+ import { TotpQr, totpKey } from "./totp-qr.js";
8
+ import { useState } from "react";
9
+ import { AuthError } from "@protobase/client";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ //#region src/account/two-factor-setup.tsx
12
+ const choices = [{
13
+ value: "app",
14
+ label: "Authenticator app",
15
+ description: "A code from an app such as Google Authenticator, Microsoft Authenticator or 1Password. Comes with backup codes."
16
+ }, {
17
+ value: "email",
18
+ label: "Emailed code",
19
+ description: "A code mailed to your address each time you sign in with a password or an emailed sign-in code."
20
+ }];
21
+ /** Turns on two-factor authentication: choose the app or emailed codes, confirm the password, scan, and keep the backup codes. */
22
+ const TwoFactorSetup = ({ offerEmailedCodes, needsPassword, onStartApp, onConfirmApp, onEmailedCodes, onDone, onCancel }) => {
23
+ const [step, setStep] = useState({ kind: "choose" });
24
+ const [choice, setChoice] = useState("app");
25
+ const [password, setPassword] = useState("");
26
+ const [code, setCode] = useState("");
27
+ const [busy, setBusy] = useState(false);
28
+ const [error, setError] = useState();
29
+ const offered = offerEmailedCodes ? choices : choices.filter((option) => option.value === "app");
30
+ const attempt = async (action) => {
31
+ setBusy(true);
32
+ setError(void 0);
33
+ try {
34
+ await action();
35
+ } catch (failure) {
36
+ if (!(failure instanceof AuthError)) throw failure;
37
+ setError(authMessage(failure));
38
+ }
39
+ setBusy(false);
40
+ };
41
+ const start = () => attempt(async () => {
42
+ const given = needsPassword ? password : void 0;
43
+ if (choice === "email") {
44
+ await onEmailedCodes(given);
45
+ return onDone();
46
+ }
47
+ setStep({
48
+ kind: "scan",
49
+ setup: await onStartApp(given)
50
+ });
51
+ });
52
+ const confirm = (setup) => attempt(async () => {
53
+ await onConfirmApp(code.trim());
54
+ setStep({
55
+ kind: "backup-codes",
56
+ codes: setup.backupCodes
57
+ });
58
+ });
59
+ const errorLine = error && /* @__PURE__ */ jsx("p", {
60
+ role: "alert",
61
+ className: "text-xs font-medium text-danger-text",
62
+ children: error
63
+ });
64
+ if (step.kind === "backup-codes") return /* @__PURE__ */ jsxs("div", {
65
+ className: "flex flex-col gap-4",
66
+ children: [
67
+ /* @__PURE__ */ jsx("p", {
68
+ className: "text-[13px] text-muted-foreground",
69
+ children: "Two-factor authentication is on. Keep these backup codes somewhere safe: each one signs you in once if you lose your phone. They are not shown again."
70
+ }),
71
+ /* @__PURE__ */ jsx(BackupCodes, { codes: step.codes }),
72
+ /* @__PURE__ */ jsx(Button, {
73
+ variant: "primary",
74
+ onClick: onDone,
75
+ className: "min-h-10",
76
+ children: "I saved my backup codes"
77
+ })
78
+ ]
79
+ });
80
+ if (step.kind === "scan") return /* @__PURE__ */ jsxs("form", {
81
+ className: "flex flex-col gap-4",
82
+ onSubmit: (event) => {
83
+ event.preventDefault();
84
+ confirm(step.setup);
85
+ },
86
+ children: [
87
+ /* @__PURE__ */ jsx("p", {
88
+ className: "text-[13px] text-muted-foreground",
89
+ children: "Scan this code with your authenticator app, then enter the 6-digit code it shows."
90
+ }),
91
+ /* @__PURE__ */ jsxs("div", {
92
+ className: "flex flex-col items-center gap-2",
93
+ children: [/* @__PURE__ */ jsx(TotpQr, {
94
+ uri: step.setup.totpURI,
95
+ className: "size-44 rounded-md border"
96
+ }), /* @__PURE__ */ jsxs("p", {
97
+ className: "text-center text-xs text-muted-foreground",
98
+ children: ["Can't scan it? Enter this key in the app:", /* @__PURE__ */ jsx("span", {
99
+ className: "mt-1 block break-all font-mono text-[13px] text-foreground",
100
+ children: totpKey(step.setup.totpURI)
101
+ })]
102
+ })]
103
+ }),
104
+ /* @__PURE__ */ jsxs("label", {
105
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
106
+ children: ["Code from the app", /* @__PURE__ */ jsx(CodeInput, {
107
+ name: "code",
108
+ required: true,
109
+ autoFocus: true,
110
+ value: code,
111
+ invalid: Boolean(error),
112
+ onChange: (event) => setCode(event.target.value)
113
+ })]
114
+ }),
115
+ errorLine,
116
+ /* @__PURE__ */ jsx(Button, {
117
+ variant: "primary",
118
+ type: "submit",
119
+ loading: busy,
120
+ className: "min-h-10",
121
+ children: "Turn on"
122
+ }),
123
+ onCancel && /* @__PURE__ */ jsx(Button, {
124
+ variant: "ghost",
125
+ onClick: onCancel,
126
+ className: "min-h-10",
127
+ children: "Cancel"
128
+ })
129
+ ]
130
+ });
131
+ return /* @__PURE__ */ jsxs("form", {
132
+ className: "flex flex-col gap-4",
133
+ onSubmit: (event) => {
134
+ event.preventDefault();
135
+ start();
136
+ },
137
+ children: [
138
+ /* @__PURE__ */ jsx("div", {
139
+ role: "radiogroup",
140
+ "aria-label": "Second step",
141
+ className: "flex flex-col gap-2",
142
+ children: offered.map((option) => /* @__PURE__ */ jsxs("button", {
143
+ type: "button",
144
+ role: "radio",
145
+ "aria-checked": choice === option.value,
146
+ onClick: () => setChoice(option.value),
147
+ className: cn("rounded-md border p-3 text-left transition-colors", choice === option.value ? "border-primary bg-primary-soft" : "border-border-strong hover:bg-muted"),
148
+ children: [/* @__PURE__ */ jsx("span", {
149
+ className: "block text-[13px] font-medium",
150
+ children: option.label
151
+ }), /* @__PURE__ */ jsx("span", {
152
+ className: "mt-0.5 block text-xs text-muted-foreground",
153
+ children: option.description
154
+ })]
155
+ }, option.value))
156
+ }),
157
+ needsPassword && /* @__PURE__ */ jsxs("label", {
158
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
159
+ children: ["Your password", /* @__PURE__ */ jsx(Input, {
160
+ type: "password",
161
+ name: "password",
162
+ autoComplete: "current-password",
163
+ required: true,
164
+ value: password,
165
+ invalid: Boolean(error),
166
+ onChange: (event) => setPassword(event.target.value),
167
+ className: "min-h-6"
168
+ })]
169
+ }),
170
+ errorLine,
171
+ /* @__PURE__ */ jsx(Button, {
172
+ variant: "primary",
173
+ type: "submit",
174
+ loading: busy,
175
+ className: "min-h-10",
176
+ children: "Continue"
177
+ }),
178
+ onCancel && /* @__PURE__ */ jsx(Button, {
179
+ variant: "ghost",
180
+ onClick: onCancel,
181
+ className: "min-h-10",
182
+ children: "Cancel"
183
+ })
184
+ ]
185
+ });
186
+ };
187
+ //#endregion
188
+ export { TwoFactorSetup };
@@ -1,8 +1,8 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { useMediaQuery } from "../lib/use-media-query.js";
3
+ import { formatInt } from "../format/number.js";
3
4
  import { formatDayShort } from "../format/date.js";
4
5
  import { activitySeries } from "../mocks/activity.js";
5
- import { formatInt } from "../format/number.js";
6
6
  import { Segmented } from "../primitives/segmented.js";
7
7
  import { halfOverHalfDelta, totalCount } from "./activity-math.js";
8
8
  import { ActivityTooltip } from "./activity-tooltip.js";
@@ -1,5 +1,5 @@
1
- import { formatDate } from "../format/date.js";
2
1
  import { formatInt } from "../format/number.js";
2
+ import { formatDate } from "../format/date.js";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/activity-chart/activity-tooltip.tsx
5
5
  const ActivityTooltip = ({ active, payload, noun }) => {
package/dist/app/app.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { SidebarMode } from "../app-shell/sidebar-mode.js";
2
2
  import "../app-shell/index.js";
3
3
  import { ProjectUi } from "./pages/project-ui.js";
4
- import { AssistantClient, AuthSession, Client } from "@protobase/client";
4
+ import { AssistantClient, AuthSession, Client, RuntimeClient } from "@protobase/client";
5
5
  //#region src/app/app.d.ts
6
6
  export type AppProps = {
7
7
  /** Where `/api/v1` lives. Defaults to the current origin. */
@@ -20,6 +20,8 @@ export type AppProps = {
20
20
  ui?: ProjectUi;
21
21
  /** A ready-made client of the assistant backend, for stories and tests; used only when `/meta` names an assistant. */
22
22
  assistant?: AssistantClient;
23
+ /** A ready-made client of the runtime endpoint, for stories and tests; used only when `/meta` names one. */
24
+ runtime?: RuntimeClient;
23
25
  };
24
26
  /** The admin: sign-in first, then a sidebar from `/meta`, `/:page` composed pages, `/:resource` lists and `/:resource/:key` records. Fills its parent. */
25
27
  export declare const App: ({ auth, workspace, ...rest }: AppProps) => import("react").JSX.Element;
package/dist/app/app.js CHANGED
@@ -1,8 +1,11 @@
1
+ import { accountMenuItems } from "../account/account-pages.js";
1
2
  import { AppShell } from "../app-shell/app-shell.js";
2
- import { ApiProvider } from "../data/api-provider.js";
3
3
  import { AuthProvider, useAuth } from "../auth/auth-provider.js";
4
+ import { AccountSecurityPage } from "../account/account-security-page.js";
5
+ import { SignInPolicyPage } from "../account/sign-in-policy-page.js";
6
+ import { ApiProvider } from "../data/api-provider.js";
4
7
  import { AuthGate } from "../auth/auth-gate.js";
5
- import { userToShell } from "../auth/user-to-shell.js";
8
+ import { isAdmin, userToShell } from "../auth/user-to-shell.js";
6
9
  import { useRecord } from "../data/use-record.js";
7
10
  import { recordTitle } from "../live/model-helpers.js";
8
11
  import { humanize } from "../live/naming.js";
@@ -19,15 +22,17 @@ import { ProjectUiProvider, useProjectUi } from "./pages/project-ui.js";
19
22
  import { ComposedPage } from "./pages/composed-page.js";
20
23
  import { RecordPage } from "./record-page.js";
21
24
  import { useGlobalSearch } from "./search/use-global-search.js";
25
+ import { RuntimeUpdateButton } from "./runtime/runtime-update.js";
26
+ import { StaffBanner } from "./staff-banner.js";
22
27
  import { useNavRecent } from "./use-nav-recent.js";
23
28
  import { useEffect, useState } from "react";
24
29
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
25
30
  //#region src/app/app.tsx
26
- const Routes = ({ workspace, sidebarMode, assistant }) => {
31
+ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
27
32
  const meta = useAdminMeta();
28
- const { state, signOut } = useAuth();
33
+ const { state, signOut, stopStaffSession } = useAuth();
29
34
  if (state.kind !== "signed-in") throw new Error("The shell is only rendered for a signed-in user");
30
- const { user } = state;
35
+ const { user, staff } = state;
31
36
  const { path, params, basePath, navigate } = useRouter();
32
37
  const route = matchRoute(path);
33
38
  const search = useGlobalSearch();
@@ -63,7 +68,14 @@ const Routes = ({ workspace, sidebarMode, assistant }) => {
63
68
  page: params.size > 0 ? `${path}?${params}` : path,
64
69
  onClose: () => setAssistantOpen(false)
65
70
  });
66
- const breadcrumb = breadcrumbFor({
71
+ const runtimeUrl = meta.runtime?.url;
72
+ const runtimeButton = runtimeUrl && /* @__PURE__ */ jsx(RuntimeUpdateButton, {
73
+ url: runtimeUrl,
74
+ client: runtime
75
+ });
76
+ const admin = isAdmin(user);
77
+ const accountPage = path === "/-/account" ? "Sign-in & security" : path === "/-/sign-in-policy" ? "Sign-in policy" : void 0;
78
+ const breadcrumb = accountPage ? [accountPage] : breadcrumbFor({
67
79
  basePath,
68
80
  route,
69
81
  group: group?.label,
@@ -71,7 +83,17 @@ const Routes = ({ workspace, sidebarMode, assistant }) => {
71
83
  resourceLabel: view?.names?.plural ?? (route.resource && humanize(route.resource)),
72
84
  recordTitle: title
73
85
  });
74
- return /* @__PURE__ */ jsx(AppShell, {
86
+ const content = path === "/-/account" ? /* @__PURE__ */ jsx(AccountSecurityPage, { email: user.email }) : path === "/-/sign-in-policy" ? admin ? /* @__PURE__ */ jsx(SignInPolicyPage, {}) : /* @__PURE__ */ jsx(Notice, {
87
+ title: "Admins only",
88
+ children: "Only an admin can change how people sign in."
89
+ }) : !route.resource ? /* @__PURE__ */ jsx(Notice, {
90
+ title: "Choose a resource",
91
+ children: "Pick one from the sidebar."
92
+ }) : page ? /* @__PURE__ */ jsx(ComposedPage, { name: page.name }, page.name) : route.key === "new" ? /* @__PURE__ */ jsx(CreatePage, { resource: route.resource }, `${route.resource}/new`) : route.key ? /* @__PURE__ */ jsx(RecordPage, {
93
+ resource: route.resource,
94
+ recordKey: route.key
95
+ }, `${route.resource}/${route.key}`) : /* @__PURE__ */ jsx(ListPage, { resource: route.resource }, route.resource);
96
+ const appShell = /* @__PURE__ */ jsx(AppShell, {
75
97
  sidebarMode: sidebarMode ?? "text-small",
76
98
  activeItem: route.resource ?? "",
77
99
  breadcrumb,
@@ -79,7 +101,7 @@ const Routes = ({ workspace, sidebarMode, assistant }) => {
79
101
  onSignOut: () => void signOut(),
80
102
  workspace,
81
103
  navGroups: groups,
82
- userMenu: userMenuFromMeta(meta, basePath, route.resource),
104
+ userMenu: [...userMenuFromMeta(meta, basePath, route.resource), ...accountMenuItems(basePath, path, admin)],
83
105
  onNavigate: open,
84
106
  search: search.available ? {
85
107
  placeholder: search.placeholder,
@@ -90,18 +112,27 @@ const Routes = ({ workspace, sidebarMode, assistant }) => {
90
112
  groups: search.groups,
91
113
  onSelect: open
92
114
  } : void 0,
93
- actions: (Actions || assistantButton) && /* @__PURE__ */ jsxs(Fragment$1, { children: [Actions && /* @__PURE__ */ jsx(Actions, {}), assistantButton] }),
115
+ actions: (Actions || runtimeButton || assistantButton) && /* @__PURE__ */ jsxs(Fragment$1, { children: [
116
+ Actions && /* @__PURE__ */ jsx(Actions, {}),
117
+ runtimeButton,
118
+ assistantButton
119
+ ] }),
94
120
  rightPanel: (RightPanel || assistantPanel) && /* @__PURE__ */ jsxs(Fragment$1, { children: [RightPanel && /* @__PURE__ */ jsx(RightPanel, {}), assistantPanel] }),
95
- children: !route.resource ? /* @__PURE__ */ jsx(Notice, {
96
- title: "Choose a resource",
97
- children: "Pick one from the sidebar."
98
- }) : page ? /* @__PURE__ */ jsx(ComposedPage, { name: page.name }, page.name) : route.key === "new" ? /* @__PURE__ */ jsx(CreatePage, { resource: route.resource }, `${route.resource}/new`) : route.key ? /* @__PURE__ */ jsx(RecordPage, {
99
- resource: route.resource,
100
- recordKey: route.key
101
- }, `${route.resource}/${route.key}`) : /* @__PURE__ */ jsx(ListPage, { resource: route.resource }, route.resource)
121
+ children: content
122
+ });
123
+ if (!staff) return appShell;
124
+ return /* @__PURE__ */ jsxs("div", {
125
+ className: "flex h-full min-h-0 flex-col",
126
+ children: [/* @__PURE__ */ jsx(StaffBanner, {
127
+ signIn: staff,
128
+ onStop: stopStaffSession
129
+ }), /* @__PURE__ */ jsx("div", {
130
+ className: "min-h-0 flex-1",
131
+ children: appShell
132
+ })]
102
133
  });
103
134
  };
104
- const Authenticated = ({ baseUrl, client, basePath, initialUrl, workspace, sidebarMode, ui, assistant }) => {
135
+ const Authenticated = ({ baseUrl, client, basePath, initialUrl, workspace, sidebarMode, ui, assistant, runtime }) => {
105
136
  const { session, markSignedOut } = useAuth();
106
137
  return /* @__PURE__ */ jsx(ApiProvider, {
107
138
  baseUrl,
@@ -116,7 +147,8 @@ const Authenticated = ({ baseUrl, client, basePath, initialUrl, workspace, sideb
116
147
  children: /* @__PURE__ */ jsx(Routes, {
117
148
  workspace,
118
149
  sidebarMode,
119
- assistant
150
+ assistant,
151
+ runtime
120
152
  })
121
153
  }) }) })
122
154
  })
@@ -2,8 +2,8 @@ import { Button } from "../primitives/button.js";
2
2
  import { AssistantDock } from "../assistant-dock/assistant-dock.js";
3
3
  import { useAssistant } from "../assistant-dock/use-assistant.js";
4
4
  import { useAssistantClient } from "../assistant-dock/use-assistant-client.js";
5
- import { jsx, jsxs } from "react/jsx-runtime";
6
5
  import { Sparkles } from "lucide-react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
7
  //#region src/app/assistant-shell.tsx
8
8
  /** Below `md`, where search is a button too, only the icon shows; the hidden text still names it. */
9
9
  const AssistantButton = ({ open, onToggle }) => /* @__PURE__ */ jsxs(Button, {
@@ -9,9 +9,9 @@ import { useLiveSave } from "../use-live-save.js";
9
9
  import { CreateRecordForm } from "./create-record-form.js";
10
10
  import { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isRequired } from "./create-values.js";
11
11
  import { useState } from "react";
12
+ import { isApiError } from "@protobase/client";
12
13
  import { jsx, jsxs } from "react/jsx-runtime";
13
14
  import { useQueryClient } from "@tanstack/react-query";
14
- import { isApiError } from "@protobase/client";
15
15
  import { encodeKey } from "@protobase/schema";
16
16
  //#region src/app/create/create-page.tsx
17
17
  const without = (source, name) => Object.fromEntries(Object.entries(source).filter(([key]) => key !== name));
@@ -4,9 +4,9 @@ import { useToast } from "../../toasts/toast-provider.js";
4
4
  import { BulkDeleteDialog, ConfirmDeleteDialog, ConflictDialog } from "./dialogs.js";
5
5
  import { diffRecords } from "./record-diff.js";
6
6
  import { useState } from "react";
7
+ import { PreconditionFailedError, isApiError } from "@protobase/client";
7
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
9
  import { useQueryClient } from "@tanstack/react-query";
9
- import { PreconditionFailedError, isApiError } from "@protobase/client";
10
10
  //#region src/app/delete/use-delete-flow.tsx
11
11
  const concurrency = 4;
12
12
  /**
@@ -1,6 +1,6 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
1
  import { TriangleAlert } from "lucide-react";
3
2
  import { isApiError } from "@protobase/client";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/app/error-banner.tsx
5
5
  const describe = (error) => {
6
6
  if (!isApiError(error)) return [{ message: error instanceof Error ? error.message : "Something went wrong" }];
@@ -25,8 +25,8 @@ import { ListChart } from "./list-chart.js";
25
25
  import { defaultOrderBy, parseSort, sortToOrderBy } from "./list-order.js";
26
26
  import { ListSearch } from "./list-search.js";
27
27
  import { useMemo, useState } from "react";
28
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
29
28
  import { Plus, SlidersHorizontal } from "lucide-react";
29
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
30
30
  import { createWhere, encodeKey } from "@protobase/schema";
31
31
  //#region src/app/list-page.tsx
32
32
  const where = createWhere();
@@ -1,7 +1,7 @@
1
1
  import { Input } from "../primitives/input.js";
2
2
  import { useEffect, useState } from "react";
3
- import { jsx } from "react/jsx-runtime";
4
3
  import { Search } from "lucide-react";
4
+ import { jsx } from "react/jsx-runtime";
5
5
  //#region src/app/list-search.tsx
6
6
  /** A search box that commits after a short pause or on Enter. */
7
7
  const ListSearch = ({ value, onCommit, placeholder }) => {
@@ -2,8 +2,8 @@ import { Spinner } from "../primitives/spinner.js";
2
2
  import { allowEverything } from "../data/meta-data.js";
3
3
  import { useMeta } from "../data/use-meta.js";
4
4
  import { createContext, useContext } from "react";
5
- import { jsx, jsxs } from "react/jsx-runtime";
6
5
  import { isApiError } from "@protobase/client";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
7
  //#region src/app/meta-gate.tsx
8
8
  const MetaContext = createContext(null);
9
9
  const useAdminMeta = () => {
@@ -1,8 +1,8 @@
1
- import { useClient } from "../../../data/api-provider.js";
2
1
  import { Button } from "../../../primitives/button.js";
2
+ import { Dialog } from "../../../primitives/dialog.js";
3
+ import { useClient } from "../../../data/api-provider.js";
3
4
  import { humanize } from "../../../live/naming.js";
4
5
  import { useToast } from "../../../toasts/toast-provider.js";
5
- import { Dialog } from "../../../primitives/dialog.js";
6
6
  import { differs, toDraft, toPatchValue } from "../../../live/field-values.js";
7
7
  import { ErrorBanner } from "../../error-banner.js";
8
8
  import { FieldEditor } from "../../field-editor.js";
@@ -14,8 +14,8 @@ import { useLayoutRecord } from "../record-scope.js";
14
14
  import { useFirstRecord, useRefreshResource } from "../use-layout-data.js";
15
15
  import { propsOf } from "./block-props.js";
16
16
  import { useState } from "react";
17
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
18
17
  import { isApiError } from "@protobase/client";
18
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
19
19
  //#region src/app/pages/blocks/modal-form.tsx
20
20
  /** The literal defaults of new records, or the record's values when editing; relations as `{ key, label }` once chosen. */
21
21
  const startingDraft = (model, fields, record) => Object.fromEntries(fields.map((name) => {
@@ -6,8 +6,8 @@ import { scopeOf } from "./use-layout-data.js";
6
6
  import { FieldValue } from "./field-value.js";
7
7
  import { usePathValue } from "./use-path-value.js";
8
8
  import { useRelationLabels } from "./use-relation-labels.js";
9
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
9
  import { ChevronLeft, ChevronRight } from "lucide-react";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
11
  import { resolveFieldPath } from "@protobase/layout";
12
12
  //#region src/app/pages/record-table.tsx
13
13
  const isPath = (column) => column.includes(".");
@@ -1,13 +1,13 @@
1
1
  import { Badge } from "../primitives/badge.js";
2
- import { keys } from "../data/query-keys.js";
3
- import { useClient } from "../data/api-provider.js";
4
2
  import { useAuth } from "../auth/auth-provider.js";
3
+ import { formatRelativeTime } from "../format/date.js";
5
4
  import { Button } from "../primitives/button.js";
5
+ import { keys } from "../data/query-keys.js";
6
+ import { useClient } from "../data/api-provider.js";
6
7
  import { useRecord } from "../data/use-record.js";
7
8
  import { recordTitle } from "../live/model-helpers.js";
8
9
  import { humanize } from "../live/naming.js";
9
10
  import { useNavigationGuard, useRouter } from "./router.js";
10
- import { formatRelativeTime } from "../format/date.js";
11
11
  import { displayValue } from "../live/display-value.js";
12
12
  import { ErrorBanner } from "./error-banner.js";
13
13
  import { FieldEditor } from "./field-editor.js";
@@ -29,10 +29,10 @@ import { useRecordEditor } from "./use-record-editor.js";
29
29
  import { useRelatedRecords } from "./use-related-records.js";
30
30
  import { RelatedSection } from "./related-section.js";
31
31
  import { useMemo } from "react";
32
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
33
32
  import { Check } from "lucide-react";
34
- import { useQueryClient } from "@tanstack/react-query";
35
33
  import { isApiError } from "@protobase/client";
34
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
35
+ import { useQueryClient } from "@tanstack/react-query";
36
36
  import { enumLabel } from "@protobase/schema";
37
37
  //#region src/app/record-page.tsx
38
38
  const hidden = (model, name) => name === model.tenant || model.primaryKey.includes(name);